use std::time::Duration; use anyhow::{Context, Result, bail}; use audioadapter_buffers::direct::InterleavedSlice; use cpal::{ Device, FromSample, Sample, SampleFormat, SizedSample, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, }; use itertools::Itertools; use rubato::{Adjustable as _, Resampler}; use tracing::warn; struct CpalAudio { #[allow(unused)] stream: cpal::Stream, sampler: rubato::Async, input_buffer: Vec, output_buffer: Vec, sample_sink: rtrb::Producer, } const VB_FREQUENCY: usize = 41700; impl CpalAudio { fn init() -> Result { let host = cpal::default_host(); let device = host .default_output_device() .context("no output device available")?; let default_rate = device.default_output_config()?.sample_rate(); let supported_config = device .supported_output_configs()? .filter(|c| { c.contains_rate(default_rate) && matches!( c.sample_format(), SampleFormat::F32 | SampleFormat::F64 | SampleFormat::I32 | SampleFormat::I16 ) }) .max_by(|a, b| a.cmp_default_heuristics(b)) .context("no suitable output config available")? .with_sample_rate(default_rate); let mut config = supported_config.config(); let resample_ratio = config.sample_rate as f64 / VB_FREQUENCY as f64; let chunk_size = (834.0 * resample_ratio) as usize; let sampler = rubato::Async::new_poly( resample_ratio, 64.0, rubato::PolynomialDegree::Cubic, chunk_size, 2, rubato::FixedAsync::Output, )?; config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); match supported_config.sample_format() { SampleFormat::F32 if config.channels == 2 => { Self::new_f32_stereo(device, config, sampler) } SampleFormat::F32 => Self::new::(device, config, sampler), SampleFormat::F64 => Self::new::(device, config, sampler), SampleFormat::I32 => Self::new::(device, config, sampler), SampleFormat::I16 => Self::new::(device, config, sampler), _ => bail!("unsupported sample format"), } } fn new_f32_stereo( device: Device, config: StreamConfig, sampler: rubato::Async, ) -> Result { let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max()); let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()]; let (sample_sink, mut sample_source) = rtrb::RingBuffer::new(sampler.output_frames_max() * 4); let stream = device.build_output_stream( config, move |data: &mut [f32], _| { let requested = data.len(); let chunk = match sample_source.read_chunk(data.len()) { Ok(c) => c, Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { sample_source.read_chunk(n).unwrap() } }; let len = chunk.len(); let (first, second) = chunk.as_slices(); data[0..first.len()].copy_from_slice(first); data[first.len()..len].copy_from_slice(second); for rest in &mut data[len..requested] { *rest = 0.0; } chunk.commit_all(); }, move |error| warn!(%error, "stream error"), None, )?; stream.play()?; Ok(Self { stream, sampler, input_buffer, output_buffer, sample_sink, }) } fn new>( device: Device, config: StreamConfig, sampler: rubato::Async, ) -> Result { let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max()); let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()]; let (sample_sink, mut sample_source) = rtrb::RingBuffer::new(sampler.output_frames_max() * 4); let channels = config.channels as usize; let stream = device.build_output_stream( config, move |data: &mut [S], _| { let requested = data.len() * 2 / channels; let chunk = match sample_source.read_chunk(requested) { Ok(c) => c, Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { sample_source.read_chunk(n).unwrap() } }; let (first, second) = chunk.as_slices(); let new_samples = first .iter() .chain(second) .map(|s: &f32| (*s).to_sample()) .chunks(2); let buffer = data.iter_mut().chunks(channels); for (dst, samples) in buffer.into_iter().zip(&new_samples) { // channels beyond the first two are zeroed let src = samples.chain(std::iter::repeat(S::EQUILIBRIUM)); for (d, s) in dst.zip(src) { *d = s; } } chunk.commit_all(); }, move |error| warn!(%error, "stream error"), None, )?; stream.play()?; Ok(Self { stream, sampler, input_buffer, output_buffer, sample_sink, }) } fn update(&mut self, mut samples: &[f32]) { while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 { let samples_needed = (self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len()); let (current_samples, future_samples) = samples.split_at(samples_needed); self.input_buffer.extend_from_slice(current_samples); samples = future_samples; let buffer_in = InterleavedSlice::new(&self.input_buffer, 2, self.sampler.input_frames_next()) .unwrap(); let mut buffer_out = InterleavedSlice::new_mut( &mut self.output_buffer, 2, self.sampler.output_frames_next(), ) .unwrap(); let (_, output_samples) = self .sampler .process_into_buffer(&buffer_in, &mut buffer_out, None) .unwrap(); let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) { Ok(c) => c, Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { self.sample_sink.write_chunk_uninit(n).unwrap() } }; chunk.fill_from_iter(self.output_buffer[..output_samples * 2].iter().copied()); self.input_buffer.clear(); } self.input_buffer.extend_from_slice(samples); while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 { std::thread::sleep(Duration::from_micros(500)); } } fn set_speed(&mut self, speed: f64) -> Result<()> { self.sampler .set_resample_ratio_relative(1.0 / speed, false)?; Ok(()) } } struct NoAudio { speed: f64, } impl NoAudio { fn new() -> Self { Self { speed: 1.0 } } fn update(&mut self, samples: &[f32]) { let samples = (samples.len() / 2) as f64; let elapsed = Duration::from_secs_f64(samples / (VB_FREQUENCY as f64 * self.speed)); std::thread::sleep(elapsed); } fn set_speed(&mut self, speed: f64) -> Result<()> { self.speed = speed; Ok(()) } } #[allow(clippy::large_enum_variant)] enum AudioInner { Cpal(CpalAudio), None(NoAudio), } pub struct Audio(AudioInner); impl Audio { pub fn init() -> Self { match CpalAudio::init() { Ok(a) => Self(AudioInner::Cpal(a)), Err(e) => { warn!("could not play audio: {e}"); Self(AudioInner::None(NoAudio::new())) } } } pub fn update(&mut self, samples: &[f32]) { match &mut self.0 { AudioInner::Cpal(a) => a.update(samples), AudioInner::None(a) => a.update(samples), } } pub fn set_speed(&mut self, speed: f64) -> Result<()> { match &mut self.0 { AudioInner::Cpal(a) => a.set_speed(speed), AudioInner::None(a) => a.set_speed(speed), } } }