Merge pull request #369 from cogwheel0/fix-voice-call-freezes
fix(voice-call): Improve async handling and state management
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user