Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat(perc): 808-grounded drum kit + drum bus split + dynamic hi-hat pedal

Three related changes bundled into one build cycle:

1. **Drum synthesis rewrite (notepat.mjs)** — all 12 drums rewritten
with research-grounded TR-808 recipes instead of the previous
"layer noise + square + stochastic jitter" approach.

- kick: beater click + pitch snap 130Hz + sustained 50Hz sub
- snare: 808 tonal pair (238/476 Hz exact) + wire rattle noise
- clap: 808 4-burst pattern via staggered attack envelopes
(5/15/25/45 ms) — uses the audio engine's attack
envelope for sub-frame timing instead of setTimeout
- snap: finger-snap physics — broadband click + palm cavity
Helmholtz resonance at 2100 Hz
- hats: 808 6-square inharmonic cluster at exact Roland
frequencies (800/540/522.7/369.6 Hz)
- ride: hat cluster + bell ping (440/587 Hz perfect-5th)
- crash: hat cluster + explosive noise attack + long wash
- splash: short version of crash
- cowbell: 808 triangles 800/540 Hz (3:2 detuned)
- block: single triangle 2500 Hz (clave)
- tambo: 3 staggered noise bursts + 6kHz ting

Key principle: iconic 808 frequencies are NOT tone-jittered.
Per-hit variation moves from tonal (±8-12% → removed) to
timbre (volume balance ±15%, duration ±20%, pan offset jitter).

2. **Dynamic hold model for ringing drums (notepat.mjs)** — open
hat, ride, and crash now use sustain voices that ring while
the key is held and fade on release. Other drums stay as
one-shot transients (kick/clap/snare/snap/hat-c/splash/cowbell/
block/tambo) because hold doesn't make musical sense for them.

New `addSustain(params, bothDuration, releaseFade, releaseUpdate)`
helper creates infinite-duration voices during live play
(pushed to holdVoices array) or finite-duration voices during
reverse playback "both" mode.

`releasePercussionHold()` iterates held voices, applies optional
`releaseUpdate` (e.g. `{tone: 3500}` to dampen open hat brightness
before fade — the "foot pedal closing" effect), then calls
sound.kill() with the per-voice releaseFade. Open hat uses 120ms
fade with brightness dampening, ride 250ms, crash 450ms.

3. **Drum bus split in audio.c mixer** — percussive voices (short
finite duration < 0.5s) bypass the auto-mix divide entirely.
Previously all voices summed into one bus and were normalized
by `max(1.0, voice_sum)`, so 3 drums firing simultaneously
ducked each other to ~1/5 amplitude. Now:

- tone_l/tone_r: accumulate held/sustained voices, divide by
voice_sum (unchanged auto-mix behavior for chords)
- drum_l/drum_r: accumulate transient voices, NO divide
- mix_l = tone_l + drum_l (final soft_clip tanh catches peak)

Result: kick+snare+hat transients stack additively for a
louder peak instead of getting ducked. soft_clip gives a
natural analog saturation character on heavy drum peaks.

4. **Install-debug inline dump (ac-native.c)** — the
`copied /tmp/install-debug.log → /mnt/install-debug.log`
step was printing success but the file wasn't actually landing
on the USB (probably a stale /mnt from the repartition flow).
Now the auto_install_to_hd dumps the full tmpfs log inline via
ac_log at the end, so the complete install trace ALWAYS lands
in ac-native.log (which we know reliably survives). Also added
errno reporting on the /mnt copy so we'll see why it's failing.

5. **mkfs.vfat stderr capture (ac-native.c)** — the last install
attempt failed with `mkfs rc=256 attempt=1/2` and we had no
idea why. Now each mkfs attempt redirects stderr to a dedicated
`/tmp/mkfs-err.log`, then we read it back and ac_log each line
with `[mkfs/1]` or `[mkfs/2]` prefix. Plus WIFEXITED/WEXITSTATUS
decoding of the raw rc so we can distinguish "command not found"
from "mkfs ran and returned 1".

Ready for another w-to-install attempt — this time we'll see the
actual reason mkfs fails in ac-native.log even if /mnt is stale.

