Patterns¶
Patterns are reusable templates that produce a fresh iterator each time they
are iterated, so one pattern object can drive many players. Value patterns
(Pseq, Prand, Pwhite, ...) yield numbers; event patterns (Pbind and the
composites below) yield events, which a Player turns into synths on a
Clock. See the Patterns API reference for full signatures.
from nanosynth import Clock, Pbind, Pseq, Server
with Server() as server:
synthdef.send(server)
server.sync()
clock = Clock(bpm=120)
player = Pbind(
instrument="sine",
degree=Pseq([0, 2, 4, 7], repeats=4),
dur=0.25,
).play(clock, server)
Timing¶
Events are scheduled as timestamped OSC bundles rather than sent when the
Python thread happens to wake, so onset accuracy comes from the engine. The
latency window (default 0.1s) is how far ahead events are stamped:
clock = Clock(bpm=120, latency=0.2) # more slack under load
player = pattern.play(clock, server, latency=0.05) # per-player override
Raise it if playback stutters when the machine is busy; lower it for tighter response to live input. See the Threading Model for why this removes Python-side jitter from the audible result.
Quantization¶
quant starts playback on the next boundary of the clock's beat grid instead
of immediately, which is what keeps parts phase-aligned when you launch them by
hand at arbitrary moments:
bass.play(clock, server, quant=4) # next bar in 4/4
hat.play(clock, server, quant=4, offset=0.5) # half a beat after the bar
All players on one clock share a grid, so two patterns started seconds apart
still begin together. clock.reset_grid() moves beat 0 to now.
The event model¶
Pbind merges its bindings with defaults, then fills in derived keys. Anything
you bind explicitly wins, so binding a downstream key bypasses the derivation
that would otherwise produce it.
Pitch¶
The chain runs degree -> note -> midinote -> freq:
-
degreeindexes intoscale(default"major"), wrapping across octaves, so degree 7 of a 7-note scale is the octave above degree 0. Fractional degrees interpolate between adjacent steps. -
scaleis a name fromSCALESor an explicit list of semitone offsets. -
roottransposes in semitones;octave(default 5) places note 0 at MIDI 60, middle C, as in sclang. -
Binding
freqdirectly skips all of it.
Pbind(degree=Pseq([0, 2, 4]), scale="minor", root=3, octave=4)
Amplitude and timing¶
-
dbderivesamp(amp = 10 ** (db / 20)). -
sustaindefaults todur * legato * stretch(legato default 0.8), and is what the gate release is scheduled from. -
delta-- how far the player advances after the event -- defaults todur * stretch. Bind it directly to decouple note spacing from note length.
Keys that feed this machinery (degree, note, midinote, octave, root,
scale, db, dur, delta, sustain, legato, stretch, instrument) are
metadata and are never sent to the synth as controls. Everything else is.
Referencing sibling keys¶
Pkey reads another key of the event being built. Patterns do not overload
arithmetic operators, so pass a transform rather than writing Pkey("freq") * 2:
Pbind(degree=Pseq([0, 4]), amp=Pkey("freq", lambda f: 40.0 / f))
A Pkey bound to a chain input (degree, dur, db, ...) resolves before
the chain runs, so it can drive it. Bound to anything else it resolves after,
so it can read the chain's output.
Composite patterns¶
-
Pparruns event patterns in parallel, merging them into one time-ordered stream. Each voice keeps its ownsustain; onlydeltais rewritten to the gap in the merged stream. Simultaneous events get a delta of 0, which puts them in the same bundle -- they start on the same sample, not merely in the same tick. -
PtparisPparwith a beat offset per pattern, for entries that stagger. -
Pmonoholds one synth for the whole pattern and sends/n_setper event instead of creating a new node, then releases it at the end. This is how a monophonic line glides -- portamento and filter sweeps that separate synths cannot produce.sustainis ignored. -
Pdefis a named, hot-swappable pattern registry.Pdef("bass", pattern)registers or replaces;Pdef("bass")looks up. A running player picks up a replacement at the next event, so a part can be rewritten without stopping playback -- the pattern analogue ofNdef. -
Pfinbounds a pattern by event count;Pfindurbounds it by total duration, clipping the lastdeltaso the total is exact and the pattern lands on a bar line.
from nanosynth import Pdef, Pmono, Pn, Ppar, Pseq, Pbind
Pdef("bass", Pn(Pmono("acid", degree=Pseq([0, 0, 3, 5]), dur=0.25)))
Pdef("lead", Pn(Pbind(instrument="blip", degree=Pseq([7, 9, 11]), dur=0.5)))
player = Ppar([Pdef("bass"), Pdef("lead")]).play(clock, server, quant=4)
# ... later, without stopping playback:
Pdef("lead", Pn(Pbind(instrument="blip", degree=Pseq([12, 11, 9]), dur=0.25)))
Every pattern has play(), not just the event composites, so the generic
wrappers stay playable -- Pn(Pbind(...), 4), Pfin(8, part), partA | partB.
Only patterns yielding events are meaningful to play; a value pattern will
produce nonsense.
Demos¶
make demos runs the scsynth demo scripts, four of which cover this material:
-
22_bundle_scheduling.py-- the same rhythm sent immediately and as timestamped bundles, under GIL load, so the timing difference is audible. -
23_pitch_chain.py-- one degree sequence through four scales, thenroot,octave,db,legato,stretchandPkey. -
24_parallel_quant.py--Ppar,Ptpar, and parts launched mid-bar that snap to the downbeat viaquant. -
25_pmono_pdef.py--PbindversusPmonoon the same line, livePdefswaps, andPfin/Pfindur.
Not yet implemented¶
-
Array-valued keys for chords (one event expanding to several synths).
-
Arithmetic operators on patterns; use
Pkeywith a transform. -
Score.from_pattern()-- the realtime pattern engine and NRT scoring do not yet share a code path, so patterns cannot be rendered offline.