use std::io::{Cursor, Read, Seek};
use anyhow::{Context, ensure};
use hound::{SampleFormat, WavReader, WavWriter};
pub(crate) fn interval<R: Read + Seek>(
source: R,
start_ms: u64,
end_ms: u64,
) -> anyhow::Result<Vec<u8>> {
let mut reader = WavReader::new(source).context("opening retained WAV audio")?;
let spec = reader.spec();
ensure!(end_ms > start_ms, "audio interval is empty");
let start_frame = frame_at(start_ms, spec.sample_rate)?;
let end_frame = frame_at(end_ms, spec.sample_rate)?.min(reader.duration());
ensure!(
start_frame < reader.duration(),
"audio interval starts beyond the retained WAV"
);
ensure!(
end_frame > start_frame,
"audio interval contains no samples"
);
let sample_count = usize::try_from(
u64::from(end_frame - start_frame)
.checked_mul(u64::from(spec.channels))
.context("audio interval sample count overflowed")?,
)
.context("audio interval is too large for this platform")?;
reader
.seek(start_frame)
.context("seeking retained WAV interval")?;
let mut output = Cursor::new(Vec::new());
{
let mut writer = WavWriter::new(&mut output, spec).context("creating WAV interval")?;
let written = match spec.sample_format {
SampleFormat::Int => {
let mut written = 0;
for sample in reader.samples::<i32>().take(sample_count) {
writer
.write_sample(sample.context("reading retained integer WAV sample")?)
.context("writing integer WAV interval sample")?;
written += 1;
}
written
}
SampleFormat::Float => {
let mut written = 0;
for sample in reader.samples::<f32>().take(sample_count) {
writer
.write_sample(sample.context("reading retained float WAV sample")?)
.context("writing float WAV interval sample")?;
written += 1;
}
written
}
};
ensure!(written == sample_count, "retained WAV interval ended early");
writer.finalize().context("finalizing WAV interval")?;
}
Ok(output.into_inner())
}
fn frame_at(milliseconds: u64, sample_rate: u32) -> anyhow::Result<u32> {
let frame = milliseconds
.checked_mul(u64::from(sample_rate))
.context("audio interval timestamp overflowed")?
/ 1_000;
u32::try_from(frame).context("audio interval exceeds WAV frame limits")
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use hound::{SampleFormat, WavReader, WavSpec, WavWriter};
use super::interval;
fn stereo_wav() -> Vec<u8> {
let mut output = Cursor::new(Vec::new());
{
let mut writer = WavWriter::new(
&mut output,
WavSpec {
channels: 2,
sample_rate: 1_000,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
},
)
.unwrap();
for frame in 0_i16..10 {
writer.write_sample(frame).unwrap();
writer.write_sample(-frame).unwrap();
}
writer.finalize().unwrap();
}
output.into_inner()
}
#[test]
fn returns_only_the_requested_frames_in_the_source_format() {
let bytes = interval(Cursor::new(stereo_wav()), 2, 6).unwrap();
let mut reader = WavReader::new(Cursor::new(bytes)).unwrap();
assert_eq!(reader.spec().channels, 2);
assert_eq!(reader.spec().sample_rate, 1_000);
assert_eq!(reader.spec().bits_per_sample, 16);
assert_eq!(reader.spec().sample_format, SampleFormat::Int);
assert_eq!(reader.duration(), 4);
assert_eq!(
reader
.samples::<i16>()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![2, -2, 3, -3, 4, -4, 5, -5]
);
}
#[test]
fn clips_the_requested_end_and_rejects_an_out_of_range_start() {
let bytes = interval(Cursor::new(stereo_wav()), 8, 20).unwrap();
let reader = WavReader::new(Cursor::new(bytes)).unwrap();
assert_eq!(reader.duration(), 2);
assert!(interval(Cursor::new(stereo_wav()), 10, 11).is_err());
}
}