use cpal::{Error, ErrorKind, SampleFormat};
use super::AudioStream;
use crate::output::SampleType;
pub struct Section<S: SampleType> {
stream: Box<dyn AudioStream<S>>,
start: u64,
end: Option<u64>,
looping: bool,
at: u64,
passes: u64,
}
impl<S: SampleType> Section<S> {
pub fn new(mut stream: Box<dyn AudioStream<S>>, start: u64, end: Option<u64>, looping: bool) -> Result<Self, Error> {
if end.is_some_and(|end| end <= start) {
return Err(Error::with_message(
ErrorKind::InvalidInput,
format!("a section must end after it starts, not at frame {} from {start}", end.unwrap_or(0)),
));
}
let at = if start > 0 { stream.seek(start)? } else { 0 };
Ok(Section { stream, start, end, looping, at, passes: 0 })
}
pub fn passes(&self) -> u64 {
self.passes
}
pub fn pass_frames(&self) -> Option<u64> {
self.end.or(self.stream.total_frames()).map(|end| end.saturating_sub(self.start))
}
fn wrap(&mut self) -> Result<bool, Error> {
if self.at <= self.start {
return Ok(false);
}
self.at = self.stream.seek(self.start)?;
self.passes += 1;
Ok(true)
}
}
impl<S: SampleType> AudioStream<S> for Section<S> {
fn sample_rate(&self) -> u32 {
self.stream.sample_rate()
}
fn channels(&self) -> u16 {
self.stream.channels()
}
fn source_format(&self) -> SampleFormat {
self.stream.source_format()
}
fn total_frames(&self) -> Option<u64> {
if self.looping {
None
} else {
self.pass_frames()
}
}
fn read(&mut self, out: &mut [S]) -> Result<usize, Error> {
let channels = usize::from(self.stream.channels().max(1));
loop {
let mut room = out.len() / channels * channels;
if let Some(end) = self.end {
let left = end.saturating_sub(self.at) as usize;
if left == 0 {
if self.looping && self.wrap()? {
continue;
}
return Ok(0);
}
room = room.min(left.saturating_mul(channels));
}
if room == 0 {
return Ok(0);
}
let read = self.stream.read(&mut out[..room])?;
if read == 0 {
if self.looping && self.wrap()? {
continue;
}
return Ok(0);
}
self.at += (read / channels) as u64;
return Ok(read);
}
}
fn seek(&mut self, frame: u64) -> Result<u64, Error> {
let target = self.start.saturating_add(frame);
let target = self.end.map_or(target, |end| target.min(end));
self.at = self.stream.seek(target)?;
Ok(self.at - self.start)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Ramp {
len: u64,
at: u64,
}
impl AudioStream<f32> for Ramp {
fn sample_rate(&self) -> u32 {
48_000
}
fn channels(&self) -> u16 {
1
}
fn source_format(&self) -> SampleFormat {
SampleFormat::F32
}
fn total_frames(&self) -> Option<u64> {
Some(self.len)
}
fn read(&mut self, out: &mut [f32]) -> Result<usize, Error> {
let count = out.len().min(7).min((self.len - self.at) as usize);
for (offset, slot) in out[..count].iter_mut().enumerate() {
*slot = (self.at + offset as u64) as f32;
}
self.at += count as u64;
Ok(count)
}
fn seek(&mut self, frame: u64) -> Result<u64, Error> {
self.at = frame.min(self.len);
Ok(self.at)
}
}
fn take(section: &mut Section<f32>, frames: usize) -> Vec<u32> {
let mut got = Vec::new();
let mut block = [0.0_f32; 16];
while got.len() < frames {
let read = section.read(&mut block).unwrap();
if read == 0 {
break;
}
got.extend(block[..read].iter().map(|sample| *sample as u32));
}
got.truncate(frames);
got
}
#[test]
fn a_section_plays_from_its_in_to_its_out() {
let mut section = Section::new(Box::new(Ramp { len: 100, at: 0 }), 10, Some(20), false).unwrap();
assert_eq!(take(&mut section, 100), (10..20).collect::<Vec<_>>());
assert_eq!(section.total_frames(), Some(10));
}
#[test]
fn a_looping_section_runs_into_its_start_with_nothing_between() {
let mut section = Section::new(Box::new(Ramp { len: 100, at: 0 }), 10, Some(13), true).unwrap();
assert_eq!(take(&mut section, 9), [10, 11, 12, 10, 11, 12, 10, 11, 12]);
assert_eq!(section.passes(), 2);
assert_eq!(section.total_frames(), None, "a loop never ends");
}
#[test]
fn with_no_out_point_a_loop_wraps_at_the_end() {
let mut section = Section::new(Box::new(Ramp { len: 5, at: 0 }), 3, None, true).unwrap();
assert_eq!(take(&mut section, 6), [3, 4, 3, 4, 3, 4]);
}
#[test]
fn an_out_before_the_in_is_refused() {
assert!(Section::new(Box::new(Ramp { len: 5, at: 0 }), 3, Some(3), false).is_err());
}
}