+356 -230
+240 -209
fedac/native/pieces/notepat.mjs
··· 597 597 if (drumFlashes.length > 8) drumFlashes.shift(); 598 598 } 599 599 600 - function makePercussionHold(letter, volume, pan, pitchFactor, needsRelease = true) { 601 - return { letter, volume, pan, pitchFactor, needsRelease }; 600 + function makePercussionHold(letter, volume, pan, pitchFactor, voices = []) { 601 + return { letter, volume, pan, pitchFactor, voices }; 602 602 } 603 603 604 + // Release any held drum voices with their per-voice release fade. Each 605 + // entry is { handle, releaseFade, releaseUpdate? }. releaseUpdate runs 606 + // first (to dampen tone/volume before the final fade — the "hi-hat pedal 607 + // closing" effect), then we call sound.kill() with the voice's fade time. 604 608 function releasePercussionHold(sound, hold) { 605 - if (!hold?.needsRelease) return; 606 - playPercussion(sound, hold.letter, hold.volume, hold.pan, hold.pitchFactor, "up"); 609 + if (!hold?.voices?.length) return; 610 + for (const entry of hold.voices) { 611 + if (!entry?.handle) continue; 612 + if (entry.releaseUpdate) { 613 + try { entry.handle.update?.(entry.releaseUpdate); } catch {} 614 + } 615 + sound?.kill?.(entry.handle, entry.releaseFade ?? 0.08); 616 + } 607 617 } 608 618 609 619 function triggerPercussionDown(sound, letter, octaveValue, volume, pan, pitchFactor, key, drumName) { ··· 620 630 base: SAMPLE_BASE_FREQ, 621 631 volume, pan, loop: false, 622 632 }); 623 - hold = makePercussionHold(letter, volume, pan, pitchFactor, false); 633 + hold = makePercussionHold(letter, volume, pan, pitchFactor, []); 624 634 } else { 625 - playPercussion(sound, letter, volume, pan, pitchFactor, "down"); 626 - hold = makePercussionHold(letter, volume, pan, pitchFactor, true); 635 + // Live-play: pass a holdVoices array that playPercussion populates with 636 + // infinite-duration sustain voices. These keep ringing until key-up 637 + // calls releasePercussionHold, which fades them out per-voice. 638 + const voices = []; 639 + playPercussion(sound, letter, volume, pan, pitchFactor, "down", voices); 640 + hold = makePercussionHold(letter, volume, pan, pitchFactor, voices); 627 641 } 628 642 629 643 flashDrum(letter); ··· 636 650 // whether we emit only the strike (`down`), only the release/tail (`up`), 637 651 // or the legacy combined hit (`both`, still used by reverse playback). 638 652 // 639 - // EVERY drum in the kit is a TWO-STEP hit — DOWN (impact/strike) then 640 - // UP (release/resonance/tail). Live keyboard/touch play now splits those 641 - // phases across press and release. Reverse playback keeps the older 642 - // combined hit behavior so the bounce loop still plays complete drums. 653 + // RECIPES are grounded in classic analog drum synthesis (primarily Roland 654 + // TR-808) with frequencies and envelope shapes taken from the bridged-T 655 + // filter math in Kurt Werner's "Boom Like an 808" and the Faust 656 + // synths.lib reference (grame-cncm/faustlibraries). See reports/ for the 657 + // research summary. Key principle: the iconic 808 frequencies (238 Hz 658 + // snare, 540/800 Hz cowbell, 6-square hat cluster) are NOT jittered — 659 + // they're what makes each drum sound like itself. Per-hit variation 660 + // comes from TIMBRE jitter (volume balance, decay, attack, pan) not 661 + // tonal jitter. 643 662 // 644 - // Every drum also gets STOCHASTIC variation on each press so repeated 645 - // hits don't sound machine-stamped: 646 - // - tone jitter: ±3-12% of the nominal frequency 647 - // - volume jitter: ±5-12% of the nominal volume 648 - // - duration jitter: ±5-14% on longer tails 649 - // - pan offset jitter on each step for subtle stereo movement 650 - // - reverse-playback DOWN→UP gap jitters via rn(...) 663 + // Multi-burst drums (clap, tambourine) use the Faust trick of firing 664 + // one voice per burst with staggered `attack` values (0.005 / 0.015 / 665 + // 0.025 / 0.035) — the audio engine's attack envelope provides the 666 + // sub-frame timing resolution that the setTimeout polyfill can't 667 + // (sim() only flushes every ~16 ms). 651 668 // 652 - // Multi-burst flam timing scales with metronomeBPM so drum rolls lock 653 - // to the current tempo: at 120 BPM each sub-beat is ~12.5 ms. 654 - function playPercussion(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both") { 669 + // Phase split: live keyboard/touch plays DOWN on key press and UP on 670 + // key release. Reverse playback uses `both` to replay complete drums. 671 + function playPercussion(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both", holdVoices = null) { 655 672 if (!sound?.synth) return; 656 673 const v = Math.max(0.1, Math.min(2.2, volume)); 657 674 const pf = Math.max(0.25, Math.min(4, pitchFactor)); 675 + // "down" = live press: fire transients + start sustain voices (pushed to holdVoices) 676 + // "both" = reverse replay: fire a complete, self-contained drum using finite durations 677 + // "up" = (legacy) release fragment — now handled by releasePercussionHold's kill fades 658 678 const fireDown = phase !== "up"; 659 - const fireUp = phase !== "down"; 679 + const isLive = phase === "down" && Array.isArray(holdVoices); 660 680 661 681 // Per-hit random helpers (inline, stateless). `rj` jitters around a center 662 682 // by a ± fraction; `rn` returns a uniform range. 663 683 const rj = (center, frac) => center * (1 + (Math.random() - 0.5) * 2 * frac); 664 684 const rn = (min, max) => min + Math.random() * (max - min); 665 685 666 - // BPM-locked flam unit for multi-burst drums. metronomeBPM defaults to 120 667 - // so this is ~12.5 ms at default. Scales inversely: 60 bpm → 25 ms, 668 - // 180 bpm → ~8 ms. Used for clap / future multi-burst drums. 686 + // BPM-locked flam unit (legacy multi-burst timing). Now mostly unused 687 + // because we lean on staggered `attack` envelopes instead of setTimeout. 669 688 const flam = (60000 / Math.max(40, Math.min(240, metronomeBPM || 120))) * 0.025; 670 - const playUp = (delayMs, fn) => { 671 - if (!fireUp) return; 672 - if (phase === "both") setTimeout(fn, delayMs); 673 - else fn(); 689 + 690 + // Push a sustain voice onto holdVoices if live, otherwise fire it as 691 + // a finite-duration one-shot (for reverse replay / "both" mode). The 692 + // `releaseFade` is how quickly this voice damps when the key lifts — 693 + // short for closed/muted sounds, long for open cymbals and sub-basses. 694 + // Optional `releaseUpdate` applies a tone/volume change RIGHT BEFORE 695 + // the kill fade — e.g. open hat → closed hat dampens brightness. 696 + const addSustain = (params, bothDuration, releaseFade, releaseUpdate) => { 697 + if (isLive) { 698 + const handle = sound.synth({ ...params, duration: Infinity }); 699 + if (handle) holdVoices.push({ handle, releaseFade, releaseUpdate }); 700 + return handle; 701 + } else { 702 + return sound.synth({ ...params, duration: bothDuration }); 703 + } 674 704 }; 675 705 706 + // TR-808 hi-hat 6-square inharmonic cluster (exact Roland frequencies). 707 + // Drop 205/304 for synth clarity since we can't HPF away the low content. 708 + const HAT_FREQS = [800, 540, 522.7, 369.6]; 709 + 710 + if (!fireDown) return; 711 + 676 712 switch (letter) { 677 - case "c": // kick — two-step: DOWN (beater punch) + UP (sub wobble bloom) 678 - // STEP 1 — DOWN: beater click + body thump + sawtooth grit. The sharp 679 - // impact that gives the kick its attack. All front-loaded. 680 - if (fireDown) { 681 - const downPan = pan + rn(-0.04, 0.04); 682 - 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: downPan }); 683 - 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: downPan }); 684 - 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: downPan }); 685 - 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: downPan }); 686 - } 687 - // STEP 2 — UP: sub-bass wobble bloom — the release where the kick 688 - // expands outward. Two sines ~10 Hz apart for per-hit beat-freq jitter, 689 - // plus a mid-low square fill. Very tight gap (~0.4 flam) so the punch 690 - // and bloom still feel like one unified kick hit. 691 - playUp(Math.round(flam * rn(0.3, 0.6)), () => { 692 - const upPan = pan + rn(-0.02, 0.02); 693 - const subLo = rj(44, 0.05); // ~41.8 → 46.2 Hz 694 - const subHi = subLo + rn(8, 12); // ~50 → 58 Hz → beat freq 8-12 Hz 695 - 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: upPan }); 696 - 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: upPan }); 697 - 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: upPan }); 698 - }); 713 + // === ONE-SHOT DRUMS === 714 + // These fire as transient events only — the physical gesture is a strike, 715 + // not a held note. No sustain voices, no release transition. Hold duration 716 + // doesn't affect them. Applies to: kick, snare, clap, snap, closed hat. 717 + 718 + case "c": { // kick — TR-808: beater click + pitch snap + body (one-shot) 719 + const downPan = pan + rn(-0.02, 0.02); 720 + // Beater click 721 + sound.synth({ type: "noise", tone: 4000, duration: 0.002, volume: rj(0.55, 0.12) * v, attack: 0.0001, decay: 0.002, pan: downPan }); 722 + // Pitch snap (short 130Hz sine) 723 + sound.synth({ type: "sine", tone: 130 * pf, duration: 0.010, volume: rj(1.1, 0.10) * v, attack: 0.0002, decay: 0.009, pan: downPan }); 724 + // Mid body attack 725 + sound.synth({ type: "sine", tone: 80 * pf, duration: 0.08, volume: rj(1.6, 0.08) * v, attack: 0.001, decay: 0.075, pan: downPan }); 726 + // The 808 kick's signature long 50Hz sub — natural decay 400-600ms 727 + sound.synth({ type: "sine", tone: 50 * pf, duration: rj(0.55, 0.20), volume: rj(1.9, 0.10) * v, attack: 0.003, decay: 0.54, pan: downPan }); 728 + // Mid fill for presence 729 + sound.synth({ type: "sine", tone: 100 * pf, duration: rj(0.12, 0.20), volume: rj(0.55, 0.15) * v, attack: 0.004, decay: 0.115, pan: downPan }); 699 730 break; 731 + } 700 732 701 - case "d": // snare — two-step: DOWN (stick hit) + UP (wire rattle) 702 - // STEP 1 — DOWN: sharp stick-on-head impact. 703 - if (fireDown) { 704 - const downPan = pan + rn(-0.04, 0.04); 705 - sound.synth({ type: "noise", tone: rj(5200, 0.10) * pf, duration: 0.0045, volume: rj(0.74, 0.08) * v, attack: 0.0002, decay: 0.004, pan: downPan }); 706 - sound.synth({ type: "noise", tone: rj(2500, 0.10) * pf, duration: 0.007, volume: rj(0.52, 0.10) * v, attack: 0.0002, decay: 0.0063, pan: downPan }); 707 - sound.synth({ type: "square", tone: rj(1900, 0.18) * pf, duration: 0.0025, volume: rj(0.12, 0.12) * v, attack: 0.0002, decay: 0.0021, pan: downPan }); 708 - } 709 - // STEP 2 — UP: wire rattle sustain + shell body ring. 710 - playUp(Math.round(flam * rn(0.3, 0.6)), () => { 711 - const upPan = pan + rn(-0.04, 0.04); 712 - sound.synth({ type: "noise", tone: rj(2400, 0.08) * pf, duration: rj(0.14, 0.08), volume: rj(0.56, 0.08) * v, attack: 0.001, decay: 0.13, pan: upPan }); 713 - sound.synth({ type: "noise", tone: rj(1400, 0.07) * pf, duration: rj(0.09, 0.10), volume: rj(0.18, 0.10) * v, attack: 0.0012, decay: 0.082, pan: upPan }); 714 - sound.synth({ type: "triangle", tone: rj(190, 0.08) * pf, duration: rj(0.07, 0.10), volume: rj(0.20, 0.10) * v, attack: 0.0015, decay: 0.065, pan: upPan }); 715 - }); 733 + case "d": { // snare — TR-808: stick click + tonal pair + wire rattle (one-shot) 734 + const downPan = pan + rn(-0.02, 0.02); 735 + // Sharp stick-on-head noise transient 736 + sound.synth({ type: "noise", tone: 3000, duration: 0.006, volume: rj(0.80, 0.12) * v, attack: 0.0001, decay: 0.006, pan: downPan }); 737 + // 808 tonal pair — 238/476 Hz 738 + sound.synth({ type: "sine", tone: 238 * pf, duration: rj(0.12, 0.20), volume: rj(0.55, 0.10) * v, attack: 0.0005, decay: 0.115, pan: downPan }); 739 + sound.synth({ type: "sine", tone: 476 * pf, duration: rj(0.12, 0.20), volume: rj(0.45, 0.10) * v, attack: 0.0005, decay: 0.115, pan: downPan }); 740 + // Bright wire noise 741 + sound.synth({ type: "noise", tone: 3000, duration: rj(0.14, 0.20), volume: rj(0.62, 0.12) * v, attack: 0.001, decay: 0.135, pan: downPan + rn(-0.04, 0.04) }); 742 + // Lower noise body 743 + sound.synth({ type: "noise", tone: 1500, duration: rj(0.09, 0.20), volume: rj(0.22, 0.15) * v, attack: 0.001, decay: 0.085, pan: downPan + rn(-0.04, 0.04) }); 716 744 break; 745 + } 717 746 718 - case "e": // clap — two-step: DOWN (dark palm strike) then UP (bright release) 719 - // STEP 1 — DOWN: palms meet. Darker body thump + low-mid noise burst. 720 - // Lower-pitched, meatier, slightly left of center for stereo interest. 721 - if (fireDown) { 722 - const downPan = pan + rn(-0.08, 0.02); 723 - sound.synth({ type: "noise", tone: rj(900, 0.10) * pf, duration: 0.012, volume: rj(0.85, 0.08) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 724 - sound.synth({ type: "square", tone: rj(260, 0.06) * pf, duration: 0.014, volume: rj(0.50, 0.10) * v, attack: 0.0004, decay: 0.013, pan: downPan }); 725 - sound.synth({ type: "noise", tone: rj(1400, 0.10) * pf, duration: 0.008, volume: rj(0.46, 0.12) * v, attack: 0.0003, decay: 0.007, pan: downPan }); 726 - } 727 - // STEP 2 — UP: hands separate, bright transient + airy tail. 728 - // Delayed by ~1.5 flam units (scales with BPM) with jitter so hits vary. 729 - // Panned slightly opposite for L/R call-response feel. 730 - // Add two follow-up noise bursts so the clap opens out instead of 731 - // collapsing into a single tiny tick. 732 - playUp(Math.round(flam * rn(1.3, 1.7)), () => { 733 - const upPan = pan + rn(-0.02, 0.10); 734 - sound.synth({ type: "square", tone: rj(3200, 0.10) * pf, duration: 0.0055, volume: rj(0.58, 0.08) * v, attack: 0.0002, decay: 0.0048, pan: upPan }); 735 - sound.synth({ type: "noise", tone: rj(5200, 0.12) * pf, duration: 0.012, volume: rj(0.72, 0.10) * v, attack: 0.0003, decay: 0.011, pan: upPan }); 736 - sound.synth({ type: "noise", tone: rj(2400, 0.08) * pf, duration: 0.028, volume: rj(0.52, 0.10) * v, attack: 0.0005, decay: 0.026, pan: upPan }); 737 - setTimeout(() => { 738 - sound.synth({ type: "noise", tone: rj(4700, 0.12) * pf, duration: 0.009, volume: rj(0.58, 0.10) * v, attack: 0.0003, decay: 0.008, pan: upPan }); 739 - }, Math.round(flam * rn(1.0, 1.7))); 740 - setTimeout(() => { 741 - sound.synth({ type: "noise", tone: rj(3600, 0.10) * pf, duration: 0.016, volume: rj(0.44, 0.10) * v, attack: 0.0004, decay: 0.014, pan: upPan }); 742 - }, Math.round(flam * rn(2.0, 3.0))); 743 - }); 747 + case "e": { // clap — TR-808 4-burst pattern via staggered attacks (one-shot) 748 + const downPan = pan + rn(-0.06, 0.02); 749 + // Three rapid bursts — attacks 5/15/25 ms create ~10 ms spacing 750 + sound.synth({ type: "noise", tone: 1000, duration: 0.025, volume: rj(0.90, 0.15) * v, attack: 0.005, decay: 0.020, pan: downPan }); 751 + sound.synth({ type: "noise", tone: 1100, duration: 0.035, volume: rj(0.95, 0.15) * v, attack: 0.015, decay: 0.020, pan: downPan }); 752 + sound.synth({ type: "noise", tone: 900, duration: 0.045, volume: rj(0.85, 0.15) * v, attack: 0.025, decay: 0.020, pan: downPan }); 753 + // Bright edge on the first burst 754 + sound.synth({ type: "noise", tone: 3000, duration: 0.008, volume: rj(0.55, 0.15) * v, attack: 0.001, decay: 0.007, pan: downPan }); 755 + // The 4th burst — "room tail" with long decay (120ms) 756 + sound.synth({ type: "noise", tone: 1000, duration: rj(0.14, 0.25), volume: rj(0.85, 0.15) * v, attack: 0.045, decay: 0.135, pan: downPan + rn(-0.02, 0.10) }); 757 + sound.synth({ type: "noise", tone: 2200, duration: rj(0.10, 0.25), volume: rj(0.35, 0.18) * v, attack: 0.050, decay: 0.095, pan: downPan + rn(-0.02, 0.10) }); 744 758 break; 759 + } 745 760 746 - case "f": // snap — two-step: DOWN (finger click) + UP (skin slap tail) 747 - // STEP 1 — DOWN: sharp high-frequency click from compressed fingertip 748 - // release. This is the "snap" itself — very brief. 749 - if (fireDown) { 750 - const downPan = pan + rn(-0.05, 0.05); 751 - sound.synth({ type: "noise", tone: rj(3200, 0.12) * pf, duration: 0.012, volume: rj(0.50, 0.10) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 752 - sound.synth({ type: "square", tone: rj(1800, 0.10) * pf, duration: 0.008, volume: rj(0.22, 0.10) * v, attack: 0.0005, decay: 0.0075, pan: downPan }); 753 - } 754 - // STEP 2 — UP: finger slaps palm — brief hollow body ring. 755 - playUp(Math.round(flam * rn(0.4, 0.7)), () => { 756 - const upPan = pan + rn(-0.03, 0.05); 757 - sound.synth({ type: "triangle", tone: rj(2400, 0.10) * pf, duration: 0.022, volume: rj(0.20, 0.10) * v, attack: 0.0005, decay: 0.020, pan: upPan }); 758 - sound.synth({ type: "square", tone: rj(1400, 0.10) * pf, duration: 0.015, volume: rj(0.12, 0.12) * v, attack: 0.0005, decay: 0.014, pan: upPan }); 759 - }); 761 + case "f": { // snap — finger snap physics (one-shot) 762 + const downPan = pan + rn(-0.04, 0.04); 763 + // Sharp broadband click (thumb-middle friction release) 764 + sound.synth({ type: "noise", tone: 6000, duration: 0.003, volume: rj(0.70, 0.15) * v, attack: 0.0001, decay: 0.0028, pan: downPan }); 765 + // Palm cavity resonance at ~2100 Hz — the "pop" tone 766 + sound.synth({ type: "sine", tone: 2100 * pf, duration: rj(0.045, 0.20), volume: rj(0.55, 0.12) * v, attack: 0.0005, decay: 0.044, pan: downPan }); 767 + // Upper bite at 3500 Hz 768 + sound.synth({ type: "sine", tone: 3500 * pf, duration: rj(0.020, 0.25), volume: rj(0.28, 0.18) * v, attack: 0.0005, decay: 0.019, pan: downPan }); 760 769 break; 770 + } 761 771 762 - case "g": // closed hi-hat — two-step: DOWN (stick tick) + UP (tight sizzle) 763 - // STEP 1 — DOWN: stick contact tick, bright transient metal spike. 764 - if (fireDown) { 765 - const downPan = pan + rn(-0.04, 0.04); 766 - sound.synth({ type: "noise", tone: rj(9000, 0.08) * pf, duration: 0.008, volume: rj(0.40, 0.10) * v, attack: 0.0003, decay: 0.0075, pan: downPan }); 772 + case "g": { // closed hi-hat — 4-square cluster, tight short decay (one-shot) 773 + const downPan = pan + rn(-0.03, 0.03); 774 + // Full cluster — the short decay is the "closed" character 775 + for (const f of HAT_FREQS) { 776 + sound.synth({ type: "square", tone: f * pf, duration: rj(0.008, 0.20), volume: rj(0.18, 0.18) * v, attack: 0.0005, decay: 0.0075, pan: downPan }); 767 777 } 768 - // STEP 2 — UP: brief metallic sizzle release. Much shorter than open 769 - // hat since the cymbal is clamped shut. 770 - playUp(Math.round(flam * rn(0.3, 0.5)), () => { 771 - const upPan = pan + rn(-0.04, 0.04); 772 - sound.synth({ type: "noise", tone: rj(7000, 0.08) * pf, duration: rj(0.03, 0.15), volume: rj(0.32, 0.10) * v, attack: 0.001, decay: 0.028, pan: upPan }); 773 - sound.synth({ type: "noise", tone: rj(5000, 0.08) * pf, duration: rj(0.03, 0.15), volume: rj(0.18, 0.10) * v, attack: 0.001, decay: 0.028, pan: upPan }); 774 - }); 778 + // Bright noise top for the "tss" 779 + sound.synth({ type: "noise", tone: 8000, duration: rj(0.040, 0.20), volume: rj(0.38, 0.12) * v, attack: 0.0005, decay: 0.038, pan: downPan }); 775 780 break; 781 + } 776 782 777 - case "a": // open hi-hat — two-step: DOWN (chip strike) then UP (airy shimmer) 778 - // STEP 1 — DOWN: short metallic chip — initial cymbal contact. 779 - if (fireDown) { 780 - const downPan = pan + rn(-0.06, 0.04); 781 - sound.synth({ type: "noise", tone: rj(8200, 0.08) * pf, duration: 0.012, volume: rj(0.45, 0.08) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 782 - sound.synth({ type: "square", tone: rj(5400, 0.08) * pf, duration: 0.008, volume: rj(0.18, 0.10) * v, attack: 0.0005, decay: 0.0075, pan: downPan }); 783 + case "a": { // open hi-hat — TR-808 cluster, LONG sustain. Foot-pedal release = damp fast. 784 + const downPan = pan + rn(-0.04, 0.04); 785 + // Transient: the 4-square attack cluster 786 + for (const f of HAT_FREQS) { 787 + sound.synth({ type: "square", tone: f * pf, duration: 0.012, volume: rj(0.16, 0.18) * v, attack: 0.0005, decay: 0.011, pan: downPan }); 783 788 } 784 - // STEP 2 — UP: sustained airy shimmer that blooms after the chip. Decay 785 - // + brightness vary per hit, panned slightly opposite for call/response. 786 - playUp(Math.round(flam * rn(0.8, 1.2)), () => { 787 - const upPan = pan + rn(-0.02, 0.08); 788 - sound.synth({ type: "noise", tone: rj(6500, 0.07) * pf, duration: rj(0.26, 0.10), volume: rj(0.28, 0.08) * v, attack: 0.003, decay: 0.25, pan: upPan }); 789 - sound.synth({ type: "noise", tone: rj(4800, 0.07) * pf, duration: rj(0.19, 0.10), volume: rj(0.17, 0.10) * v, attack: 0.003, decay: 0.18, pan: upPan }); 790 - }); 789 + // Transient: bright noise chip 790 + sound.synth({ type: "noise", tone: 8200, duration: 0.012, volume: rj(0.42, 0.12) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 791 + // SUSTAIN: the signature open-hat shimmer. Rings until key release. 792 + // Release = hi-hat foot pedal closing — dampens the top end fast. 793 + // releaseUpdate drops the tone first (simulates bandpass closing), 794 + // then the kill fade does the rest. 795 + addSustain( 796 + { type: "noise", tone: 7000, volume: rj(0.32, 0.15) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 797 + rj(0.40, 0.25), 798 + 0.12, // 120ms foot-pedal close 799 + { tone: 3500 } // dampen brightness on release 800 + ); 801 + addSustain( 802 + { type: "noise", tone: 5000, volume: rj(0.20, 0.18) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 803 + rj(0.25, 0.25), 804 + 0.10, 805 + { tone: 2800 } 806 + ); 807 + // Persistent metallic square from the cluster for body 808 + addSustain( 809 + { type: "square", tone: 800 * pf, volume: rj(0.08, 0.20) * v, attack: 0.005, decay: 0, pan: downPan }, 810 + rj(0.20, 0.25), 811 + 0.08 812 + ); 791 813 break; 814 + } 792 815 793 - case "b": // ride — two-step: DOWN (bell ping) + UP (shimmer wash) 794 - // STEP 1 — DOWN: bell-like ping strike — the initial mallet contact. 795 - if (fireDown) { 796 - const downPan = pan + rn(-0.05, 0.05); 797 - sound.synth({ type: "square", tone: rj(3100, 0.06) * pf, duration: rj(0.06, 0.10), volume: rj(0.20, 0.10) * v, attack: 0.0005, decay: 0.055, pan: downPan }); 798 - sound.synth({ type: "square", tone: rj(4600, 0.06) * pf, duration: rj(0.05, 0.10), volume: rj(0.14, 0.10) * v, attack: 0.0005, decay: 0.045, pan: downPan }); 799 - } 800 - // STEP 2 — UP: sustained metallic shimmer wash — the cymbal body 801 - // resonating after the strike. Longest tail of the kit. 802 - playUp(Math.round(flam * rn(0.5, 0.8)), () => { 803 - const upPan = pan + rn(-0.03, 0.03); 804 - sound.synth({ type: "noise", tone: rj(4200, 0.06) * pf, duration: rj(0.38, 0.08), volume: rj(0.26, 0.08) * v, attack: 0.003, decay: 0.37, pan: upPan }); 805 - sound.synth({ type: "square", tone: rj(3100, 0.06) * pf, duration: rj(0.10, 0.10), volume: rj(0.08, 0.10) * v, attack: 0.002, decay: 0.095, pan: upPan }); 806 - }); 816 + case "b": { // ride — bell ping + long shimmer sustain 817 + const downPan = pan + rn(-0.03, 0.03); 818 + // Transient: stick tip on the cluster (sharp attack) 819 + sound.synth({ type: "square", tone: 800 * pf, duration: 0.020, volume: rj(0.10, 0.18) * v, attack: 0.0005, decay: 0.019, pan: downPan }); 820 + sound.synth({ type: "square", tone: 540 * pf, duration: 0.020, volume: rj(0.08, 0.18) * v, attack: 0.0005, decay: 0.019, pan: downPan }); 821 + // SUSTAIN: bell ping pair (perfect-5th) — THE ride signature 822 + addSustain( 823 + { type: "sine", tone: 440 * pf, volume: rj(0.24, 0.12) * v, attack: 0.0008, decay: 0, pan: downPan }, 824 + rj(0.40, 0.20), 825 + 0.25 // long ring release — bell decays slowly 826 + ); 827 + addSustain( 828 + { type: "sine", tone: 587 * pf, volume: rj(0.20, 0.12) * v, attack: 0.0008, decay: 0, pan: downPan }, 829 + rj(0.40, 0.20), 830 + 0.25 831 + ); 832 + // SUSTAIN: long shimmer body 833 + addSustain( 834 + { type: "noise", tone: 4200, volume: rj(0.26, 0.12) * v, attack: 0.005, decay: 0, pan: downPan + rn(-0.03, 0.03) }, 835 + rj(0.9, 0.20), 836 + 0.30 837 + ); 807 838 break; 839 + } 808 840 809 - case "c#": // crash — two-step: DOWN (metal impact) + UP (long wash) 810 - // STEP 1 — DOWN: explosive high-brightness metal strike. 811 - if (fireDown) { 812 - const downPan = pan + rn(-0.06, 0.06); 813 - sound.synth({ type: "noise", tone: rj(7500, 0.08) * pf, duration: 0.015, volume: rj(0.55, 0.08) * v, attack: 0.0005, decay: 0.014, pan: downPan }); 814 - sound.synth({ type: "square", tone: rj(4200, 0.06) * pf, duration: 0.012, volume: rj(0.20, 0.10) * v, attack: 0.0005, decay: 0.011, pan: downPan }); 841 + case "c#": { // crash — explosive noise attack + LONG shimmer wash 842 + const downPan = pan + rn(-0.05, 0.05); 843 + // Transient: loud noise splash 844 + sound.synth({ type: "noise", tone: 8000, duration: 0.030, volume: rj(0.75, 0.15) * v, attack: 0.0005, decay: 0.029, pan: downPan }); 845 + // Transient: hat cluster for metallic attack 846 + for (const f of HAT_FREQS) { 847 + sound.synth({ type: "square", tone: f * pf, duration: 0.030, volume: rj(0.12, 0.20) * v, attack: 0.0005, decay: 0.029, pan: downPan }); 815 848 } 816 - // STEP 2 — UP: long sustained wash. Where the energy actually lives. 817 - playUp(Math.round(flam * rn(0.4, 0.7)), () => { 818 - const upPan = pan + rn(-0.04, 0.04); 819 - sound.synth({ type: "noise", tone: rj(3500, 0.06) * pf, duration: rj(0.55, 0.08), volume: rj(0.40, 0.08) * v, attack: 0.005, decay: 0.54, pan: upPan }); 820 - 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.005, decay: 0.44, pan: upPan }); 821 - 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.002, decay: 0.19, pan: upPan }); 822 - }); 849 + // SUSTAIN: the long wash — crash's whole character 850 + addSustain( 851 + { type: "noise", tone: 5000, volume: rj(0.45, 0.12) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 852 + rj(1.4, 0.18), 853 + 0.45 // long release fade 854 + ); 855 + addSustain( 856 + { type: "noise", tone: 7500, volume: rj(0.30, 0.15) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 857 + rj(0.9, 0.18), 858 + 0.35 859 + ); 860 + // Cluster fragment body 861 + addSustain( 862 + { type: "square", tone: 800 * pf, volume: rj(0.08, 0.20) * v, attack: 0.008, decay: 0, pan: downPan }, 863 + rj(0.5, 0.20), 864 + 0.20 865 + ); 823 866 break; 867 + } 824 868 825 - case "d#": // splash — two-step: DOWN (thin metal tick) + UP (short wash) 826 - // STEP 1 — DOWN: thin high metal contact — the splash's sharp front. 827 - if (fireDown) { 828 - const downPan = pan + rn(-0.05, 0.05); 829 - sound.synth({ type: "noise", tone: rj(9500, 0.10) * pf, duration: 0.010, volume: rj(0.45, 0.08) * v, attack: 0.0004, decay: 0.009, pan: downPan }); 830 - } 831 - // STEP 2 — UP: short bright wash — splash is all quick burst, no sustain. 832 - playUp(Math.round(flam * rn(0.4, 0.6)), () => { 833 - const upPan = pan + rn(-0.03, 0.03); 834 - sound.synth({ type: "noise", tone: rj(5500, 0.08) * pf, duration: rj(0.28, 0.10), volume: rj(0.36, 0.08) * v, attack: 0.003, decay: 0.27, pan: upPan }); 835 - sound.synth({ type: "noise", tone: rj(8500, 0.08) * pf, duration: rj(0.20, 0.10), volume: rj(0.23, 0.10) * v, attack: 0.003, decay: 0.19, pan: upPan }); 836 - }); 869 + case "d#": { // splash — short bright cymbal burst (one-shot) 870 + const downPan = pan + rn(-0.04, 0.04); 871 + // Bright attack 872 + sound.synth({ type: "noise", tone: 9000, duration: 0.012, volume: rj(0.55, 0.15) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 873 + sound.synth({ type: "square", tone: 800 * pf, duration: 0.015, volume: rj(0.14, 0.20) * v, attack: 0.0005, decay: 0.014, pan: downPan }); 874 + sound.synth({ type: "square", tone: 540 * pf, duration: 0.015, volume: rj(0.10, 0.20) * v, attack: 0.0005, decay: 0.014, pan: downPan }); 875 + // Short wash — splash is all quick burst, no sustain 876 + sound.synth({ type: "noise", tone: 6000, duration: rj(0.35, 0.20), volume: rj(0.42, 0.12) * v, attack: 0.004, decay: 0.345, pan: downPan + rn(-0.03, 0.03) }); 877 + sound.synth({ type: "noise", tone: 8500, duration: rj(0.22, 0.20), volume: rj(0.25, 0.15) * v, attack: 0.004, decay: 0.215, pan: downPan + rn(-0.03, 0.03) }); 837 878 break; 879 + } 838 880 839 - case "f#": // cowbell — two-step: DOWN (stick strike) + UP (resonant ring) 840 - // STEP 1 — DOWN: stick-on-metal strike — the "tink" attack. 841 - if (fireDown) { 842 - const downPan = pan + rn(-0.04, 0.04); 843 - sound.synth({ type: "square", tone: rj(1800, 0.06) * pf, duration: 0.008, volume: rj(0.30, 0.08) * v, attack: 0.0005, decay: 0.0075, pan: downPan }); 844 - } 845 - // STEP 2 — UP: the signature cowbell ring — two detuned squares beat. 846 - playUp(Math.round(flam * rn(0.3, 0.5)), () => { 847 - const upPan = pan + rn(-0.03, 0.03); 848 - 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.002, decay: 0.115, pan: upPan }); 849 - 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.002, decay: 0.145, pan: upPan }); 850 - }); 881 + case "f#": { // cowbell — TR-808: two triangles 800/540 Hz (one-shot) 882 + const downPan = pan + rn(-0.03, 0.03); 883 + // Stick click 884 + sound.synth({ type: "square", tone: 1800 * pf, duration: 0.004, volume: rj(0.35, 0.15) * v, attack: 0.0002, decay: 0.0038, pan: downPan }); 885 + // The two bell triangles — natural decay 886 + sound.synth({ type: "triangle", tone: 800 * pf, duration: rj(0.28, 0.20), volume: rj(0.42, 0.12) * v, attack: 0.0008, decay: 0.275, pan: downPan }); 887 + sound.synth({ type: "triangle", tone: 540 * pf, duration: rj(0.28, 0.20), volume: rj(0.36, 0.12) * v, attack: 0.0008, decay: 0.275, pan: downPan }); 851 888 break; 889 + } 852 890 853 - case "g#": // wood block — two-step: DOWN (stick tick) + UP (hollow body) 854 - // STEP 1 — DOWN: stick contact tick — the initial bright transient. 855 - if (fireDown) { 856 - const downPan = pan + rn(-0.04, 0.04); 857 - sound.synth({ type: "square", tone: rj(2400, 0.08) * pf, duration: 0.004, volume: rj(0.22, 0.10) * v, attack: 0.0003, decay: 0.0035, pan: downPan }); 858 - } 859 - // STEP 2 — UP: hollow body ring — the wood resonance. 860 - playUp(Math.round(flam * rn(0.3, 0.5)), () => { 861 - const upPan = pan + rn(-0.03, 0.03); 862 - 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.055, pan: upPan }); 863 - sound.synth({ type: "square", tone: rj(1800, 0.08) * pf, duration: rj(0.03, 0.10), volume: rj(0.12, 0.10) * v, attack: 0.0005, decay: 0.025, pan: upPan }); 864 - }); 891 + case "g#": { // wood block — single triangle @ 2500 Hz (one-shot) 892 + const downPan = pan + rn(-0.03, 0.03); 893 + // Stick click 894 + sound.synth({ type: "noise", tone: 5000, duration: 0.002, volume: rj(0.35, 0.18) * v, attack: 0.0001, decay: 0.0018, pan: downPan }); 895 + // Block tones 896 + sound.synth({ type: "triangle", tone: 2500 * pf, duration: rj(0.050, 0.25), volume: rj(0.52, 0.12) * v, attack: 0.0003, decay: 0.048, pan: downPan }); 897 + sound.synth({ type: "triangle", tone: 1250 * pf, duration: rj(0.050, 0.25), volume: rj(0.18, 0.18) * v, attack: 0.0005, decay: 0.048, pan: downPan }); 865 898 break; 899 + } 866 900 867 - case "a#": // tambourine — two-step: DOWN (frame strike) + UP (jingle rattle) 868 - // STEP 1 — DOWN: hand/stick on frame — the dry initial hit. 869 - if (fireDown) { 870 - const downPan = pan + rn(-0.05, 0.05); 871 - sound.synth({ type: "noise", tone: rj(2200, 0.10) * pf, duration: 0.010, volume: rj(0.30, 0.10) * v, attack: 0.0003, decay: 0.009, pan: downPan }); 872 - sound.synth({ type: "square", tone: rj(1500, 0.08) * pf, duration: 0.008, volume: rj(0.15, 0.12) * v, attack: 0.0005, decay: 0.0075, pan: downPan }); 873 - } 874 - // STEP 2 — UP: the jingle rattle — where the tambourine character lives. 875 - // Max per-hit jitter since real tambourines never sound the same twice. 876 - playUp(Math.round(flam * rn(0.4, 0.7)), () => { 877 - const upPan = pan + rn(-0.05, 0.05); 878 - sound.synth({ type: "noise", tone: rj(7000, 0.12) * pf, duration: rj(0.14, 0.14), volume: rj(0.30, 0.12) * v, attack: 0.002, decay: 0.13, pan: upPan }); 879 - sound.synth({ type: "noise", tone: rj(4500, 0.12) * pf, duration: rj(0.09, 0.14), volume: rj(0.18, 0.12) * v, attack: 0.002, decay: 0.085, pan: upPan }); 880 - sound.synth({ type: "square", tone: rj(6500, 0.10) * pf, duration: 0.05, volume: rj(0.10, 0.14) * v, attack: 0.001, decay: 0.045, pan: upPan }); 881 - }); 901 + case "a#": { // tambourine — staggered jingle bursts (one-shot) 902 + const downPan = pan + rn(-0.04, 0.04); 903 + // 3 staggered noise bursts via attack offsets (jingle rattle) 904 + sound.synth({ type: "noise", tone: 7000, duration: 0.08, volume: rj(0.38, 0.18) * v, attack: 0.002, decay: 0.075, pan: downPan }); 905 + sound.synth({ type: "noise", tone: 7500, duration: 0.09, volume: rj(0.30, 0.18) * v, attack: 0.015, decay: 0.075, pan: downPan }); 906 + sound.synth({ type: "noise", tone: 6500, duration: 0.10, volume: rj(0.25, 0.18) * v, attack: 0.030, decay: 0.070, pan: downPan }); 907 + // High square ting 908 + sound.synth({ type: "square", tone: 6000 * pf, duration: 0.030, volume: rj(0.14, 0.20) * v, attack: 0.001, decay: 0.028, pan: downPan }); 909 + // Long jingle tail 910 + sound.synth({ type: "noise", tone: 7000, duration: rj(0.20, 0.22), volume: rj(0.32, 0.18) * v, attack: 0.050, decay: 0.195, pan: downPan + rn(-0.04, 0.04) }); 911 + sound.synth({ type: "noise", tone: 4500, duration: rj(0.15, 0.22), volume: rj(0.20, 0.18) * v, attack: 0.055, decay: 0.145, pan: downPan + rn(-0.04, 0.04) }); 882 912 break; 913 + } 883 914 } 884 915 } 885 916
+79 -10
fedac/native/src/ac-native.c
··· 1297 1297 // Reformat. Retry once if mkfs fails — the kernel sometimes 1298 1298 // needs a second pass after a fresh GPT to release exclusive 1299 1299 // holds on the block device. 1300 + // 1301 + // Capture mkfs stderr to a dedicated small file so we can 1302 + // read it back and ac_log the error inline. The main DLOG 1303 + // accumulates everything but may not survive to the USB; 1304 + // this per-attempt capture ensures the failing error 1305 + // message always makes it into ac-native.log. 1306 + const char *MKFS_ERR = "/tmp/mkfs-err.log"; 1300 1307 snprintf(rcmd, sizeof(rcmd), 1301 - "echo '--- mkfs attempt 1 ---' >> %s; mkfs.vfat -F 32 -n AC-NATIVE %s >> %s 2>&1", 1302 - DLOG, devpath, DLOG); 1308 + "echo '--- mkfs attempt 1 ---' >> %s; " 1309 + "mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; " 1310 + "cat %s >> %s", 1311 + DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1303 1312 rrc = system(rcmd); 1304 - ac_log("[install] mkfs rc=%d attempt=1\n", rrc); 1313 + ac_log("[install] mkfs rc=%d attempt=1 (WIFEXITED=%d status=%d)\n", 1314 + rrc, WIFEXITED(rrc), WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1); 1315 + // Inline dump mkfs output so we see the exact error 1316 + { 1317 + FILE *mf = fopen(MKFS_ERR, "r"); 1318 + if (mf) { 1319 + char line[512]; 1320 + while (fgets(line, sizeof(line), mf)) { 1321 + size_t len = strlen(line); 1322 + if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1323 + ac_log("[mkfs/1] %s\n", line); 1324 + } 1325 + fclose(mf); 1326 + } 1327 + } 1305 1328 if (rrc != 0) { 1306 1329 // Retry after giving the block layer a moment to settle 1307 1330 sync(); ··· 1309 1332 snprintf(rcmd, sizeof(rcmd), 1310 1333 "echo '--- mkfs attempt 2 ---' >> %s; " 1311 1334 "fuser -k %s >> %s 2>&1 || true; " 1312 - "mkfs.vfat -F 32 -n AC-NATIVE %s >> %s 2>&1", 1313 - DLOG, devpath, DLOG, devpath, DLOG); 1335 + "mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; " 1336 + "cat %s >> %s", 1337 + DLOG, devpath, DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1314 1338 rrc = system(rcmd); 1315 - ac_log("[install] mkfs rc=%d attempt=2\n", rrc); 1339 + ac_log("[install] mkfs rc=%d attempt=2 (WIFEXITED=%d status=%d)\n", 1340 + rrc, WIFEXITED(rrc), WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1); 1341 + // Inline dump retry output 1342 + { 1343 + FILE *mf = fopen(MKFS_ERR, "r"); 1344 + if (mf) { 1345 + char line[512]; 1346 + while (fgets(line, sizeof(line), mf)) { 1347 + size_t len = strlen(line); 1348 + if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1349 + ac_log("[mkfs/2] %s\n", line); 1350 + } 1351 + fclose(mf); 1352 + } 1353 + } 1316 1354 } 1317 1355 if (rrc != 0) { 1318 1356 snprintf(install_fail_reason, sizeof(install_fail_reason), ··· 1461 1499 } 1462 1500 if (source_mounted_tmp) umount("/tmp/src"); 1463 1501 1464 - // Copy the tmpfs install-debug.log back to /mnt so it survives the boot 1465 - // session (tmpfs is wiped on reboot). Best effort — if /mnt got trashed 1466 - // by the repartition attempt we simply skip. The log is most useful on 1467 - // failure but we copy on success too so "what worked" is visible. 1502 + // Dump the full install-debug.log inline to ac_log so the trace ALWAYS 1503 + // lands in ac-native.log (which we know reliably survives to the USB). 1504 + // This is the primary diagnostic channel — the parallel copy to 1505 + // /mnt/install-debug.log below is a best-effort secondary that may fail 1506 + // silently if /mnt got trashed by the repartition attempt. 1507 + { 1508 + FILE *src = fopen("/tmp/install-debug.log", "r"); 1509 + if (src) { 1510 + ac_log("[install] === /tmp/install-debug.log BEGIN ===\n"); 1511 + char line[1024]; 1512 + int line_count = 0; 1513 + while (fgets(line, sizeof(line), src)) { 1514 + // Strip trailing newline for consistent ac_log formatting 1515 + size_t len = strlen(line); 1516 + if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1517 + ac_log("[install-debug] %s\n", line); 1518 + line_count++; 1519 + if (line_count > 500) { 1520 + ac_log("[install] ... (truncated at 500 lines)\n"); 1521 + break; 1522 + } 1523 + } 1524 + ac_log("[install] === /tmp/install-debug.log END (%d lines) ===\n", line_count); 1525 + fclose(src); 1526 + } else { 1527 + ac_log("[install] /tmp/install-debug.log not present (errno=%d)\n", errno); 1528 + } 1529 + } 1530 + 1531 + // Parallel copy of the tmpfs log to /mnt (best effort). Even if the 1532 + // inline dump above succeeded, keep this so the file is also visible 1533 + // as a standalone artifact when /mnt is intact. 1468 1534 { 1469 1535 FILE *src = fopen("/tmp/install-debug.log", "rb"); 1470 1536 if (src) { ··· 1477 1543 fsync(fileno(dst)); 1478 1544 fclose(dst); 1479 1545 ac_log("[install] copied /tmp/install-debug.log → /mnt/install-debug.log\n"); 1546 + } else { 1547 + ac_log("[install] /mnt/install-debug.log copy failed (errno=%d: %s)\n", 1548 + errno, strerror(errno)); 1480 1549 } 1481 1550 fclose(src); 1482 1551 }
+37 -11
fedac/native/src/audio.c
··· 423 423 pthread_mutex_lock(&audio->lock); 424 424 425 425 for (unsigned int i = 0; i < period_frames; i++) { 426 - double mix_l = 0.0, mix_r = 0.0; 427 - double voice_sum = 0.0; // Total voice weight for auto-mix 426 + // Split the voice bus in two: TONES get auto-mix normalization 427 + // (divide by total voice weight so held chords stay balanced), 428 + // DRUMS stack additively (so a kick+snare+hat transient sums to 429 + // a louder peak instead of ducking itself). soft_clip at the end 430 + // catches any drum peak excess with tanh saturation — which 431 + // gives percussion a natural analog "push" character. 432 + // 433 + // Heuristic: a voice is percussive if it has a SHORT FINITE 434 + // duration (< 0.5s). Held tones (duration = Infinity) and 435 + // long one-shot tones always go through the auto-mix bus. 436 + double tone_l = 0.0, tone_r = 0.0; 437 + double drum_l = 0.0, drum_r = 0.0; 438 + double voice_sum = 0.0; // Tone-only voice weight for auto-mix 428 439 429 440 for (int v = 0; v < AUDIO_MAX_VOICES; v++) { 430 441 ACVoice *voice = &audio->voices[v]; ··· 437 448 438 449 double left_gain = (1.0 - voice->pan) * 0.5; 439 450 double right_gain = (1.0 + voice->pan) * 0.5; 440 - mix_l += amp * left_gain; 441 - mix_r += amp * right_gain; 442 451 443 - // Track voice weight for smooth auto-mix 444 - if (voice->state == VOICE_KILLING) { 445 - voice_sum += voice->volume * (1.0 - voice->fade_elapsed / voice->fade_duration); 452 + int is_percussive = !isinf(voice->duration) && voice->duration < 0.5; 453 + if (is_percussive) { 454 + // Drum bus — no auto-mix normalization. Drums stack 455 + // additively and rely on soft_clip for peak control. 456 + drum_l += amp * left_gain; 457 + drum_r += amp * right_gain; 446 458 } else { 447 - voice_sum += voice->volume; 459 + // Tone bus — contributes to voice_sum for auto-mix. 460 + tone_l += amp * left_gain; 461 + tone_r += amp * right_gain; 462 + if (voice->state == VOICE_KILLING) { 463 + voice_sum += voice->volume * (1.0 - voice->fade_elapsed / voice->fade_duration); 464 + } else { 465 + voice_sum += voice->volume; 466 + } 448 467 } 449 468 450 469 voice->elapsed += dt; ··· 457 476 } 458 477 } 459 478 460 - // Smooth auto-mix divisor — fast attack, slow release 479 + // Smooth auto-mix divisor — fast attack, slow release. 480 + // Applied ONLY to the tone bus. Drums bypass it entirely. 461 481 double target = voice_sum > 1.0 ? voice_sum : 1.0; 462 482 if (mix_divisor < target) 463 483 mix_divisor += (target - mix_divisor) * mix_att_coeff; ··· 465 485 mix_divisor += (target - mix_divisor) * mix_rel_coeff; 466 486 if (mix_divisor < 1.0) mix_divisor = 1.0; 467 487 468 - mix_l /= mix_divisor; 469 - mix_r /= mix_divisor; 488 + tone_l /= mix_divisor; 489 + tone_r /= mix_divisor; 490 + 491 + // Merge the two buses. Drums land at full amplitude (possibly 492 + // pushing above 1.0 on a heavy hit); the final soft_clip tanh 493 + // handles the peak without harsh clipping. 494 + double mix_l = tone_l + drum_l; 495 + double mix_r = tone_r + drum_r; 470 496 471 497 // Mix sample voices (pitch-shifted playback) 472 498 // Lock already held from line 246 — safe to read sample_buf