From 7aa1d6f6606e083cf5e7e86e5a2a62c63b91d6c2 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:45:08 +0000 Subject: [PATCH] Fix stop button - track and stop all active audio nodes - Track all oscillators/sources in APU activeNodes Set - Add stopAll() method that immediately stops all tracked nodes - Auto-remove nodes from tracking when they end naturally - GameBoyPlayer.stop() now calls apu.stopAll() --- src-v2/audio/apu/APU.ts | 45 +++++++++++++++++++++++++++++++++--- src-v2/core/GameBoyPlayer.ts | 2 +- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src-v2/audio/apu/APU.ts b/src-v2/audio/apu/APU.ts index d05336b..89980ac 100644 --- a/src-v2/audio/apu/APU.ts +++ b/src-v2/audio/apu/APU.ts @@ -70,6 +70,9 @@ export class GameBoyAPU { // Note scheduling stats (no limit - Web Audio handles scheduling) private scheduledNoteCount = 0; + // Track active audio nodes for stop functionality + private activeNodes: Set = new Set(); + constructor(audioContext?: AudioContext, config?: Partial) { this.audioContext = audioContext || new AudioContext(); this.config = { ...DEFAULT_V2_CONFIG, ...config }; @@ -197,7 +200,8 @@ export class GameBoyAPU { const channel = this.pulseChannels.get(channelId); if (!channel) return; - channel.playNote(midiNote, duration, velocity, startTime); + const result = channel.playNote(midiNote, duration, velocity, startTime); + this.trackNode(result.oscillator, result.stopTime); } /** @@ -213,7 +217,8 @@ export class GameBoyAPU { const channel = this.waveChannels.get(channelId); if (!channel) return; - channel.playNote(midiNote, duration, velocity, startTime); + const result = channel.playNote(midiNote, duration, velocity, startTime); + this.trackNode(result.oscillator, result.stopTime); } /** @@ -229,7 +234,21 @@ export class GameBoyAPU { const channel = this.noiseChannels.get(channelId); if (!channel) return; - channel.playNote(midiNote, duration, velocity, startTime); + const result = channel.playNote(midiNote, duration, velocity, startTime); + this.trackNode(result.source, result.stopTime); + } + + /** + * Track an audio node for stop functionality. + */ + private trackNode(node: OscillatorNode | AudioBufferSourceNode, stopTime: number): void { + this.activeNodes.add(node); + + // Auto-remove when the node ends naturally + const cleanup = () => { + this.activeNodes.delete(node); + }; + node.onended = cleanup; } /** @@ -392,6 +411,26 @@ export class GameBoyAPU { this.scheduledNoteCount = 0; } + /** + * Stop all currently playing and scheduled sounds immediately. + */ + stopAll(): void { + const now = this.audioContext.currentTime; + + // Stop all tracked nodes + for (const node of this.activeNodes) { + try { + node.stop(now); + } catch { + // Node may have already stopped + } + } + this.activeNodes.clear(); + + // Reset channel states + this.reset(); + } + /** * Get scheduled note count. */ diff --git a/src-v2/core/GameBoyPlayer.ts b/src-v2/core/GameBoyPlayer.ts index 9fc5605..b99944e 100644 --- a/src-v2/core/GameBoyPlayer.ts +++ b/src-v2/core/GameBoyPlayer.ts @@ -230,7 +230,7 @@ export class GameBoyPlayer { */ stop(): void { this.isPlaying = false; - this.apu.reset(); + this.apu.stopAll(); console.log('Playback stopped'); }