Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat: drag-to-edit gun params + pitched perc noise

Two requested features land:

1. Per-shot drag-to-edit on gun inspector cards. Tap a card → it
highlights; drag vertically → updates the value (log-scaled for
freq/duration, linear for amps/Q); lift → release. Edits live in
WAR_PARAM_OVERRIDES (per-session) and hot-tune the next shot via a
new C helper:

audio_gun_voice_set_param(audio, voice_id, key, value)

…called from the JS bindings whenever sound.synth opts include a
`params` object. The helper handles all six inspector keys per
model: classic = {click_amp, crack_fc, crack_q, boom_freq_start,
boom_amp_decay_ms, tail_decay_ms}; physical = {pressure, env_rate,
bore_length_s, body_freq0, body_q0, radiation}. Filter coefficients
(BPF/LPF for crack/tail, body-mode poles for physical) are
recomputed live so freq/Q drags actually re-tune the resonators.

2. Perc white-noise voices now pitch with octave/pitchFactor — every
addHit({type:"noise", tone: N, ...}) became `tone: N * pf`. So when
you bend the trackpad pitch shift or change octave, the noise filter
cutoff scales the same way pitched voices do, and the whole drum
stays coherent under pitch shift.

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

+280 -37
+121 -37
fedac/native/pieces/notepat.mjs
··· 72 72 let lastDrumInspect = null; // { letter, name, voices: [{type,tone,dur,vol,atk,dcy,pan}], until } 73 73 let drumInspectBuilder = null; 74 74 let lastWarInspect = null; // { letter, name, model, params, until } 75 + // Geometry of the most recently rendered inspector cards — populated 76 + // each frame by paint() so act() can hit-test for tap+drag editing. 77 + // Each entry: { x, y, w, h, key, weapon, model }. Null when no 78 + // inspector is showing. 79 + let warInspectCardRects = null; 80 + // Drag-to-edit state. Null when not dragging. While dragging, draw 81 + // events (vertical) update WAR_PARAM_OVERRIDES for the captured card. 82 + let editingWarCard = null; // { key, weapon, model, startVal, startY, lastVal } 75 83 76 84 // Effective pitch shift blended by FX mix (0% fx = no pitch shift) 77 85 function effectivePitchShift() { ··· 1435 1443 1436 1444 // 1. Stick click — very short noise transient at 2.5kHz for the 1437 1445 // beater attack. This is the CUT that makes the kick audible. 1438 - addHit({ type: "noise", tone: 2500, duration: 0.0025, volume: rj(0.50, 0.12) * v, attack: 0.0002, decay: 0.0022, pan: downPan }); 1446 + addHit({ type: "noise", tone: 2500 * pf, duration: 0.0025, volume: rj(0.50, 0.12) * v, attack: 0.0002, decay: 0.0022, pan: downPan }); 1439 1447 // 2. Pitch snap — 200Hz→150Hz perceived sweep via overlapping sines 1440 1448 // Both very short so they read as a single downward "thump" 1441 1449 addHit({ type: "sine", tone: 200 * pf, duration: 0.012, volume: rj(1.1, 0.10) * v, attack: 0.0005, decay: 0.011, pan: downPan }); ··· 1461 1469 // character but only lasts ~30ms before the noise takes over. 1462 1470 1463 1471 // 1. Sharp stick crack — higher freq, shorter, LOUDER than before 1464 - addHit({ type: "noise", tone: 3500, duration: 0.004, volume: rj(0.95, 0.10) * v, attack: 0.0001, decay: 0.004, pan: downPan }); 1472 + addHit({ type: "noise", tone: 3500 * pf, duration: 0.004, volume: rj(0.95, 0.10) * v, attack: 0.0001, decay: 0.004, pan: downPan }); 1465 1473 // 2. 808 tonal pair — SHORT decay (30ms was 120ms), QUIETER so noise dominates 1466 1474 addHit({ type: "sine", tone: 238 * pf, duration: 0.030, volume: rj(0.35, 0.12) * v, attack: 0.0003, decay: 0.029, pan: downPan }); 1467 1475 addHit({ type: "sine", tone: 476 * pf, duration: 0.030, volume: rj(0.28, 0.12) * v, attack: 0.0003, decay: 0.029, pan: downPan }); 1468 1476 // 3. Primary wire noise — DOMINANT layer, bright, medium-short decay 1469 - addHit({ 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) }); 1477 + addHit({ type: "noise", tone: 3500 * pf, 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) }); 1470 1478 // 4. Mid wire noise — adds weight without muddiness 1471 - addHit({ 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) }); 1479 + addHit({ type: "noise", tone: 1800 * pf, 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) }); 1472 1480 // 5. Triangle body fundamental — adds warmth, very short 1473 1481 addHit({ type: "triangle", tone: 180 * pf, duration: 0.025, volume: rj(0.22, 0.15) * v, attack: 0.001, decay: 0.024, pan: downPan }); 1474 1482 break; ··· 1477 1485 case "e": { // clap — TR-808 4-burst pattern via staggered attacks (one-shot) 1478 1486 const downPan = pan + rn(-0.06, 0.02); 1479 1487 // Three rapid bursts — attacks 5/15/25 ms create ~10 ms spacing 1480 - addHit({ type: "noise", tone: 1000, duration: 0.025, volume: rj(0.90, 0.15) * v, attack: 0.005, decay: 0.020, pan: downPan }); 1481 - addHit({ type: "noise", tone: 1100, duration: 0.035, volume: rj(0.95, 0.15) * v, attack: 0.015, decay: 0.020, pan: downPan }); 1482 - addHit({ type: "noise", tone: 900, duration: 0.045, volume: rj(0.85, 0.15) * v, attack: 0.025, decay: 0.020, pan: downPan }); 1488 + addHit({ type: "noise", tone: 1000 * pf, duration: 0.025, volume: rj(0.90, 0.15) * v, attack: 0.005, decay: 0.020, pan: downPan }); 1489 + addHit({ type: "noise", tone: 1100 * pf, duration: 0.035, volume: rj(0.95, 0.15) * v, attack: 0.015, decay: 0.020, pan: downPan }); 1490 + addHit({ type: "noise", tone: 900 * pf, duration: 0.045, volume: rj(0.85, 0.15) * v, attack: 0.025, decay: 0.020, pan: downPan }); 1483 1491 // Bright edge on the first burst 1484 - addHit({ type: "noise", tone: 3000, duration: 0.008, volume: rj(0.55, 0.15) * v, attack: 0.001, decay: 0.007, pan: downPan }); 1492 + addHit({ type: "noise", tone: 3000 * pf, duration: 0.008, volume: rj(0.55, 0.15) * v, attack: 0.001, decay: 0.007, pan: downPan }); 1485 1493 // The 4th burst — "room tail" with long decay (120ms) 1486 - addHit({ 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) }); 1487 - addHit({ 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) }); 1494 + addHit({ type: "noise", tone: 1000 * pf, 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) }); 1495 + addHit({ type: "noise", tone: 2200 * pf, 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) }); 1488 1496 break; 1489 1497 } 1490 1498 1491 1499 case "f": { // snap — finger snap physics (one-shot) 1492 1500 const downPan = pan + rn(-0.04, 0.04); 1493 1501 // Sharp broadband click (thumb-middle friction release) 1494 - addHit({ type: "noise", tone: 6000, duration: 0.003, volume: rj(0.70, 0.15) * v, attack: 0.0001, decay: 0.0028, pan: downPan }); 1502 + addHit({ type: "noise", tone: 6000 * pf, duration: 0.003, volume: rj(0.70, 0.15) * v, attack: 0.0001, decay: 0.0028, pan: downPan }); 1495 1503 // Palm cavity resonance at ~2100 Hz — the "pop" tone 1496 1504 addHit({ 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 }); 1497 1505 // Upper bite at 3500 Hz ··· 1506 1514 addHit({ 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 }); 1507 1515 } 1508 1516 // Bright noise top for the "tss" 1509 - addHit({ 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 }); 1517 + addHit({ type: "noise", tone: 8000 * pf, duration: rj(0.040, 0.20), volume: rj(0.38, 0.12) * v, attack: 0.0005, decay: 0.038, pan: downPan }); 1510 1518 // Lift click: on key-up, fire a tiny high-noise transient to 1511 1519 // represent the stick leaving the hat. Subtle — real closed-hat 1512 1520 // "opens" don't ring, they just have a mechanical release click. 1513 1521 addReleaseBurst(() => { 1514 - sound.synth({ type: "noise", tone: 9000, duration: 0.004, volume: rj(0.22, 0.20) * v, attack: 0.0002, decay: 0.0038, pan: downPan }); 1522 + sound.synth({ type: "noise", tone: 9000 * pf, duration: 0.004, volume: rj(0.22, 0.20) * v, attack: 0.0002, decay: 0.0038, pan: downPan }); 1515 1523 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 }); 1516 1524 }); 1517 1525 break; ··· 1524 1532 addHit({ type: "square", tone: f * pf, duration: 0.012, volume: rj(0.16, 0.18) * v, attack: 0.0005, decay: 0.011, pan: downPan }); 1525 1533 } 1526 1534 // Transient: bright noise chip 1527 - addHit({ type: "noise", tone: 8200, duration: 0.012, volume: rj(0.42, 0.12) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 1535 + addHit({ type: "noise", tone: 8200 * pf, duration: 0.012, volume: rj(0.42, 0.12) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 1528 1536 // SUSTAIN: the signature open-hat shimmer. Rings until key release. 1529 1537 // Release = hi-hat foot pedal closing — dampens the top end fast. 1530 1538 // releaseUpdate drops the tone first (simulates bandpass closing), 1531 1539 // then the kill fade does the rest. 1532 1540 addSustain( 1533 - { type: "noise", tone: 7000, volume: rj(0.32, 0.15) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 1541 + { type: "noise", tone: 7000 * pf, volume: rj(0.32, 0.15) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 1534 1542 rj(0.40, 0.25), 1535 1543 0.12, // 120ms foot-pedal close 1536 1544 { tone: 3500 } // dampen brightness on release 1537 1545 ); 1538 1546 addSustain( 1539 - { type: "noise", tone: 5000, volume: rj(0.20, 0.18) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 1547 + { type: "noise", tone: 5000 * pf, volume: rj(0.20, 0.18) * v, attack: 0.003, decay: 0, pan: downPan + rn(-0.02, 0.08) }, 1540 1548 rj(0.25, 0.25), 1541 1549 0.10, 1542 1550 { tone: 2800 } ··· 1568 1576 ); 1569 1577 // SUSTAIN: long shimmer body 1570 1578 addSustain( 1571 - { type: "noise", tone: 4200, volume: rj(0.26, 0.12) * v, attack: 0.005, decay: 0, pan: downPan + rn(-0.03, 0.03) }, 1579 + { type: "noise", tone: 4200 * pf, volume: rj(0.26, 0.12) * v, attack: 0.005, decay: 0, pan: downPan + rn(-0.03, 0.03) }, 1572 1580 rj(0.9, 0.20), 1573 1581 0.30 1574 1582 ); ··· 1578 1586 case "c#": { // crash — explosive noise attack + LONG shimmer wash 1579 1587 const downPan = pan + rn(-0.05, 0.05); 1580 1588 // Transient: loud noise splash 1581 - addHit({ type: "noise", tone: 8000, duration: 0.030, volume: rj(0.75, 0.15) * v, attack: 0.0005, decay: 0.029, pan: downPan }); 1589 + addHit({ type: "noise", tone: 8000 * pf, duration: 0.030, volume: rj(0.75, 0.15) * v, attack: 0.0005, decay: 0.029, pan: downPan }); 1582 1590 // Transient: hat cluster for metallic attack 1583 1591 for (const f of HAT_FREQS) { 1584 1592 addHit({ type: "square", tone: f * pf, duration: 0.030, volume: rj(0.12, 0.20) * v, attack: 0.0005, decay: 0.029, pan: downPan }); 1585 1593 } 1586 1594 // SUSTAIN: the long wash — crash's whole character 1587 1595 addSustain( 1588 - { type: "noise", tone: 5000, volume: rj(0.45, 0.12) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 1596 + { type: "noise", tone: 5000 * pf, volume: rj(0.45, 0.12) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 1589 1597 rj(1.4, 0.18), 1590 1598 0.45 // long release fade 1591 1599 ); 1592 1600 addSustain( 1593 - { type: "noise", tone: 7500, volume: rj(0.30, 0.15) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 1601 + { type: "noise", tone: 7500 * pf, volume: rj(0.30, 0.15) * v, attack: 0.008, decay: 0, pan: downPan + rn(-0.04, 0.04) }, 1594 1602 rj(0.9, 0.18), 1595 1603 0.35 1596 1604 ); ··· 1606 1614 case "d#": { // splash — short bright cymbal burst (one-shot) 1607 1615 const downPan = pan + rn(-0.04, 0.04); 1608 1616 // Bright attack 1609 - addHit({ type: "noise", tone: 9000, duration: 0.012, volume: rj(0.55, 0.15) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 1617 + addHit({ type: "noise", tone: 9000 * pf, duration: 0.012, volume: rj(0.55, 0.15) * v, attack: 0.0003, decay: 0.011, pan: downPan }); 1610 1618 addHit({ type: "square", tone: 800 * pf, duration: 0.015, volume: rj(0.14, 0.20) * v, attack: 0.0005, decay: 0.014, pan: downPan }); 1611 1619 addHit({ type: "square", tone: 540 * pf, duration: 0.015, volume: rj(0.10, 0.20) * v, attack: 0.0005, decay: 0.014, pan: downPan }); 1612 1620 // Short wash — splash is all quick burst, no sustain 1613 - addHit({ 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) }); 1614 - addHit({ 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) }); 1621 + addHit({ type: "noise", tone: 6000 * pf, 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) }); 1622 + addHit({ type: "noise", tone: 8500 * pf, 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) }); 1615 1623 break; 1616 1624 } 1617 1625 ··· 1628 1636 case "g#": { // wood block — single triangle @ 2500 Hz (one-shot) 1629 1637 const downPan = pan + rn(-0.03, 0.03); 1630 1638 // Stick click 1631 - addHit({ type: "noise", tone: 5000, duration: 0.002, volume: rj(0.35, 0.18) * v, attack: 0.0001, decay: 0.0018, pan: downPan }); 1639 + addHit({ type: "noise", tone: 5000 * pf, duration: 0.002, volume: rj(0.35, 0.18) * v, attack: 0.0001, decay: 0.0018, pan: downPan }); 1632 1640 // Block tones 1633 1641 addHit({ 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 }); 1634 1642 addHit({ 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 }); ··· 1638 1646 case "a#": { // tambourine — staggered jingle bursts (one-shot) 1639 1647 const downPan = pan + rn(-0.04, 0.04); 1640 1648 // 3 staggered noise bursts via attack offsets (jingle rattle) 1641 - addHit({ type: "noise", tone: 7000, duration: 0.08, volume: rj(0.38, 0.18) * v, attack: 0.002, decay: 0.075, pan: downPan }); 1642 - addHit({ type: "noise", tone: 7500, duration: 0.09, volume: rj(0.30, 0.18) * v, attack: 0.015, decay: 0.075, pan: downPan }); 1643 - addHit({ type: "noise", tone: 6500, duration: 0.10, volume: rj(0.25, 0.18) * v, attack: 0.030, decay: 0.070, pan: downPan }); 1649 + addHit({ type: "noise", tone: 7000 * pf, duration: 0.08, volume: rj(0.38, 0.18) * v, attack: 0.002, decay: 0.075, pan: downPan }); 1650 + addHit({ type: "noise", tone: 7500 * pf, duration: 0.09, volume: rj(0.30, 0.18) * v, attack: 0.015, decay: 0.075, pan: downPan }); 1651 + addHit({ type: "noise", tone: 6500 * pf, duration: 0.10, volume: rj(0.25, 0.18) * v, attack: 0.030, decay: 0.070, pan: downPan }); 1644 1652 // High square ting 1645 1653 addHit({ type: "square", tone: 6000 * pf, duration: 0.030, volume: rj(0.14, 0.20) * v, attack: 0.001, decay: 0.028, pan: downPan }); 1646 1654 // Long jingle tail 1647 - addHit({ 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) }); 1648 - addHit({ 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) }); 1655 + addHit({ type: "noise", tone: 7000 * pf, 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) }); 1656 + addHit({ type: "noise", tone: 4500 * pf, 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) }); 1649 1657 break; 1650 1658 } 1651 1659 } ··· 1696 1704 const typeStr = model === "classic" || model === "physical" 1697 1705 ? `gun-${preset}/${model}` 1698 1706 : `gun-${preset}`; 1707 + // Resolve which model the C side actually picks so we look up the 1708 + // right override slot. (Mirrors the resolvedModel logic that updates 1709 + // lastWarInspect above — keep them in sync.) 1710 + const realModel = model === "physical" 1711 + ? "physical" 1712 + : (model === "classic" ? "classic" 1713 + : (preset === "grenade" || preset === "rpg" ? "physical" : "classic")); 1714 + const overrides = paramOverridesFor(preset, realModel); 1715 + const hasOv = Object.keys(overrides).length > 0; 1699 1716 1700 1717 if (sustained && isLive) { 1701 1718 // Held weapon — infinite-duration voice killed on release. 1702 - const handle = sound.synth({ 1719 + const opts = { 1703 1720 type: typeStr, 1704 1721 tone: pressure, // encoded as pressure multiplier (see js-bindings.c) 1705 1722 duration: Infinity, ··· 1707 1724 attack: 0, 1708 1725 decay: 0, 1709 1726 pan, 1710 - }); 1727 + }; 1728 + if (hasOv) opts.params = overrides; 1729 + const handle = sound.synth(opts); 1711 1730 if (handle) { 1712 1731 holdVoices.push({ 1713 1732 handle, ··· 1722 1741 // generates the bang; `duration` just lets body-mode ring continue 1723 1742 // for the weapon's characteristic tail before auto-kill. 1724 1743 const duration = WAR_DURATION[letter] ?? 0.3; 1725 - const handle = sound.synth({ 1744 + const opts = { 1726 1745 type: typeStr, 1727 1746 tone: pressure, 1728 1747 duration, ··· 1730 1749 attack: 0, 1731 1750 decay: Math.max(0.02, duration * 0.7), 1732 1751 pan, 1733 - }); 1752 + }; 1753 + if (hasOv) opts.params = overrides; 1754 + const handle = sound.synth(opts); 1734 1755 if (isLive && handle) { 1735 1756 // Track the voice so sim() can adjust pan/volume during its tail, 1736 1757 // mirroring how addHit does for perc kit. ··· 1936 1957 if (!sound?.synth) return; 1937 1958 if (waveType === "sample") { 1938 1959 // Short percussive click for sample mode 1939 - sound.synth({ type: "noise", tone: 800, duration: 0.03, volume: 0.12, attack: 0.001, decay: 0.025, pan: 0 }); 1960 + sound.synth({ type: "noise", tone: 800 * pf, duration: 0.03, volume: 0.12, attack: 0.001, decay: 0.025, pan: 0 }); 1940 1961 return; 1941 1962 } 1942 1963 const tones = { sine: 660, triangle: 550, sawtooth: 440, square: 330, noise: 220, whistle: 880 }; ··· 2106 2127 return; 2107 2128 } 2108 2129 2130 + // === Gun inspector drag-to-edit === 2131 + // Tap a param card → start editing. Drag vertically → change value 2132 + // (log-scaled for freq/duration, linear for amps/Q). Lift → release. 2133 + // Hits intercept the touch so it doesn't also trigger a note pad press. 2134 + if (e.is("touch") && warInspectCardRects && !editingWarCard) { 2135 + const tx = e.pointer?.x ?? e.x ?? -1; 2136 + const ty = e.pointer?.y ?? e.y ?? -1; 2137 + for (const r of warInspectCardRects) { 2138 + if (tx >= r.x && tx < r.x + r.w && ty >= r.y && ty < r.y + r.h) { 2139 + const startVal = paramValue(r.weapon, r.model, r.key); 2140 + editingWarCard = { ...r, startVal, startY: ty, lastVal: startVal }; 2141 + if (lastWarInspect) lastWarInspect.until = frame + 300; // keep visible 2142 + return; 2143 + } 2144 + } 2145 + } 2146 + if (e.is("draw") && editingWarCard) { 2147 + const ty = e.pointer?.y ?? e.y ?? editingWarCard.startY; 2148 + const dy = editingWarCard.startY - ty; // up = positive 2149 + const range = WAR_PARAM_RANGES[editingWarCard.key]; 2150 + if (range) { 2151 + const [lo, hi, scale] = range; 2152 + // 100 px of vertical drag traverses the full range. 2153 + const t = Math.max(-1, Math.min(1, dy / 100)); 2154 + let baseT; 2155 + if (scale === "log") { 2156 + const logLo = Math.log(Math.max(0.0001, lo)); 2157 + const logHi = Math.log(hi); 2158 + baseT = (Math.log(Math.max(0.0001, editingWarCard.startVal)) - logLo) / (logHi - logLo); 2159 + } else { 2160 + baseT = (editingWarCard.startVal - lo) / (hi - lo); 2161 + } 2162 + let newT = Math.max(0, Math.min(1, baseT + t)); 2163 + let newVal; 2164 + if (scale === "log") { 2165 + const logLo = Math.log(Math.max(0.0001, lo)); 2166 + const logHi = Math.log(hi); 2167 + newVal = Math.exp(logLo + newT * (logHi - logLo)); 2168 + } else { 2169 + newVal = lo + newT * (hi - lo); 2170 + } 2171 + const ovKey = `${editingWarCard.weapon}/${editingWarCard.model}/${editingWarCard.key}`; 2172 + WAR_PARAM_OVERRIDES[ovKey] = newVal; 2173 + editingWarCard.lastVal = newVal; 2174 + if (lastWarInspect) lastWarInspect.until = frame + 300; 2175 + } 2176 + return; 2177 + } 2178 + if (e.is("lift") && editingWarCard) { 2179 + editingWarCard = null; 2180 + return; 2181 + } 2182 + 2109 2183 if (e.is("keyboard:down")) { 2110 2184 const key = e.key?.toLowerCase(); 2111 2185 if (!key) return; ··· 2372 2446 sound.sample.loadData(globalSample.data, globalSample.rate); 2373 2447 sampleLoaded = true; 2374 2448 } 2375 - sound.synth({ type: "noise", tone: 200, duration: 0.1, volume: 0.15, attack: 0.001, decay: 0.08 }); 2449 + sound.synth({ type: "noise", tone: 200 * pf, duration: 0.1, volume: 0.15, attack: 0.001, decay: 0.08 }); 2376 2450 return; 2377 2451 } 2378 2452 // Arrow left/right handled above (octave per side) ··· 4293 4367 const keys = INSPECTOR_KEYS[modelKey] || []; 4294 4368 const cardY = insY + rowH + rowGap; 4295 4369 const cardW = Math.max(36, Math.floor((w - 6) / Math.max(1, keys.length))); 4370 + warInspectCardRects = []; 4296 4371 for (let i = 0; i < keys.length; i++) { 4297 4372 const k = keys[i]; 4298 4373 const val = paramValue(weapon, modelKey, k); 4299 4374 const ovKey = `${weapon}/${modelKey}/${k}`; 4300 4375 const isEdited = ovKey in WAR_PARAM_OVERRIDES; 4376 + const isDragging = editingWarCard?.weapon === weapon 4377 + && editingWarCard?.model === modelKey 4378 + && editingWarCard?.key === k; 4301 4379 const cx = 3 + i * cardW; 4302 - ink(tint[0], tint[1], tint[2], Math.floor(alpha * (isEdited ? 0.5 : 0.3))); 4380 + const bgFill = isDragging ? 0.7 : (isEdited ? 0.5 : 0.3); 4381 + ink(tint[0], tint[1], tint[2], Math.floor(alpha * bgFill)); 4303 4382 box(cx, cardY, cardW - 1, rowH, true); 4304 4383 ink(tint[0], tint[1], tint[2], alpha); 4305 - // Two-line label: short key on top, value below — fits more in. 4306 4384 const shortKey = k.replace(/_amp$/, "amp").replace(/_fc$/, "fc") 4307 4385 .replace(/_ms$/, "ms").replace(/_q$/, "Q") 4308 4386 .replace(/^body_/, "b").replace(/^boom_/, "bm") ··· 4310 4388 .replace(/^click_/, "ck").replace(/freq_start/, "f0"); 4311 4389 const label = `${shortKey} ${formatParamValue(k, val)}${isEdited ? "*" : ""}`; 4312 4390 write(label, { x: cx + 1, y: cardY + 1, size: 1, font: "font_1" }); 4391 + warInspectCardRects.push({ 4392 + x: cx, y: cardY, w: cardW - 1, h: rowH, 4393 + key: k, weapon, model: modelKey, 4394 + }); 4313 4395 } 4314 4396 4315 4397 // 2.5D barrel viz — only for physical model. ··· 4353 4435 } 4354 4436 } else if (lastWarInspect) { 4355 4437 lastWarInspect = null; 4438 + warInspectCardRects = null; 4439 + editingWarCard = null; 4356 4440 } 4357 4441 4358 4442 // === PER-SIDE MASTER VOLUME SLIDERS ===
+121
fedac/native/src/audio.c
··· 2101 2101 return id; 2102 2102 } 2103 2103 2104 + // Apply a single per-shot param override to a freshly-initialized gun 2105 + // voice. Called by the JS bindings between audio_synth_gun() and the 2106 + // audio thread's first read of the voice — lets the inspector's 2107 + // drag-to-edit cards push live tuning values into the next shot 2108 + // without rebuilding gun_presets[]. Unknown keys are silently ignored. 2109 + // 2110 + // Layer-state fields (envelopes, biquad coefficients, the Friedlander 2111 + // pulse position) are NOT exposed; we only change the constants the 2112 + // preset would have set in init. 2113 + void audio_gun_voice_set_param(ACAudio *audio, uint64_t id, 2114 + const char *key, double value) { 2115 + if (!audio || !key) return; 2116 + pthread_mutex_lock(&audio->lock); 2117 + ACVoice *v = NULL; 2118 + for (int i = 0; i < AUDIO_MAX_VOICES; i++) { 2119 + if (audio->voices[i].id == id && audio->voices[i].type == WAVE_GUN) { 2120 + v = &audio->voices[i]; 2121 + break; 2122 + } 2123 + } 2124 + if (!v) { pthread_mutex_unlock(&audio->lock); return; } 2125 + 2126 + double sr = (double)(audio->actual_rate ? audio->actual_rate 2127 + : AUDIO_SAMPLE_RATE); 2128 + if (v->gun_model == GUN_MODEL_CLASSIC) { 2129 + if (strcmp(key, "click_amp") == 0) v->gun_click_amp = value; 2130 + else if (strcmp(key, "click_decay_ms") == 0) { 2131 + double tau = (value > 0.05 ? value : 0.05) * 0.001; 2132 + v->gun_click_decay_mult = exp(-1.0 / (tau * sr)); 2133 + } 2134 + else if (strcmp(key, "crack_amp") == 0) v->gun_body_amp[0] = value; 2135 + else if (strcmp(key, "crack_decay_ms") == 0) { 2136 + double tau = (value > 0.1 ? value : 0.1) * 0.001; 2137 + v->gun_env_decay_mult = exp(-1.0 / (tau * sr)); 2138 + } 2139 + else if (strcmp(key, "crack_fc") == 0 || strcmp(key, "crack_q") == 0) { 2140 + // Both freq and Q feed the same biquad, recompute together. 2141 + // For partial updates we just recompute with the latest value 2142 + // and keep the other from existing coefs (lossy but adequate). 2143 + // Approximate Q from a2 = r² → r = √a2 → tau = -π·f/(Q·sr·ln r). 2144 + double a2 = v->gun_body_a2[0]; 2145 + double r = a2 > 0 ? sqrt(a2) : 0.95; 2146 + double cur_w = acos(v->gun_body_a1[0] / (2.0 * r)); 2147 + double cur_f = cur_w * sr / (2.0 * M_PI); 2148 + double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001)); 2149 + double f = (strcmp(key, "crack_fc") == 0) ? value : cur_f; 2150 + double q = (strcmp(key, "crack_q") == 0) ? value : cur_q; 2151 + compute_resonator(f, q, sr, &v->gun_body_a1[0], 2152 + &v->gun_body_a2[0], &v->gun_crack_b0); 2153 + } 2154 + else if (strcmp(key, "boom_amp") == 0) v->gun_body_amp[1] = value; 2155 + else if (strcmp(key, "boom_freq_start") == 0) { 2156 + v->gun_boom_freq_start = value; 2157 + v->gun_boom_freq = value; 2158 + } 2159 + else if (strcmp(key, "boom_freq_end") == 0) v->gun_boom_freq_end = value; 2160 + else if (strcmp(key, "boom_pitch_decay_ms") == 0) { 2161 + double tau = (value > 0.1 ? value : 0.1) * 0.001; 2162 + v->gun_boom_pitch_mult = exp(-1.0 / (tau * sr)); 2163 + } 2164 + else if (strcmp(key, "boom_amp_decay_ms") == 0) { 2165 + double tau = (value > 0.1 ? value : 0.1) * 0.001; 2166 + v->gun_boom_decay_mult = exp(-1.0 / (tau * sr)); 2167 + } 2168 + else if (strcmp(key, "tail_amp") == 0) v->gun_body_amp[2] = value; 2169 + else if (strcmp(key, "tail_decay_ms") == 0) { 2170 + double tau = (value > 0.1 ? value : 0.1) * 0.001; 2171 + v->gun_tail_decay_mult = exp(-1.0 / (tau * sr)); 2172 + } 2173 + else if (strcmp(key, "tail_fc") == 0 || strcmp(key, "tail_q") == 0) { 2174 + double a2 = v->gun_body_a2[1]; 2175 + double r = a2 > 0 ? sqrt(a2) : 0.95; 2176 + double cur_w = acos(v->gun_body_a1[1] / (2.0 * r)); 2177 + double cur_f = cur_w * sr / (2.0 * M_PI); 2178 + double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001)); 2179 + double f = (strcmp(key, "tail_fc") == 0) ? value : cur_f; 2180 + double q = (strcmp(key, "tail_q") == 0) ? value : cur_q; 2181 + compute_resonator(f, q, sr, &v->gun_body_a1[1], 2182 + &v->gun_body_a2[1], &v->gun_tail_b0); 2183 + } 2184 + } else { 2185 + // Physical model overrides. 2186 + if (strcmp(key, "pressure") == 0) v->gun_pressure = value; 2187 + else if (strcmp(key, "env_rate") == 0) { 2188 + v->gun_phys_t_plus = (3.0 / (value > 100 ? value : 100.0)) * sr; 2189 + if (v->gun_phys_t_plus < 32.0) v->gun_phys_t_plus = 32.0; 2190 + if (v->gun_phys_t_plus > 4096.0) v->gun_phys_t_plus = 4096.0; 2191 + } 2192 + else if (strcmp(key, "bore_length_s") == 0) { 2193 + v->gun_bore_delay = value * sr; 2194 + if (v->gun_bore_delay < 4.0) v->gun_bore_delay = 4.0; 2195 + if (v->gun_bore_delay > 2040.0) v->gun_bore_delay = 2040.0; 2196 + } 2197 + else if (strcmp(key, "bore_loss") == 0) v->gun_bore_loss = value; 2198 + else if (strcmp(key, "breech_reflect") == 0) v->gun_breech_reflect = value; 2199 + else if (strcmp(key, "noise_gain") == 0) v->gun_noise_gain = value; 2200 + else if (strcmp(key, "radiation") == 0) v->gun_radiation_a = value; 2201 + else if (strncmp(key, "body_freq", 9) == 0 || 2202 + strncmp(key, "body_q", 6) == 0) { 2203 + int idx = key[strlen(key) - 1] - '0'; 2204 + if (idx < 0 || idx > 2) { pthread_mutex_unlock(&audio->lock); return; } 2205 + // Recompute the resonator with one swapped param, the other inferred. 2206 + double a2 = v->gun_body_a2[idx]; 2207 + double r = a2 > 0 ? sqrt(a2) : 0.95; 2208 + double cur_w = acos(v->gun_body_a1[idx] / (2.0 * r)); 2209 + double cur_f = cur_w * sr / (2.0 * M_PI); 2210 + double cur_q = -M_PI * cur_f / (sr * log(r > 0.0001 ? r : 0.0001)); 2211 + double f = (strncmp(key, "body_freq", 9) == 0) ? value : cur_f; 2212 + double q = (strncmp(key, "body_q", 6) == 0) ? value : cur_q; 2213 + double b0_unused; 2214 + compute_resonator(f, q, sr, &v->gun_body_a1[idx], 2215 + &v->gun_body_a2[idx], &b0_unused); 2216 + } 2217 + else if (strncmp(key, "body_amp", 8) == 0) { 2218 + int idx = key[strlen(key) - 1] - '0'; 2219 + if (idx >= 0 && idx <= 2) v->gun_body_amp[idx] = value; 2220 + } 2221 + } 2222 + pthread_mutex_unlock(&audio->lock); 2223 + } 2224 + 2104 2225 void audio_update(ACAudio *audio, uint64_t id, double freq, 2105 2226 double volume, double pan) { 2106 2227 if (!audio) return;
+7
fedac/native/src/audio.h
··· 344 344 double volume, double attack, double decay, 345 345 double pan, double pressure_scale, int force_model); 346 346 347 + // Override one preset-derived parameter on a freshly-created gun voice. 348 + // Call between audio_synth_gun() and the next audio thread tick to 349 + // retune the next shot. Unknown keys are ignored. Used by the inspector 350 + // drag-to-edit cards. Key names match the gun_presets[] field names. 351 + void audio_gun_voice_set_param(ACAudio *audio, uint64_t id, 352 + const char *key, double value); 353 + 347 354 // Kill a voice with fade 348 355 void audio_kill(ACAudio *audio, uint64_t id, double fade); 349 356
+31
fedac/native/src/js-bindings.c
··· 1001 1001 wt, freq, volume, duration, (unsigned long)id); 1002 1002 } 1003 1003 1004 + // For gun voices, an optional `params` object on opts passes per-shot 1005 + // overrides into the C-side synth state (drag-to-edit on inspector 1006 + // cards). Each numeric property maps to a gun_presets[] field key. 1007 + // Applied immediately after synth init so the next audio thread tick 1008 + // sees the patched values. 1009 + if (is_gun && id) { 1010 + JSValue params_v = JS_GetPropertyStr(ctx, opts, "params"); 1011 + if (JS_IsObject(params_v)) { 1012 + JSPropertyEnum *props = NULL; 1013 + uint32_t plen = 0; 1014 + if (JS_GetOwnPropertyNames(ctx, &props, &plen, params_v, 1015 + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { 1016 + for (uint32_t i = 0; i < plen; i++) { 1017 + const char *k = JS_AtomToCString(ctx, props[i].atom); 1018 + if (k) { 1019 + JSValue pv = JS_GetProperty(ctx, params_v, props[i].atom); 1020 + double pd; 1021 + if (JS_IsNumber(pv) && JS_ToFloat64(ctx, &pd, pv) == 0) { 1022 + audio_gun_voice_set_param(audio, id, k, pd); 1023 + } 1024 + JS_FreeValue(ctx, pv); 1025 + JS_FreeCString(ctx, k); 1026 + } 1027 + JS_FreeAtom(ctx, props[i].atom); 1028 + } 1029 + js_free(ctx, props); 1030 + } 1031 + } 1032 + JS_FreeValue(ctx, params_v); 1033 + } 1034 + 1004 1035 // Return sound object with kill(), update(), startedAt 1005 1036 JSValue snd = JS_NewObject(ctx); 1006 1037 JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id));