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)]
60pub struct DecodedAudioSources {
61 sources: HashMap<SourceId, AudioBlock>,
62}
63
64impl DecodedAudioSources {
65 #[must_use]
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn insert(&mut self, source: SourceId, block: AudioBlock) {
73 self.sources.insert(source, block);
74 }
75
76 pub fn remove(&mut self, source: &SourceId) -> bool {
78 self.sources.remove(source).is_some()
79 }
80
81 #[must_use]
83 pub fn len(&self) -> usize {
84 self.sources.len()
85 }
86
87 #[must_use]
89 pub fn is_empty(&self) -> bool {
90 self.sources.is_empty()
91 }
92}
93
94impl AudioBlockProvider for DecodedAudioSources {
97 fn block(
98 &self,
99 source: &SourceId,
100 _start_sample: i64,
101 _frames: usize,
102 ) -> Option<AudioSpan<'_>> {
103 self.sources.get(source).map(AudioSpan::whole)
104 }
105}
106
107#[cfg(feature = "timeline")]
109#[derive(Copy, Clone, Debug, Eq, PartialEq)]
110pub struct ProjectAudioRequest<'a> {
111 pub channels: usize,
113 pub start_sample: i64,
116 pub total_frames: usize,
118 pub block_sizes: &'a [usize],
120}
121
122#[cfg(feature = "timeline")]
135pub fn render_project_audio(
136 snapshot: &EngineProject,
137 request: &ProjectAudioRequest<'_>,
138 sources: &dyn AudioBlockProvider,
139 sink: &mut dyn AudioBlockSink,
140 cancel: &CancelToken,
141) -> Result<(), OfflineAudioError> {
142 let ProjectAudioRequest {
143 channels,
144 start_sample,
145 total_frames,
146 block_sizes,
147 } = *request;
148 if total_frames == 0 || block_sizes.is_empty() || block_sizes.contains(&0) || start_sample < 0 {
149 return Err(OfflineAudioError::InvalidInput);
150 }
151 let max_block = block_sizes.iter().copied().max().unwrap_or(1);
152 let mut renderer =
153 AudioRenderer::new(snapshot, channels, max_block).map_err(OfflineAudioError::Engine)?;
154
155 let mut produced = 0;
156 let mut sequence = 0;
157 let mut missing: Vec<SourceId> = Vec::new();
158 while produced < total_frames {
159 if cancel.is_cancelled() {
160 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
161 return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
162 }
163 let count = block_sizes[sequence % block_sizes.len()].min(total_frames - produced);
164 let offset = i64::try_from(produced).unwrap_or(i64::MAX);
165 let status = renderer
166 .render(
167 snapshot,
168 start_sample.saturating_add(offset),
169 count,
170 sources,
171 TransportState::Playing,
172 )
173 .map_err(OfflineAudioError::Engine)?;
174 for source in &status.missing_sources {
175 if !missing.contains(source) {
176 missing.push(source.clone());
177 }
178 }
179 let mut block = renderer.block(&status);
180 block.start_sample = offset;
182 sink.write_audio(&block, cancel)
183 .map_err(OfflineAudioError::Sink)?;
184 produced += count;
185 sequence += 1;
186 }
187 if !missing.is_empty() {
188 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
190 return Err(OfflineAudioError::MissingSources(missing));
191 }
192 sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
193}
194
195#[derive(Copy, Clone, Debug, Eq, PartialEq)]
197pub enum HostSampleFormat {
198 F32,
200 I16,
202 Unsupported,
204}
205
206#[derive(Copy, Clone, Debug, Eq, PartialEq)]
208pub struct HostAudioConfig {
209 pub channels: u16,
211 pub min_sample_rate: u32,
213 pub max_sample_rate: u32,
215 pub format: HostSampleFormat,
217}
218
219#[must_use]
230pub fn negotiate_output_config(
231 configs: &[HostAudioConfig],
232 project_rate: u32,
233) -> Option<HostAudioConfig> {
234 configs
235 .iter()
236 .copied()
237 .filter(|config| {
238 config.format == HostSampleFormat::F32
239 && config.channels > 0
240 && (config.min_sample_rate..=config.max_sample_rate).contains(&project_rate)
241 })
242 .min_by_key(|config| match config.channels {
243 2 => (0, 0),
244 1 => (2, 0),
245 more => (1, more),
246 })
247}
248
249pub fn render_offline(
261 graph: &mut AudioGraph,
262 sample_rate: u32,
263 inputs: &[Vec<f32>],
264 block_sizes: &[usize],
265 sink: &mut dyn AudioBlockSink,
266 cancel: &CancelToken,
267) -> Result<(), OfflineAudioError> {
268 let layout = graph.layout();
269 let frames = inputs.first().map_or(0, Vec::len);
270 if sample_rate == 0
271 || inputs.is_empty()
272 || graph.buses().checked_mul(layout.channels()) != Some(inputs.len())
273 || inputs.iter().any(|channel| channel.len() != frames)
274 || block_sizes.is_empty()
275 || block_sizes.contains(&0)
276 {
277 return Err(OfflineAudioError::InvalidInput);
278 }
279 let max_block = block_sizes.iter().copied().max().unwrap_or(1);
280 graph.prepare(sample_rate, max_block);
281 let mut output: Vec<Vec<f32>> = (0..layout.channels())
282 .map(|_| vec![0.0; max_block])
283 .collect();
284 let mut offset = 0;
285 let mut sequence = 0;
286 while offset < frames {
287 if cancel.is_cancelled() {
288 sink.abort_audio().map_err(OfflineAudioError::Sink)?;
289 return Err(OfflineAudioError::Sink(EncodeError::Cancelled));
290 }
291 let count = block_sizes[sequence % block_sizes.len()].min(frames - offset);
292 let refs: Vec<&[f32]> = inputs
293 .iter()
294 .map(|channel| &channel[offset..offset + count])
295 .collect();
296 let mut outputs: Vec<&mut [f32]> = output
297 .iter_mut()
298 .map(|channel| &mut channel[..count])
299 .collect();
300 let sample_position = i64::try_from(offset).unwrap_or(i64::MAX);
301 graph.process(
302 &ProcessCtx {
303 position: MediaTime::from_ticks(
304 sample_position
305 .saturating_mul(TICKS_PER_SECOND)
306 .div_euclid(i64::from(sample_rate)),
307 ),
308 sample_position,
309 sample_rate,
310 transport: TransportState::Playing,
311 },
312 &refs,
313 &mut outputs,
314 );
315 drop(outputs);
316 sink.write_audio(
317 &AudioBlock {
318 start_sample: sample_position,
319 sample_rate,
320 speakers: match layout {
321 ChannelLayout::Mono => alpha2_media::SpeakerLayout::MONO,
322 ChannelLayout::Stereo => alpha2_media::SpeakerLayout::STEREO,
323 },
324 channels: output
325 .iter()
326 .map(|channel| channel[..count].to_vec())
327 .collect(),
328 },
329 cancel,
330 )
331 .map_err(OfflineAudioError::Sink)?;
332 offset += count;
333 sequence += 1;
334 }
335 sink.finish_audio(cancel).map_err(OfflineAudioError::Sink)
336}
337
338#[cfg(feature = "cpal-host")]
339mod realtime {
340 use std::sync::Arc;
341 use std::sync::atomic::{AtomicU64, Ordering};
342
343 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
344 use cpal::{SampleFormat, Stream, StreamConfig};
345
346 use super::{
347 HostAudioConfig, HostSampleFormat, MediaTime, ProcessCtx, TICKS_PER_SECOND, TransportState,
348 negotiate_output_config,
349 };
350
351 pub trait RealtimeAudioSource: Send + 'static {
358 fn render(&mut self, ctx: &ProcessCtx, output: &mut [&mut [f32]]);
360 fn latency_samples(&self) -> u32 {
362 0
363 }
364 }
365
366 #[derive(Debug)]
368 #[non_exhaustive]
369 pub enum CpalHostError {
370 NoOutputDevice,
372 NoCompatibleFormat,
374 InvalidCallbackSize,
376 Device(String),
378 }
379
380 impl std::fmt::Display for CpalHostError {
381 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382 match self {
383 Self::NoOutputDevice => formatter.write_str("no audio output device is available"),
384 Self::NoCompatibleFormat => {
385 formatter.write_str("no compatible audio output format is available")
386 }
387 Self::InvalidCallbackSize => {
388 formatter.write_str("the audio callback limit must be at least one frame")
389 }
390 Self::Device(message) => write!(formatter, "audio device failed: {message}"),
391 }
392 }
393 }
394
395 impl std::error::Error for CpalHostError {}
396
397 pub struct CpalOutputHost {
399 stream: Stream,
400 underruns: Arc<AtomicU64>,
401 sample_rate: u32,
402 channels: u16,
403 latency_samples: u32,
404 }
405
406 impl CpalOutputHost {
407 pub fn open(
419 mut source: impl RealtimeAudioSource,
420 project_rate: u32,
421 max_callback_frames: usize,
422 ) -> Result<Self, CpalHostError> {
423 let device = cpal::default_host()
424 .default_output_device()
425 .ok_or(CpalHostError::NoOutputDevice)?;
426 if max_callback_frames == 0 {
427 return Err(CpalHostError::InvalidCallbackSize);
428 }
429 let supported = device
430 .supported_output_configs()
431 .map_err(|error| CpalHostError::Device(error.to_string()))?
432 .collect::<Vec<_>>();
433 let offered = supported
434 .iter()
435 .map(|config| HostAudioConfig {
436 channels: config.channels(),
437 min_sample_rate: config.min_sample_rate(),
438 max_sample_rate: config.max_sample_rate(),
439 format: match config.sample_format() {
440 SampleFormat::F32 => HostSampleFormat::F32,
441 SampleFormat::I16 => HostSampleFormat::I16,
442 _ => HostSampleFormat::Unsupported,
443 },
444 })
445 .collect::<Vec<_>>();
446 let chosen = negotiate_output_config(&offered, project_rate)
447 .ok_or(CpalHostError::NoCompatibleFormat)?;
448 let selected = offered
449 .iter()
450 .position(|config| *config == chosen)
451 .map(|index| supported[index])
452 .ok_or(CpalHostError::NoCompatibleFormat)?;
453 let device_rate = project_rate;
454 let config: StreamConfig = selected.with_sample_rate(device_rate).config();
455 let channels = usize::from(config.channels);
456 let mut planar = [
457 vec![0.0_f32; max_callback_frames],
458 vec![0.0_f32; max_callback_frames],
459 ];
460 let underruns = Arc::new(AtomicU64::new(0));
461 let callback_underruns = Arc::clone(&underruns);
462 let error_underruns = Arc::clone(&underruns);
463 let mut sample_position = 0_i64;
464 let latency_samples = source.latency_samples();
465 let stream = device
466 .build_output_stream(
467 &config,
468 move |data: &mut [f32], _| {
469 if !fill_device_buffer(
470 &mut source,
471 &mut planar,
472 data,
473 channels,
474 device_rate,
475 &mut sample_position,
476 ) {
477 callback_underruns.fetch_add(1, Ordering::Relaxed);
478 }
479 },
480 move |_| {
481 error_underruns.fetch_add(1, Ordering::Relaxed);
482 },
483 None,
484 )
485 .map_err(|error| CpalHostError::Device(error.to_string()))?;
486 Ok(Self {
487 stream,
488 underruns,
489 sample_rate: device_rate,
490 channels: config.channels,
491 latency_samples,
492 })
493 }
494 pub fn play(&self) -> Result<(), CpalHostError> {
499 self.stream
500 .play()
501 .map_err(|error| CpalHostError::Device(error.to_string()))
502 }
503 #[must_use]
506 pub fn underruns(&self) -> u64 {
507 self.underruns.load(Ordering::Relaxed)
508 }
509 #[must_use]
511 pub const fn sample_rate(&self) -> u32 {
512 self.sample_rate
513 }
514 #[must_use]
516 pub const fn channels(&self) -> u16 {
517 self.channels
518 }
519 #[must_use]
521 pub const fn latency_samples(&self) -> u32 {
522 self.latency_samples
523 }
524 }
525
526 pub(super) fn fill_device_buffer(
535 source: &mut impl RealtimeAudioSource,
536 planar: &mut [Vec<f32>; 2],
537 data: &mut [f32],
538 channels: usize,
539 device_rate: u32,
540 sample_position: &mut i64,
541 ) -> bool {
542 let piece = planar[0].len();
543 if channels == 0 || piece == 0 || !data.len().is_multiple_of(channels) {
544 data.fill(0.0);
545 return false;
546 }
547 let rendered = channels.min(2);
548 for chunk in data.chunks_mut(piece * channels) {
549 let frames = chunk.len() / channels;
550 let ctx = ProcessCtx {
551 position: MediaTime::from_ticks(
552 sample_position
553 .saturating_mul(TICKS_PER_SECOND)
554 .div_euclid(i64::from(device_rate)),
555 ),
556 sample_position: *sample_position,
557 sample_rate: device_rate,
558 transport: TransportState::Playing,
559 };
560 let (left, right) = planar.split_at_mut(1);
561 if rendered == 1 {
562 source.render(&ctx, &mut [&mut left[0][..frames]]);
563 } else {
564 source.render(&ctx, &mut [&mut left[0][..frames], &mut right[0][..frames]]);
565 }
566 for (frame, destination) in chunk.chunks_exact_mut(channels).enumerate() {
567 for (channel, sample) in destination.iter_mut().enumerate() {
568 *sample = if channel < rendered {
569 planar[channel][frame]
570 } else {
571 0.0
572 };
573 }
574 }
575 *sample_position =
576 sample_position.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
577 }
578 true
579 }
580}
581
582#[cfg(feature = "cpal-host")]
583pub use realtime::*;
584
585#[cfg(test)]
586#[path = "audio_host_tests.rs"]
587mod tests;