Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(percussion): stochastic per-hit variation + BPM-locked flam timing

User: "every perc sound needs stochastics... claps are not always the
same etc... like the clap and bass should have subtle variation in
timbre across multiple pressings" + "can the repeat be based on
timing / bpm".

playPercussion() now randomizes tone, volume, and (where it matters)
duration on every hit so repeated presses don't sound machine-stamped.
Two inline helpers:

rj(center, frac) — jitter center by ±frac (e.g. rj(180, 0.05) =
180 ± 5% = 171 to 189)
rn(min, max) — uniform random in range

Per-drum variation spread (most subtle → most dramatic):

cowbell tone ±4% vol ±8% (mechanical sound, tight)
ride tone ±6% vol ±10%
crash tone ±6-7% vol ±8-10%
snare tone ±4-8% vol ±8-10%
hat-closed tone ±8% vol ±10%
hat-open tone ±7% vol ±8-10% duration ±10%
snap tone ±10% vol ±10%
clap tone ±8-12% vol ±8-12% + BPM-locked flam + timing ±30%
kick tone ±4-12% vol ±5-10% + sub beat freq ±4Hz
tambourine tone ±8-10% vol ±10-12% (the point of the instrument)

The kick's TWO-SINE sub wobble now also jitters its beat frequency
per hit — f1 is chosen around 44 Hz ±5%, f2 is then f1 + 8-12 Hz, so
the beat frequency lands somewhere between 8 and 12 Hz on each press
instead of being locked at exactly 10 Hz. Feels more organic.

The clap's three body bursts plus the tail now derive their timing
from a `flam` unit computed from metronomeBPM:

flam = (60000 / bpm) * 0.025 → 12.5 ms at 120 bpm,
25 ms at 60 bpm,
8.3 ms at 180 bpm

Each burst fires at `flam * rn(lo, hi)` so a faster tempo makes the
clap tighter and a slower tempo looser. This is the answer to "can
the repeat be based on timing / bpm" — drum rolls and flam bursts now
lock to the current metronome tempo.

The rj/rn helpers are inline closures inside playPercussion, so there's
no per-call allocation overhead beyond one Math.random() per jitter,
and no cross-hit state to manage.

