audio_service_impl.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:record/record.dart';
  4. import 'package:flutter_sound/flutter_sound.dart';
  5. import 'package:gemini_live_app/services/audio_service.dart';
  6. import 'package:audio_session/audio_session.dart';
  7. /// An implementation of the [AudioService] interface.
  8. ///
  9. /// This class handles the recording and playback of audio using the `record`
  10. /// and `flutter_sound` packages.
  11. class AudioServiceImpl implements AudioService {
  12. final _audioRecorder = AudioRecorder();
  13. final FlutterSoundPlayer _audioPlayer = FlutterSoundPlayer();
  14. bool _isRecording = false;
  15. StreamSubscription? _recordingSubscription;
  16. StreamSubscription? _playbackSubscription;
  17. Timer? _llmSpeechTimeout;
  18. @override
  19. final isLLMSpeaking = ValueNotifier<bool>(false);
  20. final _outgoingAudioStreamController =
  21. StreamController<Uint8List>.broadcast();
  22. @override
  23. Stream<Uint8List> get outgoingAudioStream =>
  24. _outgoingAudioStreamController.stream;
  25. /// Initializes the audio service.
  26. ///
  27. /// This method requests microphone permission and configures the audio session.
  28. @override
  29. Future<bool> initialize() async {
  30. try {
  31. if (!await _audioRecorder.hasPermission()) {
  32. return false;
  33. }
  34. final session = await AudioSession.instance;
  35. await session.configure(
  36. const AudioSessionConfiguration(
  37. avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
  38. avAudioSessionCategoryOptions:
  39. AVAudioSessionCategoryOptions.defaultToSpeaker,
  40. avAudioSessionMode: AVAudioSessionMode.defaultMode,
  41. avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
  42. androidAudioAttributes: AndroidAudioAttributes(
  43. contentType: AndroidAudioContentType.speech,
  44. flags: AndroidAudioFlags.none,
  45. usage: AndroidAudioUsage.media,
  46. ),
  47. androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
  48. androidWillPauseWhenDucked: true,
  49. ),
  50. );
  51. await _audioPlayer.openPlayer();
  52. return true;
  53. } catch (e) {
  54. return false;
  55. }
  56. }
  57. /// Starts recording audio from the microphone.
  58. ///
  59. /// The recorded audio is added to the [outgoingAudioStream].
  60. @override
  61. Future<void> startRecording() async {
  62. if (_isRecording) {
  63. return;
  64. }
  65. _isRecording = true;
  66. try {
  67. final stream = await _audioRecorder.startStream(
  68. const RecordConfig(
  69. encoder: AudioEncoder.pcm16bits,
  70. sampleRate: 16000,
  71. numChannels: 1,
  72. noiseSuppress: true,
  73. autoGain: true,
  74. iosConfig: IosRecordConfig(
  75. categoryOptions: [
  76. IosAudioCategoryOption.defaultToSpeaker,
  77. IosAudioCategoryOption.allowBluetooth,
  78. IosAudioCategoryOption.allowBluetoothA2DP,
  79. ],
  80. ),
  81. ),
  82. );
  83. await _audioRecorder.ios?.manageAudioSession(true);
  84. _recordingSubscription = stream.listen(
  85. (data) {
  86. _outgoingAudioStreamController.add(data);
  87. },
  88. onDone: () {
  89. _isRecording = false;
  90. },
  91. onError: (e) {
  92. _isRecording = false;
  93. },
  94. );
  95. } catch (e) {
  96. _isRecording = false;
  97. }
  98. }
  99. /// Stops recording audio.
  100. @override
  101. Future<void> stopRecording() async {
  102. if (!_isRecording) {
  103. return;
  104. }
  105. await _audioRecorder.stop();
  106. _recordingSubscription?.cancel();
  107. _recordingSubscription = null;
  108. _isRecording = false;
  109. }
  110. @override
  111. Future<void> clearPlaybackQueue() async {
  112. if (_audioPlayer.isPlaying) {
  113. await _audioPlayer.stopPlayer();
  114. }
  115. _llmSpeechTimeout?.cancel();
  116. if (isLLMSpeaking.value) {
  117. isLLMSpeaking.value = false;
  118. }
  119. }
  120. /// Plays the incoming audio stream.
  121. @override
  122. Future<void> playIncomingAudio(Stream<Uint8List> audioStream) async {
  123. if (!_audioPlayer.isOpen()) {
  124. return;
  125. }
  126. _playbackSubscription?.cancel();
  127. _playbackSubscription = audioStream.listen(
  128. (audioChunk) async {
  129. _playbackSubscription?.pause();
  130. try {
  131. if (!_audioPlayer.isPlaying) {
  132. await _audioPlayer.setVolume(1.0);
  133. await _audioPlayer.startPlayerFromStream(
  134. codec: Codec.pcm16,
  135. numChannels: 1,
  136. sampleRate: 24000,
  137. interleaved: true,
  138. bufferSize: 8192,
  139. );
  140. }
  141. await _audioPlayer.feedUint8FromStream(audioChunk);
  142. if (!isLLMSpeaking.value) {
  143. isLLMSpeaking.value = true;
  144. }
  145. _llmSpeechTimeout?.cancel();
  146. _llmSpeechTimeout = Timer(const Duration(milliseconds: 1200), () {
  147. if (isLLMSpeaking.value) {
  148. isLLMSpeaking.value = false;
  149. }
  150. });
  151. } finally {
  152. _playbackSubscription?.resume();
  153. }
  154. },
  155. onDone: () {
  156. _llmSpeechTimeout?.cancel();
  157. _llmSpeechTimeout = Timer(const Duration(milliseconds: 1200), () {
  158. if (isLLMSpeaking.value) {
  159. isLLMSpeaking.value = false;
  160. }
  161. });
  162. },
  163. onError: (e) {
  164. if (_audioPlayer.isPlaying) {
  165. _audioPlayer.stopPlayer();
  166. }
  167. _llmSpeechTimeout?.cancel();
  168. if (isLLMSpeaking.value) {
  169. isLLMSpeaking.value = false;
  170. }
  171. },
  172. cancelOnError: true,
  173. );
  174. }
  175. /// Stops the audio playback.
  176. @override
  177. Future<void> stopPlayback() async {
  178. if (_audioPlayer.isPlaying) {
  179. await _audioPlayer.stopPlayer();
  180. _llmSpeechTimeout?.cancel();
  181. if (isLLMSpeaking.value) {
  182. isLLMSpeaking.value = false;
  183. }
  184. }
  185. }
  186. /// Disposes of the audio resources.
  187. @override
  188. void dispose() {
  189. _llmSpeechTimeout?.cancel();
  190. _recordingSubscription?.cancel();
  191. _playbackSubscription?.cancel();
  192. if (_isRecording) {
  193. _audioRecorder.stop();
  194. }
  195. _audioRecorder.dispose();
  196. _audioPlayer.closePlayer();
  197. _outgoingAudioStreamController.close();
  198. isLLMSpeaking.dispose();
  199. }
  200. @override
  201. bool get isRecording => _isRecording;
  202. }