diff --git a/crates/audio-io/src/flac.rs b/crates/audio-io/src/flac.rs index 8a8c0ce1..657b9119 100644 --- a/crates/audio-io/src/flac.rs +++ b/crates/audio-io/src/flac.rs @@ -3,7 +3,7 @@ //! partitioned Rice-coded residuals. Decoding is symphonia's, after [`check_metadata`]. use crate::pcm::{Quantizer, be_u32, le_u32, validate_buffer}; -use crate::{AudioBuffer, AudioError, BitDepth, Result}; +use crate::{AudioBuffer, AudioError, AudioInfo, BitDepth, FileFormat, Result, SampleFormat}; use std::io::{Cursor, Seek, SeekFrom, Write}; const BLOCK_SIZE: usize = 4096; @@ -454,6 +454,48 @@ pub(crate) fn check_metadata(b: &[u8], at: usize) -> Result<()> { Ok(()) } +/// The info of a FLAC stream with no audio frames at all: STREAMINFO declares 0 samples and +/// nothing follows the metadata blocks. Symphonia's reader fails on these, as it looks for a first +/// frame, so the caller answers them itself. +pub(crate) fn empty_stream_info(b: &[u8], at: usize) -> Option { + let mut pos = at.checked_add(4)?; + let mut first = true; + loop { + let head = be_u32(b, pos)?; + let (body, len) = (pos.checked_add(4)?, (head & 0x00ff_ffff) as usize); + let end = body.checked_add(len)?; + if end > b.len() { + return None; + } + if first && ((head >> 24) & 0x7f != 0 || len != 34) { + return None; + } + first = false; + if head >> 31 == 1 { + if end != b.len() { + return None; + } + break; + } + pos = end; + } + // STREAMINFO, from the sample rate on: 20 bits rate, 3 channels - 1, 5 bits per sample - 1, 36 bits total. + let packed = u64::from_be_bytes(b.get(at + 4 + 4 + 10..at + 4 + 4 + 18)?.try_into().ok()?); + let sample_rate = (packed >> 44) as u32; + let channels = ((packed >> 41) & 0x7) as u16 + 1; + let bps = ((packed >> 36) & 0x1f) as u32 + 1; + if packed & 0xf_ffff_ffff != 0 || sample_rate == 0 { + return None; + } + let sample_format = match bps { + 1..=8 => SampleFormat::Int8, + 9..=16 => SampleFormat::Int16, + 17..=24 => SampleFormat::Int24, + _ => SampleFormat::Int32, + }; + Some(AudioInfo { format: FileFormat::Flac, sample_format, sample_rate, channels, frames: 0, bwf: None }) +} + /// Whether a comment in a (little-endian) Vorbis comment block claims more than the block holds. /// The vendor string is skipped unread. fn comments_overrun(b: &[u8], body: usize, end: usize) -> bool { diff --git a/crates/audio-io/src/symph.rs b/crates/audio-io/src/symph.rs index 61691114..5bfe91da 100644 --- a/crates/audio-io/src/symph.rs +++ b/crates/audio-io/src/symph.rs @@ -36,9 +36,15 @@ fn guarded(f: impl FnOnce() -> Result) -> Result { catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| Err(AudioError::Malformed("decoder failed on malformed input".into()))) } +/// What [`open`] found: a reader, or a stream that holds no audio frames and has no reader. +enum Opened { + Reader(Box), + Empty(AudioInfo), +} + /// Symphonia's probe loop (`Probe::format`, whose hint 0.5 ignores), checking the container it /// finds before its reader allocates whatever the file claims. -fn open(bytes: &[u8]) -> Result> { +fn open(bytes: &[u8]) -> Result { let mut mss = MediaSourceStream::new(Box::new(Cursor::new(bytes.to_vec())), MediaSourceStreamOptions::default()); let fmt_opts = FormatOptions { enable_gapless: true, ..FormatOptions::default() }; loop { @@ -50,11 +56,16 @@ fn open(bytes: &[u8]) -> Result> { match head.get(..4) { Some(b"caff") => crate::caf::check(bytes, at)?, Some(b"ftyp") => crate::mp4::check(bytes, at.saturating_sub(4))?, - Some(b"fLaC") => crate::flac::check_metadata(bytes, at)?, + Some(b"fLaC") => { + crate::flac::check_metadata(bytes, at)?; + if let Some(info) = crate::flac::empty_stream_info(bytes, at) { + return Ok(Opened::Empty(info)); + } + } Some(b"RIFF") => crate::wav::check_info(bytes, at)?, _ => {} } - return reader(mss, &fmt_opts).map_err(map_err); + return reader(mss, &fmt_opts).map(Opened::Reader).map_err(map_err); } // Tags aren't used, and symphonia allocates whatever an ID3v2 frame claims: skip it. (Instantiate::Metadata(_), Some([b'I', b'D', b'3', _, _, _, size @ ..])) => { @@ -141,7 +152,13 @@ fn info_of(reader: &dyn FormatReader, detected: FileFormat) -> Result<(u32, Audi /// Decode the whole first audio track. When `count_only`, samples are counted but not stored. fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInfo, AudioBuffer)> { - let mut reader = open(bytes)?; + let mut reader = match open(bytes)? { + Opened::Reader(reader) => reader, + Opened::Empty(info) => { + let buf = AudioBuffer::new(info.sample_rate, usize::from(info.channels), 0); + return Ok((info, buf)); + } + }; let (track_id, mut info) = info_of(reader.as_ref(), detected)?; if let Some(n) = reader.tracks().iter().find(|t| t.id == track_id).and_then(|t| t.codec_params.n_frames) { let total = n.saturating_mul(u64::from(info.channels.max(1))); @@ -237,7 +254,10 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf pub(crate) fn probe(bytes: &[u8], detected: FileFormat) -> Result { guarded(|| { - let reader = open(bytes)?; + let reader = match open(bytes)? { + Opened::Reader(reader) => reader, + Opened::Empty(info) => return Ok(info), + }; let (_, info) = info_of(reader.as_ref(), detected)?; if info.frames > 0 && info.channels > 0 { return Ok(info); diff --git a/crates/audio-io/tests/roundtrip.rs b/crates/audio-io/tests/roundtrip.rs index a53c272a..374e3aa7 100644 --- a/crates/audio-io/tests/roundtrip.rs +++ b/crates/audio-io/tests/roundtrip.rs @@ -284,3 +284,28 @@ fn float_wav_header_is_rewritten_in_place_as_the_file_grows() { assert_eq!((&big[..4], big.len()), (&b"RF64"[..], len)); assert!(wav_float_header(0, 48_000, 0).is_err()); } + +#[test] +fn zero_frame_flac_round_trips_like_wav_and_aiff() { + for format in [FileFormat::Wav, FileFormat::Aiff, FileFormat::Flac] { + for depth in [BitDepth::Int16, BitDepth::Int24] { + for channels in [1, 2] { + let opts = EncodeOptions { format, bit_depth: depth, ..EncodeOptions::default() }; + let bytes = encode(&AudioBuffer::new(48_000, channels, 0), &opts).unwrap(); + let info = probe(&bytes, None).unwrap(); + assert_eq!((info.format, info.sample_format, info.sample_rate, info.frames), (format, expected_sf(depth), 48_000, 0)); + assert_eq!(usize::from(info.channels), channels); + let (info, buf) = decode(&bytes, None).unwrap(); + assert_eq!((info.frames, buf.frames(), buf.num_channels()), (0, 0, channels)); + } + } + } +} + +#[test] +fn flac_claiming_frames_but_holding_none_still_errors() { + let opts = EncodeOptions { format: FileFormat::Flac, bit_depth: BitDepth::Int16, ..EncodeOptions::default() }; + let mut bytes = encode(&AudioBuffer::new(48_000, 2, 0), &opts).unwrap(); + bytes[25] = 1; // STREAMINFO total samples (low byte of the 36-bit field) = 1 + assert!(decode(&bytes, None).is_err()); +}