//! Multi-channel audio decoding: preserves original channel layout for export. //! //! Unlike `analysis::decode` (mono mixdown) or `preview` (forced stereo), //! this decoder returns interleaved samples in their original channel count. use std::path::Path; use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::DecoderOptions; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use tracing::instrument; use crate::error::{io_err, AnalysisError, CoreError}; /// Decoded audio preserving original channel layout. #[derive(Debug)] pub struct DecodedMultichannel { /// Interleaved sample data in original channel order. pub samples: Vec, /// Sample rate of the source file in Hz. pub sample_rate: u32, /// Number of channels in the source file. pub channels: u16, } /// Decode an audio file preserving its original channel layout. #[instrument(skip_all)] pub fn decode_multichannel(path: &Path) -> Result { let file = std::fs::File::open(path).map_err(|e| io_err(path, e))?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); if let Some(ext) = path.extension().and_then(|e| e.to_str()) { hint.with_extension(ext); } let probed = symphonia::default::get_probe() .format( &hint, mss, &FormatOptions::default(), &MetadataOptions::default(), ) .map_err(|e| AnalysisError::ProbeFailed(e.to_string()))?; let mut format = probed.format; let track = format .default_track() .ok_or(AnalysisError::NoAudioTrack)?; let track_id = track.id; let source_sample_rate = track .codec_params .sample_rate .ok_or(AnalysisError::ProbeFailed("missing sample rate".to_string()))?; let source_channels = track .codec_params .channels .map(|c| c.count() as u16) .ok_or(AnalysisError::ProbeFailed("missing channel count".to_string()))?; let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) .map_err(|e| AnalysisError::DecoderFailed(e.to_string()))?; let mut all_samples: Vec = Vec::new(); loop { let packet = match format.next_packet() { Ok(p) => p, Err(symphonia::core::errors::Error::IoError(ref e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => { break; } Err(e) => return Err(AnalysisError::PacketError(e.to_string()).into()), }; if packet.track_id() != track_id { continue; } let decoded = match decoder.decode(&packet) { Ok(d) => d, Err(symphonia::core::errors::Error::DecodeError(_)) => continue, Err(e) => return Err(AnalysisError::DecodeError(e.to_string()).into()), }; let num_frames = decoded.frames(); let mut sample_buf = SampleBuffer::::new(num_frames as u64, *decoded.spec()); sample_buf.copy_interleaved_ref(decoded); all_samples.extend_from_slice(sample_buf.samples()); } if all_samples.is_empty() { return Err(AnalysisError::NoAudioData.into()); } Ok(DecodedMultichannel { samples: all_samples, sample_rate: source_sample_rate, channels: source_channels, }) } #[cfg(test)] mod tests { use super::*; use std::io::Write; use std::path::PathBuf; /// Write a minimal WAV file with f32 PCM data. fn write_wav(path: &Path, channels: u16, sample_rate: u32, samples: &[f32]) { let bytes_per_sample = 4u16; let block_align = channels * bytes_per_sample; let data_size = (samples.len() as u32) * 4; let file_size = 36 + data_size; let mut buf = Vec::with_capacity(44 + data_size as usize); buf.extend_from_slice(b"RIFF"); buf.extend_from_slice(&file_size.to_le_bytes()); buf.extend_from_slice(b"WAVE"); buf.extend_from_slice(b"fmt "); buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&3u16.to_le_bytes()); // IEEE float buf.extend_from_slice(&channels.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes()); buf.extend_from_slice(&(sample_rate * block_align as u32).to_le_bytes()); buf.extend_from_slice(&block_align.to_le_bytes()); buf.extend_from_slice(&(bytes_per_sample * 8).to_le_bytes()); buf.extend_from_slice(b"data"); buf.extend_from_slice(&data_size.to_le_bytes()); for &s in samples { buf.extend_from_slice(&s.to_le_bytes()); } let mut file = std::fs::File::create(path).unwrap(); file.write_all(&buf).unwrap(); } #[test] fn decode_preserves_mono() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("mono.wav"); write_wav(&path, 1, 44100, &[0.5, -0.5, 0.25]); let decoded = decode_multichannel(&path).unwrap(); assert_eq!(decoded.channels, 1); assert_eq!(decoded.samples, vec![0.5, -0.5, 0.25]); assert_eq!(decoded.sample_rate, 44100); } #[test] fn decode_preserves_stereo() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("stereo.wav"); let samples = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; write_wav(&path, 2, 48000, &samples); let decoded = decode_multichannel(&path).unwrap(); assert_eq!(decoded.channels, 2); assert_eq!(decoded.samples, samples); assert_eq!(decoded.sample_rate, 48000); } #[test] fn decode_nonexistent_returns_error() { let result = decode_multichannel(&PathBuf::from("/nonexistent/audio.wav")); assert!(result.is_err()); } }