1111// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212// See the License for the specific language governing permissions and
1313// limitations under the License.
14- import 'dart:typed_data' ;
1514import 'dart:async' ;
16- import 'dart:developer' ;
15+ import 'dart:developer' as developer ;
1716
1817import 'package:flutter/material.dart' ;
1918import 'package:firebase_ai/firebase_ai.dart' ;
19+
20+ import '../utils/audio_input.dart' ;
21+ import '../utils/audio_output.dart' ;
2022import '../widgets/message_widget.dart' ;
21- import '../utils/audio_player.dart' ;
22- import '../utils/audio_recorder.dart' ;
2323
2424class BidiPage extends StatefulWidget {
2525 const BidiPage ({super .key, required this .title, required this .model});
@@ -48,11 +48,9 @@ class _BidiPageState extends State<BidiPage> {
4848 bool _recording = false ;
4949 late LiveGenerativeModel _liveModel;
5050 late LiveSession _session;
51- final _audioManager = AudioStreamManager ();
52- final _audioRecorder = InMemoryAudioRecorder ();
53- var _chunkBuilder = BytesBuilder ();
54- var _audioIndex = 0 ;
5551 StreamController <bool > _stopController = StreamController <bool >();
52+ final AudioOutput _audioOutput = AudioOutput ();
53+ final AudioInput _audioInput = AudioInput ();
5654
5755 @override
5856 void initState () {
@@ -65,13 +63,20 @@ class _BidiPageState extends State<BidiPage> {
6563 ],
6664 );
6765
66+ // ignore: deprecated_member_use
6867 _liveModel = FirebaseAI .vertexAI ().liveGenerativeModel (
6968 model: 'gemini-2.0-flash-exp' ,
7069 liveGenerationConfig: config,
7170 tools: [
7271 Tool .functionDeclarations ([lightControlTool]),
7372 ],
7473 );
74+ _initAudio ();
75+ }
76+
77+ Future <void > _initAudio () async {
78+ await _audioOutput.init ();
79+ await _audioInput.init ();
7580 }
7681
7782 void _scrollDown () {
@@ -89,13 +94,7 @@ class _BidiPageState extends State<BidiPage> {
8994 @override
9095 void dispose () {
9196 if (_sessionOpening) {
92- _audioManager.stopAudioPlayer ();
93- _audioManager.disposeAudioPlayer ();
94-
95- _audioRecorder.stopRecording ();
96-
9797 _stopController.close ();
98-
9998 _sessionOpening = false ;
10099 _session.close ();
101100 }
@@ -234,7 +233,7 @@ class _BidiPageState extends State<BidiPage> {
234233 _sessionOpening = true ;
235234 _stopController = StreamController <bool >();
236235 unawaited (
237- processMessagesContinuously (
236+ _processMessagesContinuously (
238237 stopSignal: _stopController,
239238 ),
240239 );
@@ -243,8 +242,6 @@ class _BidiPageState extends State<BidiPage> {
243242 await _stopController.close ();
244243
245244 await _session.close ();
246- await _audioManager.stopAudioPlayer ();
247- await _audioManager.disposeAudioPlayer ();
248245 _sessionOpening = false ;
249246 }
250247
@@ -258,21 +255,25 @@ class _BidiPageState extends State<BidiPage> {
258255 _recording = true ;
259256 });
260257 try {
261- await _audioRecorder. checkPermission ();
262- final audioRecordStream = _audioRecorder. startRecordingStream ();
258+ var inputStream = await _audioInput. startRecordingStream ();
259+ await _audioOutput. playStream ();
263260 // Map the Uint8List stream to InlineDataPart stream
264- final mediaChunkStream = audioRecordStream.map ((data) {
265- return InlineDataPart ('audio/pcm' , data);
266- });
267- await _session.sendMediaStream (mediaChunkStream);
261+ if (inputStream != null ) {
262+ final inlineDataStream = inputStream.map ((data) {
263+ return InlineDataPart ('audio/pcm' , data);
264+ });
265+
266+ await _session.sendMediaStream (inlineDataStream);
267+ }
268268 } catch (e) {
269+ developer.log (e.toString ());
269270 _showError (e.toString ());
270271 }
271272 }
272273
273274 Future <void > _stopRecording () async {
274275 try {
275- await _audioRecorder .stopRecording ();
276+ await _audioInput .stopRecording ();
276277 } catch (e) {
277278 _showError (e.toString ());
278279 }
@@ -298,7 +299,7 @@ class _BidiPageState extends State<BidiPage> {
298299 });
299300 }
300301
301- Future <void > processMessagesContinuously ({
302+ Future <void > _processMessagesContinuously ({
302303 required StreamController <bool > stopSignal,
303304 }) async {
304305 bool shouldContinue = true ;
@@ -335,11 +336,8 @@ class _BidiPageState extends State<BidiPage> {
335336 if (message.modelTurn != null ) {
336337 await _handleLiveServerContent (message);
337338 }
338- if (message.turnComplete != null && message.turnComplete! ) {
339- await _handleTurnComplete ();
340- }
341339 if (message.interrupted != null && message.interrupted! ) {
342- log ('Interrupted: $response ' );
340+ developer. log ('Interrupted: $response ' );
343341 }
344342 } else if (message is LiveServerToolCall && message.functionCalls != null ) {
345343 await _handleLiveServerToolCall (message);
@@ -355,7 +353,7 @@ class _BidiPageState extends State<BidiPage> {
355353 } else if (part is InlineDataPart ) {
356354 await _handleInlineDataPart (part);
357355 } else {
358- log ('receive part with type ${part .runtimeType }' );
356+ developer. log ('receive part with type ${part .runtimeType }' );
359357 }
360358 }
361359 }
@@ -376,29 +374,7 @@ class _BidiPageState extends State<BidiPage> {
376374
377375 Future <void > _handleInlineDataPart (InlineDataPart part) async {
378376 if (part.mimeType.startsWith ('audio' )) {
379- _chunkBuilder.add (part.bytes);
380- _audioIndex++ ;
381- if (_audioIndex == 15 ) {
382- Uint8List chunk = await audioChunkWithHeader (
383- _chunkBuilder.toBytes (),
384- 24000 ,
385- );
386- _audioManager.addAudio (chunk);
387- _chunkBuilder.clear ();
388- _audioIndex = 0 ;
389- }
390- }
391- }
392-
393- Future <void > _handleTurnComplete () async {
394- if (_chunkBuilder.isNotEmpty) {
395- Uint8List chunk = await audioChunkWithHeader (
396- _chunkBuilder.toBytes (),
397- 24000 ,
398- );
399- _audioManager.addAudio (chunk);
400- _audioIndex = 0 ;
401- _chunkBuilder.clear ();
377+ _audioOutput.addAudioStream (part.bytes);
402378 }
403379 }
404380
0 commit comments