267 lines
8.9 KiB
Rust
267 lines
8.9 KiB
Rust
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<f32>,
|
|
input_buffer: Vec<f32>,
|
|
output_buffer: Vec<f32>,
|
|
sample_sink: rtrb::Producer<f32>,
|
|
}
|
|
|
|
const VB_FREQUENCY: usize = 41700;
|
|
|
|
impl CpalAudio {
|
|
fn init() -> Result<Self> {
|
|
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::<f32>(device, config, sampler),
|
|
SampleFormat::F64 => Self::new::<f64>(device, config, sampler),
|
|
SampleFormat::I32 => Self::new::<i32>(device, config, sampler),
|
|
SampleFormat::I16 => Self::new::<i16>(device, config, sampler),
|
|
_ => bail!("unsupported sample format"),
|
|
}
|
|
}
|
|
|
|
fn new_f32_stereo(
|
|
device: Device,
|
|
config: StreamConfig,
|
|
sampler: rubato::Async<f32>,
|
|
) -> Result<Self> {
|
|
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<S: SizedSample + FromSample<f32>>(
|
|
device: Device,
|
|
config: StreamConfig,
|
|
sampler: rubato::Async<f32>,
|
|
) -> Result<Self> {
|
|
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),
|
|
}
|
|
}
|
|
}
|