use base64::Engine as _;
use cpal::{
SampleFormat, SampleRate, StreamConfig, SupportedStreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use crossbeam_channel::{Receiver, Sender, bounded};
use gemini_live_api::{
GeminiLiveClientBuilder,
client::{ServerContentContext, UsageMetadataContext},
types::*,
};
use std::{
env,
sync::{Arc, Mutex as StdMutex},
time::Duration,
};
use tokio::sync::Notify;
use tracing::{debug, error, info, warn};
#[derive(Clone, Debug)]
struct ContinuousAudioAppState {
full_response_text: Arc<StdMutex<String>>,
model_turn_complete_signal: Arc<Notify>,
playback_sender: Arc<Sender<Vec<i16>>>,
is_microphone_active: Arc<StdMutex<bool>>,
}
const INPUT_SAMPLE_RATE_HZ: u32 = 16000;
const INPUT_CHANNELS_COUNT: u16 = 1;
const OUTPUT_SAMPLE_RATE_HZ: u32 = 24000;
const OUTPUT_CHANNELS_COUNT: u16 = 1;
async fn handle_on_content(ctx: ServerContentContext, app_state: Arc<ContinuousAudioAppState>) {
if let Some(model_turn) = &ctx.content.model_turn {
for part in &model_turn.parts {
if let Some(text) = &part.text {
let mut full_res = app_state.full_response_text.lock().unwrap();
*full_res += text;
*full_res += " ";
info!("[Handler] Model Text: {}", text.trim());
}
if let Some(blob) = &part.inline_data {
if blob.mime_type.starts_with("audio/") {
debug!(
"[Handler] Received audio blob. Mime: {}, Size: {} bytes",
blob.mime_type,
blob.data.len()
);
match base64::engine::general_purpose::STANDARD.decode(&blob.data) {
Ok(decoded_bytes) => {
if decoded_bytes.len() % 2 != 0 {
warn!("[Handler] Decoded audio data has odd number of bytes.");
continue;
}
let samples_i16 = decoded_bytes
.chunks_exact(2)
.map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<i16>>();
if let Err(e) = app_state.playback_sender.send(samples_i16) {
error!("[Handler] Failed to send audio for playback: {}", e);
}
}
Err(e) => error!("[Handler] Failed to decode base64 audio: {}", e),
}
}
}
}
}
if let Some(transcription) = &ctx.content.output_transcription {
info!(
"[Handler] Model Output Transcription: {}",
transcription.text
);
}
if ctx.content.turn_complete {
info!("[Handler] Model turn_complete message received.");
app_state.model_turn_complete_signal.notify_one();
}
if ctx.content.generation_complete {
info!("[Handler] Model generation_complete message received.");
}
if ctx.content.interrupted {
info!("[Handler] Model generation was interrupted (likely by barge-in).");
}
}
async fn handle_usage_metadata(
_ctx: UsageMetadataContext,
_app_state: Arc<ContinuousAudioAppState>,
) {
info!("[Handler] Usage Metadata: {:?}", _ctx.metadata);
}
fn find_supported_config_generic<F, I>(
mut configs_iterator_fn: F,
target_sample_rate: u32,
target_channels: u16,
) -> Result<SupportedStreamConfig, anyhow::Error>
where
F: FnMut() -> Result<I, cpal::SupportedStreamConfigsError>,
I: Iterator<Item = cpal::SupportedStreamConfigRange>,
{
let mut best_config: Option<SupportedStreamConfig> = None;
let mut min_rate_diff = u32::MAX;
for config_range in configs_iterator_fn()? {
if config_range.channels() != target_channels {
continue;
}
if config_range.sample_format() != SampleFormat::I16 {
continue;
}
let current_min_rate = config_range.min_sample_rate().0;
let current_max_rate = config_range.max_sample_rate().0;
let rate_to_check =
if target_sample_rate >= current_min_rate && target_sample_rate <= current_max_rate {
target_sample_rate
} else if target_sample_rate < current_min_rate {
current_min_rate
} else {
current_max_rate
};
let rate_diff = (rate_to_check as i32 - target_sample_rate as i32).abs() as u32;
if best_config.is_none() || rate_diff < min_rate_diff {
min_rate_diff = rate_diff;
best_config = Some(config_range.with_sample_rate(SampleRate(rate_to_check)));
}
if rate_diff == 0 {
break;
}
}
best_config.ok_or_else(|| {
anyhow::anyhow!(
"No i16 config for ~{}Hz {}ch",
target_sample_rate,
target_channels
)
})
}
fn setup_audio_input(
audio_chunk_sender: tokio::sync::mpsc::Sender<Vec<i16>>,
app_state: Arc<ContinuousAudioAppState>,
) -> Result<cpal::Stream, anyhow::Error> {
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or_else(|| anyhow::anyhow!("No input device"))?;
info!("[AudioInput] Using input: {}", device.name()?);
let supported_config = find_supported_config_generic(
|| device.supported_input_configs(),
INPUT_SAMPLE_RATE_HZ,
INPUT_CHANNELS_COUNT,
)?;
let config: StreamConfig = supported_config.into();
let stream = device.build_input_stream(
&config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
if !*app_state.is_microphone_active.lock().unwrap() {
return;
}
if data.is_empty() {
return;
}
match audio_chunk_sender.try_send(data.to_vec()) {
Ok(_) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
warn!("[AudioInput] Chunk channel full.")
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
error!("[AudioInput] Chunk channel closed.")
}
}
},
|err| error!("[AudioInput] CPAL Error: {}", err),
None,
)?;
stream.play()?;
info!("[AudioInput] Mic stream started with config: {:?}", config);
Ok(stream)
}
fn setup_audio_output(
playback_receiver: Receiver<Vec<i16>>,
) -> Result<cpal::Stream, anyhow::Error> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow::anyhow!("No output device"))?;
info!("[AudioOutput] Using output: {}", device.name()?);
let supported_config = find_supported_config_generic(
|| device.supported_output_configs(),
OUTPUT_SAMPLE_RATE_HZ,
OUTPUT_CHANNELS_COUNT,
)?;
let config: StreamConfig = supported_config.into();
let mut samples_buffer: Vec<i16> = Vec::new();
let stream = device.build_output_stream(
&config,
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
while samples_buffer.len() < data.len() {
if let Ok(new_samples) = playback_receiver.try_recv() {
samples_buffer.extend(new_samples);
} else {
break;
}
}
let len_to_write = std::cmp::min(data.len(), samples_buffer.len());
for i in 0..len_to_write {
data[i] = samples_buffer.remove(0);
}
for sample in data.iter_mut().skip(len_to_write) {
*sample = 0;
}
},
|err| error!("[AudioOutput] CPAL Error: {}", err),
None,
)?;
stream.play()?;
info!(
"[AudioOutput] Playback stream started with config: {:?}",
config
);
Ok(stream)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
dotenv::dotenv().ok();
let api_key = env::var("GEMINI_API_KEY").map_err(|_| "GEMINI_API_KEY not set")?;
let model_name =
env::var("GEMINI_MODEL").unwrap_or_else(|_| "models/gemini-2.0-flash-exp".to_string());
let (playback_tx, playback_rx) = bounded::<Vec<i16>>(100);
let (audio_input_chunk_tx, mut audio_input_chunk_rx) =
tokio::sync::mpsc::channel::<Vec<i16>>(20);
let app_state_instance = Arc::new(ContinuousAudioAppState {
full_response_text: Arc::new(StdMutex::new(String::new())),
model_turn_complete_signal: Arc::new(Notify::new()),
playback_sender: Arc::new(playback_tx),
is_microphone_active: Arc::new(StdMutex::new(false)),
});
let mut builder = GeminiLiveClientBuilder::<ContinuousAudioAppState>::new_with_state(
api_key,
model_name.clone(),
(*app_state_instance).clone(),
);
builder = builder.generation_config(GenerationConfig {
response_modalities: Some(vec![ResponseModality::Audio]),
temperature: Some(0.7),
speech_config: Some(SpeechConfig {
language_code: Some(SpeechLanguageCode::EnglishUS),
}),
..Default::default()
});
builder = builder.realtime_input_config(RealtimeInputConfig {
automatic_activity_detection: Some(AutomaticActivityDetection {
disabled: Some(false), start_of_speech_sensitivity: Some(StartSensitivity::StartSensitivityHigh),
prefix_padding_ms: Some(100),
end_of_speech_sensitivity: Some(EndSensitivity::EndSensitivityHigh),
silence_duration_ms: Some(1200), }),
activity_handling: Some(ActivityHandling::StartOfActivityInterrupts), turn_coverage: Some(TurnCoverage::TurnIncludesOnlyActivity),
});
builder = builder.output_audio_transcription(AudioTranscriptionConfig {});
builder = builder.system_instruction(Content {
parts: vec![Part {
text: Some("You are a helpful voice assistant.".to_string()),
..Default::default()
}],
role: Some(Role::System),
..Default::default()
});
builder = builder.on_server_content(handle_on_content);
builder = builder.on_usage_metadata(handle_usage_metadata);
info!("Connecting to Gemini model: {}", model_name);
let mut client = builder.connect().await?;
let client_outgoing_mpsc_sender = client
.get_outgoing_mpsc_sender_clone()
.ok_or_else(|| anyhow::anyhow!("Client MPSC sender unavailable"))?;
tokio::spawn(async move {
info!("[AudioProcessingTask] Started.");
while let Some(samples_vec) = audio_input_chunk_rx.recv().await {
if samples_vec.is_empty() {
continue;
}
let mut byte_data = Vec::with_capacity(samples_vec.len() * 2);
for sample in &samples_vec {
byte_data.extend_from_slice(&sample.to_le_bytes());
}
let encoded_data = base64::engine::general_purpose::STANDARD.encode(&byte_data);
let mime_type = format!("audio/pcm;rate={}", INPUT_SAMPLE_RATE_HZ);
let audio_blob = Blob {
mime_type,
data: encoded_data,
};
let client_message =
ClientMessagePayload::RealtimeInput(BidiGenerateContentRealtimeInput {
audio: Some(audio_blob),
..Default::default()
});
if let Err(e) = client_outgoing_mpsc_sender.send(client_message).await {
error!("[AudioProcessingTask] Failed to send audio: {:?}", e);
break;
}
}
info!("[AudioProcessingTask] Stopped.");
});
let _input_stream = setup_audio_input(audio_input_chunk_tx, app_state_instance.clone())?;
let _output_stream = setup_audio_output(playback_rx)?;
*app_state_instance.is_microphone_active.lock().unwrap() = true;
info!("Microphone is active. Continuous chat started. Press Ctrl+C to exit.");
info!("Speak to Gemini. It should respond, and you should be able to interrupt (barge-in).");
loop {
tokio::select! {
_ = app_state_instance.model_turn_complete_signal.notified() => {
info!("[MainLoop] Model completed its turn.");
}
_ = tokio::signal::ctrl_c() => {
info!("Ctrl+C received. Shutting down...");
break;
}
}
}
*app_state_instance.is_microphone_active.lock().unwrap() = false;
info!("Sending audioStreamEnd before closing...");
let audio_stream_end_payload = BidiGenerateContentRealtimeInput {
audio_stream_end: Some(true),
..Default::default()
};
if let Some(sender) = client.get_outgoing_mpsc_sender_clone() {
if sender
.send(ClientMessagePayload::RealtimeInput(
audio_stream_end_payload,
))
.await
.is_err()
{
warn!("Failed to send final audioStreamEnd, client might be already closing.");
}
} else {
warn!("Could not get client sender for final audioStreamEnd.");
}
tokio::time::sleep(Duration::from_millis(100)).await;
client.close().await?;
info!("Client closed. Exiting.");
Ok(())
}