guitar — a physically-modeled strummed six-string #
A single-file C engine (like pop/accordion/c/ and pop/bell/c/) whose
sound comes from six waveguide strings on one bridge, played by one hand.
The string is an extended Karplus-Strong loop; the hand is a strum pattern
with down- and up-strokes that rake the strings at different speeds, catch
different strings, and damp each string for a moment before letting it go.
The cult track's guitar() had the string and not the hand; this is the
hand.
Zero dependencies beyond libm.
pop/guitar/
c/strum.c the engine
c/build.sh cc -O2 -std=c11 -Wall -Wextra -o strum strum.c -lm
out/ demo bank (gitignored, like every pop out/)
The model #
String — a fractional-delay Karplus-Strong loop per string:
y = allpass(line[idx]) first-order allpass → exact pitch
lp = b·y + (1-b)·lp the string forgets its highs
line[idx] = g·lp + c·(bridge - lp) + exc
gis a per-sample loss from a T60 per string (5.5 s on the low E down to 2.8 s on the high E, ×1.6 for electric), shortened a little per fret, and blended toward a 0.14 s muted T60 bymute— which is what a palm, a lifting fret hand, and a chuck all are.bis the loop brightness: wound strings 0.80–0.86, plain 0.90–0.95, every fret shaving 0.6 %.excis a one-period noise burst comb-filtered at the pick position (--pick, default 18 % of the length — a pluck there cannot feed the harmonics with a node there) and lowpassed by force (900 Hz at a feather, ~8 kHz at full), with a 1.5 ms click for the pick leaving the string. It is added into whatever the string was already doing.- Pick contact: 3 ms before each pluck the loop loss is multiplied by 0.55 — the pick is resting on the string. A re-strum catches the old note; it does not stack a fresh burst on top of a ringing one.
Bridge — every string feeds c·(mean − own) back into its loop through
a 2.5 kHz lowpass (c = 0.03). The mixing matrix is g·I + c·(J/n − I):
its common mode keeps gain g, every differential mode gets g − c, so the
coupling is passive by construction and energy leaks from a plucked string
into the others. A strum rings as one body of six, and a single plucked
string makes the others whisper.
Body (acoustic) — a dreadnought's three modal peaks plus presence over a 62 % dry path: Helmholtz air 101 Hz (Q 9), top plate 203 Hz (Q 7) and 398 Hz (Q 5.5), a broad 2.3 kHz bump. Left and right carry the modes skewed ±2.5 % so the box has a width; strings are panned low→left, high→right. DC blocker, 9 kHz rolloff.
Amp (electric) — the strings sum at the pickup then the amp drives
them: the cult engine's two-stage asymmetric tanh (tanh(v·k + bias) − tanh(bias), then tanh(·1.8)/2) and its dark one-pole (0.16 @ 48 k),
into a 2nd-order 4.2 kHz cab with a 2.4 kHz presence peak. Driving the chord
rather than each string is the point: the grind between the notes is the
amp intermodulating, and six separately-driven strings never produce it.
Hand — a pattern is one bar, one character per step (16 chars = 16ths, 8 = 8ths, 12 = triplet 8ths), repeating:
| char | stroke |
|---|---|
D / d |
down, full / soft — rakes low→high across every sounding string |
U / u |
up, full / soft — rakes high→low across the top --up strings only, 0.6× the rake time |
x |
chuck — every string muted, then scraped near the bridge |
. |
rest |
Rake time is --rake at full force and shortens with force (a hard strum
is a fast strum); the wrist accelerates through the strings (t ∝ k^0.85);
later strings in a rake are hit slightly lighter; the top string of an
up-stroke slightly harder. --human puts a seeded ±4 ms / ±8 % / ±15 %
wobble on timing, force and rake.
Fret hand — --chord "Bm|D|G|Em" cycles one chord per bar. 30 ms
before the bar line the strings whose fret changes are choked; on the bar
line they retune (the ringing delay line is simply read at the new length,
which is what a hammer-on is); and if a wound string moved two or more
frets, a 70 ms band of noise around 2.2–3.1 kHz squeaks.
The default pattern is the cult track's STRUM table written in 16ths:
D..d..u.u.D..u.. (beats 0, 0.75, 1.5, 2.0, 2.5, 3.25).
CLI #
./build.sh
./strum --chord Bm --out bm.wav
./strum --chord "Bm|D|G|Em" --bars 4 --electric --out cycle.wav
./strum --chord 47,54,59,62 --pattern "D.D.uu.D..u." --electric --out cult.wav
./strum --chord Bm --mute palm --pattern "DdDdDdDdDdDdDdDd" --electric --out chug.wav
./strum --chord "Bm9|Gmaj7|Em9|F#7sus4" --bars 4 --pattern "..D...u.d.U..u.." --rake 24 --force 0.55 --out flower.wav
| flag | meaning | default |
|---|---|---|
--chord C |
a name (Bm, F#m7, Cadd9, Dsus4 …) or a midi list (47,54,59,62); |-separated list cycles one per bar |
Bm |
--pattern P |
one bar of D d U u x . |
D..d..u.u.D..u.. |
--bpm B |
tempo | 120 |
--bars N |
bars to play | 1 |
--electric / --acoustic |
amp chain vs body | acoustic |
--drive 0..1 |
amp drive (electric) | 0.7 |
--rake MS |
full-force down-stroke, low to high | 16 |
--up N |
strings an up-stroke catches | 4 |
--force 0..1 |
hand force | 0.8 |
--pick 0..1 |
pick position along the string | 0.18 |
--mute palm|open|0..1 |
fret-hand damping | open |
--damp 0..1 |
shorter string sustain | 0.15 |
--human 0..1 |
seeded timing/force/rake wobble | 0.5 |
--tail S |
ring past the last bar (the hand chokes the strings 220 ms before the end) | 1.0 |
--sr HZ |
sample rate | 48000 |
--seed N |
RNG seed | 1 |
--out FILE |
float32 stereo WAV, peak-normalized to 0.9 | required |
Chord names: 31 open-position shapes are tabled (E Em E7 Em7 A Am A7 Am7
Asus2 Asus4 D Dm D7 Dmaj7 Dsus2 Dsus4 G G7 C Cmaj7 Cadd9 C7 F Fmaj7 Bm B B7
Bm7 F#m F# F#m7); anything else becomes a barre — E-shape for roots E…B,
A-shape for C…D# — with suffixes m 7 m7 maj7 sus2 sus4 7sus4 add9 9 m9 5.
A midi list is placed top note first, each on the highest free string that
reaches it (the lowest fret), so 47,54,59,62 is x A2+2 D3+4 G3+4 B3+3 x
and the up-stroke's "top strings" are the right ones. The voicing is
printed on stderr.
Deterministic: one seeded xorshift, drawn in a fixed order. Same flags, same bytes. A 16-bar electric render takes ~0.3 s.
Demos (out/) #
for ch in Bm D G Em; do
./strum --chord $ch --out ../out/$(echo $ch | tr A-Z a-z)-acoustic.wav
./strum --chord $ch --electric --out ../out/$(echo $ch | tr A-Z a-z)-electric.wav
done # 1 bar @ 120 = 3.00 s each
./strum --chord "Bm|D|G|Em" --bars 4 --out ../out/cycle-acoustic.wav
./strum --chord "Bm|D|G|Em" --bars 4 --electric --out ../out/cycle-electric.wav
./strum --chord 47,54,59,62 --pattern "D.D.uu.D..u." --electric --out ../out/cult-voicing-electric.wav
./strum --chord Bm --mute palm --pattern "DdDdDdDdDdDdDdDd" --electric --out ../out/bm-palm-electric.wav
./strum --chord "Bm9|Gmaj7|Em9|F#7sus4" --bars 4 --pattern "..D...u.d.U..u.." --rake 24 --force 0.55 --out ../out/flower-acoustic.wav
Notes / honest limits #
- The string is a lossy loop with a one-pole, not a stiff-string model: no inharmonicity, so the wound strings lack the slight "piano" stretch of real bronze, and there is no longitudinal / two-polarization coupling — the slow beating of a real open string is absent.
- The body is a fixed modal EQ, not a measured impulse response; swap the
body_inittable to chase a specific box. - The pick is a noise burst: it carries position and force but not the pick's own stiffness, so a felt pick and a steel one are the same pick.
- The fret hand changes chords only on bar lines, and the squeak is a band of noise with no relation to which fret it slid from.
- Output is peak-normalized like the accordion, which means a soft render and a hard one land at the same peak — the dynamics live between the strokes, not between files. Score the level at the call site.
note/WAV conventions followpop/bell/c/bell.c.