use super::header::{GRANULE_RATE, OpusHead};
use super::reader::OggPacket;
use crate::{Error, Result};
use std::ops::Range;
#[derive(Debug, Clone)]
pub struct Trim {
channels: usize,
ticks: u64,
pre_skip_48k: u64,
skip_remaining: u64,
emitted: u64,
}
impl Trim {
pub fn new(head: &OpusHead, sample_rate: i32, channels: usize) -> Result<Self> {
if channels == 0 {
return Err(Error::InvalidArgument("channels must be at least 1"));
}
crate::packet::check_rate(sample_rate)?;
let ticks = u64::from(GRANULE_RATE) / sample_rate as u64;
let pre_skip_48k = u64::from(head.pre_skip);
Ok(Trim {
channels,
ticks,
pre_skip_48k,
skip_remaining: pre_skip_48k.div_ceil(ticks),
emitted: 0,
})
}
pub fn keep<'a, T>(&mut self, packet: &OggPacket, decoded: &'a [T]) -> &'a [T] {
let audio = self.keep_range(packet, decoded.len());
&decoded[audio]
}
pub fn keep_range(&mut self, packet: &OggPacket, decoded_len: usize) -> Range<usize> {
debug_assert_eq!(
decoded_len % self.channels,
0,
"decoded PCM is not a whole number of {}-channel frames",
self.channels
);
let total = (decoded_len / self.channels) as u64;
let start = self.skip_remaining.min(total);
self.skip_remaining -= start;
let mut end = total;
if packet.page_granule >= 0 {
let playable =
(packet.page_granule as u64).saturating_sub(self.pre_skip_48k) / self.ticks;
end = start + playable.saturating_sub(self.emitted).min(end - start);
}
self.emitted += end - start;
start as usize * self.channels..end as usize * self.channels
}
pub fn samples_emitted(&self) -> u64 {
self.emitted
}
}
#[cfg(test)]
mod tests {
use super::*;
fn head(pre_skip: u16, channels: u8) -> OpusHead {
let mut h = OpusHead::new(channels, 48_000).unwrap();
h.pre_skip = pre_skip;
h
}
fn pcm(n: usize, channels: usize) -> Vec<f32> {
(0..n * channels).map(|i| i as f32).collect()
}
#[test]
fn pre_skip_is_taken_from_the_front_of_the_first_packet() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
let kept = t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
assert_eq!(kept.len(), 648);
assert_eq!(kept[0], 312.0);
assert_eq!(t.samples_emitted(), 648);
}
#[test]
fn a_pre_skip_longer_than_one_packet_spans_packets() {
let mut t = Trim::new(&head(3120, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
for i in 1..=3 {
let p = OggPacket::new(vec![0xfc], 960 * i, false);
assert_eq!(t.keep(&p, &block).len(), 0, "packet {i}");
}
let kept = t.keep(&OggPacket::new(vec![0xfc], 3840, false), &block);
assert_eq!(kept.len(), 960 - 240);
assert_eq!(kept[0], 240.0);
}
#[test]
fn the_final_granule_trims_the_end() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
let kept = t.keep(&OggPacket::new(vec![0xfc], 1500, true), &block);
assert_eq!(kept.len(), 1188 - 648);
assert_eq!(
kept[0], 0.0,
"the end-trim takes from the tail, not the head"
);
assert_eq!(t.samples_emitted(), 1188);
}
#[test]
fn an_exact_final_granule_trims_nothing() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
let kept = t.keep(&OggPacket::new(vec![0xfc], 1920, true), &block);
assert_eq!(kept.len(), 960);
assert_eq!(t.samples_emitted(), 1920 - 312);
}
#[test]
fn a_granule_that_over_claims_does_not_extend_the_audio() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
let kept = t.keep(&OggPacket::new(vec![0xfc], 99_999, true), &block);
assert_eq!(kept.len(), 648);
}
#[test]
fn a_final_packet_can_be_trimmed_away_completely() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
let kept = t.keep(&OggPacket::new(vec![0xfc], 960, true), &block);
assert_eq!(kept.len(), 0);
assert_eq!(t.samples_emitted(), 648);
}
#[test]
fn a_stream_shorter_than_its_own_pre_skip_yields_nothing() {
let mut t = Trim::new(&head(3120, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
let kept = t.keep(&OggPacket::new(vec![0xfc], 960, true), &block);
assert_eq!(kept.len(), 0);
assert_eq!(t.samples_emitted(), 0);
}
#[test]
fn an_ordinary_page_granule_clamps_nothing() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
for i in 0..4 {
let p = OggPacket::new(vec![0xfc], 3840, false);
let kept = t.keep(&p, &block);
assert_eq!(kept.len(), if i == 0 { 648 } else { 960 }, "packet {i}");
}
assert_eq!(t.samples_emitted(), 3840 - 312);
}
#[test]
fn the_trim_is_taken_from_an_unflagged_final_page() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
let kept = t.keep(&OggPacket::new(vec![0xfc], 1500, false), &block);
assert_eq!(kept.len(), (1500 - 312) - 648);
assert_eq!(t.samples_emitted(), 1500 - 312);
}
#[test]
fn an_absent_granule_trims_nothing() {
let mut t = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let block = pcm(960, 1);
let kept = t.keep(&OggPacket::new(vec![0xfc], -1, true), &block);
assert_eq!(kept.len(), 648);
}
#[test]
fn keep_range_locates_a_cut_that_a_length_cannot() {
let mut by_range = Trim::new(&head(312, 1), 48_000, 1).unwrap();
let mut by_slice = by_range.clone();
let block = pcm(960, 1);
let only = OggPacket::new(vec![0xfc], 700, true);
let audio = by_range.keep_range(&only, block.len());
assert_eq!(audio, 312..700);
assert_eq!(by_slice.keep(&only, &block), &block[audio]);
assert_eq!(by_range.samples_emitted(), by_slice.samples_emitted());
}
#[test]
fn keep_range_indexes_values_not_frames() {
let mut t = Trim::new(&head(312, 2), 48_000, 2).unwrap();
assert_eq!(
t.keep_range(&OggPacket::new(vec![0xfc], 960, false), 960 * 2),
624..1920
);
}
#[test]
fn stereo_counts_sample_frames_not_values() {
let mut t = Trim::new(&head(312, 2), 48_000, 2).unwrap();
let block = pcm(960, 2);
let kept = t.keep(&OggPacket::new(vec![0xfc], 960, false), &block);
assert_eq!(kept.len(), 648 * 2);
assert_eq!(
kept[0], 624.0,
"trimmed at a frame boundary, not a value one"
);
assert_eq!(t.samples_emitted(), 648);
}
#[test]
fn a_lower_decode_rate_scales_both_ends() {
let mut t = Trim::new(&head(312, 1), 16_000, 1).unwrap();
let block = pcm(320, 1); assert_eq!(
t.keep(&OggPacket::new(vec![0xfc], 960, false), &block)
.len(),
216
);
let kept = t.keep(&OggPacket::new(vec![0xfc], 1500, true), &block);
assert_eq!(kept.len(), 396 - 216);
assert_eq!(t.samples_emitted(), 396);
}
#[test]
fn a_pre_skip_that_does_not_divide_the_rate_still_clamps_nowhere() {
for (rate, pre_skip) in [
(8_000i32, 313u16),
(8_000, 317),
(16_000, 316),
(24_000, 317),
(12_000, 313),
] {
let ticks = 48_000 / rate as u64;
let n = (rate / 50) as usize; let mut t = Trim::new(&head(pre_skip, 1), rate, 1).unwrap();
let block = pcm(n, 1);
let skip = u64::from(pre_skip).div_ceil(ticks) as usize;
for k in 1..=5u64 {
let p = OggPacket::new(vec![0xfc], (k * 960) as i64, k == 5);
let kept = t.keep(&p, &block);
let label = format!("{rate} Hz, pre-skip {pre_skip}: packet {k}");
assert_eq!(
*kept.last().unwrap(),
(n - 1) as f32,
"{label} was clamped at the tail"
);
let want_first = if k == 1 { skip } else { 0 };
assert_eq!(
kept[0], want_first as f32,
"{label} starts in the wrong place"
);
assert_eq!(kept.len(), n - want_first, "{label} length");
}
assert_eq!(t.samples_emitted() as usize, 5 * n - skip);
}
}
#[test]
fn a_rate_opus_cannot_decode_at_is_rejected() {
assert!(Trim::new(&head(312, 1), 44_100, 1).is_err());
assert!(Trim::new(&head(312, 1), 48_000, 0).is_err());
}
}