use std::collections::HashMap;
use std::sync::Arc;
use super::AudioBuffer;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AudioRenderRequest {
start_frame: i64,
frame_count: usize,
rate: u32,
channels: u16,
local_start: f64,
local_step: f64,
}
impl AudioRenderRequest {
pub fn new(start_frame: i64, frame_count: usize, rate: u32, channels: u16) -> Self {
assert!(rate > 0, "audio sample rate must be non-zero");
assert!(channels > 0, "audio channel count must be non-zero");
let local_step = 1.0 / f64::from(rate);
let local_start = start_frame as f64 * local_step;
let request = Self {
start_frame,
frame_count,
rate,
channels,
local_start,
local_step,
};
let _ = request.sample_len();
request
}
pub fn start_frame(self) -> i64 {
self.start_frame
}
pub fn frame_count(self) -> usize {
self.frame_count
}
pub fn rate(self) -> u32 {
self.rate
}
pub fn channels(self) -> u16 {
self.channels
}
pub fn local_start(self) -> f64 {
self.local_start
}
pub fn local_step(self) -> f64 {
self.local_step
}
pub fn sample_len(self) -> usize {
self.frame_count
.checked_mul(usize::from(self.channels))
.expect("audio block sample count overflow")
}
pub fn time_at(self, frame_offset: usize) -> f64 {
assert!(
frame_offset <= self.frame_count,
"audio frame offset {frame_offset} exceeds request length {}",
self.frame_count
);
(frame_offset as f64).mul_add(self.local_step, self.local_start)
}
pub fn may_overlap_local(self, start: f64, end: f64) -> bool {
if self.frame_count == 0 || !start.is_finite() || !end.is_finite() || end <= start {
return false;
}
let first = self.time_at(0);
let last = self.time_at(self.frame_count - 1);
let (earliest, latest) = if first <= last {
(first, last)
} else {
(last, first)
};
earliest < end && latest >= start
}
pub fn with_local_timing(mut self, local_start: f64, local_step: f64) -> Self {
assert!(local_start.is_finite(), "audio local start must be finite");
assert!(local_step.is_finite(), "audio local step must be finite");
self.local_start = local_start;
self.local_step = local_step;
self
}
pub fn shift_local(mut self, delta: f64) -> Self {
assert!(delta.is_finite(), "audio local translation must be finite");
self.local_start += delta;
assert!(
self.local_start.is_finite(),
"translated audio local start must be finite"
);
self
}
pub fn translated(self, delta: f64) -> Self {
self.shift_local(delta)
}
pub fn remapped(self, local_start: f64, local_step: f64) -> Self {
self.with_local_timing(local_start, local_step)
}
pub fn subrange(self, frame_offset: usize, frame_count: usize) -> Self {
let end = frame_offset
.checked_add(frame_count)
.expect("audio subrange overflow");
assert!(
end <= self.frame_count,
"audio subrange {frame_offset}..{end} exceeds request length {}",
self.frame_count
);
let start_delta = i64::try_from(frame_offset).expect("audio frame offset exceeds i64");
Self {
start_frame: self
.start_frame
.checked_add(start_delta)
.expect("audio root frame overflow"),
frame_count,
local_start: self.time_at(frame_offset),
..self
}
}
pub fn expanded(self, left_frames: usize, right_frames: usize) -> Self {
let left = i64::try_from(left_frames).expect("left audio halo exceeds i64");
let frame_count = self
.frame_count
.checked_add(left_frames)
.and_then(|count| count.checked_add(right_frames))
.expect("expanded audio request length overflow");
let local_start = (-(left_frames as f64)).mul_add(self.local_step, self.local_start);
let expanded = Self {
start_frame: self
.start_frame
.checked_sub(left)
.expect("expanded audio root frame underflow"),
frame_count,
local_start,
..self
};
let _ = expanded.sample_len();
expanded
}
}
pub struct AudioBlockMut<'a> {
request: AudioRenderRequest,
samples: &'a mut [f32],
}
impl<'a> AudioBlockMut<'a> {
pub fn new(request: AudioRenderRequest, samples: &'a mut [f32]) -> Self {
assert_eq!(
samples.len(),
request.sample_len(),
"audio block must contain frame_count * channels interleaved samples"
);
Self { request, samples }
}
pub fn request(&self) -> AudioRenderRequest {
self.request
}
pub fn samples(&self) -> &[f32] {
self.samples
}
pub fn samples_mut(&mut self) -> &mut [f32] {
self.samples
}
pub fn clear(&mut self) {
self.samples.fill(0.0);
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ConformedSourceMemoKey {
path: String,
rate: u32,
channels: u16,
gain_bits: u32,
}
#[derive(Debug, Default)]
pub struct AudioRenderContext {
scratch: Vec<Vec<f32>>,
source_durations: HashMap<String, Option<f64>>,
conformed_sources: HashMap<ConformedSourceMemoKey, Option<Arc<AudioBuffer>>>,
}
impl AudioRenderContext {
pub fn new() -> Self {
Self::default()
}
pub fn take_scratch(&mut self, len: usize) -> Vec<f32> {
let mut scratch = self.scratch.pop().unwrap_or_default();
scratch.resize(len, 0.0);
scratch.fill(0.0);
scratch
}
pub fn recycle_scratch(&mut self, mut scratch: Vec<f32>) {
scratch.clear();
self.scratch.push(scratch);
}
pub(crate) fn source_duration(&mut self, path: &str) -> Option<f64> {
self.source_duration_with(path, || crate::audio::decoded_duration(path, None).ok())
}
fn source_duration_with<F>(&mut self, path: &str, load: F) -> Option<f64>
where
F: FnOnce() -> Option<f64>,
{
if let Some(duration) = self.source_durations.get(path) {
return *duration;
}
let duration = load();
self.source_durations.insert(path.to_owned(), duration);
duration
}
pub(crate) fn conformed_source(
&mut self,
path: &str,
rate: u32,
channels: u16,
gain: f32,
) -> Option<Arc<AudioBuffer>> {
let key = ConformedSourceMemoKey {
path: path.to_owned(),
rate,
channels,
gain_bits: gain.to_bits(),
};
self.conformed_source_with(key, || {
crate::audio::conform_file_cached(path, None, rate, channels, gain, 1.0).ok()
})
}
fn conformed_source_with<F>(
&mut self,
key: ConformedSourceMemoKey,
load: F,
) -> Option<Arc<AudioBuffer>>
where
F: FnOnce() -> Option<Arc<AudioBuffer>>,
{
if let Some(source) = self.conformed_sources.get(&key) {
return source.clone();
}
let source = load();
self.conformed_sources.insert(key, source.clone());
source
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn large_offset_keeps_adjacent_48khz_samples_distinct() {
let start_frame = 48_000_i64 * 60 * 60 * 10;
let request = AudioRenderRequest::new(start_frame, 2, 48_000, 2);
let first = request.time_at(0);
let second = request.time_at(1);
let sample_period = 1.0 / 48_000.0;
assert_ne!(first, second);
assert!(((second - first) - sample_period).abs() < 1.0e-11);
assert_eq!(first, start_frame as f64 / 48_000.0);
}
#[test]
fn subrange_and_expansion_preserve_the_affine_mapping() {
let request = AudioRenderRequest::new(100, 20, 1_000, 2).with_local_timing(5.0, 0.002);
let subrange = request.subrange(4, 6);
assert_eq!(subrange.start_frame(), 104);
assert_eq!(subrange.frame_count(), 6);
assert_eq!(subrange.local_start(), request.time_at(4));
assert_eq!(subrange.time_at(6), request.time_at(10));
let expanded = subrange.expanded(2, 3);
assert_eq!(expanded.start_frame(), 102);
assert_eq!(expanded.frame_count(), 11);
assert!((expanded.local_start() - request.time_at(2)).abs() < 1.0e-14);
assert!((expanded.time_at(11) - request.time_at(13)).abs() < 1.0e-14);
let shifted = expanded.shift_local(1.25);
assert_eq!(shifted.start_frame(), expanded.start_frame());
assert_eq!(shifted.local_step(), expanded.local_step());
assert_eq!(shifted.time_at(5), expanded.time_at(5) + 1.25);
let remapped = shifted.remapped(-3.0, 0.5);
assert_eq!(remapped.time_at(0), -3.0);
assert_eq!(remapped.time_at(2), -2.0);
}
#[test]
fn overlap_test_respects_half_open_local_intervals() {
let request = AudioRenderRequest::new(10, 4, 10, 1);
assert!(request.may_overlap_local(1.0, 1.1));
assert!(request.may_overlap_local(1.2, 2.0));
assert!(!request.may_overlap_local(0.0, 1.0));
assert!(!request.may_overlap_local(1.4, 2.0));
assert!(!request.subrange(0, 0).may_overlap_local(0.0, 2.0));
}
#[test]
fn audio_block_enforces_shape_and_exposes_samples() {
let request = AudioRenderRequest::new(0, 3, 48_000, 2);
assert_eq!(request.sample_len(), 6);
let mut samples = vec![1.0_f32; request.sample_len()];
let mut block = AudioBlockMut::new(request, &mut samples);
assert_eq!(block.request(), request);
assert_eq!(block.len(), 6);
assert!(!block.is_empty());
block.samples_mut()[1] = 0.5;
assert_eq!(block.samples()[1], 0.5);
block.clear();
assert!(block.samples().iter().all(|sample| *sample == 0.0));
}
#[test]
#[should_panic(expected = "audio block must contain frame_count * channels")]
fn audio_block_rejects_wrong_interleaved_length() {
let request = AudioRenderRequest::new(0, 3, 48_000, 2);
let mut samples = vec![0.0_f32; 5];
let _ = AudioBlockMut::new(request, &mut samples);
}
#[test]
fn render_context_recycles_zeroed_scratch() {
let mut context = AudioRenderContext::new();
let mut scratch = context.take_scratch(4);
scratch.fill(1.0);
let capacity = scratch.capacity();
context.recycle_scratch(scratch);
let reused = context.take_scratch(3);
assert!(reused.capacity() >= capacity);
assert_eq!(reused, vec![0.0; 3]);
}
#[test]
fn render_context_memoizes_source_duration_success_and_failure() {
let mut context = AudioRenderContext::new();
assert_eq!(
context.source_duration_with("voice.wav", || Some(2.5)),
Some(2.5)
);
assert_eq!(
context.source_duration_with("voice.wav", || panic!("duration reloaded")),
Some(2.5)
);
assert_eq!(context.source_duration_with("missing.wav", || None), None);
assert_eq!(
context.source_duration_with("missing.wav", || panic!("failure retried")),
None
);
}
#[test]
fn render_context_memoizes_conformed_source_success_and_failure() {
let mut context = AudioRenderContext::new();
let key = ConformedSourceMemoKey {
path: "voice.wav".to_owned(),
rate: 48_000,
channels: 2,
gain_bits: 0.5_f32.to_bits(),
};
let source = Arc::new(AudioBuffer {
samples: vec![0.25, 0.25],
rate: 48_000,
channels: 2,
});
let first = context
.conformed_source_with(key.clone(), || Some(Arc::clone(&source)))
.expect("first load succeeds");
let second = context
.conformed_source_with(key, || panic!("source reloaded"))
.expect("memoized load succeeds");
assert!(Arc::ptr_eq(&first, &second));
let missing_key = ConformedSourceMemoKey {
path: "missing.wav".to_owned(),
rate: 48_000,
channels: 2,
gain_bits: 1.0_f32.to_bits(),
};
assert!(context
.conformed_source_with(missing_key.clone(), || None)
.is_none());
assert!(context
.conformed_source_with(missing_key, || panic!("failure retried"))
.is_none());
}
}