use std::collections::HashMap;
use std::ops::Range;
use alpha2_media::{AudioBlock, AudioBlockProvider, AudioSpan, SourceId, SpeakerLayout};
use alpha2_time::{MediaTime, TICKS_PER_SECOND};
const MAX_RETAINED_SECONDS: usize = 600;
pub(crate) const MAX_RETAINED_BYTES: usize = 512 << 20;
const SLIDE_FRACTION: usize = 4;
pub(crate) trait UploadSamples {
fn values(&self) -> usize;
fn append(&self, range: Range<usize>, plane: &mut Vec<f32>);
}
impl UploadSamples for [f32] {
fn values(&self) -> usize {
self.len()
}
fn append(&self, range: Range<usize>, plane: &mut Vec<f32>) {
plane.extend_from_slice(&self[range]);
}
}
pub(crate) struct AudioStore {
sources: HashMap<String, Vec<Window>>,
source_ends: HashMap<String, i64>,
sample_rate: u32,
uploads: u64,
byte_budget: usize,
}
struct Window {
block: AudioBlock,
uploaded: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UploadRefusal {
WrongRate { expected: u32 },
Planes,
TooLarge,
}
impl Default for AudioStore {
fn default() -> Self {
Self::new(0)
}
}
impl AudioStore {
pub(crate) fn new(sample_rate: u32) -> Self {
Self {
sources: HashMap::new(),
source_ends: HashMap::new(),
sample_rate,
uploads: 0,
byte_budget: MAX_RETAINED_BYTES,
}
}
#[cfg(test)]
pub(crate) fn upload(
&mut self,
source: &str,
start_sample: i64,
sample_rate: u32,
speakers: SpeakerLayout,
samples: &[f32],
ends_source: bool,
) -> Result<(), UploadRefusal> {
self.upload_from(
source,
start_sample,
sample_rate,
speakers,
samples,
ends_source,
)
}
pub(crate) fn upload_from<S: UploadSamples + ?Sized>(
&mut self,
source: &str,
start_sample: i64,
sample_rate: u32,
speakers: SpeakerLayout,
samples: &S,
ends_source: bool,
) -> Result<(), UploadRefusal> {
let channels = speakers.channels();
self.admits(sample_rate, channels, samples.values())?;
let frames = samples.values() / channels;
let limit = self.window_frame_limit(channels);
let end = start_sample.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
let (left, right) = self.take_neighbours(source, start_sample, end, speakers);
let room = limit - frames;
let right_frames = right.as_ref().map_or(0, AudioBlock::frames).min(room);
let left_held = left.as_ref().map_or(0, AudioBlock::frames);
let mut left_frames = left_held.min(room - right_frames);
if left_frames < left_held {
left_frames = left_frames.saturating_sub(limit / SLIDE_FRACTION);
}
let needed = left_frames + frames + right_frames;
let frame_bytes = channels.saturating_mul(std::mem::size_of::<f32>());
let free_frames = self.byte_budget.saturating_sub(self.retained_bytes()) / frame_bytes;
let capacity = left.as_ref().map_or(needed, |left| {
let held = left.channels.iter().map(Vec::capacity).min().unwrap_or(0);
if held >= needed {
held
} else {
held.saturating_mul(2)
.min(limit)
.min(free_frames)
.max(needed)
}
});
self.enforce_total(
self.byte_budget
.saturating_sub(capacity.saturating_mul(frame_bytes)),
);
let upload = |channel: usize| channel * frames..(channel + 1) * frames;
let head = |channel: usize| {
right
.as_ref()
.map_or(&[][..], |right| &right.channels[channel][..right_frames])
};
let block = if let Some(mut left) = left {
let dropped = left_held - left_frames;
for (channel, plane) in left.channels.iter_mut().enumerate() {
plane.drain(..dropped);
plane.reserve_exact(capacity.saturating_sub(plane.len()));
samples.append(upload(channel), plane);
plane.extend_from_slice(head(channel));
}
left.start_sample = start_sample - i64::try_from(left_frames).unwrap_or(0);
left
} else {
AudioBlock {
start_sample,
sample_rate,
speakers,
channels: (0..channels)
.map(|channel| {
let mut plane = Vec::with_capacity(capacity);
samples.append(upload(channel), &mut plane);
plane.extend_from_slice(head(channel));
plane
})
.collect(),
}
};
self.uploads = self.uploads.saturating_add(1);
self.sources.entry(source.to_owned()).or_default().insert(
0,
Window {
block,
uploaded: self.uploads,
},
);
self.enforce_total(self.byte_budget);
if ends_source {
self.source_ends.insert(source.to_owned(), end);
}
Ok(())
}
fn take_neighbours(
&mut self,
source: &str,
start: i64,
end: i64,
speakers: SpeakerLayout,
) -> (Option<AudioBlock>, Option<AudioBlock>) {
let Some(windows) = self.sources.get_mut(source) else {
return (None, None);
};
windows.retain(|window| {
let (window_start, window_end) = span(&window.block);
window_end <= start || end <= window_start
});
let sample_rate = self.sample_rate;
let mut meeting = |edge: fn(&AudioBlock) -> i64, at: i64| {
windows
.iter()
.position(|window| {
window.block.speakers == speakers
&& window.block.sample_rate == sample_rate
&& edge(&window.block) == at
})
.map(|index| windows.remove(index).block)
};
let neighbours = (
meeting(AudioBlock::end_sample, start),
meeting(|block| block.start_sample, end),
);
if windows.is_empty() {
self.sources.remove(source);
}
neighbours
}
pub(crate) fn admits(
&self,
sample_rate: u32,
channels: usize,
samples: usize,
) -> Result<(), UploadRefusal> {
if sample_rate != self.sample_rate {
return Err(UploadRefusal::WrongRate {
expected: self.sample_rate,
});
}
if samples == 0 || channels == 0 || !samples.is_multiple_of(channels) {
return Err(UploadRefusal::Planes);
}
if samples / channels > self.window_frame_limit(channels) {
return Err(UploadRefusal::TooLarge);
}
Ok(())
}
fn window_frame_limit(&self, channels: usize) -> usize {
let seconds = usize::try_from(self.sample_rate)
.unwrap_or(usize::MAX)
.saturating_mul(MAX_RETAINED_SECONDS);
let bytes = self.byte_budget / channels.max(1) / std::mem::size_of::<f32>();
seconds.min(bytes)
}
fn enforce_total(&mut self, budget: usize) {
while self.retained_bytes() > budget {
let oldest = self
.sources
.iter()
.flat_map(|(name, windows)| {
windows
.iter()
.enumerate()
.map(move |(index, window)| (window.uploaded, name.clone(), index))
})
.min();
let Some((_, name, index)) = oldest else {
break;
};
if let Some(windows) = self.sources.get_mut(&name) {
windows.remove(index);
if windows.is_empty() {
self.sources.remove(&name);
}
}
}
}
pub(crate) fn retained_bytes(&self) -> usize {
self.sources
.values()
.flatten()
.map(|window| {
window
.block
.channels
.iter()
.map(|plane| plane.capacity().saturating_mul(std::mem::size_of::<f32>()))
.sum::<usize>()
})
.sum()
}
pub(crate) fn release(&mut self, source: &str) -> bool {
self.source_ends.remove(source);
self.sources.remove(source).is_some()
}
pub(crate) fn clear(&mut self) {
self.sources.clear();
self.source_ends.clear();
}
pub(crate) fn retain_sources(&mut self, keep: impl Fn(&str) -> bool) {
self.sources.retain(|source, _| keep(source));
self.source_ends.retain(|source, _| keep(source));
}
pub(crate) const fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub(crate) fn trim_before(&mut self, source: &str, before: MediaTime) {
let rate = self.sample_rate;
let cut = samples_at(before, rate);
if let Some(windows) = self.sources.get_mut(source) {
windows.retain_mut(|Window { block, .. }| {
let (start, end) = span(block);
if cut <= start {
return true;
}
if cut >= end {
return false;
}
let drop = usize::try_from(cut - start).unwrap_or(0);
for plane in &mut block.channels {
plane.drain(..drop.min(plane.len()));
if plane.len() < plane.capacity() / 2 {
let compact = plane.clone();
*plane = compact;
}
}
block.start_sample = cut;
true
});
}
if self.sources.get(source).is_some_and(Vec::is_empty) {
self.sources.remove(source);
}
}
pub(crate) fn windows(&self, source: &str) -> Vec<(i64, i64)> {
self.sources.get(source).map_or_else(Vec::new, |windows| {
windows.iter().map(|window| span(&window.block)).collect()
})
}
pub(crate) fn retained_frames(&self) -> usize {
self.sources
.values()
.flatten()
.map(|window| block_frames(&window.block))
.sum()
}
}
impl AudioBlockProvider for AudioStore {
fn block(&self, source: &SourceId, start_sample: i64, frames: usize) -> Option<AudioSpan<'_>> {
let windows = self.sources.get(source.as_str())?;
let request_start = start_sample;
let request_end = request_start.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX));
let blocks = windows.iter().map(|window| &window.block);
blocks
.clone()
.find(|block| {
let (start, end) = span(block);
start <= request_start && request_end <= end
})
.or_else(|| {
blocks.clone().find(|block| {
let (start, end) = span(block);
start <= request_start && request_start < end
})
})
.or_else(|| {
blocks
.filter(|block| {
let (start, _) = span(block);
request_start < start && start < request_end
})
.min_by_key(|block| block.start_sample)
})
.map(|block| AudioSpan {
block,
starts_source: block.start_sample <= 0,
ends_source: self
.source_ends
.get(source.as_str())
.is_some_and(|end| block.end_sample() >= *end),
})
}
}
fn block_frames(block: &AudioBlock) -> usize {
block.frames()
}
fn span(block: &AudioBlock) -> (i64, i64) {
(block.start_sample, block.end_sample())
}
fn samples_at(time: MediaTime, sample_rate: u32) -> i64 {
let rate = i128::from(sample_rate.max(1));
let value = (i128::from(time.ticks()) * rate).div_euclid(i128::from(TICKS_PER_SECOND));
i64::try_from(value).unwrap_or(i64::MAX)
}
#[cfg(test)]
#[path = "audio_store_tests.rs"]
mod tests;