brainstate

octetta/k-synth · source

Snapshot 0 · 2026-11-04T12:47:28Z · C ·

Content fingerprint84b1fc3848f56a5ccb065118a14e6ede1c829b00aea4923b1c01c18398a9eec0

GitHub description at capture

A pocket-calculator version of a synthesizer

README at capture

# k/synth
<image src="ksynth.png" width="200">

> "A pocket-calculator version of a synthesizer."

**k/synth** is a minimalist, array-oriented synthesis environment. Heavily
inspired by the **K/Simple** lineage and the work of Arthur Whitney, it
treats sound not as a stream, but as a holistic mathematical vector.

### 📐 The Philosophy

This isn't a DAW; it's a vector-processing engine designed for "Base Camp"
signal processing.

It uses:
* **Custom Variables & Functions**: Define descriptive multi-letter variables and lambda functions (e.g., `freq: 440`, `saw: {w P $ A}`).
* **Right-Associativity**: Expressions evaluate from right to left.
* **Vectorized Verbs**: Math applied to entire buffers at once (using single-character verbs like `s`, `+`, `w` or full word aliases like `sin`, `sum`, `norm`).

Sound is a vector. A kick drum is a vector. A two-second bell tone is a
vector. You do math on vectors and the result is audio. There are no tracks,
no timelines, no patch cables — only expressions.

---

### 🚀 Quick Start

To hear the engine in action, try these incantations:

```
/ make a simple kick drum
A: !8000
E: 1 - 0.000125 * A
K: 0.5 & -0.5 | 5 * (s 0.007 * A) * E * E

/ now a snare drum
N: 0.5 f r A
P: E * E * E * E * E
T: (s 0.2 * P * A) * P
S: 0.7 & -0.7 | (2 * T) + N * E * E

/ now closed and open hi-hats
C: (0.95 f r A) * P
O: (0.95 f r A) * (0.8 * E) + (0.5 * P)

/ now make a pattern
W: K,K,S,K,K,S,K,K,S,C,C,C,O,S,S

/ on the web interface click run then click on the waveform to hear it
/ on the CLI type \pW to hear it
```

---

### 🔧 Build

```sh
gcc -O2 ksynth.c -lm -o ksynth
```

The core engine is a single C file with no dependencies beyond libc and libm.
It can also be embedded as a library: include `ksynth.h` and use the
context API (`ks_create`/`ks_eval`) directly, or the wrapper-handle API
(`ks_ctx_create`/`ks_ctx_run`) when you want a stable C ABI (including WebAssembly).
Legacy singleton wrappers like `ks_run()` are still available for compatibility.

#### Embedding

If you are embedding k/synth in another C program, the file set depends on
which API surface you use:

- Core context API only: `ksynth.c` + `ksynth.h`
- Wrapper / handle API: `ksynth.c` + `ks_api.c` + `ksynth.h`

You do not need `main.c`, `miniaudio.*`, `bestline.*`, or `kgnuplot.*`
unless you specifically want the standalone CLI or audio playback shell.

Why choose one over the other:

- Use the core context API when you are writing normal C and want direct access
  to `K` values, host-side array helpers, and the fewest layers between your code
  and the evaluator.
- Use the wrapper API when you want opaque handles instead of `ks_ctx *`, a
  narrower ABI for foreign-function bindings, or the same surface used by the
  WebAssembly build.

For the simplest host-side example, see
[`examples/simple_api.c`](examples/simple_api.c), which shows:

- evaluating expressions with `ks_eval()`
- binding `float[]`, `int[]`, and `double[]` from C
- copying results back out to normal C arrays

---

### 🎛️ What It Can Do

#### Oscillators

Any waveform you can express mathematically. The phase accumulator pattern
`+\(N#F)` where `F` is a per-sample phase increment gives a clean oscillator
at any frequency. Apply `s` for sine, `c` for cosine, or do math on the raw
ramp for triangle and sawtooth shapes.

#### Fundamental Waveforms Reference

- Duration: 1 second (44100 samples)
- Frequency: 220 Hz (A2)

##### Sine wave

```
/ The simplest oscillator using the 's' monadic verb.
/ Logic: 
/ 1. Calculate phase increment: freq * (2pi / sr).
/ 2. Tile to length N and accumulate (+\) to create a continuous phase ramp.
/ 3. Apply sine verb and normalize (w) to peak +/- 1.0.
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)
W: w s P
```

##### Sawtooth wave (additive)

```
/ Constructed using the '$' weighted additive verb.
/ Logic:
/ 1. A sawtooth contains all harmonics (1, 2, 3...).
/ 2. Create amplitude vector A where each harmonic 'n' has amplitude 1/n.
/ 3. '$' sums sin(P*1)*1 + sin(P*2)*0.5 + sin(P*3)*0.33...
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)
H: !32
H: H+1
A: 1%H
W: w P $ A
```

##### Square wave (additive)

```
/ A "hollow" timbre constructed from odd harmonics only.
/ Logic:
/ 1. A square wave contains only harmonics 1, 3, 5, 7...
/ 2. Amplitudes follow 1/n, but even positions in vector A are set to 0.
/ 3. Results in the characteristic binary-state sound.
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)
A: 1 0 0.333 0 0.2 0 0.143 0 0.111
W: w P $ A
```

