Newer
Older
Micah Elizabeth Scott
committed
#!/usr/bin/env coffee
#
# Particle system, playable with a MIDI keyboard!
#
# Dependencies:
# npm install midi coffee
#
# Assumes the default MIDI input port (0).
# Specify a layout file on the command line, or use the default (grid32x16z)
#
# Controls:
# (Tested with Alesis Q49 keyboard controller)
# Left hand -> Low frequency oscillators (wobble)
# Right hand -> Particles, shifting in color and pointiness
#
# 2014, Micah Elizabeth Scott & Keroserene
#
# Default MIDI input
midi = require 'midi'
input = new midi.input
Micah Elizabeth Scott
committed
input.ignoreTypes false, false, false
# Default OPC output
OPC = new require './opc'
model = OPC.loadModel process.argv[2] || '../layouts/grid32x16z.json'
client = new OPC 'localhost', 7890
# Live particles
particles = []
# Notes for low frequency oscillators
lfoNotes = {}
# Notes for particles
particleNotes = {}
# Adjustable parameters
particleLifetime = 1.0
brightness = 1.0
spinRate = 1.0
previousNow = 0
spinAngle = 0
# Time clock in seconds
clock = () -> 0.001 * new Date().getTime()
# Midi to frequency
midiToHz = (key) -> 440 * Math.pow 2, (key - 69) / 12
Micah Elizabeth Scott
committed
# Midi note to angle, one rev per octave
# Boundary between the left-hand and right-hand patterns.
LIMINAL_KEY = 40
SMOOTH_FACTOR = 0.1
input.on 'message', (deltaTime, message) ->
console.log message
Micah Elizabeth Scott
committed
switch message[0]
when 0x80 # Voice 0, note off
key = message[1]
delete lfoNotes[key]
delete particleNotes[key]
when 0x90 # Voice 0, note on
key = message[1]
info =
key: key
velocity: message[2]
timestamp: clock()
Micah Elizabeth Scott
committed
# Split keyboard into particles and LFOs
Micah Elizabeth Scott
committed
particleNotes[key] = info
else
lfoNotes[key] = info
when 0xb0 # Voice 0, Control Change
switch message[1]
when 7 # "Data entry" slider, brightness
brightness = message[2] * 2.0 / 127
Micah Elizabeth Scott
committed
when 1 # "Modulation" slider, particle speed
particleLifetime = 0.1 + message[2] * 2.0 / 127
when 0xe0 # Voice 0, Pitch Bend
# Default spin 1.0, but allow forward/backward
spinRate = 1.0 + (message[2] - 64) * 20.0 / 64
draw = () ->
# Time delta calculations
now = clock()
Micah Elizabeth Scott
committed
timeStep = now - previousNow
previousNow = now
# Global spin update
spinAngle = (spinAngle + timeStep * spinRate) % (Math.PI * 2)
# Experiment - moves the origin in a circle
theta = previousNow * 1.5
initX = 0.5 * Math.cos theta
initY = 0.5 * Math.sin theta
Micah Elizabeth Scott
committed
# Launch new particles for all active notes
for key, note of particleNotes
particles.push
life: 1
Micah Elizabeth Scott
committed
note: note
timestamp: note.timestamp
# Update appearance of all particles
for p in particles
# Angle: Global spin, thne positional mapping to key
theta = midiToAngle p.note.key
Micah Elizabeth Scott
committed
# Radius: Particles spawn in center, fly outward
radius = 3.0 * (1 - p.life)
Micah Elizabeth Scott
committed
# Positioned in polar coordinates
x = radius * Math.cos(theta) + initX
y = radius * Math.sin(theta) + initY
Micah Elizabeth Scott
committed
# One rainbow per octave
p.color = OPC.hsv hue, 0.3, 0.8
Micah Elizabeth Scott
committed
# Intensity mapped to velocity, nonlinear
p.intensity = Math.pow(p.note.velocity / 100, 3.0) * 0.25 * brightness
Micah Elizabeth Scott
committed
# Falloff gets sharper as the note gets higher
p.falloff = 20 + (p.note.key - LIMINAL_KEY) * 20
Micah Elizabeth Scott
committed
# Add influence of LFOs
for key, note of lfoNotes
age = now - note.timestamp
hz = midiToHz key
lfoAngle = midiToAngle key
Micah Elizabeth Scott
committed
# Amplitude starts with left hand velocity
wobbleAmp = Math.pow(note.velocity / 100, 3.0)
Micah Elizabeth Scott
committed
# Scale based on particle fuzziness
wobbleAmp *= 100.0 / p.falloff
Micah Elizabeth Scott
committed
# Fade over time
wobbleAmp /= 1 + age
Micah Elizabeth Scott
committed
# Wobble
wobbleAmp *= Math.sin(p.life * Math.pow(3, (p.note.key - 35) / 12.0))
Micah Elizabeth Scott
committed
# Wobble angle driven by LFO note and particle life
x += wobbleAmp * Math.cos lfoAngle
y += wobbleAmp * Math.sin lfoAngle
Micah Elizabeth Scott
committed
# Use the XZ plane
[oldx, _, oldy] = p.point
p.point = [oldx + (oldx - x)*SMOOTH_FACTOR,
0,
oldy + (oldy - y)*SMOOTH_FACTOR]
# p.point += ([x, 0, y] - p.point) * SMOOTH_FACTOR
# p.point = [x, 0, y]
Micah Elizabeth Scott
committed
p.life -= timeStep / particleLifetime
# Filter out dead particles
particles = particles.filter (p) -> p.life > 0
# Render particles to the LEDs
client.mapParticles particles, model
setInterval draw, 10