use crate::{Cancellation, Error, Result, recording::SAMPLE_RATE};
use block2::RcBlock;
use cpal::{FromSample, Sample};
use objc2::{
AnyThread,
rc::{Retained, autoreleasepool},
};
use objc2_avf_audio::{
AVAudioBuffer, AVAudioConverter, AVAudioConverterInputStatus, AVAudioConverterOutputStatus,
AVAudioFile, AVAudioFormat, AVAudioPCMBuffer, AVAudioQuality,
AVSampleRateConverterAlgorithm_Mastering,
};
use objc2_foundation::{NSError, NSURL};
use std::{
cell::Cell,
collections::VecDeque,
fs::File,
io::{BufReader, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
ptr::NonNull,
};
const NATIVE_FRAMES: u32 = 16384;
const OUTPUT_FRAMES: u32 = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PcmFormat {
pub sample_rate: u32,
pub channels: u16,
}
impl PcmFormat {
fn validate(self) -> Result<()> {
if !(8000..=192000).contains(&self.sample_rate) || !(1..=32).contains(&self.channels) {
return Err(Error::Invalid(format!(
"Unsupported PCM format: sample_rate={}, channels={}",
self.sample_rate, self.channels
)));
}
Ok(())
}
fn native(self) -> Result<Retained<AVAudioFormat>> {
self.validate()?;
unsafe {
AVAudioFormat::initStandardFormatWithSampleRate_channels(
AVAudioFormat::alloc(),
self.sample_rate as f64,
self.channels as u32,
)
}
.ok_or_else(|| Error::Invalid(format!("Cannot create PCM format: {self:?}")))
}
}
#[derive(Clone, Copy)]
pub enum PcmInput<'a, S: Sample> {
Interleaved(&'a [S]),
Planar(&'a [&'a [S]]),
}
impl<S: Sample> PcmInput<'_, S>
where
f32: FromSample<S>,
{
fn frames(self, format: PcmFormat) -> Result<usize> {
let channels = format.channels as usize;
let frames = match self {
Self::Interleaved(samples) => {
if !samples.len().is_multiple_of(channels)
|| samples.iter().any(|v| !f32::from_sample(*v).is_finite())
{
return Err(Error::Invalid(format!(
"Invalid interleaved PCM: format={format:?}, samples={}",
samples.len()
)));
}
samples.len() / channels
}
Self::Planar(planes) => {
let frames = planes.first().map_or(0, |p| p.len());
if planes.len() != channels
|| planes.iter().any(|p| {
p.len() != frames || p.iter().any(|v| !f32::from_sample(*v).is_finite())
})
{
return Err(Error::Invalid(format!(
"Invalid planar PCM: format={format:?}, planes={}, first_plane_frames={frames}",
planes.len()
)));
}
frames
}
};
Ok(frames)
}
fn sample(self, channel: usize, frame: usize, channels: usize) -> f32 {
match self {
Self::Interleaved(samples) => f32::from_sample(samples[frame * channels + channel]),
Self::Planar(planes) => f32::from_sample(planes[channel][frame]),
}
}
}
pub(crate) fn mean_channels(samples: impl Iterator<Item = f32>, channels: usize) -> f32 {
samples.map(f64::from).sum::<f64>() as f32 / channels as f32
}
fn pcm_buffer(format: &AVAudioFormat, capacity: u32) -> Result<Retained<AVAudioPCMBuffer>> {
unsafe {
AVAudioPCMBuffer::initWithPCMFormat_frameCapacity(
AVAudioPCMBuffer::alloc(),
format,
capacity,
)
}
.ok_or_else(|| {
Error::Invalid(format!(
"Cannot allocate PCM buffer: format={format:?}, frames={capacity}"
))
})
}
fn conversion_error(operation: &str, error: &NSError) -> Error {
Error::Invalid(format!(
"Audio {operation} failed: domain={}, code={}, description={error}",
error.domain(),
error.code()
))
}
fn float_plane(buffer: &AVAudioPCMBuffer, channel: usize) -> Result<NonNull<f32>> {
unsafe {
let planes = buffer.floatChannelData();
if planes.is_null() {
return Err(Error::Invalid(
"PCM buffer has no Float32 channel storage".to_owned(),
));
}
Ok(*planes.add(channel))
}
}
pub struct AudioConverter {
format: PcmFormat,
input_format: Retained<AVAudioFormat>,
converter: Retained<AVAudioConverter>,
output: Retained<AVAudioPCMBuffer>,
finished: bool,
}
impl AudioConverter {
pub fn new(format: PcmFormat) -> Result<Self> {
format.validate()?;
Self::with_format(format)
}
fn with_native(input_format: Retained<AVAudioFormat>) -> Result<Self> {
let rate = unsafe { input_format.sampleRate() };
let channels = unsafe { input_format.channelCount() };
if !rate.is_finite()
|| rate.fract() != 0.0
|| rate > u32::MAX as f64
|| channels > u16::MAX as u32
{
return Err(Error::Invalid(format!(
"Unsupported decoded PCM: sample_rate={rate}, channels={channels}"
)));
}
let format = PcmFormat {
sample_rate: rate as u32,
channels: channels as u16,
};
Self::with_format(format)
}
fn with_format(format: PcmFormat) -> Result<Self> {
format.validate()?;
let input_format = PcmFormat {
sample_rate: format.sample_rate,
channels: 1,
}
.native()?;
let target = PcmFormat {
sample_rate: SAMPLE_RATE,
channels: 1,
}
.native()?;
let converter = unsafe { AVAudioConverter::initFromFormat_toFormat(AVAudioConverter::alloc(), &input_format, &target) }
.ok_or_else(|| Error::Invalid(format!("Cannot create audio converter: source={format:?}, target_rate={SAMPLE_RATE}, target_channels=1")))?;
unsafe {
converter.setSampleRateConverterAlgorithm(AVSampleRateConverterAlgorithm_Mastering);
converter.setSampleRateConverterQuality(AVAudioQuality::Max.0);
}
let output = pcm_buffer(&target, OUTPUT_FRAMES)?;
Ok(Self {
format,
input_format,
converter,
output,
finished: false,
})
}
pub fn input_format(&self) -> PcmFormat {
self.format
}
pub fn push<S: Sample>(
&mut self,
input: PcmInput<'_, S>,
cancellation: &Cancellation,
) -> Result<Vec<f32>>
where
f32: FromSample<S>,
{
self.check_open(cancellation)?;
let frames = input.frames(self.format)?;
if frames > self.format.sample_rate as usize {
return Err(Error::Invalid(format!(
"PCM push exceeds one second: format={:?}, frames={frames}",
self.format
)));
}
if frames == 0 {
return Ok(Vec::new());
}
let buffer = pcm_buffer(&self.input_format, frames as u32)?;
let plane = float_plane(&buffer, 0)?;
for frame in 0..frames {
let value = mean_channels(
(0..self.format.channels as usize)
.map(|channel| input.sample(channel, frame, self.format.channels as usize)),
self.format.channels as usize,
);
if !value.is_finite() {
return Err(Error::Invalid(format!(
"PCM channel average is non-finite: format={:?}, frame={frame}",
self.format
)));
}
unsafe {
plane.as_ptr().add(frame).write(value);
}
}
unsafe {
buffer.setFrameLength(frames as u32);
}
self.push_mono(&buffer, cancellation)
}
fn check_open(&self, cancellation: &Cancellation) -> Result<()> {
cancellation.check()?;
if self.finished {
return Err(Error::Invalid(
"Audio converter is finished; create one for the next stream".to_owned(),
));
}
Ok(())
}
fn push_mono(
&mut self,
buffer: &AVAudioPCMBuffer,
cancellation: &Cancellation,
) -> Result<Vec<f32>> {
self.check_open(cancellation)?;
let supplied = Cell::new(false);
let input = RcBlock::new(
|_: u32, mut status: NonNull<AVAudioConverterInputStatus>| -> *mut AVAudioBuffer {
if supplied.replace(true) {
unsafe {
*status.as_mut() = AVAudioConverterInputStatus::NoDataNow;
}
std::ptr::null_mut()
} else {
unsafe {
*status.as_mut() = AVAudioConverterInputStatus::HaveData;
}
(buffer as *const AVAudioPCMBuffer).cast_mut().cast()
}
},
);
let result = self.drain(&input, cancellation);
if result.is_err() {
self.finished = true;
}
result
}
pub fn finish(&mut self, cancellation: &Cancellation) -> Result<Vec<f32>> {
self.check_open(cancellation)?;
self.finished = true;
let input = RcBlock::new(
|_: u32, mut status: NonNull<AVAudioConverterInputStatus>| -> *mut AVAudioBuffer {
unsafe {
*status.as_mut() = AVAudioConverterInputStatus::EndOfStream;
}
std::ptr::null_mut()
},
);
self.drain(&input, cancellation)
}
fn drain(
&self,
input: &block2::DynBlock<
dyn Fn(u32, NonNull<AVAudioConverterInputStatus>) -> *mut AVAudioBuffer + '_,
>,
cancellation: &Cancellation,
) -> Result<Vec<f32>> {
let mut samples = Vec::new();
loop {
cancellation.check()?;
unsafe {
self.output.setFrameLength(0);
}
let mut error = None;
let status = unsafe {
self.converter.convertToBuffer_error_withInputFromBlock(
&self.output,
Some(&mut error),
input as *const _ as *mut _,
)
};
if let Some(error) = error {
return Err(conversion_error("conversion", &error));
}
if status == AVAudioConverterOutputStatus::Error {
return Err(Error::Invalid(format!(
"Audio conversion failed without NSError: source={:?}, status={status:?}",
self.format
)));
}
let count = unsafe { self.output.frameLength() } as usize;
let plane = float_plane(&self.output, 0)?;
let block = unsafe { std::slice::from_raw_parts(plane.as_ptr(), count) };
if block.iter().any(|v| !v.is_finite()) {
return Err(Error::Invalid(
"Audio converter produced non-finite output".to_owned(),
));
}
samples.extend_from_slice(block);
cancellation.check()?;
match status {
AVAudioConverterOutputStatus::InputRanDry
| AVAudioConverterOutputStatus::EndOfStream => return Ok(samples),
AVAudioConverterOutputStatus::HaveData if count > 0 => {}
_ => {
return Err(Error::Invalid(format!(
"Audio converter made no progress: source={:?}, status={status:?}, frames={count}",
self.format
)));
}
}
}
}
}
pub fn convert_pcm<S: Sample>(
input: PcmInput<'_, S>,
format: PcmFormat,
cancellation: &Cancellation,
) -> Result<Vec<f32>>
where
f32: FromSample<S>,
{
format.validate()?;
cancellation.check()?;
let frames = input.frames(format)?;
let mut converter = AudioConverter::new(format)?;
let mut output = Vec::new();
for start in (0..frames).step_by(format.sample_rate as usize) {
let end = (start + format.sample_rate as usize).min(frames);
match input {
PcmInput::Interleaved(samples) => output.extend(converter.push(
PcmInput::Interleaved(
&samples[start * format.channels as usize..end * format.channels as usize],
),
cancellation,
)?),
PcmInput::Planar(planes) => {
let slices: Vec<&[S]> = planes.iter().map(|p| &p[start..end]).collect();
output.extend(converter.push(PcmInput::Planar(&slices), cancellation)?);
}
}
}
output.extend(converter.finish(cancellation)?);
Ok(output)
}
pub struct AudioFileReader {
file: Retained<AVAudioFile>,
path: PathBuf,
converter: AudioConverter,
input: Retained<AVAudioPCMBuffer>,
pending: VecDeque<f32>,
failure: Option<String>,
finished: bool,
total_samples: u64,
}
impl AudioFileReader {
pub fn open(path: &Path) -> Result<Self> {
autoreleasepool(|_| {
let url = NSURL::from_file_path(path)
.ok_or_else(|| Error::Invalid(format!("Invalid audio path: {}", path.display())))?;
let file = unsafe { AVAudioFile::initForReading_error(AVAudioFile::alloc(), &url) }
.map_err(|e| {
Error::Invalid(format!(
"Open audio file: path={}, error={}",
path.display(),
conversion_error("file open", &e)
))
})?;
let format = unsafe { file.processingFormat() };
let converter = AudioConverter::with_native(format.clone())?;
let input = pcm_buffer(&format, NATIVE_FRAMES)?;
Ok(Self {
file,
path: path.into(),
converter,
input,
pending: VecDeque::new(),
failure: None,
finished: false,
total_samples: 0,
})
})
}
pub fn input_format(&self) -> PcmFormat {
self.converter.input_format()
}
pub fn total_samples(&self) -> u64 {
self.total_samples
}
pub fn read_block(&mut self, count: usize, cancellation: &Cancellation) -> Result<Vec<f32>> {
cancellation.check()?;
if count == 0 || count > SAMPLE_RATE as usize {
return Err(Error::Invalid(format!(
"Audio read block must contain 1..={SAMPLE_RATE} samples: requested={count}"
)));
}
if let Some(error) = &self.failure {
return Err(Error::Invalid(format!(
"Audio reader is failed: path={}, previous_error={error}",
self.path.display()
)));
}
let result = self.read_decoded_block(count, cancellation);
if let Err(error) = &result {
self.failure = Some(error.to_string());
}
result
}
fn read_decoded_block(
&mut self,
count: usize,
cancellation: &Cancellation,
) -> Result<Vec<f32>> {
while self.pending.len() < count && !self.finished {
cancellation.check()?;
let remaining = unsafe { self.file.length() - self.file.framePosition() };
if remaining <= 0 {
self.end(cancellation)?;
break;
}
let requested = remaining
.min(NATIVE_FRAMES as i64)
.min(self.converter.input_format().sample_rate as i64)
as u32;
unsafe {
self.input.setFrameLength(0);
}
unsafe {
self.file
.readIntoBuffer_frameCount_error(&self.input, requested)
}
.map_err(|e| {
Error::Invalid(format!(
"Read audio file: path={}, frame_position={}, requested={requested}, error={}",
self.path.display(),
unsafe { self.file.framePosition() },
conversion_error("file read", &e)
))
})?;
let received = unsafe { self.input.frameLength() };
if received > 0 {
let planes: Result<Vec<&[f32]>> = (0..self.converter.input_format().channels
as usize)
.map(|channel| {
let pointer = float_plane(&self.input, channel)?;
Ok(unsafe {
std::slice::from_raw_parts(pointer.as_ptr(), received as usize)
})
})
.collect();
self.pending.extend(
self.converter
.push(PcmInput::Planar(&planes?), cancellation)?,
);
}
if received == 0 {
self.end(cancellation)?;
}
}
let received = count.min(self.pending.len());
let output: Vec<f32> = self.pending.drain(..received).collect();
self.total_samples = self
.total_samples
.checked_add(output.len() as u64)
.ok_or_else(|| Error::Invalid("Decoded audio sample count overflow".to_owned()))?;
Ok(output)
}
fn end(&mut self, cancellation: &Cancellation) -> Result<()> {
self.finished = true;
self.pending.extend(self.converter.finish(cancellation)?);
Ok(())
}
}
fn convert_file_blocks<F>(path: &Path, cancellation: &Cancellation, mut consume: F) -> Result<u64>
where
F: FnMut(&[f32]) -> Result<()>,
{
cancellation.check()?;
let mut reader = AudioFileReader::open(path)?;
loop {
let samples = reader.read_block(4096, cancellation)?;
if samples.is_empty() {
break;
}
consume(&samples)?;
}
cancellation.check()?;
Ok(reader.total_samples())
}
pub fn convert_file(path: &Path, cancellation: &Cancellation) -> Result<Vec<f32>> {
let mut samples = Vec::new();
convert_file_blocks(path, cancellation, |block| {
samples.extend_from_slice(block);
Ok(())
})?;
Ok(samples)
}
pub trait AudioSampleSource {
fn origin(&self) -> Option<&Path>;
fn total_samples(&self) -> u64;
fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>>;
fn read_block(&mut self, count: usize) -> Result<Vec<f32>>;
}
impl<S: AudioSampleSource + ?Sized> AudioSampleSource for &mut S {
fn origin(&self) -> Option<&Path> {
(**self).origin()
}
fn total_samples(&self) -> u64 {
(**self).total_samples()
}
fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>> {
(**self).read_window(start, count)
}
fn read_block(&mut self, count: usize) -> Result<Vec<f32>> {
(**self).read_block(count)
}
}
pub struct WavSource {
reader: hound::WavReader<BufReader<File>>,
path: PathBuf,
total_samples: u64,
}
impl WavSource {
pub fn open(path: &Path) -> Result<Self> {
let reader = hound::WavReader::open(path).map_err(|source| Error::Wav {
path: path.into(),
source,
})?;
let spec = reader.spec();
if spec.sample_rate != SAMPLE_RATE
|| spec.channels != 1
|| !matches!(
(spec.sample_format, spec.bits_per_sample),
(hound::SampleFormat::Float, 32) | (hound::SampleFormat::Int, 16)
)
{
return Err(Error::Invalid(format!(
"Expected mono 16000 Hz float32/PCM16 WAV: path={}, actual={spec:?}",
path.display()
)));
}
let total_samples = reader.duration() as u64;
Ok(Self {
reader,
path: path.into(),
total_samples,
})
}
fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>> {
let offset = u32::try_from(start).map_err(|_| {
Error::Invalid(format!(
"WAV seek offset exceeds format limit: path={}, start={start}",
self.path.display()
))
})?;
self.reader.seek(offset).map_err(|source| Error::Wav {
path: self.path.clone(),
source: hound::Error::IoError(source),
})?;
let audio = self.read_block(count)?;
if audio.len() != count {
return Err(Error::Invalid(format!(
"WAV ended before requested window: path={}, start={start}, requested={count}, received={}",
self.path.display(),
audio.len()
)));
}
Ok(audio)
}
pub fn total_samples(&self) -> u64 {
self.total_samples
}
pub fn read_block(&mut self, count: usize) -> Result<Vec<f32>> {
if count == 0 {
return Err(Error::Invalid("WAV block length must be positive".into()));
}
let result = match self.reader.spec().sample_format {
hound::SampleFormat::Float => self
.reader
.samples::<f32>()
.take(count)
.collect::<std::result::Result<Vec<_>, _>>(),
hound::SampleFormat::Int => self
.reader
.samples::<i16>()
.take(count)
.map(|v| v.map(|s| s as f32 / 32768.0))
.collect(),
};
result.map_err(|source| Error::Wav {
path: self.path.clone(),
source,
})
}
}
impl AudioSampleSource for WavSource {
fn origin(&self) -> Option<&Path> {
Some(&self.path)
}
fn total_samples(&self) -> u64 {
self.total_samples()
}
fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>> {
self.read_window(start, count)
}
fn read_block(&mut self, count: usize) -> Result<Vec<f32>> {
self.read_block(count)
}
}
fn is_direct_pcm_wav(file: &mut File, path: &Path) -> Result<bool> {
let io_error = |source| Error::Io {
path: path.into(),
source,
};
let size = file.metadata().map_err(io_error)?.len();
let mut offset = 12u64;
while offset.checked_add(8).is_some_and(|end| end <= size) {
file.seek(SeekFrom::Start(offset)).map_err(io_error)?;
let mut header = [0; 8];
file.read_exact(&mut header).map_err(io_error)?;
let length = u32::from_le_bytes([header[4], header[5], header[6], header[7]]) as u64;
if &header[..4] == b"fmt " {
if length < 16 || offset.checked_add(24).is_none_or(|end| end > size) {
return Err(Error::Invalid(format!(
"Incomplete WAV format chunk: path={}, offset={offset}, length={length}, file_bytes={size}",
path.display()
)));
}
let mut format = [0; 16];
file.read_exact(&mut format).map_err(io_error)?;
let mut tag = u16::from_le_bytes([format[0], format[1]]);
if tag == 0xfffe && length >= 40 {
let mut extension = [0; 24];
file.read_exact(&mut extension).map_err(io_error)?;
let standard_guid_tail =
[0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71];
if u16::from_le_bytes([extension[0], extension[1]]) >= 22
&& extension[10..] == standard_guid_tail
{
tag = u16::from_le_bytes([extension[8], extension[9]]);
}
}
let channels = u16::from_le_bytes([format[2], format[3]]);
let rate = u32::from_le_bytes([format[4], format[5], format[6], format[7]]);
let bits = u16::from_le_bytes([format[14], format[15]]);
return Ok(channels == 1
&& rate == SAMPLE_RATE
&& matches!((tag, bits), (1, 16) | (3, 32)));
}
offset = offset
.checked_add(8 + length + (length % 2))
.ok_or_else(|| {
Error::Invalid(format!(
"WAV chunk offset overflow: path={}, offset={offset}, length={length}",
path.display()
))
})?;
}
Ok(false)
}
enum Storage {
Memory(Vec<f32>),
Wav(WavSource),
Converted(File),
}
pub struct AudioSource {
origin: Option<PathBuf>,
storage: Storage,
total_samples: u64,
cursor: u64,
}
impl AudioSource {
pub fn from_samples(samples: Vec<f32>) -> Result<Self> {
if samples.iter().any(|v| !v.is_finite()) {
return Err(Error::Invalid(format!(
"Audio source contains non-finite samples: samples={}",
samples.len()
)));
}
Ok(Self {
origin: None,
total_samples: samples.len() as u64,
storage: Storage::Memory(samples),
cursor: 0,
})
}
pub fn open(path: &Path, cancellation: &Cancellation) -> Result<Self> {
cancellation.check()?;
let mut file = File::open(path).map_err(|source| Error::Io {
path: path.into(),
source,
})?;
let mut magic = [0; 12];
let count = file.read(&mut magic).map_err(|source| Error::Io {
path: path.into(),
source,
})?;
if count == 12
&& &magic[..4] == b"RIFF"
&& &magic[8..] == b"WAVE"
&& is_direct_pcm_wav(&mut file, path)?
{
let source = WavSource::open(path)?;
return Ok(Self {
origin: Some(path.into()),
total_samples: source.total_samples(),
storage: Storage::Wav(source),
cursor: 0,
});
}
let mut converted = tempfile::tempfile().map_err(|source| Error::Io {
path: std::env::temp_dir(),
source,
})?;
let total_samples = convert_file_blocks(path, cancellation, |samples| {
let bytes: Vec<u8> = samples.iter().flat_map(|v| v.to_le_bytes()).collect();
converted.write_all(&bytes).map_err(|source| Error::Io {
path: PathBuf::from("<temporary converted audio>"),
source,
})
})?;
converted.flush().map_err(|source| Error::Io {
path: PathBuf::from("<temporary converted audio>"),
source,
})?;
Ok(Self {
origin: Some(path.into()),
storage: Storage::Converted(converted),
total_samples,
cursor: 0,
})
}
pub fn total_samples(&self) -> u64 {
self.total_samples
}
pub fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>> {
let end = start.checked_add(count as u64).ok_or_else(|| {
Error::Invalid(format!(
"Audio window overflow: start={start}, count={count}"
))
})?;
if count == 0 || end > self.total_samples {
return Err(Error::Invalid(format!(
"Audio window outside source: start={start}, count={count}, total={}",
self.total_samples
)));
}
let samples = match &mut self.storage {
Storage::Memory(samples) => samples[start as usize..end as usize].to_vec(),
Storage::Wav(source) => source.read_window(start, count)?,
Storage::Converted(file) => {
let offset = start.checked_mul(4).ok_or_else(|| {
Error::Invalid("Converted audio byte offset overflow".to_owned())
})?;
let bytes_len = count.checked_mul(4).ok_or_else(|| {
Error::Invalid("Converted audio read size overflow".to_owned())
})?;
let mut bytes = vec![0; bytes_len];
file.seek(SeekFrom::Start(offset))
.and_then(|_| file.read_exact(&mut bytes))
.map_err(|source| Error::Io {
path: PathBuf::from("<temporary converted audio>"),
source,
})?;
bytes
.chunks_exact(4)
.map(|v| f32::from_le_bytes([v[0], v[1], v[2], v[3]]))
.collect()
}
};
if samples.iter().any(|v| !v.is_finite()) {
return Err(Error::Invalid(format!(
"Non-finite audio source window: start={start}, count={count}"
)));
}
self.cursor = end;
Ok(samples)
}
pub fn read_block(&mut self, count: usize) -> Result<Vec<f32>> {
if count == 0 {
return Err(Error::Invalid(
"Audio block length must be positive".to_owned(),
));
}
let remaining = self.total_samples - self.cursor;
if remaining == 0 {
return Ok(Vec::new());
}
self.read_window(self.cursor, count.min(remaining as usize))
}
}
impl AudioSampleSource for AudioSource {
fn origin(&self) -> Option<&Path> {
self.origin.as_deref()
}
fn total_samples(&self) -> u64 {
self.total_samples()
}
fn read_window(&mut self, start: u64, count: usize) -> Result<Vec<f32>> {
self.read_window(start, count)
}
fn read_block(&mut self, count: usize) -> Result<Vec<f32>> {
self.read_block(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tone(rate: u32, channels: u16, frames: usize) -> Vec<f32> {
(0..frames)
.flat_map(|i| {
(0..channels).map(move |c| {
(std::f32::consts::TAU * (440.0 + c as f32 * 220.0) * i as f32 / rate as f32)
.sin()
* 0.25
})
})
.collect()
}
fn wav(samples: &[f32], rate: u32, channels: u16) -> tempfile::NamedTempFile {
let file = tempfile::Builder::new().suffix(".wav").tempfile().unwrap();
let spec = hound::WavSpec {
channels,
sample_rate: rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
};
let mut writer = hound::WavWriter::create(file.path(), spec).unwrap();
for v in samples {
writer.write_sample(*v).unwrap();
}
writer.finalize().unwrap();
file
}
#[test]
fn canonical_pcm_keeps_amplitude_samples_and_empty_streams_exact() {
let format = PcmFormat {
sample_rate: 16000,
channels: 1,
};
let token = Cancellation::new();
let input = tone(16000, 1, 16000 + 173);
assert_eq!(
convert_pcm(PcmInput::Interleaved(&input), format, &token).unwrap(),
input
);
let mut converter = AudioConverter::new(format).unwrap();
assert!(
converter
.push(PcmInput::Interleaved(&[] as &[f32]), &token)
.unwrap()
.is_empty()
);
assert!(converter.finish(&token).unwrap().is_empty());
assert!(converter.finish(&token).is_err());
assert!(
converter
.push(PcmInput::Interleaved(&[0.0]), &token)
.is_err()
);
}
#[test]
fn resampling_is_independent_of_packet_size_and_preserves_tail() {
let token = Cancellation::new();
for rate in [8000, 22050, 44100, 48000, 96000] {
let input = tone(rate, 1, rate as usize + 173);
let format = PcmFormat {
sample_rate: rate,
channels: 1,
};
let expected = convert_pcm(PcmInput::Interleaved(&input), format, &token).unwrap();
assert!(
expected.len().abs_diff(input.len() * 16000 / rate as usize) <= 1,
"rate={rate}, count={}",
expected.len()
);
for packet in [1, 997, 4096] {
let mut converter = AudioConverter::new(format).unwrap();
let mut output = Vec::new();
for chunk in input.chunks(packet.min(rate as usize)) {
output.extend(
converter
.push(PcmInput::Interleaved(chunk), &token)
.unwrap(),
);
}
output.extend(converter.finish(&token).unwrap());
assert_eq!(output, expected, "rate={rate}, packet={packet}");
}
let start = expected.len() - 1000;
let rms = (start..expected.len() - 100)
.map(|i| {
let target = (std::f32::consts::TAU * 440.0 * i as f32 / 16000.0).sin() * 0.25;
(expected[i] - target).powi(2)
})
.sum::<f32>()
/ 900.0;
assert!(rms.sqrt() < 0.005, "rate={rate}, rms={}", rms.sqrt());
}
}
#[test]
fn planar_and_interleaved_stereo_have_identical_mono_output() {
let input = tone(48000, 2, 48000 + 37);
let planes: Vec<Vec<f32>> = (0..2)
.map(|c| input.chunks_exact(2).map(|v| v[c]).collect())
.collect();
let refs: Vec<&[f32]> = planes.iter().map(Vec::as_slice).collect();
let format = PcmFormat {
sample_rate: 48000,
channels: 2,
};
let token = Cancellation::new();
assert_eq!(
convert_pcm(PcmInput::Interleaved(&input), format, &token).unwrap(),
convert_pcm(PcmInput::Planar(&refs), format, &token).unwrap()
);
}
#[test]
fn invalid_pcm_is_rejected_before_mutating_converter_state() {
let token = Cancellation::new();
let format = PcmFormat {
sample_rate: 44100,
channels: 2,
};
let mut converter = AudioConverter::new(format).unwrap();
for input in [vec![0.1], vec![f32::NAN, 0.0], vec![0.0; 88202]] {
assert!(
converter
.push(PcmInput::Interleaved(&input), &token)
.is_err()
);
}
assert!(
converter
.push(PcmInput::Planar(&[&[0.0], &[0.0, 0.0]]), &token)
.is_err()
);
let valid = tone(44100, 2, 1000);
let mut output = converter
.push(PcmInput::Interleaved(&valid), &token)
.unwrap();
output.extend(converter.finish(&token).unwrap());
assert_eq!(
output,
convert_pcm(PcmInput::Interleaved(&valid), format, &token).unwrap()
);
for format in [
PcmFormat {
sample_rate: 0,
channels: 1,
},
PcmFormat {
sample_rate: 16000,
channels: 0,
},
PcmFormat {
sample_rate: 16000,
channels: 33,
},
] {
assert!(convert_pcm(PcmInput::Interleaved(&[] as &[f32]), format, &token).is_err());
}
}
#[test]
fn direct_wav_and_memory_sources_keep_exact_random_access_and_eof() {
let input = tone(16000, 1, 16000 + 173);
let file = wav(&input, 16000, 1);
let token = Cancellation::new();
for mut source in [
AudioSource::open(file.path(), &token).unwrap(),
AudioSource::from_samples(input.clone()).unwrap(),
] {
assert_eq!(source.total_samples(), input.len() as u64);
assert_eq!(source.read_window(17, 997).unwrap(), input[17..1014]);
assert_eq!(source.read_block(19).unwrap(), input[1014..1033]);
assert!(source.read_window(u64::MAX, 2).is_err());
assert!(source.read_window(0, 0).is_err());
assert!(source.read_block(0).is_err());
assert!(source.read_window(input.len() as u64, 1).is_err());
assert_eq!(
source.read_window(input.len() as u64 - 1, 1).unwrap(),
input[input.len() - 1..]
);
assert!(source.read_block(7).unwrap().is_empty());
}
assert!(matches!(
AudioSource::open(file.path(), &token).unwrap().storage,
Storage::Wav(_)
));
}
#[test]
fn file_conversion_cache_and_block_reader_match_complete_pcm_conversion() {
use std::os::unix::fs::MetadataExt;
let input = tone(48000, 2, 48000 + 173);
let file = wav(&input, 48000, 2);
let token = Cancellation::new();
let expected = convert_pcm(
PcmInput::Interleaved(&input),
PcmFormat {
sample_rate: 48000,
channels: 2,
},
&token,
)
.unwrap();
assert_eq!(convert_file(file.path(), &token).unwrap(), expected);
let mut source = AudioSource::open(file.path(), &token).unwrap();
assert_eq!(source.total_samples(), expected.len() as u64);
match &source.storage {
Storage::Converted(file) => assert_eq!(file.metadata().unwrap().nlink(), 0),
_ => panic!("conversion must use anonymous disk storage"),
};
assert_eq!(
source.read_window(17, expected.len() - 17).unwrap(),
expected[17..]
);
for count in [1, 997, 4096, 16000] {
let mut reader = AudioFileReader::open(file.path()).unwrap();
let mut actual = Vec::new();
loop {
let chunk = reader.read_block(count, &token).unwrap();
if chunk.is_empty() {
break;
}
actual.extend(chunk);
}
assert_eq!(actual, expected);
assert_eq!(reader.total_samples(), expected.len() as u64);
}
}
#[test]
fn cancellation_and_file_failures_remain_explicit() {
let token = Cancellation::new();
token.cancel();
let format = PcmFormat {
sample_rate: 48000,
channels: 1,
};
assert!(matches!(
convert_pcm(PcmInput::Interleaved(&[0.0]), format, &token),
Err(Error::Cancelled)
));
assert!(matches!(
AudioSource::open(Path::new("missing-audio.wav"), &token),
Err(Error::Cancelled)
));
let malformed = tempfile::NamedTempFile::new().unwrap();
assert!(AudioSource::open(malformed.path(), &Cancellation::new()).is_err());
let invalid = wav(&[f32::NAN; 8000], 8000, 1);
assert!(convert_file(invalid.path(), &Cancellation::new()).is_err());
}
#[test]
fn failed_readers_do_not_resume_and_consumer_cancellation_never_succeeds() {
let file = wav(&[f32::NAN; 8000], 8000, 1);
let token = Cancellation::new();
let mut reader = AudioFileReader::open(file.path()).unwrap();
assert!(reader.read_block(4096, &token).is_err());
assert!(
reader
.read_block(4096, &token)
.unwrap_err()
.to_string()
.contains("Audio reader is failed")
);
let input = tone(48000, 2, 96000);
let file = wav(&input, 48000, 2);
let token = Cancellation::new();
let result = convert_file_blocks(file.path(), &token, |_| {
token.cancel();
Ok(())
});
assert!(matches!(result, Err(Error::Cancelled)));
let result = convert_file_blocks(file.path(), &Cancellation::new(), |_| {
Err(Error::Invalid("test consumer failed".to_owned()))
});
assert!(
result
.unwrap_err()
.to_string()
.contains("test consumer failed")
);
}
#[test]
fn compressed_wav_is_decoded_instead_of_rejected_by_the_direct_pcm_probe() {
let mut file = tempfile::Builder::new().suffix(".wav").tempfile().unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(b"RIFF");
bytes.extend_from_slice(&74u32.to_le_bytes());
bytes.extend_from_slice(b"WAVEfmt ");
bytes.extend_from_slice(&16u32.to_le_bytes());
bytes.extend_from_slice(&7u16.to_le_bytes());
bytes.extend_from_slice(&1u16.to_le_bytes());
bytes.extend_from_slice(&8000u32.to_le_bytes());
bytes.extend_from_slice(&8000u32.to_le_bytes());
bytes.extend_from_slice(&1u16.to_le_bytes());
bytes.extend_from_slice(&8u16.to_le_bytes());
bytes.extend_from_slice(b"data");
bytes.extend_from_slice(&37u32.to_le_bytes());
bytes.extend_from_slice(&[255; 37]);
bytes.push(0);
file.write_all(&bytes).unwrap();
let token = Cancellation::new();
let expected = convert_file(file.path(), &token).unwrap();
assert_eq!(expected.len(), 74);
let mut source = AudioSource::open(file.path(), &token).unwrap();
assert_eq!(source.read_window(0, 74).unwrap(), expected);
}
#[test]
fn equal_channel_mix_preserves_right_only_speech_and_supports_multiple_channels() {
let token = Cancellation::new();
for channels in [2u16, 6, 32] {
let input: Vec<f32> = (0..37)
.flat_map(|_| {
(0..channels).map(|channel| if channel == channels - 1 { 0.5 } else { 0.0 })
})
.collect();
let output = convert_pcm(
PcmInput::Interleaved(&input),
PcmFormat {
sample_rate: 16000,
channels,
},
&token,
)
.unwrap();
assert_eq!(output, vec![0.5 / channels as f32; 37]);
}
}
#[test]
fn short_native_reads_before_eof_do_not_drop_float32_audio() {
let samples = tone(16000, 1, 48000 + 173);
let file = wav(&samples, 16000, 1);
let token = Cancellation::new();
assert_eq!(convert_file(file.path(), &token).unwrap(), samples);
let source = AudioSource::open(file.path(), &token).unwrap();
assert!(matches!(source.storage, Storage::Wav(_)));
assert_eq!(source.total_samples(), samples.len() as u64);
}
#[test]
fn integer_pcm_layouts_use_normalized_values_without_peak_scaling() {
let token = Cancellation::new();
let mono = PcmFormat {
sample_rate: 16000,
channels: 1,
};
let signed = [i16::MIN, -16384, 0, 16384, i16::MAX];
let expected: Vec<f32> = signed.iter().map(|v| *v as f32 / 32768.0).collect();
assert_eq!(
convert_pcm(PcmInput::Interleaved(&signed), mono, &token).unwrap(),
expected
);
let unsigned = [0u16, 16384, 32768, 49152, 65535];
assert_eq!(
convert_pcm(PcmInput::Interleaved(&unsigned), mono, &token).unwrap(),
expected
);
let signed32 = [i32::MIN, -1073741824, 0, 1073741824, i32::MAX];
let values = [-1.0, -0.5, 0.0, 0.5, 1.0];
assert_eq!(
convert_pcm(PcmInput::Interleaved(&signed32), mono, &token).unwrap(),
values
);
let planes: [&[i16]; 2] = [&signed, &signed];
assert_eq!(
convert_pcm(
PcmInput::Planar(&planes),
PcmFormat {
sample_rate: 16000,
channels: 2
},
&token
)
.unwrap(),
expected
);
let floats = [-0.25f64, 0.0, 0.25];
assert_eq!(
convert_pcm(PcmInput::Interleaved(&floats), mono, &token).unwrap(),
[-0.25, 0.0, 0.25]
);
assert!(convert_pcm(PcmInput::Interleaved(&[f64::MAX]), mono, &token).is_err());
}
}