synth() -- Web Audio Synth Cue
Oscilla "Native WebAudio Utility Synth"
The synth() cue creates a lightweight Web Audio sound source inside Oscilla. It is intended for reference tones, drones, textures, chords, and simple patterned sound processes, integrated directly into the score timeline.
The design prioritises deterministic, cue-scoped behaviour; readable parameter syntax; low default amplitudes and click-free envelopes; optional pattern sequencing; optional OSC mirroring; and real-time parameter binding from animation and control sources.
The synth is not intended as a full synthesiser environment. For full electroacoustic work, Oscilla is designed to operate in conjunction with external audio systems via OSC (e.g. SuperCollider, Pure Data, Max). The built-in synth provides a bounded, score-aligned sound source intended for rehearsal contexts away from a complete setup, as well as for simple tone cues, drones, and basic animation- or pattern-driven sound sequences embedded directly in the score.
Basic Usage
synth(uid:refA, wave:sine, freq:440, amp:0.1)
Pitch may be specified as Hz, note name, or MIDI number:
synth(uid:tuning, wave:sine, freq:A4, amp:0.08)
synth(uid:sharp, wave:sine, freq:C#3, amp:0.08)
synth(uid:midi, wave:sine, freq:m69, amp:0.08)
The ~ prefix on note names (~A4) is accepted but optional -- except before cents down on a natural or flat: ~E1-30 (E1+10 needs none).
Parameters
| Key | Default | Description |
|---|---|---|
uid |
auto | Unique voice identity. Auto-generated from element id if omitted |
wave |
sine |
Waveform type. wave:choose(square, noise, triangle) picks one each time the line runs; a pattern -- wave:Pchoose(square, noise, triangle), wave:Pseq(saw, square) -- changes it every step (drums not included) |
freq |
440 | Frequency: Hz, note name (~A4), MIDI number (m69), array (chord), pattern, rand(), rest (r), or signal ref |
degree |
-- | In place of freq: steps of scale: from root: (see Degrees of a scale) |
amp |
0.08 | Amplitude 0--0.25. Accepts rand() or signal ref |
pan |
0 | Stereo position -1 (left) to 1 (right). rand(-1, 1) picks one each time the line runs; a pattern -- pan:Pwhite(-1, 1), pan:Pseq(-1, 1) -- moves it every step; pan:sinOsc(0.4) sweeps it smoothly (oscillators); or a signal ref |
fm |
-- | {ratio, index, decay}: frequency modulation (see FM) |
drive |
0 | Soft clip 0--1 before the envelope and filter (see Drive) |
fuzz |
-- | A starved fuzz pedal: fuzz:0.7 or {starve, gain, bias, tone} (see Fuzz) |
sub |
-- | An octave (or two) down, under the sound: sub:0.8 or {level, octave, tone} (see Sub) |
fold |
0 | A wavefolder, 0..1: driven past the edge, the wave folds back in (see Grit) |
clip |
0 | Hard clipping, 0..1: flat at the top and bottom (drive: is the soft one) |
ring |
-- | Ring modulation: ring:300 (Hz) or {freq, mix} |
comb |
-- | A comb filter fed back, ringing at its frequency: comb:110 or {freq, fb, mix} |
crush |
-- | A bitcrusher: crush:4 (bits) or {bits, rate} (rate in Hz) |
stack |
-- | Every note with these above it, in semitones: stack:[0, 7] parallel fifths, stack:12 octaves (see stack) |
octave |
0 | Up or down so many octaves; a pattern draws one each step: octave:Prand(0, 0, 1, -1, inf) (see octave) |
env |
{a:0.02, d:0, s:1, r:0.1} |
ADSR envelope object |
envPerStep |
false | Retrigger envelope on each sequence step (articulated notes). envPerStep:1 strikes each step with env:; envPerStep:{a:0.005, d:0.03, s:0, r:0.01} is that envelope itself |
dur |
-- | Step duration for patterns, or voice lifetime if no step engine, in beats (seconds at the default 60 bpm, see Tempo). Omit for gate mode |
filter |
-- | Filter object {type, freq, q} |
delay |
-- | Delay object {time, fb, mix, tone, sat}: fb up to 1.5 -- past 1 it runs away into a held squeal, tone (Hz) darkens each time round, sat (0..1) how hard the loop's saturator holds it (see Feedback) |
reverb |
-- | A room: {mix, time, damp, pre} -- see effects |
trig |
-- | A signal (trig:every(4, beat.hit)): the line runs on each hit. trig:wait (or trig:solo): learnt, not played -- solo() or bang() starts it (see below) |
once |
-- | 1: one step, then the voice lets go -- a drum struck once instead of every step. A line run by a signal trig: (trig:w.hit) is once by itself unless it has a dur: |
vu |
-- | stereo: a pair of bars, left and right, in vu() meters (vu(all) too); mono: one |
bus |
-- | Through a shared fx() bus with other sounds: bus:room; bus:none its own way again |
glide |
0.02 (0 with envPerStep) |
Portamento: beats to slide from one note to the next. Struck notes (envPerStep) do not slide unless asked: glide:0.1 |
overlap |
-- | With envPerStep: up to this many notes sounding at once, each its own voice -- a note's release rings on under the next ones instead of being squeezed into its step. overlap:3 |
interp |
smooth |
Interpolation mode: smooth or step |
poly |
0 | Polyphony: 0=mono (retrigger updates), N=overlapping voices |
lifetime |
region |
region (stop on playhead exit) or process (until explicit stop) |
waveform |
self |
Scope display target: self, none, or element id |
overlay |
2 | Overlay detail: 0/off, 1/brief, 2/full |
osc |
0 | Enable OSC mirroring (1=on) |
oscAddr |
-- | Custom OSC address |
rel |
env.r | Override release time for synthStop |
fadeIn |
0 | Whole-voice fade-in (beats): ramps the voice's output as a whole, independent of the per-note envelope — fades in an articulated sequence (envPerStep) where env.a would only shape each note |
fadeOut |
-- | Whole-voice fade-out (beats) on stop / region exit — alias of rel (delay/reverb feedback decays inside the fade) |
extent |
element width | Active-region width in score units from the element's left edge, with a draggable score-visible line (see extent) |
Wave Types
sine
square
saw
triangle
noise
pink
brown
dust
step
in
Oscillator waveforms (sine, square, saw, triangle) map directly to standard Web Audio oscillator types. kick, snare and hat are drums (see Drums).
Noise types produce broadband noise from pre-computed looping audio buffers with different spectral slopes:
| Type | Spectrum | Character |
|---|---|---|
noise |
White -- flat spectrum | Bright, hissy, equal energy per Hz |
pink |
-3 dB/octave | Balanced, natural -- equal energy per octave |
brown |
-6 dB/octave | Deep, rumbly -- strong low-frequency emphasis |
Pink noise is generated using the Paul Kellet algorithm (six first-order IIR filters). Brown noise uses a leaky integrator of white noise. Both are normalized to full scale. Each loop is 8 seconds, so a loud wall of it does not audibly repeat.
Two noises take their rate from freq: -- so a pattern, follow() or sinOsc on freq moves it, as it would a pitch:
| Type | ||
|---|---|---|
dust |
freq clicks a second, on average |
Single-sample clicks at random times, each its own height and sign: sparse crackle, fed through fuzz:, comb: or a delay a fire, rain, a bad cable |
step |
a new level freq times a second |
Stepped noise (sample and hold): a random staircase at low rates, a grainy, pitched hiss at high ones -- freq:Pwhite(20, 2000) |
wave:in is the external audio input -- the one adc() measures --
as the voice's sound: through its envelope, fuzz:, grit, filter, delay,
reverb and scope, as any sound is. synth(uid:them, wave:in, fuzz:0.8, crush:{bits:4, rate:3000})
mangles whoever is plugged in. It has no pitch (freq: does nothing). The
browser asks for the input the first time; with a microphone and speakers in
one room it feeds back -- headphones, or a line in.
Drums — wave:kick, snare, hat
synth(uid:k, wave:kick, dur:1)
synth(uid:s, wave:snare, dur:2, quant:2)
synth(uid:h, wave:hat, dur:0.25, amp:Pseq(0.1, 0.03, 0.06, 0.03, inf))
synth(uid:k, wave:kick, dur:Pseq(0.75, 0.25, 1, inf), freq:45)
Three struck sounds, each hit once a step: kick a sine falling from three
times its note to the note (freq:, default 50 Hz), snare noise through a
band-pass, hat noise through a high-pass. They are ordinary synths with
short envelopes and envPerStep on, so everything else works on them -- an
amp: pattern is the accents (0 a silent step), dur: patterns the rhythm,
filter:, drive:, delay:, reverb: and env: change them. They keep to
the beat grid and follow tempo().
A drum with no dur: strikes every beat. Run by a signal -- trig:w.hit,
a word of a text -- it strikes once a hit; once:1 makes any line one strike:
synth(uid:pop, wave:kick, amp:1, quant:0, trig:w.hit) a kick with every word
synth(uid:k, wave:kick, once:1) one kick, now
FM — fm:
synth(freq:~A2, fm:{ratio:2, index:3}) hollow, reedy
synth(freq:~A2, fm:{ratio:0.5, index:4}, filter:{type:lp, freq:800}) a growl
synth(degree:Prand(0, 2, 4, 7, inf), dur:0.5, envPerStep:1, fm:{ratio:3.5, index:6, decay:0.5}) bells
A second oscillator (a sine) bends the pitch of the first, fast: it runs at
ratio × the note and swings it by index × its own frequency. index is the
brightness -- 0 is the plain wave, 1 to 3 warm, 5 and up bright and metallic.
Whole-number ratios stay harmonic (1 brassy, 2 hollow, 3 reedy), others are
inharmonic: bells, gongs, metal. decay: (beats) lets the brightness fall
away after every note, the way a struck or plucked sound does -- with
envPerStep each step is struck again. fm:3 is short for fm:{index:3}.
The modulator follows the note through steps, glides and chords. FM on a sine is the classic; on a square or saw it gets harsh quickly.
Drive — drive:
synth(wave:saw, freq:~A1, drive:0.6, filter:{type:lp, freq:900, q:6})
drive: 0..1 rounds the wave off against a ceiling (a soft clip) before the
envelope and the filter: a little warms it, more turns a sine into something
near a square. A filter after it tames what it adds. A square is already
clipped flat, and a fuzz: after it clips far harder: there drive: adds
nothing to hear -- it is for sines, triangles, saws.
Fuzz — fuzz:
synth(uid:fz, wave:saw, freq:[E1, B1, E2], fm:{ratio:1.41, index:6},
fuzz:{starve:0.7, gain:40, tone:1800}, amp:0.2)
synth(uid:grit, wave:pink, fuzz:0.85, filter:{type:lp, freq:sinOsc(0.1, 0, 300, 3000)}, amp:0.25)
A fuzz pedal on a dying battery. Where drive: is a fixed curve, fuzz:
lives: its supply sits low and sags under load, the transistor drifts off
centre, the signal near zero falls into a dead zone -- the gated, splatty
buzz, quiet passages choked off -- and the failing supply wobbles chaotically
and drops out in short crackles. fuzz:0.7 is how starved (0 healthy, 1
nearly dead); the object sets the rest:
| Key | Default | |
|---|---|---|
starve |
0.5 | 0..1: lower, saggier rails, a wider dead zone, more wobble and crackle |
gain |
30 | 1..200: how hard it is driven |
bias |
0.3 | 0..1: how far off centre -- uneven, buzzier |
tone |
2500 | Hz: warmth, lowpassed above it |
It sits after drive: and before the envelope, so the envelope still shapes
what comes out; run the line again to change it. The fuller the input, the
richer the mash: a chord, fm:, noise.
Sub — sub:
synth(uid:bass, wave:saw, freq:Pseq(~A2, ~E2, inf), dur:1, sub:0.8)
synth(uid:bass, wave:saw, freq:~A2, sub:{level:1, octave:2, tone:0.2}, fuzz:0.6)
An octave pedal's sub under the sound. It finds the pitch in the wave
itself -- a switch flipping at each cycle, as an analogue divider does --
so it follows patterns, glides, follow() and sinOsc() as they play, and
rounds what it makes into a fat, near-sine sub an octave (or two) down.
sub:0.8 is how much; the object sets the rest:
| Key | Default | |
|---|---|---|
level |
0.8 | 0..2: how much sub, under the dry sound |
octave |
1 | 1 or 2 octaves down |
tone |
0.5 | 0 round, near a sine .. 1 brighter, more of the divider's square |
It is first in the voice -- before drive:, fuzz: and the envelope -- as
an octave pedal before a fuzz: sub: with fuzz: mashes the sub too. On
one line of pitches it tracks cleanly; on a chord or noise it growls.
Grit — fold:, clip:, ring:, comb:, crush:
synth(uid:a, wave:saw, freq:55, fold:0.6)
synth(uid:w, wave:noise, crush:{bits:3, rate:2000}, comb:{freq:82, fb:0.97}, amp:0.15)
synth(uid:r, wave:square, freq:40, ring:follow(r.level, 30, 900), clip:0.7, amp:0.1)
synth(uid:f, wave:sine, freq:110, fold:follow(knob.t, 0, 1))
| Key | ||
|---|---|---|
fold |
0..1 | A wavefolder: the wave driven up to ten times over, and what goes past the edge folded back in -- 0 the sound as it is, a little a brighter edge, all of it a buzz of folds |
clip |
0..1 | Hard clipping: driven up to 31 times into a flat ceiling -- a square of anything |
ring |
Hz, or {freq, mix} |
Times a sine at freq: the tone goes, and its sum and difference with freq come in -- bells, metal, on noise a grind. mix (1) how much |
comb |
Hz, or {freq, fb, mix} |
The sound with itself a period of freq ago, fed back (fb 0.9, -0.99..0.99): it rings at freq and its multiples -- noise into comb:82 is a string of E, fb:0.98 a long one; negative fb rings an octave down, hollow. mix (1) how much |
crush |
bits, or {bits, rate} |
Fewer bits (16 is none: crush:4 is 16 steps, crush:1 on or off) and held at a lower rate (rate:2000 Hz: aliased, metallic) |
In that order, after fuzz: and before the envelope: the envelope still
shapes what comes out. Every value can follow() a signal -- the sound
changes as it moves (gliding over about 10 ms, so a moving fold does not
click). fx(bus, crush:4, fold:0.4) puts them on a bus.
drive: and fuzz: follow signals too: drive:follow(bass.level, 0, 1),
fuzz:follow(knob.t, 0, 1) (how starved), fuzz:{starve:0.6, tone:follow(knob.t, 300, 9000)}.
Feedback — delay:{fb:} past 1
synth(uid:s, wave:saw, freq:220, dur:0.25, envPerStep:1, env:{a:0.005, d:0.1, s:0}, delay:{time:0.1, fb:1.2, tone:2000, sat:0.5, mix:0.5})
A delay's echoes go round a loop: through a lowpass (tone:, none by
default) and a saturator, then fb of them back in. Below 1 they die away;
past 1 (up to 1.5) each is louder than the last until the saturator holds
them -- a squeal that sustains itself after the sound stops, darker with a
lower tone, dirtier with more sat (0..1). hush or a stop ramps the
feedback down and it dies. Quiet echoes go round as they always did.
However loud a patch gets, the output never goes past a ceiling just under full scale: after the master limiter, a last stage rounds off anything above 0.8 and stops it at 0.95.
Frequency and Pitch
Frequency accepts several formats:
| Format | Example | Description |
|---|---|---|
| Hz | freq:440 |
Direct frequency in Hertz |
| Note name | freq:A4 |
Standard pitch notation (A0--C8), sharps/flats (C#3, Bb2), cents offsets (E1+10, C#3+25, ~A4-50, ~A4+12.5), drawn cents (~G2+rand(0, 25), ~G2-rand(0, 25), ~G2+birand(5, 20): a fresh detune each time it is played -- each run, each step of a pattern); the ~ prefix is optional except before a cents offset down on a natural or flat (~E1-30: without it, E1-30 would be read as a name, as ids can have hyphens) |
| MIDI number | freq:m69 |
MIDI note number with m prefix (m69 = A4 = 440 Hz, m60 = middle C); fractions are microtones (m60.5 = quarter tone up). Bare numbers are always Hz |
| Chord array | freq:[440, 550, 660] |
Multiple oscillators summed |
| Random | freq:rand(200, 800) |
Random Hz per trigger |
| Pattern | freq:Pseq(220, 330, 440) |
Sequenced values |
| Rest | freq:Pseq(~A4, r, ~C5) |
Silence in patterns (r, ~r, rest, ~rest) |
| Signal ref | freq: follow(fader.t, 200, 2000) |
Real-time binding from control source |
Through a pitch world -- value | filter
synth(freq:Pwhite(40, 100) | scale(minor, root:A1), dur:0.25) random, but A minor
synth(freq:Pwhite(40, 100) | notes([0, 3, 7], root:E1)) only E, G and B
synth(freq:Pwhite(40, 100) | notes([E, G, B])) ... the same, by name
synth(freq:Pwhite(40, 100) | edo(19)) 19 equal steps to the octave
synth(freq:Pwhite(40, 100) | harm(27.5)) overtones of a low A: 55, 82.5 ...
synth(freq:Pwhite(40, 100) | sieve(3@0 + 4@1, root:C1)) a Xenakis sieve
synth(freq:Pwhite(40, 100) | scale(minor, root:A1) | harm(55)) chained, left to right
synth(freq:Pwhite(40, 100) | scale(minor, root:A1, even)) each note of the range as likely
synth(freq:~E2 | edo(19)) one note, or a chord, through it too
After |, a pitch filter: every value drawn -- each step of a pattern,
a rand(), a note, a chord's notes -- goes to the nearest pitch it lets
through, then on to the next filter. The filters work in pitch, not Hz, so
the grid is as even at 40 Hz (where a semitone is 2 Hz) as at 4000.
| filter | lets through |
|---|---|
scale(name, root:) |
the notes of a scale -- the names scale: takes, or a list of semitones -- from root: (C4), in every octave |
notes([...], root:) |
these notes in every octave: numbers are semitones above root: (C4), names (E, G#) are those notes |
edo(n, root:) |
n equal steps to the octave, from root: (A 440) |
harm(f, max:) |
the overtones of f (Hz, or a note: harm(A0)): f, 2f, 3f ... up to the max:th (64) |
sieve(m@r + ..., root:) |
Xenakis's sieves on the semitones above root: (C4): 3@0 every third (0, 3, 6 ...), 4@1 every fourth from 1; + either, * both (3@0 * 2@0: every sixth) |
A filter moves a value to its nearest pitch, so a note with wide gaps
round it comes up more often (it catches more of the draws). even (in
any filter of the line) picks instead among the pitches the filters let
through between Pwhite's (or Pexprand's) two ends, each as likely.
octave: and stack: come after: the snapped note moves, and its stack
stays in exact intervals over it. The pipe is for freq:.
Out of tune on purpose -- drawn cents
synth(uid:lowsine, wave:sine, freq:[~E2+12, ~G2+rand(0, 25)], poly:3, amp:0.8)
synth(uid:drift, freq:Pseq(~E4+rand(-20, 20), ~G4+rand(-20, 20), ~B4, inf), dur:0.5)
A note's cents can be drawn: ~G2+rand(0, 25) is G2 up to a quarter of a
semitone sharp, -rand(0, 25) flat, +birand(5, 20) 5 to 20 either way.
Run as a line, it is drawn once a run (each Ctrl+Enter a new tuning, each
voice of a poly: its own); in a pattern, at every step.
Degrees of a scale — degree:, scale:, root:
synth(degree:Prand(0, 2, 4, 7, inf), scale:minor, root:~A3, dur:0.25)
synth(degree:Pseq([0, 2, 4], [3, 5, 7], inf), scale:dorian, dur:2) chords
synth(degree:Pseq(0, -1, -3, -5, inf), scale:[0, 3, 5, 7, 10]) your own steps
degree: in place of freq: counts steps of a scale, as in SuperCollider's
Pbind: 0 is the root, 1 the next note of the scale, and past the last note
the scale goes on an octave up (7 in a seven-note scale is the root an octave
higher, -1 the note below the root). A fraction lies between two notes, a
microtone. Everything freq: takes works on degrees: lists are chords,
patterns step, r rests, rand() and irand() inside a pattern draw a degree
each step, Pwhite(0, 7) a degree between.
| param | default | |
|---|---|---|
scale |
major |
major, minor, dorian, phrygian, lydian, mixolydian, locrian, harmonicMinor, melodicMinor, majPent, minPent, blues, whole, chromatic, hirajoshi -- or a list of semitones above the root, [0, 3, 5, 7, 10] (fractions for other tunings). |
root |
C4 |
Degree 0: a note (~A3, C#4), a MIDI number (m57) or Hz. |
Run the line again with another scale: or root: and the same degrees play
in it, on the next beat -- a change of mode or key in the middle of a pattern.
Every note doubled — stack:
synth(degree:Prand(0, 2, 4, 7, inf), scale:minor, root:~A2, dur:0.125, stack:[0, 7]) parallel fifths
synth(..., stack:[0, 3, 7]) a minor triad on every note
synth(..., stack:12) each with its octave: stack:7 is [0, 7]
stack: sounds every note with these above it, in semitones (0 the note
itself) -- exact intervals, so the fifths stay parallel whatever the scale
(for fifths that keep to the scale, write the chords in degree:, as
[0, 4]). With freq: too, a held note or chord, a pattern, each step. The
stacked notes go through the voice's sub:, drive: and fuzz: as the
note does.
Octave jumps — octave:
synth(degree:Prand(0, 2, 4, 7, inf), scale:minor, root:~A2, dur:0.125,
octave:Prand(0, 0, 1, -1, inf)) now and then an octave up or down
synth(freq:220, dur:0.25, octave:Prand(0, 1, -1, inf)) one note, three octaves
synth(freq:[220, 330], octave:-1) the lot an octave down
octave: moves each note up (or down) so many octaves; a pattern draws one
each step, apart from the notes' own pattern -- random octave displacement.
It moves stack:'s notes with the note, and hold() holds it too.
Chords
When freq is an array, multiple oscillators are created and summed. All voices share the same envelope and effects chain. A mixer gain node scales amplitude by 1 / sqrt(N) to prevent clipping.
synth(
uid:pad3,
wave:sine,
freq:[440, 477, 644, 777],
env:{a:1.5},
amp:0.12
)
Chord arrays can contain rand() expressions. Each element is evaluated independently at voice creation time:
synth(
uid:cluster,
wave:triangle,
freq:[rand(200, 400), rand(300, 600), rand(400, 800)],
env:{a:3, r:2},
amp:0.1
)
Retriggering from the live console re-evaluates all rand() calls, producing a new chord voicing each time.
Chord frequencies can be updated live. Existing oscillators ramp to new pitches, excess oscillators stop, and new oscillators start as needed.
Amplitude Envelope (ADSR)
synth(
uid:env1,
wave:saw,
freq:220,
amp:0.12,
env:{a:0.5, d:0.2, s:0.7, r:1.2}
)
Envelope parameters:
| Key | Default | Description |
|---|---|---|
a |
0.02 | Attack time in beats (ramp from 0 to peak) |
d |
0 | Decay time in beats (ramp from peak to sustain) |
s |
1 | Sustain level as fraction of peak amplitude (0--1) |
r |
0.1 | Release time in beats (ramp to silence on stop) |
The envelope uses linearRampToValueAtTime for the attack and decay stages. Sustain holds indefinitely until the voice is stopped (by explicit synthStop, dur timeout, or region exit), at which point the release ramp begins from the current gain value.
When both env and dur are specified, dur controls voice lifetime: the voice auto-stops after dur beats (seconds at the default 60 bpm), triggering the release envelope. This allows self-terminating voices with shaped envelopes:
synth(uid:swell, wave:sine, freq:220, env:{a:6, r:4}, amp:0.12, dur:12)
This voice fades in over 6 beats, sustains, then stops at 12 beats and fades out over 4 beats.
Per-Step Envelope (envPerStep)
By default, pitch sequences glide smoothly between notes with the envelope applied only at the start and end of the entire sequence. For articulated sequences where each note should have its own attack-decay-sustain-release cycle, enable envPerStep:
synth(
uid:melody,
wave:triangle,
freq:Pseq(~C4, ~E4, ~G4, ~C5),
dur:0.5,
env:{a:0.05, d:0.1, s:0.6, r:0.15},
envPerStep:true,
amp:0.1
)
Long releases over short notes — overlap:
Each step's envelope has to fit inside the step: an r: longer than the
step is squeezed (the attack never under 5 ms, so it never clicks; the
decay and the release take what is left). For notes that ring on under the
ones after them, give the line overlap:N -- each step is a note of its
own, up to N sounding at once:
synth(uid:s6, wave:sine, scale:minor, degree:Prand(0, 2, 4, 3, inf), dur:Prand(0.5, 0.25, 1, inf),
envPerStep:{a:0.005, d:0.3, s:1, r:5.1}, overlap:3)
Each note holds to the end of its step, then dies away over its whole r:.
When N are sounding, the one ending soonest fades out in 8 ms for the new
one. With glide:, each new note slides from the last note's pitch to its
own over that many beats, while the notes before it ring on where they are. The notes are each at 1/√N (as a chord's are), so piled up they keep
their headroom; all of them still go through the line's sub:, drive:,
fuzz:, filter and effects -- like several notes into one pedal.
With envPerStep:true, each pitch step triggers a complete ADSR cycle:
- Attack ramps gain from 0 to peak
- Decay ramps from peak to sustain level
- Sustain holds until release begins
- Release ramps to silence, timed to complete as the next step starts
The envelope can be given right there:
synth(freq:Pwhite(60, 190), dur:0.1, envPerStep:{a:0.003, d:0.04, s:0, r:0.01}) choppy
For a choppy line the sustain (s) is what matters: with s:0 each step is
only its attack and decay, a click with a short tail; s:1 (the default) holds
every step at full level to the next, and nothing chops. Reverb and a long
delay fill the gaps again.
Envelope Compression
If the step duration is shorter than the total envelope time (attack + decay + release), the envelope phases are compressed proportionally to fit within the step. This ensures smooth transitions even at fast tempos.
Comparison
| Mode | Behaviour |
|---|---|
envPerStep:false (default) |
Legato: pitches glide smoothly, envelope only at start/end |
envPerStep:true |
Articulate: each pitch gets full ADSR, distinct note attacks |
env:{...} and envPerStep:{...} |
Each note its envPerStep:, the whole sound env: -- a swell in at the start and seq(s, 1), the last note fading out at seq(s, 0) (seq()) |
Polyphony
The poly parameter controls how retriggers with the same uid behave.
| Value | Behaviour |
|---|---|
poly:0 (default) |
Mono mode. Retrigger updates the running voice in place |
poly:N |
Up to N overlapping voices. Oldest voice is stopped when count exceeds N |
In mono mode, retriggering with the same uid calls updateSynthVoice -- parameters change smoothly without restarting playback or envelope. This is the default and is suitable for live console tweaking.
In poly mode, each trigger creates an independent sub-voice with its own oscillator(s), envelope, and effects chain. Sub-voices are internally named uid__0, uid__1, etc. The dur parameter and rand() expressions are especially useful in poly mode, since each voice evaluates its own random values and manages its own lifetime.
Example: Overlapping Swells
synth(
uid:swell,
wave:triangle,
freq:[rand(33, 333), rand(33, 533), rand(33, 1333)],
env:{a:6, r:4},
amp:0.12,
dur:12,
poly:12
)
Each trigger creates a new 3-oscillator chord with random frequencies, fading in over 6 beats. Up to 12 can overlap. The 13th trigger stops the oldest voice (with its release envelope). All poly voices share a single scope display rect in the GUI.
Stopping by hand
Unless the cue has click:0, the element answers to the finger or mouse:
a tap (or click) on a playing synth toggles it off (a second tap starts it
again), and a press held for half a second stops it — every voice of a
poly group at once. Works the same on a touch screen: a finger on a synth
never starts a seek.
Stopping Poly Voices
synthStop(uid:swell) stops all sub-voices in the group. Each voice fades out independently using its own release time.
Patterned Parameters
The following parameters accept static values, arrays, or pattern functions. When patterns are present, a step engine advances through values at the rate specified by dur.
| Parameter | Description |
|---|---|
freq |
Pitch sequence |
amp |
Amplitude sequence |
dur |
Step duration sequence |
filter.freq |
Filter cutoff sequence |
filter.q |
Filter resonance sequence |
pan |
Stereo position sequence |
Frequency Sequence
synth(
uid:seq1,
wave:triangle,
freq:Pseq(220, 330, 440),
dur:Pseq(1, 1, 2),
amp:0.08
)
Patterned Chord Progression
synth(
uid:chordSeq,
wave:saw,
freq:Pseq(
[220, 330, 440],
[247, 370, 494],
[196, 294, 392]
),
dur:1.5,
amp:0.1
)
Random Pitch Selection
synth(
uid:randFreq,
wave:square,
freq:Prand(200, 400, 600, inf),
dur:0.8,
amp:0.09
)
Available Patterns
| Pattern | Behaviour |
|---|---|
Pseq(...) |
Sequential, loops forever |
Prand(...) |
Random selection per step (may repeat) |
Pxrand(...) |
Random selection, never same value twice in a row |
Pshuf(...) |
Shuffle once, then repeat the shuffled order |
Pwhite(lo, hi, n) |
A random value between lo and hi, n times (inf, or leave it out: for ever) |
Pexprand(lo, hi, n) |
The same, spread evenly across octaves: for pitch, as many values between 100 and 200 Hz as between 1000 and 2000 |
A trailing number is the repeat count. Prand(200, 400, 600) chooses
between 200 and 400, 600 times; to choose among three, end with the count:
Prand(200, 400, 600, inf). (Pwhite and Pexprand are the exception:
Pwhite(100, 1000) is the range, for ever.)
Lists are chords. Prand([220, 330], inf) gives the pattern one value,
the chord 220 + 330, every step; Prand(220, 330, inf) chooses between
the two.
On the beat — quant
synth(uid:a, dur:0.25, freq:Pseq(220, 330, 440, inf), quant:1) next beat
synth(uid:b, dur:0.12, freq:Pexprand(100, 2000, inf), quant:4) next bar
synth(uid:c, dur:0.5, freq:Pwhite(60, 90, inf), quant:0) at once
As quant in SuperCollider: a pattern starts on the next multiple of quant
beats of a shared beat grid. dur: is in beats too, so patterns with dur:
values that divide a beat line up with each other. The grid is counted on the
server's clock: the projector window, and every machine joined to the score,
keep the same beats.
Tempo — tempo()
tempo(120) 120 beats a minute, from the next whole beat
tempo(bpm:90) the same, named
tempo() says the tempo (in the browser's console)
The grid runs at 60 bpm until told otherwise, so a beat is a second and
dur:0.25 a quarter of one. tempo(120) makes every beat half a second from
the next whole beat on: a dur:0.25 pattern already playing steps twice as
fast, a quant:4 still lands on a bar, and the lifetime of a plain
synth(dur:2) is two beats. A change from the live console starts on the same
beat in the projector window. Every other time in the DSL counts these beats
too -- the animation cues' dur:, fades, holds (see
Time is counted in beats).
- From the live console a pattern defaults to
quant:1. Run lines at any moment and they come in on the beat, in step. A plain synth with no pattern starts at once. - From the score (the playhead reaching it) a synth starts where the
playhead is, unless it has a
quant:. - Running a line again replaces its voice on the beat too.
- Steps are scheduled on the audio clock at exact times, so patterns stay in step for as long as they run.
Random Values Inside Patterns
Pattern steps — including chord members — can be rand() / irand() /
birand() expressions. Two evaluation modes are available:
| Function | Evaluation |
|---|---|
rand(a, b) |
Re-evaluated every pass — a fresh value each time the step comes around |
hrand(a, b) |
"Held random" — evaluated once per trigger, then constant across passes |
// The first chord's bottom note re-rolls between 100–300 Hz on every pass:
synth(freq:Pseq([rand(100,300), 330, 440], [247, 370, 494], [196, 294, 392]), dur:0.3)
// One random detune chosen when the cue fires, held for the whole run:
synth(freq:Pseq([hrand(215,225), 330], [247, 370]), dur:0.5)
Works for freq, dur, and amp patterns, and for speed patterns on the
audio cues. Reminder: a trailing bare number in a pattern is read as the
repeat count — write Prand(0.25, 2, 4, inf) if 4 is meant as a value.
Scaling a pattern — *k, +=k
synth(uid:h, wave:hat, dur:0.125, amp:Prand(0.01, 0.1, inf)*0.7) every value times 0.7
synth(uid:b, freq:Pseq(55, 82, 110, inf)*1.5) a fifth up
synth(uid:b, dur:Pwhite(0.25, 1, inf)*0.5, filter:{type:lp, freq:Pseq(400, 800, inf)*2})
synth(uid:b, freq:Pseq(55, 82, inf)+=12) 12 Hz up
*k (or *=k), +=k and -=k after a pattern change every value it draws,
in order -- numbers and chords (a note name passes as it is). The values stay
as written, so the shape of the pattern is kept.
On a sound already playing, the same written alone scales the pattern it has
from now on -- like level() for loudness, but for any parameter:
synth(uid:b, freq:*2) an octave up, the pattern going on
synth(uid:b, dur:*0.5) twice as fast
synth(uid:pads, amp:*0.7) a group: each from where it is
(For loudness, level(b, 0.7) is the fader -- it leaves amp: alone.)
Holding a pattern — hold()
hold(s) s's patterns stay where they are; the rhythm goes on
hold(s, 0) they carry on from there
hold([a, b], 1) several; hold(all, 0) every one
hold(bsc) an animation by its uid (bass > scale(uid:bsc, ...)): it stops
hold(bsc, 0) where it is, and goes on from there
In the console, Ctrl+_ (Ctrl+Shift+-) on a synth's line is hold() and,
pressed again, hold(s, 0): the line under the cursor frozen on its note
and let go (on a #$ head, every synth of the block). On an animation's
line -- bass > scale(values:[0.1, 1], dur:5.2), shape(uid:sh1) rotate(dur:4)
-- it stops the animation where it is, and the next press lets it carry on
from there (with no uid:, the console finds it by what the line animates).
hold(s) (or hold(s, 1) -- one character to change) freezes every
pattern on the sound on the value it drew last: the note, amp:, dur:,
wave:, the filter, pan:. The steps keep coming, so a struck synth keeps
striking the same note and a held one holds it. hold(s, 0) lets them go on
from where they stopped. A poly: sound holds every voice, and any it starts
while held; hush() lets go of what it stops.
It holds a sound's time too: a voice with dur: (or stopAfter:) keeps
sounding while held, and let go has the time it had left. A chord you like,
kept past its end:
synth(uid:s3, wave:sine, freq:[rand(20, 80), rand(30, 1000)], poly:12,
dur:rand(6, 20), env:{a:2, r:3})
hold(s3) it stays
hold(s3, 0) each voice ends when its dur: would have, the held time not counted
(gate(s, 0) is something else: it lets a sound go silent, still running.)
Ready, not playing -- trig:wait
synth(uid:xsine, wave:sine, freq:rand(4000, 8000), amp:0.1, trig:wait)
solo(xsine, 2.5) the sine starts, alone, for 2.5 beats -- then goes
bang(xsine) ... or starts it to play on
A line with trig:wait (trig:solo says the same) is learnt but not
played: run its whole block and everything else starts, this one waits.
solo() or bang() starts it from that line, rolling its rand()s. The
console tags its line waiting while it is silent. Run it to play by
hand: take the trig:wait out, or bang() it.
Rolling it again — bang()
bang(s3) the line s3 was last run with, its rand()s rolled afresh
bang([a, b]) several; bang(all) every one
Pd's bang: a sound's line run again, every voice sounding re-rolling its
rand()s -- a chord built up with Ctrl+Enter becomes a new chord of as many
notes, through the envelope (each releases on its r: as the new ones come
in on their a:), as Ctrl+Alt+Enter on the line would. One short line to
run again and again, wherever the synth's line is in the code. A sound not
sounding starts again as its line would.
A second argument makes it a metro, in beats, like dur::
bang(s3, 4) now, then every 4 beats
bang(s3, Pwhite(1, 5, inf)) the time to the next drawn each bang
bang(s3, rand(2, 8)) likewise
bang(s3, 0) the metro stops -- hush(s3) and Ctrl+Shift+. stop it too
Run again, a metro replaces the one going; a pattern that runs out
(Pseq(4, 2, 1)) stops it.
Letting go and pressing again — gate()
gate(s, 0) let go: s fades out over its own release (env r)
gate(s, 1) press again: it fades in over its attack (env a)
gate(s, 0, 4) over 4 beats instead
gate(s, 1, 6, exp3) along a curve (lin exp2 exp3 exp4 log sqrt)
gate([a, b], 0) several; gate(all, 1) every one
As a keyboard's key. Let go, the sound dies away before its effects, so
reverb and delay ring on, and the voice runs on silent -- its patterns,
hold(), everything where it was. Pressed again, it comes back: nothing is
restarted or remembered. A poly: sound gates every voice, and any it starts
while closed. Where hush() stops a sound and level() turns it down
(after its effects, so the tail goes too), gate() is the one for "stop
playing, let the room ring, come back later". hush() opens it again.
A paused sound still runs, and costs: in the live console its lines dim,
tagged paused, and resources() counts it -- so it is not left there
forgotten. A held one (hold()) is tagged held; one a metro is banging
(bang(s, 4)) is tagged active, and so is the bang line, and
resources() counts the metros going.
Stopping the sequence after its note — seq()
seq(s, 0) s finishes the note it is on and stops there
seq(s, 1) on again from the next beat, its patterns where they were
seq([a, b], 0) several; seq(all, 1) every one
No cut and nothing left sustaining: the note playing ends on its own
envelope (envPerStep:), or a note held from step to step dies away over
env:'s r:, and then the sound waits, silent -- unlike gate(s, 0), its
sequence does not run on. Effects ring on. Ctrl+Enter turns seq(s, 0) into
seq(s, 1) for the next press, the sound's lines dim, tagged stopped, and
running the sound's own line again starts it too. hush() forgets it.
With both envelopes, envPerStep: shapes each note and env: the whole
sound: it swells in over env:'s a: when it starts (and at seq(s, 1)),
and at seq(s, 0) no note comes after the one sounding, which is not
released but rings on and dies away over env:'s r:. gate() takes its
times from it too.
synth(uid:s1, degree:Prand(0, 2, 3, 5, inf), scale:minor, root:A3, dur:0.5,
env:{a:2, r:4}, envPerStep:{a:0.01, d:0.1, s:0.66, r:0.05})
seq(s1, 0) the last note rings, fading over 4 beats
hush(s1) (Ctrl+Shift+. on its line) the pattern plays on, fading over 4 beats
Stopped -- hush(s1), or Ctrl+Shift+. on its line -- a stepping sound
fades out over env:'s r: (not envPerStep:'s, the end of each note)
with its pattern going on under the fade until it is silent; hush(s1, 2)
over 2 beats instead. A release under about a third of a beat stops the
pattern at once. Running the line again during the fade starts afresh, and
hush() (Ctrl+.) cuts everything, fading or not.
Rests
Use r or rest in frequency patterns to insert silences. The synth glides smoothly to inaudibility during rests, then resumes with the next pitch.
synth(
uid:melody,
wave:sine,
freq:Pseq(~C4, ~E4, r, ~G4, r, ~C5),
dur:1,
envPerStep:true,
amp:0.08
)
Rest values: r, ~r, rest, ~rest (all equivalent, case insensitive).
Rests work with any pattern type:
synth(
uid:sparse,
wave:triangle,
freq:Pxrand(~A3, ~C4, r, ~E4, r, ~G4),
dur:Prand(2, 3, 5),
env:{a:0.5, r:1},
envPerStep:true,
amp:0.06
)
Nested Patterns
Patterns can be nested inside other patterns for complex generative sequences. The outer pattern controls the overall structure, while inner patterns provide variation at specific steps.
Basic Nesting
A Pseq with a nested Prand plays the sequence in order, but randomly selects from the inner pattern's values when it reaches that step:
synth(
uid:nested1,
wave:sine,
freq:Pseq(~A4, Prand(~C4, ~D4, ~E4), ~G4),
dur:2,
amp:0.08
)
This plays: A4 → (random choice of C4/D4/E4) → G4 → repeat.
Multiple Nested Patterns
synth(
uid:nested2,
wave:triangle,
freq:Pseq(
Prand(~C3, ~D3, ~E3),
~G3,
Pxrand(~A3, ~B3, ~C4)
),
dur:Prand(1, 2, 3),
amp:0.07
)
Nesting Depth
Patterns can be nested multiple levels deep:
synth(
uid:deepNest,
wave:sine,
freq:Pseq(
~A3,
Prand(
~C4,
Pseq(~D4, ~E4, ~F4)
),
~G3
),
dur:1.5,
amp:0.06
)
Chord Arrays in Patterns
Patterns can contain chord arrays (multiple simultaneous pitches) alongside single notes. Each step can be either a single pitch or a chord.
Basic Chord Sequence
synth(
uid:chordSeq,
wave:sine,
freq:Pseq(
[~C3, ~E3, ~G3],
[~F3, ~A3, ~C4],
[~G3, ~B3, ~D4],
[~C3, ~E3, ~G3]
),
dur:3,
env:{a:1, r:2},
envPerStep:true,
amp:0.07
)
Mixed Single Notes and Chords
Alternate between single pitches and chords within the same pattern:
synth(
uid:mixed,
wave:sine,
freq:Pseq(
~A3,
[~C4, ~E4, ~G4],
~G3,
[~F4, ~A4, ~C5]
),
dur:2,
amp:0.06
)
Random Chord Selection
Use Prand to randomly select between different chord voicings:
synth(
uid:randChords,
wave:sine,
freq:Prand(
[~C3, ~E3, ~G3],
[~D3, ~F3, ~A3],
[~E3, ~G3, ~B3],
[~F3, ~A3, ~C4]
),
dur:Prand(5, 7, 11, 13),
env:{a:2, r:3},
envPerStep:true,
amp:0.06
)
Nested Patterns with Chords
Combine nesting and chords for maximum flexibility:
synth(
uid:nestedChords,
wave:sine,
freq:Pseq(
Prand(
[~C3, ~F#3, ~B3, ~E4],
[~D3, ~G3, ~C4, ~F4],
[~E3, ~A3, ~D4, ~G4]
),
Prand(~C4, ~D4, ~E4, ~F4, ~G4)
),
dur:Prand(5, 7, 11, 13),
env:{a:1.5, r:2.5},
envPerStep:true,
amp:0.06
)
This alternates between:
- A random chord (from three options)
- A random single pitch
Random Expressions
rand(min, max) and irand(min, max) can be used for any numeric parameter. They are evaluated at voice creation time for static params.
synth(uid:x, wave:sine, freq:rand(200, 2000), amp:rand(0.02, 0.12))
rand() returns a continuous float. irand() returns an integer. Both work inside chord arrays, filter configs, and anywhere a numeric value is expected.
For patterned parameters, random values evaluate once when the pattern is built. For per-step randomness, use Prand(...).
Random Expressions in Chords
rand() expressions inside freq arrays are evaluated individually per oscillator per trigger:
synth(uid:x, wave:sine, freq:[rand(100, 500), rand(200, 800), rand(300, 1200)])
Each trigger produces three oscillators at different random frequencies. Retriggering from the live console rolls new values each time.
Filters
synth(
uid:filterSeq,
wave:saw,
freq:330,
filter:{type:lp, freq:Pseq(400, 800, 1600, 800), q:0.7},
dur:0.5,
amp:0.08
)
Filter parameters:
| Key | Default | Description |
|---|---|---|
type |
lp |
Filter type: lp, hp, bp, notch |
freq / cutoff |
1200 | Cutoff frequency. Accepts rand() or signal ref |
q / resonance |
1 | Resonance. Accepts rand() or signal ref |
Delay
synth(
uid:delayLead,
wave:square,
freq:550,
delay:{time:0.25, fb:0.35, mix:0.2},
amp:0.12
)
| Key | Default | Description |
|---|---|---|
time |
0.3 | Delay time in beats |
fb |
0.3 | Feedback amount 0--0.95 |
mix |
0.3 | Wet/dry mix 0--1 |
Reverb
synth(
uid:revDrone,
wave:sine,
freq:110,
reverb:{mix:0.3, time:2, damp:3000},
amp:0.07
)
| Key | Default | Description |
|---|---|---|
mix |
0.3 | Wet/dry mix 0--1 |
time |
1.5 | Reverb time (controls delay line lengths) |
damp |
4000 | Damping lowpass frequency in Hz |
Glide
synth(
uid:glide1,
wave:sine,
freq:Pseq(220, 330, 440),
glide:0.1,
dur:1,
amp:0.1
)
glide specifies the time in beats used to interpolate frequency changes between steps. It applies only to pitch. Default is 0.02 (near-instant). Set higher values for audible portamento.
Interpolation Mode
synth(
uid:stepSeq,
wave:triangle,
freq:Pshuf(300, 450, 600),
interp:step,
dur:1,
amp:0.08
)
| Value | Behaviour |
|---|---|
smooth (default) |
Ramps between values using setTargetAtTime |
step |
Immediate value changes using setValueAtTime |
Pan
synth(uid:panTest, wave:sine, freq:440, pan:-0.6, amp:0.09)
Pan accepts static values, rand() expressions, patterns, or signal references. Range is -1 (left) to 1 (right).
Lifetime and Duration
Gate Mode (default)
When no dur is specified, the synth operates in gate mode: it plays while the playhead is inside the cue element's region and releases when the playhead exits. This is ideal for sustained tones that follow the score timeline.
synth(uid:regionTone, wave:sine, freq:220, amp:0.06)
The synth starts on playhead entry, sustains indefinitely, and triggers its release envelope on playhead exit. If the cue element has an extent handle (for waveform-line or handle display modes), the region boundary extends to that handle position.
Explicit duration
synth(uid:fixedDur, wave:sine, freq:220, dur:5, amp:0.07)
The synth auto-stops after 5 beats. If an envelope is specified, the release ramp begins at the stop point. This switches lifetime to process mode internally.
Process lifetime
synth(uid:persist, wave:saw, freq:110, lifetime:process, amp:0.05)
The synth continues until explicitly stopped with synthStop(uid:persist).
Extent Handle Support
When using waveform-line or handle display modes, the cue element has a draggable extent handle that defines the right boundary of the region. Synths with region-based lifetime (gate mode) respect this extended boundary -- they continue playing until the playhead exits past the extent handle, not just the original element bounding box.
Parent Group Extent (SVG-Authored Cues)
For SVG-authored cues where the synth DSL is placed directly in an element's id attribute (authored in Inkscape or another SVG editor), the extent region can be defined by a parent group. If the synth element is placed inside a group (<g>) that contains an extent path or line, the playhead uses the group's bounding box rather than just the synth element's box.
This enables workflows where the synth rect and an extent line are grouped together in the SVG editor, with the group defining the active region:
<g id="g20">
<rect id="tone" data-oscilla="synth(wave:sine, freq:440, amp:0.1)" ... />
<path d="m 100,50 h 500" /> <!-- extent line -->
</g>
The synth remains active until the playhead exits the group's combined bounding box.
Duration vs. Step Engine
When pattern parameters are present (making _hasStepEngine true), dur controls step timing rather than voice lifetime. Use stopAfter or lifeDur to set an explicit voice lifetime independently of step duration:
synth(uid:seq, wave:saw, freq:Pseq(220, 330, 440), dur:0.5, stopAfter:10, amp:0.08)
This steps through frequencies every 0.5 beats and stops the entire voice after 10 beats.
Signal Binding (Control Plane)
Synth parameters can be bound to real-time signal sources such as controlXY faders, o2p animations, rotate, and scale cues. Bound parameters update continuously at audio rate.
Syntax
param: follow(source.channel, min, max)
The source is the uid of a publishing cue, and the channel specifies which signal dimension to follow. The optional [min,max] range maps the 0--1 signal to the specified output range.
Bindable Parameters
| Parameter | Default Range | Description |
|---|---|---|
freq |
20--20000 | Oscillator frequency |
amp |
0--0.5 | Output amplitude |
pan |
-1 to 1 | Stereo position |
cutoff / filterFreq |
20--20000 | Filter cutoff frequency |
q / resonance |
0.1--30 | Filter resonance |
Examples
synth(uid:pad, freq: follow(myFader.t, 200, 800), amp:0.15)
synth(uid:drone, freq:220, amp: follow(slider.y, 0, 0.25), pan: follow(slider.x, -1, 1))
synth(uid:filter1, wave:saw, freq:440, filter:{type:lp, freq: follow(knob.t, 200, 8000), q:2})
// (filter freq: and q: follow live -- a follow() inside {…} is a binding too)
When a signal ref is used for freq, chord mode is disabled (binding applies to a single oscillator). Static values work alongside bound values: you can bind freq while keeping amp static, or vice versa.
Bindings are automatically cleaned up when the voice stops.
What a synth publishes
Each step puts the synth's pitch on the signal bus, under its own name (a
poly: synth's voices all as that name), for anything to follow():
| Channel | Meaning |
|---|---|
freq |
the step's pitch, Hz (a chord: its first note) |
amp |
the step's amp |
t |
where the pitch is between the lowest and highest it has played, 0..1 -- in pitch, every octave an equal share |
env |
its envelope, 0..1: where each note's a d s r has got to -- unflattened by fuzz: or drive: |
level |
how loud it is, 0..1 of its own loudest lately (rms: unscaled) |
hit |
1 for about 0.1 s at each step |
n |
how many steps so far |
t is the one to map with: follow(name.channel, lo, hi) reads its channel
as 0..1, so follow(bass.t, 0.2, 1.5) goes 0.2 at the lowest note to 1.5 at
the highest.
synth(uid:bass, freq:Pwhite(60, 90), dur:0.2)
bass > scale(follow(bass.t, 0.2, 1.5)) the box grows with the pitch
OSC Output
osc:1 mirrors the voice's life cycle to the OSC out port; oscAddr: replaces the default address.
/oscilla/synth/<uid> state(string) freq amp dur wave(string)
state |
when | dur |
|---|---|---|
start |
voice created | lifetime in seconds (0 = gate mode) |
step |
each pattern step | step length in seconds, at the tempo |
update |
amplitude / lifetime changed | lifetime |
stop |
released | release time |
Example receivers for Pd, Max and SuperCollider: OSC Output.
Oscilloscope Display
Each synth voice renders a real-time oscilloscope trace driven by a Web Audio AnalyserNode. The scope shows the time-domain waveform shape updating at display framerate.
Display Modes
waveform value |
Behaviour |
|---|---|
self (default) |
Render scope inside the cue element |
none |
Suppress scope display |
<element_id> |
Render inside a different SVG element by id |
Scope Stacking
Multiple synths can share a single display area. When waveform points to another synth's uid, the scope renders into that synth's rect with a distinct colour:
synth(uid:pad1, wave:sine, freq:[740, 477, 644], amp:0.12)
synth(uid:pad2, wave:square, freq:880, waveform:pad1, amp:0.1)
pad2's scope stacks into pad1's display element with a different colour from the 12-colour palette.
Poly Scope Display
All poly voices for a given uid share a single display rect. The rect is created when the first voice triggers and persists across group recreation. Each voice gets its own scope trace in a distinct colour, and traces are cleaned up when their voice stops.
Info Text
A text line above the scope shows the voice's current parameters: uid, waveform type, frequency/chord voicing, amplitude, and any active effects. This text updates when parameters change via live console.
Pattern parameters render in full: the currently sounding step is highlighted in red, chords keep their brackets, rand()/hrand() appear by name, and the repeat count shows as a suffix — Pseq[[196,294],[220,330]]×inf, dur Prand[0.15,0.3,0.6]×inf.
Live Console Scopes
When synth cues are triggered from the live console without a score element, an ephemeral SVG rect is created in the viewport as a scope anchor. These rects stack vertically and can be dragged and resized. They persist until the global stop button clears them.
OSC Mirroring
synth(
uid:oscLead,
osc:1,
oscAddr:/synth/lead,
wave:saw,
freq:440,
amp:0.1
)
When osc:1 is enabled, synth state changes (start, stop, parameter updates) are sent as OSC messages to external software via the Oscilla server.
Stopping
synthStop(uid:seq1, rel:0.5)
The rel parameter overrides the envelope release time. Without it, the voice's env.r value is used.
Voices can also be stopped by:
- Region exit (lifetime:region)
- Duration timeout (
durorstopAfter) - Transport stop/rewind (stops all voices)
stopSynth(uid)from JavaScript- Click-to-toggle (clicking a playing synth stops it)
Click-to-Trigger
Synth cues are click-triggerable. Clicking the synth element in the score starts the synth immediately, regardless of playhead position.
By default, clicking an already-playing synth stops it (toggle behaviour). This can be controlled with the toggle parameter:
toggle value |
Behaviour |
|---|---|
| (omitted) | Toggle on click, retrigger on playhead |
toggle:true |
Always toggle (click and playhead) |
toggle:false |
Never toggle, always retrigger |
synth(wave:sine, freq:440, amp:0.1, toggle:false)
This synth will layer multiple instances on repeated clicks instead of toggling.
For poly voices, synthStop(uid:X) stops all sub-voices in the group. Each voice fades out independently using its own release time.
Live Console
Hot-Update (Mono Mode)
In mono mode (default, poly:0), retriggering a synth from the live console with the same uid updates the running voice without restarting it. The following parameters update immediately:
| Parameter | Behaviour |
|---|---|
amp |
Gain ramp via setTargetAtTime |
pan |
Pan ramp on StereoPannerNode |
wave |
Oscillator type change (instant) |
freq (single) |
Frequency ramp with glide |
freq (chord) |
Ramp existing oscs, stop excess, create new |
filter.freq |
Cutoff ramp |
filter.q |
Resonance ramp |
lifetime |
Switch between region/process |
Pattern parameters (Pseq, Prand, etc.) are rebuilt on update. Duration and stop-after timers are reset.
Poly Mode from Console
With poly:N, each execution from the live console creates a new overlapping voice. The rand() expressions re-evaluate on each trigger, so repeated execution of the same line produces different results:
synth(uid:cloud, wave:sine, freq:[rand(100,2000), rand(100,2000)], env:{a:4, r:6}, dur:8, poly:20)
Execute this line repeatedly to build up a cloud of overlapping two-note chords, each with random pitches, each fading in over 4 beats and releasing over 6 beats after the 8-beat lifetime.
Ctrl+Alt+Enter instead gives the line to every voice still sounding. Held voices (a chord) are replaced: each releases on its r: while as many new voices come in on the line's a:, each rolling its own rand() -- a chord built up with Ctrl+Enter crossfades to another chord of as many notes. Stepping voices take the line in place, each on its own step (its next notes have their envelopes), adding none -- a whole poly part transposed in one go:
synth(uid:bass, wave:saw, poly:4, freq:Pwhite(40, 100), dur:0.1)
synth(uid:bass, wave:saw, poly:4, freq:Pwhite(80, 200), dur:0.1)
Signal Chain
The internal audio graph for each voice is:
source (oscillator or noise buffer)
-> filter (optional, BiquadFilterNode)
-> gain (GainNode, envelope target)
-> panner (optional, StereoPannerNode)
-> delay (optional, feedback delay)
-> reverb (optional, multi-tap reverb)
-> analyser (AnalyserNode for scope display)
-> destination
For chords, all oscillators feed through a shared mixer gain node (scaled by 1/sqrt(N)) before the filter stage.
Examples
// Simple reference tone
synth(uid:ref, wave:sine, freq:~A4, amp:0.08)
// Drone with slow attack
synth(uid:drone, wave:saw, freq:55, env:{a:3, s:0.6, r:2}, amp:0.1)
// Chord with envelope
synth(uid:pad, wave:sine, freq:[440, 550, 660], env:{a:2, d:0.5, s:0.7, r:3}, amp:0.12)
// Filtered sequence
synth(uid:bass, wave:square, freq:Pseq(55, 82, 110), dur:0.5,
filter:{type:lp, freq:Pseq(200, 800, 1600), q:2}, amp:0.1)
// Random chord cloud (poly, from live console)
synth(uid:cloud, wave:triangle,
freq:[rand(100, 1000), rand(200, 1500), rand(300, 2000)],
env:{a:5, r:3}, dur:10, amp:0.08, poly:16)
// Signal-bound drone (controlled by fader)
synth(uid:ctrlDrone, wave:saw, freq: follow(fader1.t, 80, 800),
amp: follow(fader1.y, 0, 0.2), filter:{type:lp, freq: follow(knob1.t, 200, 4000)})
// Noise burst with delay
synth(uid:noiseBurst, wave:noise, dur:0.1,
delay:{time:0.15, fb:0.6, mix:0.4}, amp:0.15)
// Patterned pan movement
synth(uid:ping, wave:sine, freq:1200, dur:0.3,
pan:Pseq(-1, -0.5, 0, 0.5, 1), amp:0.06)
// Self-terminating swell with long envelope
synth(uid:swell, wave:sine, freq:rand(100, 400),
env:{a:8, r:6}, dur:15, amp:0.1)
Summary
The synth() cue provides a bounded, score-aligned sound source suitable for reference tones, drones, chords, patterned textures, and signal-controlled processes. All behaviour is deterministic and cue-scoped. Polyphony enables overlapping voices with independent envelopes and random parameters. The oscilloscope display provides real-time visual feedback. Signal binding connects synth parameters to Oscilla's control plane for continuous modulation from faders, animations, and other score elements.
See Also
- audio -- file-based audio playback
- audioPool -- one-shot selection from a folder
- audioImpulse -- stochastic repeating process
- Control and Modulation -- signal binding reference
Demo
See demo-synth — waveforms, envelopes, chords, patterns and parameters bound to a fader.
http://localhost:8001/?project=demo-synth
Tip: use ← → or ↑ ↓ to navigate the docs