Score

synth() -- Web Audio Synth Cue

Oscilla "Native WebAudio Utility Synth"

Oscilla 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.

demo-synth — a classic 8-step sequence (a Pseq of note names) with its scope
demo-synth — a classic 8-step sequence (a Pseq of note names) with its scope

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:

  1. Attack ramps gain from 0 to peak
  2. Decay ramps from peak to sustain level
  3. Sustain holds until release begins
  4. 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).

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:

  1. A random chord (from three options)
  2. 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:


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

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