Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ GitHub are its section here (`tools/release.sh` copies them), so add to

## Unreleased

- **Hammerwork Grand, a physically modeled piano.** Felt hammers strike up
to three detuned strings per key, which ring into a soundboard through
a moving bridge. Velocity changes the tone the way a real hammer does,
bass and treble keep their own inharmonicity and decay, and the
unisons give the piano's two-stage decay. PEDAL lifts the dampers on
held notes too. Six presets. See docs/41.

- **Skinwork Kit, a physically modeled drum kit.** Kick, snare with
rattling wires, side stick, three toms, closed, pedal and open hats,
crash, ride and ride bell on the GM drum map. Toms and kick drop in
pitch when hit hard, cymbals bloom into a wash, and a closed hat chokes
an open one. Five presets. See docs/41.

- **Separate STOP and PLAY.** Pressing STOP again while stopped returns
to the loop's start, or to the song's start with the loop off.

Expand Down
6 changes: 6 additions & 0 deletions bench/golden/grand.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# bench goldens for grand: case sha256(f32 L ++ f32 R) [nonharm_db thd_db]
notes 761e28dacea9ae368aa89ba9a1368ac6390492cd74a218da1de21e76d5645d44
velocity aa6f16e703000592e2740b47d48208dd2b0bfabaed8d9081637a271bf3347218
held 007e11ce89fde6e29f858b9de604174aae2df4552cacf9614e4ab16387b1bab8 -23.2 2.8
high af415e8ec0017c9614e979c29044f8ff6d6ab4d51e1871286140ef345f4e61a5 -43.9 -18.5
chord d5d017df9abba1412baa474df8d47366015f4b16bc7e0aea26801dcd9ae622a0 -0.3 4.0
2 changes: 2 additions & 0 deletions bench/golden/kit.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# bench goldens for kit: case sha256(f32 L ++ f32 R) [nonharm_db thd_db]
hits 7deadce04bd5313d21b102b1bc471057214b193fc9aac810353e65128ec5c598
5 changes: 5 additions & 0 deletions docs/02-machines.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ The two modes:
`note-expr!` and `control!` hook the voice lifecycle. Pools with more
than one voice render voices in pairs, one per lane.

A state region holds up to 2 KB inline. A machine whose `state-size!` is
larger, up to 32 KB, gets its regions from the heap when it is created;
the piano's and the kit's mode banks do (docs/41). Params are capped at
4 KB.

The entry words and their stack effects are in docs/04 §Kernel ABI.
`prepare!` runs every block, not once. Other declarations:

Expand Down
37 changes: 37 additions & 0 deletions docs/04-block-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,43 @@ is empty before its machine sleeps and delays correctly when it wakes.
`Track.inst_quiet`, `Effect.quiet`, and samples of silence (saturating).
Replacing a machine resets its counter.

## Shared state (implemented, 2026-10-16)

A voice machine can keep one region for the whole machine, next to its
per-voice state: a body every voice acts on, such as a piano's
soundboard, a kit's shared shell or a resonator all notes ring into. The
host knows nothing of what the region holds. It allocates the bytes,
zeroes them, passes the pointer, and orders the calls.

- `bytes shared-size!` asks for the region (up to 1 MiB,
`MAX_SHARED`). It is zeroed at load and on reset. Every entry word,
note hooks, block-prepare, control and render included, finds it at
`ctx.shared` (`ctx.shared& p@64 | body:MyBody |`).
- `"word" period shared!` names the region's render word,
`( io ctx shared params -- )`. The host renders the sounding voices
for `period` samples, then this word over the same samples, and
repeats. With a period of 1 a voice reads, each sample, what the shared
word left after the previous one, and the shared word sees everything
the voices wrote during that sample. Longer periods cost fewer calls
and delay the coupling by up to that many samples. During the call
`ctx.clock` is the step's first sample, so the word can branch on it
(an `ifte` on the clock is versioned: the arm not taken costs nothing).
- `offset voice-level!` names a voice state field that reports the
voice's level for idle skipping, for a voice heard only through the
shared word. A released voice sleeps once both its own output and that
field stay under `IDLE_FLOOR` for a block.
- Voices of a machine with a shared region render one at a time, in
voice order, never in lane pairs, so their writes into the region land
in a fixed order and renders stay bit-exact at any thread count. The
convention for writing is to add into fields the shared word clears.
- The shared word accumulates into io like a voice. It wakes on any
note-on and runs while a voice sounds; once none does, it sleeps after
a block whose own output stayed under `IDLE_FLOOR`, until the next
note-on. Its output counts as the machine's for idle skipping.

