Skip to content

Commit eaf7874

Browse files
Stop council audio when the player is closed
The Web Audio BufferSource was created and started without keeping a reference, so cleanup had no handle to call .stop() on it; the current segment played out after close. The LLM stream had an AbortController in place but cleanup never aborted it, so the streaming callback kept arriving and queueing TTS work. Track the active source for stop() and abort the LLM stream first in cleanup() so closing silences audio and halts further segment arrivals.
1 parent f86438e commit eaf7874

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

client/src/services/council/CustomCouncilService.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export class CustomCouncilService {
135135
private councilSessionId: string | null;
136136
private councilVoiceMappings: Record<string, string> | null;
137137
private abortController: AbortController | null;
138+
// Reference to the BufferSource currently feeding the speakers, so cleanup
139+
// can call .stop() on user-close instead of letting the segment play out.
140+
private activeSource: AudioBufferSourceNode | null;
138141

139142
constructor(mainService: MainService) {
140143
this.mainService = mainService;
@@ -150,6 +153,8 @@ export class CustomCouncilService {
150153

151154
// Abort controller for cancellation
152155
this.abortController = null;
156+
157+
this.activeSource = null;
153158

154159
// Loader state management
155160
this.loaderState = {
@@ -696,6 +701,9 @@ export class CustomCouncilService {
696701

697702
source.onended = () => {
698703
councilLog(`🎵 Finished playing segment ${segment.id}`);
704+
if (this.activeSource === source) {
705+
this.activeSource = null;
706+
}
699707
resolve();
700708
};
701709

@@ -712,6 +720,8 @@ export class CustomCouncilService {
712720
duration: audioBuffer.duration
713721
});
714722

723+
// Set BEFORE start() so a same-tick cleanup still has the ref to stop.
724+
this.activeSource = source;
715725
source.start();
716726
});
717727
}
@@ -1181,6 +1191,28 @@ export class CustomCouncilService {
11811191
cleanup(): void {
11821192
councilLog('🧹 Cleaning up CustomCouncilService resources');
11831193

1194+
// Cut the currently-playing segment so closing the council silences audio
1195+
// immediately. .stop() throws if the source never started or already
1196+
// stopped — that's fine, ignore.
1197+
if (this.activeSource) {
1198+
councilLog('🛑 Stopping active audio segment');
1199+
try {
1200+
this.activeSource.stop();
1201+
} catch (err) {
1202+
councilWarn('activeSource.stop() failed:', err);
1203+
}
1204+
this.activeSource = null;
1205+
}
1206+
1207+
// Abort any in-flight LLM stream so further segments stop arriving.
1208+
// Without this, the streaming callback keeps queueing TTS work in the
1209+
// background even after the user has closed the council.
1210+
if (this.abortController) {
1211+
councilLog('🛑 Aborting in-flight LLM stream');
1212+
this.abortController.abort();
1213+
this.abortController = null;
1214+
}
1215+
11841216
// 🔄 Hide loader if still visible
11851217
this._hideLoader();
11861218

0 commit comments

Comments
 (0)