#!/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 midiTime = 0 previousNow = 0 spinAngle = 0 input.on 'message', (deltaTime, message) -> # Keep time midiTime += deltaTime 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: midiTime # 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] * 3.0 / 127 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 = 0.001 * new Date().getTime() timeStep = now - previousNow previousNow = now # Global spin update spinAngle = (spinAngle + timeStep * spinRate) % (Math.PI * 2) # Launch new particles for all active notes for key, note of particleNotes particles.push life: 1 note: note timestamp: note.timestamp # Update appearance of all particles for p in particles # Angle: Global spin, thne positional mapping to key theta = spinAngle + (Math.PI / 5) * p.note.key # Positioned in polar coordinates, on unit circle x = Math.cos theta y = Math.sin theta # Add influence of LFOs for key, note of lfoNotes # Down several octaves, to useful LFO frequencies transpose = -12 * 5 # Midi note to frequency hz = 440 * Math.pow 2, (note.key - 69 + transpose) / 12 # Wobble amplitude driven by LFO wobbleAmp = Math.sin Math.PI * 2 * now * hz # Wobble angle driven by LFO note and particle life wobbleAngle = 10.0 * p.life + (Math.PI / 5) * p.note.key x += wobbleAmp * Math.cos wobbleAngle y += wobbleAmp * Math.sin wobbleAngle # Radius: Particles spawn in center, fly outward radius = 3.0 * (1 - p.life) x *= radius y *= radius # Use the XZ plane p.point = [x, 0, y] # One rainbow per octave hue = (p.note.key - 60) / 12.0 p.color = OPC.hsv hue, 0.5, 0.8 # Intensity mapped to velocity, nonlinear p.intensity = Math.pow(p.note.velocity / 100, 5.0) * brightness # Falloff gets sharper as the note gets higher p.falloff = 20 + (p.note.key - 60) * 20 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