Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

Add dedicated reverse replay to native notepat

+614 -275
+184 -203
fedac/native/pieces/notepat.mjs
··· 27 27 28 28 let sounds = {}; 29 29 let trail = {}; 30 + let activeDrumKeys = {}; // keyboard key -> { letter, volume, pan, pitchFactor, needsRelease } 30 31 let frame = 0; 31 32 let escCount = 0; 32 33 let escLastFrame = 0; ··· 331 332 }; 332 333 let wifiCursorBlink = 0; // cursor blink counter 333 334 // Touch-note state (for clickable grid buttons) 334 - let touchNotes = {}; // pointer id -> { key, note, octave } 335 + let touchNotes = {}; // pointer id -> { key, drumHold? } 335 336 // Hover / cursor position (updated from touch + draw events) 336 337 let hoverX = -1, hoverY = -1; 337 338 ··· 400 401 let rightVolDragging = false; 401 402 402 403 // === REVERSE PLAYBACK / INSTANT REPLAY LOOP === 403 - // Hold space to instantly play the CURRENT phase of history in reverse. 404 - // Release space to stop mid-reverse — the duration you held space defines 405 - // the length of the next loop. Pressing space starts a NEW phase: previous 406 - // history is snapshotted as the source, history resets to empty, and every 407 - // reverse event that fires during the press gets RECORDED BACK into the new 408 - // history. That means pressing space twice in a row bounces the sequence 409 - // forward → reverse → forward → reverse, creating a natural loop pedal. 404 + // Hold space to reverse-play the ACTUAL recent audio, not just re-trigger 405 + // note events. The native mixer keeps a recent mono output buffer; pressing 406 + // space snapshots the current phase, reverses it, loads it into a dedicated 407 + // global replay buffer, and starts playback immediately. Releasing space 408 + // stops that replay voice without touching the normal sample bank. 410 409 // 411 - // User-played notes during a reverse phase also go into the new history, so 412 - // you can layer over the reverse and the layers get captured into the next 413 - // pass. "The length of holding space is a bit like setting the out point on 414 - // the loop" (your words). 415 - const PLAYBACK_MAX_EVENTS = 256; 416 - const PLAYBACK_MAX_AGE_MS = 12000; 417 - let playbackHistory = []; // [{ts, kind:'note'|'drum', letter, octave, freq, vel, pan, wave, pitch}] 410 + // Phase semantics stay loop-pedal-like: each press snapshots audio since the 411 + // previous press, then resets the phase start. So what happened during the 412 + // previous phase becomes the new reverse source. 413 + const REVERSE_MAX_AGE_MS = 12000; 414 + const REVERSE_MIN_BUFFER_SAMPLES = 256; 418 415 let spaceHeld = false; 419 - // Queue of pending reverse events, sorted by playAtMs (monotonic clock). 420 - let reverseQueue = []; // [{playAtMs, ev}] 416 + let reversePhaseStartMs = Date.now(); 417 + let reversePlaybackSound = null; 421 418 422 - function recordPlayback(entry) { 423 - const now = Date.now(); 424 - playbackHistory.push({ ...entry, ts: now }); 425 - if (playbackHistory.length > PLAYBACK_MAX_EVENTS) { 426 - playbackHistory.splice(0, playbackHistory.length - PLAYBACK_MAX_EVENTS); 427 - } 428 - const cutoff = now - PLAYBACK_MAX_AGE_MS; 429 - while (playbackHistory.length && playbackHistory[0].ts < cutoff) { 430 - playbackHistory.shift(); 431 - } 419 + function recordPlayback(_entry) {} 420 + 421 + function reversePlaybackTone() { 422 + return SAMPLE_BASE_FREQ * Math.pow(2, effectivePitchShift()); 432 423 } 433 424 434 - function startReversePlayback() { 435 - // Snapshot the CURRENT phase as the source, then RESET the phase history 436 - // so reverse events and any new user notes populate a fresh buffer. 437 - const snapshot = playbackHistory.slice(); 438 - playbackHistory = []; 439 - if (snapshot.length === 0) return; 440 - const now = Date.now(); 441 - const latestTs = snapshot[snapshot.length - 1].ts; 442 - reverseQueue = []; 443 - // Walk events from most-recent to earliest. Most recent plays at delay 0; 444 - // each older event plays at (latest - itsTs) ms later, so the original 445 - // intervals are preserved but the sequence is reversed. 446 - for (let i = snapshot.length - 1; i >= 0; i--) { 447 - const e = snapshot[i]; 448 - const delay = latestTs - e.ts; 449 - reverseQueue.push({ playAtMs: now + delay, ev: e }); 425 + function updateReversePlaybackPitch() { 426 + if (reversePlaybackSound?.update) { 427 + reversePlaybackSound.update({ 428 + tone: reversePlaybackTone(), 429 + base: SAMPLE_BASE_FREQ, 430 + }); 450 431 } 451 432 } 452 433 453 - function stopReversePlayback() { 454 - // Clear pending reverse events — anything not yet fired is dropped. 455 - // playbackHistory keeps whatever got recorded during this phase (reverse 456 - // events that DID fire + user-played notes), and that becomes the source 457 - // for the next space press. 458 - reverseQueue = []; 434 + function stopReversePlayback(sound) { 435 + if (!reversePlaybackSound) return; 436 + if (sound?.replay?.kill) sound.replay.kill(reversePlaybackSound, 0.03); 437 + else sound?.kill?.(reversePlaybackSound, 0.03); 438 + reversePlaybackSound = null; 459 439 } 460 440 461 - // Fire one reverse-playback event via the sound API AND record it back into 462 - // history so the next space press reverses THIS phase. 463 - function playReverseEvent(ev, sound) { 464 - if (!sound) return; 465 - if (ev.kind === "drum") { 466 - playPercussion(sound, ev.letter, (ev.vel || 1) * 1.5, ev.pan || 0, ev.pitch || 1); 467 - flashDrum(ev.letter); 468 - } else { 469 - const playFreq = (ev.freq || 440) * (ev.pitch || 1); 470 - sound?.synth?.({ 471 - type: ev.wave || "sine", 472 - tone: playFreq, 473 - duration: 0.12, 474 - volume: (ev.vel || 0.7) * 0.7, 475 - attack: 0.003, 476 - decay: 0.09, 477 - pan: ev.pan || 0, 478 - }); 441 + function startReversePlayback(sound) { 442 + if (!sound?.speaker?.getRecentBuffer || !sound?.replay?.loadData || !sound?.replay?.play) { 443 + reversePhaseStartMs = Date.now(); 444 + return false; 479 445 } 480 - // Record the fired event back into the new phase's history (with the 481 - // CURRENT timestamp, not the original one — this is what makes the 482 - // bounce loop work: the new phase captures events in the order they 483 - // played, then the next reverse unwinds them back to the original order). 484 - recordPlayback({ 485 - kind: ev.kind, 486 - letter: ev.letter, 487 - octave: ev.octave, 488 - freq: ev.freq, 489 - vel: ev.vel, 490 - pan: ev.pan, 491 - wave: ev.wave, 492 - pitch: ev.pitch, 446 + 447 + const now = Date.now(); 448 + const captureMs = Math.min(REVERSE_MAX_AGE_MS, Math.max(0, now - reversePhaseStartMs)); 449 + reversePhaseStartMs = now; 450 + stopReversePlayback(sound); 451 + if (captureMs < 40) return false; 452 + 453 + const snapshot = sound.speaker.getRecentBuffer(captureMs / 1000); 454 + const src = snapshot?.data; 455 + const rate = snapshot?.rate || 0; 456 + if (!src || src.length < REVERSE_MIN_BUFFER_SAMPLES || rate <= 0) return false; 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]; 461 + } 462 + 463 + sound.replay.loadData(reversed, rate); 464 + reversePlaybackSound = sound.replay.play({ 465 + tone: reversePlaybackTone(), 466 + base: SAMPLE_BASE_FREQ, 467 + volume: 1.0, 468 + pan: 0.0, 469 + loop: false, 493 470 }); 471 + return !!reversePlaybackSound; 494 472 } 495 473 496 474 // Returns the master volume for the grid that produced a note, based on the ··· 619 597 if (drumFlashes.length > 8) drumFlashes.shift(); 620 598 } 621 599 622 - // Fire a drum hit from short auto-stopping synth voices. No cleanup 623 - // needed on key-up because every voice uses a finite duration. 600 + function makePercussionHold(letter, volume, pan, pitchFactor, needsRelease = true) { 601 + return { letter, volume, pan, pitchFactor, needsRelease }; 602 + } 603 + 604 + function releasePercussionHold(sound, hold) { 605 + if (!hold?.needsRelease) return; 606 + playPercussion(sound, hold.letter, hold.volume, hold.pan, hold.pitchFactor, "up"); 607 + } 608 + 609 + function triggerPercussionDown(sound, letter, octaveValue, volume, pan, pitchFactor, key, drumName) { 610 + const bankSample = wave === "sample" ? percussionSampleBank[drumName] : null; 611 + let hold; 612 + 613 + if (wave === "sample" && bankSample) { 614 + if (bankSample !== lastLoadedSample) { 615 + sound.sample.loadData(bankSample.data, bankSample.rate); 616 + lastLoadedSample = bankSample; 617 + } 618 + sound.sample.play({ 619 + tone: SAMPLE_BASE_FREQ * pitchFactor, 620 + base: SAMPLE_BASE_FREQ, 621 + volume, pan, loop: false, 622 + }); 623 + hold = makePercussionHold(letter, volume, pan, pitchFactor, false); 624 + } else { 625 + playPercussion(sound, letter, volume, pan, pitchFactor, "down"); 626 + hold = makePercussionHold(letter, volume, pan, pitchFactor, true); 627 + } 628 + 629 + flashDrum(letter); 630 + recordPlayback({ kind: "drum", letter, octave: octaveValue, vel: volume, pan, pitch: pitchFactor }); 631 + if (key) trail[key] = { note: letter, octave: octaveValue, brightness: 1.0 }; 632 + return hold; 633 + } 634 + 635 + // Fire a drum hit from short auto-stopping synth voices. `phase` controls 636 + // whether we emit only the strike (`down`), only the release/tail (`up`), 637 + // or the legacy combined hit (`both`, still used by reverse playback). 624 638 // 625 639 // EVERY drum in the kit is a TWO-STEP hit — DOWN (impact/strike) then 626 - // UP (release/resonance/tail). The gap between the two steps is locked 627 - // to `flam` (BPM-derived) with per-hit jitter, so hits feel unified but 628 - // stretch/shrink with tempo. This gives every pad a natural percussive 629 - // arc instead of a single simultaneous layer stack. 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. 630 643 // 631 644 // Every drum also gets STOCHASTIC variation on each press so repeated 632 645 // hits don't sound machine-stamped: ··· 634 647 // - volume jitter: ±5-12% of the nominal volume 635 648 // - duration jitter: ±5-14% on longer tails 636 649 // - pan offset jitter on each step for subtle stereo movement 637 - // - the DOWN→UP gap itself jitters via rn(...) 650 + // - reverse-playback DOWN→UP gap jitters via rn(...) 638 651 // 639 652 // Multi-burst flam timing scales with metronomeBPM so drum rolls lock 640 653 // to the current tempo: at 120 BPM each sub-beat is ~12.5 ms. 641 - function playPercussion(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0) { 654 + function playPercussion(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both") { 642 655 if (!sound?.synth) return; 643 656 const v = Math.max(0.1, Math.min(2.2, volume)); 644 657 const pf = Math.max(0.25, Math.min(4, pitchFactor)); 658 + const fireDown = phase !== "up"; 659 + const fireUp = phase !== "down"; 645 660 646 661 // Per-hit random helpers (inline, stateless). `rj` jitters around a center 647 662 // by a ± fraction; `rn` returns a uniform range. ··· 652 667 // so this is ~12.5 ms at default. Scales inversely: 60 bpm → 25 ms, 653 668 // 180 bpm → ~8 ms. Used for clap / future multi-burst drums. 654 669 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(); 674 + }; 655 675 656 676 switch (letter) { 657 677 case "c": // kick — two-step: DOWN (beater punch) + UP (sub wobble bloom) 658 678 // STEP 1 — DOWN: beater click + body thump + sawtooth grit. The sharp 659 679 // impact that gives the kick its attack. All front-loaded. 660 - { 680 + if (fireDown) { 661 681 const downPan = pan + rn(-0.04, 0.04); 662 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 }); 663 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 }); ··· 668 688 // expands outward. Two sines ~10 Hz apart for per-hit beat-freq jitter, 669 689 // plus a mid-low square fill. Very tight gap (~0.4 flam) so the punch 670 690 // and bloom still feel like one unified kick hit. 671 - setTimeout(() => { 691 + playUp(Math.round(flam * rn(0.3, 0.6)), () => { 672 692 const upPan = pan + rn(-0.02, 0.02); 673 693 const subLo = rj(44, 0.05); // ~41.8 → 46.2 Hz 674 694 const subHi = subLo + rn(8, 12); // ~50 → 58 Hz → beat freq 8-12 Hz 675 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 }); 676 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 }); 677 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 }); 678 - }, Math.round(flam * rn(0.3, 0.6))); 698 + }); 679 699 break; 680 700 681 701 case "d": // snare — two-step: DOWN (stick hit) + UP (wire rattle) 682 702 // STEP 1 — DOWN: sharp stick-on-head impact. 683 - { 703 + if (fireDown) { 684 704 const downPan = pan + rn(-0.04, 0.04); 685 705 sound.synth({ type: "square", tone: rj(800, 0.08) * pf, duration: 0.008, volume: rj(0.70, 0.08) * v, attack: 0.0003, decay: 0.007, pan: downPan }); 686 706 sound.synth({ type: "noise", tone: rj(3500, 0.10) * pf, duration: 0.006, volume: rj(0.60, 0.10) * v, attack: 0.0003, decay: 0.0055, pan: downPan }); 687 707 } 688 708 // STEP 2 — UP: wire rattle sustain + shell body ring. 689 - setTimeout(() => { 709 + playUp(Math.round(flam * rn(0.3, 0.6)), () => { 690 710 const upPan = pan + rn(-0.04, 0.04); 691 711 sound.synth({ type: "noise", tone: rj(2200, 0.08) * pf, duration: rj(0.12, 0.08), volume: rj(0.50, 0.08) * v, attack: 0.001, decay: 0.11, pan: upPan }); 692 712 sound.synth({ type: "triangle", tone: rj(220, 0.04) * pf, duration: rj(0.1, 0.08), volume: rj(0.38, 0.08) * v, attack: 0.001, decay: 0.09, pan: upPan }); 693 713 sound.synth({ type: "square", tone: rj(180, 0.04) * pf, duration: rj(0.05, 0.10), volume: rj(0.20, 0.10) * v, attack: 0.001, decay: 0.045, pan: upPan }); 694 - }, Math.round(flam * rn(0.3, 0.6))); 714 + }); 695 715 break; 696 716 697 717 case "e": // clap — two-step: DOWN (dark palm strike) then UP (bright release) 698 718 // STEP 1 — DOWN: palms meet. Darker body thump + low-mid noise burst. 699 719 // Lower-pitched, meatier, slightly left of center for stereo interest. 700 - { 720 + if (fireDown) { 701 721 const downPan = pan + rn(-0.08, 0.02); 702 722 sound.synth({ type: "noise", tone: rj(900, 0.10) * pf, duration: 0.010, volume: rj(0.85, 0.08) * v, attack: 0.0003, decay: 0.009, pan: downPan }); 703 723 sound.synth({ type: "square", tone: rj(260, 0.06) * pf, duration: 0.012, volume: rj(0.55, 0.10) * v, attack: 0.0004, decay: 0.011, pan: downPan }); ··· 706 726 // STEP 2 — UP: hands separate, bright transient + airy tail. 707 727 // Delayed by ~1.5 flam units (scales with BPM) with jitter so hits vary. 708 728 // Panned slightly opposite for L/R call-response feel. 709 - setTimeout(() => { 729 + playUp(Math.round(flam * rn(1.3, 1.7)), () => { 710 730 const upPan = pan + rn(-0.02, 0.10); 711 731 sound.synth({ type: "square", tone: rj(3200, 0.10) * pf, duration: 0.004, volume: rj(0.80, 0.08) * v, attack: 0.0002, decay: 0.0035, pan: upPan }); 712 732 sound.synth({ type: "noise", tone: rj(5200, 0.12) * pf, duration: 0.008, volume: rj(0.70, 0.10) * v, attack: 0.0003, decay: 0.007, pan: upPan }); 713 733 sound.synth({ type: "noise", tone: rj(2400, 0.08) * pf, duration: 0.018, volume: rj(0.45, 0.10) * v, attack: 0.0005, decay: 0.017, pan: upPan }); 714 - }, Math.round(flam * rn(1.3, 1.7))); 734 + }); 715 735 break; 716 736 717 737 case "f": // snap — two-step: DOWN (finger click) + UP (skin slap tail) 718 738 // STEP 1 — DOWN: sharp high-frequency click from compressed fingertip 719 739 // release. This is the "snap" itself — very brief. 720 - { 740 + if (fireDown) { 721 741 const downPan = pan + rn(-0.05, 0.05); 722 742 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 }); 723 743 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 }); 724 744 } 725 745 // STEP 2 — UP: finger slaps palm — brief hollow body ring. 726 - setTimeout(() => { 746 + playUp(Math.round(flam * rn(0.4, 0.7)), () => { 727 747 const upPan = pan + rn(-0.03, 0.05); 728 748 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 }); 729 749 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 }); 730 - }, Math.round(flam * rn(0.4, 0.7))); 750 + }); 731 751 break; 732 752 733 753 case "g": // closed hi-hat — two-step: DOWN (stick tick) + UP (tight sizzle) 734 754 // STEP 1 — DOWN: stick contact tick, bright transient metal spike. 735 - { 755 + if (fireDown) { 736 756 const downPan = pan + rn(-0.04, 0.04); 737 757 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 }); 738 758 } 739 759 // STEP 2 — UP: brief metallic sizzle release. Much shorter than open 740 760 // hat since the cymbal is clamped shut. 741 - setTimeout(() => { 761 + playUp(Math.round(flam * rn(0.3, 0.5)), () => { 742 762 const upPan = pan + rn(-0.04, 0.04); 743 763 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 }); 744 764 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 }); 745 - }, Math.round(flam * rn(0.3, 0.5))); 765 + }); 746 766 break; 747 767 748 768 case "a": // open hi-hat — two-step: DOWN (chip strike) then UP (airy shimmer) 749 769 // STEP 1 — DOWN: short metallic chip — initial cymbal contact. 750 - { 770 + if (fireDown) { 751 771 const downPan = pan + rn(-0.06, 0.04); 752 772 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 }); 753 773 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 }); 754 774 } 755 775 // STEP 2 — UP: sustained airy shimmer that blooms after the chip. Decay 756 776 // + brightness vary per hit, panned slightly opposite for call/response. 757 - setTimeout(() => { 777 + playUp(Math.round(flam * rn(0.8, 1.2)), () => { 758 778 const upPan = pan + rn(-0.02, 0.08); 759 779 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 }); 760 780 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 }); 761 - }, Math.round(flam * rn(0.8, 1.2))); 781 + }); 762 782 break; 763 783 764 784 case "b": // ride — two-step: DOWN (bell ping) + UP (shimmer wash) 765 785 // STEP 1 — DOWN: bell-like ping strike — the initial mallet contact. 766 - { 786 + if (fireDown) { 767 787 const downPan = pan + rn(-0.05, 0.05); 768 788 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 }); 769 789 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 }); 770 790 } 771 791 // STEP 2 — UP: sustained metallic shimmer wash — the cymbal body 772 792 // resonating after the strike. Longest tail of the kit. 773 - setTimeout(() => { 793 + playUp(Math.round(flam * rn(0.5, 0.8)), () => { 774 794 const upPan = pan + rn(-0.03, 0.03); 775 795 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 }); 776 796 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 }); 777 - }, Math.round(flam * rn(0.5, 0.8))); 797 + }); 778 798 break; 779 799 780 800 case "c#": // crash — two-step: DOWN (metal impact) + UP (long wash) 781 801 // STEP 1 — DOWN: explosive high-brightness metal strike. 782 - { 802 + if (fireDown) { 783 803 const downPan = pan + rn(-0.06, 0.06); 784 804 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 }); 785 805 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 }); 786 806 } 787 807 // STEP 2 — UP: long sustained wash. Where the energy actually lives. 788 - setTimeout(() => { 808 + playUp(Math.round(flam * rn(0.4, 0.7)), () => { 789 809 const upPan = pan + rn(-0.04, 0.04); 790 810 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 }); 791 811 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 }); 792 812 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 }); 793 - }, Math.round(flam * rn(0.4, 0.7))); 813 + }); 794 814 break; 795 815 796 816 case "d#": // splash — two-step: DOWN (thin metal tick) + UP (short wash) 797 817 // STEP 1 — DOWN: thin high metal contact — the splash's sharp front. 798 - { 818 + if (fireDown) { 799 819 const downPan = pan + rn(-0.05, 0.05); 800 820 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 }); 801 821 } 802 822 // STEP 2 — UP: short bright wash — splash is all quick burst, no sustain. 803 - setTimeout(() => { 823 + playUp(Math.round(flam * rn(0.4, 0.6)), () => { 804 824 const upPan = pan + rn(-0.03, 0.03); 805 825 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 }); 806 826 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 }); 807 - }, Math.round(flam * rn(0.4, 0.6))); 827 + }); 808 828 break; 809 829 810 830 case "f#": // cowbell — two-step: DOWN (stick strike) + UP (resonant ring) 811 831 // STEP 1 — DOWN: stick-on-metal strike — the "tink" attack. 812 - { 832 + if (fireDown) { 813 833 const downPan = pan + rn(-0.04, 0.04); 814 834 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 }); 815 835 } 816 836 // STEP 2 — UP: the signature cowbell ring — two detuned squares beat. 817 - setTimeout(() => { 837 + playUp(Math.round(flam * rn(0.3, 0.5)), () => { 818 838 const upPan = pan + rn(-0.03, 0.03); 819 839 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 }); 820 840 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 }); 821 - }, Math.round(flam * rn(0.3, 0.5))); 841 + }); 822 842 break; 823 843 824 844 case "g#": // wood block — two-step: DOWN (stick tick) + UP (hollow body) 825 845 // STEP 1 — DOWN: stick contact tick — the initial bright transient. 826 - { 846 + if (fireDown) { 827 847 const downPan = pan + rn(-0.04, 0.04); 828 848 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 }); 829 849 } 830 850 // STEP 2 — UP: hollow body ring — the wood resonance. 831 - setTimeout(() => { 851 + playUp(Math.round(flam * rn(0.3, 0.5)), () => { 832 852 const upPan = pan + rn(-0.03, 0.03); 833 853 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 }); 834 854 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 }); 835 - }, Math.round(flam * rn(0.3, 0.5))); 855 + }); 836 856 break; 837 857 838 858 case "a#": // tambourine — two-step: DOWN (frame strike) + UP (jingle rattle) 839 859 // STEP 1 — DOWN: hand/stick on frame — the dry initial hit. 840 - { 860 + if (fireDown) { 841 861 const downPan = pan + rn(-0.05, 0.05); 842 862 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 }); 843 863 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 }); 844 864 } 845 865 // STEP 2 — UP: the jingle rattle — where the tambourine character lives. 846 866 // Max per-hit jitter since real tambourines never sound the same twice. 847 - setTimeout(() => { 867 + playUp(Math.round(flam * rn(0.4, 0.7)), () => { 848 868 const upPan = pan + rn(-0.05, 0.05); 849 869 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 }); 850 870 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 }); 851 871 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 }); 852 - }, Math.round(flam * rn(0.4, 0.7))); 872 + }); 853 873 break; 854 874 } 855 875 } ··· 976 996 delete sounds[key]; 977 997 } 978 998 999 + function releaseTouchNote(pid, sound, system, fade = 0.08) { 1000 + const entry = touchNotes[pid]; 1001 + if (!entry) return; 1002 + if (entry.drumHold) { 1003 + releasePercussionHold(sound, entry.drumHold); 1004 + } else if (entry.key) { 1005 + stopSoundKey(entry.key, sound, system, fade); 1006 + } 1007 + delete touchNotes[pid]; 1008 + } 1009 + 979 1010 function stopAllSounds(sound, system, fade = 0.08) { 980 1011 heldKeys.clear(); 981 1012 holdActive = false; 1013 + stopReversePlayback(sound); 982 1014 for (const key of Object.keys(sounds)) stopSoundKey(key, sound, system, fade); 1015 + activeDrumKeys = {}; 983 1016 touchNotes = {}; 984 1017 system?.usbMidi?.allNotesOff?.(0); 985 1018 sounds = {}; ··· 1272 1305 return; 1273 1306 } 1274 1307 if (key === "space") { 1275 - // Space = INSTANT REPLAY IN REVERSE (loop pedal semantics). 1276 - // Press: snapshot current history, reset the phase, start firing events 1277 - // backwards. The fallback kick drum only plays if there's nothing to 1278 - // reverse (empty history — first-press behavior). 1279 - // Release: stop the reverse queue. What got recorded during the press 1280 - // becomes the next phase's source. 1308 + // Space = true reverse replay from the recent audio buffer. 1281 1309 if (!spaceHeld) { 1282 1310 spaceHeld = true; 1283 - if (playbackHistory.length > 0) { 1284 - startReversePlayback(); 1285 - } else if (sound && sound.synth) { 1286 - // Fallback kick drum when there's no history to reverse. 1311 + if (!startReversePlayback(sound) && sound && sound.synth) { 1312 + // Fallback kick drum when there's no recent audio to reverse. 1287 1313 sound.synth({ type: "sine", tone: 150, duration: 0.15, volume: 0.9, attack: 0.001, decay: 0.14, pan: 0.0 }); 1288 1314 sound.synth({ type: "sine", tone: 60, duration: 0.2, volume: 0.7, attack: 0.001, decay: 0.19, pan: 0.0 }); 1289 1315 } ··· 1467 1493 // in `sounds[key]`; the visual flash is driven purely by `trail`. 1468 1494 const drumName = percussionDrumFor(letter, offset); 1469 1495 if (drumName) { 1496 + if (activeDrumKeys[key]) return; 1470 1497 // Per-drum sampling: End armed + drum key = record to this drum slot. 1471 1498 if (wave === "sample" && endArmed && !perKeyRecording && !recording) { 1472 1499 const ok = !!sound?.microphone?.rec?.(); ··· 1490 1517 // Drums get their own pan (kit geometry + grid bias), not the 1491 1518 // pitch-based tone pan calculated above. 1492 1519 const drumPan = drumPanFor(letter, offset); 1493 - const bankSample = percussionSampleBank[drumName]; 1494 - if (wave === "sample" && bankSample) { 1495 - if (bankSample !== lastLoadedSample) { 1496 - sound.sample.loadData(bankSample.data, bankSample.rate); 1497 - lastLoadedSample = bankSample; 1498 - } 1499 - sound.sample.play({ 1500 - tone: SAMPLE_BASE_FREQ * drumPitch, 1501 - base: SAMPLE_BASE_FREQ, 1502 - volume: drumVol, pan: drumPan, loop: false, 1503 - }); 1504 - } else { 1505 - playPercussion(sound, letter, drumVol, drumPan, drumPitch); 1506 - } 1507 - flashDrum(letter); 1508 - recordPlayback({ kind: "drum", letter, octave: noteOctave, vel: drumVol, pan: drumPan, pitch: drumPitch }); 1520 + activeDrumKeys[key] = triggerPercussionDown( 1521 + sound, letter, noteOctave, drumVol, drumPan, drumPitch, key, drumName 1522 + ); 1509 1523 trail[key] = { note: letter, octave: noteOctave, brightness: velocity }; 1510 1524 return; 1511 1525 } ··· 1538 1552 }); 1539 1553 rememberSound(key, { synth, note: letter, octave: noteOctave, baseFreq: freq, gridOffset: offset, baseVol }, system, velocity); 1540 1554 } 1541 - // Record for the reverse-playback loop. Captures the parameters of 1542 - // the user's hit so the next space press can replay it backwards. 1555 + // Legacy no-op hook from the older event-trigger reverse loop path. 1543 1556 recordPlayback({ 1544 1557 kind: "note", letter, octave: noteOctave, freq, vel: velocity, pan, 1545 1558 wave, pitch: Math.pow(2, effectivePitchShift()), ··· 1555 1568 // press ends up defining the next loop's length. 1556 1569 if (key === "space") { 1557 1570 spaceHeld = false; 1558 - stopReversePlayback(); 1571 + stopReversePlayback(sound); 1559 1572 return; 1560 1573 } 1561 1574 // F11 release — exit flourish mode (new notes will auto-latch again) ··· 1605 1618 perKeyRecording = null; 1606 1619 return; 1607 1620 } 1621 + if (activeDrumKeys[key]) { 1622 + releasePercussionHold(sound, activeDrumKeys[key]); 1623 + delete activeDrumKeys[key]; 1624 + return; 1625 + } 1608 1626 if (sounds[key]) { 1609 1627 // Don't stop held/latched notes on key release 1610 1628 if (heldKeys.has(key)) return; ··· 1747 1765 else s.synth.update({ tone: s.baseFreq * factor }); 1748 1766 } 1749 1767 } 1768 + updateReversePlaybackPitch(); 1750 1769 return; 1751 1770 } 1752 1771 // Per-side master volume sliders (vertical bars flanking the grids) ··· 1824 1843 const touchDrumPitch = Math.pow(2, effectivePitchShift()); 1825 1844 // Drums pan by kit geometry + grid bias, not pitch-based tone pan. 1826 1845 const touchDrumPan = drumPanFor(hitNote.letter, hitNote.gridOffset); 1827 - const bankSample = percussionSampleBank[touchDrum]; 1828 - if (wave === "sample" && bankSample) { 1829 - if (bankSample !== lastLoadedSample) { 1830 - sound.sample.loadData(bankSample.data, bankSample.rate); 1831 - lastLoadedSample = bankSample; 1832 - } 1833 - sound.sample.play({ 1834 - tone: SAMPLE_BASE_FREQ * touchDrumPitch, 1835 - base: SAMPLE_BASE_FREQ, 1836 - volume: 1.6, pan: touchDrumPan, loop: false, 1837 - }); 1838 - } else { 1839 - playPercussion(sound, hitNote.letter, 1.8, touchDrumPan, touchDrumPitch); 1840 - } 1841 - flashDrum(hitNote.letter); 1842 - recordPlayback({ kind: "drum", letter: hitNote.letter, octave: hitNote.octave, vel: 1.8, pan: touchDrumPan, pitch: touchDrumPitch }); 1846 + const drumHold = triggerPercussionDown( 1847 + sound, hitNote.letter, hitNote.octave, 1.8, touchDrumPan, touchDrumPitch, hitNote.key, touchDrum 1848 + ); 1843 1849 trail[hitNote.key] = { note: hitNote.letter, octave: hitNote.octave, brightness: 1.0 }; 1844 - touchNotes[pid] = { key: hitNote.key }; 1850 + touchNotes[pid] = { key: hitNote.key, drumHold }; 1845 1851 return; 1846 1852 } 1847 1853 let synth; ··· 1935 1941 else s.synth.update({ tone: s.baseFreq * factor }); 1936 1942 } 1937 1943 } 1944 + updateReversePlaybackPitch(); 1938 1945 } 1939 1946 if (volDragging) { 1940 1947 const vb = globalThis.__volBar; ··· 1962 1969 const hitNote = hitTestGrid(x, y, gridInfo); 1963 1970 if (hitNote && hitNote.key && hitNote.key !== touchNotes[pid]?.key) { 1964 1971 // Release current note 1965 - const oldKey = touchNotes[pid].key; 1966 - stopSoundKey(oldKey, sound, system, 0.02); 1972 + releaseTouchNote(pid, sound, system, 0.02); 1967 1973 // Trigger new note 1968 1974 if (!sounds[hitNote.key]) { 1969 1975 const freq = noteToFreq(hitNote.letter, hitNote.octave); ··· 1976 1982 const rollDrumPitch = Math.pow(2, effectivePitchShift()); 1977 1983 // Drums pan by kit geometry + grid bias, not pitch-based tone pan. 1978 1984 const rollDrumPan = drumPanFor(hitNote.letter, hitNote.gridOffset); 1979 - const bankSample = percussionSampleBank[rollDrum]; 1980 - if (wave === "sample" && bankSample) { 1981 - if (bankSample !== lastLoadedSample) { 1982 - sound.sample.loadData(bankSample.data, bankSample.rate); 1983 - lastLoadedSample = bankSample; 1984 - } 1985 - sound.sample.play({ 1986 - tone: SAMPLE_BASE_FREQ * rollDrumPitch, 1987 - base: SAMPLE_BASE_FREQ, 1988 - volume: 1.6, pan: rollDrumPan, loop: false, 1989 - }); 1990 - } else { 1991 - playPercussion(sound, hitNote.letter, 1.8, rollDrumPan, rollDrumPitch); 1992 - } 1993 - flashDrum(hitNote.letter); 1994 - recordPlayback({ kind: "drum", letter: hitNote.letter, octave: hitNote.octave, vel: 1.8, pan: rollDrumPan, pitch: rollDrumPitch }); 1985 + const drumHold = triggerPercussionDown( 1986 + sound, hitNote.letter, hitNote.octave, 1.8, rollDrumPan, rollDrumPitch, hitNote.key, rollDrum 1987 + ); 1995 1988 trail[hitNote.key] = { note: hitNote.letter, octave: hitNote.octave, brightness: 1.0 }; 1996 - touchNotes[pid] = { key: hitNote.key }; 1989 + touchNotes[pid] = { key: hitNote.key, drumHold }; 1997 1990 return; 1998 1991 } 1999 1992 let synth; ··· 2051 2044 if (brtDragging) brtDragging = false; 2052 2045 // Release touch-triggered note 2053 2046 if (touchNotes[pid]) { 2054 - const key = touchNotes[pid].key; 2055 - stopSoundKey(key, sound, system, 0.08); 2056 - delete touchNotes[pid]; 2047 + releaseTouchNote(pid, sound, system, 0.08); 2057 2048 } 2058 2049 } 2059 2050 } ··· 2124 2115 } 2125 2116 } 2126 2117 } 2118 + updateReversePlaybackPitch(); 2127 2119 } 2128 2120 } 2129 2121 ··· 3997 3989 function sim({ pressures, sound }) { 3998 3990 // Flush any due setTimeout callbacks (polyfill for missing QuickJS timer) 3999 3991 __tickPendingTimeouts(); 4000 - // Fire any reverse-playback events whose scheduled time has arrived. This 4001 - // runs every frame so the timing resolution is ~16 ms (fine for percussive 4002 - // replay). We drain in a while loop so multiple due events in the same 4003 - // frame all fire. 4004 - if (reverseQueue.length > 0) { 4005 - const nowMs = Date.now(); 4006 - while (reverseQueue.length > 0 && reverseQueue[0].playAtMs <= nowMs) { 4007 - const { ev } = reverseQueue.shift(); 4008 - playReverseEvent(ev, sound); 4009 - } 4010 - } 4011 3992 // Auto-stop recording at max duration 4012 3993 if (recording && (Date.now() - recStartTime) / 1000 >= MAX_REC_SECS) { 4013 3994 stopSampleRecording(sound, "max-duration");
+217 -70
fedac/native/src/audio.c
··· 326 326 static unsigned long xrun_count = 0; 327 327 static unsigned long short_write_count = 0; 328 328 329 + static void mix_sample_voice(SampleVoice *sv, const float *buf, int slen, int smax, 330 + double rate, double *mix_l, double *mix_r) { 331 + if (!sv || !sv->active || !buf || slen <= 0 || smax <= 0) { 332 + if (sv) sv->active = 0; 333 + return; 334 + } 335 + 336 + if (slen > smax) slen = smax; 337 + 338 + // Fade envelope (5ms attack/release at output rate) 339 + double fade_speed = 1.0 / (0.005 * rate); 340 + if (sv->fade < sv->fade_target) { 341 + sv->fade += fade_speed; 342 + if (sv->fade > sv->fade_target) sv->fade = sv->fade_target; 343 + } else if (sv->fade > sv->fade_target) { 344 + sv->fade -= fade_speed; 345 + if (sv->fade <= 0.0) { sv->fade = 0.0; sv->active = 0; return; } 346 + } 347 + 348 + // Pan controls both amplitude and a small Haas-style stereo offset. 349 + double delay_samps = sv->pan * 0.0004 * rate; 350 + double pos_l = sv->position - (delay_samps > 0 ? delay_samps : 0); 351 + double pos_r = sv->position + (delay_samps > 0 ? 0 : delay_samps); 352 + if (pos_l < 0) pos_l = 0; 353 + if (pos_r < 0) pos_r = 0; 354 + 355 + int p0l = (int)pos_l; 356 + if (sv->loop) { 357 + p0l = ((p0l % slen) + slen) % slen; 358 + } else if (p0l >= slen) { 359 + sv->active = 0; 360 + return; 361 + } 362 + int p1l = p0l + 1; 363 + if (p1l >= slen) p1l = sv->loop ? 0 : p0l; 364 + if (p0l >= smax || p1l >= smax) { sv->active = 0; return; } 365 + double fl = pos_l - p0l; 366 + double samp_l = buf[p0l] * (1.0 - fl) + buf[p1l] * fl; 367 + 368 + int p0r = (int)pos_r; 369 + if (sv->loop) { 370 + p0r = ((p0r % slen) + slen) % slen; 371 + } else if (p0r >= slen) { 372 + p0r = slen - 1; 373 + } 374 + if (p0r < 0) p0r = 0; 375 + int p1r = p0r + 1; 376 + if (p1r >= slen) p1r = sv->loop ? 0 : p0r; 377 + if (p0r >= smax || p1r >= smax) { sv->active = 0; return; } 378 + double fr = pos_r - p0r; 379 + double samp_r = buf[p0r] * (1.0 - fr) + buf[p1r] * fr; 380 + 381 + double vol = sv->volume * sv->fade; 382 + double l_gain = sv->pan <= 0 ? 1.0 : 1.0 - sv->pan * 0.6; 383 + double r_gain = sv->pan >= 0 ? 1.0 : 1.0 + sv->pan * 0.6; 384 + *mix_l += samp_l * vol * l_gain; 385 + *mix_r += samp_r * vol * r_gain; 386 + 387 + sv->position += sv->speed; 388 + if (sv->position >= slen) { 389 + if (sv->loop) { 390 + while (sv->position >= slen) sv->position -= slen; 391 + } else { 392 + sv->active = 0; 393 + } 394 + } else if (sv->position < 0.0) { 395 + if (sv->loop) { 396 + while (sv->position < 0.0) sv->position += slen; 397 + } else { 398 + sv->active = 0; 399 + } 400 + } 401 + } 402 + 329 403 static void *audio_thread_fn(void *arg) { 330 404 ACAudio *audio = (ACAudio *)arg; 331 405 const unsigned int period_frames = audio->actual_period ? audio->actual_period : AUDIO_PERIOD_SIZE; ··· 398 472 // Lock already held from line 246 — safe to read sample_buf 399 473 for (int v = 0; v < AUDIO_MAX_SAMPLE_VOICES; v++) { 400 474 SampleVoice *sv = &audio->sample_voices[v]; 401 - if (!sv->active) continue; 402 - 403 - // Fade envelope (5ms attack/release at output rate) 404 - double fade_speed = 1.0 / (0.005 * rate); 405 - if (sv->fade < sv->fade_target) { 406 - sv->fade += fade_speed; 407 - if (sv->fade > sv->fade_target) sv->fade = sv->fade_target; 408 - } else if (sv->fade > sv->fade_target) { 409 - sv->fade -= fade_speed; 410 - if (sv->fade <= 0.0) { sv->fade = 0.0; sv->active = 0; continue; } 411 - } 412 - 413 - // Stereo spread: micro-delay between L/R (Haas effect) 414 - // Pan controls both amplitude and a small time offset 415 - // giving mono samples a wide stereo image. 416 - double delay_samps = sv->pan * 0.0004 * rate; // ~0.4ms max at pan=1 417 - double pos_l = sv->position - (delay_samps > 0 ? delay_samps : 0); 418 - double pos_r = sv->position + (delay_samps > 0 ? 0 : delay_samps); 419 - if (pos_l < 0) pos_l = 0; 420 - if (pos_r < 0) pos_r = 0; 421 - 422 - // Interpolated read for L channel 423 - int slen = audio->sample_len; 424 - int smax = audio->sample_max_len; 425 - if (slen <= 0 || smax <= 0) { sv->active = 0; continue; } 426 - // Clamp slen to buffer bounds (sample_len may change mid-read) 427 - if (slen > smax) slen = smax; 428 - int p0l = (int)pos_l; 429 - if (sv->loop) { 430 - p0l = ((p0l % slen) + slen) % slen; 431 - } else if (p0l >= slen) { 432 - sv->active = 0; continue; 433 - } 434 - int p1l = p0l + 1; if (p1l >= slen) p1l = sv->loop ? 0 : p0l; 435 - // Final bounds check against actual buffer size 436 - if (p0l >= smax || p1l >= smax) { sv->active = 0; continue; } 437 - double fl = pos_l - p0l; 438 - double samp_l = audio->sample_buf[p0l] * (1.0 - fl) 439 - + audio->sample_buf[p1l] * fl; 475 + mix_sample_voice(sv, audio->sample_buf, audio->sample_len, audio->sample_max_len, 476 + rate, &mix_l, &mix_r); 477 + } 440 478 441 - // Interpolated read for R channel 442 - int p0r = (int)pos_r; 443 - if (sv->loop) { 444 - p0r = ((p0r % slen) + slen) % slen; 445 - } else if (p0r >= slen) { 446 - p0r = slen - 1; 447 - } 448 - if (p0r < 0) p0r = 0; 449 - int p1r = p0r + 1; if (p1r >= slen) p1r = sv->loop ? 0 : p0r; 450 - if (p0r >= smax || p1r >= smax) { sv->active = 0; continue; } 451 - double fr = pos_r - p0r; 452 - double samp_r = audio->sample_buf[p0r] * (1.0 - fr) 453 - + audio->sample_buf[p1r] * fr; 454 - 455 - double vol = sv->volume * sv->fade; 456 - double l_gain = sv->pan <= 0 ? 1.0 : 1.0 - sv->pan * 0.6; 457 - double r_gain = sv->pan >= 0 ? 1.0 : 1.0 + sv->pan * 0.6; 458 - mix_l += samp_l * vol * l_gain; 459 - mix_r += samp_r * vol * r_gain; 460 - 461 - sv->position += sv->speed; 462 - if (sv->position >= audio->sample_len) { 463 - if (sv->loop && audio->sample_len > 0) { 464 - // Wrap for seamless looping 465 - while (sv->position >= audio->sample_len) 466 - sv->position -= audio->sample_len; 467 - } else { 468 - sv->active = 0; // one-shot 469 - } 470 - } 471 - } 479 + // Dedicated global replay voice. Uses its own buffer so reverse 480 + // playback can overlap normal sample-bank activity. 481 + mix_sample_voice(&audio->replay_voice, audio->replay_buf, 482 + audio->replay_len, audio->replay_max_len, 483 + rate, &mix_l, &mix_r); 472 484 // (lock released at end of buffer loop) 473 485 474 486 // Mix DJ deck audio (lock-free: single consumer = audio thread) ··· 535 547 536 548 // Save dry signal before FX chain 537 549 double dry_l = mix_l, dry_r = mix_r; 550 + 551 + // Capture recent dry output for true reverse replay. This stores 552 + // the actual mixed audio (not note events) before room/glitch/TTS 553 + // so the reverse replay can run back through the live FX chain. 554 + if (audio->output_history_buf && audio->output_history_size > 0) { 555 + unsigned int stride = audio->output_history_downsample_n; 556 + if (stride == 0) stride = 1; 557 + audio->output_history_downsample_pos++; 558 + if (audio->output_history_downsample_pos >= stride) { 559 + audio->output_history_downsample_pos = 0; 560 + uint64_t wp = audio->output_history_write_pos; 561 + audio->output_history_buf[wp % (uint64_t)audio->output_history_size] = 562 + (float)((dry_l + dry_r) * 0.5); 563 + audio->output_history_write_pos = wp + 1; 564 + } 565 + } 538 566 539 567 // Room (reverb) effect — tap delays based on actual sample rate 540 568 if (audio->room_enabled && audio->room_buf_l) { ··· 818 846 audio->sample_len = 0; 819 847 audio->sample_rate = 48000; // default, overwritten by actual capture rate 820 848 audio->sample_next_id = 1; 849 + audio->replay_max_len = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS; 850 + audio->replay_buf = calloc(audio->replay_max_len, sizeof(float)); 851 + audio->replay_buf_back = calloc(audio->replay_max_len, sizeof(float)); 852 + audio->replay_len = 0; 853 + audio->replay_rate = AUDIO_OUTPUT_HISTORY_RATE; 854 + memset(&audio->replay_voice, 0, sizeof(audio->replay_voice)); 821 855 audio->mic_connected = 0; 822 856 audio->mic_hot = 0; 823 857 audio->mic_level = 0.0f; ··· 828 862 audio->mic_ring = calloc(audio->sample_max_len, sizeof(float)); 829 863 audio->mic_ring_pos = 0; 830 864 audio->rec_start_ring_pos = 0; 865 + audio->output_history_buf = calloc(AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS, sizeof(float)); 866 + audio->output_history_size = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS; 867 + audio->output_history_rate = AUDIO_OUTPUT_HISTORY_RATE; 868 + audio->output_history_downsample_n = 1; 869 + audio->output_history_downsample_pos = 0; 870 + audio->output_history_write_pos = 0; 831 871 snprintf(audio->mic_device, sizeof(audio->mic_device), "none"); 832 872 audio->mic_last_error[0] = 0; 833 873 seed_default_sample(audio); ··· 1028 1068 // Update glitch rate for actual sample rate 1029 1069 audio->glitch_rate = rate / 1600; 1030 1070 1071 + // Recent output history targets ~48k mono regardless of playback rate. 1072 + unsigned int hist_target = rate > AUDIO_OUTPUT_HISTORY_RATE ? AUDIO_OUTPUT_HISTORY_RATE : rate; 1073 + unsigned int hist_stride = rate > hist_target ? (rate + hist_target / 2) / hist_target : 1; 1074 + if (hist_stride == 0) hist_stride = 1; 1075 + audio->output_history_rate = rate / hist_stride; 1076 + if (audio->output_history_rate == 0) audio->output_history_rate = rate; 1077 + audio->output_history_downsample_n = hist_stride; 1078 + audio->output_history_downsample_pos = 0; 1079 + audio->output_history_size = (int)(audio->output_history_rate * AUDIO_OUTPUT_HISTORY_SECS); 1080 + if (audio->output_history_size <= 0) { 1081 + audio->output_history_size = AUDIO_OUTPUT_HISTORY_RATE * AUDIO_OUTPUT_HISTORY_SECS; 1082 + audio->output_history_rate = AUDIO_OUTPUT_HISTORY_RATE; 1083 + audio->output_history_downsample_n = 1; 1084 + } 1085 + 1031 1086 // Reallocate room buffers for actual rate 1032 1087 int actual_room_size = (int)(0.12 * rate) * 3; 1033 1088 if (actual_room_size != audio->room_size) { ··· 1550 1605 return len; 1551 1606 } 1552 1607 1608 + int audio_output_get_recent(ACAudio *audio, float *out, int max_len, unsigned int *out_rate) { 1609 + if (!audio || !out || max_len <= 0 || !audio->output_history_buf || audio->output_history_size <= 0) { 1610 + if (out_rate) *out_rate = 0; 1611 + return 0; 1612 + } 1613 + 1614 + pthread_mutex_lock(&audio->lock); 1615 + if (out_rate) *out_rate = audio->output_history_rate; 1616 + 1617 + uint64_t write_pos = audio->output_history_write_pos; 1618 + int available = write_pos < (uint64_t)audio->output_history_size 1619 + ? (int)write_pos 1620 + : audio->output_history_size; 1621 + int len = available < max_len ? available : max_len; 1622 + uint64_t start = write_pos - (uint64_t)len; 1623 + for (int i = 0; i < len; i++) { 1624 + out[i] = audio->output_history_buf[(start + (uint64_t)i) % (uint64_t)audio->output_history_size]; 1625 + } 1626 + 1627 + pthread_mutex_unlock(&audio->lock); 1628 + return len; 1629 + } 1630 + 1553 1631 void audio_sample_load_data(ACAudio *audio, const float *data, int len, unsigned int rate) { 1554 1632 if (!audio || !data || len <= 0 || !audio->sample_buf_back) return; 1555 1633 if (len > audio->sample_max_len) len = audio->sample_max_len; ··· 1580 1658 len > 3 ? audio->sample_buf[3] : 0); 1581 1659 } 1582 1660 1661 + void audio_replay_load_data(ACAudio *audio, const float *data, int len, unsigned int rate) { 1662 + if (!audio || !data || len <= 0 || !audio->replay_buf_back) return; 1663 + if (len > audio->replay_max_len) len = audio->replay_max_len; 1664 + 1665 + memcpy(audio->replay_buf_back, data, len * sizeof(float)); 1666 + if (len < audio->replay_max_len) 1667 + memset(audio->replay_buf_back + len, 0, (audio->replay_max_len - len) * sizeof(float)); 1668 + 1669 + pthread_mutex_lock(&audio->lock); 1670 + audio->replay_voice.active = 0; 1671 + float *tmp = audio->replay_buf; 1672 + audio->replay_buf = audio->replay_buf_back; 1673 + audio->replay_buf_back = tmp; 1674 + audio->replay_len = len; 1675 + if (rate > 0) audio->replay_rate = rate; 1676 + __sync_synchronize(); 1677 + pthread_mutex_unlock(&audio->lock); 1678 + } 1679 + 1583 1680 // --- Sample playback --- 1584 1681 uint64_t audio_sample_play(ACAudio *audio, double freq, double base_freq, 1585 1682 double volume, double pan, int loop) { ··· 1612 1709 return sv->id; 1613 1710 } 1614 1711 1712 + uint64_t audio_replay_play(ACAudio *audio, double freq, double base_freq, 1713 + double volume, double pan, int loop) { 1714 + if (!audio || audio->replay_len == 0) return 0; 1715 + pthread_mutex_lock(&audio->lock); 1716 + 1717 + SampleVoice *sv = &audio->replay_voice; 1718 + sv->active = 1; 1719 + sv->loop = loop; 1720 + sv->position = 0.0; 1721 + sv->speed = (freq / base_freq) * ((double)audio->replay_rate / (double)audio->actual_rate); 1722 + sv->volume = volume; 1723 + sv->pan = pan; 1724 + sv->fade = 0.0; 1725 + sv->fade_target = 1.0; 1726 + sv->id = audio->sample_next_id++; 1727 + 1728 + pthread_mutex_unlock(&audio->lock); 1729 + return sv->id; 1730 + } 1731 + 1615 1732 void audio_sample_kill(ACAudio *audio, uint64_t id, double fade) { 1616 1733 if (!audio) return; 1617 1734 pthread_mutex_lock(&audio->lock); ··· 1628 1745 pthread_mutex_unlock(&audio->lock); 1629 1746 } 1630 1747 1748 + void audio_replay_kill(ACAudio *audio, uint64_t id, double fade) { 1749 + if (!audio) return; 1750 + pthread_mutex_lock(&audio->lock); 1751 + SampleVoice *sv = &audio->replay_voice; 1752 + if (sv->active && sv->id == id) { 1753 + if (fade <= 0.001) sv->active = 0; 1754 + else sv->fade_target = 0.0; 1755 + } 1756 + pthread_mutex_unlock(&audio->lock); 1757 + } 1758 + 1631 1759 void audio_sample_update(ACAudio *audio, uint64_t id, double freq, 1632 1760 double base_freq, double volume, double pan) { 1633 1761 if (!audio) return; ··· 1641 1769 if (pan > -2) sv->pan = pan; 1642 1770 break; 1643 1771 } 1772 + } 1773 + pthread_mutex_unlock(&audio->lock); 1774 + } 1775 + 1776 + void audio_replay_update(ACAudio *audio, uint64_t id, double freq, 1777 + double base_freq, double volume, double pan) { 1778 + if (!audio) return; 1779 + pthread_mutex_lock(&audio->lock); 1780 + SampleVoice *sv = &audio->replay_voice; 1781 + if (sv->active && sv->id == id) { 1782 + if (freq > 0 && base_freq > 0) 1783 + sv->speed = (freq / base_freq) * ((double)audio->replay_rate / (double)audio->actual_rate); 1784 + if (volume >= 0) sv->volume = volume; 1785 + if (pan > -2) sv->pan = pan; 1644 1786 } 1645 1787 pthread_mutex_unlock(&audio->lock); 1646 1788 } ··· 1923 2065 free(audio->room_buf_l); 1924 2066 free(audio->room_buf_r); 1925 2067 free(audio->sample_buf); 2068 + free(audio->sample_buf_back); 2069 + free(audio->mic_ring); 2070 + free(audio->replay_buf); 2071 + free(audio->replay_buf_back); 2072 + free(audio->output_history_buf); 1926 2073 free(audio->tts_buf); 1927 2074 pthread_mutex_destroy(&audio->lock); 1928 2075 free(audio);
+26
fedac/native/src/audio.h
··· 12 12 #define AUDIO_WAVEFORM_SIZE 512 13 13 #define AUDIO_MAX_SAMPLE_VOICES 12 14 14 #define AUDIO_MAX_SAMPLE_SECS 10 15 + #define AUDIO_OUTPUT_HISTORY_SECS 12 16 + #define AUDIO_OUTPUT_HISTORY_RATE 48000 15 17 #define AUDIO_MAX_DECKS 2 16 18 17 19 typedef enum { ··· 163 165 SampleVoice sample_voices[AUDIO_MAX_SAMPLE_VOICES]; 164 166 uint64_t sample_next_id; 165 167 168 + // Dedicated global replay voice/buffer so reverse playback does not 169 + // steal or overwrite the regular sample bank. 170 + float *replay_buf; 171 + float *replay_buf_back; 172 + volatile int replay_len; 173 + int replay_max_len; 174 + unsigned int replay_rate; 175 + SampleVoice replay_voice; 176 + 177 + // Recent rendered-output history for true reverse replay. 178 + float *output_history_buf; // mono output ring tapped before room/glitch/TTS 179 + int output_history_size; // ring capacity in samples 180 + unsigned int output_history_rate; // capture rate exposed to JS 181 + unsigned int output_history_downsample_n; // output-rate -> history-rate stride 182 + unsigned int output_history_downsample_pos; // current stride counter 183 + uint64_t output_history_write_pos; // monotonic write position 184 + 166 185 // DJ deck audio (persistent across piece switches) 167 186 ACDeck decks[AUDIO_MAX_DECKS]; 168 187 float crossfader; // 0.0 = deck A, 1.0 = deck B ··· 227 246 void audio_sample_kill(ACAudio *audio, uint64_t id, double fade); 228 247 void audio_sample_update(ACAudio *audio, uint64_t id, double freq, 229 248 double base_freq, double volume, double pan); 249 + void audio_replay_load_data(ACAudio *audio, const float *data, int len, unsigned int rate); 250 + uint64_t audio_replay_play(ACAudio *audio, double freq, double base_freq, 251 + double volume, double pan, int loop); 252 + void audio_replay_kill(ACAudio *audio, uint64_t id, double fade); 253 + void audio_replay_update(ACAudio *audio, uint64_t id, double freq, 254 + double base_freq, double volume, double pan); 230 255 231 256 // Sample bank: get/load data for per-key sample storage 232 257 int audio_sample_get_data(ACAudio *audio, float *out, int max_len); 233 258 void audio_sample_load_data(ACAudio *audio, const float *data, int len, unsigned int rate); 259 + int audio_output_get_recent(ACAudio *audio, float *out, int max_len, unsigned int *out_rate); 234 260 235 261 // Adjust system volume: delta is -5 to +5 (percentage points), 0 = toggle mute 236 262 void audio_volume_adjust(ACAudio *audio, int delta);
+187 -2
fedac/native/src/js-bindings.c
··· 945 945 return snd; 946 946 } 947 947 948 - // sound.kill(idOrObj, fade) — accepts raw id or synth object 948 + // sound.kill(idOrObj, fade) — accepts raw id or synth/sample/replay object 949 949 static JSValue js_sound_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 950 950 (void)this_val; 951 951 ACAudio *audio = current_rt->audio; ··· 969 969 970 970 // Check if it's a sample voice 971 971 int is_sample = 0; 972 + int is_replay = 0; 972 973 if (JS_IsObject(argv[0])) { 973 974 JSValue is_v = JS_GetPropertyStr(ctx, argv[0], "isSample"); 974 975 is_sample = JS_ToBool(ctx, is_v); 976 + JS_FreeValue(ctx, is_v); 977 + is_v = JS_GetPropertyStr(ctx, argv[0], "isReplay"); 978 + is_replay = JS_ToBool(ctx, is_v); 975 979 JS_FreeValue(ctx, is_v); 976 980 } 977 981 978 - if (is_sample) { 982 + if (is_replay) { 983 + audio_replay_kill(audio, id, fade); 984 + } else if (is_sample) { 979 985 audio_sample_kill(audio, id, fade); 980 986 } else { 981 987 audio_kill(audio, id, fade); ··· 1158 1164 return JS_UNDEFINED; 1159 1165 } 1160 1166 1167 + // replayObj.update({tone, base, volume, pan}) — update the dedicated replay voice 1168 + static JSValue js_replay_obj_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1169 + ACAudio *audio = current_rt->audio; 1170 + if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1171 + 1172 + JSValue id_v = JS_GetPropertyStr(ctx, this_val, "id"); 1173 + double id_d = 0; 1174 + if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1175 + JS_FreeValue(ctx, id_v); 1176 + uint64_t id = (uint64_t)id_d; 1177 + if (id == 0) return JS_UNDEFINED; 1178 + 1179 + double freq = -1, base_freq = -1, vol = -1, pan = -3; 1180 + JSValue v; 1181 + v = JS_GetPropertyStr(ctx, argv[0], "tone"); 1182 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1183 + JS_FreeValue(ctx, v); 1184 + v = JS_GetPropertyStr(ctx, argv[0], "base"); 1185 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1186 + JS_FreeValue(ctx, v); 1187 + v = JS_GetPropertyStr(ctx, argv[0], "volume"); 1188 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &vol, v); 1189 + JS_FreeValue(ctx, v); 1190 + v = JS_GetPropertyStr(ctx, argv[0], "pan"); 1191 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1192 + JS_FreeValue(ctx, v); 1193 + 1194 + if (freq > 0 && base_freq < 0) base_freq = 261.63; 1195 + audio_replay_update(audio, id, freq, base_freq, vol, pan); 1196 + return JS_UNDEFINED; 1197 + } 1198 + 1161 1199 // sound.sample.play({tone, base, volume, pan}) — play recorded sample at pitch 1162 1200 static JSValue js_sample_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1163 1201 (void)this_val; ··· 1204 1242 return snd; 1205 1243 } 1206 1244 1245 + // sound.replay.play({tone, base, volume, pan}) — play dedicated global replay buffer 1246 + static JSValue js_replay_play(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1247 + (void)this_val; 1248 + ACAudio *audio = current_rt->audio; 1249 + if (!audio || argc < 1 || !JS_IsObject(argv[0])) return JS_UNDEFINED; 1250 + 1251 + JSValue opts = argv[0]; 1252 + double freq = 261.63, base_freq = 261.63, volume = 0.7, pan = 0.0; 1253 + JSValue v; 1254 + 1255 + v = JS_GetPropertyStr(ctx, opts, "tone"); 1256 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &freq, v); 1257 + JS_FreeValue(ctx, v); 1258 + 1259 + v = JS_GetPropertyStr(ctx, opts, "base"); 1260 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &base_freq, v); 1261 + JS_FreeValue(ctx, v); 1262 + 1263 + v = JS_GetPropertyStr(ctx, opts, "volume"); 1264 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &volume, v); 1265 + JS_FreeValue(ctx, v); 1266 + 1267 + v = JS_GetPropertyStr(ctx, opts, "pan"); 1268 + if (JS_IsNumber(v)) JS_ToFloat64(ctx, &pan, v); 1269 + JS_FreeValue(ctx, v); 1270 + 1271 + int loop = 0; 1272 + v = JS_GetPropertyStr(ctx, opts, "loop"); 1273 + if (JS_IsBool(v)) loop = JS_ToBool(ctx, v); 1274 + JS_FreeValue(ctx, v); 1275 + 1276 + uint64_t id = audio_replay_play(audio, freq, base_freq, volume, pan, loop); 1277 + if (id == 0) return JS_UNDEFINED; 1278 + 1279 + JSValue snd = JS_NewObject(ctx); 1280 + JS_SetPropertyStr(ctx, snd, "id", JS_NewFloat64(ctx, (double)id)); 1281 + JS_SetPropertyStr(ctx, snd, "isReplay", JS_TRUE); 1282 + JS_SetPropertyStr(ctx, snd, "update", JS_NewCFunction(ctx, js_replay_obj_update, "update", 1)); 1283 + return snd; 1284 + } 1285 + 1207 1286 // sound.sample.kill(idOrObj, fade) 1208 1287 static JSValue js_sample_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1209 1288 (void)this_val; ··· 1226 1305 return JS_UNDEFINED; 1227 1306 } 1228 1307 1308 + // sound.replay.kill(idOrObj, fade) 1309 + static JSValue js_replay_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1310 + (void)this_val; 1311 + ACAudio *audio = current_rt->audio; 1312 + if (!audio || argc < 1) return JS_UNDEFINED; 1313 + 1314 + double id_d = 0; 1315 + if (JS_IsNumber(argv[0])) { 1316 + JS_ToFloat64(ctx, &id_d, argv[0]); 1317 + } else if (JS_IsObject(argv[0])) { 1318 + JSValue id_v = JS_GetPropertyStr(ctx, argv[0], "id"); 1319 + if (JS_IsNumber(id_v)) JS_ToFloat64(ctx, &id_d, id_v); 1320 + JS_FreeValue(ctx, id_v); 1321 + } 1322 + 1323 + double fade = 0.02; 1324 + if (argc >= 2 && JS_IsNumber(argv[1])) JS_ToFloat64(ctx, &fade, argv[1]); 1325 + 1326 + audio_replay_kill(audio, (uint64_t)id_d, fade); 1327 + return JS_UNDEFINED; 1328 + } 1329 + 1229 1330 // sound.sample.getData() — returns Float32Array of current sample buffer 1230 1331 // Proper free callback for JS_NewArrayBuffer (3-arg signature, not plain free) 1231 1332 static void js_free_array_buffer(JSRuntime *rt, void *opaque, void *ptr) { ··· 1320 1421 audio_sample_load_data(audio, data, len, rate); 1321 1422 JS_FreeValue(ctx, ab); // Free AFTER memcpy — ptr is into this buffer 1322 1423 return JS_TRUE; 1424 + } 1425 + 1426 + // sound.replay.loadData(float32array, rate) — load data into dedicated replay buffer 1427 + static JSValue js_replay_load_data(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1428 + (void)this_val; 1429 + if (!current_rt) return JS_FALSE; 1430 + ACAudio *audio = current_rt->audio; 1431 + if (!audio || argc < 1) return JS_FALSE; 1432 + 1433 + size_t byte_len = 0; 1434 + size_t byte_off = 0; 1435 + size_t bytes_per = 0; 1436 + JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_off, &byte_len, &bytes_per); 1437 + if (JS_IsException(ab)) return JS_FALSE; 1438 + 1439 + size_t ab_len = 0; 1440 + uint8_t *ptr = JS_GetArrayBuffer(ctx, &ab_len, ab); 1441 + if (!ptr) { JS_FreeValue(ctx, ab); return JS_FALSE; } 1442 + 1443 + float *data = (float *)(ptr + byte_off); 1444 + int len = (int)(byte_len / sizeof(float)); 1445 + 1446 + unsigned int rate = 48000; 1447 + if (argc >= 2 && JS_IsNumber(argv[1])) { 1448 + double r; JS_ToFloat64(ctx, &r, argv[1]); 1449 + if (r > 0) rate = (unsigned int)r; 1450 + } 1451 + 1452 + audio_replay_load_data(audio, data, len, rate); 1453 + JS_FreeValue(ctx, ab); 1454 + return JS_TRUE; 1455 + } 1456 + 1457 + // sound.speaker.getRecentBuffer(seconds) -> { data: Float32Array, rate: number } 1458 + static JSValue js_speaker_get_recent_buffer(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 1459 + (void)this_val; 1460 + ACAudio *audio = current_rt ? current_rt->audio : NULL; 1461 + if (!audio || !audio->output_history_buf || audio->output_history_size <= 0) return JS_NULL; 1462 + 1463 + double seconds = 1.0; 1464 + if (argc >= 1 && JS_IsNumber(argv[0])) JS_ToFloat64(ctx, &seconds, argv[0]); 1465 + if (seconds <= 0.0) return JS_NULL; 1466 + 1467 + unsigned int rate_guess = audio->output_history_rate ? audio->output_history_rate : AUDIO_OUTPUT_HISTORY_RATE; 1468 + int want_len = (int)(seconds * (double)rate_guess + 0.5); 1469 + if (want_len < 1) want_len = 1; 1470 + if (want_len > audio->output_history_size) want_len = audio->output_history_size; 1471 + 1472 + float *copy = malloc((size_t)want_len * sizeof(float)); 1473 + if (!copy) return JS_NULL; 1474 + 1475 + unsigned int actual_rate = 0; 1476 + int len = audio_output_get_recent(audio, copy, want_len, &actual_rate); 1477 + if (len <= 0 || actual_rate == 0) { 1478 + free(copy); 1479 + return JS_NULL; 1480 + } 1481 + 1482 + size_t byte_len = (size_t)len * sizeof(float); 1483 + JSValue ab = JS_NewArrayBuffer(ctx, (uint8_t *)copy, byte_len, 1484 + js_free_array_buffer, NULL, 0); 1485 + if (JS_IsException(ab)) { free(copy); return JS_NULL; } 1486 + 1487 + JSValue global = JS_GetGlobalObject(ctx); 1488 + JSValue ctor = JS_GetPropertyStr(ctx, global, "Float32Array"); 1489 + JSValue f32 = JS_CallConstructor(ctx, ctor, 1, &ab); 1490 + JS_FreeValue(ctx, ctor); 1491 + JS_FreeValue(ctx, global); 1492 + JS_FreeValue(ctx, ab); 1493 + if (JS_IsException(f32)) return JS_NULL; 1494 + 1495 + JSValue out = JS_NewObject(ctx); 1496 + JS_SetPropertyStr(ctx, out, "data", f32); 1497 + JS_SetPropertyStr(ctx, out, "rate", JS_NewInt32(ctx, (int)actual_rate)); 1498 + return out; 1323 1499 } 1324 1500 1325 1501 // sound.speak(text) ··· 2201 2377 // speaker sub-object 2202 2378 JSValue speaker = JS_NewObject(ctx); 2203 2379 JS_SetPropertyStr(ctx, speaker, "poll", JS_NewCFunction(ctx, js_noop, "poll", 0)); 2380 + JS_SetPropertyStr(ctx, speaker, "getRecentBuffer", 2381 + JS_NewCFunction(ctx, js_speaker_get_recent_buffer, "getRecentBuffer", 1)); 2204 2382 JS_SetPropertyStr(ctx, speaker, "sampleRate", 2205 2383 JS_NewInt32(ctx, rt->audio ? (int)rt->audio->actual_rate : AUDIO_SAMPLE_RATE)); 2206 2384 ··· 2296 2474 JS_SetPropertyStr(ctx, samp, "saveTo", JS_NewCFunction(ctx, js_sample_save_to, "saveTo", 1)); 2297 2475 JS_SetPropertyStr(ctx, samp, "loadFrom", JS_NewCFunction(ctx, js_sample_load_from, "loadFrom", 1)); 2298 2476 JS_SetPropertyStr(ctx, sound, "sample", samp); 2477 + 2478 + // dedicated global replay voice/buffer 2479 + JSValue replay = JS_NewObject(ctx); 2480 + JS_SetPropertyStr(ctx, replay, "play", JS_NewCFunction(ctx, js_replay_play, "play", 1)); 2481 + JS_SetPropertyStr(ctx, replay, "kill", JS_NewCFunction(ctx, js_replay_kill, "kill", 2)); 2482 + JS_SetPropertyStr(ctx, replay, "loadData", JS_NewCFunction(ctx, js_replay_load_data, "loadData", 2)); 2483 + JS_SetPropertyStr(ctx, sound, "replay", replay); 2299 2484 2300 2485 // DJ deck 2301 2486 JSValue deck_obj = JS_NewObject(ctx);