An interactive, hierarchical MIDI sequencer that runs in a terminal. You describe music as tracks → patterns → sequences → phrases, play it to a MIDI output port, and modulate parameters with up to 8 global LFOs.
A pattern can play notes, send control-change (CC) data, play positions
inside a .wav (loop patterns) or drive SuperCollider synths (osc
patterns). For the last one you can author the SynthDefs yourself, in the CLI:
write the voice and declare its parameters, check them, send them to sclang from
the synthdef menu, and play them with synth / fx — see 4.6.
Everything is edited with short commands; there is no file format to learn beyond
save/load.
seq> t1p1s0 0 2 4 r
seq> t1p1s1 5*lfo1 7
seq> t1p1f1 inf*(s0+s1)
seq> t1p1v0 90
seq> t1p1sus0 0.5+0.5*lfo2
seq> t1p1 division 1/8
seq> start t1p1f1
Requirements: Python 3.12+ on Windows (MIDI output uses winmm through ctypes;
no third-party packages needed).
python seq.py # start the interactive shell
python seq.py help # print the root command list and exitUseful one-shot command-line helpers (see COMMAND_SCRIPTS): if additional
script wrappers are present in the folder (e.g. list-ports.py) they can be run
as python seq.py list-ports.
If no MIDI port is selected yet, start playing will tell you:
No MIDI output port selected. Use 'select-port'….
-
On Windows without
readline, the shell uses a built-in line editor: Tab completes commands, Up/Down recall history, Left/Right/Home/End/Del edit, Ctrl+C cancels the line, Ctrl+D / Ctrl+Z send EOF. -
Tab completion works across menus and paths, not just for the current shell's commands:
typed Tab completes to / t1p2 chan/ t1p2 channel/t1p2 cont/t1p2 controllert1p2 div(from any menu)t1p2 division/lfo1 frequ/lfo1 frequencycp lfo1 lfocp lfo1 lfo2…lfo8rm lform lfo1…lfo8hist/hist flive once stop static/f1 f2 …scale(pattern menu)scale names sh(LFO menu)shape show -
ANSI colours are enabled automatically; the histograms use Unicode block glyphs when the output can carry them, ASCII otherwise.
-
Use a reasonably wide window: the histogram adapts its column count to the terminal width.
| Prompt | Menu | What lives here |
|---|---|---|
seq> |
main | global BPM, port, LFO entry, save/load, panic, show, play/stop by path |
seq:t1> |
track | 16 patterns |
seq:t1p1> |
pattern | sequences, phrases, note/CC settings, LFO value sequences, hist |
seq:lfo:1> |
LFO | one oscillator's frequency, shape, phase, start/stop, live view |
seq:synthdef> |
SynthDefs | list the definitions, author or load one, send them to SuperCollider (see 4.6) |
seq:def:mine> |
SynthDef editor | one definition: its controls, body, check, install, test (see 4.6) |
| Input | Effect |
|---|---|
t1 |
enter track 1 (from the main menu) |
t1p2 |
enter pattern 2 of track 1 — from any menu |
p3 |
inside a track menu: switch to pattern 3 in place |
p3 |
inside a pattern menu: switch to pattern 3 in place |
s2, f1, v0, sus0, mt0 |
touch a leaf of the current pattern |
synthdef |
enter the SynthDef menu from any menu (see 4.6) |
def rob1 |
open that definition in the editor from any menu (def alone shows the usage) |
lfo2, lfo 2 |
enter an LFO menu (also lfo2 start, lfo2 shape square without entering) |
/ |
return to the main menu (seq>) from anywhere — unwinds every level |
/ <command> |
run a command as if typed at the main menu; an editing command leaves you where you are (/ bpm 100, / lfo1 start), while a navigation one moves you there (/ lfo1, / t2p3, /t1p1) |
exit |
go up one layer: a definition editor -> the synthdef menu, a synthdef/LFO menu -> the main menu, a pattern -> its track, a track -> the main menu (playback keeps running) |
help / help <command> |
command list / detail for one command |
save, load, clock, status, dump, start-all, … |
main-menu commands also work inside a submenu (no need for /) |
exit is strictly one layer up in the hierarchy, not "back to where I came
from": menus never nest behind each other, so whichever way you arrived, the
chain is always
seq:def:rob1> exit # the definition editor goes to the synthdef menu
seq:synthdef> exit # and that goes to the main menu
seq>
seq:t1p2> exit # a pattern goes to its own track
seq:t1> exit # a track goes to the main menu
seq>
and an LFO menu (which hangs off the main menu) exits straight to seq>. This
also means jumping around with full paths (t3p2, lfo4) cannot leave you
"underneath" a menu you visited earlier:
seq> t2p1
seq:t2p1> t1p3 # jump anywhere, the previous menu is left behind
seq:t1p3> def rob1 # ...including into a SynthDef definition
seq:def:rob1> exit # -> the synthdef menu, not t2p1
seq:synthdef> exit
seq>
/ and / <command> are deliberately Unix-like: a lone / is cd / (back to
the main menu), and / <command> runs that command with the full path - so
/ bpm 99 edits the global tempo and you stay where you are, while a path such
as / lfo1 or / t2p3 is a cd and does move you:
seq:t1p1> /lfo1 frequency 10 # run an LFO command from here
Frequency set to 10 Hz (5bpm)
seq:t1p1> ← still here (it only edited a value)
seq:lfo:2> / bpm 99 ← from an LFO menu too
Global BPM set to: 99
seq:lfo:2> ← still here
seq:t1p2> / lfo1 ← a *path*: move into LFO 1
seq:lfo:1>
seq:lfo:1> / t1p2 ← and on to a pattern
seq:t1p2>
seq:t1p1> / ← lone slash: go to the top
seq>
Every leaf and setting is addressable by a compact path:
t<track> t2
t<track>p<pattern> t2p4
...s<seq> t2p4s0 (sequences s0..s9)
...f<phrase> t2p4f3 (phrases f0..f7)
...v<seq> t2p4v0 (velocity of a sequence)
...sus<seq> t2p4sus0 (sustain of a sequence)
...mt<seq> t2p4mt0 (microtiming of a sequence)
At the main menu a path followed by a value edits the leaf:
t1p1s0 0 0 5 # set sequence 0
t1p1f1 inf*s0 # define phrase 1
t1p1v0 64+0.4*lfo1 # velocity expression
t1p1sus0 0.5+0.5*lfo1 # sustain expression
t1p1mt0 -0.05 # microtiming expression
t1p1 scale dorian # pattern setting
t1p1 root E4
t1p1 bpm 95
t1p1 channel 10
t1p1 division 1/8
t1p1 type CC
t1p1 controller 74
t1p1 hist f1 live # view
t1p1s0 # show one leaf
An = is accepted anywhere a value follows a leaf name:
t1p1v0=64+0.4*lfo1, v3=-5, sus1=0.5.
-
BPM — global (
bpm 120) or per pattern (bpm 95). Range 1–300. -
division — the note value of one step:
1,1/2,1/4,1/8,1/16(default),1/32,1/64, or any1/<integer>(1/3,1/5,1/7…). A bare integer is accepted as the denominator (division 8=1/8).step_ms = 240000 / (bpm × division_denominator)At 120 BPM:
1/16→ 125 ms,1/8→ 250 ms,1/3→ 666.67 ms,1→ 2000 ms. -
sustain — how long a note holds, expressed in steps (not a MIDI value: MIDI has no note-length byte, a note lasts from Note On to Note Off).
0.5= half a step (default),2= two steps,1/4= a quarter step, or an expression (0.5+0.5*lfo1); clamped to0..8steps. The real length in ms follows the division and BPM.Playback is polyphonic: every Note On carries its own Note Off, so a sustain longer than one step rings over the following notes — the Note Off is sent as soon as its length has elapsed, which is after the next Note On when it overlaps. The sequencer never waits for a note to end, so long sustains neither block the grid nor delay the following steps, and
stop/panicrelease everything immediately. -
microtiming — an earlier/later offset in steps, signed like sustain is unsigned:
1/4= a quarter step late,-1/8= an eighth early,0.5,-1, clamped to-8..8steps, default0(on the grid). Negative values are allowed; the real ms follow the division and BPM. -
LFO start alignment — starting an LFO latches it to the next beat of the main BPM grid and restarts it from its configured phase, so several LFOs started within the same beat stay phase-locked. A stopped LFO reads
0.
Changing division, BPM, scale, root, channel, sequences or phrases while playing takes effect at the start of the next cycle (the player re-reads the pattern before every pass).
seq:t1p1>
A sequence is a list of steps. In a note pattern each step is a scale degree (relative to the pattern's current scale, wrapping across octaves — changing the scale later re-reads the same degrees in the new scale); in a CC pattern each step is a raw controller value.
s0 0 2 4 r # degrees: root, 3rd, 5th, rest
s0 O0 o3 # octave shifts: O = +1 octave, o = -1 octave
s0 4+2*lfo1 7 # LFO term (see below): degree 4 +/- 2 scale steps
s0 # show the sequence with resolved values
list-s # list all configured sequences
| Token | Meaning |
|---|---|
<int> |
scale degree 0..127 (note pattern) or raw value 0..127 (CC pattern) |
Degrees are scale-relative: s0 1 2 3 4 in chromatic gives C#4 D4 D#4 E4,
and after scale dorian the very same sequence plays D4 Eb4 F4 G4. Degrees at
or beyond the scale size wrap into the next octave (s0 7 in a 7-note scale =
root one octave up), so no step is ever silently dropped.
scales.py holds (a) a curated, named list and (b) the complete catalogue
of set classes for 5, 6 and 7 notes, generated and verified (38 / 50 / 38
classes, the published counts for those cardinalities):
seq:t1p1> scale ?
Named scales (aliases accepted, 5/6/7 notes):
aeolian altered augmented blues chromatic chromatic-hexachord dorian …
Catalogue of set classes (126 generated, complete for those note counts):
5 notes: pent-01..pent-38 (38 classes, prefix 'pent')
6 notes: hex-01..hex-50 (50 classes, prefix 'hex')
7 notes: hept-01..hept-38 (38 classes, prefix 'hept')
seq:t1p1> scale ? 6 # one cardinality, with prime forms and vectors
Set classes matching '6' (50):
hex-01 (0,1,2,3,4,5) vector 543210 modes 6 [chromatic-hexachord]
hex-02 (0,1,2,3,4,6) vector 443211 modes 6
…
seq:t1p1> scale hex-07@3 # a mode of a class (0 = the prime form)
Scale set: hex-07@3 on C4: C4 D4 F4 A4 A#4 B4
seq:t1p1> scale 0,2,4,7,9 # or just give the semitone offsets
Scale set: custom-0-2-4-7-9 on C4: C4 D4 E4 G4 A4 (custom interval list)
- The named scales are the common ones: the major modes and the harmonic /
melodic minor families, plus
whole-tone,augmented,hexatonic,blues,major-blues,prometheus,tritone,persian, and the pentatonics (major-pentatonic,minor-pentatonic,egyptian,hirajoshi,insen,yo,iwato,kumoi,pelog). Aliases work too (minor,gypsy,spanish,romanian,hijaz,japanese,major-pent, …). - The catalogue is the full set-class content (
pent-/hex-/hept-), complete for those note counts: every 5-, 6- and 7-note pitch-class set is reachable, either directly or through one of its modes. Entries are named by prime form order (our own numbering, not the Forte numbers); the listing shows the prime form, the interval vector, how many modes it has, and any named scales that share the class — e.g.hept-38is the diatonic set:vector 254361 [aeolian, dorian, ionian, locrian, lydian, mixolydian, phrygian]. - Rooted in the listing at
allthescales.org(see thescales.pydocstring), which is where the 7-note named scales came from; the 6-note and the complete classes are generated from the set theory behind that listing rather than copied row by row, so the data is exact and checkable. |O…,o…| octave up/down prefixes (O0,o3); in CC mode they add ±12 | |r| rest — the step is skipped (no note, no CC) | |A*lfo1| multiplication: value =round(A × lfo1)| |A*B*lfo1| multiplication: value =round(A × B × lfo1)| |A+B*lfo1| sum: value =A + round(B × lfo1)— how you write an offset | |A*lfo1*lfo2| several LFOs multiply together | |A-B*lfo1| subtraction (alsolfo1*30+30, any sum of products) |
The value of a step is a small algebraic expression: products bind first, then sums, exactly like normal maths. Numbers may be fractional or negative.
s0 127*lfo1 # 127 x lfo1 -> 0..127 with a ramp LFO
s0 63+63*lfo1 # 63 + 63 x lfo1 -> 0..126 with a bipolar LFO
s0 64+63*lfo1 # centred sweep -> 1..127
s0 4+2*lfo1 # a note degree +/- 2 scale steps
s0 2*lfo1*lfo2 # two LFOs multiplied
Two things to keep in mind:
- Bipolar vs unipolar —
sin,tri,saw,square,randomrun-1 … 1, so127*lfo1reaches-127…127and a CC value clamps to0…127(useshape rampfor a clean0…127);rampand anyshape rampfactor run0 … 1. 0*5*lfo1now means(0 × 5) × lfo1 = 0. For an offset write the sum:0+5*lfo1, or just5*lfo1(the setter warns when an LFO term multiplies out to 0).
In a note pattern the result is a scale step index and octaves wrap, so
5*lfo1 walks ±5 notes of the selected scale (in C major: +5 → A4, -5 →
E3). In a CC pattern it is clamped to 0..127.
seq:t1p1> s1 0*5*lfo1 0 0 # a note that moves with LFO 1
seq:t1p1> s1 # s1: 0*5*lfo1 0 0 -> A4 C4 C4
seq:t1p1> s2 0*5*lfo1*lfo2 # amplitude shaped by two LFOs
seq:t1p1> s2 # s2: 0*5*lfo1*lfo2 -> C4
seq:t1p1> type CC
seq:t1p1> controller 1
seq:t1p1> s3 63*30*lfo1*lfo2 # CC1 = 63 + round(30 x lfo1 x lfo2)
A stopped (or not-yet-latched) LFO counts as 0, so any chain containing one currently reads 0 and the value falls back to the base number.
A phrase is a looping or one-shot arrangement of sequences (8 phrases per pattern, numbered 0 to 7, like sequences):
f0 inf*s0 # loop sequence 0 forever
f1 4*s0 # play s0 four times, once
f2 inf*(s0+2*s1) # loop [s0, s1, s1]
f7 2*(3*s0+s1)+s2 # weighted groups
f0 # show the phrase
list-f # list all phrases
Grammar:
expr := 'inf*' unit | sum
sum := term ( '+' term )*
term := count '*' unit | unit
unit := 's' DIGIT | '(' sum ')'
count := positive integer
Weights repeat, + concatenates, parentheses group, inf loops until stopped.
seq0 is accepted as a synonym of s0. A phrase with no inf plays one pass
and stops by itself.
These attach to the sequence with the same index: notes coming from s2 use
v2, sus2 and mt2. Each accepts a number or an LFO expression.
In a loop pattern the same v0…v9 work: they attach to the order list with
the same index, so v0 is the velocity of o0 and controls the volume of
the sample played by those steps (v1 32 = quiet, v1 100+27*lfo1 = swelling).
sus/mt do not apply to a sample (the next step already cuts it).
| Sequence | Meaning | Range (clamped) | Default |
|---|---|---|---|
v<k> |
MIDI velocity — or the volume of o<k> in a loop pattern |
0..127 (integer) |
100 |
sus<k> |
note length, in steps (0.5, 2, 1/4) |
0..8 steps |
0.5 |
Fractions also work inside expressions, for both values and LFO coefficients —
sus0 1/8+1/16*lfo1 sweeps the length from 1/16 to 3/16 of a step, and
mt0 1/8-1/16*lfo1 swings the timing between +1/16 and +3/16 of a step.
A length of 0 means the note is not played at all — a rest. Nothing is
emitted: no MIDI note-on (which would only click) and no OSC event. A sustain
swept to zero by an LFO does the same, so sus0 0.5+0.5*lfo1 gates the notes
off at the bottom of the sweep.
| mt<k> | timing offset, in steps, signed (1/4, -1/8) | -8..8 steps | 0 |
Expression grammar (all three):
expr := term ( ('+'|'-') term )*
term := number | lfo<N> | <coef>*lfo<N>
Examples:
v0 45 # constant velocity
v1 =64+0.4*lfo1 # 64 plus 40% of LFO 1
sus0 0.5 # hold half the step
sus1 1/4 # a quarter of a step
sus2 2 # two steps long (e.g. 250 ms at 1/16, 120 BPM)
sus3 0.5+0.5*lfo1 # vary the length
mt0 -0.05 # 5% of a step early (same as -1/20)
mt1 1/4 # a quarter step late
mt2 -1/8 # an eighth of a step early
mt3 0.01*lfo1 # swing with LFO 1
v0 / sus0 / mt0 # show one entry
list-v, list-sus, list-mt
mt values are shown in milliseconds wherever the step length is known
(mt0 0.1 at 1/16, 120 BPM → +25.0 ms).
type # "Type: note", "Type: CC (controller 74)", "Type: loop", "Type: osc"
type CC # this pattern sends control-change data (also: type note)
type loop # this pattern plays positions of a .wav sample (see 4.5)
type osc # this pattern sends SuperCollider events (see 4.6)
controller 74 # CC number 0..127 used by a CC pattern
CC patterns behave exactly like note patterns — same sequences, phrases,
phrases weights, division, BPM, microtiming, channel, port, cp/rm,
save/load — with these differences:
- a sequence value is the CC data byte (
0..127, scale-independent), - every step sends
CC(controller, value)on the pattern's channel/port, - no note on/off is sent, so velocity and sustain sequences are not used,
- the number of controllers is unlimited: use several CC patterns.
seq:t1p1> type CC
seq:t1p1> controller 1
seq:t1p1> channel 3
seq:t1p1> s0 63+30*lfo1 r # CC1 sweeps 33..93 (offset form)
seq:t1p1> f1 inf*s0
seq:t1p1> division 1/8
seq> lfo1 shape sin
seq> lfo1 start
seq> start t1p1f1
Because the LFO term is an offset (base*amp*lfo = base ± amp), the base
decides how much room there is. show and list-s annotate the reachable
range and warn you when it cannot move:
s0: 127*lfo1 -> 127 [LFO range 0..127] # multiply (ramp: 0..127)
s0: 63+30*lfo1 -> 63 [LFO range 33..93] # add an offset
s0: 64+63*lfo1 -> 64 [LFO range 1..127] # full centred sweep
s0: 127*lfo1 -> 0 [lfo1 stopped -> contributes 0 (value stays 0)]
A loop pattern plays a .wav file. The sample's length is normalised to 1,
so an order entry is a position in it: 0.2 means "start at 20 %", 0.5 the
middle, 1/0 the beginning. Positions wrap modulo 1, so negatives work too
(-0.2 -> 0.8) - an LFO can therefore sweep the whole file.
type loop # this pattern plays the sample
sample Drums.wav # pick a file from the 'samples' folder next to seq.py
sample # show the current file and its length
division 1/8 # step length (250 ms at 120 BPM)
o0 0.2 0.3 0.6 # order list 0: play from 20 %, 30 %, 60 % of the sample
o1 0 0.5 r 0.75 # order list 1 (o0..o9); 'r' is a rest (silent step)
o2 0.2 0.5*lfo1 # an entry may be an LFO: position = round-free 0.5 x lfo1
o3 lfo2 # with shape ramp, lfo2 sweeps 0 -> 1 across the sample
o0 # show one order list (with the values it resolves to now)
list-o # list the sample and every order list
f0 inf*o0 # phrase: arrangement of order lists (like inf*s0)
f1 2*o0+o1 # ... 0.2 0.3 0.6 | 0.2 0.3 0.6 | 0 0.5 r 0.75, then repeat
start f0 # play the arrangement (loops forever)
stop f0 # stop it
start o0 # shortcut: play one order list (same as inf*o0)
-
divisionsets the step length, so a pass through an order list follows the BPM grid (a rest also takes one step); messages show where the order list sits in the bar at that division (4 passes per 4/4 bar,5/16 of a 4/4 bar,1 bar (4/4)). -
Every step starts the sample at its position and the next step cuts it off (single audio channel, like a sampler). A position of
0.8on a 3.10 s sample therefore has 0.62 s left to ring before the next step. -
An order entry may be an expression with LFOs (same algebra as sequences:
lfo1,0.5*lfo1,0.3+0.1*lfo1,2*lfo1*lfo2). It is evaluated at every step; the value is used as a position and wrapped modulo 1, so arampLFO sweeps the sample and a bipolar one crosses its whole length.show,o<k>andlist-oprint the values it resolves to at that moment (o1: 0.75 0.2 r 0.5*lfo1 -> now 0.75 0.20 r 0.25) and note a stopped LFO. -
Playback is audio, not MIDI: positions are played by feeding the audio device the sample's frames from that offset through winmm's
waveOutAPI (ctypes, no extra packages, Windows only). The file must be a plain PCM .wav.panicandexitsilence it. -
Live editing works like the rest of the sequencer: changing the order list, the sample, the division or the BPM takes effect on the next pass.
-
Order lists are the loop-pattern equivalent of sequences, and phrases are still the arrangement layer — just like
s0->f0 inf*s0in a note pattern:f0 inf*o0loops order list 0,f1 2*o0+o1chains repeats and groups (4*(o0+2*o1)works too). A loop phrase must useo<k>units (a note/CC phrase must uses<k>) and the setter tells you when they are mixed.start f0plays it and the running phrase shows in green, like any phrase. -
start o0still works as a shortcut (it isinf*o0), from the pattern menu (start o0), the track menu (start p1o0) or the main menu (start t1p1o0), andpanicstops loops. -
Saving stores
type = loop,sample = Drums.wav,order-<k> = 0.2 0.3 0.6and the arrangements (phrase-<k> = inf*o0) in the pattern section;cp o0 o1copies an order list (andcp t1p1o0 t3p4o1across patterns),rm t1p1o0deletes one. -
v<k>sets the volume of the steps that come from order listo<k>(same numbering as note patterns), and it may be an LFO expression:v0 100,v1 32,v0 100+27*lfo1. The value scales the played sample through the audio device's volume, sov=0is silent andv=127is full level.list-vand the histogram show it, and it can be edited live like everything else.seq:t1p3> v1 32 v1 set: 32 (now 32; scales o1) -
histworks here too, on the same horizontal layout: one row per time step of a bar (the order list cycles through them, exactly as the player does), each row evaluated at its own moment, so an LFO-driven position - or the per-order velocityv<k>- differs from row to row. Row 0 is the step playing now (tagged>), the others are what comes next. The axis is the sample's length (0 at the left, 1 at the right) and av<k>column appears when the steps use a velocity. The marker is the position the step plays from and the short line after it is what is actually heard - the sample runs only until the next step (or the end of the file):seq:t1p3> hist o0 live o0 step 250 ms bpm 120 division 1/8 Motorway.wav (live) step 3 0 1/4 1/2 3/4 1 |-------------|-------------|--------------|-------------| s1 .....*-----............................................... 0.094 v100 s2 ............................*-----........................ 0.5 v100 s3 ...................................................*-----. 0.9 v100 s4 ...........*-----......................................... 0.2 v32 s5 ........................................*----............. 0.7 v32With a step longer than the sample the line runs to the right edge (the whole rest of the file is heard);
hist liverefreshes the view and the row of the step being played is marked>(a rest row shows>too, with no marker).
seq> t1p1 type loop
seq> t1p1 sample Drums.wav
Sample set to Drums.wav (C:\CLI_1\samples)
Length 4.04 s.
seq> t1p1 division 1/8
seq> t1p1o0 0 0.25 0.5 r 0.9
o0 set: 0 0.25 0.5 r 0.9 (5 steps x 250 ms = 1250 ms per pass, 5/16 of a 4/4 bar; positions in the sample)
seq> t1p1 list-o
Loop (pattern 1, step 250 ms at division 1/8, bpm 120):
sample: Drums.wav (4.04 s, C:\CLI_1\samples)
o0: 0 0.25 0.5 r 0.9 -> 5 steps = 1250 ms per pass, 5/16 of a 4/4 bar
seq> start t1p1o0
Playing o0 of pattern 1, track 1 ('Drums.wav', 4.04 s): 5 steps
(0 0.25 0.5 r 0.9) = positions in the sample, 120 BPM, step 250.0 ms
= 5/16 of a 4/4 bar, looped. Type 'stop o0' to end.
seq> stop t1p1o0
Loop stopped.
An osc pattern plays the same sequences and phrases as a note pattern, but
sends SuperCollider events over OSC instead of MIDI: one time-tagged bundle
per step, scheduled on the pattern's grid, carrying the pitch, the amplitude
(from v<k>), the length (from sus<k>, sent as the SynthDef's dur — a
sustain of 0 sends nothing at all) and any effect parameters you define.
type osc # this pattern sends to SuperCollider
osc 127.0.0.1:57110 # scsynth server (default, so optional)
sclang 127.0.0.1:57120 # sclang, used to install SynthDefs (default)
synth seq # the SynthDef to play (default 'seq')
# definitions themselves are managed at the main menu: type 'synthdef' there
# (list, new, edit, load, send all) -- sclang answers, so a compile error
# shows up in the CLI.
fx cutoff 2000+1500*lfo1 # effect parameters, LFO expressions welcome
fx reverb 0.3
fx # list them (with the value they resolve to now)
rm t1p1 fx cutoff # clear one
s0 0 2 4 7 # sequences, exactly like a note pattern
v0 90 # velocity -> amp
sus0 2 # length in steps -> dur (seconds)
f0 inf*s0 # phrases as usual
start f0 # play it
dump on # record the OSC packets (then 'dump' to print them)
status # ask the server for /status (is it running?)
-
Timing / 'late' messages: bundles are scheduled
latencyseconds in the future (latency 0.2is the default, FoxDot uses 0.25). scsynth postslate <t>whenever a scheduled message arrives after its time, so raise it if you see those (latency 0.3), or lower it for tighter feel (latency 0.05still arrives ~48 ms early on a local machine):seq:t1p1> latency latency: 0.2 s (default) seq:t1p1> latency 0.05 latency set to 0.05 s (applies at the next cycle of any playing osc pattern) -
Notes become a bundle at the scheduled time:
/s_new <synth> <node> 1 1 midinote <n> freq <Hz> amp <v/127> dur <seconds> <fx…>— the layout FoxDot uses, except that the SynthDef's own envelope frees the node (no groups to clean up).dumpshows exactly what goes on the wire:bundle(t=1789397464.508) [/s_new seq 2000 1 1 midinote 60 freq 261.626 amp 0.787402 dur 0.0625 cutoff 2000] -
FX parameters are per note, may be LFO expressions, and are clamped to a sensible range per name (
cutoff 20..20000,resonance 0.05..1,drive 0.05..8,delay 0..1,reverb 0..1,pan -1..1); an unknown name is passed through for your own SynthDef. Editingfx/synthwhile playing takes effect at the next cycle, like every other pattern edit. -
A catalogue of SynthDefs ships with the sequencer (
scdefs.py, like FoxDot's_SynthDefs.py). Thesynthdefmenu lists it, andsend allinstalls every one of them in one shot: missing files are written tosynthdefs/, then all of them are concatenated intosynthdefs/_all.scdand that single path goes to sclang on one/seqdmessage. Then each pattern chooses what it plays withsynth <name>:seq> synthdef seq:synthdef> list Built-in SynthDefs (send them with 'send all'): seq saw + pulse through a resonant lowpass (the default) bass round sub bass: triangle plus saw, driven pad soft detuned saw pad with a slow filter pluck plucked string: an impulse into a tuned comb filter bell FM bell with inharmonic partial and a long tail fm, blip, noise, perc, glass, growl, organ, stab seq:synthdef> send all Sent 13 SynthDefs to sclang at 127.0.0.1:57120 as one file: C:\CLI_1\synthdefs\_all.scd confirmed by sclang (13 SynthDefs in the library) seq:synthdef> / t1p1 seq:t1p1> synth bass synth set to 'bass' (round sub bass: triangle plus saw, driven) ...The output stage is deliberately cheap (no
FreeVerb— 8 delay lines — and no 1-secondDelayCper note): the echo is a short feedback comb and the space a two-comb + allpass tail, both scaled by their control, so dozens of notes can sound at once. All of them accept the same controls (freq amp dur pan atk rel cutoff resonance drive delay reverb) plus a few extras of their own (bell:ratio/index/decay,pad/stab:detune,blip:numharm), and only core UGens are used, so no sc3-plugins are needed. Different patterns can play different SynthDefs at the same time, at their own divisions:bundle(t=…) [/s_new bass 2000 1 1 midinote 60 freq 261.626 amp 0.787402 dur 0.125] bundle(t=…) [/s_new bell 2001 1 1 midinote 67 freq 391.995 amp 0.787402 dur 0.25] bundle(t=…) [/s_new pad 2002 1 1 midinote 63 freq 311.127 amp 0.787402 dur 0.5] -
Definitions are managed in one place, the
synthdefmenu at the main menu (they are global, like the LFOs; a pattern just picks one):Command Effect list [all|mine|files]what exists, and what each one takes new <name>author a SynthDef of your own (opens the editor) edit [<name>]change one — inside it, loadstarts from an existing SynthDefload <name> [<yours>]import a built-in or a .scdas an editable definition (and open the editor)cp <source> <name>copy a built-in / a definition / a file into one of yours, writing its .scdsend allcompile every definition in sclang, in one message send <name> [<name> …]one, or a subset — or send <name> file.scdfor a fileshow <name>description, parameters and file of one definition confirm on|offwait for sclang's reply when sending (default on) rm <name> [file]delete one of your definitions; add fileto deletesynthdefs/<name>.scdtoorm all [file]every definition of yours (the built-in catalogue is untouched) rm <name>.scdjust the file, whichever definition it belongs to A definition you author lives in the project, and
synthdefs/<name>.scdis regenerated from it on every send, so the file always matches the editor. A file that is not one of your definitions is sent exactly as it is — never regenerated over — either withsend <name> file.scdor byloading it first.cpis the same import asloadwithout entering the editor, so it is the one to use when you want the new definition and its.scdon disk straight away:cp bell rob3createsrob3(description, body and controls taken from the built-in) and writessynthdefs/rob3.scd, ready foredit rob3. It works from thesynthdefmenu and from the main menu. A copiedrawdefinition has its ownSynthDef(\name)line renamed to match, so it cannot install under the original name by accident.Deleting is split the same way, so nothing disappears by surprise:
rm <name>drops the definition and tells you the file is still there,rm <name> fileremoves both, andrm <name>.scdtouches only the file (a built-in's file, e.g.rm bell.scd, is written again by the nextsend bell). Everyrmalso asks the server to drop the SynthDef (/d_free), best effort — it stays in the library until SuperCollider restarts or the definitions are sent again. -
How they reach sclang: the source file's path is sent to the address
/seqd. Run the shipped bridge once in SuperCollider (afters.boot) so that responder exists:File("C:/CLI_1/sc/seqd.scd".standardizePath).load;
It prints
[seqd] loading SynthDef from …for every definition you send and answers on/seqd/reply(see the next bullet).send <name> mydef.scdsends an existing file fromsynthdefs/instead (and leaves it alone). -
Every install is confirmed: the bridge answers
/seqd/reply, sosend <name>/send all/ the editor'sinstallwait briefly and report the outcome —confirmed by sclang (15 SynthDefs in the library), orsclang reported an error: ERROR: syntax error line 3, or, when nothing answers, how to fix it:seq:synthdef> send bell SynthDef 'bell' sent to sclang at 127.0.0.1:57120 (C:\CLI_1\synthdefs\bell.scd). confirmed by sclang (15 SynthDefs in the library)If you installed SynthDefs with an older
sc/seqd.scd, reload it once to get the replies (the CLI then says so instead of a confirmation).confirm off(in thesynthdefmenu) skips the wait (and the hints) entirely. -
panic(andexit) free the SuperCollider nodes as well as stopping MIDI and sample playback;statusreports synths/groups/SynthDefs/CPU when a server is listening and explains what to do when there is none.
The definition is authored in the CLI, not in a file. In the synthdef menu,
new <name> opens an editor at seq:def:<name>>; the file synthdefs/<name>.scd is then
generated from what you set, so what you install always matches what you see,
and save carries it along in the [synthdef-<name>] section of the .cfg.
seq> synthdef
seq:synthdef> new mine
New definition 'mine' - set it up here ('help' lists the commands).
seq:def:mine> param ratio 2 0.5 8
Control 'ratio': default 2, range 0.5..8 ('fx ratio <value|expr>' sets it)
seq:def:mine> body Saw.ar(freq) * 0.3 + SinOsc.ar(freq * ratio) * 0.4
Body set.
seq:def:mine> desc two-operator fm-ish voice
Description set: two-operator fm-ish voice
seq:def:mine> check
'mine' looks fine - 'install' sends it to sclang.
seq:def:mine> install
SynthDef 'mine' sent to sclang at 127.0.0.1:57120 (C:\CLI_1\synthdefs\mine.scd).
two-operator fm-ish voice
parameters: ratio=2 (0.5..8) ('fx <name> ...' sets them)
seq:def:mine> test 62 # hear one note right away
seq:def:mine> / t1p4
seq:t1p4> synth mine
synth set to 'mine' (two-operator fm-ish voice) (per-note /s_new on 127.0.0.1:57110)
seq:t1p4> fx ratio 4 # clamped to your declared 0.5..8
fx ratio set: 4 (now 4, range 0.5..8) (a control your definition declares)
| Editor command | Effect |
|---|---|
param <name> [<default> [<min> <max>]] |
declare a control (no args lists them); rm param <name> drops one |
body [<expression>] |
the voice; the wrapper adds the envelope (which frees the node), the filter, drive, the echo/space tail and Out.ar |
desc [<text>] |
one-line description used in listings and in synth <name> |
controls |
everything the definition accepts, with the ranges fx clamps to |
source |
the SuperCollider source that will be sent |
check |
validate it without SuperCollider: name, body, braces, Out.ar, unbalanced brackets, unused controls, a default outside its range |
install (alias send) |
write synthdefs/<name>.scd and ask sclang to compile it |
test [<midinote>] |
send one note with it (install after changing anything) |
load <name|file> |
import an existing SynthDef — a built-in or a .scd from synthdefs/ — into this definition, so you can modify it (no argument lists what there is) |
pd export | open | import |
edit this voice as a Pure Data patch (boxes and wires) and take the patch back (see below) |
raw [<src> | load <file> | clear] |
use a complete SynthDef of your own instead of the generated one |
show, help, def <name>, /, exit |
summary, help, switch definition, root command, up one layer |
install (from the editor) or send <name> (from the synthdef menu) waits for
sclang's confirmation; confirm off turns that wait off.
The controls the wrapper always declares (out freq amp dur pan atk rel cutoff resonance drive delay reverb) are not yours to add — param says so if you try.
The note itself comes from the pattern: freq/midinote from the sequence, amp
from v<k> and dur from sus<k> (a length of 0 never reaches the synth),
so a definition only describes its voice.
load is the way to start from something that already exists:
seq:def:mine> load bell # a built-in, as a starting point
built-in 'bell' imported - body and parameters are editable.
'check' for the details, 'install' to compile it in sclang.
seq:def:mine> load minw # a file in synthdefs/
minw.scd imported - the body, the parameters and the description are editable now.
problem: SinOsc(...) needs a rate: write SinOsc.ar(...) (or .kr for control rate)
seq:def:mine> load # what is available, with its provenance
built-in seq, bass, pad, pluck, bell, fm, blip, noise, perc, glass, growl, organ, stab
files bass (catalogue), ..., minw (yours), ...
In the example above the problem: comes from the file itself — minw.scd
called SinOsc(...) without a rate — and importing is exactly how you find out.
A file this program generated is taken apart again into body, parameters and
description (the generator's skeleton is recognised, so the wrapper is left
out and only your voice comes back). Any other .scd is imported verbatim as
raw, which can only be replaced whole — the message says which happened.
check then reports anything it can see, including a UGen used without a rate
(SinOsc(...) instead of SinOsc.ar(...), which sclang would refuse).
The voice of a definition can be edited as a patch in Pure Data: boxes with
inlets and wires, not text. You can hear it in Pd as you patch it, and the
controls (freq, ratio, cutoff, …) are inlet boxes, so they stay the
parameters fx and the LFOs drive.
seq:def:rob1> pd open # export and open it in Pure Data
Exported 'rob1' to C:\CLI_1\synthdefs\rob1.pd
note: the FM/index envelope is a vline~ in the patch (attack 1 ms, 2000 ms)
15 boxes. Edit it in Pure Data (the inlets are the fx parameters),
opened in C:\Program Files (x86)\Pd-L2Ork\bin\pd-l2ork.exe
... patch away, save the file in Pure Data ...
seq:def:rob1> pd import rob1.pd # take the patch back
Imported rob1.pd into 'rob1'.
controls used: freq, ratio, index
'source' shows the result, 'install' compiles it in sclang.
Which editor pd open launches is yours to choose:
seq> pd-executable # asks: Command to open Pure Data patches [pd-l2ork]:
seq> pd-executable pd-l2ork # set it; the name is looked up in PATH when opening
seq> pd-executable C:\Pd\bin\pd.exe # a full path works too
seq> pd-executable default # back to auto-discovery
seq> pd-executable ? # show what is set and what it resolves to
A bare name is passed to the launcher as it is, so the executable has to be in
PATH — keeping it there is your side of the bargain. Nothing else is looked
for once you have set a command, so a typo shows up as a message instead of
surprising you with a different editor. Until you set one, the CLI looks for
pd-l2ork (Pd-L2Ork / Purr Data) and falls back to plain pd; the PD
environment variable overrides everything. Our patches use plain Pd objects, so
any of them opens them. The setting is saved in seq.cfg (see 8).
Oscillators Pd cannot make. Plain Pd has no triangle, sawtooth, pulse or
impulse oscillator (tri~, saw~, pulse~, impulse~ are cyclone objects,
not vanilla), so the export builds them from a phasor~ ramp plus arithmetic —
tri~ is phasor~ → -~ 0.5 → *~ 2 → abs~ → *~ 2 → -~ 1, a pulse is
phasor~ → -~ <width> → *~ 1000000 → clip~ -1 1, and so on. Everything
in the table above is a real vanilla object; the ones that are not are reported
when you export, and come back as a phasor~ when you import a patch that
started life as one. Reading a patch that uses tri~/comb~/blip~ still
works — those are cyclones, which pd-l2ork bundles.
The patch is laid out as a ring by default — the signal path runs around
the circle, ordered by how far each box is from the inlets, so the whole graph
fits on one screen and you can see where the wires loop. pd export line gives
the older left-to-right columns instead.
The split is the same as with body: the patch is the voice. seq.py adds
the note-side wrapper around it — the envelope (which also frees the node)
scaled by amp with panning, the filter, drive, the echo/space tail and
Out.ar — and says so in a comment box in the patch. So a patch can add more
stages (lop~, hip~, another osc~, a comb~) and they become part of the
voice, while note length (sus<k>, dur) keeps working because that lives in
the envelope, not in the patch.
A patch you import becomes the definition's own source (like raw), so
body/param stop applying until raw clear; the controls you used in the
patch are re-derived automatically, so fx <name> <expr> works on them (and
param <name> <default> <lo> <hi> sets their range).
| Patch object | SuperCollider | Patch object | SuperCollider | |
|---|---|---|---|---|
osc~ |
SinOsc.ar |
vcf~ |
RLPF.ar |
|
phasor~ |
Saw.ar |
lop~ |
LPF.ar |
|
tri~ |
LFTri.ar |
hip~ |
HPF.ar |
|
pulse~ |
Pulse.ar |
comb~ |
CombC.ar |
|
blip~ |
Blip.ar |
clip~ |
.clip(a, b) |
|
noise~, pink~ |
WhiteNoise, PinkNoise |
+~ -~ *~ /~ |
+ - * / |
|
impulse~ |
Impulse.ar |
vline~ + a message |
EnvGen.kr(Env.perc) |
|
inlet <name> |
a SynthDef control | dac~ |
the voice's output |
Arithmetic works as it does in both languages (products before sums, several
wires into one inlet sum). What the vocabulary has no equivalent for — Klank,
XLine — is reported, not silently dropped: the import tells you which
object it left out, and the patch keeps a comment where it was.
A name you author is refused if it collides with a built-in or with a
hand-written file in synthdefs/ (that file would win on install) — pick another
name. list shows both kinds with their declared parameters, and send all
installs the catalogue and your definitions in one message (regenerated from the project, so edits in the editor are never lost).
A portable definition travels inside the .cfg:
[synthdef-mine]
desc = two-operator fm-ish voice
body = Saw.ar(freq) * 0.3 + SinOsc.ar(freq * ratio) * 0.4
param-ratio = 2 0.5 8scale [<name>] # chromatic (default); 'scale ?' lists all scales
root [<note>] # C4 default; e.g. F#3, Bb2
bpm [<value>] # per-pattern BPM (default: inherits the global BPM)
channel [<n>] # 1..16, default 1
division [1/16] # step note value
select-port # choose this pattern's output port (default: global)
start f1 # start phrase 1
stop # stop this pattern's playback
stop f1 # stop phrase 1 of this pattern
A per-step view of what the phrase plays, one column per division step:
phrase f1 division 1/8 step 250 ms bpm 120 chromatic on C4
0 1 2 3 4 5 6 7
--------------------|-------------------
note C4 D4 E4 · F4 E4 C4 D4
vel ▆ 90 ▆ 90 ▆ 90 ▃ 40 ▃ 40 ▆ 90 ▆ 90
sus ▋ 50 ▋ 50 ▋ 50 ▍ 25 ▍ 25 ▋ 50 ▋ 50
mt > 25 > 25 > 25 < 50 < 50 > 25 > 25
| Command | Effect |
|---|---|
hist |
snapshot of the phrase (the one that is playing, else the lowest) |
hist f2 / hist s3 |
a specific phrase or sequence |
hist live |
keep refreshing while you work (panel above the prompt) |
hist once |
force a one-shot snapshot |
hist stop / hist live stop |
end the live view |
The panel flows with the music: column 0 is the step playing now and the
rest are the steps coming after it, each evaluated at its own moment
(step start + i × step) — so a running phrase scrolls through the display
rather than always starting at the phrase's head, and the column numbers (and
the | beat lines) are the step's position in the bar, with > on the step
that is sounding. Nothing playing? You get a static preview from the phrase's
head. What you see is what will be sent: a 3 Hz LFO on v0 64+63*lfo1 prints
24 58 113 1 104 70 15 127 … and the player sends exactly that sequence.
Reading it:
- note — resolved note name (octave-wrapped),
·for a rest; - vel — velocity glyph (height ∝ value) and number;
- sus — note length in steps, shown as the fraction you would type
(
▏ 1/8,▎3/10,█ 2= two steps; decimals never exceed two, so1.3333reads1.33); glyph height scales up to 2 steps; - mt — microtiming in steps (two decimals at most, fractions when exact):
<1/4= a quarter step early,>0.33late,|on the grid; a bare|means "no microtiming defined",| 0means defined but currently zero; - the header marks the currently sounding step with
>; - in a CC pattern the rows are
val(the CC values) andmt, and the title readstype CC<controller> … values 0-127; - the title also explains zero values:
(lfo1 stopped),(lfo1 starts at the next beat).
seq:t1> p3 # enter pattern 3
seq:t1> p3s0 0 2 4 # edit a sequence of pattern 3 directly
seq:t1> t2 # switch track in place
seq:t1> start p3f1 # play a phrase of this track
seq:t1> stop p3f1
seq:t1> show p2 # show a subtree of this track
seq:t1> show # show this track
seq> bpm [<value>] # global tempo
seq> list-ports # MIDI ports (uses the helper script)
seq> select-port # choose the global MIDI output port
seq> lfo 3 # enter the LFO 3 menu
seq> lfo3 start # start/stop an LFO without entering its menu
seq> lfo1 shape square # any LFO command can be run this way
seq> status-lfo # parameters of all running LFOs
seq> start t1p1f1 # play a phrase
seq> stop t1p1f1 # stop a phrase
seq> panic # stop everything and silence MIDI
seq> show # whole configuration tree
seq> show t1p1 # one pattern
seq> show t1p1s0 # one sequence
seq> show t1p1f1 # one phrase
seq> show t1p1v0 # one velocity entry
seq> save [<file>] # write seq.cfg
seq> load [<file>] # read seq.cfg
seq> help / help start
seq> exit
show prints only branches that lead to configured leaves, and running phrases
are highlighted in green. In a note pattern a sequence is resolved to note
names; in a CC pattern the values are printed as numbers.
The sequencer can send its tempo as MIDI clock to an output port, or
receive clock from an input port and follow it. It is a global setting
(saved in [global] of the .cfg).
seq> clock # state, plus the input and output ports found
seq> clock port out 0 # the port used for send (name or index)
seq> clock port in 1
seq> clock send # we are the master: 24 PPQN + Start/Stop
seq> clock receive # we are the slave: the clock sets the global BPM
seq> clock start received # a MIDI Start starts every configured pattern
seq> clock off
-
24 PPQN: one
0xF8per 24th of a quarter note, so at 120 BPM that is 48 ticks per second. The tick interval follows the live global BPM (change it while sending and the gear follows). -
Send: Start (
0xFA) is emitted when playback begins and Stop (0xFC) when nothing is playing any more (apanicsends Stop too). -
Receive: the ticks are averaged (last 24) to estimate the tempo, which is written to the global BPM;
clockshows the estimate and the tick count, and a lost clock (no ticks for 0.5 s) is reported as "waiting for ticks". -
clock start receivedis the interesting one: an incoming MIDI Start launches every configured pattern — for each pattern the lowest phrase (f<k>, or an order listo<k>for loop patterns), skipping patterns that have no phrase:seq> clock start received clock start: received (MIDI Start starts all configured patterns) seq> start-all # the same thing by hand, to test it Clock start: started 2 pattern(s): t1p1, t1p2 (no phrase configured in: t2p1)In
internalmode (the default) MIDI Start is ignored and only your ownstart/stopcommands control playback. -
showstarts with the global line so you can see the mode at a glance:Global: bpm 120, port TestPort, clock send on Microsoft GS Wavetable Synth
Ports are listed by number and name (clock port with no argument shows both
directions); the direction is taken from the mode, or given explicitly with
in/out. A number is resolved to the port's real name (never stored as a
bare index, which would break if the device list changes), and if the index is
out of range or no device is enumerated at that moment you get an error and
nothing is changed.
Enter a menu with lfo <n> (1–8) or lfo<n>; from anywhere you can use
/ lfo<n> <command> or lfo<n> <command>.
| Command | Syntax | Default |
|---|---|---|
frequency [<expr>] (also freq, f) |
Hertz number (2), fraction (1/4), tempo multiple (8bpm), submultiple (1/8bpm), or a basic operation on other LFOs (see below) |
1 Hz |
shape [<name>] |
sin, tri, saw, square, random (bipolar -1..1) or ramp (0..1) (shape ? lists them) |
sin |
phase [<expr>] |
start point of the waveform, in cycles: -1..1, or a basic operation like 0.5*lfo2 (clamped to -1..1) |
0 |
Both frequency and phase accept expressions over numbers and other LFOs.
Products (*) bind tighter than sums (+, -), exactly like sequences and
value sequences do:
lfo1 freq 3+0.1*lfo2 # 3 Hz, plus a tenth of LFO 2 -> range 2.9..3.1 Hz
lfo1 freq 0.1*lfo2*lfo3 # two LFOs multiplied -> range -0.1..0.1 Hz
lfo1 freq 2*lfo1*lfo2 # not allowed: LFO 1 cannot modulate itself
lfo1 freq 2*lfo3+0.5*lfo4 # a sum of two modulated terms
lfo1 phase 0.5*lfo2 # phase swept by LFO 2 -> range -0.5..0.5
lfo1 freq 1/8 # fractions are fine (0.125 Hz)
show,freqandphasereport the expression and the range it can reach, computed from the shapes it uses (bipolar-1..1,ramp0..1), solfo1 freq 3+0.1*lfo2displays3+0.1*lfo2 (range 2.9..3.1 Hz); while the referenced LFOs are still stopped it addssources not running yet.- A frequency that dips to or below zero just pauses the oscillator for that part of the cycle (with a note when you set it); an expression that could never be positive is rejected.
- Cycles are refused: self-references, and any reference that would reach back
to the LFO being edited (
that reference would create a cycle: LFO 3 depends on LFO 1). The check walks every reference of every parameter, so a cycle throughphaseis caught too. - A bpm multiple cannot be mixed into an expression (
3bpm+0.1*lfo2is an error) — it would freeze the tempo; use Hertz there. - Expressions are stored canonically and survive
save/loadandcp lfo1 lfo2unchanged.
Everything that reads an LFO (velocity, sustain, microtiming, sequence steps, CC
values, OSC fx) follows these nested modulations, and a modulated LFO keeps its
beat-aligned, phase-fresh restart.
<k>bpm means k cycles per beat, i.e. k × BPM / 60 Hz; 1/8bpm means one
cycle every 8 beats. Both display as e.g. 8bpm (16 Hz) / 2 Hz (1bpm), and
tempo-relative values follow the global BPM. phase 0.25 starts a sine at
its positive peak (0.5 is the old "inverted amplitude" case); square is a
50 % duty cycle with phase moving its edges.
ramp is unipolar: it rises linearly from 0 to 1 across the cycle and
jumps back to 0, so modulated values only ever move upwards from the base —
63*30*lfo1 with lfo1 shape ramp sweeps a CC from 63 to 93, and
40+80*lfo1 sweeps velocity from 40 to 120. Its live meter is drawn as a
left-anchored progress bar instead of a centered needle.
random is sample-and-hold: every cycle it draws a new value in -1..1 and
holds it for the whole cycle, so frequency is simply how fast the values
change (frequency 4 = four new values per second; 1/4bpm = a new value
every 4 beats). The draw is deterministic for a given LFO and cycle, so the
histogram, the live meter and the playback all agree on the same value, and the
same phase always reproduces the same sequence — phase shifts which values are
drawn (lfo1 shape random, then s0 0*12*lfo1 steps randomly through the
scale). Its meter stays centered, like the other bipolar shapes.
seq:lfo:1> start # latch to the next main-BPM beat, start from `phase`
seq:lfo:1> stop # a later start begins fresh again (never resumes)
seq:lfo:1> live # single-line meter next to the prompt
seq:lfo:1> live stop
seq:lfo:1> show # all parameters
seq> status-lfo # all running LFOs
The live meter draws a normalised level ([-···|###···] +0.42) and stays at
0.00 while the LFO is stopped. Updating it is flicker-free: only the meter
field is repainted.
lfo<N> can be referenced by:
- sequence variations —
s0 0*5*lfo1 - velocity / sustain / microtiming expressions —
v1 64+0.4*lfo1,sus0 0.5+0.5*lfo2,mt0 0.01*lfo1 - LFO frequency modulation —
frequency 2*lfo3(cycles and self-references are rejected).
cp t1 t2 # whole track (all its patterns)
cp t1p1 t3p4 # a pattern (settings + all leaves)
cp t1p1s1 t3p4s4 # a sequence (+ its v/sus/mt, see below)
cp t1p1f1 t3p4f3 # a phrase
cp t1p1v1 t3p4v4 # velocity / sus / mt entries
cp t1p1sus1 t3p4sus2
cp t1p1mt1 t3p4mt0
cp lfo1 lfo2 # an LFO (see below)
cp bell rob3 # a SynthDef -> a definition of yours (see below)
Copying a sequence also copies the velocity, sustain and microtiming that belong to it (they are indexed by the sequence number), so a copied sequence keeps its dynamics and timing — and the destination's old companions are cleared, so the copy is exact:
seq:t1p1> cp s0 s1
Copied sequence t1p1s0 -> t1p1s1 (v1, sus1, mt1 too).
cp is total, like a Unix cp: a source that was never configured counts as
its default/empty state, so the copy still succeeds.
| situation | result |
|---|---|
cp t1p1 t1p1 |
succeeds, reports unchanged |
| source empty, destination configured | destination is reset to defaults (cleared) |
| source configured, destination empty | destination gets a full copy |
| overwriting a playing pattern/track | its phrases are stopped first |
cp t1p1 t1p2 → Copied pattern t1p1 -> t1p2; t1p2 reset to defaults (t1p1 is empty).
cp t3 t2 → Copied track t3 -> t2; t2 is empty (t3 has nothing configured).
cp t1p1s1 t1p2s2 → Copied sequence t1p1s1 -> t1p2s2; t1p2s2 cleared (t1p1s1 is not configured).
cp t1p1s0 t3p4s4 → Copied sequence t1p1s0 -> t3p4s4.
Mixing kinds is still refused (cp t1p1s0 t1p1f1), since that is a type error.
Operands can be relative to the current menu, exactly like the other commands:
seq:t1> cp p1 p2 → Copied pattern t1p1 -> t1p2.
seq:t1> rm p2 → Removed pattern t1p2.
seq:t1p1> cp s0 s1 → Copied sequence t1p1s0 -> t1p1s1.
seq:t1p1> cp s1 p3s2 → Copied sequence t1p1s1 -> t1p3s2.
seq:t1p1> rm division → Reset division of t1p1 (1/16).
seq:t1p1> rm s0 → Removed sequence t1p1s0.
Absolute (t2, t3p4) and LFO (lfo2) operands are still accepted as-is, and
/cp t1p1 t4p4 from a submenu runs the copy at the root (with absolute paths)
without leaving your menu.
cp also copies a definition, including from a built-in or from a file (see
4.6):
seq> synthdef
seq:synthdef> cp bell rob3 # a built-in becomes your own definition
Copied bell -> rob3.
FM bell with inharmonic partial and a long tail
Written: C:\CLI_1\synthdefs\rob3.scd
'edit rob3' opens it; 'send rob3' compiles it in sclang.
Nothing is sent to SuperCollider by the copy itself: send rob3 (or install
from the editor) is what compiles it. Removing definitions is rm <name> [file]
in the same menu — and only there, since they are not paths:
seq:synthdef> rm rob3 # drops the definition, leaves rob3.scd
seq:synthdef> rm rob3 file # drops both
cp lfo1 lfo2 # LFO 2 gets LFO 1's frequency, shape and phase
cp /lfo1 /lfo2 # same thing, from inside any menu
The leading / is optional and may be used on either or both operands
(cp /lfo1 lfo2). cp is total, like in a Unix shell: an LFO that was never
configured simply has its default values (1 Hz, sin, phase 0, stopped), so
cp lfo1 lfo1succeeds (reportsunchanged) and leaves the LFO alone, even while it is running;- copying identical settings reports
no changeinstead of restarting the destination, so a running LFO keeps its phase; - copying a default LFO over a configured one resets the destination to the
defaults (
Copied LFO 5 -> LFO 1 (frequency 1, shape sin, phase 0); LFO 1 stopped.).
The destination's running state follows the source: if LFO 1 is running, LFO 2 is started too (fresh, beat-aligned); otherwise the destination is left stopped.
rm t1 # delete a track
rm t1p1 # delete a pattern (back to defaults)
rm t1p1s1 rm t1p1f1 # delete one sequence / phrase
rm t1p1v1 rm t1p1sus1 rm t1p1mt1
rm t1p1 scale # reset a setting to its default
rm t1p1 root | bpm | channel | division | type | controller | port
rm lfo1 # LFO 1 back to defaults: 1 Hz, sin, phase 0, stopped
rm lfo2 frequency # reset one parameter (frequency | shape | phase | running)
rm lfo # reset all 8 LFOs (also: 'rm lfo all')
seq:lfo:4> rm # reset the LFO you are inside
seq:lfo:4> rm shape # reset one parameter of that LFO
seq:lfo:4> rm lfo2 # reset another LFO without leaving the menu
A running LFO is stopped by the reset (it reports "was running and has been
stopped"), and a fully default LFO is no longer written to the configuration
file. Resets work from every menu, because rm is routed to the main shell.
Removing a leaf/setting prunes empty patterns and tracks automatically. Copying and removing always targets the same kind on both sides (mismatches are refused), and a running phrase of a touched pattern is stopped first.
save [<file>] (default seq.cfg) writes a plain INI file; load [<file>]
reads it back. Only non-default settings are written.
[global]
bpm = 120
port = Elektron Digitone # the selected MIDI output port
clock-mode = send # off | send | receive
clock-port = Elektron Digitone
clock-start = internal # internal | received
pd-command = pd-l2ork # what 'pd open' launches (see 4.6)
[lfo-1]
frequency = 2bpm
shape = square
phase = 0.25
running = true
[track-1:pattern-1]
seq-0 = 0 2 4 r
seq-1 = 0+5*lfo1 7 # '0*5*lfo1' would multiply out to 0 (products bind first)
phrase-1 = inf*(s0+1*s1)
velocity-0 = 90
velocity-1 = 64+0.4*lfo1
sustain-0 = 1/4
microtime-0 = -0.02
division = 1/8
type = cc
controller = 74
[track-1:pattern-2] # a loop pattern
type = loop
sample = Motorway.wav
order-0 = 0.2 0.6
phrase-0 = inf*o0
velocity-0 = 60
[track-1:pattern-3] # an osc pattern
type = osc
osc-server = 127.0.0.1:57110
osc-sclang = 127.0.0.1:57120
osc-latency = 0.2
synth = bell
fx-cutoff = 2000+1500*lfo1
fx-reverb = 0.3
[synthdef-mine] # a SynthDef defined in the CLI (see 4.6)
desc = my fm-ish voice
body = Saw.ar(freq) * 0.3 + SinOsc.ar(freq * ratio) * 0.4
param-ratio = 2 0.5 8 # default, low, high (''fx' clamps to these)
param-fold = 0 0 1A definition saved this way is self-contained: its synthdefs/<name>.scd is
generated from these lines whenever you install it, so load on another machine
brings the voice along.
Notes:
bpm,scale,root,channel,port,type,divisionare omitted when they equal their defaults; the globalbpmis always written.- The selected output port is stored as
port = <name>in[global](omitted while no port is selected), and the MIDI clock settings asclock-mode,clock-port,clock-start(see 5.3). - LFOs saved as
running = trueare re-armed at load: they start at the next beat from their stored phase. - Loop and osc patterns no longer write
controller(it only applies totype cc), and sequence text is stored canonically: the LFO terms are products, so an offset is written with a sum (0+5*lfo1, not0*5*lfo1). - Loop patterns keep
sampleand oneorder-<k>per order list; osc patterns keeposc-server,osc-sclang,osc-latency,synthand onefx-<name>per effect parameter (scale, sequences, phrases, v/sus/mt and division are the same keys as for note patterns).
seq> panic # stop every phrase and send All Sound Off (CC120) +
# All Notes Off (CC123) on all 16 channels of every
# reachable port
panic is available in every menu (main, track, pattern, LFO). It also
sends Stop (0xFC) on the MIDI clock output and frees the SuperCollider nodes
(/g_freeAll, /clearSched) of every server in use, so OSC patterns stop too.
exit does the same cleanup (without the MIDI panic bytes).
Playback otherwise survives navigation: leaving a pattern or track keeps phrases
running until you stop, panic, or exit the program.
Multiple phrases (and multiple CC patterns) can play simultaneously on the same port and channel — the sequencer shares one device handle per port and each player is independent.
Main (seq>)
bpm, list-ports, select-port, lfo <n>, lfo<n> start|stop,
lfo<n> <param> …, status-lfo, start <path>, stop <path>, start-all,
show [<path>], clock […], cp, rm, dump, status, save, load,
pd-executable […], synthdef (the SynthDef menu), panic, help, exit,
<path> … (edit by path).
rm— tracks, patterns, leaves and LFOs (e.g.rm t1p1s0,rm lfo2). SynthDef definitions are deleted in thesynthdefmenu (rm <name> [file]).clock— MIDI clock:clock send | receive | off,clock port [in|out] <name|index>,clock start internal|received(see 5.3).pd-executable— the commandpd openuses to open patches. With no argument it asks for the command;pd-executable defaultgoes back to auto-discovery, andpd-executable ?just shows the current setting. The executable is expected to be inPATH— that is your business, not the CLI's — and a bare name is handed to the launcher as it is (see 4.6).dump/status— inspect the OSC traffic and ping the SuperCollider server (see 4.6).
Track (seq:t1>)
p<m>, p<m>s<k> …, p<m>f<k> …, p<m>o<k> …,
p<m> division/type/controller/scale/root/bpm/channel/sample/osc/synth/fx/latency …,
show [p<m>[s<k>|f<k>|o<k>]], start p<m>f<k>, stop p<m>f<k>, t<n>, cp,
rm, panic, help, /, / <command>, exit.
Pattern (seq:t1p1>)
type [note|CC|loop|osc], controller, division, scale [<name>|?], root,
bpm, channel, select-port, s<k> [tokens…], f<k> <expr>,
v<k> <expr>, sus<k> <expr>, mt<k> <expr>, o<k> <positions…>,
list-s, list-f, list-v, list-sus, list-mt, list-o,
hist [f<k>|s<k>|o<k>] [live|once|stop], start f<k> / stop [f<k>],
start o<k> / stop o<k>, sample [<file.wav>], p<m>, t<n>, cp, rm,
dump [on|off], status, panic, help, /, / <command>, exit.
For an osc pattern also: osc [<host>:<port>], sclang [<host>:<port>],
synth [<name>], fx [<name> <expr>], latency [<seconds>] (see 4.6).
dump/status work in any pattern: they record the OSC traffic and ping the
server.
SynthDefs (seq:synthdef>)
list [all|mine|files], new <name>, edit [<name>], load <name> [<yours>],
cp <source> <name>,
send all / send <name> [<name> …] / send <name> file.scd,
show <name>, confirm on|off, rm <name> [file] / rm all [file],
/, / <command>, help, exit.
SynthDef editor (seq:def:mine>)
load <name|file>, param <name> [<default> [<min> <max>]], rm param <name>,
pd export [<file>] [line] / pd open (launches pd-l2ork) / pd import <file>,
body [<expr>], desc [<text>], controls, source, check, install/send,
test [<midinote>], raw [<src>|load <file>|clear], show, def <name>,
help, /, / <command>, exit (see 4.6).
LFO (seq:lfo:1>)
frequency (also freq, f), shape, phase, start, stop,
live [stop], show,
rm [lfo<n>] [param] (reset this/another LFO or one parameter),
start <path>/stop <path> (phrase playback), cp, panic, help,
/, / <command>, exit.
| Symptom | Cause / fix |
|---|---|
Values read 0 and never move |
The referenced LFO is stopped (lfo1 start), or it is still waiting for its beat-aligned start — the title of hist says which. |
hist live does not appear to move |
Check the title: (live) plus a small spinner and a moving > playhead mean it is refreshing. If the terminal is narrower than the panel, the column count is reduced automatically. |
invalid expression term '…' |
A typo in an LFO reference — the message suggests the corrected token (0.01*lof1 → 0.01*lfo1). |
| Nothing sounds | No output port selected (select-port), or the phrase is a rest-only sequence. |
| Notes stick | panic (sends All Sound Off / All Notes Off on all channels). |
| A pattern sends no notes | Its type is CC — check with type and switch back with type note. |
status says no /status.reply |
No SuperCollider server is listening on that host:port — start scsynth (s.boot) or point osc at the right one. |
SuperCollider prints late <t> |
The OSC bundles arrive after their time tag: raise the slack with latency 0.3 (default 0.2, FoxDot-style). |
| A MIDI clock port will not open | Another program holds it (winmm error reported); pick another port with clock port out <name> / in <name>. |
no answer from sclang within … |
The /seqd bridge is not loaded in sclang (or is an older copy without the reply). Evaluate File("C:/CLI_1/sc/seqd.scd").standardizePath.load; after s.boot, or skip the wait with confirm off in the synthdef menu. |
sclang reported an error: ERROR: … |
The definition did not compile — the message is sclang's own. Run check in the editor (seq:def:<name>>) for the static problems, fix body/raw, then install again. |
| A definition is silent | Is it installed (install), is the pattern pointing at it (synth <name>), and does its body actually use freq? check warns when it does not. Also check fx: a value outside your declared range is clamped. |
pd says Pure Data was not found |
Set PD to your pd.exe (or install Pd); pd export still writes the patch, you just open it yourself. |
pd import reports "not in the vocabulary" |
The patch uses an object with no SuperCollider equivalent (e.g. Klank, XLine): that part was left out of the imported voice. Rebuild it with vocabulary objects, or keep editing the .scd. |
'<name>' is neither a path nor a SynthDef |
cp got an operand it cannot read: paths look like t1p2s0, and SynthDef names are the ones synthdef lists. Check the spelling, or list inside the synthdef menu. |
nothing here is called '<name>' |
The name is not a definition of yours, not a built-in and not a file in synthdefs/. list shows what there is; load <name> imports a file so you can edit it, or send <name> <name>.scd sends that file as it is. |
| Nodes keep sounding after a note | A raw definition that never frees its node: give its envelope doneAction: 2 (check warns). panic clears whatever is left. |
| File | Role |
|---|---|
seq.py |
shells (main/track/pattern/LFO), commands, views, save/load |
player.py |
playback threads, step grid, note/CC output, panic_ports |
midiclock.py |
MIDI clock (24 PPQN) send/receive and Start/Stop over winmm |
sampler.py |
wav loading and position playback (winmm waveOut) for loop patterns |
lfo.py |
waveform evaluation, frequency/phase parsing, LFO graph |
velocity.py |
expression grammar for velocity / sustain / microtiming |
osc.py |
minimal OSC 1.0 encoder/decoder, UDP client and reply listener |
supercollider.py |
SuperCollider notes, FX, SynthDef source/params, install confirmation |
scdefs.py |
generator for SynthDef source: the built-in catalogue and your own definitions |
pdpatch.py |
Pure Data patches as a visual editor: .pd reader/writer and the UGen vocabulary both ways |
sc/seqd.scd |
SuperCollider side bridge: compiles what we send and replies on /seqd/reply |
synthdefs/ |
the .scd files: the built-in catalogue plus what you author |
phrases.py |
phrase expression parser and canonicaliser |
scales.py |
named scales + complete 5/6/7-note set-class catalogue, note names, MIDI numbers |
midi_ports.py, list-ports.py |
MIDI port discovery helpers |
Special thanks to Qirky and FoxDot, where the idea of building a Python MIDI sequencer originated, and for the method used to push SynthDefs to SuperCollider. The way "patterns" are defined and calculated here — and some of the improvements on it — was also inspired by that work. The scale definitions come from allthescales.org.