Skip to content

Commit 4fe0d71

Browse files
committed
fix: resolve inconsistent audio interruption behavior
1 parent 0321d1a commit 4fe0d71

File tree

3 files changed

+91
-50
lines changed

3 files changed

+91
-50
lines changed

src/renderer/src/hooks/footer/use-text-input.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function useTextInput() {
1010
const [inputText, setInputText] = useState('');
1111
const [isComposing, setIsComposing] = useState(false);
1212
const wsContext = useWebSocket();
13-
const { aiState, setAiState } = useAiState();
13+
const { aiState } = useAiState();
1414
const { interrupt } = useInterrupt();
1515
const { appendHumanMessage } = useChatHistory();
1616
const { stopMic, autoStopMic } = useVAD();
@@ -35,7 +35,6 @@ export function useTextInput() {
3535
images,
3636
});
3737

38-
setAiState('thinking-speaking');
3938
if (autoStopMic) stopMic();
4039
setInputText('');
4140
};

src/renderer/src/hooks/utils/use-audio-task.ts

Lines changed: 11 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useAiState } from '@/context/ai-state-context';
77
import { useSubtitle } from '@/context/subtitle-context';
88
import { useChatHistory } from '@/context/chat-history-context';
99
import { audioTaskQueue } from '@/utils/task-queue';
10+
import { audioManager } from '@/utils/audio-manager';
1011
import { toaster } from '@/components/ui/toaster';
1112
import { useWebSocket } from '@/context/websocket-context';
1213
import { DisplayText } from '@/services/websocket-service';
@@ -45,9 +46,7 @@ export const useAudioTask = () => {
4546
appendAIMessage,
4647
});
4748

48-
// Track current audio and model
49-
const currentAudioRef = useRef<HTMLAudioElement | null>(null);
50-
const currentModelRef = useRef<Live2DModel | null>(null);
49+
// Note: currentAudioRef and currentModelRef are now managed by the global audioManager
5150

