mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
fix(stories): hard-cut the audio graph on stop so long imports actually halt
source.stop() was the only thing happening when a clip was halted, and on long imported buffers (multi-minute MP3s scheduled via source.start with a duration argument) it was silently failing to halt the buffer in some browsers — pause left the music playing and seek stacked another source on top of the original. The mute-the-WaveSurfer-element fix was a different bug along the same path; this is the one that actually addresses the duplicated audio. ActiveSource now carries the per-clip GainNode alongside the source, and stopSource detaches the onended handler before calling stop() (so the natural-end callback can't race with explicit teardown and re-delete a freshly rescheduled entry at the same id), then disconnects both nodes inside their own try/catch blocks. Even when stop() doesn't actually halt the buffer the graph is severed — no path from source to destination, no audio.
This commit is contained in:
@@ -5,6 +5,7 @@ import { useStoryStore } from '@/stores/storyStore';
|
|||||||
|
|
||||||
interface ActiveSource {
|
interface ActiveSource {
|
||||||
source: AudioBufferSourceNode;
|
source: AudioBufferSourceNode;
|
||||||
|
clipGain: GainNode;
|
||||||
itemId: string;
|
itemId: string;
|
||||||
generationId: string;
|
generationId: string;
|
||||||
startTimeMs: number;
|
startTimeMs: number;
|
||||||
@@ -61,11 +62,29 @@ export function useStoryPlayback(items: StoryItemDetail[] | undefined) {
|
|||||||
const stopSource = useCallback((itemId: string) => {
|
const stopSource = useCallback((itemId: string) => {
|
||||||
const activeSource = activeSourcesRef.current.get(itemId);
|
const activeSource = activeSourcesRef.current.get(itemId);
|
||||||
if (activeSource) {
|
if (activeSource) {
|
||||||
|
// Detach onended first so the natural-end handler doesn't race with
|
||||||
|
// the explicit teardown below and re-delete a fresh entry that has
|
||||||
|
// already been re-scheduled at this id.
|
||||||
|
activeSource.source.onended = null;
|
||||||
try {
|
try {
|
||||||
activeSource.source.stop();
|
activeSource.source.stop();
|
||||||
} catch {
|
} catch {
|
||||||
// Source may have already stopped
|
// Source may have already stopped
|
||||||
}
|
}
|
||||||
|
// Hard-cut the audio graph regardless of whether stop() actually
|
||||||
|
// halted the buffer. Long imports were leaking audio when stop()
|
||||||
|
// was called on a source that was scheduled with a multi-minute
|
||||||
|
// duration; disconnecting from the destination guarantees silence.
|
||||||
|
try {
|
||||||
|
activeSource.source.disconnect();
|
||||||
|
} catch {
|
||||||
|
// already disconnected
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
activeSource.clipGain.disconnect();
|
||||||
|
} catch {
|
||||||
|
// already disconnected
|
||||||
|
}
|
||||||
activeSourcesRef.current.delete(itemId);
|
activeSourcesRef.current.delete(itemId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -274,6 +293,7 @@ export function useStoryPlayback(items: StoryItemDetail[] | undefined) {
|
|||||||
|
|
||||||
const activeSource: ActiveSource = {
|
const activeSource: ActiveSource = {
|
||||||
source,
|
source,
|
||||||
|
clipGain,
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
generationId: item.generation_id,
|
generationId: item.generation_id,
|
||||||
startTimeMs: item.start_time_ms,
|
startTimeMs: item.start_time_ms,
|
||||||
|
|||||||
Reference in New Issue
Block a user