ksynth is a compact line-oriented DSL for audio synthesis. It processes one expression per line, accumulates results in single-letter variables, and outputs a wavetable in variable W. This guide is written for an AI agent generating .ks programs.
- One expression per line. Lines execute in order.
- Variables are single uppercase letters
A–Z. No exceptions. - All values are
doublevectors. A scalar is a 1-element vector. - The interpreter is right-associative — this is the most important thing to internalize.
- Comments begin with
/and run to end of line. - Output convention: store final audio in
W. UseW: w exprto normalize to peak ±1.
/ minimal program
N: 4096
T: !N
P: ~N
W: w s P
/ VALID
A: 42
T: !N
F: 440*(6.28318%44100)
/ INVALID — will silently fail or misbehave
AB: 42 / multi-letter — forbidden
F1: 440 / digit suffix — forbidden
You have 26 variables. Track usage on complex programs — the 808 cymbal uses ~17.
42 scalar
3.14 scalar
.5 0.5 ← leading dot is valid
-.25 -0.25
1 2 3 vector [1, 2, 3]
1 .5 .25 vector [1.0, 0.5, 0.25]
Every operator chains right: A op B op C = A op (B op C).
This is correct for most uses but breaks linear mixes:
/ WRONG: parses as S * (0.3 + U * (0.7 + V * 0.2))
W: w S*.3+U*.7+V*.2
/ CORRECT: explicit parens around each scaled term
W: w (S*.3)+(U*.7)+(V*.2)
Rule: whenever you mix multiple sources with + and *, parenthesize every term.
This also affects the s verb:
/ WRONG: FM synthesis — s(P + s(Q))
S: s P+s Q
/ CORRECT: mix of two sines
S: (s P)+(s Q)
Rule: always parenthesize (s X) before using + or * on the result.
| Verb | Returns | Notes |
|---|---|---|
!N |
[0,1,...,N-1] |
iota — fundamental index vector |
~N |
[0, 2π/N, ..., 2π(N-1)/N] |
phase ramp over one cycle |
+V |
scalar sum | reduce: sum all elements |
>V |
scalar peak | reduce: max absolute value |
w V |
normalized vector | scale to peak ±1.0 |
s V |
sin(v) | elementwise |
c V |
cos(v) | elementwise |
t V |
tan(v) | elementwise |
h V |
tanh(v) | elementwise — soft limit |
d V |
tanh(3v) | elementwise — harder soft clip |
a V |
|v| | elementwise absolute value |
q V |
sqrt(|v|) | elementwise |
l V |
log(|v|) | elementwise, log(v+ε) safe |
e V |
exp(v) | elementwise — use for envelopes |
x V |
exp(-5v) | elementwise — fast exp-env shape |
r V |
noise | uniform [-1,1], one sample per input element |
m V |
1-bit noise | ±0.7, deterministic hash, metallic timbre |
b V |
band noise | sum of 6 inharmonic square waves |
u V |
attack ramp | 0→1 over first 10 samples, then 1.0 |
n V |
MIDI→Hz | 440 × 2^((v-69)/12) |
i V |
reverse | reverses vector (not floor!) |
_ V |
floor | elementwise floor to integer |
p V |
print+pass | debug: prints and returns input |
v V |
quantize/4 | round to nearest 0.25 step |
j V |
left channel | extract even-indexed samples from interleaved stereo |
k V |
right channel | extract odd-indexed samples |
+\V |
scan sum | cumulative sum — use for phase accumulation |
| ` | \V` | scan max |
| Verb | Operation | Notes |
|---|---|---|
+ - * % |
arithmetic | % is divide |
^ |
power | A^2 |
& |
min | elementwise |
| |
max | elementwise |
< > = |
compare | returns 0.0 or 1.0 |
, |
concatenate | A,B — append B after A |
# |
tile | N#V — tile V to length N (scalar N) |
o |
additive equal-amp | P o H — Σ sin(P×h) for h in H |
$ |
additive weighted | P $ A — Σ A[j]×sin(P×(j+1)) |
f |
lowpass filter | ct f sig — 2-pole LP, ct in (0, 0.95) |
y |
feedback delay | [d g] y sig — comb filter, d=delay samples, g=gain |
z |
stereo interleave | L z R — interleave two mono vectors |
v |
quantize/N | N v sig — quantize to N levels |
Always start with T: !N (sample indices) and build from there.
N: 4096 / buffer length in samples
T: !N / [0, 1, 2, ..., 4095]
For a one-cycle wavetable, ~N gives the phase directly:
P: ~N / [0, 2π/N, ..., 2π(N-1)/N]
W: w s P / one cycle of sine
N: 44100 / 1 second at 44100 Hz
T: !N
F: 440*(6.28318%44100) / phase increment per sample for 440 Hz
P: +\(N#F) / N#F tiles scalar to vector; +\ cumulates
W: w (s P) / parentheses prevent FM misparse
The formula freq*(6.28318%sr) gives radians-per-sample. Always compute it this way.
N: 13230
T: !N
/ frequency decays from (base+range) to base
F: 50+91*e(T*(0-60%N)) / 141Hz → 50Hz
D: F*(6.28318%44100)
P: +\D
S: (s P)
e(T*(0-k%N)) gives exp(-k*t/N) — exponential decay from 1.0 to exp(-k).
At k=6.9, the value reaches ~0.001 (−60 dB) at t=N.
N: 44100
T: !N
E: e(T*(0-6.9%N)) / full decay over buffer
| k value | -60 dB reached at | Tau |
|---|---|---|
| 6.9 | end of buffer (t=N) | N/6.9 samples |
| 30 | N/30 | fast attack/decay |
| 80 | N/80 | very fast snap |
| 200 | N/200 | near-instant |
Tau in samples = N/k. Tau in ms = N/(k*sr) * 1000.
Negation: always write (0-k%N), not (-k%N) or -k%N, to avoid parse ambiguity.
T * e(-T*k/N) rises from 0, peaks at sample N/k, then decays.
N: 8820
T: !N
X: T*e(T*(0-8%N)) / peaks at N/8 = 1102 samples = 25ms
E: w X / normalize peak to 1.0
Use this for staggered drum bursts (e.g., 808 clap layers).
T: !N
R: r T / white noise [-1,1], N samples
M: m T / 1-bit noise ±0.7, N samples, deterministic metallic character
Critical: r is element-wise. r N where N is a scalar gives one noise sample. Always use r T where T: !N.
ct f signal
ct is the coefficient. Cutoff frequency: fc ≈ ct × sr / (2π).
ct |
Cutoff at 44100 Hz sr |
|---|---|
| 0.04 | 281 Hz |
| 0.08 | 561 Hz |
| 0.114 | 800 Hz |
| 0.15 | 1053 Hz |
| 0.256 | 1800 Hz |
| 0.3 | 2106 Hz |
| 0.5 | 3509 Hz |
| 0.7 | 4913 Hz |
| 0.9 | 6317 Hz |
With resonance (second element of left arg, 0–3.98):
0.3 0.5 f R / LP at 2106Hz with moderate resonance
ksynth has no native HP. Subtract a lowpass:
R: r T
L: 0.15 f R / LP at 1053 Hz
H: R-L / HP above 1053 Hz
The filter attenuates -12 dB/octave below the cutoff. Not steep — use high ct values (0.5–0.8) to push the cutoff high enough for bright sounds.
R: r T
A: 0.256 f R / LP at 1800 Hz
B: 0.114 f R / LP at 800 Hz
C: A-B / bandpass: 800–1800 Hz
The order matters: A (higher cutoff, more content) minus B (lower cutoff, less content) = the band between them.
P $ A
result[i] = Σ_j A[j] × sin(P[i] × (j+1))
A[0] = amplitude of harmonic 1, A[1] = harmonic 2, etc. Use 0 to skip harmonics.
/ sawtooth
H: !32
H: H+1 / [1, 2, 3, ..., 32]
A: 1%H / [1, 0.5, 0.333, ..., 0.03125]
W: w P $ A
/ square wave (odd harmonics only)
A: 1 0 0.333 0 0.2 0 0.143 0 0.111
W: w P $ A
/ organ (drawbars 1,2,3,4,6,8,10,12,16 equal)
A: 1 1 1 1 0 1 0 1 0 1 0 1 0 0 0 1
W: w P $ A
/ bell-like (inharmonic via non-integer phase scaling)
F: 1.0*(6.28318%44100)
G: 2.756*(6.28318%44100) / inharmonic ratio
P: +\(N#F)
Q: +\(N#G)
W: w (s P)+(s Q)*.6
For o (equal amplitude, explicit harmonic numbers):
W: w P o (1 3 5 7 9) / odd harmonics, equal amplitude
Use o when choosing which harmonics are present. Use $ when controlling their amplitudes.
Every time you combine scaled vectors, parenthesize:
/ Three layers mixed linearly
W: w (A*.5)+(B*.3)+(C*.2)
/ Two sources with individual envelopes
W: w (E1*S1)+(E2*S2)
Without parens: E1*S1+E2*S2 = E1*(S1+(E2*S2)) — S1 and S2 are entangled.
L: w left_signal
R: w right_signal
W: L z R / interleave into stereo
Extract channels from an interleaved vector: j = left (even samples), k = right (odd samples).
N: 13230 / 300ms
T: !N
E: e(T*(0-6.9%N))
/ pitch sweep: start freq → end freq
F: 50+91*e(T*(0-60%N)) / 141Hz → 50Hz
D: F*(6.28318%44100)
P: +\D
S: (s P)
/ short sub-bass thump at attack
Q: e(T*(0-300%N))
R: r T
C: 0.04 f R / LP at 281Hz — sub-only noise
W: w (E*S)+(Q*C*.1)
Key parameters: start frequency (determines pitch character), end frequency (the "note" you hear sustained), sweep speed (k in pitch envelope), overall decay (k=6.9 for full buffer).
N: 12348 / 280ms
T: !N
/ body: two detuned sines, fast decay
B: e(T*(0-25%N))
F: 170*(6.28318%44100)
G: 183*(6.28318%44100)
P: +\(N#F)
Q: +\(N#G)
S: B*(s P+s Q)*.5 / NOTE: s P+s Q is FM here — body freq is being modulated
/ For clean mixing, assign sines separately:
/ SP: (s P)
/ SQ: (s Q)
/ S: B*((SP+SQ)*.5)
E: e(T*(0-6.9%N))
R: r T
U: E*R
V: e(T*(0-80%N))
K: r T
/ body dominates spectrum, noise provides rattle
W: w (S*.7)+(U*.4)+(V*K*.2)
Body frequency: 150–200 Hz for 808-style. Higher = tighter snare. Noise tail length is controlled by the buffer N and k=6.9.
N: 8820 / 200ms
T: !N
E: e(T*(0-6.9%N))
M: m T / 1-bit noise: naturally bright and metallic
R: r T
L: 0.5 f R / LP at 3509 Hz
H: R-L / HP: removes lows from white noise
W: w E*(M*.6+H*.4)
Open hi-hat: same but longer buffer (N=12348 or more). The m verb is the key to metallic hi-hat timbre — it produces a deterministic hash-based ±0.7 pattern that sounds distinctly metallic versus white noise.
The 808 clap has 3–4 noise bursts arriving ~10ms apart, each bandpassed to 800–1800 Hz, with the last burst loudest. Approximate with rise-then-fall envelopes peaking at different times:
N: 8820 / 200ms
T: !N
/ burst 1: peaks at ~7ms (k=30 → N/30 = 294 samples)
R: r T
A: 0.256 f R / LP at 1800Hz
B: 0.114 f R / LP at 800Hz
C: A-B / bandpass
X: w (T*e(T*(0-30%N)))
/ burst 2: peaks at ~13ms (k=15 → N/15 = 588 samples)
J: r T
D: 0.256 f J
F: 0.114 f J
G: D-F
Y: w (T*e(T*(0-15%N)))
/ burst 3 (main clap): peaks at ~25ms (k=8 → N/8 = 1102 samples)
K: r T
E: K-(0.114 f K)
H: 0.256 f E
Z: w (T*e(T*(0-6%N)))
W: w (X*C*.3)+(Y*G*.5)+Z*H
Note: Z*H at the end without parens is fine since it's the last term — there's nothing after it for right-assoc to entangle.
N: 22050 / 500ms
T: !N
E: e(T*(0-6.9%N))
/ pitch sweep: small range, fast
/ hi tom: 212→170Hz, mid: 165→122Hz, lo: 125→80Hz
F: 170+42*e(T*(0-80%N))
D: F*(6.28318%44100)
P: +\D
S: (s P) / parentheses required — no FM
/ tiny lowpassed thump
Q: e(T*(0-200%N))
R: r T
C: 0.1 f R
W: w (E*S)+(Q*C*.08)
N: 39690 / 900ms
T: !N
E: e(T*(0-6.9%N))
/ two close frequencies (measured 808: 735Hz + 850Hz)
F: 735*(6.28318%44100)
G: 850*(6.28318%44100)
P: +\(N#F)
Q: +\(N#G)
/ $ adds harmonic content for slightly square character
A: 1 0 0.3 0 0.15
J: P $ A
K: Q $ A
M: J+K*.8
W: w E*M
N: 2646 / 60ms — rim is very short
T: !N
E: e(T*(0-6.9%N))
F: 1800*(6.28318%44100) / 1800Hz dominant
P: +\(N#F)
S: (s P)
R: r T
L: 0.15 f R
H: R-L / HP noise for broadband crack
W: w E*(S*.6+H*.5)
N: 66150 / 1500ms
T: !N
E: e(T*(0-6.9%N))
/ inharmonic frequency ratios (standard cymbal ratios × base)
B: 3000*(6.28318%44100)
P: +\(N#(B*1.000))
Q: +\(N#(B*1.342))
R: +\(N#(B*1.200))
S: +\(N#(B*1.618))
U: +\(N#(B*1.478))
/ odd-harmonic content from $
A: 1 0 0.5 0 0.25
J: P $ A
K: Q $ A
L: R $ A
M: S $ A
X: U $ A
Z: J+K+L+M+X
/ 1-bit noise shimmer
C: m T
G: e(T*(0-6.9%N))
W: w (E*Z*.7)+(G*C*.4)
N: 4096
P: ~N / phase ramp 0..2π
H: !32
H: H+1
A: 1%H
W: w P $ A
N: 4096
P: ~N
A: 1 0 0.333 0 0.2 0 0.143 0 0.111 0 0.0909
W: w P $ A
/ Triangle = 1/h² odd harmonics, alternating sign
N: 4096
P: ~N
A: 1 0 -.111 0 .04 0 -.0204 0 .0123
W: w P $ A
/ Classic B3 registration: 008080800 (approximate)
N: 4096
P: ~N
A: 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1
W: w P $ A
Drawbar harmonic positions: the 9 drawbars correspond to harmonics 1, 2, 3, 4, 5, 6, 8, 10, 16. Map to $ amplitude vector with zeros for unused harmonics.
N: 4096
P: ~N
/ Two inharmonic partials — adjust ratio for different bell characters
/ Ratio 2.756 = classic bell
Q: ~N
F: 1.0
G: 2.756
/ Scale phases by respective multipliers
/ then $ adds overtone content to each partial
A: 1 0 0.3 0 0.1
W: w (P $ A)+((Q*2.756) $ (A*.5))
N: 44100
T: !N
R: r T
L: 0.08 f R / LP at 561Hz — dark wind sound
/ or: 0.3 f R for brighter noise
W: w L
/ Sum odd harmonics with amplitude modulation
/ True PWM needs a comparator but $ can approximate
N: 4096
P: ~N
/ Narrow pulse: many odd harmonics
A: 1 0 0.9 0 0.7 0 0.5 0 0.3 0 0.1
W: w P $ A
N: 4096
P: ~N
/ EE vowel: F1≈270Hz, F2≈2300Hz
/ For a 100Hz fundamental: h3≈270Hz, h23≈2300Hz
/ Gaussian peaks at h=3 and h=23
/ (build amplitude array with bumps at those positions)
H: !32
H: H+1
/ Gaussian centered at harmonic 3, width 2
G1: e((H-3)^2*(0-0.5))
/ Gaussian centered at harmonic 23
G2: e((H-23)^2*(0-0.5))
A: G1+G2*.6
W: w P $ A
/ WRONG — parser treats HP as H applied to P
HP: R-L
/ RIGHT
H: R-L
/ WRONG — generates 1 noise sample
N: 4410
R: r N
/ RIGHT — element-wise over index vector
T: !N
R: r T
/ WRONG — FM synthesis: s(P + s(Q)*0.3)
S: s P+s Q*.3
/ RIGHT
SP: (s P)
SQ: (s Q)
S: SP+SQ*.3 / still right-assoc: SP + (SQ*0.3) — OK here
/ WRONG — not a linear blend
W: w A*.5+B*.3+C*.2
/ RIGHT
W: w (A*.5)+(B*.3)+(C*.2)
/ WRONG — may parse as (-k) % N or miss the unary minus
E: e(T*(-k%N))
/ RIGHT — explicit subtraction from zero
E: e(T*(0-k%N))
/ WRONG — B*(s P + s Q) parses as B*(s(P + s(Q))) = FM
S: B*(s P+s Q)*.5
/ RIGHT
SP: s P
SQ: s Q
S: B*((SP+SQ)*.5)
/ WRONG — H is overwritten before use in W
H: !32
H: H+1 / fine, rebinds H
A: 1%H
H: R-L / now H is the highpass — but if you need the harmonic H later, it's gone
You have 26. Count before writing complex programs. Use short names for intermediates that won't be reused.
p Xprints the vector X and passes it through:W: p (s P)shows sine output+Xreturns the sum as a scalar — useful to check a vector has nonzero content>Xreturns the peak absolute value — check for unexpectedly large or zero signals- Add
W: w intermediateat any point to inspect a partial result - If a drum voice is silent, check: (a)
r Tnotr N, (b) envelope k values aren't so large the signal decays instantly, (c) no FM parsing of sines
/ Remove sub-bass (<280Hz): subtract LP at 0.04
H: R-(0.04 f R)
/ Keep only lows (<560Hz): LP at 0.08
L: 0.08 f R
/ Band 560–1800Hz: LP(0.256) - LP(0.08)
A: 0.256 f R
B: 0.08 f R
C: A-B
/ Remove lows, keep mid-highs (>800Hz):
H: R-(0.114 f R)
/ Very bright only (>3500Hz):
H: R-(0.5 f R)
/ Add resonance to LP:
L: 0.15 0.8 f R / LP at 1053Hz with resonance=0.8
/ Comb filter / echo:
/ [delay_samples, gain] y signal
E: 441 0.4 y R / echo at 10ms, gain 0.4
/ Standard -60dB decay over buffer
E: e(T*(0-6.9%N))
/ Fast body (dies in N/30 of buffer)
B: e(T*(0-30%N))
/ Snap transient only
V: e(T*(0-80%N))
/ Near-instant click
C: e(T*(0-200%N))
/ Rise-then-fall, peak at 25ms (N=8820)
X: T*e(T*(0-8%N))
E: w X
/ Pitch envelope for sweep (higher k = faster sweep)
F: F_low + F_range*e(T*(0-60%N))
| File | Voice | Dominant freq | Duration | Key technique |
|---|---|---|---|---|
drums-kick.ks |
Bass Drum | 141→50 Hz | 300ms | Pitch sweep |
drums-snare.ks |
Snare | 170 Hz | 280ms | Pitched body + noise |
drums-clap.ks |
Clap | 800–1800 Hz | 200ms | 3 staggered bandpassed bursts |
drums-chh.ks |
Closed Hat | >6 kHz | 200ms | 1-bit noise + HP |
drums-ohh.ks |
Open Hat | >6 kHz | 280ms | Same, longer |
drums-hitom.ks |
Hi Tom | 170 Hz | 500ms | Pitch sweep, small range |
drums-midtom.ks |
Mid Tom | 122 Hz | 500ms | Pitch sweep |
drums-lotom.ks |
Lo Tom | 80 Hz | 640ms | Pitch sweep |
drums-rim.ks |
Rimshot | 1800 Hz | 60ms | Short tone + HP noise |
drums-cowbell.ks |
Cowbell | 735+850 Hz | 900ms | Two tones + $ |
drums-crash.ks |
Crash | 3 kHz inharmonic | 1500ms | 5 inharmonic OSCs + 1-bit |
drums-clave.ks |
Clave | 2500 Hz | 100ms | Two detuned sines |
drums-maracas.ks |
Maracas | 4 kHz | 30ms | 1-bit noise + HP |
drums-trigger.ks |
Trigger | 100 Hz | 650ms | Fixed sine |