1use std::collections::HashMap;
2
3use alpha2_audio::{AudioGraph, ChannelLayout, ProcessCtx, TransportState};
4#[cfg(feature = "timeline")]
5use alpha2_engine::{AudioRenderer, EngineProject};
6use alpha2_media::{
7 AudioBlock, AudioBlockProvider, AudioBlockSink, AudioSpan, CancelToken, EncodeError, SourceId,
8};
9use alpha2_time::{MediaTime, TICKS_PER_SECOND};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum OfflineAudioError {
15 InvalidInput,
17 Sink(EncodeError),
19 #[cfg(feature = "timeline")]
21 Engine(alpha2_engine::EngineError),
22 MissingSources(Vec<SourceId>),
24}
25
26impl std::fmt::Display for OfflineAudioError {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::InvalidInput => formatter.write_str("offline audio input is invalid"),
30 Self::Sink(error) => write!(formatter, "offline audio sink failed: {error}"),
31 #[cfg(feature = "timeline")]
32 Self::Engine(error) => write!(formatter, "offline audio rendering failed: {error}"),
33 Self::MissingSources(sources) => {
34 write!(
35 formatter,
36 "offline audio is missing {} sources",
37 sources.len()
38 )
39 }
40 }
41 }
42}
43
44impl std::error::Error for OfflineAudioError {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 Self::Sink(error) => Some(error),
48 #[cfg(feature = "timeline")]
49 Self::Engine(error) => Some(error),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Default)]
58pub struct DecodedAudioSources {
59 sources: HashMap<SourceId, AudioBlock>,
60}
61
62impl DecodedAudioSources {
63 #[must_use]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn insert(&mut self, source: SourceId, block: AudioBlock) {
71 self.sources.insert(source, block);
72 }
73
74 pub fn remove(&mut self, source: &SourceId) -> bool {
76 self.sources.remove(source).is_some()
77 }
78
79 #[must_use]
81 pub fn len(&self) -> usize {
82 self.sources.len()
83 }
84
85 #[must_use]
87 pub fn is_empty(&self) -> bool {
88 self.sources.is_empty()
89 }
90}
91
92impl AudioBlockProvider for DecodedAudioSources {
94 fn block(
95 &self,
96 source: &SourceId,
97 _start_sample: i64,
98 _frames: usize,
99 ) -> Option<AudioSpan<'_>> {
100 self.sources.get(source).map(AudioSpan::whole)
101 }
102}
103
104#[cfg(feature = "timeline")]
106#[derive(Copy, Clone, Debug, Eq, PartialEq)]
107pub struct ProjectAudioRequest<'a> {
108 pub channels: usize,
110 pub start_sample: i64,
112 pub total_frames: usize,
114 pub block_sizes: &'a [usize],
116}
117
118#[cfg(feature = "timeline")]
130pub fn render_project_audio(
131 snapshot: &EngineProject,
132 request: &ProjectAudioRequest<'_>,
133 sources: &dyn AudioBlockProvider,
134 sink: &mut dyn AudioBlockSink,
135 cancel: &CancelToken,
136) -> Result<(), OfflineAudioError> {
137 let ProjectAudioRequest {
138 channels,
139 start_sample,
140 total_frames,
141 block_sizes,
142 } = *request;
143 if total_frames == 0 || block_sizes.is_empty() || block_sizes.contains(&0) || start_sample < 0 {
144 return Err(OfflineAudioError::InvalidInput);
145 }
146 let max_block = block_sizes.iter().copied().max().unwrap_or(1);
147 let mut renderer =
148 AudioRenderer::new(snapshot, channels, max_block).map_err(OfflineAudioError::Engine)?;
149
150 let mut produced = 0;
151 let mut sequence = 0;
152 let mut missing: Vec<SourceId> = Vec::new();
153 while produced < total_frames {
154 if cancel.is_cancelled() {
155 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
156 return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
157 }
158 let count = block_sizes[sequence % block_sizes.len()].min(total_frames - produced);
159 let offset = i64::try_from(produced).unwrap_or(i64::MAX);
160 let status = renderer
161 .render(
162 snapshot,
163 start_sample.saturating_add(offset),
164 count,
165 sources,
166 TransportState::Playing,
167 )
168 .map_err(OfflineAudioError::Engine)?;
169 for source in &status.missing_sources {
170 if !missing.contains(source) {
171 missing.push(source.clone());
172 }
173 }
174 let mut block = renderer.block(&status);
175 block.start_sample = offset;
177 sink.write_audio(&block, cancel)
178 .map_err(OfflineAudioError::Sink)?;
179 produced += count;
180 sequence += 1;
181 }
182 if !missing.is_empty() {
183 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
185 return Err(OfflineAudioError::MissingSources(missing));
186 }
187 sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
188}
189
190#[derive(Copy, Clone, Debug, Eq, PartialEq)]
192pub enum HostSampleFormat {
193 F32,
195 I16,
197 Unsupported,
199}
200
201#[derive(Copy, Clone, Debug, Eq, PartialEq)]
203pub struct HostAudioConfig {
204 pub channels: u16,
206 pub min_sample_rate: u32,
208 pub max_sample_rate: u32,
210 pub format: HostSampleFormat,
212}
213
214#[must_use]
221pub fn negotiate_output_config(
222 configs: &[HostAudioConfig],
223 project_rate: u32,
224) -> Option<HostAudioConfig> {
225 configs
226 .iter()
227 .copied()
228 .filter(|config| {
229 config.format == HostSampleFormat::F32
230 && config.channels > 0
231 && (config.min_sample_rate..=config.max_sample_rate).contains(&project_rate)
232 })
233 .min_by_key(|config| match config.channels {
234 2 => (0, 0),
235 1 => (2, 0),
236 more => (1, more),
237 })
238}
239
240pub fn render_offline(
249 graph: &mut AudioGraph,
250 sample_rate: u32,
251 inputs: &[Vec<f32>],
252 block_sizes: &[usize],
253 sink: &mut dyn AudioBlockSink,
254 cancel: &CancelToken,
255) -> Result<(), OfflineAudioError> {
256 let layout = graph.layout();
257 let frames = inputs.first().map_or(0, Vec::len);
258 if sample_rate == 0
259 || inputs.is_empty()
260 || graph.buses().checked_mul(layout.channels()) != Some(inputs.len())
261 || inputs.iter().any(|channel| channel.len() != frames)
262 || block_sizes.is_empty()
263 || block_sizes.contains(&0)
264 {
265 return Err(OfflineAudioError::InvalidInput);
266 }
267 let max_block = block_sizes.iter().copied().max().unwrap_or(1);
268 graph.prepare(sample_rate, max_block);
269 let mut output: Vec<Vec<f32>> = (0..layout.channels())
270 .map(|_| vec![0.0; max_block])
271 .collect();
272 let mut offset = 0;
273 let mut sequence = 0;
274 while offset < frames {
275 if cancel.is_cancelled() {
276 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
277 return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
278 }
279 let count = block_sizes[sequence % block_sizes.len()].min(frames - offset);
280 let refs: Vec<&[f32]> = inputs
281 .iter()
282 .map(|channel| &channel[offset..offset + count])
283 .collect();
284 let mut outputs: Vec<&mut [f32]> = output
285 .iter_mut()
286 .map(|channel| &mut channel[..count])
287 .collect();
288 let sample_position = i64::try_from(offset).unwrap_or(i64::MAX);
289 graph.process(
290 &ProcessCtx {
291 position: MediaTime::from_ticks(
292 sample_position
293 .saturating_mul(TICKS_PER_SECOND)
294 .div_euclid(i64::from(sample_rate)),
295 ),
296 sample_position,
297 sample_rate,
298 transport: TransportState::Playing,
299 },
300 &refs,
301 &mut outputs,
302 );
303 drop(outputs);
304 sink.write_audio(
305 &AudioBlock {
306 start_sample: sample_position,
307 sample_rate,
308 speakers: match layout {
309 ChannelLayout::Mono => alpha2_media::SpeakerLayout::MONO,
310 ChannelLayout::Stereo => alpha2_media::SpeakerLayout::STEREO,
311 },
312 channels: output
313 .iter()
314 .map(|channel| channel[..count].to_vec())
315 .collect(),
316 },
317 cancel,
318 )
319 .map_err(OfflineAudioError::Sink)?;
320 offset += count;
321 sequence += 1;
322 }
323 sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
324}
325
326#[cfg(feature = "cpal-host")]
327mod realtime {
328 use std::sync::Arc;
329 use std::sync::atomic::{AtomicU64, Ordering};
330
331 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
332 use cpal::{SampleFormat, Stream, StreamConfig};
333
334 use super::{
335 HostAudioConfig, HostSampleFormat, MediaTime, ProcessCtx, TICKS_PER_SECOND, TransportState,
336 negotiate_output_config,
337 };
338
339 pub trait RealtimeAudioSource: Send + 'static {
345 fn render(&mut self, ctx: &ProcessCtx, output: &mut [&mut [f32]]);
347 fn latency_samples(&self) -> u32 {
349 0
350 }
351 }
352
353 #[derive(Debug)]
355 #[non_exhaustive]
356 pub enum CpalHostError {
357 NoOutputDevice,
359 NoCompatibleFormat,
361 InvalidCallbackSize,
363 Device(String),
365 }
366
367 impl std::fmt::Display for CpalHostError {
368 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 match self {
370 Self::NoOutputDevice => formatter.write_str("no audio output device is available"),
371 Self::NoCompatibleFormat => {
372 formatter.write_str("no compatible audio output format is available")
373 }
374 Self::InvalidCallbackSize => {
375 formatter.write_str("the audio callback limit must be at least one frame")
376 }
377 Self::Device(message) => write!(formatter, "audio device failed: {message}"),
378 }
379 }
380 }
381
382 impl std::error::Error for CpalHostError {}
383
384 pub struct CpalOutputHost {
386 stream: Stream,
387 underruns: Arc<AtomicU64>,
388 sample_rate: u32,
389 channels: u16,
390 latency_samples: u32,
391 }
392
393 impl CpalOutputHost {
394 pub fn open(
404 mut source: impl RealtimeAudioSource,
405 project_rate: u32,
406 max_callback_frames: usize,
407 ) -> Result<Self, CpalHostError> {
408 let device = cpal::default_host()
409 .default_output_device()
410 .ok_or(CpalHostError::NoOutputDevice)?;
411 if max_callback_frames == 0 {
412 return Err(CpalHostError::InvalidCallbackSize);
413 }
414 let supported = device
415 .supported_output_configs()
416 .map_err(|error| CpalHostError::Device(error.to_string()))?
417 .collect::<Vec<_>>();
418 let offered = supported
419 .iter()
420 .map(|config| HostAudioConfig {
421 channels: config.channels(),
422 min_sample_rate: config.min_sample_rate(),
423 max_sample_rate: config.max_sample_rate(),
424 format: match config.sample_format() {
425 SampleFormat::F32 => HostSampleFormat::F32,
426 SampleFormat::I16 => HostSampleFormat::I16,
427 _ => HostSampleFormat::Unsupported,
428 },
429 })
430 .collect::<Vec<_>>();
431 let chosen = negotiate_output_config(&offered, project_rate)
432 .ok_or(CpalHostError::NoCompatibleFormat)?;
433 let selected = offered
434 .iter()
435 .position(|config| *config == chosen)
436 .map(|index| supported[index])
437 .ok_or(CpalHostError::NoCompatibleFormat)?;
438 let device_rate = project_rate;
439 let config: StreamConfig = selected.with_sample_rate(device_rate).config();
440 let channels = usize::from(config.channels);
441 let mut planar = [
442 vec![0.0_f32; max_callback_frames],
443 vec![0.0_f32; max_callback_frames],
444 ];
445 let underruns = Arc::new(AtomicU64::new(0));
446 let callback_underruns = Arc::clone(&underruns);
447 let error_underruns = Arc::clone(&underruns);
448 let mut sample_position = 0_i64;
449 let latency_samples = source.latency_samples();
450 let stream = device
451 .build_output_stream(
452 &config,
453 move |data: &mut [f32], _| {
454 if !fill_device_buffer(
455 &mut source,
456 &mut planar,
457 data,
458 channels,
459 device_rate,
460 &mut sample_position,
461 ) {
462 callback_underruns.fetch_add(1, Ordering::Relaxed);
463 }
464 },
465 move |_| {
466 error_underruns.fetch_add(1, Ordering::Relaxed);
467 },
468 None,
469 )
470 .map_err(|error| CpalHostError::Device(error.to_string()))?;
471 Ok(Self {
472 stream,
473 underruns,
474 sample_rate: device_rate,
475 channels: config.channels,
476 latency_samples,
477 })
478 }
479 pub fn play(&self) -> Result<(), CpalHostError> {
484 self.stream
485 .play()
486 .map_err(|error| CpalHostError::Device(error.to_string()))
487 }
488 #[must_use]
491 pub fn underruns(&self) -> u64 {
492 self.underruns.load(Ordering::Relaxed)
493 }
494 #[must_use]
496 pub const fn sample_rate(&self) -> u32 {
497 self.sample_rate
498 }
499 #[must_use]
501 pub const fn channels(&self) -> u16 {
502 self.channels
503 }
504 #[must_use]
506 pub const fn latency_samples(&self) -> u32 {
507 self.latency_samples
508 }
509 }
510
511 pub(super) fn fill_device_buffer(
518 source: &mut impl RealtimeAudioSource,
519 planar: &mut [Vec<f32>; 2],
520 data: &mut [f32],
521 channels: usize,
522 device_rate: u32,
523 sample_position: &mut i64,
524 ) -> bool {
525 let piece = planar[0].len();
526 if channels == 0 || piece == 0 || !data.len().is_multiple_of(channels) {
527 data.fill(0.0);
528 return false;
529 }
530 let rendered = channels.min(2);
531 for chunk in data.chunks_mut(piece * channels) {
532 let frames = chunk.len() / channels;
533 let ctx = ProcessCtx {
534 position: MediaTime::from_ticks(
535 sample_position
536 .saturating_mul(TICKS_PER_SECOND)
537 .div_euclid(i64::from(device_rate)),
538 ),
539 sample_position: *sample_position,
540 sample_rate: device_rate,
541 transport: TransportState::Playing,
542 };
543 let (left, right) = planar.split_at_mut(1);
544 if rendered == 1 {
545 source.render(&ctx, &mut [&mut left[0][..frames]]);
546 } else {
547 source.render(&ctx, &mut [&mut left[0][..frames], &mut right[0][..frames]]);
548 }
549 for (frame, destination) in chunk.chunks_exact_mut(channels).enumerate() {
550 for (channel, sample) in destination.iter_mut().enumerate() {
551 *sample = if channel < rendered {
552 planar[channel][frame]
553 } else {
554 0.0
555 };
556 }
557 }
558 *sample_position =
559 sample_position.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
560 }
561 true
562 }
563}
564
565#[cfg(feature = "cpal-host")]
566pub use realtime::*;
567
568#[cfg(test)]
569#[path = "audio_host_tests.rs"]
570mod tests;