7575import javax .sound .sampled .LineUnavailableException ;
7676import javax .sound .sampled .SourceDataLine ;
7777import javax .sound .sampled .TargetDataLine ;
78+ import java .io .ByteArrayOutputStream ;
79+ import java .io .ByteArrayInputStream ;
80+ import java .io .IOException ;
81+ import java .io .File ;
82+ import java .nio .file .Files ;
83+ import java .nio .file .Paths ;
84+ import javax .sound .sampled .AudioInputStream ;
85+ import javax .sound .sampled .AudioFileFormat ;
86+ import com .google .genai .types .ReplicatedVoiceConfig ;
87+ import com .google .genai .types .VoiceConsentSignature ;
7888
7989/** Example of using the live module for a streaming audio conversation. */
8090public final class LiveAudioConversationAsync {
@@ -93,14 +103,15 @@ public final class LiveAudioConversationAsync {
93103 private static TargetDataLine microphoneLine ;
94104 private static SourceDataLine speakerLine ;
95105 private static AsyncSession session ;
96- private static ExecutorService micExecutor = Executors .newSingleThreadExecutor ();
106+ private static final ExecutorService micExecutor = Executors .newSingleThreadExecutor ();
107+ private static String promptString = null ;
108+ private static final ByteArrayOutputStream audioResponseBytes = new ByteArrayOutputStream ();
97109
98110 /** Creates the parameters for sending an audio chunk. */
99111 public static LiveSendRealtimeInputParameters createAudioContent (byte [] audioData ) {
100112
101113 if (audioData == null ) {
102- System .err .println ("Error: Audio is null" );
103- return null ;
114+ throw new IllegalArgumentException ("Audio data cannot be null" );
104115 }
105116
106117 return LiveSendRealtimeInputParameters .builder ()
@@ -155,31 +166,95 @@ public static void main(String[] args) throws LineUnavailableException {
155166 System .out .println ("Using Gemini Developer API" );
156167 }
157168
169+ String getModelFromArgs = null ;
170+ String voiceSamplePath = null ;
171+ String voiceConsentPath = null ;
172+ String voiceSignature = null ;
173+ promptString = null ;
174+
175+ for (String arg : args ) {
176+ if (arg .startsWith ("--model=" )) {
177+ getModelFromArgs = arg .substring ("--model=" .length ());
178+ } else if (arg .startsWith ("--voice-sample=" )) {
179+ voiceSamplePath = arg .substring ("--voice-sample=" .length ());
180+ } else if (arg .startsWith ("--voice-consent=" )) {
181+ voiceConsentPath = arg .substring ("--voice-consent=" .length ());
182+ } else if (arg .startsWith ("--voice-signature=" )) {
183+ voiceSignature = arg .substring ("--voice-signature=" .length ());
184+ } else if (arg .startsWith ("--prompt=" )) {
185+ promptString = arg .substring ("--prompt=" .length ());
186+ } else if (!arg .startsWith ("--" ) && getModelFromArgs == null ) {
187+ getModelFromArgs = arg ;
188+ }
189+ }
190+
191+ byte [] voiceSampleAudio = null ;
192+ byte [] consentAudio = null ;
193+
194+ if (voiceSamplePath != null ) {
195+ try {
196+ voiceSampleAudio = Files .readAllBytes (Paths .get (voiceSamplePath ));
197+ } catch (IOException e ) {
198+ throw new IllegalStateException ("Failed to read voice sample" , e );
199+ }
200+ if (voiceConsentPath == null && voiceSignature == null ) {
201+ throw new IllegalArgumentException (
202+ "Either --voice-consent or --voice-signature must be provided when --voice-sample is"
203+ + " used." );
204+ }
205+ }
206+ if (voiceConsentPath != null ) {
207+ try {
208+ consentAudio = Files .readAllBytes (Paths .get (voiceConsentPath ));
209+ } catch (IOException e ) {
210+ throw new IllegalStateException ("Failed to read voice consent" , e );
211+ }
212+ }
213+
158214 final String modelId ;
159- if (args . length != 0 ) {
160- modelId = args [ 0 ] ;
215+ if (getModelFromArgs != null ) {
216+ modelId = getModelFromArgs ;
161217 } else if (client .vertexAI ()) {
162218 modelId = Constants .GEMINI_LIVE_MODEL_NAME ;
163219 } else {
164220 modelId = Constants .GEMINI_LIVE_MODEL_NAME_PREVIEW ;
165221 }
166222
167223 // --- Audio Line Setup ---
168- microphoneLine = getMicrophoneLine ();
169- speakerLine = getSpeakerLine ();
224+ if (promptString == null ) {
225+ microphoneLine = getMicrophoneLine ();
226+ speakerLine = getSpeakerLine ();
227+ }
170228
171229 // --- Live API Config for Audio ---
172230 // Choice of ["Aoede", "Puck", "Charon", "Kore", "Fenrir", "Leda", "Orus", "Zephyr"]
173231 String voiceName = "Aoede" ;
232+
233+ VoiceConfig .Builder voiceConfigBuilder = VoiceConfig .builder ();
234+ if (voiceSampleAudio != null ) {
235+ ReplicatedVoiceConfig .Builder repBuilder =
236+ ReplicatedVoiceConfig .builder ()
237+ .mimeType ("audio/wav" )
238+ .voiceSampleAudio (voiceSampleAudio );
239+ if (consentAudio != null ) {
240+ repBuilder .consentAudio (consentAudio );
241+ }
242+ if (voiceSignature != null ) {
243+ repBuilder .voiceConsentSignature (
244+ VoiceConsentSignature .builder ().signature (voiceSignature ));
245+ }
246+ voiceConfigBuilder .replicatedVoiceConfig (repBuilder );
247+ } else {
248+ voiceConfigBuilder .prebuiltVoiceConfig (
249+ PrebuiltVoiceConfig .builder ().voiceName (voiceName ));
250+ }
251+
174252 LiveConnectConfig config =
175253 LiveConnectConfig .builder ()
176254 .responseModalities (Modality .Known .AUDIO )
177255 .speechConfig (
178256 SpeechConfig .builder ()
179- .voiceConfig (
180- VoiceConfig .builder ()
181- .prebuiltVoiceConfig (
182- PrebuiltVoiceConfig .builder ().voiceName (voiceName )))
257+ .voiceConfig (voiceConfigBuilder )
183258 .languageCode ("en-US" ))
184259 .realtimeInputConfig (
185260 RealtimeInputConfig .builder ()
@@ -215,8 +290,11 @@ public static void main(String[] args) throws LineUnavailableException {
215290 System .out .println ("Closing API session..." );
216291 session .close ().get (5 , TimeUnit .SECONDS ); // Wait with timeout
217292 System .out .println ("API session closed." );
218- } catch (Exception e ) {
293+ } catch (ExecutionException | java . util . concurrent . TimeoutException e ) {
219294 System .err .println ("Error closing API session: " + e .getMessage ());
295+ } catch (InterruptedException e ) {
296+ System .err .println ("Interrupted while closing API session" );
297+ Thread .currentThread ().interrupt ();
220298 }
221299 }
222300 // Close audio lines
@@ -232,19 +310,33 @@ public static void main(String[] args) throws LineUnavailableException {
232310 session = client .async .live .connect (modelId , config ).get ();
233311 System .out .println ("Connected." );
234312
235- // --- Start Audio Lines ---
236- microphoneLine .start ();
237- speakerLine .start ();
238- System .out .println ("Microphone and speakers started. Speak now (Press Ctrl+C to exit)..." );
313+ if (session .setupComplete () != null && session .setupComplete ().voiceConsentSignature ().isPresent ()) {
314+ System .out .println (
315+ "\n === Voice Consent Signature Received ===\n "
316+ + session .setupComplete ().voiceConsentSignature ().get ().signature ().orElse ("" )
317+ + "\n ========================================\n " );
318+ }
239319
240320 // --- Start Receiving Audio Responses ---
241321 CompletableFuture <Void > receiveFuture =
242322 session .receive (LiveAudioConversationAsync ::handleAudioResponse );
243- System .err .println ("Receive stream started." ); // Add this line
244-
245- // --- Start Sending Microphone Audio ---
246- CompletableFuture <Void > sendFuture =
247- CompletableFuture .runAsync (LiveAudioConversationAsync ::sendMicrophoneAudio , micExecutor );
323+ System .err .println ("Receive stream started." );
324+
325+ CompletableFuture <Void > sendFuture ;
326+ if (promptString == null ) {
327+ // --- Start Audio Lines ---
328+ microphoneLine .start ();
329+ speakerLine .start ();
330+ System .out .println ("Microphone and speakers started. Speak now (Press Ctrl+C to exit)..." );
331+
332+ // --- Start Sending Microphone Audio ---
333+ sendFuture = CompletableFuture .runAsync (LiveAudioConversationAsync ::sendMicrophoneAudio , micExecutor );
334+ } else {
335+ System .out .println ("Sending prompt: " + promptString );
336+ session .sendRealtimeInput (
337+ LiveSendRealtimeInputParameters .builder ().text (promptString ).build ()).get ();
338+ sendFuture = CompletableFuture .completedFuture (null );
339+ }
248340
249341 // Keep the main thread alive. Wait for sending or receiving to finish (or
250342 // error).
@@ -313,13 +405,19 @@ public static void handleAudioResponse(LiveServerMessage message) {
313405 content -> {
314406 // Handle interruptions from Gemini.
315407 if (content .interrupted ().orElse (false )) {
316- speakerLine .flush ();
408+ if (speakerLine != null && speakerLine .isOpen ()) {
409+ speakerLine .flush ();
410+ }
317411 return ; // Skip processing the rest of this message's audio.
318412 }
319413
320414 // Handle Model turn completion.
321415 if (content .turnComplete ().orElse (false )) {
322- // The turn is over, no more audio will be sent for this turn.
416+ if (promptString != null ) {
417+ saveWavFile ();
418+ System .out .println ("Response received, exiting." );
419+ System .exit (0 );
420+ }
323421 return ;
324422 }
325423
@@ -334,15 +432,45 @@ public static void handleAudioResponse(LiveServerMessage message) {
334432 if (speakerLine != null && speakerLine .isOpen ()) {
335433 // Write audio data to the speaker
336434 speakerLine .write (audioBytes , 0 , audioBytes .length );
435+ } else {
436+ System .out .println (
437+ "Received audio response chunk: " + audioBytes .length + " bytes." );
438+ }
439+ try {
440+ audioResponseBytes .write (audioBytes );
441+ } catch (IOException e ) {
442+ System .err .println ("Failed to accumulate audio bytes: " + e .getMessage ());
337443 }
338444 });
339445
340446 // If this is the last message of a generation, drain the buffer.
341447 if (content .generationComplete ().orElse (false )) {
342- speakerLine .drain ();
448+ if (speakerLine != null && speakerLine .isOpen ()) {
449+ speakerLine .drain ();
450+ }
343451 }
344452 });
345453 }
346454
455+ private static void saveWavFile () {
456+ byte [] audioData = audioResponseBytes .toByteArray ();
457+ if (audioData .length == 0 ) {
458+ System .out .println ("No audio data received to save." );
459+ return ;
460+ }
461+ try {
462+ AudioInputStream ais = new AudioInputStream (
463+ new ByteArrayInputStream (audioData ),
464+ SPEAKER_AUDIO_FORMAT ,
465+ audioData .length / SPEAKER_AUDIO_FORMAT .getFrameSize ()
466+ );
467+ File outputFile = new File ("output.wav" );
468+ AudioSystem .write (ais , AudioFileFormat .Type .WAVE , outputFile );
469+ System .out .println ("Saved audio response to " + outputFile .getAbsolutePath ());
470+ } catch (IOException e ) {
471+ System .err .println ("Failed to save WAV file: " + e .getMessage ());
472+ }
473+ }
474+
347475 private LiveAudioConversationAsync () {}
348476}
0 commit comments