11import 'dart:async' ;
22import 'dart:typed_data' ;
33import 'package:agixt/services/bluetooth_manager.dart' ;
4+ import 'package:agixt/services/bluetooth_reciever.dart' ;
45import 'package:agixt/services/watch_service.dart' ;
56import 'package:agixt/services/wake_word_service.dart' ;
7+ import 'package:agixt/utils/lc3.dart' ;
68import 'package:flutter/foundation.dart' ;
7- import 'package:flutter/services.dart' ;
89import 'package:flutter_sound/flutter_sound.dart' ;
910import 'package:path_provider/path_provider.dart' ;
1011import 'package:shared_preferences/shared_preferences.dart' ;
@@ -33,10 +34,8 @@ class VoiceInputService {
3334 final FlutterSoundRecorder _recorder = FlutterSoundRecorder ();
3435 bool _recorderInitialized = false ;
3536
36- // Method channel for glasses microphone
37- static const MethodChannel _glassesAudioChannel = MethodChannel (
38- 'dev.agixt.agixt/glasses_audio' ,
39- );
37+ // Bluetooth receiver for glasses audio
38+ final BluetoothReciever _bluetoothReciever = BluetoothReciever .singleton;
4039
4140 // State
4241 bool _isRecording = false ;
@@ -75,8 +74,8 @@ class VoiceInputService {
7574 // Initialize phone recorder
7675 await _initializeRecorder ();
7776
78- // Set up glasses audio callback
79- _glassesAudioChannel. setMethodCallHandler (_handleGlassesAudioCall);
77+ // Note: Glasses audio is handled by BluetoothReciever.voiceCollectorAI
78+ // No additional setup needed here
8079
8180 // Note: Wake word callback is handled by AIService which coordinates
8281 // the full flow (recording -> transcription -> AGiXT -> response)
@@ -99,29 +98,9 @@ class VoiceInputService {
9998 }
10099 }
101100
102- /// Handle method calls from native glasses audio code
103- Future <dynamic > _handleGlassesAudioCall (MethodCall call) async {
104- switch (call.method) {
105- case 'onAudioData' :
106- final audioData = call.arguments['audioData' ] as Uint8List ? ;
107- if (audioData != null &&
108- _isRecording &&
109- _activeSource == VoiceInputSource .glasses) {
110- _audioChunkController.add (audioData);
111- }
112- return true ;
113-
114- case 'onRecordingComplete' :
115- final audioData = call.arguments['audioData' ] as Uint8List ? ;
116- if (_isRecording && _activeSource == VoiceInputSource .glasses) {
117- await _handleRecordingComplete (audioData);
118- }
119- return true ;
120-
121- default :
122- return null ;
123- }
124- }
101+ // Note: Glasses audio is collected by BluetoothReciever.voiceCollectorAI
102+ // when the mic is enabled. We get the data via getAllDataAndReset() in
103+ // _stopGlassesRecording().
125104
126105 // Note: Wake word detection is handled by AIService which listens to
127106 // WakeWordService.eventStream and coordinates the full voice input flow.
@@ -235,19 +214,26 @@ class VoiceInputService {
235214 /// Start recording from glasses microphone
236215 Future <bool > _startGlassesRecording (Duration maxDuration) async {
237216 try {
217+ // Reset voice collector buffer before starting
218+ _bluetoothReciever.voiceCollectorAI.reset ();
219+ _bluetoothReciever.voiceCollectorAI.isRecording = true ;
220+
238221 // Enable microphone on glasses
239222 await _bluetoothManager.setMicrophone (true );
223+ debugPrint ('VoiceInputService: Glasses mic enabled, recording started' );
240224
241225 // Set up auto-stop timer
242226 Timer (maxDuration, () {
243227 if (_isRecording && _activeSource == VoiceInputSource .glasses) {
228+ debugPrint ('VoiceInputService: Auto-stopping glasses recording after $maxDuration ' );
244229 stopRecording ();
245230 }
246231 });
247232
248233 return true ;
249234 } catch (e) {
250235 debugPrint ('VoiceInputService: Error starting glasses recording: $e ' );
236+ _bluetoothReciever.voiceCollectorAI.isRecording = false ;
251237 return false ;
252238 }
253239 }
@@ -337,11 +323,13 @@ class VoiceInputService {
337323 final source = _activeSource;
338324 _activeSource = null ;
339325
326+ // Use 'complete' status when we have audio data so AIService processes it
340327 _stateController.add (
341328 VoiceInputState (
342329 isRecording: false ,
343330 source: source,
344- status: VoiceInputStatus .stopped,
331+ status: audioData != null ? VoiceInputStatus .complete : VoiceInputStatus .stopped,
332+ audioData: audioData,
345333 ),
346334 );
347335
@@ -356,18 +344,33 @@ class VoiceInputService {
356344 /// Stop glasses recording
357345 Future <Uint8List ?> _stopGlassesRecording () async {
358346 try {
347+ // Stop recording and disable mic
348+ _bluetoothReciever.voiceCollectorAI.isRecording = false ;
359349 await _bluetoothManager.setMicrophone (false );
360350
361- // Get recorded audio from native code
362- final result = await _glassesAudioChannel. invokeMethod (
363- 'getRecordedAudio' ,
364- );
365- if (result is Uint8List ) {
366- return result ;
351+ // Get all collected voice data (LC3 encoded)
352+ final lc3Data = await _bluetoothReciever.voiceCollectorAI. getAllDataAndReset ();
353+
354+ if (lc3Data.isEmpty) {
355+ debugPrint ( 'VoiceInputService: No voice data collected from glasses' );
356+ return null ;
367357 }
368- return null ;
358+
359+ debugPrint ('VoiceInputService: Got ${lc3Data .length } bytes of LC3 data from glasses' );
360+
361+ // Decode LC3 to PCM
362+ final pcmData = await LC3 .decodeLC3 (Uint8List .fromList (lc3Data));
363+
364+ if (pcmData.isEmpty) {
365+ debugPrint ('VoiceInputService: LC3 decode returned empty PCM data' );
366+ return null ;
367+ }
368+
369+ debugPrint ('VoiceInputService: Decoded to ${pcmData .length } bytes of PCM audio' );
370+ return pcmData;
369371 } catch (e) {
370372 debugPrint ('VoiceInputService: Error stopping glasses recording: $e ' );
373+ _bluetoothReciever.voiceCollectorAI.isRecording = false ;
371374 return null ;
372375 }
373376 }
0 commit comments