Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

native: attack/decay modes, per-side octave/volume arrows, install fix

notepat.mjs:
- , and . keys cycle attack/decay through zero/short/long modes
- Left/right arrows adjust per-grid octave offset
- Up/down arrows adjust per-grid volume (last selected side)
- On-screen envelope/arrow indicators with shape icons
- Help panel updated with new bindings

ac-native.c:
- Use BLKRRPART ioctl as primary partition refresh (no partx needed)
- dd fallback when wipefs missing from initramfs
- Fixes install-to-HD failure on machines where partx/wipefs
weren't bundled at build time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+172 -48
+149 -35
fedac/native/pieces/notepat.mjs
··· 557 557 // the status bar when there's space. 558 558 let lastKeyPressed = ""; 559 559 let lastKeyFrame = 0; 560 + 561 + // Attack / Decay envelope modes — cycled with , and . keys 562 + // Each has 3 states: "zero" (instant), "short", "long" 563 + const ATTACK_MODES = ["zero", "short", "long"]; 564 + const DECAY_MODES = ["zero", "short", "long"]; 565 + let attackModeIdx = 1; // default: short 566 + let decayModeIdx = 1; // default: short 567 + const ATTACK_VALUES = { zero: 0.001, short: 0.005, long: 0.06 }; 568 + const DECAY_VALUES = { zero: 0.01, short: 0.1, long: 0.9 }; 569 + let envelopeNotice = null; // { text, icon, until } — transient overlay 570 + 571 + // Arrow-key octave offsets for left and right grids (added to base `octave`) 572 + let leftOctaveOffset = 0; // left grid plays at octave + leftOctaveOffset 573 + let rightOctaveOffset = 1; // right grid plays at octave + rightOctaveOffset (default +1) 574 + let arrowSelectedSide = 0; // 0 = left, 1 = right (for up/down volume) 560 575 let reversePhaseStartMs = Date.now(); 561 576 let reversePlaybackSound = null; 562 577 ··· 1192 1207 percussionNotice = { text, until: frame + 120 }; // ~2 seconds at 60fps 1193 1208 } 1194 1209 1210 + function currentAttack() { return ATTACK_VALUES[ATTACK_MODES[attackModeIdx]]; } 1211 + function currentDecay() { return DECAY_VALUES[DECAY_MODES[decayModeIdx]]; } 1212 + 1213 + function flashEnvelopeNotice(param, mode) { 1214 + // param: "attack" or "decay", mode: "zero"/"short"/"long" 1215 + const icons = { zero: "·", short: "/", long: "~" }; 1216 + const labels = { attack: "ATK", decay: "DEC" }; 1217 + const text = `${labels[param]} ${icons[mode]} ${mode}`; 1218 + envelopeNotice = { text, param, mode, until: frame + 90 }; // ~1.5s 1219 + } 1220 + 1221 + function flashArrowNotice(text) { 1222 + envelopeNotice = { text, param: "arrow", mode: "", until: frame + 90 }; 1223 + } 1224 + 1195 1225 function noteToFreq(note, oct) { 1196 1226 const idx = CHROMATIC.indexOf(note); 1197 1227 if (idx < 0) return 440; ··· 1338 1368 // Hit-test a touch point against the note grid 1339 1369 function hitTestGrid(x, y, gi) { 1340 1370 const grids = [ 1341 - { grid: LEFT_GRID, startX: gi.leftX, octOffset: 0 }, 1342 - { grid: RIGHT_GRID, startX: gi.rightX, octOffset: 0 }, 1371 + { grid: LEFT_GRID, startX: gi.leftX, sideOffset: leftOctaveOffset }, 1372 + { grid: RIGHT_GRID, startX: gi.rightX, sideOffset: rightOctaveOffset }, 1343 1373 ]; 1344 - for (const { grid, startX, octOffset } of grids) { 1374 + for (const { grid, startX, sideOffset } of grids) { 1345 1375 for (let r = 0; r < 3; r++) { 1346 1376 for (let c = 0; c < 4; c++) { 1347 1377 const bx = startX + c * (gi.btnW + gi.gap); ··· 1350 1380 const noteName = grid[r][c]; 1351 1381 const key = NOTE_TO_KEY[noteName]; 1352 1382 const [letter, off] = parseNote(noteName); 1353 - return { key, letter, octave: octave + off + octOffset, gridOffset: off }; 1383 + return { key, letter, octave: octave + sideOffset, gridOffset: off }; 1354 1384 } 1355 1385 } 1356 1386 } ··· 1582 1612 } 1583 1613 return; 1584 1614 } 1585 - if (key === ",") { djTapTempo(); return; } 1615 + if (key === ",") { 1616 + attackModeIdx = (attackModeIdx + 1) % ATTACK_MODES.length; 1617 + flashEnvelopeNotice("attack", ATTACK_MODES[attackModeIdx]); 1618 + return; 1619 + } 1620 + if (key === ".") { 1621 + decayModeIdx = (decayModeIdx + 1) % DECAY_MODES.length; 1622 + flashEnvelopeNotice("decay", DECAY_MODES[decayModeIdx]); 1623 + return; 1624 + } 1586 1625 1587 1626 if (key === "escape" && activeScreen === "wifi") { activeScreen = "notepat"; return; } 1588 1627 if (key === "escape" && activeScreen === "os") { activeScreen = "notepat"; osState = "idle"; return; } ··· 1731 1770 return; 1732 1771 } 1733 1772 if (key >= "1" && key <= "9") { octave = parseInt(key); return; } 1734 - if (key === "arrowup") { octave = Math.min(9, octave + 1); return; } 1735 - if (key === "arrowdown") { octave = Math.max(1, octave - 1); return; } 1773 + if (key === "arrowleft") { 1774 + arrowSelectedSide = 0; 1775 + leftOctaveOffset = leftOctaveOffset === 0 ? -1 : leftOctaveOffset === -1 ? 1 : 0; 1776 + flashArrowNotice(`L oct ${octave + leftOctaveOffset}`); 1777 + return; 1778 + } 1779 + if (key === "arrowright") { 1780 + arrowSelectedSide = 1; 1781 + rightOctaveOffset = rightOctaveOffset === 1 ? 0 : rightOctaveOffset === 0 ? 2 : 1; 1782 + flashArrowNotice(`R oct ${octave + rightOctaveOffset}`); 1783 + return; 1784 + } 1785 + if (key === "arrowup") { 1786 + if (arrowSelectedSide === 0) { 1787 + leftMasterVol = Math.min(1.5, leftMasterVol + 0.1); 1788 + flashArrowNotice(`L vol ${Math.round(leftMasterVol * 100)}%`); 1789 + } else { 1790 + rightMasterVol = Math.min(1.5, rightMasterVol + 0.1); 1791 + flashArrowNotice(`R vol ${Math.round(rightMasterVol * 100)}%`); 1792 + } 1793 + return; 1794 + } 1795 + if (key === "arrowdown") { 1796 + if (arrowSelectedSide === 0) { 1797 + leftMasterVol = Math.max(0, leftMasterVol - 0.1); 1798 + flashArrowNotice(`L vol ${Math.round(leftMasterVol * 100)}%`); 1799 + } else { 1800 + rightMasterVol = Math.max(0, rightMasterVol - 0.1); 1801 + flashArrowNotice(`R vol ${Math.round(rightMasterVol * 100)}%`); 1802 + } 1803 + return; 1804 + } 1736 1805 // Home key: hold to record GLOBAL sample 1737 1806 if (key === "home" && wave === "sample" && !recording && !perKeyRecording) { 1738 1807 const ok = !!sound?.microphone?.rec?.(); ··· 1760 1829 sound.synth({ type: "noise", tone: 200, duration: 0.1, volume: 0.15, attack: 0.001, decay: 0.08 }); 1761 1830 return; 1762 1831 } 1763 - if (key === "arrowleft") { 1764 - const idx = (waveIndex - 1 + wavetypes.length) % wavetypes.length; 1765 - setWave(wavetypes[idx], sound); 1766 - return; 1767 - } 1768 - if (key === "arrowright") { 1769 - const idx = (waveIndex + 1) % wavetypes.length; 1770 - setWave(wavetypes[idx], sound); 1771 - return; 1772 - } 1832 + // Arrow left/right handled above (octave per side) 1773 1833 if (key === "\\") { 1774 1834 trackpadFX = !trackpadFX; 1775 1835 sound.synth({ ··· 1807 1867 if (perKeyRecording === key) return; 1808 1868 if (noteName && !sounds[key]) { 1809 1869 const [letter, offset] = parseNote(noteName); 1810 - const noteOctave = octave + offset; 1870 + // Map parseNote offset to per-side octave: offset<=0 = left grid, offset>=1 = right grid 1871 + const sideOctaveAdj = offset >= 1 ? rightOctaveOffset : leftOctaveOffset; 1872 + const noteOctave = octave + sideOctaveAdj + (offset >= 2 ? offset - 1 : offset <= -1 ? offset : 0); 1811 1873 const freq = noteToFreq(letter, noteOctave); 1812 1874 const semitones = (noteOctave - 4) * 12 + CHROMATIC.indexOf(letter); 1813 1875 const pan = Math.max(-0.8, Math.min(0.8, (semitones - 12) / 15)); ··· 1889 1951 } else { 1890 1952 const synth = sound.synth({ 1891 1953 type: "sine", tone: playFreq, duration: Infinity, 1892 - volume: vol, attack: quickMode ? 0.002 : 0.005, decay: 0.1, pan, 1954 + volume: vol, attack: quickMode ? 0.002 : currentAttack(), decay: currentDecay(), pan, 1893 1955 }); 1894 1956 rememberSound(key, { synth, note: letter, octave: noteOctave, baseFreq: freq, gridOffset: offset, baseVol }, system, velocity); 1895 1957 } ··· 1897 1959 const synth = sound.synth({ 1898 1960 type: wave, tone: playFreq, 1899 1961 duration: Infinity, 1900 - volume: vol, attack: quickMode ? 0.002 : 0.005, 1901 - decay: 0.1, pan: pan, 1962 + volume: vol, attack: quickMode ? 0.002 : currentAttack(), 1963 + decay: currentDecay(), pan: pan, 1902 1964 }); 1903 1965 rememberSound(key, { synth, note: letter, octave: noteOctave, baseFreq: freq, gridOffset: offset, baseVol }, system, velocity); 1904 1966 } ··· 2212 2274 } else if (wave === "composite") { 2213 2275 // Rich layered pad: 5 detuned oscillators 2214 2276 const detune = () => Math.floor(Math.random() * 13) - 6; 2215 - const a = sound.synth({ type: "sine", tone: playFreq, duration: Infinity, volume: 0.5, attack: 0.003, decay: 0.9, pan }); 2216 - const b = sound.synth({ type: "sine", tone: playFreq + 9 + detune(), duration: Infinity, volume: 0.17, attack: 0.003, decay: 0.9, pan }); 2217 - const c = sound.synth({ type: "sawtooth", tone: playFreq + detune(), duration: 0.15 + Math.random() * 0.05, volume: 0.01, attack: 0.005, decay: 0.1, pan }); 2218 - const d = sound.synth({ type: "triangle", tone: playFreq + 8 + detune(), duration: Infinity, volume: 0.016, attack: 0.999, decay: 0.9, pan }); 2219 - const e2 = sound.synth({ type: "square", tone: playFreq + detune(), duration: Infinity, volume: 0.008, attack: 0.05, decay: 0.9, pan }); 2277 + const atk = currentAttack(), dcy = currentDecay(); 2278 + const a = sound.synth({ type: "sine", tone: playFreq, duration: Infinity, volume: 0.5, attack: atk, decay: dcy, pan }); 2279 + const b = sound.synth({ type: "sine", tone: playFreq + 9 + detune(), duration: Infinity, volume: 0.17, attack: atk, decay: dcy, pan }); 2280 + const c = sound.synth({ type: "sawtooth", tone: playFreq + detune(), duration: 0.15 + Math.random() * 0.05, volume: 0.01, attack: atk * 1.5, decay: dcy * 0.2, pan }); 2281 + const d = sound.synth({ type: "triangle", tone: playFreq + 8 + detune(), duration: Infinity, volume: 0.016, attack: Math.min(0.999, atk * 20), decay: dcy, pan }); 2282 + const e2 = sound.synth({ type: "square", tone: playFreq + detune(), duration: Infinity, volume: 0.008, attack: atk * 10, decay: dcy, pan }); 2220 2283 synth = a; // primary handle for kill 2221 2284 rememberSound(hitNote.key, { 2222 2285 synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq, ··· 2225 2288 } else { 2226 2289 synth = sound.synth({ 2227 2290 type: wave, tone: playFreq, duration: Infinity, 2228 - volume: 0.5, attack: 0.005, decay: 0.1, pan, 2291 + volume: 0.5, attack: currentAttack(), decay: currentDecay(), pan, 2229 2292 }); 2230 2293 rememberSound(hitNote.key, { synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq }, system, 1); 2231 2294 } 2232 2295 if (!sounds[hitNote.key]) { 2233 2296 synth = sound.synth({ 2234 2297 type: "sine", tone: playFreq, duration: Infinity, 2235 - volume: 0.5, attack: 0.005, decay: 0.1, pan, 2298 + volume: 0.5, attack: currentAttack(), decay: currentDecay(), pan, 2236 2299 }); 2237 2300 rememberSound(hitNote.key, { synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq }, system, 1); 2238 2301 } ··· 2340 2403 } else { 2341 2404 synth = sound.synth({ 2342 2405 type: wave, tone: playFreq, 2343 - duration: Infinity, volume: 0.5, attack: 0.005, decay: 0.1, pan, 2406 + duration: Infinity, volume: 0.5, attack: currentAttack(), decay: currentDecay(), pan, 2344 2407 }); 2345 2408 rememberSound(hitNote.key, { synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq }, system, 1); 2346 2409 } 2347 2410 if (!sounds[hitNote.key]) { 2348 2411 synth = sound.synth({ 2349 2412 type: "sine", tone: playFreq, 2350 - duration: Infinity, volume: 0.5, attack: 0.005, decay: 0.1, pan, 2413 + duration: Infinity, volume: 0.5, attack: currentAttack(), decay: currentDecay(), pan, 2351 2414 }); 2352 2415 rememberSound(hitNote.key, { synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq }, system, 1); 2353 2416 } ··· 3404 3467 for (let c = 0; c < cols; c++) { 3405 3468 const noteName = grid[r][c]; 3406 3469 const [letter, off] = parseNote(noteName); 3407 - const noteOctave = octave + off + octOffset; 3470 + // octOffset is the per-side offset (leftOctaveOffset or rightOctaveOffset) 3471 + // off from parseNote: 0 for left grid notes, 1 for right grid "+" notes 3472 + // Use octOffset directly as the side's octave shift 3473 + const noteOctave = octave + octOffset; 3408 3474 const key = NOTE_TO_KEY[noteName]; 3409 3475 const isActive = key && sounds[key] !== undefined; 3410 3476 const trailInfo = key && trail[key]; ··· 3570 3636 } 3571 3637 } 3572 3638 3573 - drawGrid(LEFT_GRID, leftX, 0, "left"); 3574 - drawGrid(RIGHT_GRID, rightX, 0, "right"); 3639 + drawGrid(LEFT_GRID, leftX, leftOctaveOffset, "left"); 3640 + drawGrid(RIGHT_GRID, rightX, rightOctaveOffset, "right"); 3575 3641 3576 3642 // === PER-SIDE MASTER VOLUME SLIDERS === 3577 3643 // Thin vertical bars flanking each grid. Drag to set the master volume ··· 3632 3698 percussionNotice = null; 3633 3699 } 3634 3700 3701 + // Envelope / arrow-key notice (attack, decay, octave, volume) 3702 + if (envelopeNotice && frame < envelopeNotice.until) { 3703 + const dark2 = isDark(); 3704 + const remaining = envelopeNotice.until - frame; 3705 + const alpha = Math.min(255, Math.round(remaining * 3.5)); 3706 + const txt = envelopeNotice.text; 3707 + const tw = Math.max(1, txt.length) * 6 + 12; 3708 + const tx = Math.floor((w - tw) / 2); 3709 + const ty = gridTop + gridH + 4; 3710 + // Background 3711 + ink(0, 0, 0, Math.floor(alpha * 0.7)); 3712 + box(tx - 1, ty - 1, tw + 2, 14, true); 3713 + // Colored fill based on type 3714 + const isAttack = envelopeNotice.param === "attack"; 3715 + const isDecay = envelopeNotice.param === "decay"; 3716 + const isArrow = envelopeNotice.param === "arrow"; 3717 + const r2 = isAttack ? 80 : isDecay ? 40 : isArrow ? 60 : 40; 3718 + const g2 = isAttack ? 40 : isDecay ? 80 : isArrow ? 50 : 40; 3719 + const b2 = isAttack ? 120 : isDecay ? 120 : isArrow ? 100 : 55; 3720 + ink(dark2 ? r2 : 220, dark2 ? g2 : 220, dark2 ? b2 : 240, Math.floor(alpha * 0.9)); 3721 + box(tx, ty, tw, 12, true); 3722 + // Envelope shape icon (small 3-segment drawing) 3723 + if (isAttack || isDecay) { 3724 + const mode = envelopeNotice.mode; 3725 + const ix = tx + 2, iy = ty + 2, ih = 8, iw = 12; 3726 + ink(dark2 ? 255 : 30, dark2 ? 220 : 30, dark2 ? 180 : 60, alpha); 3727 + if (isAttack) { 3728 + // Attack icon: vertical rise speed varies by mode 3729 + const peakX = mode === "zero" ? 1 : mode === "short" ? 4 : 9; 3730 + line(ix, iy + ih, ix + peakX, iy); // rise 3731 + line(ix + peakX, iy, ix + iw, iy); // sustain 3732 + } else { 3733 + // Decay icon: peak then fall speed varies 3734 + const fallStart = mode === "zero" ? 1 : mode === "short" ? 4 : 9; 3735 + line(ix, iy, ix + (iw - fallStart), iy); // sustain 3736 + line(ix + (iw - fallStart), iy, ix + iw, iy + ih); // fall 3737 + } 3738 + } 3739 + // Text 3740 + const textX = (isAttack || isDecay) ? tx + 16 : tx + 4; 3741 + ink(dark2 ? 240 : 30, dark2 ? 230 : 30, dark2 ? 200 : 60, alpha); 3742 + write(txt, { x: textX, y: ty + 2, size: 1, font: "font_1" }); 3743 + } else if (envelopeNotice) { 3744 + envelopeNotice = null; 3745 + } 3746 + 3635 3747 // === SLIDERS: fx mix, echo, pitch, bitcrush + per-effect X/Y routing === 3636 3748 const settingsY = topBarH; 3637 3749 const sliderH = 12; ··· 4286 4398 const categories = [ 4287 4399 ["NOTES", [120, 200, 255], [ 4288 4400 ["a–l ; '", "play notes"], 4289 - ["1–9 ↑↓", "octave"], 4401 + ["1–9", "octave"], 4402 + ["← →", "L/R grid octave"], 4403 + ["↑ ↓", "L/R volume"], 4404 + [", .", "attack / decay"], 4290 4405 ["shift", "quick mode"], 4291 4406 ]], 4292 4407 ["DRUMS", [255, 140, 90], [ ··· 4303 4418 ["F3", "prev track"], 4304 4419 ["F4", "usb rescan"], 4305 4420 ["[ / `", "speed − / reset"], 4306 - [",", "tap-sync bpm"], 4307 4421 ["drag", "scratch platter"], 4308 4422 ]], 4309 4423 ["HOLD", [255, 220, 120], [
+23 -13
fedac/native/src/ac-native.c
··· 1294 1294 sync(); 1295 1295 usleep(500000); 1296 1296 1297 - // Refresh the kernel's partition table via partx -u. 1298 - // Unlike BLKRRPART this updates the kernel's view of 1299 - // existing partitions in-place without needing exclusive 1300 - // access. partprobe is a fallback. Then sfdisk --verify 1301 - // logs the final state for diagnostics. 1297 + // Refresh the kernel's partition table. Primary method: 1298 + // BLKRRPART ioctl (no external binary needed). Falls back 1299 + // to partx/partprobe if available for belt-and-suspenders. 1300 + { 1301 + char disk_dev[64]; 1302 + snprintf(disk_dev, sizeof(disk_dev), "/dev/%s", parent_blk); 1303 + ac_log("[install] BLKRRPART on %s...\n", disk_dev); 1304 + int brr = blkrrpart_with_retry(disk_dev, DLOG); 1305 + ac_log("[install] BLKRRPART result=%d\n", brr); 1306 + } 1307 + // Also try partx/partprobe as secondary refresh (may not 1308 + // be in initramfs — that's OK, || true swallows the error). 1302 1309 snprintf(rcmd, sizeof(rcmd), 1303 1310 "echo '--- partx refresh ---' >> %s; " 1304 1311 "partx -u /dev/%s >> %s 2>&1 || true; " ··· 1323 1330 if (!devpath_ready) { 1324 1331 ac_log("[install] device %s never appeared after sfdisk — see %s\n", devpath, DLOG); 1325 1332 } 1326 - // Wipe any remaining FS signature with wipefs. The earlier 1327 - // dd zero-pass handled the GPT header but didn't touch the 1328 - // data area of the actual partition (which still has the 1329 - // old FAT boot sector). wipefs -a scrubs all recognized 1330 - // filesystem signatures from /dev/nvme0n1p1 so mkfs.vfat 1331 - // doesn't try to preserve old metadata. 1333 + // Wipe any remaining FS signature. Try wipefs first; if 1334 + // it's missing from initramfs, fall back to dd'ing zeros 1335 + // over the first 4KB of the partition (covers FAT BPB, 1336 + // ext superblock, and any other FS magic). 1332 1337 snprintf(rcmd, sizeof(rcmd), 1333 1338 "echo '--- wipefs partition ---' >> %s; " 1334 - "wipefs -a %s >> %s 2>&1 || true", 1335 - DLOG, devpath, DLOG); 1339 + "if command -v wipefs >/dev/null 2>&1; then " 1340 + " wipefs -a %s >> %s 2>&1; " 1341 + "else " 1342 + " echo 'wipefs not found, using dd fallback' >> %s; " 1343 + " dd if=/dev/zero of=%s bs=512 count=8 conv=fsync >> %s 2>&1; " 1344 + "fi", 1345 + DLOG, devpath, DLOG, DLOG, devpath, DLOG); 1336 1346 system(rcmd); 1337 1347 // Drop the kernel page cache so any lingering references 1338 1348 // to the old filesystem's cached pages are released.