use std::collections::HashMap;
use alpha2_audio::{AudioGraph, ChannelLayout, ProcessCtx, TransportState};
#[cfg(feature = "timeline")]
use alpha2_engine::{AudioRenderer, EngineProject};
use alpha2_media::{
AudioBlock, AudioBlockProvider, AudioBlockSink, AudioSpan, CancelToken, EncodeError, SourceId,
};
use alpha2_time::{MediaTime, TICKS_PER_SECOND};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OfflineAudioError {
InvalidInput,
Sink(EncodeError),
#[cfg(feature = "timeline")]
Engine(alpha2_engine::EngineError),
MissingSources(Vec<SourceId>),
}
impl std::fmt::Display for OfflineAudioError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidInput => formatter.write_str("offline audio input is invalid"),
Self::Sink(error) => write!(formatter, "offline audio sink failed: {error}"),
#[cfg(feature = "timeline")]
Self::Engine(error) => write!(formatter, "offline audio rendering failed: {error}"),
Self::MissingSources(sources) => {
write!(
formatter,
"offline audio is missing {} sources",
sources.len()
)
}
}
}
}
impl std::error::Error for OfflineAudioError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Sink(error) => Some(error),
#[cfg(feature = "timeline")]
Self::Engine(error) => Some(error),
_ => None,
}
}
}
#[derive(Default)]
pub struct DecodedAudioSources {
sources: HashMap<SourceId, AudioBlock>,
}
impl DecodedAudioSources {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, source: SourceId, block: AudioBlock) {
self.sources.insert(source, block);
}
pub fn remove(&mut self, source: &SourceId) -> bool {
self.sources.remove(source).is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.sources.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sources.is_empty()
}
}
impl AudioBlockProvider for DecodedAudioSources {
fn block(
&self,
source: &SourceId,
_start_sample: i64,
_frames: usize,
) -> Option<AudioSpan<'_>> {
self.sources.get(source).map(AudioSpan::whole)
}
}
#[cfg(feature = "timeline")]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ProjectAudioRequest<'a> {
pub channels: usize,
pub start_sample: i64,
pub total_frames: usize,
pub block_sizes: &'a [usize],
}
#[cfg(feature = "timeline")]
pub fn render_project_audio(
snapshot: &EngineProject,
request: &ProjectAudioRequest<'_>,
sources: &dyn AudioBlockProvider,
sink: &mut dyn AudioBlockSink,
cancel: &CancelToken,
) -> Result<(), OfflineAudioError> {
let ProjectAudioRequest {
channels,
start_sample,
total_frames,
block_sizes,
} = *request;
if total_frames == 0 || block_sizes.is_empty() || block_sizes.contains(&0) || start_sample < 0 {
return Err(OfflineAudioError::InvalidInput);
}
let max_block = block_sizes.iter().copied().max().unwrap_or(1);
let mut renderer =
AudioRenderer::new(snapshot, channels, max_block).map_err(OfflineAudioError::Engine)?;
let mut produced = 0;
let mut sequence = 0;
let mut missing: Vec<SourceId> = Vec::new();
while produced < total_frames {
if cancel.is_cancelled() {
sink.abort_audio().map_err(OfflineAudioError::Sink)?;
return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
}
let count = block_sizes[sequence % block_sizes.len()].min(total_frames - produced);
let offset = i64::try_from(produced).unwrap_or(i64::MAX);
let status = renderer
.render(
snapshot,
start_sample.saturating_add(offset),
count,
sources,
TransportState::Playing,
)
.map_err(OfflineAudioError::Engine)?;
for source in &status.missing_sources {
if !missing.contains(source) {
missing.push(source.clone());
}
}
let mut block = renderer.block(&status);
block.start_sample = offset;
sink.write_audio(&block, cancel)
.map_err(OfflineAudioError::Sink)?;
produced += count;
sequence += 1;
}
if !missing.is_empty() {
sink.abort_audio().map_err(OfflineAudioError::Sink)?;
return Err(OfflineAudioError::MissingSources(missing));
}
sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum HostSampleFormat {
F32,
I16,
Unsupported,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct HostAudioConfig {
pub channels: u16,
pub min_sample_rate: u32,
pub max_sample_rate: u32,
pub format: HostSampleFormat,
}
#[must_use]
pub fn negotiate_output_config(
configs: &[HostAudioConfig],
project_rate: u32,
) -> Option<HostAudioConfig> {
configs
.iter()
.copied()
.filter(|config| {
config.format == HostSampleFormat::F32
&& config.channels > 0
&& (config.min_sample_rate..=config.max_sample_rate).contains(&project_rate)
})
.min_by_key(|config| match config.channels {
2 => (0, 0),
1 => (2, 0),
more => (1, more),
})
}
pub fn render_offline(
graph: &mut AudioGraph,
sample_rate: u32,
inputs: &[Vec<f32>],
block_sizes: &[usize],
sink: &mut dyn AudioBlockSink,
cancel: &CancelToken,
) -> Result<(), OfflineAudioError> {
let layout = graph.layout();
let frames = inputs.first().map_or(0, Vec::len);
if sample_rate == 0
|| inputs.is_empty()
|| graph.buses().checked_mul(layout.channels()) != Some(inputs.len())
|| inputs.iter().any(|channel| channel.len() != frames)
|| block_sizes.is_empty()
|| block_sizes.contains(&0)
{
return Err(OfflineAudioError::InvalidInput);
}
let max_block = block_sizes.iter().copied().max().unwrap_or(1);
graph.prepare(sample_rate, max_block);
let mut output: Vec<Vec<f32>> = (0..layout.channels())
.map(|_| vec![0.0; max_block])
.collect();
let mut offset = 0;
let mut sequence = 0;
while offset < frames {
if cancel.is_cancelled() {
sink.abort_audio().map_err(OfflineAudioError::Sink)?;
return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
}
let count = block_sizes[sequence % block_sizes.len()].min(frames - offset);
let refs: Vec<&[f32]> = inputs
.iter()
.map(|channel| &channel[offset..offset + count])
.collect();
let mut outputs: Vec<&mut [f32]> = output
.iter_mut()
.map(|channel| &mut channel[..count])
.collect();
let sample_position = i64::try_from(offset).unwrap_or(i64::MAX);
graph.process(
&ProcessCtx {
position: MediaTime::from_ticks(
sample_position
.saturating_mul(TICKS_PER_SECOND)
.div_euclid(i64::from(sample_rate)),
),
sample_position,
sample_rate,
transport: TransportState::Playing,
},
&refs,
&mut outputs,
);
drop(outputs);
sink.write_audio(
&AudioBlock {
start_sample: sample_position,
sample_rate,
speakers: match layout {
ChannelLayout::Mono => alpha2_media::SpeakerLayout::MONO,
ChannelLayout::Stereo => alpha2_media::SpeakerLayout::STEREO,
},
channels: output
.iter()
.map(|channel| channel[..count].to_vec())
.collect(),
},
cancel,
)
.map_err(OfflineAudioError::Sink)?;
offset += count;
sequence += 1;
}
sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
}
#[cfg(feature = "cpal-host")]
mod realtime {
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, Stream, StreamConfig};
use super::{
HostAudioConfig, HostSampleFormat, MediaTime, ProcessCtx, TICKS_PER_SECOND, TransportState,
negotiate_output_config,
};
pub trait RealtimeAudioSource: Send + 'static {
fn render(&mut self, ctx: &ProcessCtx, output: &mut [&mut [f32]]);
fn latency_samples(&self) -> u32 {
0
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum CpalHostError {
NoOutputDevice,
NoCompatibleFormat,
InvalidCallbackSize,
Device(String),
}
impl std::fmt::Display for CpalHostError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoOutputDevice => formatter.write_str("no audio output device is available"),
Self::NoCompatibleFormat => {
formatter.write_str("no compatible audio output format is available")
}
Self::InvalidCallbackSize => {
formatter.write_str("the audio callback limit must be at least one frame")
}
Self::Device(message) => write!(formatter, "audio device failed: {message}"),
}
}
}
impl std::error::Error for CpalHostError {}
pub struct CpalOutputHost {
stream: Stream,
underruns: Arc<AtomicU64>,
sample_rate: u32,
channels: u16,
latency_samples: u32,
}
impl CpalOutputHost {
pub fn open(
mut source: impl RealtimeAudioSource,
project_rate: u32,
max_callback_frames: usize,
) -> Result<Self, CpalHostError> {
let device = cpal::default_host()
.default_output_device()
.ok_or(CpalHostError::NoOutputDevice)?;
if max_callback_frames == 0 {
return Err(CpalHostError::InvalidCallbackSize);
}
let supported = device
.supported_output_configs()
.map_err(|error| CpalHostError::Device(error.to_string()))?
.collect::<Vec<_>>();
let offered = supported
.iter()
.map(|config| HostAudioConfig {
channels: config.channels(),
min_sample_rate: config.min_sample_rate(),
max_sample_rate: config.max_sample_rate(),
format: match config.sample_format() {
SampleFormat::F32 => HostSampleFormat::F32,
SampleFormat::I16 => HostSampleFormat::I16,
_ => HostSampleFormat::Unsupported,
},
})
.collect::<Vec<_>>();
let chosen = negotiate_output_config(&offered, project_rate)
.ok_or(CpalHostError::NoCompatibleFormat)?;
let selected = offered
.iter()
.position(|config| *config == chosen)
.map(|index| supported[index])
.ok_or(CpalHostError::NoCompatibleFormat)?;
let device_rate = project_rate;
let config: StreamConfig = selected.with_sample_rate(device_rate).config();
let channels = usize::from(config.channels);
let mut planar = [
vec![0.0_f32; max_callback_frames],
vec![0.0_f32; max_callback_frames],
];
let underruns = Arc::new(AtomicU64::new(0));
let callback_underruns = Arc::clone(&underruns);
let error_underruns = Arc::clone(&underruns);
let mut sample_position = 0_i64;
let latency_samples = source.latency_samples();
let stream = device
.build_output_stream(
&config,
move |data: &mut [f32], _| {
if !fill_device_buffer(
&mut source,
&mut planar,
data,
channels,
device_rate,
&mut sample_position,
) {
callback_underruns.fetch_add(1, Ordering::Relaxed);
}
},
move |_| {
error_underruns.fetch_add(1, Ordering::Relaxed);
},
None,
)
.map_err(|error| CpalHostError::Device(error.to_string()))?;
Ok(Self {
stream,
underruns,
sample_rate: device_rate,
channels: config.channels,
latency_samples,
})
}
pub fn play(&self) -> Result<(), CpalHostError> {
self.stream
.play()
.map_err(|error| CpalHostError::Device(error.to_string()))
}
#[must_use]
pub fn underruns(&self) -> u64 {
self.underruns.load(Ordering::Relaxed)
}
#[must_use]
pub const fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[must_use]
pub const fn channels(&self) -> u16 {
self.channels
}
#[must_use]
pub const fn latency_samples(&self) -> u32 {
self.latency_samples
}
}
pub(super) fn fill_device_buffer(
source: &mut impl RealtimeAudioSource,
planar: &mut [Vec<f32>; 2],
data: &mut [f32],
channels: usize,
device_rate: u32,
sample_position: &mut i64,
) -> bool {
let piece = planar[0].len();
if channels == 0 || piece == 0 || !data.len().is_multiple_of(channels) {
data.fill(0.0);
return false;
}
let rendered = channels.min(2);
for chunk in data.chunks_mut(piece * channels) {
let frames = chunk.len() / channels;
let ctx = ProcessCtx {
position: MediaTime::from_ticks(
sample_position
.saturating_mul(TICKS_PER_SECOND)
.div_euclid(i64::from(device_rate)),
),
sample_position: *sample_position,
sample_rate: device_rate,
transport: TransportState::Playing,
};
let (left, right) = planar.split_at_mut(1);
if rendered == 1 {
source.render(&ctx, &mut [&mut left[0][..frames]]);
} else {
source.render(&ctx, &mut [&mut left[0][..frames], &mut right[0][..frames]]);
}
for (frame, destination) in chunk.chunks_exact_mut(channels).enumerate() {
for (channel, sample) in destination.iter_mut().enumerate() {
*sample = if channel < rendered {
planar[channel][frame]
} else {
0.0
};
}
}
*sample_position =
sample_position.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
}
true
}
}
#[cfg(feature = "cpal-host")]
pub use realtime::*;
#[cfg(test)]
#[path = "audio_host_tests.rs"]
mod tests;