@@ -94,6 +94,8 @@ public final class LiveAudioConversationAsync {
9494 private static SourceDataLine speakerLine ;
9595 private static AsyncSession session ;
9696 private static ExecutorService micExecutor = Executors .newSingleThreadExecutor ();
97+ private static String promptString = null ;
98+ private static java .io .ByteArrayOutputStream audioResponseBytes = new java .io .ByteArrayOutputStream ();
9799
98100 /** Creates the parameters for sending an audio chunk. */
99101 public static LiveSendRealtimeInputParameters createAudioContent (byte [] audioData ) {
@@ -155,31 +157,95 @@ public static void main(String[] args) throws LineUnavailableException {
155157 System .out .println ("Using Gemini Developer API" );
156158 }
157159
160+ String getModelFromArgs = null ;
161+ String voiceSamplePath = null ;
162+ String voiceConsentPath = null ;
163+ String voiceSignature = null ;
164+ promptString = null ;
165+
166+ for (String arg : args ) {
167+ if (arg .startsWith ("--model=" )) {
168+ getModelFromArgs = arg .substring ("--model=" .length ());
169+ } else if (arg .startsWith ("--voice-sample=" )) {
170+ voiceSamplePath = arg .substring ("--voice-sample=" .length ());
171+ } else if (arg .startsWith ("--voice-consent=" )) {
172+ voiceConsentPath = arg .substring ("--voice-consent=" .length ());
173+ } else if (arg .startsWith ("--voice-signature=" )) {
174+ voiceSignature = arg .substring ("--voice-signature=" .length ());
175+ } else if (arg .startsWith ("--prompt=" )) {
176+ promptString = arg .substring ("--prompt=" .length ());
177+ } else if (!arg .startsWith ("--" ) && getModelFromArgs == null ) {
178+ getModelFromArgs = arg ;
179+ }
180+ }
181+
182+ byte [] voiceSampleAudio = null ;
183+ byte [] consentAudio = null ;
184+
185+ if (voiceSamplePath != null ) {
186+ try {
187+ voiceSampleAudio = java .nio .file .Files .readAllBytes (java .nio .file .Paths .get (voiceSamplePath ));
188+ } catch (java .io .IOException e ) {
189+ throw new RuntimeException ("Failed to read voice sample: " + e .getMessage ());
190+ }
191+ if (voiceConsentPath == null && voiceSignature == null ) {
192+ throw new IllegalArgumentException (
193+ "Either --voice-consent or --voice-signature must be provided when --voice-sample is"
194+ + " used." );
195+ }
196+ }
197+ if (voiceConsentPath != null ) {
198+ try {
199+ consentAudio = java .nio .file .Files .readAllBytes (java .nio .file .Paths .get (voiceConsentPath ));
200+ } catch (java .io .IOException e ) {
201+ throw new RuntimeException ("Failed to read voice consent: " + e .getMessage ());
202+ }
203+ }
204+
158205 final String modelId ;
159- if (args . length != 0 ) {
160- modelId = args [ 0 ] ;
206+ if (getModelFromArgs != null ) {
207+ modelId = getModelFromArgs ;
161208 } else if (client .vertexAI ()) {
162209 modelId = Constants .GEMINI_LIVE_MODEL_NAME ;
163210 } else {
164211 modelId = Constants .GEMINI_LIVE_MODEL_NAME_PREVIEW ;
165212 }
166213
167214 // --- Audio Line Setup ---
168- microphoneLine = getMicrophoneLine ();
169- speakerLine = getSpeakerLine ();
215+ if (promptString == null ) {
216+ microphoneLine = getMicrophoneLine ();
217+ speakerLine = getSpeakerLine ();
218+ }
170219
171220 // --- Live API Config for Audio ---
172221 // Choice of ["Aoede", "Puck", "Charon", "Kore", "Fenrir", "Leda", "Orus", "Zephyr"]
173222 String voiceName = "Aoede" ;
223+
224+ VoiceConfig .Builder voiceConfigBuilder = VoiceConfig .builder ();
225+ if (voiceSampleAudio != null ) {
226+ com .google .genai .types .ReplicatedVoiceConfig .Builder repBuilder =
227+ com .google .genai .types .ReplicatedVoiceConfig .builder ()
228+ .mimeType ("audio/wav" )
229+ .voiceSampleAudio (voiceSampleAudio );
230+ if (consentAudio != null ) {
231+ repBuilder .consentAudio (consentAudio );
232+ }
233+ if (voiceSignature != null ) {
234+ repBuilder .voiceConsentSignature (
235+ com .google .genai .types .VoiceConsentSignature .builder ().signature (voiceSignature ));
236+ }
237+ voiceConfigBuilder .replicatedVoiceConfig (repBuilder );
238+ } else {
239+ voiceConfigBuilder .prebuiltVoiceConfig (
240+ PrebuiltVoiceConfig .builder ().voiceName (voiceName ));
241+ }
242+
174243 LiveConnectConfig config =
175244 LiveConnectConfig .builder ()
176245 .responseModalities (Modality .Known .AUDIO )
177246 .speechConfig (
178247 SpeechConfig .builder ()
179- .voiceConfig (
180- VoiceConfig .builder ()
181- .prebuiltVoiceConfig (
182- PrebuiltVoiceConfig .builder ().voiceName (voiceName )))
248+ .voiceConfig (voiceConfigBuilder )
183249 .languageCode ("en-US" ))
184250 .realtimeInputConfig (
185251 RealtimeInputConfig .builder ()
@@ -232,19 +298,33 @@ public static void main(String[] args) throws LineUnavailableException {
232298 session = client .async .live .connect (modelId , config ).get ();
233299 System .out .println ("Connected." );
234300
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)..." );
301+ if (session .setupComplete () != null && session .setupComplete ().voiceConsentSignature ().isPresent ()) {
302+ System .out .println (
303+ "\n === Voice Consent Signature Received ===\n "
304+ + session .setupComplete ().voiceConsentSignature ().get ().signature ().orElse ("" )
305+ + "\n ========================================\n " );
306+ }
239307
240308 // --- Start Receiving Audio Responses ---
241309 CompletableFuture <Void > receiveFuture =
242310 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 );
311+ System .err .println ("Receive stream started." );
312+
313+ CompletableFuture <Void > sendFuture ;
314+ if (promptString == null ) {
315+ // --- Start Audio Lines ---
316+ microphoneLine .start ();
317+ speakerLine .start ();
318+ System .out .println ("Microphone and speakers started. Speak now (Press Ctrl+C to exit)..." );
319+
320+ // --- Start Sending Microphone Audio ---
321+ sendFuture = CompletableFuture .runAsync (LiveAudioConversationAsync ::sendMicrophoneAudio , micExecutor );
322+ } else {
323+ System .out .println ("Sending prompt: " + promptString );
324+ session .sendRealtimeInput (
325+ LiveSendRealtimeInputParameters .builder ().text (promptString ).build ()).get ();
326+ sendFuture = CompletableFuture .completedFuture (null );
327+ }
248328
249329 // Keep the main thread alive. Wait for sending or receiving to finish (or
250330 // error).
@@ -313,13 +393,19 @@ public static void handleAudioResponse(LiveServerMessage message) {
313393 content -> {
314394 // Handle interruptions from Gemini.
315395 if (content .interrupted ().orElse (false )) {
316- speakerLine .flush ();
396+ if (speakerLine != null && speakerLine .isOpen ()) {
397+ speakerLine .flush ();
398+ }
317399 return ; // Skip processing the rest of this message's audio.
318400 }
319401
320402 // Handle Model turn completion.
321403 if (content .turnComplete ().orElse (false )) {
322- // The turn is over, no more audio will be sent for this turn.
404+ if (promptString != null ) {
405+ saveWavFile ();
406+ System .out .println ("Response received, exiting." );
407+ System .exit (0 );
408+ }
323409 return ;
324410 }
325411
@@ -334,15 +420,45 @@ public static void handleAudioResponse(LiveServerMessage message) {
334420 if (speakerLine != null && speakerLine .isOpen ()) {
335421 // Write audio data to the speaker
336422 speakerLine .write (audioBytes , 0 , audioBytes .length );
423+ } else {
424+ System .out .println (
425+ "Received audio response chunk: " + audioBytes .length + " bytes." );
426+ }
427+ try {
428+ audioResponseBytes .write (audioBytes );
429+ } catch (java .io .IOException e ) {
430+ System .err .println ("Failed to accumulate audio bytes: " + e .getMessage ());
337431 }
338432 });
339433
340434 // If this is the last message of a generation, drain the buffer.
341435 if (content .generationComplete ().orElse (false )) {
342- speakerLine .drain ();
436+ if (speakerLine != null && speakerLine .isOpen ()) {
437+ speakerLine .drain ();
438+ }
343439 }
344440 });
345441 }
346442
443+ private static void saveWavFile () {
444+ byte [] audioData = audioResponseBytes .toByteArray ();
445+ if (audioData .length == 0 ) {
446+ System .out .println ("No audio data received to save." );
447+ return ;
448+ }
449+ try {
450+ javax .sound .sampled .AudioInputStream ais = new javax .sound .sampled .AudioInputStream (
451+ new java .io .ByteArrayInputStream (audioData ),
452+ SPEAKER_AUDIO_FORMAT ,
453+ audioData .length / SPEAKER_AUDIO_FORMAT .getFrameSize ()
454+ );
455+ java .io .File outputFile = new java .io .File ("output.wav" );
456+ javax .sound .sampled .AudioSystem .write (ais , javax .sound .sampled .AudioFileFormat .Type .WAVE , outputFile );
457+ System .out .println ("Saved audio response to " + outputFile .getAbsolutePath ());
458+ } catch (Exception e ) {
459+ System .err .println ("Failed to save WAV file: " + e .getMessage ());
460+ }
461+ }
462+
347463 private LiveAudioConversationAsync () {}
348464}
0 commit comments