##### Triangle wave (additive)

```
/ A mellow oscillation with odd harmonics that decay rapidly.
/ Logic:
/ 1. Uses odd harmonics (1, 3, 5...) like a square wave.
/ 2. Amplitudes decay by 1/n^2 (1, 1/9, 1/25...).
/ 3. Signs alternate (+, 0, -, 0, +) to create the peaked "triangle" shape.
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)
A: 1 0 -.111 0 .04 0 -.0204 0 .0123
W: w P $ A
```

##### Sawtooth with exponential envelope

```
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)

/ 1. Sawtooth Oscillator (using additive synthesis)
H: !32
H: H+1
A: 1%H
S: P $ A

/ 2. Exponential Envelope (decays to -60dB over the buffer)
/ Formula: e(T*(0-k%N)) where k=6.9 is standard decay
E: e(T*(0-6.9%N))

/ 3. Apply envelope and normalize
/ Parentheses are mandatory to prevent right-assoc parsing errors
W: w (E*S)
```

##### Sawtooth Pluck

```
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)

/ 1. Sawtooth Oscillator
H: !32
H: H+1
A: 1%H
S: P $ A

/ 2. Sharp Envelope ...  k=40 creates a sharp percussive decay
/ Tau in samples = N/40 = 1102 samples (approx 25ms)
E: e(T*(0-40%N))

/ 3. Apply envelope and normalize
W: w (E*S)
```

#### FM Synthesis

Right-associativity makes FM natural. `s P + Q` parses as `s(P + s(Q))` —
that is the FM formula. Carrier phase plus modulator sine, with no extra
syntax required.

```
/ FM bell: fast index decay, slow amplitude decay
N: 88200
T: !N
A: e(T*(0-3%N))
I: 3.5*e(T*(0-40%N))
C: 440*(6.28318%44100)
M: 440*(6.28318%44100)
P: +\(N#C)
Q: +\(N#M)
W: w A*(s P+(I*s Q))
```

Vary the carrier-to-modulator ratio for different timbres: `1.0` is warm and
round, `1.4` is metallic and bell-like, `3.5` is tubular and bright.

#### Additive Synthesis

The `o` and `$` operators sum harmonic series directly. `P o H` sums
`sin(P×h)` for each harmonic h in H at equal amplitude. `P $ A` weights each
harmonic by a corresponding amplitude in A.

```
/ organ tone: odd harmonics at equal amplitude
N: 44100
T: !N
F: 220*(6.28318%44100)
P: +\(N#F)
H: 1 3 5 7
W: w P o H

/ cello-ish: weighted harmonic series
A: 1 0.6 0.4 0.25 0.15 0.08
W: w P $ A
```

#### Additive Analysis and Reconstruction

The `analyze` verb (operator `ft`) computes a Discrete Fourier Transform (DFT) specifically formatted to extract peak harmonic amplitudes from an array. It creates perfect symmetry with the `$` synthesis verb, allowing you to sample, analyze, and instantly reconstruct any waveform.

```
/ Load a single-cycle piano waveform using the host REPL command

\ra wave piano_cycle.wav

/ Analyze the wave and extract the first 64 harmonic amplitudes
amps: wave analyze 64

/ Synthesize a new pitch using those extracted harmonics!
N: 44100
F: 110*(6.28318%44100)
P: +\(N#F)
reconstruction: P $ amps
```

#### Envelopes and Dynamics

`e(T*(0-k%N))` gives a pure exponential decay from 1 to `e^-k` over N
samples. Adjust `k` to control the decay time. For a percussive rise-and-fall
shape, `T*e(T*(0-k%N))` peaks at sample `N/k` then decays naturally.

#### Envelope Extraction (Envelope Following)

You can extract the amplitude contour (ADSR envelope) from any loaded sample natively using K-Synth's primitives. By taking the absolute value (`a`) to rectify the signal, and passing it through a heavy low-pass filter (`f`), you create a classic envelope follower.

```
/ Load a drum sample (e.g. a TR-808 snare)

\ra snare tr808_snare.wav

/ Extract the envelope (lower filter cutoff = smoother envelope)
env: 0.005 f a snare

/ Apply the snare's organic envelope to a synthesized oscillator
N: 44100
P: +\(N#(110*6.28318%N))
bass: w P o 1 3 5
result: bass * env
```

Soft clipping with `d` (`tanh(3x)`) adds saturation and tames peaks without
hard discontinuities. Hard clipping uses min/max: `x & -limit | limit`.

#### Filters

Two-pole lowpass via `ct f signal` where `ct` is a normalized cutoff (0–1).
Subtract a lowpass from the original signal for highpass. Subtract two
lowpass filters for bandpass.

```
/ bandpass filtered noise
N: 44100
T: !N
R: r T
H: 0.7 f R      / lowpass ~7 kHz
L: 0.08 f R     / lowpass ~600 Hz
B: H-L          / band between them
W: w B
```

