Make sound by exciting modeled strings and resonators, then shape how they vibrate, decay and interact.
Sound from resonating objects instead of oscillators. Four per-voice audio sources (siblings of saw / granular — keyed to pitch, retriggered per note, deterministic), following the classic exciter → resonator decomposition: a short wideband burst drives a long-memory linear body, and Volta spells that composition literally — the exciter is an ordinary audio argument.
On top of the four sources sits the resonator algebra (§1.5–1.7): a body's mode table can be fitted from a recording (modalOf), a body can be hung on any signal chain rather than an exciter burst (src |> body …), and a bank of coupled strings can be hung across a whole bus (|> sympathetic). The modal stdlib module (The standard library) ships seventeen measured tables to start from.
Hear it and change it#
Listen for: a sharp excitation becoming the ringing body of a string. The source is a physical model, so you can change the damping and the place where the virtual string is picked.
tempo = 112bpm
timingVersion = 2
harpvoice = patch {
source = string (freq pitch) 0.22 0.7 0.18,
gain = vel * 0.4
}
play harpvoice "c4 [g4 bb4] eb5 g4" |> reverb 0.41 0.2
Make it yours: change the pick position from 0.18 to 0.45. Different positions emphasize different parts of the string's tone. Explore Physical modeling for bowed strings and resonators.
1. The primitives#
bell = patch {
source = modal (freq pitch)
[[1, 3.2s, 1], [2.76, 2.1s, 0.55], [5.4, 1.3s, 0.32]],
gain = vel * 0.6 + 0.2
}
harp = patch { source = string (freq pitch) 0.22 0.7 0.18 }
cello = patch { source = bowed (freq pitch) (0.35 + vel * 0.45) 0.14,
ampEnv = adsr 120ms 0ms 1 400ms }
bowl = patch { source = modal (freq pitch) modes (scrape 0.65) }
modal freq [[ratio, decay, amp], …] [exciter]#
A bank of resonators, one per mode (engine/src/modal_bank.rs: f64 complex one-poles, each an exact sampled damped sinusoid — pitch, T60 and level exact at every rate; a glide keeps each mode's amplitude and phase):
ratio— compile-time constant > 0; multiplies the played fundamental. Inharmonic stacks (2.76, 5.4, 8.9…) are bells/gongs; near-integer stacks are marimbas/tines.decay— that mode's T60 as a compile-time duration (3.2s,800ms, or a bare scalar in seconds).amp— compile-time level (clamped ≥ 0).- ≤ 64 modes. Every mode below Nyquist sounds; one at or above it is silent, holding its state in case a glide brings it back.
exciter— optional; any audio signal. Defaultstrike 0.5. A sustained exciter (scrape,noise * env, asample) keeps the bank singing instead of striking it.
Level and the peak bound. The bank's gain is not unity: each mode's impulse response has amplitude amp·(α/sr)/θ_lowest (α = ln 1000 / T60, θ_lowest the lowest mode below 0.45·sr in radians per sample; the old direct form's (1 − R²)/(2·sin θ) to first order), so a struck bank gets louder as the note falls and as the decay shortens. One [1, 1s, 1] mode struck by strike 0.5 peaks near −22 dB at C4 but +16 dB at C0 and +27 dB at C1 with a 20 ms decay (measured offline over 60 seeds, not listened to). The compile-time bound (OutputPeak, refine.rs::modal_burst_bound) models it for a strike/pick/scrape exciter (also scaled by a constant): per mode a Gaussian σ from the burst's envelope and noise spectrum, the modes combined through their correlation in the burst's spectrum, then 3.3 σ — calibrated against 49 seeds × 273 configurations at 44.1/48 kHz (worst 2.71 σ) and 2000-note runs (worst 2.75 σ); engine/tests/peak_calibration.rs re-checks it in both directions. It is evaluated over the MIDI pitches the patch's tracks play (event pitches, |> pitch lanes, chooseBy alternatives); a patch no track plays is bounded down to MIDI 0 and reported as "not statically proven" rather than warned. Typical slack is 1.2–2.5× over a 2000-note render, more where the exciter's param or the pitch range is wide. The level is the same at every device rate: the bank integrates its drive over time (its input gain carries 48 kHz / sr) and the burst's noise keeps its density per hertz (√(sr / 48 kHz)), so a struck note, a resampled exciterOf or a sample through body rings within 0.5 dB of its 48 kHz level at 44.1, 96 and 192 kHz (and the same burst fills a string's loop at the same level); at 48 kHz both factors are exactly 1. Any other exciter (a sustained signal, exciterOf) and a fitted modalOf table keep a nominal ±1 per unit of drive level — exciterOf "x" 40 * 12 is bounded at 12 — never below ±1, and not a proof: a sustained exciter at a mode's frequency rings far past it.
string freq damping [brightness] [pickPos] [exciter] [stiffness] [sympathy] [bridge] [weight] [bank] [group]#
Extended Karplus-Strong waveguide: a fractional-delay loop (first-order allpass read → pitch accurate to a few cents across the note range), a one-pole loss filter, and a pick-position comb on the excitation.
damping(0..1) — log-mapped sustain: 0 → ≈ 6 s, 1 → ≈ 60 ms T60 at 220 Hz. The T60 tracks pitch like a real string,× (220/f0)^0.4: the same knob is ×1.32 at 110 Hz and ×0.57 at 880 Hz (≈2.3× shorter across three octaves). 220 Hz is the calibration anchor — a patch sitting around A3 decays exactly as it always did.brightness(0..1, default 0.5) — the loop loss filter (dark → open). The corner is note-relative (how many harmonics survive, not an absolute Hz):(800 + b·11200)/220 · f0, clamped into the old 800 Hz…12 kHz band, so again 220 Hz is unchanged. Below the anchor a bright string is less glassy, above it a dark string stays playable instead of losing its own fundamental.pickPos(0..1, default 0.28) — pick-position comb depth/color.exciter— optional; defaultpick 0.7.stiffness(0..1, default 0) — dispersion: stretched partials, ideal string → piano → sitar. §1.8.sympathy(0..1, default 0) — a shared bridge across this patch's voices: inter-string ring + two-stage decay. §1.8.bridge("bus"default,"passive"),weight,bank,group— the passive scattering bridge and who shares it. §1.10. Usually written as named options:string (freq pitch) 0.15 0.8 0.13 (strike 0.6) 0.28 0.4 { bridge = "passive" }.
bowed freq pressure [position] [sympathy] [weight] [bank] [group] [speed] [brightness] [rosin] [body] [noise] [stiffness] [touch] [touchAmount] [fingerNoise] [mute] [strings] [string] [bowLength] [bowChange] [bowDir] [messa] [understrings] [buzz] [bounce] [throw] [bowMass] [bowPoint] [attackPlan]#
A physical bowed string (Stage 9, Bowed Strings §6). A digital waveguide run near 192 kHz (4× at 44.1/48 kHz, 2× at 96 kHz, decimated through a Kaiser-windowed FIR) meets the bow at a stick–slip junction: while stuck, the contact moves with the bow as long as the force stays under μs·N; slipping, the force follows the hyperbolic friction curve, and the string re-sticks only when the slipping solution vanishes (Friedlander's hysteresis rule; McIntyre, Schumacher & Woodhouse 1983). The contact also drives a faster, heavily damped torsional waveguide pair (Serafin, Smith & Woodhouse 1999 — impedance ratio 0.6, 5× the transverse speed, Q 3 at 110 Hz rising as √f0), which is what keeps the Helmholtz motion robust against multiple slipping. Body radiation is outside the loop.
What that means for playing — it behaves like a bow on a string:
- Loudness is bow speed and contact (Helmholtz amplitude ∝ v/β), force sets the brilliance and whether the string speaks. Schelleng's region: too little force for the speed and contact gives two or more slips per period (surface sound, octave or twelfth up); too much gives a flattened, raucous, creaking tone. The minimum force rises with speed and toward the bridge (∝ v/β²), the maximum with speed and away from it (∝ v/β).
- Documented playable region: normal force
N = 1.4·pressure^3.5with force ratioN·(μs − μd)·β/vbetween 0.15 and 0.6 andN·(μs − μd)·β²/v ≥ 0.012(v = 0.3·speed,β = 0.027 + 0.2·position,μs − μd = 0.4 + 0.75·rosin). Inside it, with a sustained-tone stroke, the string is in Helmholtz motion (one slip per period) in 390 of 392 measured cells (violin G3–E7, cello C2–A5; Stage 8: 248/392). - Attacks (Guettler): Helmholtz motion from the first slip needs force in proportion to the bow's acceleration. A stroke that lands with a pressure bite (≈ 1.5× for 40–150 ms, longer than ~12 periods on low strings) while the bow accelerates over 20–55 ms speaks clean. Too little force for the acceleration gives a loose, multiple-slip start; too much a crunchy, creaking one (pre-Helmholtz raucous motion). Low strings speak slower, as on the instrument: a quick stroke is within 3 cents after 144 ms on a cello C2, 85–111 ms from G2 up, 192–430 ms on a bass E1 (the bare string; the library's bass on its strings, below, speaks in 120–250 ms).
- Tuning is feed-forward (the loss one-pole's delay on the bridge side, the dispersion cascade's on the neck side) plus a period-relative servo that only trims (±1.2 % of the period) and carries across a legato line: worst 0.03 ct for held notes C2–E7 at 44.1/48/96 kHz (Stage 8: 1.3 ct). A heavy bow near the top of the region flattens a few cents, as real strings do; the servo removes it.
Options:
pressure(0..1) — live bow force; envelope the bow here. Hand controls smooth over 0.5 ms.position(0..1, default 0.5) — live contact (β = 0.027…0.227of the string from the bridge). Pitch and contact smooth over 2 ms.sympathy,weight,bank,group— shared passive bridge (§1.10). The bridge's correction holds across the internal sub-steps of each sample.speed(−1..1, optional) — signed velocity; zero stops, negative reverses. Omit to derive it from pressure.brightness(0..1, default 0.5) — the string's high-frequency loss: a one-pole cutoff of 1.2–12 kHz at A4 (log-mapped), scaled by √(f0/440), so a mode's decay rate is fixed in Hz across registers as on a real string. Bare-string slope 1–10 kHz at the reference hand: violin A4 −10.7 dB/oct, cello A3 −10.0 (Stage 8: −14.0 and −15.6). Brighter strings have a narrower attack window (measured).rosin(0..1, default 0.5) — grip:μs − μd0.4…1.15 over μd 0.3. Rosin that has rested grips up to 30 % harder until sliding warms it, a rate-and-state bonus that helps the first breakaway.body—"none"(default),"violin","viola","cello","bass", the folk bodies"hurdyGurdy","damore","hardanger","erhu","morinKhuur"(§1.12), output-only.noise(0..1, optional, live) — seeded colored jitter of the normal force (±½ at 1), scaled by bow speed, strongest while slipping and during the attack. Deterministic per note.stiffness(0..1, optional, latched at note-on) — inharmonicity: an allpass cascade (B = 4e-3 at 1) on the neck side of the bow, so the contact stays wherepositionputs it. (On the bridge side its delay moved the effective contact by up to a quarter of the loop — the real cause of Stage 8's "stiffness knocks the bow out of Helmholtz motion".) At 0.2 and 0.5 the playable region keeps 449–450 of 476 cells single-slip against 456 without stiffness. A bowed stiff string mode-locks its partials to a period shorter than the fundamental's, so the cascade's delay comes out of the loop at the 2.6th harmonic, not at f0: full stiffness on a C2 (forty times a real C string's B) bows +0.01…+0.02 ct (compensated at f0 it locked ~20 ct sharp and the servo took 2.5 s to trim it); the free ring after the bow lifts sits a little flat of the bowed pitch, as a stiff string's does.touch(0..1, optional, live) — a light finger at that fraction of the sounding string from the nut: 0.5 the octave harmonic, 1/3 the twelfth, 1/4 two octaves, 1/5 the major third above; 0 or absent is no finger. It is a damper of resistance R on the string (the neck segment split at the finger), so only the modes with a node there survive: measured, the harmonic series at its exact pitch with the fundamental 48–95 dB down. Artificial harmonics are a stopped pitch plus a touch:freqD4 withtouch = 0.25sounds D6. Bow harmonics lightly, fast and near the bridge (pressure ≈ 0.6, speed ≈ 0.6, contact ≈ 0.15; the bow must sit inside the harmonic's first loop) — a heavy bow chokes them and the finger then stops the string (a fifth above at 1/3), as on a real string. The finger sits on the touched harmonic's node as that harmonic travels the loop (the loss filter's and the dispersion cascade's phase delays at n·f0, not at f0), and a touched string is bowed without its own inharmonicity cascade: lumped on the bow's side of the finger, the cascade detuned the node under any pitch motion (a 0.5-cent vibrato took a violin A4's lead from 120 to 52 dB on its strings, 70 dB without; with the cascade bypassed 70). What remains under vibrato is the moving delay lines' interpolation floor, about 70 dB.touchAmount(0..1, optional, live; default 0.5) — how hard the finger presses:R/2Z = 4·a/(1 − a), 0 lifts it, 1 stops the string there.fingerNoise(0..1, optional, live; absent or 0 is a silent hand and the same audio bit for bit) — the left hand's own sounds, read from the pitch control and entering at the stopped end as a moving termination, so the string filters them (engine/src/left_hand.rs). A pitch that steps or slurs up faster than 60 semitones a second (a step,-g4, a 15 msslide) is a finger hammered on: a 0.6 ms velocity pulse and, for about 1.5 ms, a slightly lossy termination (reflection 0.9, the pad before it is pressed home). Falling, it is a lift-off: the finger drags the string for 3 ms and lets go in a quarter millisecond (equal-area lobes, a soft pluck). A slow upward shift of a semitone or more ends in a softer landing (0.35). A shift — a glide faster than 12 st/s for more than 20 ms, which vibrato never sustains — scrapes: on a wound string a jittered ridge train at the finger's speed over the winding pitch (scale length 0.33–1.05 m, ridges 0.12–0.4 mm by family, so a few kHz, rising with the slide's speed), band-passed there; on a plain string (a violin's E, above about E5) a lowpassed hiss about 9 dB quieter. The slide's pitch itself is the ordinary glide. Measured atfingerNoise = 1on the free string (no bow to mask it) re the bowed note, a mid hand: an impact −27…−33 dB over its first 10 ms (violin G3–E6, viola, cello C2–D3, bass E1), 3–12 dB lower 30 ms on; lift-offs −34…−46 dB; scrapes −30…−41 dB on wound strings with the loudest third octave at 4–5 kHz, −47 dB on a plain one.0.5is 6 dB under all of these. Across 840 held notes after steps, lift-offs, shifts and slurs (every documented-region hand of the slip grid on six pitches) the single-slip count is the same with and without it (828); a deeper or longer termination dip did flip high notes (E6, A5) into double slipping, so it is kept at 0.1 for 1.5 ms. Cost about +10–20 ns per output sample.mute(0..1, optional, live, smoothed over 40 ms; absent or 0 is no mute and the same audio) — a mute clamped on the bridge (engine/src/sordino.rs, Mute under §1.12): 0.5 a concert mute, 1 a heavy practice mute. Output only (it sits between the string and the body), so the bow's motion is untouched; withbody = "none"it still loads the bridge (a violin's).strings("none"default,"violin","viola","cello","bass"; prepared) — the instrument whose four strings the bow plays (violin G3 D4 A4 E5, viola C3 G3 D4 A4, cello C2 G2 D3 A3, bass E1 A1 D2 G2;engine/src/bowed_strings.rs). The note is a stopped length of one string (r = f/f_open) or the open string, and the string's make and the finger shape the loop: inharmonicityB_open·r²(plain steel E 5e-5, wound cores 2e-5…1e-4) added tostiffness's; a loss cutoff ×0.6…1.15 (heavy wound strings darkest, plain steel brightest); a torsional Q ×0.42…1.15 (the bass E most damped); a bridge reflection that losesztimes as much on a string of relative impedancez(1 on the top string, to 2.2 on the cello C); output ×√z; and the bow's contactβ·r^⅓(the bow keeps its place while the vibrating length shortens, players move toward the bridge in high positions). A stopped note ends at a finger pad (reflection 0.993, a 9 kHz corner combined with the string's own loss as1/fc² = 1/fc_s² + 1/fc_f²); an open note (within 8 ct of the open pitch; a vibrato stays open until 50 ct) ends at the hard nut.touchstill splits the sounding length, so a touch on a stopped note is an artificial harmonic of that length.string(0..7, optional, live; read withstrings) — the string, counted from the highest as players number them: on a violin 1 is E (I), 2 A, 3 D, 4 G (sul G). 0 or absent is auto: the highest string whose open pitch the note reaches (the lowest position); a legato line then stays on its string until the note passes the next string's open pitch (an open-string pitch reached from below is a fourth finger) or falls below its own; after 100 ms with the bow off the string the next note chooses afresh. A change of string in the middle of a line is a crossing: the string the bow leaves rings on as a free ring (its own loop, last pitch, nut or finger, up to three), the bow lands on the new one with half the motion it carried, and crossing back takes up the string that still rings. On a shared bridge (sympathy/group), the notes of a chord that start together take distinct strings, the highest note the highest string it reaches and each lower note the next free string below:[a4,c5]on a violin is A4 on D and C5 on A, bit-identical to[a4(string=3),c5(string=2)]in the library.fingerNoisehears the chosen string (plain or wound) and treats a crossing as a new finger or the open string, not a slide.bowLength(metres, optional, latched) — a finite bow in a player's hand (engine/src/bow_stroke.rs), in front of the junction: it reshapes pressure, speed and contact once per sample and leaves the string physics alone. Needsspeed. The contact travels along the hair with the bow (speed = 1is 1 m/s; a violin's hair is about 0.65 m); a fresh voice starts a down-bow near the frog and an up-bow near the tip, and a stroke after a 150 ms rest is set at its own end again. When the hair runs out the bow changes by itself, timed so the turn lands at the end. The arm's weight tilts the normal force by ±12.5 % from frog to tip (the part a player leaves uncompensated), so a down-bow starts heavy and lightens and an up-bow builds. Knowing the note's length (its release), the player plans the bow after the attack (120 ms): a note that would need more hair than is left gets a slower bow (to 0.6× the written speed, drifting no lower than 0.45×) nearer the bridge (β·√k, the same force: Schelleng's minimum force stays where it was, the loudness gives √k), or evenly spaced bow changes when even that is too little. 0 is an endless bow that still shapes its reversals; absent, no hand at all (legacy calls are unchanged). A slur is one stroke. On a mono glide voice the lowerer stamps each note of a slur group (a trailing_, a leading_/-, or legato overlap;'breaks it) with the group's span (TrackEvent.slur:compiler/src/lower/slurs.rs), and the plan budgets the hair over the whole group instead of the note: one direction, the speed set so the group fits (down to 0.6×), and when it cannot, a bow change hidden at a note boundary — when the hair left at the end of a note will not carry the next one, the reversal starts half abowChangeearly so the turn lands with the finger; the stroke keeps at least to the current note's end. A note longer than what the hair can hold at 0.45× still changes mid-note. The next stroke alternates from the way the bow last travelled. The stamp is only written for a finite-bowbowedon a mono glide synth, so no other IR changes; a timeline region's events and a pattern slurred all the way round are planned note by note.bowChange(seconds, optional; default 40 ms) — every reversal withbowLength(a flippedspeedsign — a new stroke, a tremolo turn — or a forced change) followsd(t) = to + (d0 − to)·(1 + cos(πt/T))/2: decelerating to zero and accelerating back, the pressure dipping 6 % as the bow slows and catching the string with up to 30 % more as it accelerates away (Guettler's force in proportion to acceleration, which peaks at the turn). A bow already at rest sets off at once.bowDir(−1..1, optional) — a stroke direction request withbowLength, read at each fresh stroke (a re-attack): 1 down, −1 up, 0 follows thespeedsign. Asking for the way the bow already travels is a retake (re-set at the frog or tip, no reversal).bounce(0..64, optional, per stroke) — a bouncing bow (engine/src/bow_bounce.rs): the bow gets a vertical degree of freedom at the contact and the bowing force becomes a contact force, zero while the bow is in the air. The bow there is a mass on springs: the mass felt at the contact of a bow pivoting in the fingers (I/d²: a 60 g violin bow is 38 g at the balance point, 5 g near the tip), the hair (a tensioned ribbon), the stick (a tapering cantilever,K/d⁴) and the string's transverse reaction in series, a Hunt–Crossley contact for a restitution of 0.8, gravity, and the hand carrying 80 % of the weight and holding the bow like an 8 Hz damped spring. Its contact frequency is highest near the balance point (11.6 Hz at 0.3 of the hair, 9.9 at the middle, 7.0 at 0.8 — the shape Askenfelt & Guettler's "The bouncing bow" (1998) report, numbers this model's). The value is the contacts per throw before the hand catches the bow: 1 spiccato (thrown onto the string so it lands at the stroke's onset, one bounce, caught until the next stroke), 2 or more ricochet (one throw, bounces of falling height), 0 sautillé (the bow stays on the string and each stroke pushes it, so a stroke rate near its bounce lifts it every stroke). The junction is driven with the patch pressure × (force / static force)^(1/3.5). With a finite bow, each bounced stroke is set atbowPoint.throw(cm, optional; default 1) — withbounce, the height the bow is thrown from (its landing speed against the static force and the hand's spring); withbounce = 0, the size of each stroke's push.bowMass(g, optional; default 60) — withbounce: viola 70, cello 80, bass 135; a heavier bow is stiffer by √(M/60 g) and bounces a little slower (9.4 Hz at 135 g).bowPoint(0..1, optional; default 0.3) — withbounce, where on the hair (frog to tip) the bow bounces.attackPlan(0..1, optional; read at each fresh stroke) — the attack planner (engine/src/bow_attack.rs): the engine plans the stroke's bow acceleration and force from the string's pitch, contact, torsion and rosin, after Guettler's conditions for Helmholtz motion from the first period. While the contact first sticks, the friction it needs climbs as a staircase (each bridge-side round trip returns the injected velocity) and stays underv_b/(β·k)(k = 1 + Z0/Zt) until the nut's reflection returns; the plan holds the normal force at0.9·v_b/(β·k·μs)(rested rosin), following the accelerating bow, so that returning wave lets the string go; at the string's first release the hand catches at1.35·v_b/(β·k·μs)(warm rosin) and relaxes topressureover max(T0/2, 70 ms). The bow reaches its speed in max(0.3·T0, 6 ms). Givespeedandpressureat their held values from the stroke's start; 1 is the full plan, between blends it in, absent or 0 the hand's own attack. After the release the hand listens: over every four periods it counts the string's slips, and at 1.75 a period or more (multiple slipping; on a touched harmonic, 1.75 times the harmonic's number) it presses into the catch again, 1.2× harder, at most twice. With a finite bow (bowLength, 0 included) every bow change of the stroke is planned too: a reversal through zero is a fresh attack on a ringing string with the stroke's own acceleration, so through the change the force follows the hold's law0.9·v_b/(β·k·μs)on the bow's speed — down with the slowing bow, back up with the new stroke — never under 0.2× nor over the hand's force unless it hears the string multiple-slip (then it presses as above, past the hand's force), and the first release after the zero crossing hands back over max(T0/2, 70 ms). A change that starts while the stroke's own plan runs (the bow still moving) takes that plan over. On a touched harmonic the plan is the same. Measured (bow_attack_lab.rs): the staircase predicts the first slip within 0.05 period on bass E1, A1, cello C2 and violin G3 (lab_first_slip); Guettler-style attack diagrams (lab_attack_diagram, acceleration 0.25–256 m/s² × force) on a bass E1 at a constant force show no perfect cell at all (sticking for two to five periods, then multiple slipping) with the rested-rosin aging and 27 of 176 without it; the plan raises the perfect share but the heaviest strings' attacks stay chaotic (neighbouring settings move single onsets by ±100 ms). The listening hand takes the standard tier's seed-sensitive cells (CENSUS_SEED3: 147/149/22 of 150/150/25 single-slip, periodic, rising) to 150/150/25 and leaves seeds 0 and 2 there. Over 225 deluxe bow changes on held notes (lab_turns, three rosin-noise seeds) one slip per period returns in a median 80 ms, p90 150 ms, 2 over 300 ms; the same changes unplanned 80 / 210 ms and 9, by the hand's attack 80 / 190 ms and 2. Cost: active only for a stroke's first 0.1–0.5 s and about 0.5 s after each bow change (an idle check per sample otherwise); priced at the 0.5-unit floor.messa(−1..1, optional, live) — a held note's bow shape withbowLength, over the note from its re-attack to its release: positive is a messa di voce (speed1 + 0.6·m·(sin πu − ½)), negative a diminuendo through it (1 + 0.4·m·u); the force follows asg^(1/3.5)so the force ratio holds. Notes under 0.3 s are left alone; the shape grows in up to 0.9 s.- The folk instruments (
engine/src/bowed_folk.rs):stringsalso takes"hurdyGurdy"(the G4 chanterelle),"damore"(seven strings, A2 D3 A3 D4 F♯4 A4 D5),"hardanger"(A3 D4 A4 E5),"erhu"(D4 A4) and"morinKhuur"(B♭2 F3). What stops the string is the instrument's: a hurdy-gurdy key's wooden tangent is a hard stop (reflection 0.997, no pad corner) and withfingerNoiseit lands 1.8× harder and in 0.25 ms, and never scrapes (a key cannot slide); an erhu's fingertip presses the string in the air (0.975, a 3.5 kHz corner); a morin khuur's fingernail stops it from the side (0.985, 5.5 kHz). The morin khuur's strings are horsehair bundles: heavy at the bridge (impedance 1.9–2.3, so lossy), dark, slack in torsion, and every bridge reflection is jittered by a seeded, 2.5 kHz-band noise of 1 % RMS, so each partial carries a noise skirt in proportion to its level (measured: the floor between the first twelve partials of an F3 −63.2 dB re the partials, −72.3 without the jitter). understrings(0..1, optional, live, smoothed over 20 ms; withstrings = "damore"or"hardanger", otherwise ignored) — sympathetic strings under the fingerboard: the d'amore's seven in unison with its playing strings (D major), the Hardanger fiddle's five (B3 D4 E4 F♯4 A4). Each is a lossy waveguide loop at the output rate (a linear-interpolated delay tuned past its loss one-pole, 5 kHz loss corner, a 3.5 s decay at its fundamental) driven by the bowed string's bridge signal (after the mute, before the body) and radiating through it. The coupling is one way, so it is stable by construction and the bow's motion is the same sample for sample (the slip count of a d'amore D4 is identical with and without); absent or 0 is the bare instrument bit for bit.buzz(0..1, optional, live, smoothed over 20 ms) — the hurdy-gurdy trompette's buzzing bridge: one bridge foot, the dog, is loose on the soundboard and held down by the tirant. It is a mass on a spring (250 Hz, ζ 0.3) hitting a stop (a one-sided 16 kHz contact, ζ 0.5) driven by the string's bridge wave at the bow's internal rate (≈ 192 kHz, so the impacts are decimated with the string and do not alias): under the tirant's threshold the dog sits and passes the force; over it the dog flies, the board sees the clipped force, and the dog lands, once a period. Its departure from the string's force radiates 3.2× (the dog strikes the board directly).buzzis the tirant's slack: the threshold is1.35·2^(1 − 2·buzz)in the bridge wave's units, where a D4 string at contact 0.2 and pressure 0.75 reaches ≈ 3.3 × its speed, so at 0.5 it buzzes from a steady speed of 0.45 and a resting wheel at 0.35 buzzes only on accents. Output-only: the bow's slip count is identical with and without. Absent is a rigid bridge.bodyTo,bodyMorph,bodySize,scale,pickup,pickupPos— a morphing or resized body, the giant string scale and the electric pickups: § The impossible strings below.
p = patch { source = bowed (freq pitch) 0.75 0.3 {
speed = 0.5, brightness = 0.8, rosin = 0.6, body = "violin", noise = 0.2
}, gain = vel * 0.2 }
flageolet = patch { source = bowed (freq pitch) 0.6 0.15 {
speed = 0.6, touch = 0.5, body = "violin"
}, gain = 0.2 }
Existing calls keep their arguments and IR; audio changes (Stage 9). Cost: ≈ 290 ns per output sample for one held bow at 48 kHz in an optimized build (about half that per sample at 96 kHz, where the junction takes two sub-steps instead of four), ≈ 43 baseline units in the compiler's DSP-load estimate, which prices each option and body tier separately (Understand audio processing cost § Bowed strings).
The bowstrings library#
examples/songs/bowstrings.volta builds playable instruments from bowed, string and stringBody; bowlight auditions them (The standard library).
import "bowstrings" (violin, celloPizz, violinRing)
line = play violin "d4(dyn=0.4) f4_ a4 -g4 'c4@2" |> violinRing
- Solo = legato.
violin,viola,cello,bassare mono glide voices (Glide between notes): the string rings through a phrase. A plain note is a new stroke and strokes alternate direction (toggle (linram 1 0 1ms)— only a re-attack re-fireslinram); on the standard tier the bow change passes through zero speed in 10 ms with a pressure dip, with a finite bowbowedshapes it (above).f4_slurs out (no re-attack),-g4slurs in with a clean step,_g4slurs in gliding,'c4re-takes.(bow=-1)flips one stroke against the alternation;(bow=down)and(bow=up)(stored as ±2) set its direction on either tier. Every stroke glides 15 ms (oneglideliteral per synth); the…Portamentovariants glide 140 ms.…Sectionvariants are four voices for double-stops and chords on one shared bridge (bridge = 0.2, thesympathyof each bow), whose notes take adjacent strings. - Strings. Each settings record names its instrument's
strings, so every voice plays real strings with auto choice (Stage 13); a per-note(string=4)asks for one (d5(string=4)is sul G on a violin), and a slurreda4_ g4crosses from the open A to the D string, which the A then rings under. - Dynamics. One 0..1 value moves speed, pressure and contact together along a per-instrument path that holds the force ratio
N·Δμ·β/vnear 0.3 from soft to loud, inside the playable (Helmholtz) region: soft is slower, lighter and toward the fingerboard; loud is faster, heavier and toward the bridge (Schelleng). Violin speed 0.25→0.75, contact 0.55→0.25, pressure 0.57→0.92; cello 0.22→0.70, 0.60→0.30, 0.54→0.87; viola 0.24→0.72, 0.57→0.27, 0.56→0.89 and bass 0.20→0.62, 0.62→0.34, 0.52→0.82 (interpolated and solved for the same ratio). Sources: the instance'sdynamicsknob, an absolute per-note(dyn=x)(its-1default means "follow the knob"), and(cresc=±x), a ramp over the settings'swellfrom the stroke's re-attack. Changes slew 30 ms; a fresh voice starts at its own level. Raw per-noteexpression,bowSpeed,bowPosition,bendand thepressure/speed/positiontrims stay. - Attack. Both tiers play every fresh stroke, and the deluxe tier every bow change, through
bowed's attack planner (attackPlan = 1, above): the hand gives the held speed and force at once (no ramp, no bite) andbowedplans the rest.attackPlan = 0on either tier is the hand's attack: every fresh stroke (not a slur) lands with a pressure bite —bite(the hand presses 1 +bite×) decaying exponentially over max(biteTime,biteCycles12 periods) — while the bow accelerates over max(attack55 ms,rampCycles12 periods).biteis the bite atdyn0.5; a louder stroke bites harder, by 1 + 0.6·(dyn − 0.5) (a faster bow needs more force to catch the string). Tuned per instrument on its own strings and body (Measured below): violin 0.25, cello 0.2, viola 0.3 over 100 ms, bass 0.2 with the bow accelerating over max(30 ms, 4 periods); on the deluxe tier violin and viola 0.2, cello 0.35 over 100 ms with the bow accelerating over max(40 ms, 8 periods), bass 0.2 over 4 periods.bowedclamps the bitten pressure at 1.bite = 0is the attack without it. - The bow in the hand.
bowLength(default 0: off on the standard tier),bowChange(40 ms) andmessa(0; per note(messa=x)) pass tobowed's finite bow (above): bow changes on long notes, a shaped turn at every stroke, the arm-weight tilt and a planned bow. The deluxe tier turns it on — violin 0.65 m, viola 0.66, cello 0.62, bass 0.6,messa0.15 — and takes every bow change frombowed(itsbowLength = 0is an endless bow with shaped reversals);tremoloturns over 4 ms. - Articulations, per instrument:
…Tremolo(9 Hz, the bass 6 Hz, 2 ms turns),…Tasto(darker loss, more grip, a small fingerboard shift — the playable region narrows there),…Ponticello(toward the bridge, bright, light grip, more force; nearer still the string leaves one slip per period),…Harmonic,…Sordino(con sordino: thesordinotransform,mute = 0.5, a concert mute on the bridge —practiceMuteismute = 1, the heavy one; §1.12 Mute),…Spiccato,…ColLegno(astrikethroughstringandstringBody),…Pizz(pick→string→stringBody).…Harmonicis a light finger (touch = 0.5, bowed at pressure ≈ 0.6, speed ≈ 0.6, contact ≈ 0.22 across the dynamics): write the stopped or open string and it sounds an octave above. A per-note(touch=x)moves the finger on a…Harmonicvoice —0.333the twelfth,0.25two octaves (d4(touch=0.25)sounds D6),0.2two octaves and a just major third. The harmonic transform bows without stiffness (stiffness = 0): the deluxe tier's 0.01 stretched the string enough that the touched node leaked the fundamental over a held note. It also bows steadily and light: the rosin noise at 0.15× (its jitter of the force re-excites the damped fundamental), nomessa,touchAmount0.58, the attack planned on both tiers; the deluxedxHarmonicadds no finger noise, an endless bow (bowLength = 0: bow changes only where the score turns, over 120 ms). Measured (harmonics_hold_their_lead_through_ the_note, a held 3 s octave harmonic, vibrato on, 0.4 s windows from 0.4 s): the octave's lead over the written pitch, median / worst window, violin 75 / 72 dB, viola 74 / 72, cello 83 / 81, bass 88 / 85; deluxe violin 68 / 62, viola 76 / 72, cello 77 / 73, bass 77 / 75; over four more rosin-noise seeds (harmonic_voices_over_seeds) the medians are 68–87 dB and no window under 60. Bowed at the old contact 0.15 the same voices measured 55–67 dB medians. A listening hand that counted the octave's two slips a period as multiple slipping pressed it into a leaky regime (about 55 dB); it now expects them. - Register contact. Very high notes are bowed nearer the bridge, as players do (
reach = 0.08·max(0, log2(f/1200 Hz))off the contact on both tiers: A6 −0.04, E6 −0.01).…Spiccatois an approximation: a stroke speaks only after the Helmholtz build-up (about 40 ms on a violin, 80–180 ms on a bass), longer than a bounce, so it drives a plucked string with a shortscrape. The deluxe tier also bounces for real (below). - Open strings.
|> violinRing(G3 D4 A4 E5),violaRing(C3 G3 D4 A4),celloRing(C2 G2 D3 A3),bassRing(E1 A1 D2 G2) are|> sympathetic(§1.7) at 0.01 (violin, viola) or 0.02 (cello, bass);|> openStrings violinOpen xsets the amount. A patch cannot carry a track insert, and a passive-bridgegroup(§1.10) only joins sounding voices. - Bow noise, stiffness.
noise0.06 (violin) to 0.12 (bass) per settings record. The standard bowed voices keepstiffness0; the plucked and struck strings carry it (violin 0.02 … bass 0.15) and the deluxe tier's bows a moderate 0.01 (violin), 0.02, 0.03, 0.04 (bass), on top of each string's own (strings). The bass's stroke accelerates over 6 periods and bites over 8 (rampCycles,biteCycles; the others 12): over 12 periods an E1's bow was still slow when it should speak. - Extending.
bowedString (violinSettings with { … })and thetremolo/tasto/ponticello/harmonic/portamento/sordino/practiceMutetransforms compose; newbowedoptions go inbowDefaults, the parameters ofbowedStringandbowedDeluxeand theirbowedcalls. The dynamics path is one record,path = { vMid, vSpan, posMid, posSpan, pMid, pSpan }(a record parameter has no default, so a call gives it). The checker's specialization limit (64 levels) counts a module's parameters (each is a nestedlet), the depth at which a parameter is read and thewithchain of the settings record passed in, sobowedDeluxereads its parameters once in shallow patch fields and groups new ones into records (path,seat).
Deluxe tier#
No DSP budget: a deluxe voice costs 1.7–8× a standard one (see Cost below), and the compiler's DspLoad advisory fires for a song that keeps many deluxe voices sounding at once (about 166 units each, 259 for a tutti, against a 1,500-unit budget and about 3,000 units to one core). violinDeluxe, violaDeluxe, celloDeluxe, bassDeluxe are mono legato voices like the standard ones, built by bowedDeluxe from …DeluxeSettings:
- Bodies. The bows run with
body = "none"into the two-microphonestringBody "violinStereo"…"bassStereo"(§1.12 High-detail tier), so each instrument is a stereo source.stereo = falseuses the mono"violinFine"… body there instead (onestringBodyafter the players). - Stiffness 0.01–0.04 (above), the force-ratio dynamics as in the standard tier, the attack bite tuned through the deluxe bodies (above).
- Seats. Each instrument's pair is placed (
stringBody "…Stereo" { width, angle }, §1.12 The microphone pair) by its settings'seat = { width, angle }, in orchestral seating: violinsangle−0.3, violas −0.1, cellos 0.15, basses 0.3,width0.6 (the…DeluxePizz,…DeluxeColLegnoand…DeluxeSpiccatovoices too, throughoffString'swidth/angle).seated angle width smoves one:seated 0.3 0.6seats second violins opposite the firsts,seated 0 0.4a soloist in the centre. A mixerpanmoves the placed pair as a whole. - The left hand (
bowed'sfingerNoise, above): 0.8 on the violin and viola, 1.0 on the cello and bass, 0.5 per player in a section (tuttisets it); the standard tier has none (andbowedStringtakes nofingerNoise). - Mute. The settings'
mute(0) sits on the pair's bridge (stringBody … { mute }); a per-note(mute=x)or a# ctrl "mute"lane changes it from that note (smoothed over 40 ms;-1, the unmarked default, keeps the instrument's), so a part can play a passage con sordino:line # ctrl "mute" (dynBars muteBars).…DeluxeSordinoismute = 0.5. On the standard tier the mute is the settings' only (a mutedbowedStringruns its bow bare intostringBody body { mute }). - The bow in the hand is on (above): long notes change bow by themselves, every bow change is shaped by
bowed, down-bows start heavier and up-bows build, a note longer than the hair gets a slower bow,messa0.15 swells a held note slightly. - Vibrato onset. The vibrato starts
vibDelayinto the stroke (violin 180 ms … bass 260 ms, ×0.7–1.3 per note), blooms overvibRise(420–520 ms, smoothstep) and settles from 90 % of its rate; rate (4.85–5.45 Hz) and depth (×0.82–1.18) are drawn per note and the depth drifts ±15 % at 0.21 Hz. A slur keeps the vibrato going; a new stroke restarts the onset. - Glides.
glideCurve = "smooth"and a 25–35 ms default, so a per-note(glide=…)(Glide between notes) eases out of one note and into the next:d4 _a4(glide=200ms) -g4is a slurred shift,a4(glide=140ms)a shift on a new bow. - Sections.
…DeluxeTutti(mono, a section on one legato line) and…DeluxeSection(poly, four voices, for chords) are three players per voice (players, 1–4), timed as an ensemble (below): player 0 leads, each follower draws its own lateness and intonation per stroke; the followers vibrate 0.15–0.33 Hz faster or slower and lean ±0.04 indynand ±0.05 in contact, all scaled byspread; each has its own envelopes, so a section's bow changes are not sample-aligned. The players share one stereo body.tuttiturns any deluxe settings record into a three-player one. - Articulations.
…DeluxeTremolo,…DeluxeTasto,…DeluxePonticello,…DeluxeHarmonic,…DeluxeSordino(the transforms over the deluxe settings), and…DeluxePizz,…DeluxeColLegno,…DeluxeSpiccatothrough the stereo bodies.bowedDeluxe (tasto celloDeluxeSettings with { … })builds others. - The bouncing bow (
bowedBounce, one bow through the stereo body withbowed'sbounce):…DeluxeBounce(physical spiccato:bounce = 1, thrown from 1 cm at the balance point),…DeluxeRicochet(4 contacts per throw — the bass 3 — from 3 cm at 0.65 of the hair; per note(bounce=n)),…DeluxeSautille(bounce = 0, a 0.3 cm push per stroke at the middle). Bow masses violin 60 g, viola 70, cello 80, bass 135;bouncing n throw point smakes any deluxe settings record bounce; per note(throw=…).…DeluxeSpiccatokeeps the plucked approximation.
Ensemble timing#
A section's players follow a leader the way the linear phase-correction models of ensemble synchronisation describe (Vorberg & Wing; Wing, Endo, Bradbury & Vorberg on string quartets; Repp's tapping studies): each player's next onset is the leader's plus its own timing noise, corrected by a fraction of its last asynchrony. Two layers, both deterministic:
- In the voice (the default). A section's leader (player 0) plays
ensBase= 20 ms after the written onset; each follower draws its own lateness per stroke, 20 + 12 ms ± 27.7 ms (uniform, SD 16 ms; ×spread), latched on the stroke'sattackEdge(hold attackEdge (lfoSH …), a draw keyed on the voice coordinate) — so it is re-drawn on every stroke, re-articulated legato notes of a…DeluxeTuttiline included, and kept through a_slur. The 20 ms base keeps every draw ≥ 0, since a voice cannot start a player before its note. Intonation scatters the same way: each follower keeps a small tendency (+3, −2.5, +1.5 cents) plus a fresh draw of ±6 cents per stroke (SD 3.5). These are the stationary mean and spread of a phase-corrected follower; they carry no memory from note to note. A solo voice (players = 1) is unchanged. pat |> ensemble cfg(the full model, compile time). Over the pattern's events in order it runs a principal — a smooth seeded drift over score time (drift8 ms, knots everydriftCycles= 2) that every track with the sameseedrecomputes, so sections share one beat — a section leaderL(n+1) = (1 − alpha)·L(n) − ΔP(n) + sd·εbehind it (lag10 ms,alpha0.25,sd5 ms; the−ΔPterm is tempo following: the leader cannot predict the principal's interval change and corrects afterwards), and three desk partnersB_k(n+1) = (1 − playerAlpha)·B_k(n) − Δlead(n) + playerSd·εbehind the leader (playerLag12 ms,playerAlpha0.35,playerSd13 ms). It writes per-notelate0…late3(seconds after the written onset, plusbase30 ms) that the deluxe voices read in place of their own draws (trackParam "late1" -1; a negative value is unset).sectionsalts one track's draws; a second section withlag = 0.022sits about 13 ms behind the first.ensembleDefaultsholds the defaults. Randomness is a seeded integer hash (Park–Miller steps with a quadratic mix, exact in f64) filling a 1021-entry table of draws that each stream walks with its own stride.
Measured offline (compiler/tests/ensemble_timing.rs, engine/tests/ensemble_timing.rs), not listened to:
| follower − leader mean | its SD | follower pair SD | within-note SD (Rasch) | lag-1 autocorrelation | |
|---|---|---|---|---|---|
| previous fixed offsets (0/19/34 ms × one shared U(0.6, 1.4)) | 19 / 34 ms | 4.4 / 7.8 ms | 3.5 ms | 17.0 ms | — (the order never changes) |
| default draws | 12 ms | 16 ms | 22.5 ms | 13.5 ms | 0 |
ensemble (256 notes) | 11–12 ms | 17 ms | 23 ms | 13.7 ms | 0.54–0.61 (leader 0.84) |
Over a 256-note passage the leader's offset wanders 11–63 ms (SD 10 ms) with the principal's slow drift, which is stationary rather than cumulative (first and last quarters 45 and 32 ms); a second section at lag 22 ms sits 13.5 ms behind the first, and the two leaders correlate 0.56 through the shared principal. The same seed gives identical events and another seed moves nearly every note. In the engine, a probe built on the voice's idiom measures the drawn offsets: 29.6 ± 15.6 ms on a legato line, 27.8 ± 15.4 ms on poly notes (the model's 32 ± 16), a fresh draw on nearly every stroke, an exact replay, and another track's own draws. Compiling ensemble costs about 8 ms per note.
Phrasing (KTH rules)#
Optional pattern transforms after the KTH rule system (Friberg, Sundberg & Frydén, Director Musices):
pat |> phraseArch phraseArchDefaults— a tempo arch over everyphrase(4) cycles: the local inter-onset factor is1 + amount·cos(2πx′)(amount0.08) withx′mapping 0…turn(0.35)…1 to 0…½…1, slow → fastest atturn→ slow, a ritardando into each phrase end. The warp is integrated exactly, so each phrase keeps its length (measured IOIs 0.269 → 0.230 → 0.270 cycles over a 16-note, 4-cycle phrase).pat |> durationContrast amount— each inter-onset interval (to the next event; a chord's notes move together) scales by1 + 3·amount·(r − 1)/(r + 1),rits ratio to the mean, then the whole renormalises to the period: with 0.06, the shortest notes come 5 % short, a three-unit note 4.5 % long.pat |> finalRitard finalRitardDefaults— Friberg & Sundberg's (1999) running-deceleration ending over the lastspan(2) cycles, tempov(x) = (1 + (w^q − 1)·x)^(1/q)falling tow(0.6) withq= 2; onsets warp by the exact integral and the period grows to the next whole cycle (4 → 5 for a 4-cycle line).
Two of them compose in one pipe (line |> phraseArch … |> ensemble …); the checker inlines each helper and refuses past 64 levels of nesting, so a longer chain binds its steps at the top level, and durationContrast always takes its own binding, fed from one (arched = line |> phraseArch …, contrasted = arched |> durationContrast 0.05, then contrasted |> finalRitard … |> ensemble …).
bowlight-deluxe auditions the tier, with the chamber, undertow and ironbow scenes the budget-sized bowlight leaves out and a veiled scene (a violin con sordino and sul G ending under a practice mute, a ponticello tremolo, a cellist's muted double stops), every voice staged on the chamber's stage from the podium (§ The hall) and the dance's spiccato the bouncing bow (bowedBounce, with a ricochet); the black-lantern-fugue and black-lantern-circuit strings are deluxe, staged antiphonally (the library's seats moved to x −0.9 / +0.9 / +0.4 / −0.3 / −0.6 for firsts, seconds, violas, cellos and basses; the fugue in the cathedral, the circuit in the hall, both heard from the podium with one hallTail; one stage per seat on a submix of that seat's tracks, since a stage costs its full image-source loop on every track whether it sounds or not), their dark entries written on one string (the cello's first subject and the augmentation sul G, violin II's subject sul G, the basses' on their A string), entries down-bow, the octatonic episode con sordino (a mute lane on the viola and cello), the coda's cello whisper muted, and the cadence in viola double stops on one bridge (lantern-score).
Measured#
Offline, not listened to. Census (library_census, engine/tests/bowstrings_library.rs, --ignored): 25 pitches across the four registers × six dyn levels (0.1–1.0), fresh strokes on the bare string (body = "none", no vibrato). A cell is one slip per period when H1 outweighs H2 and H3 and periodic when the held note's autocorrelation at its period exceeds 0.98; onset is the first time a two-period window is periodic (r > 0.95) and within 6 dB of the held level for three 10 ms hops running.
| H1 > H2, H3 | periodic | lines rising every step | onset median / p90 | not periodic within 1.2 s | |
|---|---|---|---|---|---|
| previous library (no bite, Stage 8 paths), Stage 9 bow | 149/150 | 127/150 | 24/25 | 40 / 170 ms | 7 (bass at dyn ≥ 0.7, violin E6–A6 at 0.1) |
| standard tier, abstract string | 150/150 | 150/150 | 25/25 | 40 / 100 ms | 0 |
| deluxe, abstract string | 149/150 | 149/150 | 25/25 | 80 / 180 ms | 0 |
standard tier on its strings | 150/150 | 150/150 | 25/25 | 60 / 140 ms | 0 |
deluxe on its strings | 149/150 | 150/150 | 25/25 | 80 / 180 ms | 0 |
| standard tier, the bow-side re-tune (bites per instrument, louder strokes bite harder) | 150/150 | 150/150 | 25/25 | 50 / 110 ms | 0 |
| deluxe with the finite bow and the re-tune | 150/150 | 150/150 | 25/25 | 60 / 140 ms | 0 |
| deluxe with the left hand, seats and the mute plumbing | 150/150 | 149/150 | 25/25 | 60 / 140 ms | 0 |
Loudness rises 11.6–12.5 dB from dyn 0.1 to 1.0 on every pitch (deluxe 11.6–12.9; the previous library 9.6–18.7). The bite amount was chosen by the same census per instrument: without it the new paths fail (violin 33/42 one-slip cells, 1 of 7 lines rising); 1.6× (the physics lab's sustained-tone stroke) is clean but slow (onset median 80–160 ms, p90 120–300 ms); 1.3× is clean with the fastest onsets (violin median 40 ms, p90 70; viola 40/70; cello 50/80; bass 80/180); 1.2× starts to miss on the bass. On the abstract string the deluxe tier's one miss was a violin A4 at dyn 0.5 (H2 +1 dB, the stiffness); on its strings that cell is clean and a cello A4 at 0.3 misses instead (H2 +5 dB; with the knob's stiffness at 0 a viola C3 does — the misses move cell to cell, the count does not). With the left hand, the seats and the mute plumbing the deluxe tier's one periodic miss is a violin A6 at dyn 0.7 (r 0.978 against 0.98; 0.980 before, and the same with fingerNoise = 0: the noise draw of moved node ids); through the stereo body 70 / 160 ms. The strings slow the median onset (40 → 60 ms): heavier, lossier wound strings take longer to build Helmholtz motion. Bass E1 onsets, dyn 0.1…1.0: 110, 110, 190, 140, 240, 410 ms on the abstract string; 120, 190, 150, 140, 150, 250 ms on its strings with the 6-period stroke (the bridge loss settles the bow changes, the torsion keeps the start single-slip; the loud strokes stay slow, their larger normal force sticks longer before the first slip).
Bite through the bodies (CENSUS_BODY=1: onsets on the heard signal, the standard bodies or the deluxe …Fine, verdicts on the bare string rendered alongside; the two-microphone body's left channel measures the same): the bite was re-tuned per instrument over bite 0.15–0.45, bite time 100–220 ms, the loudness scaling 0–1.2 and the bass's acceleration; the Stage 12 bites were tuned on the bare abstract string. Through the re-fitted bodies (Stage 16), before → after: standard onset median / p90 60 / 150 → 50 / 120 ms; deluxe 90 / 170 → 70 / 160 ms; bare-string verdicts held or improved. The deluxe messa is 0.15: at 0.25 the taper inside the census window costs the violin's top notes their periodicity (r just under 0.98). Without the loudness scaling the lowest bites that keep the soft end fast (violin, cello 0.2) miss the loud end on wound strings (four lines stop rising at dyn 0.9–1.0, H2 over H1). The onset metric moves by 10–40 ms between two renders of the same stroke whose rosin noise is drawn differently, so single cells are not a verdict. Bass E1, dyn 0.1…1.0: standard 130, 210, 100, 110, 150, 210 ms (before 120, 190, 150, 140, 150, 250), deluxe 140, 150, 140, 150, 250, 160 (before 180, 170, 180, 180, 190, 190), with the bass bow accelerating over 4 periods; no setting of bite, its time, the loudness scaling or the acceleration (2–8 periods, 30–55 ms), nor the rested-rosin grip (0, 0.15, 0.3), brought every stroke under 150 ms without losing cells elsewhere.
The bow in the hand (lab_bow_change, --ignored; the bare string, the library's dyn 0.5 hands on violin G3, A4, E6 and cello C2, D3, A3, each turned at 16 phases of the period): one slip per period resumes after a 40 ms bow change in a median 73 ms (p80 168 ms; 5 of 96 turns not within 500 ms), the quietest two-period window at 0.57 of the held level and the band above 3 kHz 3.2 dB under the held note's around the turn (a smooth change, not a scratch). The previous library turn (10 ms one-pole, 20 % pressure dip): 84 / 312 ms, 16 of 96, 0.62, −0.9 dB. Without the catch the 40 ms turn is slower (105 / 231 ms, 7 of 96); a 10 ms turn with the catch resumes about as fast (77 / 139 ms) but barely disturbs the string (0.64, −0.2 dB). The deluxe census is held by the planned bow (a re-plan that flipped between a slower bow and an extra change cost 6 periodic cells, hence the kept change count). A held deluxe A4 at dyn 0.8 changes bow by itself every 1.5–2 s, dipping to 0.65 of its level (a_held_deluxe_note_changes_bow_when_the_hair_runs_out); (bow=down), (bow=up) and (bow=-1) set the stroke's polarity on both tiers (per_note_bow_direction_sets_the_stroke).
Slurs (a_slur_group_is_planned_as_one_bow; the songs through render-song with VOLTA_BOW_TRACE, against VOLTA_NO_SLUR=1, which drops the stamps — the note-by-note bow): eight slurred 0.5 s notes at 0.5 m/s (2 m of bow) take two changes, both within a millisecond of a note boundary, and end with 4 % of the hair in hand; note by note, the hair ran out twice, 246 and 68 ms from a boundary. Bow changes the score did not ask for, before → after (mid-note = more than 30 ms from a boundary):
| song | forced (mid-note) | hidden at a boundary | requested strokes |
|---|---|---|---|
black-lantern-fugue | 710 (695) → 309 (305) | 0 → 231 | 5961 → 5961 |
black-lantern-circuit | 629 (617) → 289 (285) | 0 → 205 | 5556 → 5556 |
bowlight-deluxe | 132 (132) → 110 (106) | 0 → 28 | 533 → 534 |
The fugue's remaining mid-note changes are notes longer than the hair holds at 0.45× (226 single or last notes, median 4.1 s — the whole-note pedals) and 79 first notes of a slur (median 1.55 s) whose stroke starts with too little hair left in its direction; inside a slur (a note after its first) 264 → 31. The census (fresh strokes, no slurs) is unchanged: standard 150/150/25, 50 / 110 ms; deluxe 150/149/25, 60 / 140 ms; through the stereo body 70 / 150 ms.
The bouncing bow (lab_bounce, --ignored, the bow alone; and bouncing_bow_census through the library). A 60 g bow's contact frequency along the hair (frog → tip, 0.1 … 0.9): 8.8, 11.0, 11.6, 10.9, 9.9, 8.8, 7.8, 7.0, 6.3 Hz (effective mass 211 → 5 g, stiffness 646 → 8 N/m); a free 2 cm throw bounces 7, 7, 7, 6, 5, 5, 5, 4, 4 times, at 9.9, 10.7, 11.2, 11.3, 11.2, 11.0, 10.9 Hz for 0.1–0.7 (the hand's spring makes the rate nearly independent of the height: at 0.65, 99–104 ms gaps from 1–5 cm throws, shortening as the bounces fall). Bow mass at the balance point: 60 g 11.6 Hz, 70 g 11.1, 80 g 10.8, 135 g 9.4. Spiccato (1 cm, balance point) lands once per stroke at 4–10 strokes/s with the hair on the string 15, 22, 30, 37 % of the time. Sautillé (0.3 cm push, the middle, natural 10.4 Hz): strokes at 0.6× its bounce leave the bow on the string (1.5 landings/s, contact 92 %); at 0.9–1.3× it lifts once per stroke (contact 70, 51, 49, 60 %). Through the library: the physical spiccato's first 60 ms against the plucked approximation — violin −24.1 vs −17.6 dB, spectral centroid 2.0 vs 4.1 harmonics (viola 3.6 vs 7.4, cello 3.5 vs 10.8, bass 4.2 vs 10.7): a darker, bowed onset; ricochet, one throw: 3 audible bounces (a 4 dB rise each) at 10.5 Hz on the violin, 9.8 viola, 11.8 cello (2), 10.0 bass, the later contacts too soft to rise 4 dB; sautillé at 12–14 strokes/s 1.05 articulations per stroke on the violin and viola (at 10, 1.3–1.8: the bounce and the stroke beat). Cost: bounce +20–25 ns per sample over the plain bow (3 units).
Strings (bowed_strings.rs tests and lab_strings, the bare string, the library's dyn 0.5 hand): a cello A3 on its A, D and C strings has a harmonic centroid of 1060, 1057, 905 Hz, an 8th-partial stretch in the free ring of +2.0, +5.4, +47 ct, and its fundamental rings down at 40, 61, 105 dB/s after the bow lifts (the open A against two stopped ones); a violin A4 open 83 dB/s, stopped on D 115. In the library a slurred a4_ g4 keeps the open A sounding 44 dB under the G4 0.3–0.9 s after the crossing (on one string, 70 dB under).
Harmonics (harmonics_sound_above_the_written_note, 0.8–1.6 s into the note): the sounding pitch over the written one — violinHarmonic A4 +56.4 dB at A5, violinDeluxeHarmonic +58.7 dB, celloHarmonic d3(touch=0.333) +70.5 dB at A4, deluxe +57.1 dB (before the strings and re-fitted bodies: 59.3, 77.4, 79.1, 72.6). Over a whole held deluxe A4 the lead varies from window to window (0.4 s windows: median 40 dB, minimum
- and moves with the vibrato,
messaand noise draws; with the deluxe stiffness (0.01) it was a median 18 dB, hencestiffness = 0inharmonic.
Deluxe (deluxe_voices_are_stereo_late_vibrato_sections): violinDeluxe's two microphones correlate 0.51 on a held A4; the period swings 0.22 samples peak-to-peak over 0.1–0.26 s (the same note without vibrato: 0.22) and 1.45 samples (≈ ±11 cents) over 0.9–1.3 s; a violinDeluxeTutti A4 is 2.5 dB over the solo and less periodic (0.993 against 0.994) as its three players beat.
Legato and rings (the other library tests): a violin d4_ a4 slur keeps 0.74 of the level through the change (the Stage 8 path 0.93), an e4_ b4 slur 1.00, and bow changes dip to 0.19–0.58; an A4 on violinRing still sounds 27 dB under the note 0.6–1.6 s after it stops, a B♭4 does not ring; pizzicato decays 65 dB/s (violin A4), 44 (cello G2), 31 (bass A1).
Cost (tier_cost_table, --ignored, the development build, wall clock per output sample for one held note including the engine around it, best of three on a loaded machine): violin 647 ns, violinDeluxe 1084 ns, celloDeluxe C2 1097 ns, violinDeluxeTutti 1934 ns, a three-note violinDeluxeSection chord on its shared bridge 5375 ns (the abstract string, same run: 602, 1049, 1046, 1722, 5327). Offline, black-lantern-fugue's mix renders in about 0.4× real time (volta export, --profile fast).
The folkstrings library#
examples/songs/folkstrings.volta (demo: ashwheel, The standard library) builds the folk instruments on bowed:
import "folkstrings" (hurdyGurdy, hurdyDrone, trompette, erhu)
drone = play hurdyDrone "[d3,a3]"
dog = play trompette "d4 d4(coup=0) d4(coup=0.6) d4(coup=0)"
tune = play hurdyGurdy ("a4@2 c5 d5@3 c5 bb4" |> slow 2)
lament = play erhu "a4@2 _c5(glide=320ms)@2"
hurdyGurdy,hurdyDrone,trompette— the wheel is an endless bow at the crank's speed (crankknob; 0.5 is speed 0.35), pressed at 0.75 near the tailpiece (contact 0.2), turned unevenly (±3–4 % at twice the 1.6 Hz turn rate). The chanterelle (mono, legato, keyed steps, a key snap atfingerNoise0.7), the bourdon and mouche (poly drones at their own pitch), and the trompette: every note is one crank stroke, and a coup ((coup=…)per note, thecoupknob otherwise) speeds the wheel by up to half in 12 ms, relaxing over 70 ms; thetirantknob isbuzz. Write the trompette's rhythm as repeated drone notes with no rests.violaDamore,hardanger— fiddles with the bowstrings grammar (alternating strokes, slurs,(dyn=…)) on their strings and bodies, the sympathetic strings at theringknob (0.7).erhu— wide eased glides (slide90 ms,glideCurvesmooth), a deep 5.6 Hz vibrato, the soft stop, the membrane body.morinKhuur— horsehair strings, more rosin noise, a warm box.folkFiddleandhurdyWheelbuild others; the settings records aredamoreSettings…morinSettings.
Measured (offline, not listened to)#
Slip regime (bowed_folk_lab lab_folk_slip_census: every documented-region hand of the bowed grid on each instrument's notes, 1.2 s held, single slip per period ±0.1, this lab's attack): hurdy-gurdy chanterelle 194/196 (the abstract string in the same cells 196/196), d'amore 203/224 (215), Hardanger 185/196 (193), erhu 178/196 (195), morin khuur 157/168 (162); the violin on its Stage 13 strings in the same harness 185/196 (193). The misses are double slipping, mostly at contact 0.25; the erhu's soft stop costs the most.
Trompette (lab_trompette, D4, contact 0.2, pressure 0.75, the hurdy-gurdy body; the dog is outside the loop, so the buzz is exactly the render with it minus the render without):
| steady speed | 0.25 | 0.35 | 0.4 | 0.45 | 0.5 |
|---|---|---|---|---|---|
landings per period (buzz 0.5) | 0.04 | 0.10 | 0.16 | 1.06 | 1.07 |
| buzz re string | −25.5 dB | −26.7 | −27.5 | −23.5 | −3.5 |
The buzz is one landing a period (the string's pitch, 294/s) once the wave clears the tirant, its spectrum centred at 3.0–3.8 kHz through the body; seated, the dog's compliance leaves −26 to −28 dB of difference. The lowest steady speed that buzzes: buzz 0 none up to 0.9, 0.25 → 0.65, 0.5 → 0.45, 0.75 → 0.3, 1 → 0.2. Crank accents at a resting wheel of 0.35, every 0.25 s: depth 0.2 → 4–6 landings a coup and no buzz to speak of (−37 dB), 0.3 → 8–11 landings, buzz −23 dB in the coup's first 100 ms against −37 in its last, 0.45 → 11–18 landings (−23 dB), 0.6 → 19–23 landings over 155–175 ms (−17.5 dB); the first landing comes 4–8 ms into the coup (the Helmholtz amplitude follows the wheel).
Sympathetic strings (lab_understrings, held 1 s then lifted; level re the held note, decay of the tail):
| note | understrings | 0.25–0.5 s after | 1.0–1.25 s after | decay |
|---|---|---|---|---|
| d'amore D4 (unison) | absent | −27.4 dB | −84.7 | −76 dB/s |
| 0.5 / 1 | −16.4 / −12.8 | −32.1 / −27.6 | −21 / −20 dB/s | |
| d'amore E♭4 (no string) | 0.5 / 1 | −34.3 / −32.3 | −44.2 / −38.2 | |
| Hardanger E4 (unison) | absent / 1 | −33.4 / −17.6 | −107.0 / −31.9 | −98 / −19 dB/s |
| Hardanger B4 (octave) | 1 | −23.3 | −40.5 | −23 dB/s |
A note on the instrument's scale rings on at about −20 dB/s (a 3 s T60, the authored 3.5 s less the loss corner); one off it leaves 10–16 dB less.
Bodies (string_body/folk.rs; 48 kHz sine gain, dB, at third-octave centres; authored from published descriptions, no measured response of these instruments is in the local DSP corpus):
| 126 | 200 | 317 | 504 | 800 | 1270 | 2016 | 2540 | 3200 | 5080 | 8063 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| hurdyGurdy | −5.1 | +5.9 | +0.7 | +0.8 | +3.8 | +0.8 | +3.9 | +7.7 | +6.2 | −8.2 | −10.7 |
| damore | −6.5 | +0.4 | +1.8 | −1.1 | +3.2 | −0.1 | +5.8 | +5.1 | −13.8 | +0.4 | −7.9 |
| hardanger | −12.2 | −5.2 | −6.7 | +1.8 | +3.8 | −0.8 | +2.5 | +6.7 | +5.8 | +3.3 | −3.8 |
| erhu | −14.6 | −7.8 | −0.5 | +0.4 | −1.0 | −4.7 | −1.9 | +0.4 | +6.0 | +3.4 | −3.9 |
| morinKhuur | +3.0 | +0.6 | +1.0 | −1.6 | −0.6 | +5.1 | −1.0 | −12.7 | −8.0 | −11.4 | −19.3 |
(the erhu's lightly damped membrane modes, Q 18–24 at 0.9–2.4 kHz, sit between these centres). Pink-noise centroids: hurdy-gurdy 1.72 kHz, d'amore 1.61, Hardanger 2.42 (the violin 2.17), erhu 2.50, morin khuur 0.69. Every table meets the default tier's loudness rule (pink and register sawtooths within ±3 dB, their mean within 0.5), a peak gain under +12 dB and an impulse-response L1 under 16 at 44.1/48/96 kHz.
Cost (estimator_cost_table, --profile fast, 48 kHz, over the plain bow's 299 ns): the folk strings and bodies as the violin family's (+12 / +21 ns), a morin khuur's horsehair +10 ns, understrings +21 ns for seven strings (+17 for five), buzz +13 ns (+3 at 96 kHz); priced 3 and 2 units in compiler/src/cost.rs.
blown freq pressure [kind] [noise]#
A blown pipe after STK — kind "reed" (default, a clarinet) or "jet" (a flute). pressure (0..1) is blowing pressure, live per sample; noise (0..1, default 0.1) breath turbulence. §1.11.
blownEvo freq pressure [kind] [noise]#
blown's arguments on the physical woodwind core: "jet" is the flute and "reed" the clarinet, and kind also names "flute", "recorder", "panflute", "shakuhachi", "clarinet", "sax", "oboe" and "bassoon". Pitch, breath and turbulence are read every sample; a fresh note is tongued. §1.15.
Exciters — strike sharpness · pick color · scrape rough#
Note-locked audio bursts (Sig Audio Bipolar), each with one control-signal knob read at note-on:
strike— Hann-windowed noise thump; harder = shorter + brighter (8 ms → 0.5 ms).pick— 5 ms burst;coloropens its lowpass (dark → bright).scrape— decaying rasp;roughlengthens it (80 ms → ~580 ms).
strike and pick are floored at one period of the played note (Karplus- Strong plucks a string by filling the whole loop with noise). Above ≈200 Hz the spec length already wins and nothing changes; at 55 Hz the 5 ms pick used to cover 27 % of the loop, which came out 7 dB quieter and thinner than the same patch played high. A scrape is a continuous rub, not a loop fill, and keeps its own length.
Burst noise is drawn from the seeded RNG substrate keyed on the performance coordinate + node id — different notes decorrelate, replays are bit-identical. Velocity is not implicit: write strike (0.3 + vel * 0.7) to make the hand follow it.
1.5 modalOf sample [modeCount] — fit a table from a recording#
modalOf is not a source. It fills the mode-table slot of modal or body or bodyTrack, exactly the way sawSpectrum 64 fills additive's second argument:
tumbler = patch { source = modal (freq pitch) (modalOf (sample "glasstap") 32) }
wash = patch { source = noise |> body (freq pitch) (modalOf "plate") }
Used bare (source = modalOf "bowl") it is a lowering error naming the mode-table slots it belongs in.
The compiler cannot see sample data, so the fit is an engine-side bake at load time — the additiveOf seam exactly (EngineHandle::bake_modal, called from both load_module and load_samples, so whichever arrives second completes it). The node renders silence until the table latches, then rings normally; it retries the bank lookup every block. modeCount defaults to 24 and must be a whole number in 4..=64.
What the fit measures (physical::analyse_modal):
- Welch-averaged magnitude spectrum — Hann frames of 4096 at 75 % overlap over up to ten seconds, magnitudes averaged across time. The averaging is what separates a mode (present throughout the decay) from an attack transient.
- Peak picking — 5-point local maxima (above both neighbours, at or above the pair beyond), refined by parabolic interpolation on the log-magnitude so the mode frequency is sub-bin accurate. Peaks below −60 dB of the strongest are dropped; the
modeCountstrongest survive. - Root —
onset::estimate_root_hz(the engine's McLeod tracker) when the source is pitched, else the lowest surviving peak, which is the right answer for a bell or a plate. Ratios arepeak / root. - Per-mode T60 — a least-squares fit of the mode's three-bin band energy in dB against time, from the loudest frame onward (the attack rise would otherwise flatten the regression).
T60 = 60 / slope_dB_per_s, clamped to 0.03–5 s; a flat or rising mode (a looped sample) takes the long end. - Amps — averaged peak magnitudes relative to the loudest, then divided by √n for the number of surviving modes. This keeps dense fitted banks manageable under sustained broadband drive (§1.9); literal tables are never normalized.
The compiled IR carries only the request; the fit runs in the engine on load (EngineHandle::bake_modal), so it happens on every target. graph::max_release budgets the fit's 5 s ceiling, scaled by sustainBy. The engine adds sampled drive duration when PCM arrives, then clamps the release budget to 30 s (§1.9).
1.6 src |> body freq modes — the bank as a chain element#
bellModes = [[1, 2.4s, 1], [2.76, 1.6s, 0.5], [5.4, 0.9s, 0.25]]
brush = patch { source = (noise |> lpf (const 3000Hz) (const 1))
|> body (freq pitch) [[1, 2.4s, 1], [2.76, 1.6s, 0.5]] }
bell = patch { source = (sample "tabla") |> body (freq pitch) bellModes }
body makes the piped signal the bank's exciter, so any chain can terminate in a resonating object. It is a pure argument reorder: after pipe desugar body src freq modes lowers to the same Modal (or ModalFit) node modal produces, with exciter = src. Zero new engine code — the exciter slot has always accepted arbitrary audio.
modal opens the exciter slot; body opens the body slot. Together they spell out the McIntyre/Woodhouse exciter → resonator split in both directions, and body is resonate's modal sibling (Route audio between tracks).
The mode-table slot is pre-walked through the evaluator, so a named table (bellModes from lib/modal.volta) or a computed one (map … (range …)) works, not just an inline literal — the stepSeq discipline. Without it the mode-table library would be unusable.
1.7 track |> sympathetic [pitches] amount — coupled strings#
drone = play sitar riff |> sympathetic [~D3, ~A3, ~D4, ~F4, ~A4] 0.4
A track-level effect: a bank of lightly-damped Karplus-Strong waveguides (damping 0.08 → T60 ≈ 4 s at 220 Hz and longer down low, brightness 0.6, no pick comb — a sympathetic string is driven along its whole length, not plucked at a point) is fed by the track bus scaled by amount, and the sum is added on top of the untouched dry signal. Stereo runs an independent bank per channel so the halo keeps the source's image; the bank sum is √n-normalised so 12 strings are not twelve times louder than one.
- Up to 12 strings; each pitch is a bare compile-time pitch literal (
const_pitch_midi—~D3, uppercase note names). amountis aParam, so it rides a macro;0is a true null (the effect returns before touching the buffer, bit-identical to no insert).- The point is that it is pitch-selective, which no send reverb is: material near a string's fundamental (or a harmonic of it) rings for seconds, everything else passes through. Play in the key you tuned it to and the mix blooms; step outside it and nothing happens.
- Track inserts only (not a bus/master insert) — it is an instrument-scale colour, and a whole-mix version would just be a resonant reverb.
- Reports no latency (a summed wet path over an undelayed dry), and
reset()clears the bank so a bypass toggle silences the seconds-long tail.
1.8 stiffness / sympathy — the two physical knobs#
Design spec: String Stiffness Coupling. Both default to 0, and at 0 nothing is built: no allpass, no bridge, no IR key, no extra node — a patch that omits them is bit-for-bit the string that shipped before them.
piano = patch { source = string (freq pitch) 0.2 0.75 0.13 (strike 0.6) 0.28 0.5 }
sitar = patch { source = string (freq pitch) 0.1 0.95 0.09 (pick 0.9) 0.9 0.7 }
stiffness — dispersion (stretched partials)#
A single Schroeder allpass of M = round(0.05 · sr/f₀) samples spliced into the loop in place of that much plain delay (the Rings string.cc recipe; the JOS Thiran cascade is the deferred upgrade). Its phase delay falls with frequency, so the loop is shorter for the upper partials and they resonate sharp — a stiff string's wave speed rising with frequency. (Rings' coefficient is negative too, -0.618·dispersion/(0.15+|dispersion|), reaching ≈−0.54; what differs is that Rings sweeps the segment length with the knob, up to 47.5 % of the loop, where we hold the segment fixed and move only the coefficient.)
The coefficient is solved per note, not fixed. M has to be a whole number of samples, and the design target is a fixed fraction of the period, which is not — above ~250 Hz the rounding steps in visible jumps (3 → 2 → 1 across the top octave). So the quantization is pushed back into the coefficient: the section's low-frequency dispersion is ∝ M³·k(k²−1) with k = (1−a)/(1+a), and a is chosen to hold that product at the unquantized target — one cubic in closed form. Without it the P4 stretch at stiffness 0.5 ranged 11.4…43.0 cents and jumped 31.6 cents between two adjacent semitones (G6 → G♯6); with it, 21.4…24.6 cents over 110 Hz–1.76 kHz at that setting.
Chromatic-continuity bound: ≤5 cents, over 110 Hz–1.76 kHz (2026-08-03 audit F8, refined by its review). The original "≤1.4 cents" figure was measured at three knob settings (0.25 / 0.5 / 1.0 — 1.63 / 1.40 / 2.26 c) and 1.4 c is the middle row only. stiffness is a continuum, and the peaks are narrow: a 0.005 sweep puts the worst adjacent-semitone step at 4.55 c, but at 0.00025 the true maximum is 4.79 c, at stiffness 0.876 (runners-up 4.50 c @0.75, 4.28 c @0.58, 4.02 c @0.38). Both earlier figures were grid artifacts of the sweep that produced them. So the honest bound is ≤5 c — the conventional pitch JND — a 6.6× improvement on the 31.6 c lurch, but with only 0.21 c of headroom. stiffness_is_continuous_across_the_chromatic_scale sweeps 200 coarse settings plus a fine pass across the peak so it actually resolves the 4.79 c maximum.
Above 1.76 kHz the bound is not 5 cents, and mostly not because of stiffness. The ideal string itself steps 5.8 / 9.2 / 14.2 cents between adjacent semitones above ~2.2 kHz at stiffness 0, because the loop is only 12–16 samples long and the fractional-read allpass carries a large remainder. On top of that floor, the dispersion section used to be switched off at STIFF_MIN_PERIOD = 16 samples (≈3 kHz), which at stiffness 1 was an 80.96-cent P4 jump between 2960 and 3136 Hz — the whole stretch vanishing in one semitone, against a 5.82 c baseline. stiff_fade replaces the switch with a fade that is linear per semitone across 2–3 kHz, so the seam itself now measures 5.78 c — the ideal string's own step — and the worst step anywhere above the tested range is 22.70 c at stiffness 1 (against the 14.21 c the ideal string already has there). That is a 3.6× improvement on the cliff, not its removal: ~100 cents of stretch has to be surrendered over the ~7 semitones between 2 and 3 kHz however it is spread. Nothing at or below 1.76 kHz moves — the fade is exactly 1.0 for loop periods ≥ 24 samples. Pinned by stiffness_stays_near_the_ideal_string_above_the_tested_range.
Partial n, in cents above n·f₁ (220 Hz, brightness 0.9):
| stiffness | f₁ | P2 | P3 | P4 | P6 | P8 |
|---|---|---|---|---|---|---|
| 0.00 | +0.00 c | +0.0 | +0.0 | +0.0 | +0.0 | +0.0 |
| 0.25 | +0.00 c | +1.7 | +4.2 | +7.3 | +13.9 | +19.9 |
| 0.50 | +0.00 c | +5.5 | +13.1 | +21.5 | +37.0 | +49.1 |
| 1.00 | +0.00 c | +33.1 | +66.9 | +94.7 | +133.1 | +156.6 |
Rules of thumb: ≈0.27 is a piano (a mid-range piano stretches its 8th partial ≈ +22 cents), 0.5–0.7 clav / prepared piano, 1.0 tanpura / sitar. Below a 16-sample loop (≈3 kHz) the section is not built.
The stretch is monotone over the partials a string radiates — and only those. One allpass is not √(1+Bn²); its phase delay ripples, so the curve rises, rounds over and comes back down. At 220 Hz, stiffness 0.5: +5.5 c at P2, +49 c at P8, a peak of +65.5 c at P14, back to +50.9 c by P22. 0.05 is chosen to put that turn above the tenth partial, where a plucked string has little level left and the loss filter has taken the rest; matching the whole band is the deferred Thiran cascade, not a bigger segment.
The fundamental does not move. The allpass's phase delay at f₀ is subtracted from the loop length in closed form, exactly like the loss filter's — worst case 0.14 cents over the whole brightness × stiffness × pitch grid. (Uncompensated, stiffness 1 would play nearly a whole tone sharp.) The T60 knows about it too: the loop gain is per pass, and a stiff string's pass includes the section's delay.
sympathy — the shared bridge#
Every voice of this patch's this node adds its bridge tap into one shared history ring, indexed by absolute sample position. When a block closes, the samples it accumulated are run through the bridge's own loss (a 2.5 kHz one-pole — a real bridge couples low partials far better than the top octaves), scaled by the reinjection gain, and left in the ring. So a chord on one string patch behaves like a chord on one instrument:
- Sympathy — striking one note sets the others ringing at their own pitches. Measured: pluck a 110 Hz string with a silent 220 Hz neighbour on the bridge and the neighbour comes up to −11.6 dB of the driver, 6× above any frequency it doesn't own.
- Two-stage decay — a note played on two strings splits into a mode that drives the bridge (fast) and one that cancels there (slow), so it falls and then sings. Measured on a 220 Hz pair: −26.4 dB/s vs −17.6 dB/s, a 1.50× split (1.00× uncoupled), and a single string's envelope knees 0.91×.
Each string reads the bridge a whole number of its OWN periods back, and negated. A full period is 0° at every harmonic of that string's series, so the minus sign makes the reinjection anti-phase at all of its partials — the bridge takes energy from the mode that drives it, at any pitch. The multiple is the smallest that clears one engine block (128 samples, the fixed maximum), so a read never lands in the block the arena is still filling: whichever order the voices render in, they all read the same frozen history and write into the same open block. Two things follow that a fixed one-block delay could not give:
- Pitch invariance. Measured sum/cancelling split at 220 / 330 / 375 Hz: 1.50× / 1.48× / 1.47×. (A one-block delay rotated the coupling's phase with pitch and flipped its polarity every ≈187 Hz: 1.39× at 220 Hz but 0.62× and 0.54× at 330 and 375 — the "fast" mode ringing longer than uncoupled.)
- Buffer-size invariance. At 32 / 64 / 96 / 128-frame engine blocks the render is bit-identical, and a ragged device callback (480 frames → 128, 128, 128, 96) matches an even one exactly. Live and export are the same song.
Energy arriving from the other strings is at a different pitch and lands at whatever phase its own period gives it — that part is physical and is left alone. (Residual: the bridge pole itself adds ≈2.5 samples of lag, so "anti-phase" is 4° off at 220 Hz and ≈26° at its 8th partial.)
The reinjection gain is derived, not dialed: γ = ½·sympathy·(1−g)/(N·g) for N coupled voices of per-pass loop gain g, which is half the Nyquist bound γ < (1−g)/(N·g). The read delay and the negation are unit-magnitude, so the bound is unchanged by the topology. That normalisation is why the knob means the same thing everywhere: the worst-case ring is ½·sympathy of the driving string whatever the damping or the voice count, with 6 dB of margin. Verified at the worst case the engine allows — eight voices, one pitch, sympathy = 1, damping = 0 — where the peak falls 62.8 dB over 3.5 s.
It also caps how hard the decay can split (the bridge may take at most half the string's own per-pass loss, so the fast mode tops out at 1.5× the slow one — and now reaches it). A real piano splits harder because its bridge removes the energy it takes; an add-only bus can only borrow. bridge = "passive" (§1.10) is that resistive bridge.
A coupled voice is budgeted a longer life: graph::max_release scales the waveguide's ring budget by 1/(1 − ½·sympathy) (exactly 1 — no cost — when uncoupled), because a mode fed in phase can have its decay rate halved. Otherwise the end-of-voice edge fade would cut a coupled tail at −30 dB. That budget is charged on sympathy > 0 alone, not on polyphony: a single-voice coupled patch pays the same doubled waveguide lifetime as a ten-note chord, even though its own tap can only damp it. The budget is decided at compile time, where the voice count isn't known (it depends on the pattern, poly and the arrangement), and erring the other way truncates an audible tail; the cost is one voice slot's lifetime and max_release clamps the whole budget at 30 s.
Sixteen bridges can be live at once — one per (patch name, node index), so a patch with two coupled string nodes uses two. A 17th finds no free slot and simply doesn't couple; which one loses out depends on the arena's voice render order, so it is not stable across edits. Slots are claimed by an FNV hash of the name (not the template pointer), so a hot-swap doesn't split a sustaining chord off its bridge; a slot nobody wrote to for four blocks is recycled, and VoiceArena::clear (transport stop) drops them all.
Both knobs accept live controls (0–1). Stiffness retunes the retained dispersion loop per sample; its history keeps advancing through zero. Live sympathy arms the prepared bridge before it becomes nonzero. Each sample records contributing voice count, worst loop gain and maximum sympathy, so the existing stability bound follows the actual control curve independently of callback size. The voice tail reserves the full possible coupling budget. Literal paths retain their original coefficients and compact IR.
Not to be confused with |> sympathetic#
sympathy couples the notes you play to each other. |> sympathetic [pitches] amount (§1.7) hangs a bank of extra, unplayed strings across a whole track bus. They compose: a stiff, coupled sitar patch under a sympathetic drone bank is the whole instrument.
1.9 Hybrids — the hand and the body#
modalOf is the body of a recording; exciterOf is its hand. body and bodyTrack are where you hang a body on voice or track audio. The excitation can be a different recording, synthesized audio, or an extracted hand.
bell = patch {
source = modal (freq pitch)
(modalOf "hybrid_bowl" |> sustainBy 2.5 |> stretchBy 0.03)
(exciterOf "hybrid_tabla")
}
wash = patch { source = noise |> body native (modalOf "hybrid_glass") }
kit = patch { source = sample "hybrid_tabla", gain = vel * 0.6 }
drums = play kit "c4 . c4 [c4 c4] ."
|> bodyTrack native (modalOf "hybrid_bowl") 0.35
exciterOf sample [len]fills the exciter slot ofmodalorstring, or the source piped intobody.lenis a whole number of milliseconds, 2–500, default 40. LikemodalOf, the sample can be a string, asample "name"expression, or abounceOf/sampleOf/comphandle. Baresource = exciterOf "x"is a lowering error naming its slots. It replays once from note-on at the recording's rate, draws no RNG, and is silent until its table arrives. In a string's excitation it stretches a short capture to at least one played-note period, capped at 50 ms, followingstrike/pick's period-floor convention.nativein the frequency slot plays a fitted body at its recorded root, independent of the note. It requiresmodalOf; a literal table has no measured root. Use(freq pitch)to follow notes or(const 440Hz)to pin a voice body to another frequency.bodyTrackacceptsnativeor constant Hz (440Hzor(const 440Hz)); a track has no note pitch.modes |> sustainBy kmultiplies every T60 byk(each call clampskto 0.01–16).|> stretchBy sraises ratios to1+s(sclamps to −1…1): positive values spread modes, negative values compress them; it does not quantize an arbitrary body onto a harmonic series.|> tiltBy dBchanges amplitude per octave above the root (−24…24 dB/octave). These are constant table operations. Literal tables evaluate at compile time; fitted requests retain the three folded constants until the table latches. Pipe order matters: a tilt after a stretch measures the stretched octaves. Identity operations emit no IR keys or audio nodes.track |> bodyTrack freq modes amountadds resonance to the untouched dry track.amountis 0–1 and accepts a macro or affine macro expression; zero is bit-identical to no insert, even with a stored tail. Each stereo channel owns an independent preallocated bank. There is no added latency; reset clears the tail. This is a track insert, followingsympathetic's receiver contract.
Voice dry/wet#
Use a local source binding and mix the dry and resonated branches:
tumbler = patch {
source = let src = sample "hybrid_tabla" in
src * 0.6 + (src |> body native (modalOf "hybrid_glass")) * 0.4
}
The local binding preserves one excitation shared by both branches. Two separately authored noise expressions receive different node seeds, so write the source once in the binding. A regression test pins src * 0.5 + src * 0.5 to the single source, byte for byte, for both sample and noise. Gate a sustained exciter before body when its ring should continue after the drive stops. The voice budget includes sampled excitation length plus the ring ceiling (including a filtered sample), with an overall 30 s release cap; sustainBy 2.5 makes the fit ceiling 12.5 s.
Gain-matched auditioning#
Rack's Compare sound lets you Keep A, Run a different body or exciter, and Compare B at A's note and velocity. Match level equalizes stereo RMS over a fixed four-second probe, with common peak headroom. The suggested B trim and Copy multiplier help choose an explicit makeup gain; the song and export gains stay as authored. A retains its rendered audio across edits. See Compare sound at equal level for the window, headroom policy and last-run patch scope. bodyTrack belongs to the track mix and is outside this patch-only comparison.
Extraction and levels#
Author bodyTrack after play or sound. Native/JSON effect plans also support fitted bodies on bus and master chains. All three owners prepare their mode tables and accept tables arriving after the module, so asset arrival order does not choose whether the fitted body sounds.
The bake shares the default 24-mode modalOf analysis, cached by recording and mode count. A different explicit body count needs a separate fit. Volta's bank sums its sections in parallel: extraction solves the inverse of that sum using the section memories and their actual gains. Cascading individual section inverses would instead invert a series connection. The residual keeps len milliseconds from the loudest 1024-sample frame, with a flat attack, a half-Hann tail over its final quarter, and peak normalization. The inverse processes preceding samples to establish its memory. It is an approximation to a real object's excitation: unpicked modes and noisy tails can remain, and peak normalization discards absolute recording level.
The three-mode recovery fixture gives residual spectral deviation below 0.02 dB at its modes with the exact table, and better than −20 dB error over the first 200 ms using the fitted table after matching level. The default 40 ms window limits remaining tail without claiming perfect separation. See Modal Hybrids Verification 2026 09 08 for measurements and limitations.
Fitted amplitudes use √n normalization. The 64-mode full-scale noise probe fell from 1.17 RMS / 5.37 peak to about 0.147 RMS / 0.672 peak. This intentionally reduces the level of existing fitted bodies; literal tables retain their exact authored amplitudes. It is headroom, not a limiter: resonant tones and positive tilt can still be loud. Balance the drive and mixer level for the material. A long-lived body driven by a short, peak-normalized hand can also be very quiet: the “Hybrids” bowl voice uses measured makeup gain after modal to match its recorded-drum neighbors. This is ordinary signal multiplication; it does not change the extracted table or impose a gain on other bodies.
Not to be confused with sympathetic#
bodyTrack uses the measured or authored modes of one body. sympathetic [pitches] amount adds unplayed strings tuned to an explicit pitch list, while string … sympathy couples played string voices (§1.8).
1.10 The passive bridge — bridge = "passive", weight, bank, group#
Design spec: Coupled Physical Modeling. Opt-in: without bridge, group or a bowed sympathy nothing below is built and no IR key is emitted.
piano = patch {
source = string (freq pitch) 0.12 0.8 0.2 (pick 0.75) 0.2 0.5 { bridge = "passive" }
}
bass = patch { source = string (freq pitch) 0.1 0.7 0.15 (strike 0.6) 0.28 0.45 { group = "piano", bank = 0, weight = 3 } }
treble = patch { source = string (freq pitch) 0.16 0.8 0.13 (strike 0.7) 0.28 0.45 { group = "piano", bank = 2 } }
viola = patch { source = bowed (freq pitch) 0.6 0.14 0.35 { group = "damore" } }
The §1.8 bridge is an add-only bus: each string's tap comes back a block or more later, so it can lend energy but never remove it, and a note's decay splits by at most 1.5×. The passive bridge is a lossy scattering junction every coupled string meets at the same sample:
v = 2·Σ aⱼRⱼsⱼ / (r_b + Σ aⱼ²Rⱼ) yᵢ = sᵢ − aᵢ·v
sᵢ is the wave arriving at the bridge from string i, yᵢ the wave it gets back, aᵢ its sympathy (0 = rigid, not coupled), Rᵢ its weight, and r_b = 15·ΣRⱼ the bridge's resistive load. The in-phase motion of the strings (the motion that drives the bridge) loses 2k²/(15 + k²) per pass at sympathy = k; every out-of-phase motion is the junction's null space and keeps the string's own decay. So a unison or a chord falls fast and then sings, far harder than the bus. Measured on a 220 Hz pair (damping 0.12, offline, not listened to):
sympathy | in-phase | out-of-phase | split |
|---|---|---|---|
| 0.14 | −22.6 dB/s | −17.6 dB/s | 1.28× |
| 0.25 | −33.6 dB/s | −17.6 dB/s | 1.90× |
| 0.5 | −81.4 dB/s | −17.6 dB/s | 4.62× |
(The bus at 0.5 on the same unison: −22.0 dB/s against −17.6 dry.) A string alone on a passive bridge loses energy too — a lone string on a lossy bridge decays faster, as a real one does. A silent neighbour still rings at its own pitch (a silent 220 Hz string beside a plucked 110 Hz one at sympathy 0.4: amplitude 1.05·10⁻² at 220 Hz, 106× its level at 247 Hz).
Stable by construction. With E(x) = ΣRᵢxᵢ², E(y) = E(s) − 4c²·r_b/ρ² ≤ E(s) at every sample, for any attachments, any positive weights and any number of strings, so the whole network has loop gain ≤ the slowest string's per-pass gain (small-gain theorem). There is no voice-count normalisation and no cap on members.
weight(0.1–10, default 1, latched per note) — the string's impedance: a heavier string drives the bridge harder (a 4× weight drives a silent neighbour 1.9× harder, measured). Every string is moved by the bridge according to its ownsympathy.bank(0–3, rounded, latched per note) — which of four bridge points the string hangs on. Points are joined by a resistive link (conductance ¼ of the mean load), so strings on different points couple weakly (−19.8 dB against the same point, measured) but not independently. The junction stays passive:E(y) = E(s) − vᵀZ_b vwithZ_bpositive semidefinite.group = "name"— one passive bridge shared by every voice of every patch, on any track, that names it; impliesbridge = "passive". Grouping is per arena: preview and audition voices do not join the song's bridge.bowedcouples through the same junction: a nonzero or livesympathyreplaces its bridge reflection. Force is bounded byp/(4(5 − 4p))at default rosin (at most 4/3 higher otherwise); radiation stays outside the network. Coupled sweeps check finite internal waves and peak limits. Bowed and plucked voices can share a bridge.weightorbankaway from its default without a passive bridge is a lowering error;bridgeis"bus"or"passive".sympathyis live on the passive bridge (per-sample attachment);weightandbankare latched so the junction's energy norm is fixed through a note. The voice budget keeps the §1.8 coupled factor.
How it renders. The arena renders a voice's whole graph for a block before the next voice, so the coupled path gets a pre-pass (voice/junction.rs): every voice holding a passive seat evaluates its graph up to that node and pauses; each group of paused voices runs one loop in which, at every sample, each string arrives at the bridge, the junction scatters them all, and each departs; the voices then resume in the ordinary render pass, which evaluates everything downstream where it always did. Everything else keeps the block-at-a-time pass. arrive + depart is the plain string's tick bit for bit, so attachment 0 is the uncoupled string exactly; the render is bit-identical at 32/64/96/128-frame and ragged 480-frame callbacks, and on the ordered per-track path. A patch that also reads tapLive cannot be paused (the pre-pass has no current-block tap view), and a passive node inside a feedback region or a macro-modulation graph renders as a bridge of one.
1.11 blown — reed and jet pipes#
Design spec: Coupled Physical Modeling §7 (physical-modeling Stage 5).
clarinet = patch { source = blown (freq pitch) ((0.55 + vel * 0.3) * adsr 60ms 200ms 0.85 180ms) "reed" 0.08 }
flute = patch { source = blown (freq pitch) ((0.6 + vel * 0.3) * adsr 90ms 150ms 0.9 220ms) "jet" 0.22 }
Put the envelope on the pressure, not the output: the pipe speaks from about 0.5 and holds while blown, and a swell is the player blowing harder. No breath is exact silence.
"reed"— a bore half a period long (a closed–open tube, so the odd harmonics carry it: measured odd/even energy ≈ 460× at 220 Hz), a one-zero loss and an inverting bell reflection (−0.95), and the reed tabler = clamp(0.7 − 0.3·Δp, −1, 1)onΔp = −0.95·LP(bore) − breath. In tune within 0.2 cents over 110–880 Hz (measured)."jet"— STK's flute: bore and jet delays (jet = 0.32 of the bore), a one-pole and a DC blocker in the loop, the jet nonlinearityx(x² − 1)clamped to ±1, and the bore tuned to 2/3 of the note so it speaks in its stable overblown regime; within 9 cents over 220–880 Hz (measured at pressure 0.8 — a flute's pitch moves with the breath). Below ~200 Hz the jet barely speaks, as a real flute's range starts near middle C.- Bounded. Both nonlinearities are clamped, so each is a bounded forcing of a linear loop with round-trip gain < 1; the bores' waves stay under
BlownState::wave_bound(tested across 30 Hz–8 kHz, pressure and noise end to end; worst output 0.86). - Breath noise is seeded from the performance coordinate (bit-identical replays); the state re-arms in place per note; the voice budget is 0.5 s past release.
1.12 Acoustic bodies — bowed { body } and sig |> stringBody body [mix] [mute] [width] [angle]#
engine/src/string_body.rs holds four authored bodies, shared by bowed's body option and the voice effect stringBody:
pizz = patch { source = string (freq pitch) 0.4 0.7 0.2 (pick 0.6) |> stringBody "cello", gain = 0.3 }
knock = patch { source = strike 0.8 |> stringBody "bass" 0.7 }
cello = patch { source = bowed (freq pitch) 0.65 0.25 { speed = 0.5, body = "cello" } }
body— compile-time"none","violin","viola","cello","bass"(the default tier), the high-detail"violinFine","violaFine","celloFine","bassFine", and their two-microphone twins"violinStereo","violaStereo","celloStereo","bassStereo"(see High-detail tier below). Anything else is a check error. A…Stereobody makes the voice a stereo pair onbowedtoo (plain and coupled paths):bowed … { body = "celloStereo" }is the same sound asbowed … { body = "none" } |> stringBody "celloStereo"(to float precision, since the body sits outside the string's loop); only thestringBodyform takes the pair'swidth/angle."none"passes the signal through bit for bit; on the wire it is an absentbodykey.mix(stringBodyonly; 0..1, default 1, live, read per sample) — the crossfadex + mix·(body(x) − x).mix = 0is the input exactly; the body keeps ringing underneath, so sweeping the mix never restarts it.mute(stringBodyonly; 0..1, optional, live) — a mute on the bridge, on the wet path before the body (the dry side ofmixstays the input); see Mute below. Absent or 0 is the unmuted body bit for bit.width,angle(stringBodywith a…Stereobody; optional, live, smoothed over ~20 ms; other bodies ignore them) — the microphone pair's placement, see The microphone pair below. Absent is the unplaced pair exactly (no wire key, no node).- A stereo input folds to mono. The output is mono except for a
…Stereobody, which originates the voice's L/R pair. Voice-only: on a track it is refused (put it in the patch'ssourcechain).
What a body is. Each is its own parallel bank — none is a scaled copy of another, since the larger instruments are not scaled violins — of authored resonances, plus a high-passed direct path (radiation falls away below the air mode) and a final 12 dB/oct low-pass. Every section is a TPT state-variable filter whose band output is normalized to unit gain at its centre; each mode carries a level and a sign (a negative sign is a mode radiating out of phase with its neighbours; two same-sign neighbours leave an antiresonant trough between them). Modes at or above 0.45·sr are dropped and coefficients are recomputed for each sample rate. Frequencies in Hz / Q / level dB / sign:
| Violin | Viola | Cello | Bass | |
|---|---|---|---|---|
| A0 (air) | 279 / 15.7 / +1 / + | 233 / 14.6 / +3 / + | 102 / 13.5 / +5 / + | 60 / 9.9 / +5 / + |
| CBR | 404 / 25 / −6 / + | 341 / 22 / −6 / + | 145 / 18 / −6 / + | 85 / 16 / −4 / − |
| B1− | 465 / 20 / +4 / + | 386 / 20 / +3 / + | 172 / 18 / +6 / + | 105 / 14 / +2 / + |
| B1+ | 543 / 20 / +8 / − | 472 / 18 / +6 / + | 216 / 16 / +4 / − | 134 / 13 / +5 / − |
| mid modes | 662 / −2.5 / −, 851 / −2.4 / +, 1169 / 0 / + | 589 / +1 / −, 757 / −4 / +, 1000 / −1 / + | 290 / +2 / +, 402 / −4 / +, 556 / −3 / + | 190, 270, 379 |
| bridge | hill 2489 / Q 2.2 / +5 | hill 2090 / Q 2.0 / +3 | 986 / 2.3 / +5 and 2095 / 2.9 / +4 | 653 / 2.2 / +2 |
| upper | 3297 / 0 / −, 4602 / −1 / + | 2901 / +1 / −, 4010 / −3 / + | 3202 / −4 / − | 1790 / −7 |
| direct path | −5.9 dB, HP 220 | −7.6 dB, HP 185 | −8.3 dB, HP 80 | −7.5 dB, HP 48 |
| low-pass | 8 kHz | 7 kHz | 7 kHz | 2.8 kHz |
The cello's ~1.5 kHz trough is the antiresonance between its two same-sign bridge resonances, not a mode of its own.
The folk bodies (engine/src/string_body/folk.rs, the same default-tier bank; responses under The folkstrings library): "hurdyGurdy" (air mode 205 Hz, a dense plate region, a broad 2.6 kHz lift where the dog hammers the board, a 6.5 kHz corner), "damore" (the viola's air mode with weaker corpus modes and a flat back's own mode near 600 Hz, a soft 2.2 kHz hill), "hardanger" (the violin's modes a few percent lower, a stronger B1+, a 2.8 kHz hill and more radiation above), "erhu" (almost nothing below 300 Hz, a 600 Hz cylinder cavity and a python-skin membrane's modes at the circular membrane's Bessel ratios over a 900 Hz (0,1) mode) and "morinKhuur" (a 140 Hz air mode, strong wood modes to 450 Hz, a 4 kHz corner). Authored from published descriptions of the instruments; no measured response of them is in the local DSP corpus.
Re-fitted in Stage 16 — existing songs' audio changed again. All four tables above (the bass too) come from a seeded search (string_body::design_lab) over each authored section's frequency (±4 %), Q, level and sign and the direct path, staying near the literature values, that brings every note's H2 and H3 within +8 dB of its fundamental from the air mode up across the instrument's whole range (violin G3–E7, viola C3–A6, cello C2–A5, bass E1–G4, every semitone; string_body::balance_tests), and +14 dB below A0, where a weak fundamental is what a real body radiates. The Stage 10 tables still buried whole stretches: the violin's D♯4–G4 sat −13 to −23 dB under the plateau (G4's H2/H3 +25.5/+26.5 dB over its fundamental), the viola's C5–C♯5 +16–18 dB, the cello's C♯3 +15 dB and D3 +9.7 dB, the bass's E2 +13 dB. Measured after: worst lift from A0 up +7.6 dB per body (cello D3 H2 +6.8, violin G4 within the cap); below A0 at most +12.9 (viola C3). The mostly-larger direct path (−6 to −8 dB, the non-resonant radiation) is what fills the A0–B1 gap. default_tier_is_pinned was re-pinned.
Retuned in Stage 10 — existing songs' audio changed. The violin, viola and cello tables above replace the Stage 6 ones (the bass is unchanged), so every song using bowed { body = "violin" | "viola" | "cello" } or stringBody with those names renders differently. Stage 6's mid-mode signs cut a notch near 750–830 Hz into the violin (a G5 came out with H2 +13 and H3 +20 dB over its fundamental, measured as the body's gain at h·f0 minus its gain at f0), the viola and cello buried some fundamentals the same way, and the low-passes at 4.2–6.5 kHz made a cliff. The retune keeps the same ≤12 sections; it flips mid-mode signs and levels so the nasal band stays within ~4 dB of the 1–4 kHz mean, and opens the upper modes and low-pass so radiation falls off gradually (see the comparison tables under High-detail tier). The bit-identity hash (default_tier_is_pinned) was re-pinned.
Sources. The values are recalled from the published literature — the local DSP corpus cites most of these but does not contain their data: Bissinger, "Structural acoustics of good and bad violins" (JASA 124, 2008: the A0/CBR/B1−/B1+ statistics); Jansson, Acoustics for Violin and Guitar Makers, and Woodhouse, "On the 'bridge hill' of the violin" (Acta Acustica 91, 2005); Gough (EPJ ST 145, 2007); Dünnwald's radiation bands; Reinicke's bridge resonances and the viola/cello (Bynum & Rossing) and double-bass (Askenfelt) mode ranges as collected in Rossing (ed.), The Science of String Instruments (2010). The parallel-resonator form follows JOS PASP ("Body Model Factoring", "Parallel Body Filterbank Design"; corpus html/jos-pasp), Karjalainen & Smith (ICMC 1996), Bank (ICMC 2007) and Rau & Abel's modal upright-bass body (DAFx-18, corpus). Real instruments vary by more than these tables differ — they are family signatures, not any instrument's response.
Why a modal bank. A body IR long enough to hold a 60 Hz, Q≈12 air mode is thousands of taps per voice; the bank is ≤12 resonators per sample, a few hundred bytes of boxed state re-armed in place per note (no audio-thread allocation; rt_safety's string_body fixture), and exact at any sample rate.
Measured offline, not listened to (engine/src/string_body.rs and engine/tests/string_body.rs tests, 48 kHz):
| pink noise vs none | saws across own register | pink centroid | bowed g2 / d4 / e5 vs none | |
|---|---|---|---|---|
| none | 0 dB | 0 dB | 2592 Hz | — |
| violin | −1.3 dB | +1.3 dB | 2167 Hz (Stage 10: 2154) | −0.2 / +2.9 / +1.4 dB |
| viola | −0.9 dB | +0.9 dB | 1543 Hz (1722) | +2.3 / +2.1 / +1.0 dB |
| cello | −1.4 dB | +1.4 dB | 1165 Hz (1409) | +3.6 / +4.2 / −0.8 dB |
| bass | −2.5 dB | +2.5 dB | 377 Hz (390) | +1.2 / −2.3 / −7.5 dB |
The per-body trim zeroes the mean of the two broadband references, which then sit within ±3 dB at 44.1, 48 and 96 kHz. A single note still lands on or between resonances — the cello's open G sits on its A0, an e5 is far above a bass body's register — so one note reads several dB either side. The steady-state sine gain peaks at +7.7 to +9.3 dB per body, under the 4× (+12 dB) resonant widening refine.rs gives the node's output range, as it does svf; the absolute transient bound (impulse-response L1 norm) is below 16.
High-detail tier — …Fine and …Stereo#
engine/src/string_body/fine.rs. Same node, same mix, a different bank: the ~10 broad sections of the default tier become 131–168 narrow modes, so a body has individual resonances and antiresonances all the way up, and moving partials (vibrato, glides) catch and lose them. The default tier stays the cheap ≤12-section bank (re-fitted in Stage 16, above; default_tier_is_pinned hashes it at 44.1/48/96 kHz).
solo = patch { source = bowed (freq pitch) 0.65 0.2 { speed = 0.5, body = "violinFine" } }
room = patch { source = bowed (freq pitch) 0.65 0.2 { speed = 0.5 } |> stringBody "violinStereo" { width = 0.6, angle = -0.25 } }
duo = patch { source = bowed (freq pitch) 0.65 0.2 { speed = 0.5, body = "celloStereo" } }
pizz = patch { source = string (freq pitch) 0.4 0.7 0.2 (pick 0.6) |> stringBody "celloStereo", gain = 0.3 }
The model. The radiated pressure at a microphone is a modal sum Σ aₖ·BPₖ, each BPₖ a unit-peak SVF band-pass and aₖ a signed residue (mode shape at the bridge × radiation toward that microphone). Two neighbours of the same sign cancel between their peaks — antiresonances form from the signs, nobody places them.
- Authored low modes (8–12 per body), literature frequencies and Q from loss factors (ζ ≈ 1.1–1.4 %; A0 lower-Q from its radiation loss), levels and signs re-fitted in Stage 16 (below): violin C1 186, A0 269, CBR 389, A1 455, B1− 488, B1+ 539 Hz and six corpus modes a few dB under the B1 peaks in Dünnwald's 650–1300 Hz band; viola A0 230, CBR 335, A1 385, B1− 405, B1+ 475; cello A0 102, CBR 145 (in phase with A0 and B1−, so the open D is not buried in an antiresonance), B1− 172, B1+ 215 and more to 545 Hz; bass A0 60, 85, 105, 135 and four more to 285 Hz.
- Statistical region above them (violin 1.1–13 kHz, viola 0.96–12, cello 0.59–10, bass 0.32–7 kHz), where modes overlap and identities stop mattering (Woodhouse's bridge-hill model; statistical modal theory): Wigner-surmise spacings (level repulsion) around a mean spacing from plate modal density (violin 60 Hz, viola 52, cello 36, bass 30 at the bottom, opened to 70–110 Hz at the top where the modes overlap completely), Q = 1/η with η rising 0.020 → 0.05 (violin, viola), 0.022 → 0.055 (cello), 0.025 → 0.06 (bass) and ±25 % jitter, and Gaussian residue magnitudes with
E|aₖ|² = env² · 2QΔf/(πf)so the mean response follows the envelope at any density. Signs follow a chain per microphone: each mode flips its lower neighbour's sign except with probabilitysame_sign(0.25; bass 0.4), where the two cancel into an antiresonance. A radiated transfer function has fewer zeros than poles (Lyon's progressive phase trend), so fewer notches than a fully random sign's driving-point-like response. - Floor and edits (Stage 16): a direct path at −6 dB (bass −9) low-passed at 1.9 kHz (violin), 1.4 kHz (viola), 0.7 kHz (cello), 1.3 kHz (bass) stands for the non-resonant radiation beneath the modes, and 1–7 drawn residues per body are scaled or flipped (
edits), both fromdesign_lab's search for the harmonic-balance cap. - Envelope = a log-linear radiation trend, flat-ish (±5 dB) through the playing range and falling about −12 dB/oct above the bridge region (violin from 4.5 kHz, viola 4, cello 3.2, bass 2.5 kHz) — so a violin keeps energy to 10 kHz instead of meeting a low-pass — lifted by the bridge resonances as band-pass boosts: violin rocking 2.7 kHz Q 2.2 +4 dB (the bridge hill) and bouncing 5 kHz +2 dB; viola 2.15 kHz +3.5 dB and 4 kHz; cello 985 Hz and 2.1 kHz, +3 dB each (Reinicke's two); bass 650 Hz +4 dB and 1.5 kHz.
- Directivity (
…Stereo): below an onset where the wavelength nears the body's size (violin 0.7–1.6 kHz, viola 0.6–1.4, cello 350–900 Hz, bass 280–750 Hz) both microphones share every residue; above it the second microphone's residue is a fresh draw (cosine taper between). The left microphone IS the…Finebody; the right shares the low modes, and its direct path is low-passed no higher than the onset top (the floor beams too). Instruments radiate in lobes above ~1 kHz, so two directions hear different high-frequency peaks — what a room or a spaced pair hears, unlike a single pickup. - Deterministic. Each body draws from one fixed seed (no per-voice randomness), the same modes at every sample rate (modes ≥ 0.45·sr dropped). Seeds were chosen from 400 by a survey (
string_body::fine::survey) for a typical draw: per-octave power of each microphone within ~1–2 dB of the ensemble mean, balanced L/R, low inter-microphone correlation.
Cost. Structure-of-arrays chunks of 8 sections with fixed-order lane sums, so the result is the same whether or not the compiler vectorises; a ≈6.7 KB inline bank boxed per voice, re-armed in place (rt_safety's string_body_fine). Measured 71–87 ns/sample per voice natively on the quietest run (--profile fast, 0.34–0.42 % of a core at 48 kHz; runs on a loaded machine read 2–7× that; the default tier is 16 ns); stereo costs about the same as mono, and a placed pair (width/angle given) adds 6–8 ns (rt_safety's string_body_pair).
Measured offline, not listened to (string_body::fine_tests, engine/tests/string_body.rs; 48 kHz):
| modes | pink / register saw vs none | pink centroid | peak gain | antiresonances ≥10 dB, 4·A0–5 kHz | L/R coherence 50–300 Hz / 2–5 kHz | |
|---|---|---|---|---|---|---|
| violinFine | 150 | −1.5 / +1.5 dB | 1313 Hz (Stage 11: 2223) | +13.1 dB | 12 (default tier 0, 0.4–5 kHz) | 1.000 / −0.32 |
| violaFine | 155 | −1.5 / +1.6 dB | 1000 Hz (1915) | +13.6 dB | 7 (2) | 1.000 / +0.03 |
| celloFine | 164 | −2.3 / +2.3 dB | 571 Hz (1294) | +9.6 dB | 5 (0) | 0.999 / −0.05 |
| bassFine | 132 | −2.1 / +1.9 dB | 347 Hz (616) | +11.3 dB | 3 (0) | 0.996 / +0.36 |
Coherence is the pink-weighted zero-lag correlation of the two microphones' responses over the band (the L−R level difference there is within 1.6 dB). Trims hold both references within ±3 dB at 44.1, 48 and 96 kHz. Bowed through a fine body across its own register: −1.5 (violin), +4.2 (viola), +0.9 (cello), +0.2 dB (bass) mean against the bare bow. The Stage 16 floor makes the fine bodies markedly darker on pink noise (centroids above); on bowed notes against the recordings below they moved from 0.4–0.9 octave brighter than the sections to within ±0.3. refine.rs widens a …Fine/…Stereo body's output range 8× (+18 dB) instead of the default tier's 4×; impulse-response L1 stays under 19.
Harmonic balance and top end (harmonic_balance_and_radiation_curves: the body's gain at h·f0 minus its gain at f0 — exactly the change it makes to Hh/H1, since it is linear and output-only — and third-octave power against the 1–4 kHz mean):
| note | Stage 6 default H2 / H3 lift | Stage 10 default | Stage 16 default | Stage 11 fine | Stage 16 fine |
|---|---|---|---|---|---|
| violin A4 | +1.6 / −1.7 dB | +4.7 / −0.9 dB | +2.1 / −0.3 dB | +3.1 / +4.0 dB | −2.4 / +0.1 dB |
| violin G5 | +13.0 / +20.3 dB | −3.4 / +4.3 dB | −2.5 / +4.0 dB | +4.3 / +4.8 dB | +1.8 / −12.6 dB |
| violin E6 | +9.7 / −1.3 dB | +9.7 / +1.6 dB | +7.5 / +0.9 dB | −2.1 / −6.3 dB | +4.3 / −16.6 dB |
| viola C4 | +10.6 / +12.3 dB | −2.7 / +10.9 dB | +5.1 / +7.5 dB | +1.2 / +7.1 dB | +3.4 / +0.3 dB |
| viola E5 | +7.2 / +13.0 dB | +1.6 / +5.6 dB | −0.8 / +5.8 dB | +12.2 / +5.2 dB | −3.4 / +5.9 dB |
| cello D3 | +10.6 / −4.3 dB | +9.7 / +7.0 dB | +6.8 / +1.3 dB | +5.1 / −1.9 dB | −2.8 / +2.0 dB |
| cello A3 | −11.6 / −0.1 dB | −0.5 / +3.8 dB | −1.5 / +2.1 dB | −6.7 / +3.4 dB | +2.0 / −5.1 dB |
| cello A4 | +14.0 / +5.3 dB | +7.0 / −2.9 dB | +5.5 / −5.1 dB | +6.6 / +6.7 dB | −5.2 / −12.2 dB |
Full-range cap (balance_tests::harmonic_balance_full_range_is_capped, every semitone of each instrument's range, both microphones of a …Stereo body; steady-state lifts, and the same averaged over a ±20 ct vibrato swing): no note from A0 up lifts H2 or H3 more than +8 dB in any tier; worst from A0 up, Stage 15 → Stage 16: violin default +26.5 → +7.5, viola +18.4 → +7.6, cello +15.1 → +7.6, bass +13.3 → +7.4 dB; violinFine +26.1 → +7.6, violaFine +28.3 → +7.5, celloFine +25.9 → +7.0, bassFine +21.0 → +7.3; the …Stereo right microphones +37.3 (viola D6) → ≤ +7.5 dB. 10–25 notes per body were over the cap before, none after. Below A0 the bound is +14 dB (worst +12.9, the default viola's C3): the air mode is where a real body stops radiating its fundamental, and violinists' lowest notes do carry their upper partials over it. The vibrato-averaged lifts are within 0.6 dB of the steady ones (±20 ct is narrow against the mode spacing).
| third octave re 1–4 kHz | nasal band min | 4 kHz | 5 kHz | 6.3 kHz | 8 kHz | 10 kHz | 12.5 kHz |
|---|---|---|---|---|---|---|---|
| violin, Stage 6 | −8 (800 Hz) | −6 | −4 | −10 | −15 | −21 | −28 |
| violin, retuned | −3 | −3 | −1 | −6 | −11 | −16 | −23 |
| violinFine | −7 | −2 | −1 | −8 | −14 | −16 | −17 |
| cello, Stage 6 | −3 (300–1300 Hz) | −8 | −12 | −16 | −20 | −25 | −31 |
| cello, retuned | −4 | −3 | −6 | −9 | −13 | −18 | −24 |
| celloFine | −4 | −1 | −9 | −10 | −19 | −16 | −20 |
Through the playing range every fine-tier third octave lies within ±15 dB of that plateau (the test's bound; the dips are the A0–B1 and nasal regions), and no violin, viola or cello test note's H2 or H3 is lifted more than +12.2 dB (viola E5, in the nasal band). The retuned default tier is held to ≤ +12 dB on the same notes (worst +10.9, viola C4's H3), ≤ +8 dB at violin G5, a nasal band no deeper than −4.5 dB and an 8 kHz (cello 6.4 kHz) band between −18 and −6 dB.
Vibrato shimmer. Because the modes stay put, vibrato sweeps each partial across them. Peak-to-trough AM of partials 3–10 under 6 Hz ±20 ct vibrato (heterodyned along the true phase, 4-term Blackman–Harris over four periods; the bare probe reads 0.0 dB): violinFine at A4 median 7.3 dB (Stage 11: 4.8), violaFine at D4 2.5 dB (3.4), celloFine at G3 4.2 dB (2.7), bassFine at G2 3.1 dB (3.3) — against 0.3–0.8 dB through the default tier.
The microphone pair — stringBody "…Stereo" { width, angle }#
engine/src/string_body/pair.rs. The two views of a …Stereo body are what two directions hear: shared below the directivity onset, independent residues above it — Weinreich's directional tone colour (JASA 101, 1997): above ~1 kHz an instrument's radiation pattern changes from one partial to the next, so vibrato sweeps each partial through different lobes toward each microphone. The pair stage puts that body in front of two cardioids:
width(0..1; absent 1) —r = l + width·(r − l), spacing0.5 m·width, splay±55°·width. 1 is a spaced pair about ORTF-to-AB wide, 0 one point and one direction (mono).angle(−1..1 = ±90°; absent 0) — the instrument's azimuth from the pair: the far microphone hears itspacing·sin θ / 343 m/slater (a 4-point Hermite fractional delay, up to 1.46 ms) and the cardioids1 + cos(θ ∓ φ)set the level difference, normalised to equal power.
Both are live and smoothed over 20 ms (the first value of a note is taken at once); the defaults are skipped, so an unplaced pair is the two views bit for bit. Measured offline (balance_tests::pair_*, 48 kHz, not listened to):
| placement | ITD | ILD | ρ per octave 177 / 354 / 707 / 1414 / 2828 / 5657 Hz (violinStereo) |
|---|---|---|---|
| width 1, angle 0 (default) | 0 | 0 | +1.00 / +1.00 / +0.97 / +0.26 / −0.32 / +0.04 |
| width 0.5, angle 0.25 | 279 µs | −1.7 dB | +0.93 / +0.82 / +0.35 / −0.57 / +0.27 / −0.11 |
| width 1, angle 0.25 | 558 µs | −3.6 dB | +0.74 / +0.35 / −0.65 / −0.16 / +0.25 / +0.22 |
| width 1, angle −0.5 | −1031 µs | +7.6 dB | +0.19 / −0.46 / −0.19 / −0.20 / −0.20 / −0.16 |
(ILD is L over R; a negative ITD is the left microphone first.) The pair's delay is the geometry's to 0.01 sample on identical channels; the low band's level difference is the cardioids' to 0.5 dB on every body. The spatial shimmer: under the same 6 Hz ±20 ct vibrato, the L−R level of partials 3–10 swings by a median 7.9 dB (violin A4), 3.0 (viola D4), 7.6 (cello G3) and 3.4 dB (bass G2) — a mono body's swings 0.
The hall — stage and hallTail#
engine/src/hall.rs. A track's instrument sits on the stage of a shoebox room and a stereo pair at a seat in the audience hears it; one late tail per room is shared by every track:
tone = patch { source = saw (freq pitch), ampEnv = adsr 40ms 0ms 1 200ms, gain = vel * 0.2 }
bus hall = hallTail "chamber" { listener = "row10" }
vlns = play tone !"a4 bb4 a4 g4" |> stage "chamber" (-0.6) 0.2 { listener = "row10", radiator = "violin" }
mixer { vlns = { sends = { hall = 0dB } } }
|> stage room x depth { listener, radiator, early, width }— the direct sound and every image source up to third order (Allen & Berkley; 62 reflections), each with its own delay,1/rspreading, per-surface absorption (the scattered fraction of each bounce goes to the tail), air absorption, and the instrument's radiation in the direction it left: two-band directivity split at 1 kHz,(1 − k) + k·cos θnormalised to unit mean power — violins and violas up and toward the audience above 1 kHz, cellos and basses forward (Meyer). Each arrival meets a cardioid of the pair (Stage 16's geometry: 0.5 m, ±55° atwidth1) from its own direction, so time and level differences come from the geometry. The insert's output is that early field only (the dry track does not pass through); a stereo input keeps its two views on the direct path (each microphone hears its own), and the reflections take the mid.room"chamber" / "hall" / "cathedral" / "crypt";x−1..1 audience left..right;depth0..1 front edge..back row (with the room's risers);listener"conductor" / "row10" (default: the level reference) / "balcony";radiator"omni" (default) / "violin" / "viola" / "cello" / "bass";early0..1 scales the reflections.x,depthandearlyare live (a macro's travel mapsxacross −1..1; scene addressestrack.stage.x): the seat glides over ~60 ms and the taps ramp per sample, a small Doppler shift. Delays are relative to the nearest seat anyone can take, so the listener's absolute distance is not latency.hallTail room { listener, mix }— the room's FDN (fdn.rs, no velvet reflections) with its band RT60s from the same surfaces by Eyring's formula, starting just after a centre-stage source's direct sound (the hybrid hand-over, so it fills the early field the image sources leave sparse), and its level calibrated so a 0 dB send of the dry track gives Sabine's diffuse-to-direct ratio decayed to its onset after emission (Barron's revised theory): a far seat hears less of it.mix(default 1, wet only) is the aux-bus setting the calibration assumes.
The library's seats (bowstrings, § HALL): violinStage (x −0.6, depth 0.2), secondViolinStage (−0.25, 0.5), violaStage (−0.2, 0.35), celloStage (0.3, 0.3), bassStage (0.6, 0.85), soloistStage (0, 0) — x is twice the seat angle — and t |> onStage { room, listener } seat. staged s centres a deluxe body's own pair (seat = { width = 1, angle = 0 }) so the stage does all of the placement. Demo: hall-seats (one passage, four seated sections, chamber then crypt).
Measured (hall::lab, --ignored, 48 kHz impulse responses; offline, not listened to). Rooms (L × W × H m), Eyring RT60 below / above 1 kHz, and the T30 of the full response (stage + tail, centre seat) in the 500 Hz and 2 kHz octaves, then C80 and the pair's inter-channel cross-correlation (max |ρ| within ±1 ms; between microphones, not ears) over 0–80 ms and after 80 ms at the three listeners:
| room | Eyring lo / hi | T30 500 / 2k | C80 conductor / row10 / balcony | ρ early | ρ late |
|---|---|---|---|---|---|
| chamber 26 × 14 × 9 | 1.57 / 1.07 s | 1.57–1.63 / 1.31–1.34 s | +4.0 / +1.1 / +1.0 dB | 0.65 / 0.31 / 0.29 | 0.02–0.03 |
| hall 44 × 24 × 17 | 2.50 / 1.70 s | 2.51–2.58 / 2.03–2.09 s | +3.8 / −0.7 / −1.4 dB | 0.80 / 0.41 / 0.30 | 0.03–0.06 |
| cathedral 90 × 32 × 30 | 7.72 / 4.09 s | 7.12–7.27 / 5.12–5.17 s | +2.2 / −4.5 / −9.9 dB | 0.97 / 0.86 / 0.52 | 0.03–0.05 |
| crypt 36 × 12 × 4.5 | 3.33 / 1.87 s | 3.29–3.32 / 2.42–2.46 s | +0.1 / −1.6 / −2.1 dB | 0.49 / 0.25 / 0.15 | 0.02 |
The hall's C80 falls from podium to balcony as Barron's revised theory predicts for that room (+4.1 / −0.3 / −1.4 dB). The 2 kHz T30 sits above the Eyring high band because the FDN's mid band starts at 1.2 kHz. The direct sound alone at the pair (ITD, + = the right microphone later; ILD L over R) for the library seats:
| room, listener | violins | violas | cellos | basses |
|---|---|---|---|---|
| hall, conductor | +1.19 ms, +9.7 dB | +0.48 ms, +3.7 dB | −0.73 ms, −6.2 dB | −0.71 ms, −3.7 dB |
| hall, row10 | +0.46 ms, +1.5 dB | +0.15 ms, +1.2 dB | −0.23 ms, −2.1 dB | −0.35 ms, −0.8 dB |
| hall, balcony | +0.23 ms, +2.3 dB | +0.08 ms, −1.0 dB | −0.10 ms, −0.1 dB | −0.19 ms, −0.9 dB |
| chamber, row10 | +0.31 ms, +2.6 dB | +0.10 ms, −0.3 dB | −0.15 ms, +0.3 dB | −0.25 ms, −1.4 dB |
| crypt, row10 | +0.25 ms, +0.3 dB | +0.08 ms, +0.2 dB | −0.12 ms, −0.1 dB | −0.21 ms, −0.3 dB |
(The ILD mixes the cardioids with the radiators' lobes: a cello's forward lobe and a viola's upward one reach the two microphones differently.) Moving an omni seat across the hall's stage from row 10, x −1 → +1 in steps of 0.25, moves the image monotonically: ITD +0.69 → −0.69 ms, ILD +5.0 → −5.0 dB, antisymmetric. Early pattern, violins at row 10, left microphone (ms after the direct, dB re it): the chamber's first reflection at 11.6 ms −1 dB, then 12.2 −8, 21.2 −7, 25.6 −12, 29.1 −8; the hall's at 27.6 ms −4 and 59.3 −8; the crypt's dense from 4.6 ms (0 dB) at 8.4, 8.5, 9.3, 13.0 ms — the vault's floor-to-ceiling flutter. Engine tests (engine/tests/hall_stage.rs): a left seat arrives first and louder on the left with and without reflections, a macro-graph seat walking left → right turns the ILD from positive to negative while it plays, and the tail bus decays at its room's rate. Cost (lab_hall_cost, a loaded machine): a still seat ≈ 0.09× the FDN reverb, ≈ 0.12× while it moves every block; the tail ≈ 0.8× (no velvet reflections) — priced 9 and 86 in cost.rs (the reverb is 85).
Against recordings#
string_body::realism_lab (--ignored) renders bowed notes (the library's mid-dynamic hand, bite and delayed vibrato) through each tier and compares them with the Vienna Symphonic Library section sustains the repository's vslsus* banks hold (Kontakt Factory Library ensembles, autosampled; 31 notes across the four instruments, v64). They are sections on a stage, not solo instruments, so only smoothed measures are compared: the third-octave long-term spectrum from 5·f0 to 10 kHz (dB RMS after removing the mean), the balance of H1–H8 (dB RMS), the spectral centroid (octaves,
- brighter than the recording) and the harmonic slope (dB per octave of harmonic number). Measured, not listened to; Stage 15 → Stage 16:
| LTAS | H1–H8 | centroid | slope | |
|---|---|---|---|---|
| violin bare / default / Fine | 5.1 / 3.5 → 3.6 / 3.4 → 4.0 | 4.0 / 4.1 → 3.7 / 4.9 → 6.2 | +0.05 / +0.20 → +0.22 / +0.44 → −0.24 | +4.0 / +0.7 → +1.1 / +1.8 → −0.9 |
| viola | 8.1 / 5.7 → 6.3 / 5.4 → 5.3 | 5.0 / 6.7 → 5.7 / 6.4 → 5.7 | +0.21 / +0.71 → +0.52 / +0.91 → +0.01 | +2.4 / +2.6 → +1.9 / +2.7 → +0.3 |
| cello | 5.6 / 3.4 → 3.7 / 4.6 → 3.2 | 5.9 / 6.5 → 5.8 / 7.5 → 5.9 | +0.13 / +0.66 → +0.30 / +0.47 → −0.29 | +0.3 / +1.6 → +0.5 / +0.9 → −1.8 |
| bass | 10.6 / 6.7 → 6.6 / 6.1 → 5.0 | 6.2 / 7.3 → 6.4 / 8.6 → 6.3 | +0.57 / +0.45 → +0.24 / +0.70 → +0.24 | +2.6 / +1.2 → +0.8 / +3.0 → +1.0 |
Every body brings the long-term spectrum nearer the recordings than the bare string; Stage 16 moved the fine bodies' centroids from too bright to within ±0.3 octave and the H1–H8 balance nearer for the viola, cello and bass (the fine violin's got further, 4.9 → 6.2 dB, its low register now carrying more fundamental than the sections). Attack times are the bow's, not the body's, and the samples' sustains swell (10→90 % of their held level in 0.35–0.98 s against our 10–50 ms), so they are printed but not compared. No solo recordings or measured admittance curves are available locally (the DSP corpus cites Bissinger, Jansson, Woodhouse and Maestre's measurements without their data), so this section-against-solo comparison is the only recorded reference.
Mute — bowed { mute } and stringBody … { mute }#
A mute adds mass to the bridge. The bridge's in-plane resonance — the bridge hill — falls by √(1 + μ) and is damped, and above it the bridge passes 1/(1 + μ) of the string force, μ the mute's mass over the bridge's effective mass. engine/src/sordino.rs is the ratio of the loaded bridge to the free one, one biquad (bilinear, prewarped, unity at DC): (s² + s·ωb/Qb + ωb²)/(s² + s·ωm/Qm + ωm²)·ωm²/ωb², ωm = ωb/√(1 + μ), Qb = 1.5, Qm = Qb/(1 + μ)^¼, the free bridge at 3 kHz (violin and bare string), 2.4 kHz (viola), 2 kHz (cello), 1.1 kHz (bass). μ = 30·mute^log₂10: 3 at 0.5, 30 at 1. It is a separate stage on the force side of the body, not a change to the body tables, so one filter serves a stereo pair. Its largest lift anywhere is +0.86 dB (the loaded bridge's new resonance; the range analysis widens a muted stringBody by ×1.11 for it).
Measured on held bowed notes (mid hand, third-octave change re unmuted; the level is RMS, then A-weighted over the harmonics):
| 1 kHz | 1.6 kHz | 2.5 kHz | 3.15 kHz | 4 kHz | 10 kHz | level | A-weighted | |
|---|---|---|---|---|---|---|---|---|
violin A4, mute = 0.5 | +0.7 | −2.9 | −11.6 | −14.8 | −15.1 | −12.6 | −0.5 | −1.3 |
violin A4, mute = 1 | −12.9 | −22.2 | −30.9 | −33.7 | −33.6 | −30.5 | −5.9 | −8.3 |
| viola C4, 0.5 / 1 | +0.1 / −17.2 | −5.0 / −24.6 | −14.9 / −33.8 | −14.9 / −33.4 | −13.7 / −31.9 | −12.4 / −30.3 | −2.1 / −9.5 | −3.1 / −15.4 |
| cello D3, 0.5 / 1 | −1.5 / −20.4 | −10.2 / −29.6 | −15.2 / −33.8 | −14.2 / −32.4 | −13.6 / −31.7 | −12.3 / −30.2 | 0.0 / −4.6 | −0.5 / −9.1 |
| bass A1, 0.5 / 1 | −12.3 / −31.5 | −14.5 / −32.9 | −13.2 / −31.2 | −12.7 / −30.7 | −12.5 / −30.4 | −12.1 / −29.9 | 0.0 / −0.8 | −0.8 / −7.9 |
Below half the free bridge frequency nothing moves (250 Hz within ±0.8 dB, the bass's practice mute −7.9); slips per period are unchanged (the mute is outside the loop). A concert mute changes the colour more than the level — the fundamental-heavy RMS barely moves — and a practice mute is both. On violinFine the change is the same within 0.6 dB. Measured offline, not listened to.
The impossible strings — bodyTo, bodyMorph, bodySize, scale, pickup, pickupPos#
Bowed instruments no workshop can build (Bowed Strings Stage 24; engine/src/bowed_impossible.rs, engine/src/string_body/morph.rs; the library impossiblestrings, audition the-twenty-metre-string). Every option is optional and absent on existing songs (no IR key, the same audio).
A body that changes while it sounds. bowed { body = "violinFine", bodyTo = "celloFine", bodyMorph = m } and the effect form sig |> stringBody "violinFine" { to = "celloFine", morph = m, size = s } morph the body itself, not a crossfade of two outputs. Each endpoint is reduced to the form every tier shares (high-passed direct path with the fine tier's low-passed share, unit-peak band-pass modes aₖ/Qₖ·BPₖ, final low-pass, trim) and the morph interpolates parameters: mode frequencies and Qs in log, residues linearly (a sign flip passes through zero), levels and trim in dB, corners in log. Modes pair by rank from each body's air mode A0 (the fine violin and viola have a weak C1 below it); a mode only one body has (C1, the longer statistical region, a default-tier body against a fine one) keeps its frequency and fades its residue. Any two bodies pair, across tiers; a …Stereo endpoint is its left microphone and a morphing body is mono. bodySize / size (0.1–50, live; absent 1) divides every body frequency — a body that many times larger, same material, same Qs: 0.25 a toy, 8–50 a room. The morph and size are smoothed (≈ 15 ms) and the bank's coefficients re-derived every 32 samples while they move; every section is a TPT state-variable filter, whose state stays meaningful (and stable) through any coefficient sequence. A mode pushed above 0.45·sr is pinned there with zero residue. At morph 0 and 1 the bank is the fixed body to < 1e-3 relative error on white noise, every tier (morph_endpoints_are_the_fixed_bodies).
Measured on a held bowed G3 and C3 (the library's violin hand), per-10 ms windows' largest per-sample step:
| max / p99 | worst window vs its ±40 ms neighbours | harmonic change per 50 ms (H1–H24, p95 / max) | |
|---|---|---|---|
| fixed bodies (both tiers) | 1.00–1.01 | 1.01–1.04 | 0.0–0.1 / 0.1–0.4 dB |
| violin → cello over 2 s | 1.00 | 1.04–1.05 | 1.1 / 5.9–7.3 dB |
| violinFine → celloFine over 2 s | 1.00–1.02 | 1.09–1.11 | 5.1–5.3 / 13.8–17.0 dB |
| violin → celloFine over 2 s | 1.00–1.01 | 1.10–1.11 | 3.7–3.9 / 9.6–15.9 dB |
| any pair over 50 ms | 1.00–1.10 | 1.14–1.33 | 0.1–0.2 / 8.0–12.1 dB |
| a 2 Hz sine morph | 1.00–1.03 | 1.10–1.54 | 6.7–11.0 / 9.7–21.0 dB |
bodySize 0.25 → 8 over 3 s (violinFine) | 1.08 | 1.52 | 10.3 / 19.2 dB |
No click: the largest step is the p99 step (a click is max ≫ p99), and no window stands out from its neighbours by more than the morph's own level change. The harmonic changes are the morph — a high-Q fine mode sliding across a harmonic lifts it by its peak — and scale with the morph's speed. Measured offline, not listened to.
The giant scale. scale = "giant" lets the fundamental fall to 4 Hz (normal: 20 Hz) — a string 20–30 m long at a violin string's wave speed (f = c/2L, c ≈ 290 m/s: 20 m is 7.3 Hz). The model is in normalised wave variables, so a longer string is a longer loop, and what scales with length is already pitch-relative: the loss pole follows √f0 (a partial's decay rate in Hz is held, so a 1 kHz partial dies as fast on 20 m as on 33 cm), the torsional Q falls with √f0 to its floor, the pitch servo averages over whole periods. The scale re-makes the delay lines once, at the voice's first note (the state is created there anyway; a reset keeps them and returns a reused state to the normal scale), drops the output DC blocker from 6 to 1 Hz, and lets the servo's per-step error ceiling grow with the period (0.5 % of it: the normal 2 samples left a heavy bow's −14 ct on a 7 Hz string for minutes; now in tune to 3 ct after 4.9 s at 7 Hz, 1.7 s at 14 Hz). Memory per voice: lines 217 KiB normal, 1085 KiB giant at 48/96 kHz (997 KiB at 44.1 kHz). Cost per sample is the normal string's.
The playable region narrows on a giant string. Six-second takes, the bow ramped over twelve periods, a 5 × 4 × 5 grid (pressure 0.3–0.9 × contact 0–0.45 × speed 0.03–0.5), cells in Helmholtz motion (one slip per period, the fundamental strongest): 7 Hz 11/100, 14 Hz 23/100, against 27/100 at 55 Hz and 26/100 at 110 Hz on the same grid; below the region the string double- and triple-slips (an octave or twelfth up), above it a slow bow sticks. One cell is Helmholtz at 5, 7, 10, 14 and 20 Hz: pressure 0.75, speed 0.12, contact 0.1 (slips per period 0.96–1.00) — giantString's hand. In the model's units the forces are ordinary; a real 20 m string bowed a few centimetres from its bridge (β ≈ 0.002, where position 0 here is β 0.027) would need Schelleng's F_min ∝ v/β², about 180 times the minimum at the model's closest contact. Spectrum at that hand (7 Hz): harmonics fall 1/n (H2 −6, H4 −12, H8 −18, H16 −26, H32 −32, H64 −42 dB re H1), 41 of the 571 harmonics below 4 kHz within 40 dB of the strongest and 433 within 60 dB, harmonic centroid 382 Hz (10 Hz: 586 Hz). Measured offline, not listened to.
Pickups. pickup = "piezo" is the bridge force — the wave the bridge sees, the signal every body is driven with — through the bridge's own rocking resonance on a solid body (3 kHz, Q 1.5, +4 dB), the mute still on it; pickup = "magnetic" is the string's transverse velocity at pickupPos (fraction of the string from the bridge; live; absent 0.08): the sum of the travelling waves there, read from the bridge segment's line (going out D/2 − x old, coming back D/2 + x old through the bridge's loss) or, past the bow, the neck's (y and 2·Ln − y old, the nut's reflection), at the same instant, so the two sides meet at the bow; the bridge's loss filter places the effective bridge comp/2 beyond the line's end. The comb of the pickup's place is the physics — harmonic n carries |2 sin(nπ·pos)| against the bridge wave (mean deviation from it 0.2–1.0 dB at pos 0.03 and 0.08; at 0.2 the 5th, 10th and 15th harmonics sit 46–61 dB down). With a light finger (touch) the pickup stays on the bridge side. Both bypass body; the coupled bridge path is the same.
| held note, library hand | level (dBFS) | H1 re bare | harmonic centroid |
|---|---|---|---|
violin A4 bare / violinFine | −3.5 / −4.1 | 0 / −0.7 | 3303 / 1753 Hz |
| violin A4 piezo | −2.9 | +0.1 | 3264 Hz |
| violin A4 magnetic 0.03 / 0.08 / 0.2 | −10.6 / −4.5 / −0.8 | −14.5 / −6.2 / +1.3 | 5075 / 3553 / 3374 Hz |
cello C2 bare / celloFine | −3.7 / −5.0 | 0 / −3.5 | 2104 / 695 Hz |
| cello C2 piezo | −3.7 | 0.0 | 2158 Hz |
| cello C2 magnetic 0.03 / 0.08 / 0.2 | −9.1 / −4.2 / −0.9 | −14.6 / −6.2 / +1.3 | 2588 / 2005 / 2151 Hz |
The electric voices keep the string's whole spectrum (the bodies cut the centroid to a half or a third); the piezo is the bare string with its bridge lift, a pickup at the bridge is thin and bright (−14.5 dB of fundamental at 0.03), and one past the bow is full and hollow. Measured offline, not listened to.
Cost (48 kHz, best of eleven 0.5 s runs, ns per output sample): bare bow 271; violin 292, violinFine 368; violin → cello held 298, moving 308; violinFine → celloFine held 354, moving 402; piezo 273; magnetic 326; giant 7 Hz 271–278. stringBody: violinFine 86, its morph held 68 and moving 112; a default-tier morph moving 23. The estimator prices the morph at its larger body plus 5 units, a magnetic pickup 8, the piezo 0.5 (in place of the body), the giant scale 0.5 (Understand audio processing cost).
The impossiblestrings library. morphingFiddle (violin → cello over each 8 s stroke; cellistFiddle the other way; fiddleMorph turn for any 0..1 signal), giantString (sounds three octaves below the written note: a written A1 is 6.9 Hz, a 21 m string, on a cello body ×8), electricViolin and electricCello (piezo, drive, chorus) with electricViolinFx / electricCelloFx (ping-pong delay and room), magneticViolin (a magnetic pickup at a quarter of the string), breathingString (an LFO stretches the string ±40 cents and swells its body), glassString (stiff, bright, low-grip, on a violin body ×0.25), roomCello (a cello on a body ×30). Inventions, measured offline, not listened to.
1.13 Bowed objects — bowedModal, bowedModalEvo and stickSlip#
The bowed string's stick–slip junction on things that are not strings (Bowed Strings §10). examples/songs/bowedobjects.volta plays them; examples/songs/sawglass.volta is the demo.
saw = patch { source = bowedModal (freq pitch) 0.6 [[1, 4s, 1], [2.08, 2.5s, 0.12]] { speed = 0.2, blade = 0.66 } }
glass = patch { source = bowedModal (freq pitch) 0.6 [[1, 6s, 1], [2.83, 2.2s, 0.2]] { speed = 0.18, friction = "finger" } }
hum = patch { source = feedback \back -> 5 * (back |> stickSlip 0.6 { speed = 0.2 } |> bpf (freq pitch) 200) }
bowedModal freq pressure modes [speed] [rosin] [friction] [blade]#
A bow on a mode table (the same [[ratio, decay, amp], …] rows modal takes, 1–16 of them, literal; tables from the modal library work as they are). Each mode is a resonator the friction force drives; amp is the mode's mobility at the bow (how strongly the bow drives it, and how loud it is). The object self-oscillates while pressure is held, usually on its first mode; sticking lengthens each period a little, so a bar sounds a few cents flat of its struck pitch, as bowed bars do.
pressure,speed,rosin— thebowedlaws (normal force1.4·p^3.5,speed = 1is 0.3, absent derives speed from pressure); read every sample. Too light a bow for its speed does not speak; a heavy bow flattens. Land the force first, then draw the bow: a pressure ramp under a moving bow can lock a high bar into a flat state.freq— followed every sample (coefficients refresh past 1e-4), so glides and vibrato bend the object.friction—"rosin"(default) or"finger": a clean wet fingertip (gentler velocity weakening, a viscous water film, a compliant skin) — the glass harmonica.blade(metres, 0.2–2, latched) — a musical saw: a bent steel blade whose S-bend traps the first mode. The pitch sets the bend (two octaves up from about 262 Hz for a 0.66 m saw, ∝ 1/length²); a tighter bend rings longer, traps the table's higher modes and leaves less edge noise, and the player's ear holds the bend in tune.
Level and the peak bound: sticking, the contact moves with the bow, so each mode rings near the bow's speed, and N modes can meet in phase at a slip. One [1, 4s, 1] mode peaks ≈ 0.98 at full pressure and speed; eight unit harmonics ≈ 8.0; a 5 % octave mode with a long decay, driven by the stick–slip force's second harmonic, took one bank from ≈ 1.0 to 1.8 (measured offline, not listened to). The compile-time bound (refine.rs::bowed_modal_bound) is 1.25 · N · 3 · (v + 0.08·p^3.5) — N the modes of nonzero amplitude, v the fastest bow the knobs allow (0.3·|speed|, or 0.03 + 0.2·p with no speed), p the top of the pressure range — calibrated over 56 000 random banks at 44.1/48/96 kHz (worst 1.10 of the unscaled form). It is near-tight for an equal-amplitude harmonic bank and several times loose for a bank with one dominant mode played gently; engine/tests/peak_calibration.rs re-checks it both ways.
Measured: the saw and glass within a cent, single slip per period; the vibraphone table 2–8 ct flat; a bronze plate often sings an upper mode (the bowed cymbal's screech). Saw H2 −33…−40 dB (nearly a sine), 0.4–1.2 s to speak. ≈ 90–120 ns per voice at 48 kHz.
bowedModalEvo — the evolved twin#
bowedModalEvo takes exactly bowedModal's arguments (the same lowering, BowedModal { …, evo: true } in the IR), so swapping the one word A/Bs the two in a song:
glass = patch { source = bowedModal (freq pitch) 0.6 [[1, 6s, 1], [2.83, 2.2s, 0.2]] { speed = 0.18, friction = "finger" } }
glass2 = patch { source = bowedModalEvo (freq pitch) 0.6 [[1, 6s, 1], [2.83, 2.2s, 0.2]] { speed = 0.18, friction = "finger" } }
The junction, friction laws, saw bend and servo are the same; the engine's bank (engine/src/bowed_objects_evo.rs, evolve round 7) differs: each mode's coefficients come from its exact analog poles (the shipped bilinear bank rings high modes long — about +15 % T60 at 13.8 kHz — and this one rings them at the table's T60), the bank runs at 2× or more at every host rate behind a half-band decimator, and the per-sample work is restructured (a block-local loop state, one multiply-add on the junction's feedback path). Against bowedModal over 192 closed-loop cells (6 objects × 2 notes × 4 pressures × 4 speeds) every regime agrees and sustained cells sit within 0.04 ct and 0.2 dB. In the engine the node alone costs 27–35 ns per voice-sample against the shipped 57–64 (1.8–2.1×); a whole library patch (envelopes and gain included) 38–100 ns against 66–127 (1.3–1.8×); whole songs that bow one render 1–25 % faster (measured offline, not listened to; engine/src/bowed_objects_evo_lab.rs, engine/tests/bowed_objects.rs lab_evo_engine_ab).
sig |> stickSlip pressure [speed] [rosin] [friction] [port] — bow anything#
Inside a feedback loop the junction bows whatever the loop body is (the region steps it per sample, so the loop is exact; outside a loop it is the same junction per sample).
port = "mobility"(default): the binder is the body's velocity at the bow andstickSlipreturns the friction force, so the body turns force into velocity. A resonant bandpass is one mode (give it gain:5 * … |> bpf (freq pitch) 200), parallel bandpasses a bank, acomb(with its DC removed) a harmonic body. The body must not answer DC (a held object) and must answer its resonance well above 1.port = "wave": the binder is the velocity wave arriving at the bow and the node returns the leaving wave, so the body is a reflection:(0 - 0.97) * (… |> delay (1 / freq pitch) 0)is a string bowed at its end.
The force never exceeds μs·N, so a passive body stays bounded and the loop limiter bounds the rest; the loop-gain advisory does not judge mobility loops. Measured: a bandpass loop single-slips within about a cent of its filter; a comb loop chatters (many slips per period, a growl); an end-bowed wave string slips irregularly and favours upper harmonics. The loop's own delay is the author's to compensate, as in any feedbackAt loop. +18 ns per sample over the loop it sits in.
1.14 Evolved twins — bowedEvo and stringBodyEvo#
Two drop-in nodes beside bowed and stringBody, with the SAME arguments, so a song A/Bs them by one word (evolve round 7). They are the same instruments — the same physics, junction, tables and parameter meanings — computed by the kernels an evolutionary search found; bowed and stringBody are unchanged.
-- shipped, then evolved: one word apart
cello = bowed (freq pitch) 0.6 0.25 { body = "cello" }
celloEvo = bowedEvo (freq pitch) 0.6 0.25 { body = "cello" }
viola = patch { source = string (freq pitch) 0.3 0.7 0.2 |> stringBody "viola" 0.45 }
violaEvo = patch { source = string (freq pitch) 0.3 0.7 0.2 |> stringBodyEvo "viola" 0.45 }
bowedEvo (engine/src/bowed_evo.rs, volta_bowed program #97): the waveguide string with power-of-two delay rings, the torsion line's loss once per output sample (a cubic step from a fixed exact anchor between), the decimator as a doubled history and an 8-lane dot product (Kaiser β 8, not 7), and the pitch servo's correlation on every third sub-step. Its harness scored it against the same model at twice the internal rate in f64: 108.3 (held-out 103.6) vs the shipped string's 90.5 (93.0), quality 91.0 vs 90.1. body = … radiates through the evolved body below. It covers the core path — speed, brightness, rosin, noise, stiffness and every body; any other option (touch, touchAmount, fingerNoise, mute, strings, string, the finite bow, bounce, attackPlan, understrings, buzz, bodyTo/bodyMorph/bodySize, scale = "giant", pickup, sympathy / group) renders the shipped bowed exactly (graph::bowed_evo_core, at build time), as does a section's lane group (only shipped bows join one).
stringBodyEvo (engine/src/string_body/evo.rs, volta_body program #111): every body (default, …Fine, …Stereo, folk) read from the same tables through BodyDesc, closer to the analog modal sum they describe: each mode keeps its analog skirt toward Nyquist (a bandwidth-corrected section driven by x[n] − x[n−2]), the final low-pass is the analog Butterworth's exact poles with zeros fitted to its DC gain, delay and curvature (no bilinear zero at Nyquist, so the top octave is no longer darker than the analog body), and every tier runs one 8-lane bank, block-major. The pair's placement law is the shipped one. Harness: 64.6 (held-out 62.3) vs 36.0 (38.4). mute and the morph (to, morph, size) render the shipped stringBody exactly.
The bow is chaotic, so the two strings agree by descriptors, not sample for sample — that is what the ear is for. Measured in the engine (48 kHz unless noted; offline, not listened to):
| shipped | evolved | |
|---|---|---|
bowed, lab grid (400 cells: C2/G3/A4/E6 × the lab hands) | 113 Helmholtz cells | 3 cells change regime, 1 pitch outlier > 3 ct, 1 level outlier > 1 dB; worst in-tolerance pitch Δ 0.78 ct |
bowed, Schelleng paths (G3/A4/C2/A3, speed .2–.8) | louder every step | every point within 0.1 dB of the shipped string |
bowed, near Nyquist (0.44–0.49·sr, bright E6/E7/A5, noisy G3) | within +3 dB of the shipped string (or its 0.2 %-heavier bow) on 10 of 12 probes; 44.1 kHz E6 +5.0 dB (a stronger harmonic 16 at 21.1 kHz), 48 kHz bright E7 +5.9 dB (inter-harmonic floor at −46 dB re the tone, above 21 kHz) | |
stringBody, octave-band SNR vs the analog sum (17 bodies) | 35.1 / 33.2 / 39.4 dB | 47.2 / 43.7 / 44.6 dB (44.1 / 48 / 96 kHz); every 1/3-octave band inside the harness's fence |
| level in a song (one-word swap) | within 0.15 dB (bows), 0.06 dB (bodies) |
Engine cost, per sounding voice (dsp_research/src/bin/evo_strings_ab.rs speed; a shared, loaded machine, medians of 7 alternating pairs):
| case | shipped ns/voice-frame | evolved | speedup |
|---|---|---|---|
bare pad bowed (freq pitch) 0.31 0.31, 6 voices | 210 | 96 | 2.2× |
| played line (envelope, vibrato, speed, noise), 3 voices | 233 | 124 | 1.9× |
body = "cello", 4 voices | 207 | 100 | 2.1× |
body = "celloStereo", 4 voices | 259 | 114 | 2.3× |
stringBody "viola" 0.45 over plucks, 8 voices | 14 | 8 | 1.7× |
stringBody "celloFine", 4 voices | 72 | 25 | 2.9× |
stringBody "violinStereo" with moving width/angle, 4 voices | 97 | 53 | 1.8× |
Whole songs (evo_strings_ab songs: the arrangement export, stereo, the song as written against every bowed / stringBody call renamed; ns per output frame, medians of 3 alternating pairs; first 16 bars, or 400 bars where the strings enter late). The strings are a small share of most songs, so the gain is small; 0.96–1.00× is within the machine's noise:
| song | rendered | shipped ns/frame | evolved | speedup |
|---|---|---|---|---|
| conservatory-02-passacaglia (two bodied bows) | 26 s | 934 | 726 | 1.29× |
| chromatophore-06-mobius-drone (bow + body) | 65 s | 1554 | 1418 | 1.10× |
| bm-104-chrome-on-my-tongue (12-voice bare pads) | 815 s | 569 | 524 | 1.09× |
frontier-cinema-02 (stringBody) | 40 s | 724 | 670 | 1.08× |
frontier-cinema-01 (stringBody) | 39 s | 1247 | 1183 | 1.05× |
| slipring (bare bow) | 728 s | 448 | 428 | 1.05× |
aether-rites-05 (stringBody) | 40 s | 3288 | 3258 | 1.01× |
| tungsten (bare bow) | 668 s | 1013 | 1026 | 0.99× |
| chaconne (bare bow) | 924 s | 451 | 469 | 0.96× |
In the browser (wasm32, no FMA) the body's multiply-adds are plain, not fused (the engine's fmadd rule), so a wasm render differs from a native one in the last bits. Tests: bowed_evo::tests, string_body::evo::tests (the harness's probes, engine-side), engine/tests/evo_strings.rs (the swap and every fallback, bit for bit), rt_safety's evo_bow_cold_slots / evo_bodies, and compiler/tests/string_body.rs's IR twin check (the shipped nodes carry no evo key, so no golden moved).
1.15 Woodwinds — blownEvo#
blown's drop-in twin with the SAME arguments, so a song A/Bs them by one word; blown is unchanged. Where blown is a bare STK port, blownEvo is a physical model of the player, the bore and the radiation (engine/src/blown_evo.rs), and kind names eight instruments:
flute = patch { source = blownEvo (freq pitch + lfoSine 5.3Hz * 12Cents) ((0.5 + vel * 0.4) * adsr 60ms 150ms 0.9 200ms) "flute" 0.2 }
clarinet = patch { source = blownEvo (freq pitch) ((0.45 + vel * 0.4) * adsr 30ms 200ms 0.85 180ms) "clarinet" 0.08 }
sax = patch { source = blownEvo (freq pitch) ((0.5 + vel * 0.4) * adsr 25ms 200ms 0.85 150ms) "sax" 0.1, glide = 50ms, glideMode = "legato" }
-- the shipped pipe and the woodwind core: one word apart
reedA = blown (freq pitch) (0.7 * adsr 60ms 200ms 0.85 180ms) "reed" 0.08
reedB = blownEvo (freq pitch) (0.7 * adsr 60ms 200ms 0.85 180ms) "reed" 0.08
kind | instrument | model |
|---|---|---|
"flute" ("jet") | transverse flute, B3–D7 | air jet on an open pipe |
"recorder" | recorder | the jet with a fixed windway: purer, less air |
"panflute" | pan pipe | the jet on a stopped pipe (odd harmonics), breathy |
"shakuhachi" | shakuhachi / ney-like | a lossy bamboo bore, heavy breath |
"clarinet" ("reed") | clarinet, D3–C♯6 | single reed on a cylinder (odd harmonics below the cutoff) |
"sax" | saxophone | single reed on a conical bore (all harmonics) |
"oboe" | oboe | double reed on a narrow cone, low cutoff |
"bassoon" | bassoon | double reed on a cone with a ~450 Hz cutoff |
The jets are the jet-drive model (Verge, Fabre, de la Cuadra): the jet leaves the lips at U ∝ √pressure (faster for higher notes, as players blow), crosses the window in τ = W/(c_v U), responds to the window's flow averaged over its own transit, deflects by (h/U)·e^{αW}·v and splits on the labium as bHU(1 + tanh((η − y0)/b)); the drive is −ρ δ_d/S_m · dQ/dt, with vortex-shedding loss and the window's air mass at the mouth end. The lip distance W follows the note (the player's embouchure), so the written note speaks from C4 to C7 — blown's jet speaks C4 a twelfth high. A harder breath is brighter and a few cents sharp, as a flute's is. Breath turbulence perturbs the jet (the bore filters it) and radiates as air at the mouth.
The reeds are the Guillemain–Kergomard–Voinier reed: a damped oscillator (the lip) driven by the pressure difference, stepped explicitly, which makes the flow ζ(1+x)⁺·sgn(Δp)·√|Δp| an explicit quadratic. The clarinet's bore is a cylinder; the conical reeds use Dalmont's cylindrical saxophone (the reed at the junction of the bore and an open branch of 0.3–0.33 of the length), whose input impedance has a cone's exactly harmonic peaks. A soft breath is nearly sinusoidal and a hard one grows odd harmonics: the timbre follows the breath (Benade), not only the level.
Every bore is one round-trip delay line closed by a tone-hole-lattice lowpass (its cutoff rises with high notes, as cross fingerings do), a visco-thermal wall loss ∝ √f (mode n loses g^√n, which keeps a reed on its fundamental) and the end's reflection; the clarinet adds a register damping at 4·f0. Tuning subtracts the loop filters' phase delay at the note and a calibrated per-instrument table (cents on a half-octave grid).
Player. pressure is the breath: a voice speaks softly from about 0.3, zero is silence, so put the envelope on pressure. A fresh note is tongued — the reed held shut (the jet off) for 8 ms, then released over 2 ms — and a jet gets an attack push (gain ×3 decaying over 80 ms) so a soft low note speaks in ~0.1 s; a voice that keeps sounding (glideMode = "legato") slurs: the bore length glides at most a quarter sample per sample, without a new tongue. noise (0..1, default 0.1) is breath turbulence. A mid-register note at pressure 0.75 sits at blown's level; a soft bound above |2| guards only abuse (ordinary playing peaks below 2).
Measured offline (48 kHz unless noted; not listened to — the A/B is for the ear):
blown | blownEvo | |
|---|---|---|
| flute C4, pressure 0.3 / 0.6 / 1.0 | speaks the twelfth (C4 at 0.8: strongest partial 3×) | the written note; H2 −5 to −7 dB, H3 −5 to −8 dB |
| fundamental regime (notes across the range × breath 0.3/0.6/1 for jets, 0.4/0.7/1 for reeds × 2 seeds, noise 0.15) | every cell of every instrument (shakuhachi's lowest note at a soft breath is mostly air) | |
| tuning across the range at the reference breath | reed within 0.2 c, jet within 9 c (220–880 Hz) | every instrument within 3.2 c |
| clarinet spectrum | odd harmonics nearly flat to 10 kHz | odd/even 87 → 278 (soft → loud, G3), centroid 1.11 → 1.53 harmonics |
| saxophone / oboe / bassoon even − odd energy (H1–H6) | (only the reed) | −0.9 / −2.7 / +6.8 dB |
| breath noise between harmonics (flute, noise 0.02 → 0.6) | −85 → −56 dB re the fundamental | |
| soft tongued note to half level | clarinet 30 ms, sax 50 ms, flute C4 at 0.3 90 ms | |
level in a song (breathwork's phrases, one-word swap) | reed +3.2 dB, jet +0.9 dB | |
| host callback size (32/64/96/480 frames, vibrato, chords, legato) | moves the pitch reads | bit-identical |
Engine cost (estimator_cost_table, 8-voice chords at 48 kHz, a loaded machine): blown ~7.5 ns per voice-sample; blownEvo flute ~24.5, clarinet ~20, sax and oboe ~22. Priced 7 units (the jets) and 6 (the reeds) in compiler/src/cost.rs (blown is 1.5).
Tests: blown_evo::tests (speaks and is silent, tuning, the regime grids, the low flute, the clarinet's odd harmonics and its brightening, the cones' even harmonics, breath noise, abuse at 22.05–96 kHz, ordinary peaks, reset, slurs, soft tongued onsets), engine/tests/blown_evo.rs (the one-word swap, every kind from the language, callback-size invariance, kinds and the IR twin: the shipped node carries no evo key, so no golden moved), and rt_safety's blown_evo_pipes.
2. Semantics & lowering#
- IR nodes:
Modal { freq, modes: [[ratio, decay, amp]…], exciter },ModalFit { freq, sample, count, exciter, sustain?, stretch?, tilt? },ExciterFit { sample, len },Waveguide { freq, damping, brightness, position, exciter, stiffness?, sympathy?, stiffness_node?, sympathy_node?, junction? },Bowed { freq, pressure, position, speed?, brightness?, rosin?, body?, noise?, stiffness?, touch?, touch_amount?, sympathy?, sympathy_node?, junction? },Blown { freq, pressure, noise, kind },StringBody { input, body?, mix, mute?, width?, angle? }(stringBody; an empty/absentbodyis"none"; absent optional fields emit no key),Exciter { kind, param }(junction = { group?, weight?, bank? }, §1.10); plus theSympathetic { pitches, amount }andBodyTrack { freq, modes, amount }track effects. The lowerer always fills the exciter slot (the defaults above), so backends never special-case its absence. stiffness/sympathylower throughfold_string_knob: a constant folds to its value clamped to [0, 1],#[serde(default, skip_serializing_if)]so 0 emits no key and adds no node — every pre-existing golden IR is byte-identical; a live expression lowers to a control node named bystiffness_node/sympathy_node(the live path of §1.8).resonatecarries the same pair (same waveguide core).- Mode triples fold at compile time (
const_scalarratios/amps,fold_const_secondsdecays — unit suffixes canonicalize). Malformed triples, non-positive ratios/decays, an empty list, or > 64 modes are lowering errors. refine.rstreats all of them as nominally bipolar sources and visits their live inputs for range diagnostics.- Voice lifetime.
graph::max_releasefolds in each resonator's own ring time — the longest mode T60 forModal, the 5 s fit ceiling × sustain forModalFit(plus sampled drive duration when PCM arrives), the extracted length forExciterFit,physical::string_t60_at(damping, f0)× the §1.8 coupled factor forWaveguide/Resonate(a non-constant pitch budgets 110 Hz, the bottom of the playing range), a flat 1 s forBowed, 0.5 s forBlown— clamped at 30 s. Before this a 4 s bell with noampEnvwas edge-faded at the 0.1 s floor, an audible bug on exactly the patches physical modeling exists for. - Note-on is allocation-free.
reset_node_statere-arms modal / waveguide / bowed state in place (ModalState::reset,WaveguideState::reset,BowedState::reset): the per-mode and delay-line vecs are reused across retriggers rather than dropped toNoneand re-allocated on the audio thread at the next eval. Pinned byengine/tests/rt_safety.rsfixtures (modal_poly,string_pluck,bowed_sustain,string_body) and by unit tests asserting a reset state is bit-identical to a fresh one. A slot that has never rendered abowednode takes a state module preparation built on the control thread (engine/src/voice/bowed_pool.rs: the template that needs most, itsbowednodes times itsvoicescap, at most 8, uncapped 4, with the free rings of any instrument strings), so a song reaching a new slot mid-play — an envelope-lessbowedline whose 1 s tails overlap the next note — no longer builds about 0.5 MB on the audio thread (18 allocations on a violin-strung voice;rt_safety'sbowed_cold_slots). The same pool carries freshstring/resonatewaveguides,blownbores,stringBody/stringBodyEvobodies (built for the body each node names) and voice-levelglitchShiftbuffers, so their first trigger in a slot allocates nothing either (rt_safety's cold-path harness). So does every struck resonator:modal(a bank with room for 64 modes),modalOf,bowedModal/bowedModalEvo, the square-bank drum circuits (hat606,cymbal606,cowbell808) and eachmeshgrid, prepared per node times the synth's wholevoicescap (32 without one) and summed over synths. A slot retriggered with a patch that will not re-arm the struck state it holds hands it back to the pool, renewed to exactly a fresh one (so a slot changing patches frees nothing on the audio thread), and a cold node the pool has run out for takes the one an idle voice is keeping. Past all of that a cold slot still builds its own. - Known gap (pre-existing, all combinators): a binding of a transformed pattern (
p = !"…" |> degradeBy 0.1) isn't collected as a named pattern — inline the transform in theplaycall.
3. Backends#
- Native engine (reference) —
engine/src/physical.rs:- Modal:
y = 2R·cosθ·y₁ − R²·y₂ + g·x,R = exp(−ln1000/(decay·sr))(T60-exact),g = amp·(1−R²)/2 · sinθ/sinθ_ref(the lowest sounding mode supplies the reference); coefficients refresh once per block from the freq buffer (glide steps per block). - String: loop gain
exp(−ln1000·D/(sr·t60)), brightness one-pole (800 Hz–12 kHz), DC-blocked output; optional dispersion allpass (disp_*, §1.8) whose closed-form phase delay joins the loop-length compensation and the per-pass T60, and an optional bridge injection at the loop write. - Bridge bus:
physical::{BridgeBank, BridgeSlot}, a fixed 16-slot bank owned byVoiceArenaand threaded intoVoice::eval(coupling is across voices; a voice sees only its own state). Each slot is an 8192-sample ring indexed by ABSOLUTE sample position: a cell holds the raw tap sum while its block is open and the finished bus signal once the block is closed, and each voice reads it back a whole number of its own periods (≥ oneblock::BLOCK) and negates it — see §1.8. Slots are claimed by an FNV hash of (synth name, node index) — by NAME, so a hot-swap doesn't split a sustaining chord off its bridge — and nothing allocates on the audio thread. A slot no voice wrote to last block is wiped, andVoiceArena::cleardrops all of them (stop must be silent). - Bowed:
bowed.rs, a ~192 kHz internal waveguide (FIR decimator), two continuous cubic taps, √f0-scaled loss filter (reflection gain −0.98), the hysteretic stick–slip junctionbow_junctionwith a damped torsional waveguide pair, an optional finger damper splitting the neck, seeded rosin noise and a 4-stage neck-side stiffness cascade, a period-relative correlation servo over analytic loop compensation, and output-only body radiation (§1.12). Measurement tables are--ignoredtests inbowed.rsandbowed_lab.rs. - String bodies:
string_body.rs, shared bybowedand theStringBodynode (Voice::eval_string_body, boxedNodeState::StringBodycleared in place byreset_node_state). - Exciters: xorshift32 seeded via
rng::draw(seed, [synth, track, cycle, event, node, "exciter"]). modalOffit:physical::analyse_modal(control-side only), delivered as asample::ModalTablethroughCommand::InsertModal; idempotent per(sample, count).exciterOf:physical::analyse_exciter→sample::ExciterTable→Command::InsertExciter. Live and offline share the cached modal fit. Replaced tables retune sustaining banks in place; new notes wait for their own recording, never another recording left in a reused voice slot.bodyTrack:effect::ModalBody, two preallocated modal states, one per channel; amount ramps use the shared track-Param path.sympathetic:effect::Sympathetic, twoWaveguideStatebanks (L/R) preallocated at build;processnever allocates. Offline renders (bounceOf,export, arrangement export,render_track_mono) drive a rawAudioEnginewith no control thread and no command ring, so they run the bake themselves:AudioEngine::bake_analyses_offline(the offline twin of thebake_resynth/bake_modal/bake_partialstrio) fits the table straight into the render engine's own bank beforeload.modalOfandadditiveOfandexciterOftherefore sound the same offline as they do live — including inside abounceOfwhose source track is itself fitted, since bounce defs bake in dependency order and each bounce engine bakes its own analyses.
- Modal:
- Section players run as lanes —
bowed_lanes.rs,voice/lanes.rs,lane4.rs. A deluxe section's players (players> 1:…DeluxeTutti,tutti,…DeluxeSection) are separatebowednodes stepping in lockstep, and up to four of them run together, one lane per player: each player's control prologue (hand, finite bow, attack plan, tone) stays scalar, then the hot state of all of them (delays, loss and servo states, hand smoothers, friction noise, the rosin's age, the read heads) loads into four-wide vectors for the sample and every sub-step runs once for all — the fractional taps interpolated four at a time (each lane's four taps one load, transposed), the stiffness cascade, the torsional lines, the friction junction solved without branches (every outcome computed and the scalar rule's choice selected per lane), the servo's averages and the decimator (four taps of four histories per load). Each lane performs the scalar code's operations in its order, andexpandpowfstay libm calls per lane, so every player's audio is the scalar render's bit for bit (testsbowed::lanes::testsandengine/tests/bowed_lanes.rsat 44.1, 48 and 96 kHz, solo, tutti and the shared bridge). Per-player differences — detune, the followers' lateness, rosin seeds, the bow's stroke and plan — are each lane's own state. The lane type is NEON on aarch64, SSE2 on x86_64, simd128 in the wasm engine (both builds enable it), a plain array elsewhere; no dependency. Where they run: a voice template groups its uncoupled bows (no bow of a group reads another, and nothing reads one before the group's last member, where the group renders); the shared bridge departs its bowed seats four at a time. A bow the lanes do not model this sample — a touching finger, a folk addition, a magnetic pickup — departs alone, and fewer than three ready bows run scalar (two lanes measured no faster). Solo voices,players = 1and every other path are the scalar code. Measured at 48 kHz (offline): the lanes save ≈ 86 ns per player per sample on a quiet machine; with the block-wise sums and products of the players' control graph, aviolinDeluxeTuttinote costs 2,152 → 1,561 ns per sample native and 3,843 → 2,994 in the wasm engine, andviolinDeluxeSection6,728 → 5,220 native. - Browser — the same engine compiled to wasm (engine mode); rebuild with
./build-engine-wasm.shafter engine changes. There is no separate browser DSP — the oldphysical-processor.jsworklet was deleted with the Web Audio compat path (Engine Wasm).
4. Tests#
engine/src/physical.rsunit tests: exciter determinism + seed decorrelation, modal T60 accuracy, modal mode-frequency accuracy (zero crossings), string pitch within ±2 cents across stiffness × brightness × 110–880 Hz (worst case 0.14), damping-shortens-sustain, bowed sustain + pitch hold, the period-locked pluck (a 55 Hz note within a factor 2 of a 440 Hz one's level, was 0.41×), and the T60 pitch law (measured 0.79 / 0.60 / 0.32 s at 110 / 220 / 880 Hz, anchor within 10 % of the pre-pitch-tracking map).engine/tests/physical_models.rsend-to-end renders: bell rings & decays, pluck decays, bow sustains, scrape drives the bank, and byte-identical re-renders (determinism).analyse_modaltests: recovery of a synthesized three-mode body (ratios within 5 %, T60s within a 0.4–2.5× band, the fundamental strongest), relative decay ordering, and degenerate input (silence / empty / noise / too short).engine/tests/physical_models.rsalso covers the algebra:modalOfis silent with no bake and rings onceload_samplescompletes it;bodysings on a noise drive and lowers onto a plainModalnode;sympatheticrings at a matched pitch and stays >8× quieter off-pitch, and is bit-identical to no insert atamount 0; a 4 s bell is still ringing at 0.85 s (themax_releaseregression) and the family's ring budgets are pinned directly.- Compiler lowering tests (
lower/tests.rs): mode folding +800mscanonicalization, default/custom exciter fill, malformed-mode error, live-pressure bowed, named mode tables,modalOfcount defaults/limits and the bare-use error,body's reorder and itsmodalOfcomposition,sympathetic's pitch folding and its two rejection cases. - Stiffness / sympathy (§1.8):
string_stiffness_stretches_the_partials(monotone in the knob AND in the partial index; harmonic at 0; the ripple's turning point pinned around P14),stiffness_is_continuous_across_the_chromatic_scale(the P4 stretch moves ≤ 5 c between adjacent semitones over 110 Hz–1.76 kHz, swept over 200 knob settings plus a fine pass across the 0.876 peak, with a render cross-check of the analytic loop response),stiffness_stays_near_the_ideal_string_above_the_tested_range,zero_stiffness_is_bit_identical_to_the_ideal_string(to_bits()equality against a transcription of the pre-knob loop, five pitch/damping/brightness combinations),string_sympathy_rings_a_silent_neighbour,string_sympathy_gives_a_two_stage_decay(both normal modes fitted in dB/s),two_stage_decay_holds_at_every_pitch(220 / 330 / 375 Hz — the pitches a fixed block delay inverted),coupling_is_the_same_at_every_block_size(32 / 64 / 96 / 128 frames, bit-identical),ragged_callbacks_inject_no_steps(480-frame device buffers; a max-|Δx| probe on the bridge contribution),bridge_read_delay_is_a_whole_period_clear_of_the_block,string_sympathy_is_stable_at_full_tilt(8 voices, one pitch, sympathy 1, damping 0),bridge_bus_drops_its_state_when_nobody_plays. Compiler side:string_takes_optional_stiffness_and_sympathy,resonate_takes_the_same_two_knobs,omitted_string_knobs_are_absent_from_the_emitted_ir(no key, no extra node),string_knob_arity_error_names_the_shape,builtin_arity_matches_the_lowerer(the declaredTy::Arrowofstringandresonatemust be as long as the longest argument list the lowerer accepts, or the trailing args go untyped and signature help mis-renders them),a_constant_in_the_exciter_slot_is_flagged, and anir_serderound-trip including the legacy (no-key) wire shape. - Passive bridge (§1.10):
physical.rs—arrive_then_depart_is_tick_bit_for_bit(string and bow),junction_is_passive_for_random_states(20 000 random states, 1–64 members, 1–4 banks),passive_bridge_splits_the_decay_past_the_bus_ceiling,passive_bridge_rings_a_silent_neighbour,passive_bridge_is_stable_at_full_tilt(8 strings, one pitch, attachment 1, damping 0: windowed energy never rises, −33.9 dB over 2.8 s),sub_banks_couple_weakly_across_bridge_points_and_weight_drives_harder,coupled_bows_stay_bounded_across_a_parameter_sweep,a_bow_rings_plucked_strings_on_a_shared_bridge;engine/tests/passive_bridge.rsend to end — attachment 0 bit-identical to the plain string, callback-size invariance, the ordered track path, the unison split from the language, named bridges across patches and tracks (plucked and bowed leads), and the demo song's scenes; anrt_safetyfixture;lower/tests.rs—string_bridge_seats_the_string_on_a_passive_junction,bowed_sympathy_seats_the_bow_on_a_passive_junction. blown(§1.11):engine/src/blown.rs— speaks/sustains/silent without breath,blown_is_in_tune,the_reed_is_odd_harmonic,blown_stays_bounded_across_a_parameter_sweep, in-place reset;passive_bridge.rs— both bores from the language (pitch, determinism, silence) and thebreathwork.voltascenes; anrt_safetyfixture.blownEvo(§1.15):engine/src/blown_evo_tests.rsandengine/tests/blown_evo.rs(see §1.15); anrt_safetyfixture.- Hybrids:
compiler/tests/modal_hybrids.rs,engine/tests/modal_hybrids.rsand threert_safety.rsfixtures cover slot/arity/legacy wire behavior, exact and fitted recovery, deterministic block partition, native tuning, transform equivalence, long drives, stereo/null/reset, replacement and live/offline bakes including fits of a bounce. npm --prefix runtime run test:modal-hybridsexercises the built wasm compiler and engine: the demo has no warnings, late PCM loading completes both fits, and output is byte-identical at 32/64/96/128-frame callbacks. Build both wasm packages first.- Golden IR snapshots:
resonance.json,sympathy.json,modal.json,modaltour.json,bridgework.json,hybrids.json,aftersound.json,breathwork.json.
5. Deferred#
- A coupled
blown(a pipe on a passive bridge) and a reed/jetposition. - A passive junction for
resonate, and unison courses (several strings per played note — what gives a single piano note its aftersound). - Transposed
native(pin to a constant Hz as the current workaround). - Live mode amps (signal-driven bank morphing) and per-sample glide on the plucked
stringdelay. Bowed pitch and contact already follow every sample. - Thiran-cascade dispersion (calibrated directly in the inharmonicity coefficient
B, and monotone over the whole band rather than just the radiated partials — §1.8); see String Stiffness Coupling §7. Cross-patch and cross-track coupling ship as named passive bridges (§1.10).
6. Coupled body pairs#
drive |> couple freq a b connects two independent literal or fitted mode tables through a reciprocal internal junction. Live damping, tuning, spacing, excitation position and pickup controls shape sustaining notes. See Coupled bodies for the instrument, physical approximation and source-preserving controls in Notebook and Rack.