+93 -73
+93 -73
fedac/native/pieces/notepat.mjs
··· 440 440 441 441 // Fire a drum hit from short auto-stopping synth voices. No cleanup 442 442 // needed on key-up because every voice uses a finite duration. 443 + // 444 + // Every drum gets STOCHASTIC variation on each press so repeated hits 445 + // don't sound machine-stamped: 446 + // - tone jitter: ±3-8% of the nominal frequency 447 + // - volume jitter: ±5-10% of the nominal volume 448 + // - duration jitter: ±5% on longer tails 449 + // - for multi-burst drums (clap), the flam timing also jitters 450 + // 451 + // Multi-burst flam timing scales with metronomeBPM so drum rolls lock 452 + // to the current tempo: at 120 BPM each sub-beat is ~12.5 ms. 443 453 function playPercussion(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0) { 444 454 if (!sound?.synth) return; 445 - // Cap at 2.2 so drums can push voice volumes above 1.0. The native audio 446 - // mixer auto-divides by total voice weight (audio.c mix_divisor), which 447 - // means a drum hit with vol=0.9 sharing the mix with three 0.7 sustained 448 - // melody voices gets crushed to ~0.3 — basically invisible. Pushing drum 449 - // voice volumes to ~1.6-2.0 puts them at ~2x the ratio of the melodies 450 - // even under heavy polyphony, so they cut through like real drums. 451 455 const v = Math.max(0.1, Math.min(2.2, volume)); 452 - const pf = Math.max(0.25, Math.min(4, pitchFactor)); // clamp to ±2 octaves 456 + const pf = Math.max(0.25, Math.min(4, pitchFactor)); 457 + 458 + // Per-hit random helpers (inline, stateless). `rj` jitters around a center 459 + // by a ± fraction; `rn` returns a uniform range. 460 + const rj = (center, frac) => center * (1 + (Math.random() - 0.5) * 2 * frac); 461 + const rn = (min, max) => min + Math.random() * (max - min); 462 + 463 + // BPM-locked flam unit for multi-burst drums. metronomeBPM defaults to 120 464 + // so this is ~12.5 ms at default. Scales inversely: 60 bpm → 25 ms, 465 + // 180 bpm → ~8 ms. Used for clap / future multi-burst drums. 466 + const flam = (60000 / Math.max(40, Math.min(240, metronomeBPM || 120))) * 0.025; 467 + 453 468 switch (letter) { 454 - case "c": // kick — wub/wobble bass: shorter, more mid grit, 10 Hz sub wobble 455 - // 1. Beater click — short high transient so the ear still parses this as a drum hit 456 - sound.synth({ type: "triangle", tone: 1800 * pf, duration: 0.005, volume: 1.0 * v, attack: 0.0003, decay: 0.004, pan }); 457 - sound.synth({ type: "noise", tone: 4000 * pf, duration: 0.004, volume: 0.55 * v, attack: 0.0003, decay: 0.003, pan }); 458 - // 2. Body thump — SQUARE wave at 180 Hz for rich mid harmonics (was a pure sine 459 - // which left a hollow-feeling gap between 180 Hz and the sub). Square = odd 460 - // harmonics up to ~900 Hz, filling the mid band. 461 - sound.synth({ type: "square", tone: 180 * pf, duration: 0.02, volume: 1.2 * v, attack: 0.0005, decay: 0.018, pan }); 462 - // 3. Grit layer — sawtooth at 90 Hz for dubstep-style edge. Adds 90/180/270/360 Hz 463 - // harmonics and the rasp that reads as "wub bass" to the ear. 464 - sound.synth({ type: "sawtooth", tone: 90 * pf, duration: 0.1, volume: 1.0 * v, attack: 0.001, decay: 0.095, pan }); 465 - // 4. Sub wobble — TWO sines tuned 10 Hz apart beat against each other, creating 466 - // natural amplitude modulation at |f1-f2| = 10 Hz without needing a real LFO. 467 - // Shortened from 500ms → 180ms so the kick doesn't overstay its welcome. 468 - sound.synth({ type: "sine", tone: 44 * pf, duration: 0.18, volume: 1.9 * v, attack: 0.001, decay: 0.17, pan }); 469 - sound.synth({ type: "sine", tone: 54 * pf, duration: 0.18, volume: 1.3 * v, attack: 0.001, decay: 0.17, pan }); 470 - // 5. Mid-low square at 120 Hz — fills the 120-360 Hz range with additional square 471 - // harmonics so the kick has body between the 44 Hz sub and the 180 Hz thump. 472 - sound.synth({ type: "square", tone: 120 * pf, duration: 0.07, volume: 0.85 * v, attack: 0.002, decay: 0.065, pan }); 469 + case "c": // kick — wub/wobble bass with per-hit timbre variation 470 + // 1. Beater click — short high transient 471 + sound.synth({ type: "triangle", tone: rj(1800, 0.08) * pf, duration: 0.005, volume: rj(1.0, 0.08) * v, attack: 0.0003, decay: 0.004, pan }); 472 + sound.synth({ type: "noise", tone: rj(4000, 0.12) * pf, duration: 0.004, volume: rj(0.55, 0.10) * v, attack: 0.0003, decay: 0.003, pan }); 473 + // 2. Body thump — SQUARE for mid harmonics 474 + sound.synth({ type: "square", tone: rj(180, 0.05) * pf, duration: rj(0.02, 0.10), volume: rj(1.2, 0.06) * v, attack: 0.0005, decay: 0.018, pan }); 475 + // 3. Grit layer — sawtooth for dubstep rasp 476 + sound.synth({ type: "sawtooth", tone: rj(90, 0.04) * pf, duration: rj(0.1, 0.08), volume: rj(1.0, 0.06) * v, attack: 0.001, decay: 0.095, pan }); 477 + // 4. Sub wobble — two sines ~10 Hz apart, each with per-hit pitch jitter 478 + // so the beat frequency varies slightly (sometimes 8 Hz, sometimes 12 Hz) 479 + { 480 + const subLo = rj(44, 0.05); // ~41.8 → 46.2 Hz 481 + const subHi = subLo + rn(8, 12); // ~50 → 58 Hz → beat freq 8-12 Hz 482 + sound.synth({ type: "sine", tone: subLo * pf, duration: rj(0.18, 0.05), volume: rj(1.9, 0.05) * v, attack: 0.001, decay: 0.17, pan }); 483 + sound.synth({ type: "sine", tone: subHi * pf, duration: rj(0.18, 0.05), volume: rj(1.3, 0.06) * v, attack: 0.001, decay: 0.17, pan }); 484 + } 485 + // 5. Mid-low square fill 486 + sound.synth({ type: "square", tone: rj(120, 0.05) * pf, duration: rj(0.07, 0.08), volume: rj(0.85, 0.07) * v, attack: 0.002, decay: 0.065, pan }); 473 487 break; 474 - case "d": // snare 475 - sound.synth({ type: "noise", tone: 2200 * pf, duration: 0.12, volume: 0.55 * v, attack: 0.001, decay: 0.11, pan }); 476 - sound.synth({ type: "triangle", tone: 220 * pf, duration: 0.1, volume: 0.4 * v, attack: 0.001, decay: 0.09, pan }); 477 - sound.synth({ type: "square", tone: 180 * pf, duration: 0.05, volume: 0.2 * v, attack: 0.001, decay: 0.045, pan }); 488 + 489 + case "d": // snare — noise + triangle body + square ring, all jittered 490 + sound.synth({ type: "noise", tone: rj(2200, 0.08) * pf, duration: rj(0.12, 0.08), volume: rj(0.55, 0.08) * v, attack: 0.001, decay: 0.11, pan }); 491 + sound.synth({ type: "triangle", tone: rj(220, 0.04) * pf, duration: rj(0.1, 0.08), volume: rj(0.4, 0.08) * v, attack: 0.001, decay: 0.09, pan }); 492 + sound.synth({ type: "square", tone: rj(180, 0.04) * pf, duration: rj(0.05, 0.10), volume: rj(0.2, 0.10) * v, attack: 0.001, decay: 0.045, pan }); 478 493 break; 479 - case "e": // clap — sharper "clip clap": short clicky transient + narrow band noise burst 480 - // The previous version had four soft noise bursts at ~1400 Hz that blurred into 481 - // a diffuse "shh". Real claps have a hard ~2-3 kHz click followed by a tight 482 - // 1.5 kHz body with ~20 ms stereo-flam between the two strikes of the hands. 483 - // New recipe: square-wave click (explicit high frequencies), then a compact 484 - // triple-burst body with tighter timing (6/14/22 ms) and much narrower durations. 485 - // 1. Sharp click — square at 2.5 kHz, 4 ms, panned slightly to opposite side 486 - sound.synth({ type: "square", tone: 2500 * pf, duration: 0.004, volume: 0.9 * v, attack: 0.0003, decay: 0.0035, pan: pan * 0.8 }); 487 - sound.synth({ type: "noise", tone: 6000 * pf, duration: 0.003, volume: 0.6 * v, attack: 0.0003, decay: 0.0025, pan: pan * 0.8 }); 488 - // 2. Body burst — three very short noise bursts, tighter spacing than before 489 - sound.synth({ type: "noise", tone: 1600 * pf, duration: 0.008, volume: 0.75 * v, attack: 0.0003, decay: 0.007, pan }); 490 - setTimeout(() => sound.synth({ type: "noise", tone: 1700 * pf, duration: 0.008, volume: 0.6 * v, attack: 0.0003, decay: 0.007, pan }), 6); 491 - setTimeout(() => sound.synth({ type: "noise", tone: 1500 * pf, duration: 0.008, volume: 0.5 * v, attack: 0.0003, decay: 0.007, pan }), 14); 492 - // 3. Short tail — 1.8 kHz noise for the "smack" after-ring, very brief 493 - setTimeout(() => sound.synth({ type: "noise", tone: 1800 * pf, duration: 0.025, volume: 0.45 * v, attack: 0.0005, decay: 0.024, pan }), 22); 494 + 495 + case "e": // clap — jittered click + BPM-locked flam body 496 + // 1. Click — tone jitter ±10%, brightness ±8% 497 + sound.synth({ type: "square", tone: rj(2500, 0.10) * pf, duration: 0.004, volume: rj(0.9, 0.08) * v, attack: 0.0003, decay: 0.0035, pan: pan * rn(0.6, 0.9) }); 498 + sound.synth({ type: "noise", tone: rj(6000, 0.12) * pf, duration: 0.003, volume: rj(0.6, 0.10) * v, attack: 0.0003, decay: 0.0025, pan: pan * rn(0.6, 0.9) }); 499 + // 2. Body: 3 flam bursts. Timings scale with BPM AND jitter ±30% per hit. 500 + sound.synth({ type: "noise", tone: rj(1600, 0.08) * pf, duration: 0.008, volume: rj(0.75, 0.08) * v, attack: 0.0003, decay: 0.007, pan }); 501 + setTimeout(() => sound.synth({ type: "noise", tone: rj(1700, 0.09) * pf, duration: 0.008, volume: rj(0.6, 0.10) * v, attack: 0.0003, decay: 0.007, pan }), Math.round(flam * rn(0.4, 0.6))); 502 + setTimeout(() => sound.synth({ type: "noise", tone: rj(1500, 0.10) * pf, duration: 0.008, volume: rj(0.5, 0.12) * v, attack: 0.0003, decay: 0.007, pan }), Math.round(flam * rn(1.0, 1.3))); 503 + // 3. Tail 504 + setTimeout(() => sound.synth({ type: "noise", tone: rj(1800, 0.08) * pf, duration: 0.025, volume: rj(0.45, 0.10) * v, attack: 0.0005, decay: 0.024, pan }), Math.round(flam * rn(1.7, 2.0))); 494 505 break; 495 - case "f": // snap — finger snap click + short body 496 - sound.synth({ type: "noise", tone: 3200 * pf, duration: 0.015, volume: 0.45 * v, attack: 0.0003, decay: 0.014, pan }); 497 - sound.synth({ type: "square", tone: 1800 * pf, duration: 0.02, volume: 0.22 * v, attack: 0.0005, decay: 0.018, pan }); 498 - sound.synth({ type: "triangle", tone: 2400 * pf, duration: 0.025, volume: 0.18 * v, attack: 0.0005, decay: 0.022, pan }); 506 + 507 + case "f": // snap — finger snap, tone jitter for variation 508 + sound.synth({ type: "noise", tone: rj(3200, 0.12) * pf, duration: 0.015, volume: rj(0.45, 0.10) * v, attack: 0.0003, decay: 0.014, pan }); 509 + sound.synth({ type: "square", tone: rj(1800, 0.10) * pf, duration: 0.02, volume: rj(0.22, 0.10) * v, attack: 0.0005, decay: 0.018, pan }); 510 + sound.synth({ type: "triangle", tone: rj(2400, 0.10) * pf, duration: 0.025, volume: rj(0.18, 0.10) * v, attack: 0.0005, decay: 0.022, pan }); 499 511 break; 500 - case "g": // closed hi-hat 501 - sound.synth({ type: "noise", tone: 7000 * pf, duration: 0.04, volume: 0.35 * v, attack: 0.0005, decay: 0.035, pan }); 502 - sound.synth({ type: "noise", tone: 5000 * pf, duration: 0.04, volume: 0.2 * v, attack: 0.0005, decay: 0.035, pan }); 512 + 513 + case "g": // closed hi-hat — brightness varies each hit 514 + sound.synth({ type: "noise", tone: rj(7000, 0.08) * pf, duration: 0.04, volume: rj(0.35, 0.10) * v, attack: 0.0005, decay: 0.035, pan }); 515 + sound.synth({ type: "noise", tone: rj(5000, 0.08) * pf, duration: 0.04, volume: rj(0.20, 0.10) * v, attack: 0.0005, decay: 0.035, pan }); 503 516 break; 504 - case "a": // open hi-hat 505 - sound.synth({ type: "noise", tone: 6500 * pf, duration: 0.28, volume: 0.3 * v, attack: 0.001, decay: 0.27, pan }); 506 - sound.synth({ type: "noise", tone: 4800 * pf, duration: 0.2, volume: 0.18 * v, attack: 0.001, decay: 0.19, pan }); 517 + 518 + case "a": // open hi-hat — decay + brightness vary 519 + sound.synth({ type: "noise", tone: rj(6500, 0.07) * pf, duration: rj(0.28, 0.10), volume: rj(0.30, 0.08) * v, attack: 0.001, decay: 0.27, pan }); 520 + sound.synth({ type: "noise", tone: rj(4800, 0.07) * pf, duration: rj(0.20, 0.10), volume: rj(0.18, 0.10) * v, attack: 0.001, decay: 0.19, pan }); 507 521 break; 508 - case "b": // ride 509 - sound.synth({ type: "noise", tone: 4200 * pf, duration: 0.4, volume: 0.28 * v, attack: 0.001, decay: 0.38, pan }); 510 - sound.synth({ type: "square", tone: 3100 * pf, duration: 0.12, volume: 0.1 * v, attack: 0.001, decay: 0.11, pan }); 511 - sound.synth({ type: "square", tone: 4600 * pf, duration: 0.1, volume: 0.08 * v, attack: 0.001, decay: 0.09, pan }); 522 + 523 + case "b": // ride — metallic shimmer with per-hit harmonic variation 524 + sound.synth({ type: "noise", tone: rj(4200, 0.06) * pf, duration: rj(0.4, 0.08), volume: rj(0.28, 0.08) * v, attack: 0.001, decay: 0.38, pan }); 525 + sound.synth({ type: "square", tone: rj(3100, 0.06) * pf, duration: rj(0.12, 0.10), volume: rj(0.10, 0.10) * v, attack: 0.001, decay: 0.11, pan }); 526 + sound.synth({ type: "square", tone: rj(4600, 0.06) * pf, duration: rj(0.1, 0.10), volume: rj(0.08, 0.10) * v, attack: 0.001, decay: 0.09, pan }); 512 527 break; 528 + 513 529 case "c#": // crash 514 - sound.synth({ type: "noise", tone: 3500 * pf, duration: 0.55, volume: 0.42 * v, attack: 0.001, decay: 0.53, pan }); 515 - sound.synth({ type: "noise", tone: 6500 * pf, duration: 0.45, volume: 0.28 * v, attack: 0.002, decay: 0.43, pan }); 516 - sound.synth({ type: "square", tone: 4200 * pf, duration: 0.2, volume: 0.08 * v, attack: 0.001, decay: 0.19, pan }); 530 + sound.synth({ type: "noise", tone: rj(3500, 0.06) * pf, duration: rj(0.55, 0.08), volume: rj(0.42, 0.08) * v, attack: 0.001, decay: 0.53, pan }); 531 + sound.synth({ type: "noise", tone: rj(6500, 0.07) * pf, duration: rj(0.45, 0.08), volume: rj(0.28, 0.08) * v, attack: 0.002, decay: 0.43, pan }); 532 + sound.synth({ type: "square", tone: rj(4200, 0.06) * pf, duration: rj(0.2, 0.10), volume: rj(0.08, 0.10) * v, attack: 0.001, decay: 0.19, pan }); 517 533 break; 534 + 518 535 case "d#": // splash 519 - sound.synth({ type: "noise", tone: 5500 * pf, duration: 0.3, volume: 0.38 * v, attack: 0.001, decay: 0.29, pan }); 520 - sound.synth({ type: "noise", tone: 8500 * pf, duration: 0.22, volume: 0.25 * v, attack: 0.001, decay: 0.21, pan }); 536 + sound.synth({ type: "noise", tone: rj(5500, 0.08) * pf, duration: rj(0.3, 0.10), volume: rj(0.38, 0.08) * v, attack: 0.001, decay: 0.29, pan }); 537 + sound.synth({ type: "noise", tone: rj(8500, 0.08) * pf, duration: rj(0.22, 0.10), volume: rj(0.25, 0.10) * v, attack: 0.001, decay: 0.21, pan }); 521 538 break; 522 - case "f#": // cowbell — detuned square pair 523 - sound.synth({ type: "square", tone: 810 * pf, duration: 0.12, volume: 0.22 * v, attack: 0.001, decay: 0.11, pan }); 524 - sound.synth({ type: "square", tone: 540 * pf, duration: 0.15, volume: 0.18 * v, attack: 0.001, decay: 0.14, pan }); 539 + 540 + case "f#": // cowbell — detuning jitters so hits sound organic 541 + sound.synth({ type: "square", tone: rj(810, 0.04) * pf, duration: rj(0.12, 0.08), volume: rj(0.22, 0.08) * v, attack: 0.001, decay: 0.11, pan }); 542 + sound.synth({ type: "square", tone: rj(540, 0.04) * pf, duration: rj(0.15, 0.08), volume: rj(0.18, 0.08) * v, attack: 0.001, decay: 0.14, pan }); 525 543 break; 544 + 526 545 case "g#": // wood block 527 - sound.synth({ type: "triangle", tone: 900 * pf, duration: 0.06, volume: 0.35 * v, attack: 0.001, decay: 0.05, pan }); 528 - sound.synth({ type: "square", tone: 1800 * pf, duration: 0.03, volume: 0.14 * v, attack: 0.0005, decay: 0.025, pan }); 546 + sound.synth({ type: "triangle", tone: rj(900, 0.06) * pf, duration: rj(0.06, 0.10), volume: rj(0.35, 0.08) * v, attack: 0.001, decay: 0.05, pan }); 547 + sound.synth({ type: "square", tone: rj(1800, 0.08) * pf, duration: rj(0.03, 0.10), volume: rj(0.14, 0.10) * v, attack: 0.0005, decay: 0.025, pan }); 529 548 break; 530 - case "a#": // tambourine 531 - sound.synth({ type: "noise", tone: 7000 * pf, duration: 0.15, volume: 0.3 * v, attack: 0.001, decay: 0.14, pan }); 532 - sound.synth({ type: "noise", tone: 4500 * pf, duration: 0.1, volume: 0.18 * v, attack: 0.001, decay: 0.09, pan }); 533 - sound.synth({ type: "square", tone: 6500 * pf, duration: 0.05, volume: 0.1 * v, attack: 0.001, decay: 0.045, pan }); 549 + 550 + case "a#": // tambourine — jingle varies most per-hit (it's the whole point) 551 + sound.synth({ type: "noise", tone: rj(7000, 0.10) * pf, duration: rj(0.15, 0.12), volume: rj(0.30, 0.10) * v, attack: 0.001, decay: 0.14, pan }); 552 + sound.synth({ type: "noise", tone: rj(4500, 0.10) * pf, duration: rj(0.10, 0.12), volume: rj(0.18, 0.10) * v, attack: 0.001, decay: 0.09, pan }); 553 + sound.synth({ type: "square", tone: rj(6500, 0.08) * pf, duration: 0.05, volume: rj(0.10, 0.12) * v, attack: 0.001, decay: 0.045, pan }); 534 554 break; 535 555 } 536 556 }