Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix(perc): drum bus compressor + tighter kick/snare + full pad labels + install fix

Multiple fixes based on user feedback testing ivory-flounder:

1. **Drum bus peak compressor (audio.c)** — user reported rapid kick
hits felt like they were "stacking quieter". Diagnosed as soft_clip
tanh saturation: the drum bus sums additively (no auto-mix divide
since split), so 4 rapid kicks overlap their sub tails and the
combined signal saturates through tanh, flattening each additional
hit. Fix: add a real peak compressor on the drum bus with:
- 0.95 threshold
- 5ms attack (slower than 2ms beater transient so each hit's
initial peak passes through at full amplitude)
- 200ms release (recovers quickly enough that successive hits at
120-200 BPM each get full dynamic range)
Polyphonic stacking preserved, no saturation pumping.

2. **Kick tightening** — user reported "farty" feel. Previous recipe
had 6 layers summing huge low-freq energy:
180+150+100+80+50 Hz sines at volumes 1.4/2.0/1.4/1.4/1.9
Which stacked into a muddy blob. New recipe:
- Stick 2500Hz × 2.5ms vol 0.5 (sharp attack)
- Pitch snap 200Hz × 12ms vol 1.1
- Body 150Hz × 45ms vol 1.3 (was 100ms @ 2.0 — shorter, quieter)
- Mid-low 90Hz × 80ms vol 0.85 (was 100Hz × 160ms @ 1.4)
- Sub 55Hz × 350ms vol 1.0 (was 50Hz × 550ms @ 1.9)
Dropped one layer entirely (80Hz triangle), halved volumes on
low-mids, halved durations on the body. Transient dominant now.

3. **Snare 909-leaning** — user said "farty". The 808 recipe exposed
the 238/476 Hz tonal pair for 120ms which read as a "bleh" tone.
New 909-leaning version:
- Sharp stick 3500Hz × 4ms vol 0.95 (louder, shorter)
- 238/476Hz tonal pair × 30ms (was 120ms) vol 0.35/0.28 (was 0.55/0.45)
- Primary wire noise 3500Hz × 110ms vol 0.85 (NEW dominant layer)
- Mid noise 1800Hz × 70ms vol 0.38
- Triangle body 180Hz × 25ms vol 0.22 (brief warmth)
Noise now dominates over tones, reads as a CRACK not a BLEH.

4. **Drum pad labels show full names when space allows (notepat.mjs)**
— user: "across the board in notepat no need to truncate terms
and make short versions". Previously always showed 3-char labels
(BAS/SNR/CLP/HHC). Now uses PERCUSSION_NAMES (kick/snare/clap/hat-c)
when btnW >= label width + padding. Falls back to 3-char only when
the pad is too narrow to fit the full word.

5. **INSTALL FIX UNCHANGED — CRITICAL bundled from last commit**:
mkfs rc detection (subshell preserves exit code), dd zero of first
64KB before sfdisk to wipe old FS signature, drop_caches +
blockdev-ish flushbufs before mkfs, 3s settle, 3 retry attempts
with drop_caches between each. These are all still in ac-native.c.

Cancelled in-flight satin-toucan (was at 55% kernel) to bundle these
fixes into one build cycle. Net savings: one flash cycle.

