music.* family (lang/modules/music/) is a set of composition libraries built on one shared core: four dialects for generating music, each drawing on a different tradition of computer-music system design, all producing the same List<Event> so they interoperate freely and share one player. Every layer supports any real-numbered division of the octave (or of any period — equal temperaments with fractional step counts, tritave-based tunings, just-intonation and Scala-style tables) and any real-numbered division of time (time is Float beats throughout).
Everything except music.play is pure and runs under plain tzpl (which is how the golden tests work); music.play talks to the audio engine and needs the app (tzpl_app). Each dialect module re-exports music.core, so a single import music.pat.*; (or .media, .job, .spans, .play) is enough. For a task-oriented walkthrough see the Music Cookbook, chapter 11.
The shared substrate, re-exported by every dialect: the event and pitch types (music.event), tunings and scales (music.tuning), and a few random helpers (music.rand). Pure and RT-safe.
An Event is one note (or rest) with an absolute onset in beats. Pitch stays symbolic — a Pitch value is only resolved to Hz at play time, through a Tuning and Scale, so the same events can be performed in 12ED2, a tritave tuning, or a just-intonation table. Note value and sounding time are distinct fields: dur is the time the note occupies in its line (it drives retrograde, stretching, and spacing); sustain is the time until release (it drives noteOff).
| Type / Function | Description |
|---|---|
Pitch | Enum: hz(Float), step(Float) (real-valued tuning step; in et12 a step is a MIDI note number), degree(Int, Float) (whole scale degree + accidental in tuning steps, root-relative), ratio(Float), cents(Float) (both root-relative), rest. |
degree(d, acc = 0.0) | Canonical degree constructor: degree(4) is scale degree 4, degree(4, 1.0) the same degree sharpened by one tuning step. The accidental is a per-note inflection; to transpose a whole performance chromatically, re-anchor the tuning with transposeRoot instead. |
Event | Struct: t, dur, sustain, amp, pitch, and params [(Symbol, Float)] — named per-note synth controls. |
event(t, dur, pitch, amp = 0.5, sustain = 0.0, params = []) | Constructor; sustain defaults to 0.8 * dur when not given. |
isRest(e) | True when the pitch is Pitch.rest. |
offset(es, dt) / stretch(es, k) / gain(es, k) | Shift onsets / scale time (onset, dur, sustain) / scale amplitude. Lazy. |
merge(a, b) | Onset-ordered merge of two event streams — parallel combination. Lazy; works on infinite streams. |
takeDur(es, beats) | Keep events with onset before beats; the sanctioned cut for infinite streams. |
showEvent(e) / showEvents(es) | Stable formatting, one event per line (used by the golden tests). |
A Tuning maps real-valued steps to frequency; a Scale is a separate layer that selects steps per degree (the same split as Scala's .scl tuning files versus keyboard mappings). Equal tunings are a formula — the division count is a real number, so 8.5 divisions of the octave is legal — and unequal tunings are a cents table with linear interpolation at fractional positions. Degrees, ratios, and cents resolve relative to the tuning root; steps and Hz are absolute.
| Type / Function | Description |
|---|---|
Tuning / TuningKind | ed(divisions, periodRatio) (both real) or table(cents, periodRatio); plus rootHz anchored at rootStep. |
edo(n) / ed(n, period) | n equal divisions of the octave / of an arbitrary period ratio. |
bp() | 13 equal divisions of the tritave (the Bohlen–Pierce division). |
et12 | 12ED2 anchored so step = MIDI note number (69 → 440 Hz). |
scala(cents, period = 2.0) / ji(ratios, period = 2.0) | Unequal tunings from a cents table / from just-intonation frequency ratios. |
root(t, hz, step) | Re-anchor: frequency hz at step step. |
transposeRoot(t, k) | Chromatic transposition of the whole performance: re-anchor so degree 0 (and every root-relative pitch) sounds k steps higher. Degree pitches keep their spelling; in a table tuning the new key picks up the temperament's flavor at that root. |
Scale | degrees [Float] (steps per degree) + wrap (steps per repetition). Constants: major, minor, dorian, pentMajor, pentMinor, wholeTone; chromatic(k) makes every step a degree. |
stepHz(t, step) / hzStep(t, hz) | Step ↔ frequency, continuous in both directions. |
degreeStep(s, d) | Integer scale degree → tuning step, wrapping through the scale in both directions (negative degrees descend below the root). |
degreeHz(t, s, d, acc = 0.0) | Degree → Hz with an optional accidental in tuning steps. |
pitchHz(p, t, s) | The single play-time resolver for any Pitch. |
| Function | Description |
|---|---|
coin(p) | True with probability p. |
gauss(mu, sigma) | Normally distributed value. |
wpick(xs, ws) | Weighted random choice (stream form wpicks is in music.pat). |
These sit alongside the built-ins urand/brand/rand/irand/xrand/pick/muss and their stream forms; randSeed(n) re-seeds the per-VM generator for reproducible streams and performances.
import music.core.*;
-- 13 equal divisions of the tritave, anchored at 220 Hz
let t = bp() root(220.0, 0.0);
stepHz(t, 13.0); -- 660.0 (one tritave up)
-- 8.5 equal divisions of the octave; fractional steps are ordinary
stepHz(edo(8.5), 4.25); -- halfway up the octave
-- degrees resolve through a scale, wrapping below the root
degreeStep(major, 9); -- 16.0 (a tenth: octave + third)
degreeStep(major, -1) -- -1.0 (leading tone below)
↑ Back to top
Pattern streams, inspired by SuperCollider's Patterns library and by Sound as Pure Form (SAPF). There is no separate pattern class: patterns are ordinary lazy List values, usually infinite, so the whole list vocabulary already applies (cyc, take, drop, stutter, zip, scan, the infinite random streams rands/irands/xrands/picks, …). This module adds the missing musical vocabulary plus bind, which zips per-field streams into events. Pure and RT-safe; seed with randSeed for reproducibility.
| Function | Description |
|---|---|
series(a, d) / geom(a, r) | Infinite arithmetic / geometric series. |
browns(lo, hi, step) | Brownian walk, reflecting at the bounds. |
gausses(mu, sigma) / coins(p) | Infinite gaussian / weighted-Bool streams. |
wpicks(xs, ws) | Weighted random choices. |
xpicks(xs) | Random choices, never the same element twice in a row. |
shufs(xs) | Concatenated fresh shuffles — every pass is a new permutation. |
lace(ls) | Round-robin interleave of streams; skips exhausted ones, tolerates infinite ones. |
stutter(xs, ns) | Repeat element i of xs ns[i] times (0 drops it); stream first, like the scalar-count stutter builtin. |
once(x) / embed(ls) | One-element stream / flatten a stream of streams one level — the embedding idiom. |
step / hz / ratio / cents / rest | Scalar Float → Pitch constructors; auto-mapping lifts them (and the core's degree) over lists: [0, 2, 4] cyc degree. |
bind(pitch, dur, amp = 0.5…, legato = 0.8…, extras = []) | Zip one stream per event field into List<Event>. Onsets are the running sum of dur; sustain = dur * legato; extras pairs a param name with a value stream. Ends at the shortest input, so any finite stream bounds the result. |
import music.pat.*;
randSeed(2026);
let melody = bind(
wpicks([0, 1, 2, 4], [4.0, 2.0, 3.0, 1.0]) degree, -- weighted degrees
List(0.5, 0.25, 0.25) cyc, -- durations
rands(0.2, 0.6), -- amplitudes
List(0.7) cyc, -- legato
[('cutoff, series(800.0, 50.0))]); -- a param stream
let bass = bind(List(-7, -5) cyc degree, List(2.0) cyc);
let tune = merge(melody, bass) takeDur(32.0); -- two voices, 32 beats
↑ Back to top
The player, split so its logic stays testable. music.score is pure: it lowers an event stream to a time-ordered command stream — noteOn at each onset, noteOff at onset + sustain, note IDs allocated round-robin from the voice's pool, rests skipped. NoteCmd deliberately mirrors the note cases of bundles.x's EngineCmd, so the engine adapter is a one-line map per case. music.play (engine-coupled; re-exports bundles.*) turns each group of simultaneous commands into one first-class Bundle scheduled at an absolute beat on the engine's TempoClock — chords are atomic and timing is sample-accurate.
| Type / Function | Description |
|---|---|
NoteCmd | noteOn(node, id, params) / noteOff(node, id) / noteSetParams(…). |
Voice, voice(node, names, defaults, poly = 16) | How events map onto one synth node: positional param order (= noteParam declaration order), per-name defaults, note-ID pool size. |
pitchVoice(node) / freqVoice(node) | Ready-made voices: ['pitch, 'amp] for synths taking a note number, ['freq, 'amp] for synths taking Hz. |
eventParams(e, v, t, s) | Resolve one event to positional [Float]: 'freq → pitchHz through the tuning; 'pitch → the equivalent (fractional) note number, so any tuning survives a note-number synth exactly; 'amp → the event amp; other names look up e.params, then the default. |
score(events, v, t = et12, s = major) | Lazy List<(Float, NoteCmd)> — the golden-test surface. |
showScore(sc) | One beat: command line per entry. |
play(events, v, t = et12, s = major, silo = 0, clock = 0) | Compile and start playing; returns a Player handle. |
stop(p) / replace(p, events) / enqueue(p, events) | Stop (releases held notes); swap the playing sequence at the next wake; queue a sequence to follow the current one. |
stopAll() | Stop every player started by play and not yet stopped — the recovery when a Player binding was overwritten and the old player plays on. |
stopFirst() / stopLast() | Stop one running player: the oldest (first started) or the newest (undo the latest play). Return true if one was stopped. |
voiceBundle(v, defName) | Node-graph setup (create voicer, connect to output) as a reusable Bundle. |
scorePlayer(sc) | A coroutine for code running inside a silo task: walks a score with immediate playNote/releaseNote; drive it with spawn(clock, scorePlayer(sc)). |
render(events, v, defName, path, durSecs, t = et12, s = major) | Offline render of a finite stream via renderNRT; returns the handle for await renderDone(h). |
import music.pat.*;
import music.play.*;
import audio_engine.*;
engineStart();
let v = pitchVoice(101);
voiceBundle(v, "my_voicer") go(0);
let p = play(tune, v, et12, pentMinor); -- sample-accurate Bundles on the beat
p replace(otherTune); -- swap what's playing
p stop;
-- or render offline, in any tuning:
let h = render(tune, v, "my_voicer", "/tmp/take1.wav", 20.0, bp() root(220.0, 0.0), chromatic(13));
await renderDone(h);
↑ Back to top
An algebra of notes and compositions, inspired by Paul Hudak's Haskore and the temporal-media work in The Haskell School of Music. Music<T> is a recursive value: a note (duration + payload), a rest, two Musics in sequence, two in parallel, or a control annotation over a subtree. Because a piece is a value, it can be analyzed and transformed before it is performed. Payloads are un-timed by design — time lives in the structure, and Event is the output of perform. Transformations take the Music first, so pipelines read left-to-right: theme dyn(0.7) trans(-4) tempo(2.0). Pure and RT-safe.
| Type / Function | Description |
|---|---|
Music<T> | note (Float, T), rest Float, mseq, mpar, ctl (Control, Music<T>). |
a $ b / a | b | Sequential / parallel composition (operator overloads on Music). |
note(d, x) / mrest(d) | A note / a Music<Pitch> rest (for other payloads write Music<T>.rest(d)). |
line(ms) / chord(ms) / times(m, n) | Fold an array sequentially / in parallel / repeat n times. |
tempo(m, k) / trans(m, k) / dyn(m, k) / inst(m, i) | Controls: divide durations; transpose modally — shift degree pitches by k whole scale degrees, accidentals riding along, other pitch kinds untouched (chromatic transposition of a whole piece is a tuning property: transposeRoot); scale amplitude; tag a subtree with an instrument (emitted as an 'inst param). |
mdur(m) | Duration in beats, tempo annotations included. |
retro(m) | Retrograde; parallel branches are rest-padded so they still end together. |
mmap(m, f) | Map over payloads, preserving structure. |
perform(m, t0 = 0.0, legato = 0.9) | Interpret Music<Pitch> or Music<MNote> into List<Event>; the generic form perform(m, mk, ctx) takes an event-builder for any payload type. |
MNote, mnote(pitch, amp = 0.5, params = []) | A payload carrying amplitude and synth params alongside the pitch. |
import music.media.*;
fn deg(d Int, dur Float) Music<Pitch> = note(dur, degree(d));
let theme = line([deg(0, 1.0), deg(1, 1.0), deg(2, 0.5), deg(4, 1.5)]);
-- the theme against itself: two beats late, a fifth down, quieter
let canon = theme | (mrest(2.0) $ (theme dyn(0.7) trans(-4)));
canon mdur; -- 6.0
canon retro perform showEvents; -- reversed, as events
canon perform play(v); -- or straight to the player
↑ Back to top
Shapes, morphological operators, and hierarchical forms, inspired by HMSL (the Hierarchical Music Specification Language). A Shape is a sequence of frames over named dimensions — by default [dur, degree, amp], but any dimension you add ('cutoff, 'pan, …) is carried into events as a synth param of the same name, so melodies can run over any set of control dimensions, not just pitch. music.job arranges shapes into hierarchies whose selection decisions are made at realize time: realizing the same form twice gives two readings. Pure (RNG only).
| Type / Function | Description |
|---|---|
Shape, shape(data, dims = ['dur, 'degree, 'amp]) | Frames × named dimensions. |
numFrames / dimIndex / dimOf / withDim | Introspection and column replacement. |
retrograde(sh) | Reverse the frame order. |
invertDim(sh, dim, center) / transposeDim / scaleDim / quantizeDim / warpDim(sh, dim, f) | Per-dimension morphs (reflect, shift, scale, snap to grid, arbitrary function). Missing dimensions are a no-op. |
catShapes(a, b) / interleave(a, b) | Concatenate / alternate frames. |
shapeDur(sh) / shapeEvents(sh, t0 = 0.0, legato = 0.9) | Total beats / render to events (pitch from the first of degree/step/hz/cents present). The degree dimension stays Float so morphs can interpolate; rendering rounds it to the nearest whole scale degree. |
Coll | leaf Shape, seqc [Coll], parc [Coll], rpt (Int, Coll), sel (Behavior, [Coll]). |
Behavior | inOrder (cycle), atRandom, shuffled (no repeat until all seen), weighted [Float]. |
realize(c, t0 = 0.0) / realizeDur(c) | Walk the hierarchy into List<Event>; behaviors and repeats make fresh selections per call, and selection state advances across visits (e.g. under rpt). |
realizeCo(c, t0 = 0.0) | Incremental realize: the same walk as a lazy stream. Events materialize as the consumer pulls them (cost per pull is bounded by tree depth, not form size) and selection decisions are made just-in-time, one event ahead of playback — the HMSL-player model. Seeded readings match realize unless randomness sits under parc, whose children are pulled interleaved by onset. |
import music.job.*;
let theme = shape([[0.5, 0.0, 0.5], [0.25, 1.0, 0.4], [0.5, 3.0, 0.5], [0.75, 4.0, 0.6]]);
let riffs = [
Coll.leaf(theme),
Coll.leaf(theme retrograde),
Coll.leaf(theme invertDim('degree, 2.0)),
Coll.leaf(theme transposeDim('degree, 4.0) scaleDim('dur, 0.5)),
];
let form = Coll.seqc([
Coll.rpt((4, Coll.sel((Behavior.shuffled, riffs)))), -- four shuffled riffs
Coll.parc([Coll.leaf(theme), Coll.leaf(shape([[3.0, -7.0, 0.4]]))]),
]);
let p = play(realize(form), v); -- one reading, realized up front
p enqueue(realize(form)); -- a different reading follows it
let big = Coll.rpt((10000, Coll.sel((Behavior.shuffled, riffs))));
play(realizeCo(big), v); -- incremental: selects as it plays
↑ Back to top
Patterns as functions of time, inspired by Tidal Cycles. A Pattern<T> is a callable value: query it with a Span (in cycles, half-open) and it returns the Haps — value fragments — active in that window. Structure is computed, never stored, so transformations compose freely and everything is repeatable. Time is Float cycles with an explicit edge policy: fragments shorter than kEps (10−9) are dropped, cycle iteration starts at floor(a + kEps), and onset tests compare within kEps. Combinators take the pattern first, so pipelines read left-to-right. Pure and RT-safe.
| Type / Function | Description |
|---|---|
Span / Hap<T> / Pattern<T> | span(a, b); a hap's whole is its full extent (none for continuous signals), part the fragment inside the query; patterns are callable (p call(s) or p(s)). |
pure(x) | One occurrence of x per cycle. |
signal(f) / sinePat() / sawPat() / steady(x) | Continuous signals (sampled, not evented). |
fast(p, k) / slow(p, k) / early(p, off) / late(p, off) | Time scaling and rotation, by real amounts. |
rev(p) | Reverse each cycle. |
fastcat(ps) / slowcat(ps) / stack(ps) | All in one cycle / one per cycle in rotation / all at once. |
every(p, n, f) / iterp(p, n) | Apply f every n-th cycle / rotate one n-th further each cycle. |
euclid(p, k, n, rot = 0) / euclidMask(k, n, rot) | Distribute p over the onsets of a Euclidean rhythm (e.g. euclid(p, 3, 8) is the tresillo). |
segment(p, n) | Sample a (usually continuous) pattern n times per cycle. |
pmap(p, f) / range(p, lo, hi) | Map values / scale a 0..1 signal into a range. |
filterOnsets(p) / sortHaps(hs) | Keep haps that begin in the query / order haps by part start. |
zipl(pa, pb, f) | Structure from the left, values combined with the right pattern sampled per hap — the role Tidal's # plays (the operator set is fixed, so combination is by name). |
events(pp, amp = 0.5, legato = 0.8) | Lift a Pattern<Pitch> to Pattern<Event>. |
withAmp(pe, pa) / withLegato(pe, pl) / withParam(pe, name, pv) / mix(a, b) | Combine an event pattern with value patterns / overlay two event patterns. |
patEvents(p, cycles, beatsPerCycle = 4.0) | Render onsets of the first cycles cycles to the shared List<Event> in beats — the bridge to the player. |
import music.spans.*;
import music.play.*;
fn d(x Int) Pattern<Pitch> = pure(degree(x));
let tune = stack([
d(-14) euclid(3, 8) events(0.8, 0.6), -- euclidean bass
fastcat([d(0), d(4), d(5), d(7)])
iterp(4)
every(4, fn(p Pattern<Pitch>) Pattern<Pitch> { p rev })
events(0.5, 0.9)
withAmp(sinePat() slow(4.0) range(0.25, 0.6) segment(4)),
]);
play(patEvents(tune, 16.0, 2.0), v); -- 16 cycles, 2 beats per cycle
↑ Back to top
Because every dialect ends in the same List<Event>, the layers combine freely: merge a bind stream with a performed canon, enqueue rendered span-pattern cycles after a realized form, apply the event helpers (stretch, gain, takeDur) to any of them, and resolve all of it through one tuning at play time. The pure layers golden-test under plain tzpl (see lang/tests/music/).
Complete runnable examples, each with run instructions in its header:
| Script | Shows |
|---|---|
integration-tests/scripts/pat_audible.x | Stream patterns live: weighted-random melody + bass via bind/merge. |
integration-tests/scripts/pat_nrt.x | Offline render of a tritave-tuning walk (13 equal divisions of 3/1) via render. |
integration-tests/scripts/media_audible.x | A canon built with $/|/trans/tempo and performed. |
integration-tests/scripts/shape_audible.x | Shape morphs + a shuffled hierarchical form, three readings queued on one Player. |
integration-tests/scripts/spans_audible.x | Euclidean bass, offbeat stab, rotating/reversing melody with a sine-ridden amp. |