use std::collections::HashMap;
use std::path::PathBuf;
use crate::{AUDIO_INTERPOLATION_MARGIN, error_message};
use alpha2_media::{
AudioBlock, AudioBlockProvider, AudioSpan, CancelToken, DecodeError, DecodeEvent,
DecodeRequest, DecodeRequestKind, Decoder, SourceId,
};
use alpha2_model::Project;
use alpha2_native::NativeAudioDecoder;
use alpha2_time::{MediaTime, TICKS_PER_SECOND};
pub struct WindowedAudioSource {
pub decoder: NativeAudioDecoder,
pub window: Option<AudioBlock>,
covered_start: i64,
ended: bool,
last_start: Option<i64>,
}
const BACKWARD_READAHEAD_SECONDS: i64 = 1;
const AUDIO_DECODE_BLOCK_FRAMES: usize = 4_096;
impl WindowedAudioSource {
fn continues_at(&self, start: i64) -> bool {
self.window.as_ref().is_some_and(|window| {
let window_start = window.start_sample;
let window_end = window_start.saturating_add(audio_block_frames(window));
start >= self.covered_start && (start <= window_end || self.ended)
})
}
fn ensure_window(
&mut self,
source: &SourceId,
(start, end): (i64, i64),
sample_rate: u32,
limit: u64,
cancel: &CancelToken,
) -> Result<(), String> {
let backward = self.last_start.is_some_and(|last| start < last);
self.last_start = Some(start);
if !self.continues_at(start) {
let seek_start = if backward {
start
.saturating_sub(BACKWARD_READAHEAD_SECONDS * i64::from(sample_rate))
.max(0)
} else {
start
};
let request_time =
MediaTime::from_ticks(samples_to_ticks_ceil(seek_start, sample_rate));
let result = self.decoder.seek(
DecodeRequest {
source: source.clone(),
time: request_time,
kind: DecodeRequestKind::Seek,
generation: 0,
},
cancel,
);
match result {
Ok(result) => {
let DecodeEvent::Audio(block) = result.event else {
return Err(format!(
"audio source {} did not decode a block",
source.as_str()
));
};
self.window = Some(block);
self.ended = false;
}
Err(DecodeError::OutOfRange) => {
self.window = Some(AudioBlock {
start_sample: seek_start,
sample_rate,
speakers: self.decoder.speakers(),
channels: vec![Vec::new(); self.decoder.channels()],
});
self.ended = true;
}
Err(error) => return Err(error_message(error)),
}
self.covered_start = seek_start.min(start);
}
self.reserve_to(source, end, limit)?;
loop {
let window = self
.window
.as_ref()
.ok_or_else(|| "audio window disappeared".to_owned())?;
let window_start = window.start_sample;
let window_end = window_start.saturating_add(audio_block_frames(window));
if window_end >= end || self.ended {
break;
}
match self.decoder.decode_next(cancel).map_err(error_message)? {
DecodeEvent::Audio(block) => append_audio_block(
self.window
.as_mut()
.ok_or_else(|| "audio window disappeared".to_owned())?,
block,
)?,
DecodeEvent::EndOfStream => {
self.ended = true;
break;
}
DecodeEvent::Retry | DecodeEvent::Video(_) => {}
}
}
let declared_duration = self.decoder.audio_duration();
let window = self
.window
.as_mut()
.ok_or_else(|| "audio window disappeared".to_owned())?;
if self.ended {
pad_audio_window_to_declared_end(window, end, declared_duration, sample_rate);
}
if !backward {
trim_audio_window(window, start);
self.covered_start = start;
}
Ok(())
}
fn reserve_to(&mut self, source: &SourceId, end: i64, limit: u64) -> Result<(), String> {
let ended = self.ended;
let Some(window) = self.window.as_mut() else {
return Ok(());
};
let held = audio_block_frames(window);
if ended || window.start_sample.saturating_add(held) >= end {
return Ok(());
}
let frames = usize::try_from(end.saturating_sub(window.start_sample))
.unwrap_or(usize::MAX)
.saturating_add(AUDIO_DECODE_BLOCK_FRAMES);
let bytes = u64::try_from(frames)
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(window.channels.len()).unwrap_or(u64::MAX))
.saturating_mul(4);
if bytes > limit {
return Err(crate::memory::resource_limit(format!(
"the audio mix reads {frames} samples of {} channels of source {} at once, \
{bytes} bytes, over the {limit} bytes the audio window budget has left",
window.channels.len(),
source.as_str(),
)));
}
for channel in &mut window.channels {
channel.reserve_exact(frames.saturating_sub(channel.len()));
}
Ok(())
}
}
pub struct WindowedAudioSources {
pub sample_rate: u32,
pub paths: HashMap<SourceId, PathBuf>,
pub sources: HashMap<SourceId, Vec<WindowedAudioSource>>,
pub byte_budget: crate::memory::ByteBudget,
pub retained_bytes: u64,
pub peak_bytes: u64,
pub decoding: std::sync::Arc<crate::recovery::ExportDecoding>,
}
impl WindowedAudioSources {
pub fn new(
project: &Project,
assets: &HashMap<String, PathBuf>,
sample_rate: u32,
byte_budget: impl Into<crate::memory::ByteBudget>,
decoding: std::sync::Arc<crate::recovery::ExportDecoding>,
) -> Result<Self, String> {
let mut paths = HashMap::new();
for source in project
.used_sources()
.into_iter()
.filter(|source| source.is_audible())
{
let path = assets.get(source.id.as_str()).ok_or_else(|| {
crate::protocol::missing_input(format!(
"missing uploaded asset {}",
source.id.as_str()
))
})?;
paths.insert(source.id.clone(), path.clone());
}
Ok(Self {
sample_rate,
paths,
sources: HashMap::new(),
byte_budget: byte_budget.into(),
retained_bytes: 0,
peak_bytes: 0,
decoding,
})
}
pub fn prepare(
&mut self,
needs: &[alpha2_engine::AudioNeed],
cancel: &CancelToken,
) -> Result<(), String> {
let mut ranges = HashMap::<SourceId, Vec<(i64, i64)>>::new();
for need in needs {
let start = ticks_to_samples(need.source_range.start().ticks(), self.sample_rate)
.saturating_sub(AUDIO_INTERPOLATION_MARGIN)
.max(0);
let end = ticks_to_samples_ceil(need.source_range.end().ticks(), self.sample_rate)
.saturating_add(AUDIO_INTERPOLATION_MARGIN);
if end > start {
ranges
.entry(need.source_id.clone())
.or_default()
.push((start, end));
}
}
let least = ranges.iter().fold(0_u64, |bytes, (source, spans)| {
let channels = self
.sources
.get(source)
.and_then(|states| states.first())
.map_or(1, |state| state.decoder.channels().max(1));
spans.iter().fold(bytes, |bytes, &(start, end)| {
bytes.saturating_add(
u64::try_from(end.saturating_sub(start))
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(channels).unwrap_or(u64::MAX))
.saturating_mul(4),
)
})
});
let budget = self.byte_budget.holding(least);
if least > budget {
return Err(crate::memory::resource_limit(format!(
"the audio mix needs at least {least} bytes of decoded audio at once, over the {budget} byte audio window budget"
)));
}
self.sources.retain(|source, _| ranges.contains_key(source));
for (source, spans) in ranges {
self.ensure_windows(&source, &merge_sample_ranges(spans), budget, cancel)?;
}
self.retained_bytes = window_bytes(self.sources.values().flatten());
if self.retained_bytes > budget {
return Err(crate::memory::resource_limit(format!(
"decoded audio window exceeds {budget} byte budget"
)));
}
self.peak_bytes = self.peak_bytes.max(self.retained_bytes);
Ok(())
}
fn ensure_windows(
&mut self,
source: &SourceId,
spans: &[(i64, i64)],
budget: u64,
cancel: &CancelToken,
) -> Result<(), String> {
let sample_rate = self.sample_rate;
let mut idle = self.sources.remove(source).unwrap_or_default();
let continued = spans
.iter()
.map(|&(start, _)| {
idle.iter()
.position(|state| state.continues_at(start))
.map(|index| idle.swap_remove(index))
})
.collect::<Vec<_>>();
let mut windows = Vec::with_capacity(spans.len());
let mut continued = continued.into_iter();
for &span in spans {
let mut state = match continued.next().flatten().or_else(|| idle.pop()) {
Some(state) => state,
None => self.open(source)?,
};
let held = window_bytes(self.sources.values().flatten())
.saturating_add(window_bytes(&idle))
.saturating_add(window_bytes(&windows))
.saturating_add(window_bytes(continued.as_slice().iter().flatten()));
state.ensure_window(
source,
span,
sample_rate,
budget.saturating_sub(held),
cancel,
)?;
windows.push(state);
}
self.sources.insert(source.clone(), windows);
Ok(())
}
fn open(&self, source: &SourceId) -> Result<WindowedAudioSource, String> {
let path = self.paths.get(source).ok_or_else(|| {
crate::protocol::missing_input(format!("missing audio source {}", source.as_str()))
})?;
let mut decoder = crate::recovery::audio(
NativeAudioDecoder::open(path, self.sample_rate).map_err(error_message)?,
source,
path,
&self.decoding,
);
decoder
.set_block_frames(AUDIO_DECODE_BLOCK_FRAMES)
.map_err(error_message)?;
Ok(WindowedAudioSource {
decoder,
window: None,
covered_start: 0,
ended: false,
last_start: None,
})
}
pub const fn peak_bytes(&self) -> u64 {
self.peak_bytes
}
}
impl AudioBlockProvider for WindowedAudioSources {
fn block(&self, source: &SourceId, start_sample: i64, frames: usize) -> Option<AudioSpan<'_>> {
let request_end = start_sample.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
let spans = self.sources.get(source)?.iter().filter_map(|state| {
let window = state.window.as_ref()?;
let window_start = window.start_sample;
let window_end = window_start.saturating_add(audio_block_frames(window));
(start_sample >= state.covered_start && (start_sample < window_end || state.ended))
.then_some(AudioSpan {
block: window,
starts_source: state.covered_start < window_start,
ends_source: state.ended,
})
});
let mut first = None;
for span in spans {
if span.covers(start_sample, request_end) {
return Some(span);
}
first.get_or_insert(span);
}
first
}
}
pub fn merge_sample_ranges(mut ranges: Vec<(i64, i64)>) -> Vec<(i64, i64)> {
ranges.sort_unstable();
let mut merged: Vec<(i64, i64)> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
match merged.last_mut() {
Some(last) if start <= last.1 => last.1 = last.1.max(end),
_ => merged.push((start, end)),
}
}
merged
}
pub fn audio_block_frames(block: &AudioBlock) -> i64 {
block
.channels
.first()
.and_then(|channel| i64::try_from(channel.len()).ok())
.unwrap_or(0)
}
fn window_bytes<'a>(states: impl IntoIterator<Item = &'a WindowedAudioSource>) -> u64 {
states.into_iter().fold(0_u64, |bytes, state| {
bytes.saturating_add(state.window.as_ref().map_or(0, audio_block_bytes))
})
}
pub fn audio_block_bytes(block: &AudioBlock) -> u64 {
block.channels.iter().fold(0_u64, |bytes, channel| {
bytes.saturating_add(
u64::try_from(channel.len())
.unwrap_or(u64::MAX)
.saturating_mul(4),
)
})
}
pub fn append_audio_block(window: &mut AudioBlock, block: AudioBlock) -> Result<(), String> {
if window.sample_rate != block.sample_rate || window.channels.len() != block.channels.len() {
return Err("decoded audio format changed inside a source".to_owned());
}
if block.start_sample
!= window
.start_sample
.saturating_add(audio_block_frames(window))
{
return Err("decoded audio has an unreported gap or overlap inside AudioSpan".to_owned());
}
for (output, input) in window.channels.iter_mut().zip(block.channels) {
output.extend(input);
}
Ok(())
}
pub fn pad_audio_window_to_declared_end(
window: &mut AudioBlock,
requested_end: i64,
declared_duration: Option<MediaTime>,
sample_rate: u32,
) {
let window_start = window.start_sample;
let window_end = window_start.saturating_add(audio_block_frames(window));
let Some(declared_end) =
declared_duration.map(|duration| ticks_to_samples_ceil(duration.ticks(), sample_rate))
else {
return;
};
let padded_end = requested_end.min(declared_end.saturating_add(AUDIO_INTERPOLATION_MARGIN));
let Ok(padding) = usize::try_from(padded_end.saturating_sub(window_end).max(0)) else {
return;
};
if padding == 0 {
return;
}
for channel in &mut window.channels {
channel.resize(channel.len().saturating_add(padding), 0.0);
}
}
pub fn trim_audio_window(window: &mut AudioBlock, requested_start: i64) {
let window_start = window.start_sample;
let discard = usize::try_from(requested_start.saturating_sub(window_start).max(0))
.unwrap_or(usize::MAX)
.min(window.channels.first().map_or(0, Vec::len));
if discard == 0 {
return;
}
for channel in &mut window.channels {
channel.drain(..discard.min(channel.len()));
}
window.start_sample = window_start.saturating_add(i64::try_from(discard).unwrap_or(i64::MAX));
}
pub fn ticks_to_samples(ticks: i64, sample_rate: u32) -> i64 {
ticks
.saturating_mul(i64::from(sample_rate))
.div_euclid(TICKS_PER_SECOND)
}
pub fn ticks_to_samples_ceil(ticks: i64, sample_rate: u32) -> i64 {
let numerator = i128::from(ticks).saturating_mul(i128::from(sample_rate));
let denominator = i128::from(TICKS_PER_SECOND);
let samples =
numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
i64::try_from(samples).unwrap_or(i64::MAX)
}
pub fn samples_to_ticks_ceil(samples: i64, sample_rate: u32) -> i64 {
let numerator = i128::from(samples).saturating_mul(i128::from(TICKS_PER_SECOND));
let denominator = i128::from(sample_rate.max(1));
let ticks =
numerator.div_euclid(denominator) + i128::from(numerator.rem_euclid(denominator) != 0);
i64::try_from(ticks).unwrap_or(i64::MAX)
}