Merge pull request #369 from cogwheel0/fix-voice-call-freezes

fix(voice-call): Improve async handling and state management
This commit is contained in:
cogwheel
2026-02-11 09:21:47 +08:00
committed by GitHub
5 changed files with 44 additions and 15 deletions

View File

@@ -149,6 +149,9 @@ class TextToSpeechService {
Future<void> dispose() async { Future<void> dispose() async {
await _eventSubscription?.cancel(); await _eventSubscription?.cancel();
_eventSubscription = null; _eventSubscription = null;
// Reset the singleton state for next session
await TtsManager.instance.reset();
} }
/// Updates TTS settings. /// Updates TTS settings.

View File

@@ -376,6 +376,28 @@ class TtsManager {
} }
} }
/// Resets the manager state for a new session.
///
/// Call this between voice calls to ensure clean state. This clears
/// playback buffers and resets session tracking without destroying
/// the singleton instance.
Future<void> reset() async {
await stop();
// Reset playback state
_resetPlaybackState();
_activeSession = null;
_sessionCounter = 0;
// Reset server audio buffer
_serverAudioBuffer.clear();
_serverWaitingForNext = false;
// Reset cached voice defaults so they're refetched if needed
_serverDefaultVoice = null;
_serverDefaultVoiceFuture = null;
}
/// Disposes the manager and releases resources. /// Disposes the manager and releases resources.
Future<void> dispose() async { Future<void> dispose() async {
await stop(); await stop();

View File

@@ -395,7 +395,7 @@ class VoiceCallService {
_activeAssistantMessageId = null; _activeAssistantMessageId = null;
_responseCompleted = false; _responseCompleted = false;
_listeningSuspendedForSpeech = false; _listeningSuspendedForSpeech = false;
_resetServerAudio(stopPlayback: true); await _resetServerAudio(stopPlayback: true);
if (_pauseReasons.isNotEmpty) { if (_pauseReasons.isNotEmpty) {
_listeningPaused = true; _listeningPaused = true;
@@ -584,7 +584,11 @@ class VoiceCallService {
_speechQueue.clear(); _speechQueue.clear();
_enqueuedSentenceCount = 0; _enqueuedSentenceCount = 0;
_responseCompleted = false; _responseCompleted = false;
_resetServerAudio(stopPlayback: true); // Fire-and-forget: This is a synchronous event handler where blocking
// would delay socket event processing. The audio player stop is fast
// and race conditions are acceptable here since new playback will
// wait for this session to complete anyway.
unawaited(_resetServerAudio(stopPlayback: true));
if (_isSpeaking) { if (_isSpeaking) {
_isSpeaking = false; _isSpeaking = false;
unawaited(_tts.stop()); unawaited(_tts.stop());
@@ -749,14 +753,14 @@ class VoiceCallService {
_maybeResumeListeningAfterSpeech(); _maybeResumeListeningAfterSpeech();
} }
void _resetServerAudio({bool stopPlayback = false}) { Future<void> _resetServerAudio({bool stopPlayback = false}) async {
_serverAudioBuffer.clear(); _serverAudioBuffer.clear();
_pendingServerAudioFetches = 0; _pendingServerAudioFetches = 0;
_serverAudioSession++; _serverAudioSession++;
_nextServerChunkId = 0; _nextServerChunkId = 0;
_nextServerPlaybackId = 0; _nextServerPlaybackId = 0;
if (stopPlayback) { if (stopPlayback) {
unawaited(_serverAudioPlayer.stop()); await _serverAudioPlayer.stop();
_isSpeaking = false; _isSpeaking = false;
} }
_serverPipelineActive = false; _serverPipelineActive = false;
@@ -819,7 +823,7 @@ class VoiceCallService {
if (_isDisposed) return; if (_isDisposed) return;
_isSpeaking = false; _isSpeaking = false;
_speechQueue.clear(); _speechQueue.clear();
_resetServerAudio(stopPlayback: true); unawaited(_resetServerAudio(stopPlayback: true));
_listeningSuspendedForSpeech = false; _listeningSuspendedForSpeech = false;
_updateState(VoiceCallState.error); _updateState(VoiceCallState.error);
// Try to recover by restarting listening // Try to recover by restarting listening
@@ -865,7 +869,7 @@ class VoiceCallService {
_listeningSuspendedForSpeech = false; _listeningSuspendedForSpeech = false;
_activeAssistantMessageId = null; _activeAssistantMessageId = null;
_isSpeaking = false; _isSpeaking = false;
_resetServerAudio(stopPlayback: true); await _resetServerAudio(stopPlayback: true);
_updateState(VoiceCallState.disconnected); _updateState(VoiceCallState.disconnected);
} }
@@ -911,7 +915,7 @@ class VoiceCallService {
_enqueuedSentenceCount = 0; _enqueuedSentenceCount = 0;
_responseCompleted = false; _responseCompleted = false;
_listeningSuspendedForSpeech = false; _listeningSuspendedForSpeech = false;
_resetServerAudio(stopPlayback: true); await _resetServerAudio(stopPlayback: true);
await _tts.stop(); await _tts.stop();
_isSpeaking = false; _isSpeaking = false;
_accumulatedResponse = ''; _accumulatedResponse = '';
@@ -985,7 +989,7 @@ class VoiceCallService {
_enqueuedSentenceCount = 0; _enqueuedSentenceCount = 0;
_responseCompleted = false; _responseCompleted = false;
_listeningSuspendedForSpeech = false; _listeningSuspendedForSpeech = false;
_resetServerAudio(stopPlayback: true); unawaited(_resetServerAudio(stopPlayback: true));
pauseListening(reason: VoiceCallPauseReason.mute); pauseListening(reason: VoiceCallPauseReason.mute);
} else { } else {
resumeListening(reason: VoiceCallPauseReason.mute); resumeListening(reason: VoiceCallPauseReason.mute);
@@ -1006,7 +1010,7 @@ class VoiceCallService {
_callKitEventSubscription = null; _callKitEventSubscription = null;
_socketSubscription?.dispose(); _socketSubscription?.dispose();
_voiceInput.dispose(); await _voiceInput.dispose();
await _tts.dispose(); await _tts.dispose();
await _serverAudioStateSub?.cancel(); await _serverAudioStateSub?.cancel();
await _serverAudioPlayer.dispose(); await _serverAudioPlayer.dispose();
@@ -1029,7 +1033,7 @@ class VoiceCallService {
} }
} }
@Riverpod(keepAlive: true) @riverpod
VoiceCallService voiceCallService(Ref ref) { VoiceCallService voiceCallService(Ref ref) {
final voiceInput = ref.watch(voiceInputServiceProvider); final voiceInput = ref.watch(voiceInputServiceProvider);
final api = ref.watch(apiServiceProvider); final api = ref.watch(apiServiceProvider);

View File

@@ -1026,12 +1026,12 @@ class VoiceInputService {
}); });
} }
void dispose() { Future<void> dispose() async {
stopListening(); await stopListening();
unawaited(_disposeVadHandler()); await _disposeVadHandler();
unawaited(_microphonePermissionProbe.dispose()); await _microphonePermissionProbe.dispose();
try { try {
_speech.stop(); await _speech.stop();
} catch (_) {} } catch (_) {}
} }
} }