//! # AIFF Decoder Module //! //! Streaming AIFF (Audio Interchange File Format) to PCM conversion without any //! seeking. The decoder parses the FORM/COMM/SSND chunks incrementally and emits //! little-endian interleaved PCM frames compatible with the rest of the //! pipeline. use std::{collections::VecDeque, fmt, io::Read}; use tokio::{ io::AsyncRead, sync::{mpsc, oneshot}, }; use crate::{ common::ChannelReader, decoder_common::{ spawn_ingest_task, spawn_writer_task, DecodedStream, DecoderError, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE, }, pcm::StreamInfo, stream::ManagedAsyncReader, }; /// Errors that can occur while decoding AIFF data. pub type AiffError = DecoderError; /// Streaming reader that buffers bytes as they arrive and exposes convenience helpers. struct StreamingAiffReader where E: fmt::Display + std::error::Error, { reader: ChannelReader, buffer: VecDeque, finished: bool, } impl StreamingAiffReader where E: fmt::Display + std::error::Error, { fn new(reader: ChannelReader) -> Self { Self { reader, buffer: VecDeque::new(), finished: false, } } fn fill_buffer(&mut self, len: usize) -> Result<(), AiffError> { while self.buffer.len() < len { if self.finished { break; } let mut chunk = [0u8; 4096]; let read = self.reader.read(&mut chunk)?; if read == 0 { self.finished = true; } else { self.buffer.extend(&chunk[..read]); } } Ok(()) } fn read_exact_vec(&mut self, len: usize) -> Result, AiffError> { self.fill_buffer(len)?; if self.buffer.len() < len { return Err(AiffError::Decode("unexpected EOF in AIFF stream".into())); } let mut out = Vec::with_capacity(len); for _ in 0..len { out.push(self.buffer.pop_front().unwrap()); } Ok(out) } fn skip(&mut self, mut len: usize) -> Result<(), AiffError> { while len > 0 { if !self.buffer.is_empty() { let take = len.min(self.buffer.len()); for _ in 0..take { self.buffer.pop_front(); } len -= take; continue; } let mut chunk = [0u8; 4096]; let read = self.reader.read(&mut chunk)?; if read == 0 { return Err(AiffError::Decode( "unexpected EOF while skipping chunk".into(), )); } self.buffer.extend(&chunk[..read]); } Ok(()) } } /// Compression / endianness mode for AIFF data. #[derive(Clone, Copy, Debug)] enum Compression { BigEndianPcm, LittleEndianPcm, } /// Parsed COMM chunk data. #[derive(Clone, Debug)] struct CommChunk { channels: u16, num_frames: u32, bits_per_sample: u16, sample_rate: u32, compression: Compression, } impl CommChunk { fn bytes_per_sample(&self) -> usize { ((self.bits_per_sample as usize) + 7) / 8 } fn validate(&self) -> Result<(), AiffError> { if self.channels == 0 { return Err(AiffError::Decode("AIFF channel count must be > 0".into())); } if self.sample_rate == 0 { return Err(AiffError::Decode("AIFF sample rate must be > 0".into())); } match self.bytes_per_sample() { 1 | 2 | 3 | 4 => Ok(()), other => Err(AiffError::Decode(format!( "unsupported AIFF bytes per sample: {}", other ))), } } } /// Async stream alias for decoded AIFF audio. pub type AiffDecodedStream = DecodedStream; /// Decode an AIFF stream into PCM audio (little-endian interleaved). pub async fn decode_aiff_stream(reader: R) -> Result where R: AsyncRead + Unpin + Send + 'static, { let (ingest_tx, ingest_rx) = mpsc::channel(CHANNEL_CAPACITY); spawn_ingest_task::<_, AiffError>(reader, ingest_tx); let (pcm_tx, pcm_rx) = mpsc::channel(CHANNEL_CAPACITY); let (pcm_reader, pcm_writer) = tokio::io::duplex(DUPLEX_BUFFER_SIZE); let (info_tx, info_rx) = oneshot::channel::>(); let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), AiffError> { let mut info_tx = Some(info_tx); let result: Result<(), AiffError> = (|| { let channel_reader = ChannelReader::::new(ingest_rx); let mut aiff_reader = StreamingAiffReader::new(channel_reader); // Parse FORM header let form_header = aiff_reader.read_exact_vec(12)?; if &form_header[0..4] != b"FORM" { return Err(AiffError::Decode("missing FORM header".into())); } let form_type = <[u8; 4]>::try_from(&form_header[8..12]).unwrap(); if form_type != *b"AIFF" && form_type != *b"AIFC" { return Err(AiffError::Decode( "unsupported FORM type (expected AIFF/AIFC)".into(), )); } let mut comm_chunk: Option = None; let mut stream_info_sent = false; loop { let header = match aiff_reader.read_exact_vec(8) { Ok(bytes) => bytes, Err(AiffError::Decode(msg)) if msg.contains("unexpected EOF") => break, Err(err) => return Err(err), }; let chunk_id = <[u8; 4]>::try_from(&header[..4]).unwrap(); let chunk_size = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; let padded_size = if chunk_size % 2 == 0 { chunk_size } else { chunk_size + 1 }; match &chunk_id { b"COMM" => { let data = aiff_reader.read_exact_vec(chunk_size)?; if form_type == *b"AIFF" && data.len() < 18 { return Err(AiffError::Decode("COMM chunk too small".into())); } if data.len() < 18 { return Err(AiffError::Decode("COMM chunk too small for AIFC".into())); } let channels = u16::from_be_bytes([data[0], data[1]]); let num_frames = u32::from_be_bytes([data[2], data[3], data[4], data[5]]); let bits_per_sample = u16::from_be_bytes([data[6], data[7]]); let sample_rate = parse_extended_f80(&data[8..18])?; let compression = if form_type == *b"AIFC" { if data.len() < 22 { return Err(AiffError::Decode( "AIFC COMM chunk missing compression type".into(), )); } match &data[18..22] { b"NONE" => Compression::BigEndianPcm, b"sowt" => Compression::LittleEndianPcm, code => { return Err(AiffError::Decode(format!( "unsupported AIFC compression type: {}", String::from_utf8_lossy(code) ))) } } } else { Compression::BigEndianPcm }; let comm = CommChunk { channels, num_frames, bits_per_sample, sample_rate, compression, }; comm.validate()?; comm_chunk = Some(comm); if padded_size > chunk_size { aiff_reader.skip(padded_size - chunk_size)?; } } b"SSND" => { let comm = comm_chunk.as_ref().ok_or_else(|| { AiffError::Decode("SSND chunk encountered before COMM".into()) })?; let header = aiff_reader.read_exact_vec(8)?; let offset = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize; let _block_size = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; if offset > 0 { aiff_reader.skip(offset)?; } let data_bytes = chunk_size .checked_sub(8) .ok_or_else(|| AiffError::Decode("invalid SSND chunk size".into()))?; let bytes_per_sample = comm.bytes_per_sample(); let info = StreamInfo { sample_rate: comm.sample_rate, channels: comm.channels as u8, bits_per_sample: comm.bits_per_sample as u8, total_samples: Some(comm.num_frames as u64), max_block_size: 0, min_block_size: 0, }; if !stream_info_sent { if let Some(tx) = info_tx.take() { if tx.send(Ok(info.clone())).is_err() { return Ok(()); } } stream_info_sent = true; } let mut remaining = data_bytes; while remaining > 0 { let mut to_read = remaining.min(8192); let residue = to_read % bytes_per_sample; if residue != 0 { to_read -= residue; } if to_read == 0 { to_read = bytes_per_sample; } let mut chunk = aiff_reader.read_exact_vec(to_read)?; match comm.compression { Compression::BigEndianPcm => { chunk = convert_be_pcm(chunk, comm.bits_per_sample)?; } Compression::LittleEndianPcm => { // data already little-endian; no conversion } } if !chunk.is_empty() { if pcm_tx.blocking_send(Ok(chunk)).is_err() { return Ok(()); } } remaining = remaining .checked_sub(to_read) .ok_or_else(|| AiffError::Decode("SSND chunk underflow".into()))?; } if padded_size > chunk_size { aiff_reader.skip(1)?; } break; } _ => { aiff_reader.skip(chunk_size)?; if padded_size > chunk_size { aiff_reader.skip(padded_size - chunk_size)?; } } } } if !stream_info_sent { return Err(AiffError::Decode( "no SSND chunk found in AIFF stream".into(), )); } Ok(()) })(); match result { Ok(()) => Ok(()), Err(err) => { if let Some(tx) = info_tx.take() { let _ = tx.send(Err(err.clone())); } Err(err) } } }); let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "aiff-decode"); let info = info_rx.await.map_err(|_| AiffError::ChannelClosed)??; let reader = ManagedAsyncReader::new("aiff-decode-writer", pcm_reader, writer_handle); Ok(DecodedStream::new(info, reader)) } fn parse_extended_f80(bytes: &[u8]) -> Result { if bytes.len() != 10 { return Err(AiffError::Decode("invalid 80-bit float length".into())); } let sign = if bytes[0] & 0x80 != 0 { -1.0 } else { 1.0 }; let exponent = (((bytes[0] & 0x7F) as i32) << 8 | bytes[1] as i32) - 16383; let mut mantissa: u64 = 0; for b in &bytes[2..10] { mantissa = (mantissa << 8) | (*b as u64); } if exponent == -16383 && mantissa == 0 { return Ok(0); } let magnitude = mantissa as f64 / (1u64 << 63) as f64; let value = sign * magnitude * 2f64.powi(exponent); if value <= 0.0 { return Err(AiffError::Decode("invalid or negative sample rate".into())); } Ok(value.round() as u32) } fn convert_be_pcm(mut chunk: Vec, bits_per_sample: u16) -> Result, AiffError> { let bytes_per_sample = ((bits_per_sample as usize) + 7) / 8; if chunk.len() % bytes_per_sample != 0 { return Err(AiffError::Decode( "AIFF PCM data not aligned to whole samples".into(), )); } match bytes_per_sample { 1 => Ok(chunk), 2 => { for sample in chunk.chunks_mut(2) { sample.swap(0, 1); } Ok(chunk) } 3 => { let mut out = Vec::with_capacity(chunk.len()); for sample in chunk.chunks(3) { let value = ((sample[0] as i32) << 16) | ((sample[1] as i32) << 8) | (sample[2] as i32); let value = if value & 0x0080_0000 != 0 { value | !0x00FF_FFFF } else { value }; let le = value.to_le_bytes(); out.extend_from_slice(&le[..3]); } Ok(out) } 4 => { for sample in chunk.chunks_mut(4) { sample.swap(0, 3); sample.swap(1, 2); } Ok(chunk) } other => Err(AiffError::Decode(format!( "unsupported bytes per sample: {}", other ))), } }