@@ -10,6 +10,7 @@ import 'package:agixt/services/watch_service.dart';
1010import 'package:agixt/services/wake_word_service.dart' ;
1111import 'package:agixt/services/voice_input_service.dart' ;
1212import 'package:agixt/services/tts_service.dart' ;
13+ import 'package:agixt/services/audio_player_service.dart' ;
1314import 'package:flutter/foundation.dart' ;
1415import 'package:flutter/services.dart' ; // Import Services
1516
@@ -25,6 +26,7 @@ class AIService {
2526 final WakeWordService _wakeWordService = WakeWordService .singleton;
2627 final VoiceInputService _voiceInputService = VoiceInputService .singleton;
2728 final TTSService _ttsService = TTSService .singleton;
29+ final AudioPlayerService _audioPlayerService = AudioPlayerService .singleton;
2830 WhisperService ? _whisperService;
2931 final AGiXTChatWidget _chatWidget = AGiXTChatWidget ();
3032 final AGiXTWebSocketService _webSocketService = AGiXTWebSocketService ();
@@ -55,7 +57,7 @@ class AIService {
5557 // Method channel handler will be set up when needed
5658 }
5759
58- /// Initialize new services (watch, wake word, voice input, TTS)
60+ /// Initialize new services (watch, wake word, voice input, TTS, audio player )
5961 Future <void > _initNewServices () async {
6062 try {
6163 // Initialize watch service
@@ -70,6 +72,9 @@ class AIService {
7072 // Initialize TTS service
7173 await _ttsService.initialize ();
7274
75+ // Initialize audio player service for streaming PCM playback
76+ await _audioPlayerService.initialize ();
77+
7378 // Set up wake word listener
7479 _wakeWordSubscription = _wakeWordService.eventStream.listen (
7580 _handleWakeWordEvent,
@@ -385,7 +390,8 @@ class AIService {
385390 if (_bluetoothManager.isConnected) {
386391 final now = DateTime .now ();
387392 if (lastGlassesUpdate == null ||
388- now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
393+ now.difference (lastGlassesUpdate! ) >
394+ glassesUpdateInterval) {
389395 lastGlassesUpdate = now;
390396 // Send full accumulated text so far
391397 await _bluetoothManager.sendAIResponse (
@@ -397,38 +403,48 @@ class AIService {
397403 break ;
398404
399405 case ChatStreamEventType .audioHeader:
400- // Audio format info - used for watch playback
406+ // Audio format info - start streaming playback
401407 if (event.sampleRate != null ) {
402408 debugPrint (
403409 'AIService: Audio header - ${event .sampleRate }Hz, ${event .bitsPerSample }bit, ${event .channels }ch' );
404410 audioHeaderSent = true ;
405- // Send audio header to watch if connected
411+ // Send audio header to watch if connected, otherwise start phone playback
406412 if (_watchService.isConnected) {
407413 await _watchService.sendAudioHeader (
408414 sampleRate: event.sampleRate! ,
409415 bitsPerSample: event.bitsPerSample ?? 16 ,
410416 channels: event.channels ?? 1 ,
411417 );
418+ } else {
419+ // Start streaming audio on phone speaker
420+ await _audioPlayerService.startStreaming (
421+ sampleRate: event.sampleRate! ,
422+ bitsPerSample: event.bitsPerSample ?? 16 ,
423+ channels: event.channels ?? 1 ,
424+ );
412425 }
413426 }
414427 break ;
415428
416429 case ChatStreamEventType .audioChunk:
417- // Stream audio to watch speaker
430+ // Stream audio to watch speaker or phone speaker
418431 if (event.audioData != null && audioHeaderSent) {
419- // Only mark as received if watch is connected and audio is actually played
432+ hasReceivedAudio = true ;
420433 if (_watchService.isConnected) {
421- hasReceivedAudio = true ;
422434 await _watchService.sendAudioChunk (event.audioData! );
435+ } else {
436+ // Play on phone speaker
437+ await _audioPlayerService.feedAudioChunk (event.audioData! );
423438 }
424- // If watch not connected, audio is not played - fallback TTS will be used
425439 }
426440 break ;
427441
428442 case ChatStreamEventType .audioEnd:
429443 // Audio streaming complete
430444 if (_watchService.isConnected) {
431445 await _watchService.sendAudioEnd ();
446+ } else {
447+ await _audioPlayerService.stopStreaming ();
432448 }
433449 debugPrint ('AIService: Audio streaming complete' );
434450 break ;
@@ -454,18 +470,16 @@ class AIService {
454470 if (_watchService.isConnected) {
455471 await _watchService.displayMessage (fullResponse, durationMs: 10000 );
456472 }
457- // If no audio was streamed (no watch connected or AGiXT didn't return audio),
458- // use phone TTS as fallback
459- if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
460- debugPrint ('AIService: No streaming audio received, using phone TTS' );
461- await _ttsService.speak (fullResponse);
462- }
473+ // Note: Audio is streamed in real-time via audioHeader/audioChunk events
474+ // No need for TTS fallback - AGiXT sends audio during the stream
463475 } else {
464476 await _showErrorMessage ('No response from AGiXT' );
465477 }
466478 } catch (e) {
467479 debugPrint ('AIService: Error processing audio: $e ' );
468480 await _showErrorMessage ('Error processing voice input' );
481+ // Stop any playing audio on error
482+ await _audioPlayerService.stopStreaming ();
469483 } finally {
470484 _isProcessing = false ;
471485 // Resume wake word listening if enabled
@@ -717,7 +731,8 @@ class AIService {
717731 if (_bluetoothManager.isConnected) {
718732 final now = DateTime .now ();
719733 if (lastGlassesUpdate == null ||
720- now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
734+ now.difference (lastGlassesUpdate! ) >
735+ glassesUpdateInterval) {
721736 lastGlassesUpdate = now;
722737 await _bluetoothManager.sendAIResponse (
723738 responseBuffer.toString (),
@@ -728,7 +743,7 @@ class AIService {
728743 break ;
729744
730745 case ChatStreamEventType .audioHeader:
731- // Send audio format to watch
746+ // Send audio format to watch or start phone playback
732747 if (event.sampleRate != null ) {
733748 debugPrint (
734749 'AIService: Audio header - ${event .sampleRate }Hz, ${event .bitsPerSample }bit, ${event .channels }ch' );
@@ -739,26 +754,36 @@ class AIService {
739754 bitsPerSample: event.bitsPerSample ?? 16 ,
740755 channels: event.channels ?? 1 ,
741756 );
757+ } else {
758+ // Start streaming audio on phone speaker
759+ await _audioPlayerService.startStreaming (
760+ sampleRate: event.sampleRate! ,
761+ bitsPerSample: event.bitsPerSample ?? 16 ,
762+ channels: event.channels ?? 1 ,
763+ );
742764 }
743765 }
744766 break ;
745767
746768 case ChatStreamEventType .audioChunk:
747- // Stream audio to watch speaker
769+ // Stream audio to watch speaker or phone speaker
748770 if (event.audioData != null && audioHeaderSent) {
749- // Only mark as received if watch is connected and audio is actually played
771+ hasReceivedAudio = true ;
750772 if (_watchService.isConnected) {
751- hasReceivedAudio = true ;
752773 await _watchService.sendAudioChunk (event.audioData! );
774+ } else {
775+ // Play on phone speaker
776+ await _audioPlayerService.feedAudioChunk (event.audioData! );
753777 }
754- // If watch not connected, audio is not played - fallback TTS will be used
755778 }
756779 break ;
757780
758781 case ChatStreamEventType .audioEnd:
759782 // Audio streaming complete
760783 if (_watchService.isConnected) {
761784 await _watchService.sendAudioEnd ();
785+ } else {
786+ await _audioPlayerService.stopStreaming ();
762787 }
763788 debugPrint ('AIService: Audio streaming complete' );
764789 break ;
@@ -782,17 +807,15 @@ class AIService {
782807 if (_watchService.isConnected) {
783808 await _watchService.displayMessage (response, durationMs: 10000 );
784809 }
785- // If no audio was streamed, use phone TTS as fallback
786- if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
787- debugPrint ('AIService: No streaming audio received, using phone TTS' );
788- await _ttsService.speak (response);
789- }
810+ // Note: Audio is streamed in real-time via audioHeader/audioChunk events
790811 } else {
791812 await _showErrorMessage ('No response from AGiXT' );
792813 }
793814 } catch (e) {
794815 debugPrint ('Error sending message to AGiXT: $e ' );
795816 await _showErrorMessage ('Failed to get response from AGiXT' );
817+ // Stop any playing audio on error
818+ await _audioPlayerService.stopStreaming ();
796819 }
797820 }
798821
@@ -863,7 +886,8 @@ class AIService {
863886 if (_bluetoothManager.isConnected) {
864887 final now = DateTime .now ();
865888 if (lastGlassesUpdate == null ||
866- now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
889+ now.difference (lastGlassesUpdate! ) >
890+ glassesUpdateInterval) {
867891 lastGlassesUpdate = now;
868892 await _bluetoothManager.sendAIResponse (
869893 responseBuffer.toString (),
@@ -882,24 +906,34 @@ class AIService {
882906 bitsPerSample: event.bitsPerSample ?? 16 ,
883907 channels: event.channels ?? 1 ,
884908 );
909+ } else {
910+ // Start streaming audio on phone speaker
911+ await _audioPlayerService.startStreaming (
912+ sampleRate: event.sampleRate! ,
913+ bitsPerSample: event.bitsPerSample ?? 16 ,
914+ channels: event.channels ?? 1 ,
915+ );
885916 }
886917 }
887918 break ;
888919
889920 case ChatStreamEventType .audioChunk:
890921 if (event.audioData != null && audioHeaderSent) {
891- // Only mark as received if watch is connected and audio is actually played
922+ hasReceivedAudio = true ;
892923 if (_watchService.isConnected) {
893- hasReceivedAudio = true ;
894924 await _watchService.sendAudioChunk (event.audioData! );
925+ } else {
926+ // Play on phone speaker
927+ await _audioPlayerService.feedAudioChunk (event.audioData! );
895928 }
896- // If watch not connected, audio is not played - fallback TTS will be used
897929 }
898930 break ;
899931
900932 case ChatStreamEventType .audioEnd:
901933 if (_watchService.isConnected) {
902934 await _watchService.sendAudioEnd ();
935+ } else {
936+ await _audioPlayerService.stopStreaming ();
903937 }
904938 break ;
905939
@@ -916,16 +950,15 @@ class AIService {
916950 if (_watchService.isConnected) {
917951 await _watchService.displayMessage (response, durationMs: 10000 );
918952 }
919- // Fallback to phone TTS if no streaming audio
920- if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
921- await _ttsService.speak (response);
922- }
953+ // Note: Audio is streamed in real-time via audioHeader/audioChunk events
923954 } else {
924955 await _showErrorMessage ('No response from AGiXT' );
925956 }
926957 } catch (e) {
927958 debugPrint ('Error sending message to AGiXT: $e ' );
928959 await _showErrorMessage ('Failed to get response from AGiXT' );
960+ // Stop any playing audio on error
961+ await _audioPlayerService.stopStreaming ();
929962 }
930963 }
931964
0 commit comments