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