#[cfg(feature = "audio")]
pub mod audio_io {
use std::sync::mpsc::{self, Sender};
use std::thread;
pub fn play_to_speaker(data: &[u8]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use rodio::{Decoder, OutputStream, Sink};
let owned_data = data.to_vec();
let (_stream, stream_handle) = OutputStream::try_default()?;
let source = Decoder::new(std::io::Cursor::new(owned_data))?;
let sink = Sink::try_new(&stream_handle)?;
sink.append(source);
sink.sleep_until_end();
Ok(())
}
pub struct StreamingPlayer {
sender: Option<Sender<Vec<u8>>>,
handle: Option<thread::JoinHandle<()>>,
}
impl StreamingPlayer {
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use rodio::{Decoder, OutputStream, Sink};
let (tx, rx) = mpsc::channel::<Vec<u8>>();
let handle = thread::spawn(move || {
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&stream_handle).unwrap();
let mut buffer = Vec::new();
let min_chunk_size = 4096;
loop {
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
Ok(chunk) => {
buffer.extend_from_slice(&chunk);
if buffer.len() >= min_chunk_size {
let playback_data = buffer.clone();
if let Ok(source) =
Decoder::new(std::io::Cursor::new(playback_data))
{
sink.append(source);
}
let keep = buffer.len() / 2;
if keep > 0 && keep < buffer.len() {
buffer.drain(..keep);
}
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
if !buffer.is_empty() {
let playback_data = buffer.clone();
if let Ok(source) =
Decoder::new(std::io::Cursor::new(playback_data))
{
sink.append(source);
}
buffer.clear();
}
if sink.empty() && rx.try_recv().is_err() {
break;
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
if !buffer.is_empty() {
let playback_data = buffer.clone();
if let Ok(source) =
Decoder::new(std::io::Cursor::new(playback_data))
{
sink.append(source);
}
}
break;
}
}
}
sink.sleep_until_end();
});
Ok(Self {
sender: Some(tx),
handle: Some(handle),
})
}
pub fn send_chunk(
&self,
chunk: &[u8],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(ref sender) = self.sender {
sender.send(chunk.to_vec())?;
}
Ok(())
}
pub fn finish(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
drop(self.sender);
if let Some(handle) = self.handle {
handle
.join()
.map_err(|e| format!("Thread panicked: {:?}", e))?;
}
Ok(())
}
}
pub fn record_from_microphone(
duration_secs: f32,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::SampleFormat;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or("No input device available")?;
let config = device
.default_input_config()
.map_err(|e| format!("Failed to get default input config: {}", e))?;
let sample_rate = config.sample_rate().0 as f32;
let channels = config.channels() as usize;
let samples = (duration_secs * sample_rate * channels as f32) as usize;
let buffer = Arc::new(std::sync::Mutex::new(Vec::with_capacity(samples * 2)));
let buffer_clone = buffer.clone();
let recording = Arc::new(AtomicBool::new(true));
let recording_clone = recording.clone();
let stream = match config.sample_format() {
SampleFormat::I16 => {
let buf = buffer_clone;
let rec = recording_clone;
device.build_input_stream(
&config.into(),
move |data: &[i16], _: &cpal::InputCallbackInfo| {
if rec.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
for &sample in data {
buf.push(sample as u8);
buf.push((sample >> 8) as u8);
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
SampleFormat::F32 => {
let buf = buffer_clone;
let rec = recording_clone;
device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if rec.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
for &sample in data {
let sample_i16 = (sample * i16::MAX as f32) as i16;
buf.push(sample_i16 as u8);
buf.push((sample_i16 >> 8) as u8);
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
_ => return Err("Unsupported sample format".into()),
};
stream.play()?;
std::thread::sleep(std::time::Duration::from_secs_f32(duration_secs));
recording.store(false, Ordering::Relaxed);
drop(stream);
let buffer = buffer.lock().unwrap();
Ok(buffer.clone())
}
#[allow(dead_code)]
pub fn stream_from_microphone(
_chunk_duration_secs: f32,
_callback: impl FnMut(&[u8]) + Send + 'static,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("stream_from_microphone not yet implemented - use record_from_microphone".into())
}
#[allow(dead_code)]
pub fn record_with_vad(
max_duration_secs: f32,
silence_threshold: f32,
silence_duration_secs: f32,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::SampleFormat;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or("No input device available")?;
let config = device
.default_input_config()
.map_err(|e| format!("Failed to get default input config: {}", e))?;
let sample_rate = config.sample_rate().0 as f32;
let channels = config.channels() as usize;
let max_samples = (max_duration_secs * sample_rate * channels as f32) as usize;
let buffer = Arc::new(std::sync::Mutex::new(Vec::with_capacity(max_samples * 2)));
let buffer_clone = buffer.clone();
let recording = Arc::new(AtomicBool::new(true));
let recording_clone = recording.clone();
let silence_start = Arc::new(std::sync::Mutex::new(Option::<Instant>::None));
let silence_start_clone = silence_start.clone();
let chunk_sample_count = (sample_rate * 0.1) as usize; let _silence_samples_needed =
(silence_duration_secs * sample_rate * channels as f32) as usize;
let stream = match config.sample_format() {
SampleFormat::I16 => {
let buf = buffer_clone;
let rec = recording_clone;
let silence = silence_start_clone;
device.build_input_stream(
&config.into(),
move |data: &[i16], _: &cpal::InputCallbackInfo| {
if rec.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
let sum: i64 = data.iter().map(|&s| (s as i64) * (s as i64)).sum();
let rms = (sum as f64 / data.len() as f64).sqrt();
let normalized = (rms / i16::MAX as f64).min(1.0) as f32;
for &sample in data {
buf.push(sample as u8);
buf.push((sample >> 8) as u8);
}
let mut silence_guard = silence.lock().unwrap();
if normalized > silence_threshold {
*silence_guard = None; } else if buf.len() > chunk_sample_count * 2 {
if silence_guard.is_none() {
*silence_guard = Some(Instant::now());
} else if let Some(start) = *silence_guard {
let elapsed = start.elapsed().as_secs_f32();
if elapsed >= silence_duration_secs {
rec.store(false, Ordering::Relaxed);
}
}
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
SampleFormat::F32 => {
let buf = buffer_clone;
let rec = recording_clone;
let silence = silence_start_clone;
device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if rec.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
let sum: f64 = data.iter().map(|&s| (s as f64) * (s as f64)).sum();
let rms = (sum / data.len() as f64).sqrt();
let normalized = rms.min(1.0) as f32;
for &sample in data {
let sample_i16 = (sample * i16::MAX as f32) as i16;
buf.push(sample_i16 as u8);
buf.push((sample_i16 >> 8) as u8);
}
let mut silence_guard = silence.lock().unwrap();
if normalized > silence_threshold {
*silence_guard = None;
} else if buf.len() > chunk_sample_count * 2 {
if silence_guard.is_none() {
*silence_guard = Some(Instant::now());
} else if let Some(start) = *silence_guard {
let elapsed = start.elapsed().as_secs_f32();
if elapsed >= silence_duration_secs {
rec.store(false, Ordering::Relaxed);
}
}
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
_ => return Err("Unsupported sample format".into()),
};
stream.play()?;
while recording.load(Ordering::Relaxed) {
std::thread::sleep(std::time::Duration::from_millis(100));
if buffer.lock().unwrap().len() >= max_samples * 2 {
recording.store(false, Ordering::Relaxed);
}
}
drop(stream);
let buffer = buffer.lock().unwrap();
Ok(buffer.clone())
}
#[allow(dead_code)]
pub fn stream_from_microphone_with_vad(
chunk_duration_secs: f32,
mut callback: impl FnMut(&[u8]) + Send + 'static,
use_vad: bool,
silence_threshold: f32,
silence_duration_secs: f32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::SampleFormat;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
let host = cpal::default_host();
let device = host
.default_input_device()
.ok_or("No input device available")?;
let config = device
.default_input_config()
.map_err(|e| format!("Failed to get default input config: {}", e))?;
let sample_rate = config.sample_rate().0 as f32;
let channels = config.channels() as usize;
let chunk_size = (chunk_duration_secs * sample_rate * channels as f32) as usize * 2;
let chunk_buffer = Arc::new(std::sync::Mutex::new(Vec::with_capacity(chunk_size)));
let chunk_buffer_clone = chunk_buffer.clone();
let streaming = Arc::new(AtomicBool::new(true));
let streaming_clone = streaming.clone();
let silence_start = Arc::new(std::sync::Mutex::new(None::<Instant>));
let silence_start_clone = silence_start.clone();
let stream = match config.sample_format() {
SampleFormat::I16 => {
let buf = chunk_buffer_clone;
let stream_flag = streaming_clone;
let silence = silence_start_clone;
device.build_input_stream(
&config.into(),
move |data: &[i16], _: &cpal::InputCallbackInfo| {
if stream_flag.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
let sum: i64 = data.iter().map(|&s| (s as i64) * (s as i64)).sum();
let rms = (sum as f64 / data.len() as f64).sqrt();
let normalized = (rms / i16::MAX as f64).min(1.0) as f32;
for &sample in data {
buf.push(sample as u8);
buf.push((sample >> 8) as u8);
}
if use_vad {
let mut silence_guard = silence.lock().unwrap();
if normalized > silence_threshold {
*silence_guard = None;
} else if buf.len() >= chunk_size {
if silence_guard.is_none() {
*silence_guard = Some(Instant::now());
} else if let Some(start) = *silence_guard {
if start.elapsed().as_secs_f32()
>= silence_duration_secs
{
stream_flag.store(false, Ordering::Relaxed);
}
}
}
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
SampleFormat::F32 => {
let buf = chunk_buffer_clone;
let stream_flag = streaming_clone;
let silence = silence_start_clone;
device.build_input_stream(
&config.into(),
move |data: &[f32], _: &cpal::InputCallbackInfo| {
if stream_flag.load(Ordering::Relaxed) {
if let Ok(mut buf) = buf.lock() {
let sum: f64 = data.iter().map(|&s| (s as f64) * (s as f64)).sum();
let rms = (sum / data.len() as f64).sqrt();
let normalized = rms.min(1.0) as f32;
for &sample in data {
let sample_i16 = (sample * i16::MAX as f32) as i16;
buf.push(sample_i16 as u8);
buf.push((sample_i16 >> 8) as u8);
}
if use_vad {
let mut silence_guard = silence.lock().unwrap();
if normalized > silence_threshold {
*silence_guard = None;
} else if buf.len() >= chunk_size {
if silence_guard.is_none() {
*silence_guard = Some(Instant::now());
} else if let Some(start) = *silence_guard {
if start.elapsed().as_secs_f32()
>= silence_duration_secs
{
stream_flag.store(false, Ordering::Relaxed);
}
}
}
}
}
}
},
|err| eprintln!("Error in audio stream: {}", err),
None,
)?
}
_ => return Err("Unsupported sample format".into()),
};
stream.play()?;
let chunk_interval = std::time::Duration::from_secs_f32(chunk_duration_secs);
while streaming.load(Ordering::Relaxed) {
std::thread::sleep(chunk_interval);
let mut chunk = chunk_buffer.lock().unwrap();
if !chunk.is_empty() {
callback(&chunk);
chunk.clear();
}
}
{
let chunk = chunk_buffer.lock().unwrap();
if !chunk.is_empty() {
callback(&chunk);
}
}
drop(stream);
Ok(())
}
#[allow(dead_code)]
pub fn list_input_devices() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait};
let host = cpal::default_host();
let mut devices = Vec::new();
for device in host.input_devices()? {
if let Ok(name) = device.name() {
devices.push(name);
}
}
Ok(devices)
}
#[allow(dead_code)]
pub fn list_output_devices() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait};
let host = cpal::default_host();
let mut devices = Vec::new();
for device in host.output_devices()? {
if let Ok(name) = device.name() {
devices.push(name);
}
}
Ok(devices)
}
#[allow(dead_code)]
pub fn get_input_device(
name: &str,
) -> Result<Option<cpal::Device>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait};
let host = cpal::default_host();
for device in host.input_devices()? {
if let Ok(device_name) = device.name() {
if device_name.to_lowercase().contains(&name.to_lowercase()) {
return Ok(Some(device));
}
}
}
Ok(None)
}
#[allow(dead_code)]
pub fn get_output_device(
name: &str,
) -> Result<Option<cpal::Device>, Box<dyn std::error::Error + Send + Sync>> {
use cpal::traits::{DeviceTrait, HostTrait};
let host = cpal::default_host();
for device in host.output_devices()? {
if let Ok(device_name) = device.name() {
if device_name.to_lowercase().contains(&name.to_lowercase()) {
return Ok(Some(device));
}
}
}
Ok(None)
}
}
#[cfg(not(feature = "audio"))]
pub mod audio_io {
pub fn play_to_speaker(_data: &[u8]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub struct StreamingPlayer;
#[allow(dead_code)]
impl StreamingPlayer {
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
pub fn send_chunk(
&self,
_chunk: &[u8],
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled".into())
}
pub fn finish(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled".into())
}
}
pub fn record_from_microphone(
_duration_secs: f32,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
pub fn stream_from_microphone(
_chunk_duration_secs: f32,
_callback: impl FnMut(&[u8]) + Send + 'static,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn record_with_vad(
_max_duration_secs: f32,
_silence_threshold: f32,
_silence_duration_secs: f32,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn stream_from_microphone_with_vad(
_chunk_duration_secs: f32,
_callback: impl FnMut(&[u8]) + Send + 'static,
_use_vad: bool,
_silence_threshold: f32,
_silence_duration_secs: f32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn list_input_devices() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn list_output_devices() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn get_input_device(
_name: &str,
) -> Result<Option<cpal::Device>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
#[allow(dead_code)]
pub fn get_output_device(
_name: &str,
) -> Result<Option<cpal::Device>, Box<dyn std::error::Error + Send + Sync>> {
Err("Audio feature not enabled. Rebuild with --features audio".into())
}
}