#### Noise and Percussion

`r T` generates white noise (N samples when T is a length-N index vector).
`m T` generates 1-bit noise — alternating `±0.7` — with a harder, metallic
timbre useful for cymbals and hi-hats.

Combine noise, filtered noise, and sine sweeps to build any percussion voice:

```
/ kick: pitch-swept sine + noise transient
N: 13230
T: !N
F: 50+91*e(T*(0-60%N))
D: F*(6.28318%44100)
P: +\D
S: (s P)*e(T*(0-6.9%N))
R: 0.5 f r T
E: e(T*(0-40%N))
W: w (S+(R*E))
```

#### Patterns and Sequencing

The `,` operator concatenates vectors. A bar of drums is the concatenation
of individual voice vectors in sequence:

```
Z: K,K,S,K,K,S,K,K,S,C,C,C,O,S,S
```

Longer patterns, fills, and arrangement are vector math — tile with `#`,
mix with weighted addition, chain bars with more `,`.

#### Feedback Delay and Comb Filtering

`[d g] y signal` applies a feedback delay of `d` samples with gain `g`.
Values of `g` near 1.0 give long, resonant comb filtering. Lower values
give short slap echoes. Use it to add metallic resonance to noise or space
to any signal.

#### Convolution

`A z B` convolves two vectors. Design FIR filters, apply impulse responses,
or build custom reverb tails from measured room samples.

---

### 🌐 Web Interface

k/synth compiles to WebAssembly via Emscripten and runs in the browser as a
live-coding notebook. The web interface provides:

- A **notebook** that logs every script run with waveform display and one-click replay
- **16 sample slots** in a 2×8 grid to hold synthesized buffers
- A **pad panel** — a 4×4 trigger grid with per-pad pitch control
- A **drum-grid sequencer** (4 rows × up to 16 steps) with live edit while running
- **Mode-separated patterns** — drum and melodic modes keep independent sequencer patterns
- **Pad-referenced rows** — each sequencer row selects a pad (`0..F`) and inherits that pad's slot/pitch setup
- **Melodic mode** — all 16 pads play one slot at different semitone offsets, turning any synthesized patch into a playable instrument
- A **patches browser** that fetches `.ks` files directly from this repo

```sh
source /path/to/emsdk/emsdk_env.sh
bash build.sh
python3 -m http.server 8080
```

---

### 📖 Language Quick Reference

| Concept | Example | Notes |
|---------|---------|-------|
| Variables | `freq: 440` | Assign to descriptive variable names |
| Functions | `f: {x * y}` | Define lambdas (`x` and `y` are implicit args) |
| Built-in Aliases | `sin(P)` / `norm(W)` | `sin`, `cos`, `tan`, `tanh`, `abs`, `sqrt`, `log`, `exp`, `floor`, `rand`, `pi`, `rev`, `idx`, `phase`, `sum`, `peak`, `norm`, `left`, `right`, `quantize`, `saw`, `slice`, `speed`, `delay`, `analyze` |
| Index vector | `!N` or `idx(N)` | `[0, 1, …, N-1]` |
| Phase accumulator | `+\(N#F)` | Oscillator at frequency F |
| Sine / cosine | `s P` / `c P` | Elementwise |
| Exponential decay | `e(T*(0-k%N))` | Decay from 1 to e^-k over N |
| White noise | `r T` / `rand(T)` | One sample per element of T |
| 1-bit noise | `m T` | `±0.7`, metallic timbre |
| Lowpass filter | `ct f sig` | Two-pole, ct=0..1 |
| Additive equal | `P o H` / `saw(P; H)` | Sum harmonics in H |
| Additive weighted | `P $ A` | Weighted harmonic series |
| Analyze / DFT | `wave analyze 64` / `wave F 64` | Extracts harmonic amplitudes |
| FM synthesis | `s P+(I*s Q)` | Right-assoc gives FM naturally |
| Concatenate | `A,B,C` | Build patterns |
| Tile | `N#V` | Repeat V to length N |
| Soft clip | `d x` | `tanh(3x)` |
| Normalize | `w x` / `norm(x)`| Scale to peak ±1 |
| Feedback delay | `[d g] y sig` | Comb / echo |
| Convolution | `A z B` | FIR / impulse response |

`+\` is supported as a scan (running sum / phase accumulator). Normal K-style
`/` reduce-over forms like `+/1 2 3` are **not** supported in ksynth, because
`/` starts a comment. Use monadic reduction such as `+A` for sum reduction.

Right-associativity: `a op b op c` = `a op (b op c)`. Use parentheses for
linear mixes: `(A*0.5)+(B*0.5)`, not `A*0.5+B*0.5`.

---

### ❤️ Credits

🧮 [ksimple](https://github.com/kparc/ksimple) *tiny k by arthur whitney*

🔊 [miniaudio](https://github.com/mackron/miniaudio) *audio library by mackron*

🖥️ [bestline](https://github.com/jart/bestline) *command session by jart*

**Built** by Joseph Stewart [octetta](https://octetta.com) 🐙♊ in collaboration with AI-🧠
pair programming.