5251
stateRef.current = {
5352
aiState,
@@ -57,42 +56,10 @@ export const useAudioTask = () => {
5756
};
5857

5958
/**
60-
* Stop current audio playback and lip sync
59+
* Stop current audio playback and lip sync (delegates to global audioManager)
6160
*/
6261
const stopCurrentAudioAndLipSync = useCallback(() => {
63-
if (currentAudioRef.current) {
64-
console.log('Stopping current audio and lip sync');
65-
const audio = currentAudioRef.current;
66-
audio.pause();
67-
audio.src = '';
68-
audio.load();
69-
70-
const model = currentModelRef.current;
71-
if (model && model._wavFileHandler) {
72-
try {
73-
// Release PCM data to stop lip sync calculation in update()
74-
model._wavFileHandler.releasePcmData();
75-
console.log('Called _wavFileHandler.releasePcmData()');
76-
77-
// Additional reset of state variables as fallback
78-
model._wavFileHandler._lastRms = 0.0;
79-
model._wavFileHandler._sampleOffset = 0;
80-
model._wavFileHandler._userTimeSeconds = 0.0;
81-
console.log('Also reset _lastRms, _sampleOffset, _userTimeSeconds as fallback');
82-
} catch (e) {
83-
console.error('Error stopping/resetting wavFileHandler:', e);
84-
}
85-
} else if (model) {
86-
console.warn('Current model does not have _wavFileHandler to stop/reset.');
87-
} else {
88-
console.log('No associated model found to stop lip sync.');
89-
}
90-
91-
currentAudioRef.current = null;
92-
currentModelRef.current = null;
93-
} else {
94-
console.log('No current audio playing to stop.');
95-
}
62+
audioManager.stopCurrentAudioAndLipSync();
9663
}, []);
9764

9865
/**
@@ -151,7 +118,6 @@ export const useAudioTask = () => {
151118
return;
152119
}
153120
console.log('Found model for audio playback');
154-
currentModelRef.current = model;
155121

156122
if (!model._wavFileHandler) {
157123
console.warn('Model does not have _wavFileHandler for lip sync');
@@ -182,14 +148,13 @@ export const useAudioTask = () => {
182148

183149
// Setup audio element
184150
const audio = new Audio(audioDataUrl);
185-
currentAudioRef.current = audio;
151+
152+
// Register with global audio manager IMMEDIATELY after creating audio
153+
audioManager.setCurrentAudio(audio, model);
186154
let isFinished = false;
187155

188156
const cleanup = () => {
189-
if (currentAudioRef.current === audio) {
190-
currentAudioRef.current = null;
191-
currentModelRef.current = null;
192-
}
157+
audioManager.clearCurrentAudio(audio);
193158
if (!isFinished) {
194159
isFinished = true;
195160
resolve();
@@ -201,8 +166,8 @@ export const useAudioTask = () => {
201166

202167
audio.addEventListener('canplaythrough', () => {
203168
// Check for interruption before playback
204-
if (stateRef.current.aiState === 'interrupted' || currentAudioRef.current !== audio) {
205-
console.warn('Audio playback cancelled due to interruption or new audio');
169+
if (stateRef.current.aiState === 'interrupted' || !audioManager.hasCurrentAudio()) {
170+
console.warn('Audio playback cancelled due to interruption or audio was stopped');
206171
cleanup();
207172
return;
208173
}
@@ -228,7 +193,7 @@ export const useAudioTask = () => {
228193
};
229194
}
230195

231-
if (currentAudioRef.current === audio) {
196+
if (audioManager.hasCurrentAudio()) {
232197
model._wavFileHandler.start(audioDataUrl);
233198
} else {
234199
console.warn('WavFileHandler start skipped - audio was stopped');
@@ -257,8 +222,6 @@ export const useAudioTask = () => {
257222
type: "error",
258223
duration: 2000,
259224
});
260-
currentAudioRef.current = null;
261-
currentModelRef.current = null;
262225
resolve();
263226
}
264227
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Global audio manager for handling audio playback and interruption
3+
* This ensures all components share the same audio reference
4+
*/
5+
class AudioManager {
6+
private currentAudio: HTMLAudioElement | null = null;
7+
private currentModel: any | null = null;
8+
9+
/**
10+
* Set the current playing audio
11+
*/
12+
setCurrentAudio(audio: HTMLAudioElement, model: any) {
13+
this.currentAudio = audio;
14+
this.currentModel = model;
15+
}
16+
17+
/**
18+
* Stop current audio playback and lip sync
19+
*/
20+
stopCurrentAudioAndLipSync() {
21+
if (this.currentAudio) {
22+
console.log('[AudioManager] Stopping current audio and lip sync');
23+
const audio = this.currentAudio;
24+
25+
// Stop audio playback
26+
audio.pause();
27+
audio.src = '';
28+
audio.load();
29+
30+
// Stop Live2D lip sync
31+
const model = this.currentModel;
32+
if (model && model._wavFileHandler) {
33+
try {
34+
// Release PCM data to stop lip sync calculation in update()
35+
model._wavFileHandler.releasePcmData();
36+
console.log('[AudioManager] Called _wavFileHandler.releasePcmData()');
37+
38+
// Additional reset of state variables as fallback
39+
model._wavFileHandler._lastRms = 0.0;
40+
model._wavFileHandler._sampleOffset = 0;
41+
model._wavFileHandler._userTimeSeconds = 0.0;
42+
console.log('[AudioManager] Also reset _lastRms, _sampleOffset, _userTimeSeconds as fallback');
43+
} catch (e) {
44+
console.error('[AudioManager] Error stopping/resetting wavFileHandler:', e);
45+
}
46+
} else if (model) {
47+
console.warn('[AudioManager] Current model does not have _wavFileHandler to stop/reset.');
48+
} else {
49+
console.log('[AudioManager] No associated model found to stop lip sync.');
50+
}
51+
52+
// Clear references
53+
this.currentAudio = null;
54+
this.currentModel = null;
55+
} else {
56+
console.log('[AudioManager] No current audio playing to stop.');
57+
}
58+
}
59+
60+
/**
61+
* Clear the current audio reference (called when audio ends naturally)
62+
*/
63+
clearCurrentAudio(audio: HTMLAudioElement) {
64+
if (this.currentAudio === audio) {
65+
this.currentAudio = null;
66+
this.currentModel = null;
67+
}
68+
}
69+
70+
/**
71+
* Check if there's currently playing audio
72+
*/
73+
hasCurrentAudio(): boolean {
74+
return this.currentAudio !== null;
75+
}
76+
}
77+
78+
// Export singleton instance
79+
export const audioManager = new AudioManager();

0 commit comments

Comments
 (0)