+90 -40
+50 -37
fedac/native/pieces/notepat.mjs
··· 737 737 // not a held note. No sustain voices, no release transition. Hold duration 738 738 // doesn't affect them. Applies to: kick, snare, clap, snap, closed hat. 739 739 740 - case "c": { // kick — TR-808 mastered for laptop speakers + sub for headphones 740 + case "c": { // kick — tight TR-808: transient + short body + optional sub 741 741 const downPan = pan + rn(-0.02, 0.02); 742 - // Mastering note: laptop speakers typically roll off below ~150 Hz, 743 - // so the 50 Hz sub is inaudible on them. Push the 150-250 Hz range 744 - // hard so the kick reads as PUNCH on both laptops and headphones. 745 - // The 50 Hz sub adds "boom" on real speakers but laptops hear the 746 - // mids as the "kick" character. 742 + // Design: emphasize the TRANSIENT peak. Previous version stacked six 743 + // loud low-freq sines with long decays which summed into a muddy 744 + // "farty" blob on the drum bus (no auto-normalize). New version has 745 + // ONE attack, ONE body, ONE sub — each with sharp envelopes. 747 746 // 748 - // REMOVED: 4kHz noise beater click that sounded like a digital 749 - // pop on laptop speakers. Replaced with a softer 2.5kHz noise 750 - // attack that blends into the body instead of cracking. 747 + // Layer budget: stick click + pitch snap + body + sub = 4 voices. 748 + // The body layer carries most of the perceived "kick" on laptops; 749 + // the sub adds depth on real speakers without bleeding into body. 751 750 752 - // Soft stick-like attack (not a "click") 753 - sound.synth({ type: "noise", tone: 2500, duration: 0.003, volume: rj(0.35, 0.12) * v, attack: 0.0005, decay: 0.0025, pan: downPan }); 754 - // Pitch-snap envelope: 180 Hz → decays in ~12ms. This is where 755 - // the perceived "thump" starts for laptop speakers. 756 - sound.synth({ type: "sine", tone: 180 * pf, duration: 0.018, volume: rj(1.4, 0.08) * v, attack: 0.001, decay: 0.017, pan: downPan }); 757 - // Main body — 150 Hz sine, the PRIMARY laptop-audible frequency. 758 - // Boosted to 2.0 (was 1.6 @ 80Hz) so it actually pushes the speaker. 759 - sound.synth({ type: "sine", tone: 150 * pf, duration: 0.10, volume: rj(2.0, 0.08) * v, attack: 0.002, decay: 0.095, pan: downPan }); 760 - // Mid-low fill at 100 Hz — bridge between body and sub 761 - sound.synth({ type: "sine", tone: 100 * pf, duration: 0.16, volume: rj(1.4, 0.10) * v, attack: 0.003, decay: 0.155, pan: downPan }); 762 - // Triangle "beef" layer at 80 Hz — adds harmonics the speaker CAN 763 - // reproduce (fundamental is out of range but 2nd/3rd harmonics land 764 - // at 160/240 Hz which laptop speakers handle fine) 765 - sound.synth({ type: "triangle", tone: 80 * pf, duration: 0.22, volume: rj(1.4, 0.10) * v, attack: 0.003, decay: 0.215, pan: downPan }); 766 - // The classic 808 sub — 50 Hz sine, long decay. Inaudible on 767 - // laptops but adds the "body" on headphones/subwoofers. 768 - 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 }); 751 + // 1. Stick click — very short noise transient at 2.5kHz for the 752 + // beater attack. This is the CUT that makes the kick audible. 753 + sound.synth({ type: "noise", tone: 2500, duration: 0.0025, volume: rj(0.50, 0.12) * v, attack: 0.0002, decay: 0.0022, pan: downPan }); 754 + // 2. Pitch snap — 200Hz→150Hz perceived sweep via overlapping sines 755 + // Both very short so they read as a single downward "thump" 756 + sound.synth({ type: "sine", tone: 200 * pf, duration: 0.012, volume: rj(1.1, 0.10) * v, attack: 0.0005, decay: 0.011, pan: downPan }); 757 + // 3. Body — 150Hz sine, SHORT decay (40ms). Single tone, not a drone. 758 + // This is the laptop-audible "punch" frequency. 759 + sound.synth({ type: "sine", tone: 150 * pf, duration: 0.045, volume: rj(1.3, 0.10) * v, attack: 0.001, decay: 0.044, pan: downPan }); 760 + // 4. Mid-low body — 90Hz sine, medium-short decay. Adds weight 761 + // without muddying. 762 + sound.synth({ type: "sine", tone: 90 * pf, duration: 0.080, volume: rj(0.85, 0.12) * v, attack: 0.002, decay: 0.078, pan: downPan }); 763 + // 5. Sub tail — 55Hz sine, long decay. Inaudible on laptops, adds 764 + // body on headphones/subwoofers. Volume REDUCED from 1.9 to 1.0 765 + // so it doesn't dominate the transient. 766 + sound.synth({ type: "sine", tone: 55 * pf, duration: rj(0.35, 0.20), volume: rj(1.0, 0.12) * v, attack: 0.003, decay: 0.345, pan: downPan }); 769 767 break; 770 768 } 771 769 772 - case "d": { // snare — TR-808: stick click + tonal pair + wire rattle (one-shot) 770 + case "d": { // snare — TR-909-leaning: big transient + short tone + dominant noise 773 771 const downPan = pan + rn(-0.02, 0.02); 774 - // Sharp stick-on-head noise transient 775 - sound.synth({ type: "noise", tone: 3000, duration: 0.006, volume: rj(0.80, 0.12) * v, attack: 0.0001, decay: 0.006, pan: downPan }); 776 - // 808 tonal pair — 238/476 Hz 777 - 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 }); 778 - 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 }); 779 - // Bright wire noise 780 - 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) }); 781 - // Lower noise body 782 - 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) }); 772 + // Design: previous snare had 808-style LONG tonal pair (120ms) which 773 + // exposed the sines as a "bleh" sound — too tonal, not enough snap. 774 + // New version leans 909: sharper crack, SHORT tonal pair, noise- 775 + // dominant. The 238/476 Hz pair is still there for metallic beat 776 + // character but only lasts ~30ms before the noise takes over. 777 + 778 + // 1. Sharp stick crack — higher freq, shorter, LOUDER than before 779 + sound.synth({ type: "noise", tone: 3500, duration: 0.004, volume: rj(0.95, 0.10) * v, attack: 0.0001, decay: 0.004, pan: downPan }); 780 + // 2. 808 tonal pair — SHORT decay (30ms was 120ms), QUIETER so noise dominates 781 + sound.synth({ type: "sine", tone: 238 * pf, duration: 0.030, volume: rj(0.35, 0.12) * v, attack: 0.0003, decay: 0.029, pan: downPan }); 782 + sound.synth({ type: "sine", tone: 476 * pf, duration: 0.030, volume: rj(0.28, 0.12) * v, attack: 0.0003, decay: 0.029, pan: downPan }); 783 + // 3. Primary wire noise — DOMINANT layer, bright, medium-short decay 784 + sound.synth({ type: "noise", tone: 3500, duration: rj(0.11, 0.20), volume: rj(0.85, 0.10) * v, attack: 0.0005, decay: 0.108, pan: downPan + rn(-0.04, 0.04) }); 785 + // 4. Mid wire noise — adds weight without muddiness 786 + sound.synth({ type: "noise", tone: 1800, duration: rj(0.07, 0.20), volume: rj(0.38, 0.15) * v, attack: 0.0008, decay: 0.068, pan: downPan + rn(-0.04, 0.04) }); 787 + // 5. Triangle body fundamental — adds warmth, very short 788 + sound.synth({ type: "triangle", tone: 180 * pf, duration: 0.025, volume: rj(0.22, 0.15) * v, attack: 0.001, decay: 0.024, pan: downPan }); 783 789 break; 784 790 } 785 791 ··· 3264 3270 } 3265 3271 3266 3272 // In drum mode the bottom label is the drum name, not the note. 3273 + // Prefer the full name ("kick", "snare", "splash") when the pad 3274 + // has space; fall back to the 3-char abbreviation when tight. 3275 + // font_1 is 6px per char, so "kick"=26px, "snare"=32px, "splash"=38px, 3276 + // "cowbell"=44px. Pick full name when btnW >= label width + padding. 3267 3277 if (drumActive && btnH > 12) { 3268 3278 if (isActive) ink(255, 255, 255, 210); 3269 3279 else { const sl = dark ? (sharp ? 100 : 150) : (sharp ? 120 : 80); ink(sl, sl, sl); } 3270 - const bottomLabel = PERCUSSION_LABELS[letter] || (letter + noteOctave); 3280 + const fullName = PERCUSSION_NAMES[letter] || ""; 3281 + const shortLabel = PERCUSSION_LABELS[letter] || (letter + noteOctave); 3282 + const fullPx = fullName.length * 6 + 4; 3283 + const bottomLabel = (fullName && btnW >= fullPx) ? fullName : shortLabel; 3271 3284 write(bottomLabel, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); 3272 3285 } 3273 3286
+40 -3
fedac/native/src/audio.c
··· 412 412 const double mix_att_coeff = 1.0 - exp(-1.0 / (0.004 * rate)); // ~4ms 413 413 const double mix_rel_coeff = 1.0 - exp(-1.0 / (0.060 * rate)); // ~60ms 414 414 415 + // Drum bus peak compressor — gives percussion proper "stacking" feel. 416 + // The drum bus sums additively (no auto-mix divide) so rapid hits 417 + // would otherwise saturate through soft_clip tanh, flattening peaks 418 + // and making each new hit sound QUIETER. A real peak compressor 419 + // with fast attack / slower release keeps the drum bus below ~0.95 420 + // so transients retain impact AND the compressor recovers between 421 + // hits so each kick/snare feels punchy on its own. 422 + double drum_gain = 1.0; 423 + const double DRUM_THRESH = 0.95; 424 + // 5ms attack — slower than a 2ms beater transient so the first peak 425 + // of each hit passes through at full amplitude before compression 426 + // engages. This preserves the "snap" of each individual kick/snare. 427 + const double drum_att_coeff = 1.0 - exp(-1.0 / (0.005 * rate)); 428 + // 200ms release — recovers quickly enough that successive hits at 429 + // typical tempos (120-200 BPM, 300-500ms between hits) each get 430 + // the benefit of full dynamic range. 431 + const double drum_rel_coeff = 1.0 - exp(-1.0 / (0.200 * rate)); 432 + 415 433 // Set real-time priority to prevent audio glitches from background tasks 416 434 struct sched_param sp = { .sched_priority = 50 }; 417 435 if (pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) != 0) ··· 488 506 tone_l /= mix_divisor; 489 507 tone_r /= mix_divisor; 490 508 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. 509 + // Drum bus peak compressor: detect peak, attack fast if over 510 + // threshold, release slow. Unlike the tone auto-mix divide, 511 + // this preserves individual hit dynamics — a single drum hit 512 + // passes through at full amplitude, but sustained buildup 513 + // from overlapping hits gets gain-reduced gracefully so they 514 + // stack linearly instead of saturating through soft_clip. 515 + { 516 + double peak = fabs(drum_l); 517 + double peak_r = fabs(drum_r); 518 + if (peak_r > peak) peak = peak_r; 519 + double target = (peak > DRUM_THRESH) ? (DRUM_THRESH / peak) : 1.0; 520 + if (target < drum_gain) { 521 + drum_gain += (target - drum_gain) * drum_att_coeff; 522 + } else { 523 + drum_gain += (target - drum_gain) * drum_rel_coeff; 524 + } 525 + drum_l *= drum_gain; 526 + drum_r *= drum_gain; 527 + } 528 + 529 + // Merge the two buses. Drums land compressed to ~0.95 peak 530 + // so they retain impact without saturating the final output. 494 531 double mix_l = tone_l + drum_l; 495 532 double mix_r = tone_r + drum_r; 496 533