| "C4 E4 G4" | Space-separated — equally spaced across one measure |
| "~ . x" | Rest — ~ or . are silences; x = generic trigger |
| "C4 [E4 G4]" | [ ] groups share one time slot — E4 G4 play twice as fast |
| "<C4 G3> E4" | < > alternates each cycle — C4 on cycle 0, G3 on cycle 1 |
| "C4*3 G4" | *N repeats N times inside that slot |
| pat(str, synth) | Create pattern from mini-notation + synth or callback fn |
| .speed(n) | Play n× faster (e.g. .speed(2) = double time) |
| .slow(n) | Play n× slower |
| .euclid(k, n) | Distribute k hits across n equal steps (Euclidean rhythm) |
| .every(n, fn) | Apply fn(events, cycle) every n cycles; return modified events |
| .start() / .stop() | Start/stop the pattern loop |
| stack(p1, p2, …) | Layer patterns; call .bpm(120).start() on result |
| pat(str, (note, time, dur) => {}) | Callback form — full control; route to multiple synths |
| audio.synth(opts) | Basic oscillator |
| audio.poly(opts) | Polyphonic — play chord arrays |
| audio.fm(opts) | FM synthesis — metallic/electric tones |
| audio.am(opts) | AM synthesis |
| audio.pluck(opts) | Karplus-Strong plucked string |
| audio.kick(opts) | Membrane synth — kick / tom drums |
| audio.metal(opts) | Metallic — hi-hats, cymbals (no note arg) |
| audio.noise(opts) | White noise (no note arg) |
| s.play(note, dur, time?) | Trigger synth — dur: '8n', '4n', '1n', or seconds |
| s.chain(fx1, fx2) | Route synth through effects in series to output |
| s.connect(fx) | Route synth to single effect |
| audio.reverb(decay) | Reverb — decay in seconds |
| audio.delay(time, feedback) | Feedback delay — time in sec, feedback 0–1 |
| audio.distort(amount) | Distortion 0–1 |
| audio.chorus(freq, delay, depth) | Chorus effect |
| audio.filter(type, freq, Q) | Filter — types: lowpass highpass bandpass notch |
| audio.autoFilter(rate) | LFO-swept filter — rate in Hz |
| audio.vibrato(freq, depth) | Pitch wobble |
| audio.tremolo(freq, depth) | Volume wobble |
| audio.phaser(rate, octaves) | Phase shifting |
| audio.wah(baseFreq) | Auto-wah |
| audio.pitchShift(semitones) | Pitch shift up/down |
| audio.compressor(threshold, ratio) | Dynamic range control |
| audio.eq(low, mid, high) | 3-band EQ in dB |
| audio.scale('C4', 'minor') | Returns note array for scale — names: major minor dorian phrygian lydian mixolydian pentatonic blues chromatic |
| audio.note(scaleArr, degree) | Pick scale note by degree index (wraps around) |
| audio.lfo(freq, min, max) | LFO signal — connect to synth param: lfo.connect(s._.frequency) |
| audio.meter() | RMS amplitude meter — use s.chain(m); then m.getValue() each frame |
| audio.analyser(bins) | FFT analyser — use s.chain(a); then a.getValue() → Float32Array |
| audio.bpm(120) | Set transport tempo |
| audio.volume(-6) | Master output volume in dB |
Any audio source — synth, pattern, mic, effect chain — can be treated as a live signal that drives shaders, draw calls, window properties, or anything else. Three abstractions, one idea: audio is a data bus, not just sound.
| audio.signal(source) | Live {value, bass, mid, high, fft} object — read any getter each frame, no setup loop needed |
| audio.fftCanvas(source, bins) | Live bins×1 canvas, R channel = FFT magnitude 0–1 — feed directly into any Shader as video: |
| shader.bind(source) | Auto-fills custom = [rms, bass, mid, high] every frame — chainable with .start() |
source accepts: any Tone instrument/node, Tone.Analyser, Web Audio AnalyserNode, or 'mic' (harness mic — enable with toolbar toggle).
The inverse of the audio signal bus — sample any canvas or camera feed as a live signal to drive audio params, effects, or anything numeric. Camera, viz canvas, layer canvas — all valid sources.
| video.signal(source, opts) | Live {brightness, r, g, b, motion, hue} — all lazy getters, sampled at fps (default 30) |
| sig.brightness | Luminance 0–1 of sampled region (perceptual: 0.299r + 0.587g + 0.114b) |
| sig.motion | Frame-delta RMS 0–1 — how much changed since last sample. Cheap motion detection, no Vision API needed |
| sig.hue | Dominant hue 0–360 — map to scale degree, filter cutoff, palette index |
| sig.r / .g / .b | Raw color channels 0–1 — drive independent synth voices, effect amounts |
x, y = normalized center of sample region (0–1), radius = region size (0–1), fps = sample rate.'camera' (toolbar camera), any HTMLCanvasElement (getCanvas(0), viz.canvas, mirror canvas).video.signal() object as source to onMotion/onBrightness to share one sampling loop across multiple triggers.
| video.onMotion(src, thr, onEnter, onExit?) | Edge-triggered when motion crosses threshold — fires once on enter, once on exit |
| video.onBrightness(src, thr, onEnter, onExit?) | Edge-triggered on brightness threshold — like audio.onLevel but for camera/canvas |
Every sensor returns a signal object: live getters you can read any frame + .stream(fn) RAF push + edge triggers. All streams auto-cancel on reset.
| API | Signal fields | Triggers |
|---|---|---|
sensors.mouse() | x, y, vx, vy, speed, left, right, middle | .onMove(thresh, fn) .onButton(btn, fn) |
sensors.keyboard() | held (Set), last, is(key), any(...keys) | .onKey(key, onDown, onUp?) |
sensors.gamepad(i) | connected, axis(i), button(i), pressed(i) | .onButton(i, fn) .onAxis(i, thresh, fn) |
sensors.motion() | ax/ay/az (m/s²), gx/gy/gz (°/s), alpha/beta/gamma (°) | .onShake(thresh, fn) .onTilt(axis, thresh, fn) |
sensors.geo() | lat, lon, altitude, accuracy, speed, heading | .stream(fn) (fires once ready) |
sensors.network() | online, type, downlink (Mbps), rtt (ms) | .onChange(fn) |
await sensors.battery() | level (0–1), charging, timeToEmpty (s) | .onChange(fn) |
Write shaders as plain JavaScript — no WGSL, no GPU knowledge required. Pass an arrow function to new Shader(fn) and the IDE compiles it to WebGPU automatically.
| Param | Type | Description |
|---|---|---|
uv | vec2 | Normalised pixel coord 0–1 (x right, y down) |
time | f32 | Seconds since shader started |
col | vec4 | Video/camera pixel at current uv — needs { video: src } opt |
custom | vec4 | x/y/z/w — filled by .bind(audio.signal(...)) or .set([…]) |
res | vec2 | Canvas resolution in pixels |
mouse | vec2 | Mouse position normalised 0–1 |
return [r, g, b, a] — use an array literal; automatically becomes vec4f.
Math.* → all map to WGSL equivalents: Math.sin, Math.abs, Math.pow, Math.floor, etc.
vec2(x,y) / vec3(r,g,b) / vec4(r,g,b,a) — vector constructors available globally.
WGSL built-ins available as-is: mix, clamp, smoothstep, step, fract, length, normalize, dot, cross, reflect.
Ternary cond ? a : b — works; compiles to WGSL select(b, a, cond).
if / else, for, while — all supported.
Helper functions — declare inside the arrow fn body; hoisted to WGSL. Type params with default values: r = 0.0 → f32, center = vec2(0,0) → vec2f.
Beat as state machine: Each pat() callback fires at precise musical times — use it to advance a CA, flip a palette, emit a particle burst. Audio is the clock, not the picture.
Amplitude as physics force: sig.value or sig.bass → feed to gravity, repulsion, blur strength, shader uniform. Read any frame with no setup.
Note value as index: The note string in a callback can select palette index, drawing mode, CA rule — anything discrete.
Shared parameter: The same variable that drives synth detune can drive visual distortion — change one number, both domains shift.
Press ? or Esc to close