2026-10-17 14:02:25 +02:00
|
|
|
/// Plays audio produced by a generator closure on the host's default output device.
|
|
|
|
|
///
|
|
|
|
|
/// `AudioPlayer` owns the live [`cpal::Stream`] and exposes only playback
|
|
|
|
|
/// control ([`play`](Self::play), [`pause`](Self::pause)). It has no
|
|
|
|
|
/// knowledge of how the generator produces samples, so polyphony, pitch
|
|
|
|
|
/// modulation, and voice mixing all belong behind the [`Generator`] closure,
|
|
|
|
|
/// not here.
|
|
|
|
|
pub struct AudioPlayer {
|
|
|
|
|
stream: Stream,
|
|
|
|
|
}
|
2026-10-18 12:21:06 +02:00
|
|
|
use crate::generators::GeneratorFn;
|
2026-10-17 14:02:25 +02:00
|
|
|
use cpal::{
|
|
|
|
|
Device, Error, ErrorKind, FromSample, I24, OutputCallbackInfo, Sample, SampleFormat, SizedSample, Stream, StreamConfig, SupportedStreamConfig, traits::{DeviceTrait, StreamTrait},
|
|
|
|
|
};
|
|
|
|
|
use std::io::ErrorKind as IoErrorKind;
|
|
|
|
|
|
|
|
|
|
impl AudioPlayer {
|
|
|
|
|
/// Builds a stream for `device`/`config` around `generator`, dispatching
|
|
|
|
|
/// on `config`'s sample format to the matching CPAL sample type.
|
|
|
|
|
///
|
|
|
|
|
/// The stream starts paused; call [`play`](Self::play) to produce sound.
|
|
|
|
|
/// Use [`get_device_and_config`](crate::get_device_and_config) to obtain
|
|
|
|
|
/// `device` and `config`.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if:
|
|
|
|
|
/// - the device reports a sample format `raust` does not support, or
|
|
|
|
|
/// - CPAL fails to build the output stream.
|
2026-10-18 12:21:06 +02:00
|
|
|
pub fn try_new(device: &Device, config: SupportedStreamConfig, generator: GeneratorFn) -> anyhow::Result<Self> {
|
2026-10-17 14:02:25 +02:00
|
|
|
let stream = match config.sample_format() {
|
|
|
|
|
SampleFormat::I8 => Self::create_stream::<i8>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::I16 => Self::create_stream::<i16>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::I24 => Self::create_stream::<I24>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::I32 => Self::create_stream::<i32>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::I64 => Self::create_stream::<i64>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::U8 => Self::create_stream::<u8>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::U16 => Self::create_stream::<u16>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::U32 => Self::create_stream::<u32>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::U64 => Self::create_stream::<u64>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::F32 => Self::create_stream::<f32>(device, config.into(), generator),
|
|
|
|
|
SampleFormat::F64 => Self::create_stream::<f64>(device, config.into(), generator),
|
|
|
|
|
sample_format => Err(anyhow::Error::new(std::io::Error::new(
|
|
|
|
|
IoErrorKind::InvalidData,
|
|
|
|
|
format!("Unsupported sample format '{sample_format}'"),
|
|
|
|
|
))),
|
|
|
|
|
}?;
|
|
|
|
|
Ok(Self { stream })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds, but does not start, an output stream of sample type `T` that
|
|
|
|
|
/// pulls its samples from `generator`.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if CPAL fails to build the output stream for `device`.
|
|
|
|
|
pub fn create_stream<T>(
|
|
|
|
|
device: &Device,
|
|
|
|
|
config: StreamConfig,
|
2026-10-18 12:21:06 +02:00
|
|
|
mut generator: GeneratorFn,
|
2026-10-17 14:02:25 +02:00
|
|
|
) -> Result<Stream, anyhow::Error>
|
|
|
|
|
where
|
|
|
|
|
T: SizedSample + FromSample<f32>,
|
|
|
|
|
{
|
|
|
|
|
let channels = usize::from(config.channels);
|
|
|
|
|
let err_fn = |err: Error| match err.kind() {
|
|
|
|
|
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
|
|
|
|
|
eprintln!("{err}");
|
|
|
|
|
}
|
|
|
|
|
_ => eprintln!("Stream error: {err}"),
|
|
|
|
|
};
|
|
|
|
|
let stream = device.build_output_stream(
|
|
|
|
|
config,
|
|
|
|
|
move |data: &mut [T], _: &OutputCallbackInfo| {
|
|
|
|
|
Self::write_data(data, channels, &mut generator);
|
|
|
|
|
},
|
|
|
|
|
err_fn,
|
|
|
|
|
None,
|
|
|
|
|
)?;
|
|
|
|
|
Ok(stream)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
|
|
|
|
|
where
|
|
|
|
|
T: Sample + FromSample<f32>,
|
|
|
|
|
{
|
|
|
|
|
for frame in output.chunks_mut(channels) {
|
|
|
|
|
let value: T = T::from_sample(next_sample());
|
|
|
|
|
for sample in frame.iter_mut() {
|
|
|
|
|
*sample = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resumes playback.
|
|
|
|
|
///
|
|
|
|
|
/// The generator continues from exactly where [`pause`](Self::pause)
|
|
|
|
|
/// left it; nothing resets.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if CPAL fails to resume the stream.
|
|
|
|
|
pub fn play(&self) -> anyhow::Result<()> {
|
|
|
|
|
self.stream.play()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Suspends playback.
|
|
|
|
|
///
|
|
|
|
|
/// The backend stops calling the generator until the next
|
|
|
|
|
/// [`play`](Self::play) call, so no samples are produced while paused.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if CPAL fails to suspend the stream.
|
|
|
|
|
pub fn pause(&self) -> anyhow::Result<()> {
|
|
|
|
|
self.stream.pause()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|