Newer
Older
Micah Elizabeth Scott
committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/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
input.openPort 0
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
noteSustain = 1.6
wobbleAmount = 24.0
numPhysicsTimesteps = 20
frameDelay = 5
timestepSize = 0.010
gain = 0.1
Micah Elizabeth Scott
committed
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
midiToAngle = (key) -> (2 * Math.PI / 24) * key
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
if key >= 60
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
# Global spin update
spinAngle = (spinAngle + timestepSize * spinRate) % (Math.PI * 2)
Micah Elizabeth Scott
committed
# Launch new particles for all active notes
for key, note of particleNotes
particles.push
life: 1
note: note
point: origin.slice 0
velocity: [0, 0, 0]
Micah Elizabeth Scott
committed
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
y = radius * Math.sin theta
Micah Elizabeth Scott
committed
# One rainbow per octave
hue = (p.note.key - 60 + 0.1) / 12.0
Micah Elizabeth Scott
committed
# Intensity mapped to velocity, nonlinear
p.intensity = Math.pow(p.note.velocity / 100, 2.0) * 0.2 * brightness
Micah Elizabeth Scott
committed
# Fade with age
noteAge = now - p.note.timestamp
p.intensity *= Math.max(0, 1 - (noteAge / noteSustain))
# Falloff gets sharper as the note gets higher
p.falloff = 15 * Math.pow(2, (p.note.key - 60) / 6)
Micah Elizabeth Scott
committed
# Add influence of LFOs
for key, note of lfoNotes
lfoAge = 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, 2.0) * wobbleAmount
Micah Elizabeth Scott
committed
# Scale based on particle fuzziness
Micah Elizabeth Scott
committed
# Fade over time
wobbleAmp /= 1 + lfoAge
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
p.velocity[0] += (x + origin[0] - p.point[0]) * (gain / numPhysicsTimesteps)
p.velocity[2] += (y + origin[2] - p.point[2]) * (gain / numPhysicsTimesteps)
for i in [1 .. numPhysicsTimesteps]
p.point[0] += p.velocity[0]
p.point[1] += p.velocity[1]
p.point[2] += p.velocity[2]
Micah Elizabeth Scott
committed
p.life -= timestepSize / particleLifetime
Micah Elizabeth Scott
committed
# Filter out dead particles
particles = particles.filter (p) -> p.life > 0
# Render particles to the LEDs
client.mapParticles particles, model
setInterval draw, frameDelay