The fixture `machines/raw_fixtures/shared_body.fy` and the test "shared
state: voices and the shared word alternate per step" show the order.

## Why one ctx pointer and not varargs

- Stable ABI surface. Adding a field at the end (in `_reserved`)
Expand Down
4 changes: 4 additions & 0 deletions docs/13-dsp-workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ kernels/
08-machines/
```

In the tree today, `kernels/10-physical/` holds the physical-modeling
layer: the exact modal step, the implicit striker contact and the
membrane's Bessel modes (docs/41).

Current first fixture:

```
Expand Down
70 changes: 70 additions & 0 deletions docs/20-machine-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ project or preset stores (docs/19): real units for knobs, the option
| [`cs80`](#cs80) | C-Beam 80 | instrument | 8 | `—` |
| [`drum2`](#drum2) | DS-404 Drums | instrument | mono | `—` |
| [`fm86`](#fm86) | FM-7.11 | instrument | 8 | `—` |
| [`grand`](#grand) | Hammerwork Grand | instrument | 16 | `gp-` |
| [`guitar`](#guitar) | Fretwire Guitar | instrument | 6 | `gt-` |
| [`jp8`](#jp8) | Juniper-8 | instrument | 8 | `jp-` |
| [`juno2`](#juno2) | Ju-Know | instrument | 8 | `jn-` |
| [`kit`](#kit) | Skinwork Kit | instrument | 12 | `sk-` |
| [`ms20`](#ms20) | SM-24 Mono | instrument | mono | `—` |
| [`profit5`](#profit5) | Profit-5 | instrument | 8 | `p5-` |
| [`rhodes`](#rhodes) | Rhodes E-Piano | instrument | 8 | `rd-` |
Expand Down Expand Up @@ -626,6 +628,34 @@ Presets:
- `slab/tubular-bells` — Slab: tubular bells. A clangorous strike that settles into a long, slightly sour hum while the key is held.
- `slab/vibes` — Slab: vibraphone. A bright mallet bar with a ringing high partial, a tink on the strike and a steady motor tremolo.

## grand

**Hammerwork Grand**, instrument, 16 voices.

| param | section | label | range | default | notes |
|---|---|---|---|---|---|
| `gp-hard` | HAMMER | HARD | -1 … 1 | -0.6 | linear |
| `gp-felt` | HAMMER | FELT | 0 … 1 | 0.35 | linear |
| `gp-strike` | HAMMER | STRIKE | -1 … 1 | 0 | linear |
| `gp-decay` | STRING | DECAY | 0.25 … 4 | 1.5 | exp |
| `gp-hfdec` | STRING | HF DEC | 0.2 … 8 | 3 | exp |
| `gp-stiff` | STRING | STIFF | 0 … 3 | 1 | linear |
| `gp-unison` | STRING | UNISON | 0 … 6 | 1.2 | linear |
| `gp-board` | BOARD | COUPLE | 0.1 … 2 | 1 | exp |
| `gp-width` | BOARD | WIDTH | 0 … 1 | 0.7 | linear |
| `gp-release` | DAMPER | RELEASE | 0.05 … 2 | 0.25 | exp |
| `gp-pedal` | DAMPER | PEDAL | 0 … 1 | 0 | linear |
| `gp-level` | DAMPER | LEVEL | 0 … 1 | 0.7 | pow |

Presets:

- `bright-pop` — Hard-voiced hammers and a lively board: cuts through a band mix
- `default` — Concert grand at its own voicing: medium felt, long sustain
- `dry-studio` — Short, focused and close: dampers quick, sustain trimmed
- `felt-ballad` — Soft felt and a dark top, dampers half lifted: intimate and round
- `honky-tonk` — Unisons pulled apart and a bright hammer: the saloon upright
- `sustain-wash` — Pedal down: every note rings into the next, wide board

## guitar

**Fretwire Guitar**, instrument, 6 voices.
Expand Down Expand Up @@ -838,6 +868,46 @@ Presets:
- `wide-sunny-pad` — Wide sunny melancholic pad
- `afterimage/horizon` — Slow detuned pad without sub, with two-voice stereo unison; Afterimage Express. Upper chord voicings leave space for bass.

## kit

**Skinwork Kit**, instrument, 12 voices.
Drum notes: kick = 36, snare = 38, side = 37, lt = 41, mt = 45, ht = 48, ch = 42, ph = 44, oh = 46, crash = 49, ride = 51, bell = 53.

| param | section | label | range | default | notes |
|---|---|---|---|---|---|
| `sk-kick-tune` | KICK | TUNE | -12 … 12 | 0 | linear |
| `sk-kick-decay` | KICK | DECAY | 0.25 … 4 | 1 | exp |
| `sk-kick-level` | KICK | LEVEL | 0 … 1 | 0.8 | pow |
| `sk-snare-tune` | SNARE | TUNE | -12 … 12 | 0 | linear |
| `sk-snare-decay` | SNARE | DECAY | 0.25 … 4 | 1 | exp |
| `sk-snare-level` | SNARE | LEVEL | 0 … 1 | 0.8 | pow |
| `sk-tom-tune` | TOMS | TUNE | -12 … 12 | 0 | linear |
| `sk-tom-decay` | TOMS | DECAY | 0.25 … 4 | 1 | exp |
| `sk-tom-level` | TOMS | LEVEL | 0 … 1 | 0.8 | pow |
| `sk-hat-tune` | HATS | TUNE | -12 … 12 | 0 | linear |
| `sk-hat-decay` | HATS | DECAY | 0.25 … 4 | 1 | exp |
| `sk-hat-level` | HATS | LEVEL | 0 … 1 | 0.7 | pow |
| `sk-crash-tune` | CRASH | TUNE | -12 … 12 | 0 | linear |
| `sk-crash-decay` | CRASH | DECAY | 0.25 … 4 | 1 | exp |
| `sk-crash-level` | CRASH | LEVEL | 0 … 1 | 0.7 | pow |
| `sk-ride-tune` | RIDE | TUNE | -12 … 12 | 0 | linear |
| `sk-ride-decay` | RIDE | DECAY | 0.25 … 4 | 1 | exp |
| `sk-ride-level` | RIDE | LEVEL | 0 … 1 | 0.7 | pow |
| `sk-stick` | PLAY | STICK | -1 … 1 | 0 | linear |
| `sk-wires` | PLAY | WIRES | 0 … 1 | 0.7 | linear |
| `sk-glide` | PLAY | GLIDE | 0 … 2 | 1 | linear |
| `sk-bloom` | PLAY | BLOOM | 0 … 2 | 1 | linear |
| `sk-width` | PLAY | WIDTH | 0 … 1 | 0.6 | linear |
| `sk-level` | PLAY | LEVEL | 0 … 1 | 0.7 | pow |

Presets:

- `default` — A studio kit: 18-inch kick, 14-inch snare, three toms, hats, crash and ride
- `funk-dry` — Tight and dead: short heads, crisp snare, closed hats up front
- `gong-wash` — Cymbals left to bloom: heavy coupling and long decays for swells
- `jazz` — Small kit tuned high, light sticks, long washy cymbals, loose wires
- `rock` — Big low tuning, hard sticks, the toms drop in pitch when hit hard

## ms20

**SM-24 Mono**, instrument, mono.
Expand Down
169 changes: 169 additions & 0 deletions docs/41-physical-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# 41 — Physical models: Hammerwork Grand and Skinwork Kit

Two machines synthesize their sound by simulating the instrument's
mechanics, sample by sample:

- `grand` (**Hammerwork Grand**) is a piano: per key, felt hammers strike
stiff strings that drive a soundboard through a moving bridge.
- `kit` (**Skinwork Kit**) is a drum kit: sticks and beaters strike
membranes and cymbals.

They share a small layer of physical-modeling kernels,
`kernels/10-physical/`. **Status:** built 2026-10-16 on branch
`claude/physical-piano-drums`.

**Clean-room.** Everything here comes from the published literature
cited inline and from measurements of CC0 recordings. Nothing comes from
another physical-modeling codebase or its data. The scale curves,
hammer and kit constants are literature values or our own estimates, and
each is marked below.

Code:
- `kernels/10-physical/modal.fy`: exact modal step, T60 → decay rate,
implicit power-law contact.
- `kernels/10-physical/bessel.fy`: the circular membrane's first 40
modes, generated by `tools/fit/bessel.py`.
- `kernels/06-voices/piano.fy` and `machines/grand/`: the piano.
- `kernels/06-voices/kit.fy` and `machines/kit/`: the kit.

## The building blocks (`kernels/10-physical`)

| word | what it is | source |
|---|---|---|
| `zoh-coefs` `modal-step` | one damped mode advanced exactly under a force held for the sample: the undamped closed form plus a decay factor. It is stable at any frequency below Nyquist and keeps the asked-for decay at any pitch. | modal synthesis, Adrien 1991 |
| `t60>sigma` | the decay rate that falls 60 dB in T60 | — |
| `contact-solve` | a striker pressed into a body with `F = K e^p (1 + α ė)`, solved implicitly by four Newton steps on the scalar compliance equation `e = e_free − D F`. It is stable for treble-hammer stiffness, where an explicit force blows up. | Hall 1986, Stulov 1995 (felt); Hunt & Crossley 1975 (damping); Bilbao 2009 ch. 4 |
| `memb-ratio` `memb-m` `memb-shape-at` | circular membrane modes: Bessel-zero ratios j_mn/j_01, nodal diameters, and the radial shape J_m(j_mn r/a) at any radius | Rayleigh; Fletcher & Rossing §3.6 |

A body is a bank of these modes in fixed-size state arrays, unrolled with
`times`. Each mode's coefficients are built at note-on, so the render only
multiplies and adds. The words take physical units (metres, newtons,
kilograms), so a striker's character comes from its mass and felt law,
not from a filter.

Other techniques the two voices use, each a candidate for its own kernel
once a third machine needs it:

- **Moving-bridge coupling.** A modal string whose end is shaken feels
`−c_n b̈` with `c_n = 2(−1)^(n+1)/(nπ)`, and pushes on the bridge with
`Σ (−1)^(n+1) nπT/L q_n`. This is a textbook derivation from the string's
kinetic energy.
- **Resistive dense-mode board.** Above its first modes a board acts like
a resistance at the bridge (an infinite plate's mobility `1/(8√(D m''))`,
Cremer). It is high-passed so the board stays stiff at low frequency.
- **First-order tension modulation.** Each mode's step rotation is bent
by the head's strain energy. The step stays a rotation, so it stays
stable, where an explicit stiffness force diverges on the upper modes
(Berger's approximation; Bilbao 2009 ch. 13).
- **Bounded cubic mode coupling.** One shared coordinate carries a
quartic potential, so the coupling only moves energy between modes.
Its stiffness is capped so the explicit step stays stable.
- **Diffuse-field energy store.** The modes too dense to simulate are one
energy reservoir fed by the discrete modes and radiated as band noise:
statistical energy analysis (Lyon 1975).
- **Lumped collision rattle.** A mass on a spring that collides with a
body through `contact-solve`. Its collision force, times noise, drives a
wide resonant bank. This is the snare wires.

## Hammerwork Grand

Per voice (one key), from `piano.fy`:

| part | model |
|---|---|
| strings | Up to 3 unisons: 1 string below B0, 2 below G#2 [G]. The main string carries 56 modes, the partners 16 each. Above the 16th partial the main string's modes stand for every string of the key. Bass keys stride the main string's upper modes (every 2nd to 4th partial, weighted √stride), so a low A still reaches about 7 kHz. |
| partials | `f_n = n f0 √(1+Bn²)/√(1+B)` (Fletcher 1964). B is lowest near C3 (1e-4) and rises about 3 %/semitone into the treble [G, matched to the Kawai: 2.3e-4 at C4]. |
| scale | length `0.05·2^((108−m)·0.0767)` m, capped at 1.95 [G]. Tension 700 N, rising to 1510 N in the bass [G]. Mass per length follows from f0, so string mass is physical. |
| loss | `σ_n = b1 + b3 ω_n²` (Chaigne & Askenfelt 1994), fitted through the fundamental's T60 `30·2^(−(m−21)/26)` s and HF DEC at 4 kHz [G, fitted to the Kawai]. |
| hammer | Mass, stiffness and exponent interpolate Chaigne & Askenfelt's C2/C4/C7 values: 4.9/3.0/2.2 g; K 1e9/4.5e9/1e11; p 2.3/2.5/3.0. Hysteresis α = 0.6·FELT s/m. Speed is 0.25 m/s at velocity 0 to 6 m/s at 127. |
| strike point | 1/8 of the string in the bass to 1/12.5 at the top [G]. The unison strings meet the hammer ±3 % apart, which fills the strike-point notches. |
| bridge / board | 12 plate modes, 58–985 Hz, ζ 1.6–3.4 % [G]. Each key's bridge point lies on a diagonal across the board. A resistive dense-mode term (2e-4·COUPLE s/kg, high-passed at 220 Hz) is what lets the strings lose energy at a realistic rate and gives the 6 dB/oct radiated rise. The shared bridge couples the unisons: in-phase motion drains fast, out-of-phase motion lingers (Weinreich 1977). |
| output | Board acceleration seen from two microphone points. The dense-mode part pans by key (bass left, treble right). |
| dampers | A released key adds decay to each mode by how far the damper point (strike + 3 %) moves in it. RELEASE sets the fundamental's T60. Keys above E7 (MIDI 88) have no damper. PEDAL lifts all dampers live, for held and released notes, re-blended every 32 samples. |

**Measured against the CC0 VCSL Kawai grand** (`v3` layer against
velocity 0.7, partials relative to the fundamental over 0.05–0.35 s,
analysis scripts kept in the session scratch):

| | Kawai C4 | Grand C4 | Kawai C5 | Grand C5 |
|---|---|---|---|---|
| B | 3.0e-4 | 2.3e-4 | 3.1e-4 | 4.8e-4 |
| p2..p6 dB | −2 −19 −13 −9 −26 | 6 −9 −7 −18 −18 | −18 −18 −22 −39 −30 | −7 −20 −24 −29 −29 |
| T60 fundamental | 9.5 s | 5.2 s | 9.7 s | 8.0 s |
| spectral centroid, first 100 ms | 888 Hz | 1238 Hz | 961 Hz | 1384 Hz |

Velocity runs from a 550 Hz centroid at velocity 0.1 to 2.2 kHz at 1.0,
over 26 dB of level. Against the Kawai, the model's C4 is brighter in its
2nd to 4th partials and decays about half as long.

**Cost** (ReleaseFast): about 200 ns per ringing voice, 735 ns for the
bench chord. The render word has more stores than lane mode can stash,
so the 16 voices render scalar ("renders without NEON lanes"). Note-on is
a `call:` composition of seven stages, each under the compiler's
pending-store limit.

**Not modeled:** sympathetic resonance between keys (each voice has its
own board), the duplex scale, longitudinal "phantom" partials, the
second polarization, and key and pedal noise.

## Skinwork Kit

Twelve voices, `note-pitch`, on the GM map. Notes outside 35–59 fold
into it by octave.

| note | piece | body | striker |
|---|---|---|---|
| 35, 36 | kick | membrane, 52 Hz, a 0.229 m head | felt beater, 35 g, p 2.3 |
| 37, 39 | side stick | snare head struck at the rim, light wires | stick across the rim |
| 38, 40 | snare | membrane, 185 Hz, 0.17 m, with wires | hickory stick, 12 g, Hertz p 1.5 |
| 41, 43 | floor tom | membrane, 82 Hz | stick |
| 45, 47 | mid tom | membrane, 118 Hz | stick |
| 48, 50 | high tom | membrane, 165 Hz | stick |
| 42 / 44 / 46 | closed / pedal / open hat | plate, 420 Hz | stick; closed and pedal choke open |
| 49, 52, 55, 57 | crash | plate, 190 Hz | stick |
| 51, 59 | ride | plate, 240 Hz | stick |
| 53 | ride bell | plate, 560 Hz | stick |

The lower note of each tom pair, and the second kick, sit a little lower
(×0.86). Every size, mass, tuning and T60 is an estimate of a typical kit
[G].

- **Heads** use the Bessel ratios, compressed slightly (ratio^0.94) as
air loading does to a real drum. They are struck at a radius where
`J_m(j_mn r)` sets each mode's drive. Loss is `σ = s0 + s1 f²` through
two T60s.
- **Tension modulation.** The gain is `G = 150/c²` per ply (c is the
head's wave speed). That follows from the Mylar's elastic stiffness
against its tension: at 1 mm of excursion a snare head runs 2–3 %
sharp. GLIDE scales it.
- **Strikers.** A hickory stick is a Hertz contact, `K = 4/3 E* √R`:
4.4e8 on Mylar, 1.2e9 on bronze. The kick beater is felt. The stick's
impact force, high-passed at about 1 kHz, adds the click.
- **Snare wires.** A 4 g lumped mass on a 100 Hz spring, resting a hair
under the head center. WIRES sets the gap. Collisions drive eight wide
buzz modes from 2.1 to 7.6 kHz.
- **Plates.** 40 modes on a stretched, jittered series
`((i + 1.2)/1.2)^1.17`. Strike weights fall as ratio^−0.7. A bounded
cubic coupling (BLOOM) moves energy between them, and a diffuse field
takes over the upper modes' energy as band noise: 1.8–4 kHz high-pass,
6.5 kHz two-pole low-pass.

**Cost:** about 530 ns per sample in Debug for the bench hit sequence.
The render word, like the piano's, renders without NEON lanes.

## Presets

- `grand`: default, bright-pop, felt-ballad, honky-tonk, sustain-wash,
dry-studio.
- `kit`: default, jazz, rock, funk-dry, gong-wash.

## Next

- **Fretwire and Rhodes.** Fretwire could take `contact-solve` for the
pluck and fret buzz, and moving-bridge coupling for its two-stage decay.
Rhodes could become a struck tine-and-tonebar mode pair with an
implicit hammer.
- **Lane mode.** Splitting the render into a lane-eligible word, or
raising the stash limit, would roughly halve the cost.
- **Sympathetic strings.** These need a shared board across voices: a
host-side bus or a single-voice piano.
Loading
Loading