patternsjs
v0.1.0
Published
SuperCollider patterns library for JavaScript — MIDI and OSC output with sub-millisecond timing
Maintainers
Readme
PatternsJS
A TypeScript port of SuperCollider's pattern library for Node.js. Outputs MIDI and OSC with sub-millisecond timing precision. Zero core dependencies.
Install
npm install patternsjsFor MIDI or OSC output, install the optional peer dependencies you need:
npm install @julusian/midi # MIDI output
npm install node-osc # OSC output (generic or SuperCollider)Quick Start
import {
Pbind, Pseq, Prand, Pwhite, Scale,
Clock, Player, TimingEngine, MidiBackend,
} from 'patternsjs';
// Create a clock at 140 BPM
const clock = new Clock();
clock.bpm = 140;
// Define a pattern
const pattern = new Pbind({
degree: new Pseq([0, 2, 4, 5, 7], Infinity),
dur: new Prand([0.25, 0.5, 0.125], Infinity, { seed: 42 }),
scale: Scale.dorian,
octave: 5,
amp: new Pwhite(0.3, 0.7),
});
// Play through MIDI
const player = new Player(pattern, {
clock,
backend: new MidiBackend({ port: 0 }),
});
// Start the timing engine and play
const engine = new TimingEngine({ clock });
engine.start();
await player.play();Core Concepts
PatternsJS maps SuperCollider's pattern system directly to JavaScript:
| SuperCollider | PatternsJS | How |
|---|---|---|
| Pattern.asStream | pattern.stream() | Returns a JS Generator |
| Stream.next(inval) | generator.next(inval) | Native iterator protocol |
| embedInStream | yield* | Free generator delegation |
| Event | Plain object + resolveEvent() | No Proxy overhead |
| TempoClock | Clock | Nanosecond precision via bigint |
| EventStreamPlayer | Player | With event emitter (beat, end, error) |
Pbind
The primary way to create event streams. Uses object form:
const p = new Pbind({
degree: new Pseq([0, 1, 2, 3, 4, 5, 6, 7]),
dur: new Pseq([0.25, 0.25, 0.5]),
scale: Scale.minor,
octave: 5,
amp: 0.5,
});Keys follow SC's event model. The pitch chain resolves automatically:
degree → note → midinote → freq → detunedFreqEnter at any level — provide freq directly to skip the chain, or use degree with a scale for musical composition.
Pattern Library
Sequencing: Pseq, Pser, Prand, Pxrand, Pshuf, Place, Ppatlace, Ptuple, Pwalk
Random: Pwhite, Pbrown, Pgauss, Pexprand — all support { seed } for reproducibility
Series: Pseries, Pgeom
Repeat: Pn, Pstutter, Pdup
Functional: Pfunc, Prout, Plazy
Filter: Pcollect, Pselect, Preject, Pfin, Pfinval, Pfindur
Control: Pif, Pswitch, Pswitch1
Event: Pbind, Pchain, Pkey, Pdef, Pmono, PmonoArtic
Parallel: Ppar, Ptpar
Seeded Randomness
All random patterns accept an optional { seed } for deterministic sequences. Each .stream() call from a seeded pattern produces identical output:
const p = new Prand([1, 2, 3, 4], 10, { seed: 42 });
const a = collectAll(p.stream()); // [3, 1, 4, 2, ...]
const b = collectAll(p.stream()); // identical to aSet a global seed to make all randomness deterministic:
import { setGlobalSeed } from 'patternsjs';
setGlobalSeed(42);Scales and Tunings
60+ predefined scales and 12 tuning systems, all accessible as static properties or by name:
import { Scale, Tuning } from 'patternsjs';
Scale.major // [0, 2, 4, 5, 7, 9, 11]
Scale.minorPentatonic // [0, 3, 5, 7, 10]
Scale.dorian // [0, 2, 3, 5, 7, 9, 10]
Scale.at('bhairav') // lookup by name
Scale.directory() // list all 60+ scale names
Tuning.et12 // equal temperament 12-tone
Tuning.just // 5-limit just intonation
Tuning.pythagorean // Pythagorean tuning
Tuning.bp // Bohlen-Pierce (tritave-based)
// Custom scale with just intonation tuning
const myScale = new Scale([0, 2, 4, 5, 7, 9, 11], 12, Tuning.just, 'Just Major');Event Resolution
resolveEvent() walks the full SC-compatible key hierarchy:
import { resolveEvent, Scale } from 'patternsjs';
const e = resolveEvent({
degree: 2,
scale: Scale.minor,
octave: 4,
});
e.midinote // 51
e.freq // 233.08 Hz
e.amp // 0.1 (from default db: -20)
e.velocity // 13
e.sustain // 0.8 (dur * legato)
e.delta // 1.0Timing Architecture
PatternsJS uses a lookahead scheduling architecture for precise timing:
- A worker thread wakes the main thread periodically (~10ms ticks)
- The main thread processes all events due within a lookahead window (default 50ms)
- Each event gets an exact nanosecond timestamp via
Clock.beatToTime() - Backends receive the timestamp and use it for dispatch:
- OSC/SC: Wraps in OSC bundles with NTP timetags — scsynth fires sample-accurately
- MIDI (precision mode): Dedicated dispatch worker with spin-wait for sub-ms timing
- MIDI (direct mode): Immediate send, bounded by tick interval jitter
const engine = new TimingEngine({
clock,
lookaheadMs: 50, // process events 50ms ahead
tickIntervalMs: 10, // worker wakes every 10ms
});
engine.start();Output Backends
MIDI
import { MidiBackend } from 'patternsjs';
// List available ports
const ports = await MidiBackend.listPorts();
// Open by index
const midi = new MidiBackend({ port: 0 });
// Open by name substring
const midi2 = new MidiBackend({ port: 'IAC Driver' });
// Virtual port (Mac/Linux)
const midi3 = new MidiBackend({ virtual: true, virtualName: 'PatternsJS' });
// Sub-millisecond precision mode (worker-thread dispatch)
const midi4 = new MidiBackend({ port: 0, precisionMode: true });OSC (Generic)
import { OscBackend } from 'patternsjs';
const osc = new OscBackend({
host: '127.0.0.1',
port: 9000,
address: '/synth',
});SuperCollider
import { ScBackend } from 'patternsjs';
const sc = new ScBackend({
host: '127.0.0.1',
port: 57110, // scsynth default
});
// Events are translated to SC server commands:
// type: 'note' → /s_new
// type: 'set' → /n_set
// type: 'kill' → /n_free
// type: 'group' → /g_new
// All wrapped in OSC bundles with NTP timetags for sample-accurate scheduling.Multiple Backends
import { MultiBackend, MidiBackend, ScBackend } from 'patternsjs';
const multi = new MultiBackend([
new MidiBackend({ port: 0 }),
new ScBackend(),
]);
// Error-isolated: one backend failing does not block others.Player
const player = new Player(pattern, { clock, backend: midi });
await player.play(); // start (quantized to next beat)
await player.play([4, 0]); // start at next bar (4-beat quantization)
player.mute(); // silence output, timing continues
player.unmute();
player.setPattern(newPat); // hot-swap the source pattern
await player.stop(); // stop and cancel all pending note-offs
// Event emitter
player.on('beat', (event, beat) => {
console.log(`Beat ${beat}: midinote ${event.midinote}`);
});
player.on('end', () => console.log('Pattern finished'));
player.on('error', (err) => console.error(err));Pdef — Named Patterns
import { Pdef, PdefContext } from 'patternsjs';
// Global registry
new Pdef('bass', new Pbind({ degree: new Pseq([0, 3, 5], Infinity), dur: 0.5 }));
new Pdef('bass').source; // retrieve
// Isolated context (multiple compositions)
const ctx = new PdefContext();
new Pdef('bass', myPattern, ctx); // scoped to ctxParallel Patterns
import { Ppar, Ptpar } from 'patternsjs';
// All patterns start simultaneously
const p = new Ppar([melody, bass, drums]);
// Staggered starts (beat offsets)
const p2 = new Ptpar([
0, melody,
4, bass, // starts at beat 4
0, drums,
]);Pure Pattern Use (No Audio)
The pattern engine works standalone — no MIDI/OSC required:
import { Pbind, Pseq, collectAll, resolveEvent } from 'patternsjs';
const pattern = new Pbind({
degree: new Pseq([0, 2, 4, 5, 7], 1),
dur: 0.25,
});
// Collect raw events
const events = collectAll(pattern.stream());
// Resolve through the key hierarchy
const resolved = events.map(resolveEvent);
resolved.forEach(e => {
console.log(`note: ${e.midinote}, freq: ${e.freq.toFixed(1)}, dur: ${e.dur}`);
});Performance
Benchmarked on a standard development machine:
| Metric | Value | |---|---| | Pbind stream throughput | 2-4M events/sec | | resolveEvent (full hierarchy) | 360-580K events/sec | | Clock: 50K callbacks | 24ms | | Ppar: 100 parallel streams | Zero event loss | | beatToTime precision | 1.0e-9 beats (sub-nanosecond) |
Requirements
- Node.js >= 18.0.0
- TypeScript 5.4+ (for development)
Author
Darien Brito — darienbrito.com — GitHub
