diff --git a/src/flac_decoder.rs b/src/flac_decoder.rs index afd1594a..3c30f3b5 100644 --- a/src/flac_decoder.rs +++ b/src/flac_decoder.rs @@ -5,7 +5,28 @@ pub(crate) struct FlacDecoder<'a> { } impl<'a> FlacDecoder<'a> { - fn decode(data: &'a [u8]) -> Result { + fn frame_offset(data: &[u8]) -> Result { + let mut offset = 4; + + loop { + let header = data + .get(offset..offset + 4) + .context(audio_error::FlacTruncated)?; + + let length = + usize::from(header[1]) << 16 | usize::from(header[2]) << 8 | usize::from(header[3]); + + offset += 4 + length; + + ensure!(offset <= data.len(), audio_error::FlacTruncated); + + if header[0] & 0x80 != 0 { + return Ok(offset); + } + } + } + + fn metadata(data: &'a [u8]) -> Result { let decoder = Self { reader: FlacReader::new(data).context(audio_error::FlacDecode)?, }; @@ -32,27 +53,6 @@ impl<'a> FlacDecoder<'a> { }) } - fn frame_offset(data: &[u8]) -> Result { - let mut offset = 4; - - loop { - let header = data - .get(offset..offset + 4) - .context(audio_error::FlacTruncated)?; - - let length = - usize::from(header[1]) << 16 | usize::from(header[2]) << 8 | usize::from(header[3]); - - offset += 4 + length; - - ensure!(offset <= data.len(), audio_error::FlacTruncated); - - if header[0] & 0x80 != 0 { - return Ok(offset); - } - } - } - fn number_tag(&self, tag: &'static str) -> Result { let value = self.tag(tag)?; parse_number(value).context(audio_error::TagInteger { tag }) @@ -61,7 +61,7 @@ impl<'a> FlacDecoder<'a> { pub(crate) fn read(path: &Utf8Path) -> Result { let data = filesystem::read(path)?; - FlacDecoder::decode(&data).context(error::Audio { path }) + FlacDecoder::metadata(&data).context(error::Audio { path }) } fn tag(&self, tag: &'static str) -> Result<&str, AudioError> { @@ -81,13 +81,38 @@ mod tests { use super::*; #[test] - fn decode_err() { + fn frame_offset() { + let bytes = FlacBuilder::new().build(); + assert_eq!( + FlacDecoder::frame_offset(&bytes).unwrap(), + bytes.len() - 1024, + ); + + let bytes = FlacBuilder::new().tag("foo", "bar").build(); + assert_eq!( + FlacDecoder::frame_offset(&bytes).unwrap(), + bytes.len() - 1024, + ); + + assert_matches!( + FlacDecoder::frame_offset(&FlacBuilder::new().truncate(4).build()), + Err(AudioError::FlacTruncated), + ); + + assert_matches!( + FlacDecoder::frame_offset(&FlacBuilder::new().truncate(8).build()), + Err(AudioError::FlacTruncated), + ); + } + + #[test] + fn metadata_err() { fn err(builder: FlacBuilder) -> AudioError { - FlacDecoder::decode(&builder.build()).unwrap_err() + FlacDecoder::metadata(&builder.build()).unwrap_err() } assert_matches!( - FlacDecoder::decode(b"foo").unwrap_err(), + FlacDecoder::metadata(b"foo").unwrap_err(), AudioError::FlacDecode { .. }, ); @@ -258,31 +283,6 @@ mod tests { ); } - #[test] - fn frame_offset() { - let bytes = FlacBuilder::new().build(); - assert_eq!( - FlacDecoder::frame_offset(&bytes).unwrap(), - bytes.len() - 1024, - ); - - let bytes = FlacBuilder::new().tag("foo", "bar").build(); - assert_eq!( - FlacDecoder::frame_offset(&bytes).unwrap(), - bytes.len() - 1024, - ); - - assert_matches!( - FlacDecoder::frame_offset(&FlacBuilder::new().truncate(4).build()), - Err(AudioError::FlacTruncated), - ); - - assert_matches!( - FlacDecoder::frame_offset(&FlacBuilder::new().truncate(8).build()), - Err(AudioError::FlacTruncated), - ); - } - #[test] fn read_ok() { let (_tempdir, root) = tempdir(); diff --git a/src/mp3_decoder.rs b/src/mp3_decoder.rs index 6ee864a0..c922ea55 100644 --- a/src/mp3_decoder.rs +++ b/src/mp3_decoder.rs @@ -60,61 +60,6 @@ impl Version { } impl<'a> Mp3Decoder<'a> { - fn decode(data: &'a [u8]) -> Result { - let decoder = Self { data }; - - let mut offset = 0; - let mut first = Option::::None; - let mut samples = 0; - let mut size = 0; - - while offset < decoder.data.len() { - let frame = decoder.frame(offset)?; - - if !frame.metadata { - samples += frame.samples; - size += frame.size.into_u64(); - } - - offset += frame.size; - - if let Some(first) = &first { - ensure! { - frame.channels == first.channels, - mp3_error::ChannelsMismatch { - actual: frame.channels, - expected: first.channels, - }, - } - - ensure! { - frame.sample_rate == first.sample_rate, - mp3_error::SampleRateMismatch { - actual: frame.sample_rate, - expected: first.sample_rate, - }, - } - } else { - first = Some(frame); - } - } - - let Frame { - channels, - sample_rate, - .. - } = first.context(mp3_error::Empty)?; - - ensure!(samples > 0, mp3_error::Empty); - - Ok(AudioProperties { - channels, - sample_rate, - samples, - size, - }) - } - fn frame(&self, offset: usize) -> Result { let header = self .data @@ -210,7 +155,7 @@ impl<'a> Mp3Decoder<'a> { sample_rate, samples, size, - } = Mp3Decoder::decode(&data[start..]).context(audio_error::Mp3Decode)?; + } = Mp3Decoder::properties(&data[start..]).context(audio_error::Mp3Decode)?; Ok(AudioMetadata { album, @@ -241,6 +186,61 @@ impl<'a> Mp3Decoder<'a> { )) } + fn properties(data: &'a [u8]) -> Result { + let decoder = Self { data }; + + let mut offset = 0; + let mut first = Option::::None; + let mut samples = 0; + let mut size = 0; + + while offset < decoder.data.len() { + let frame = decoder.frame(offset)?; + + if !frame.metadata { + samples += frame.samples; + size += frame.size.into_u64(); + } + + offset += frame.size; + + if let Some(first) = &first { + ensure! { + frame.channels == first.channels, + mp3_error::ChannelsMismatch { + actual: frame.channels, + expected: first.channels, + }, + } + + ensure! { + frame.sample_rate == first.sample_rate, + mp3_error::SampleRateMismatch { + actual: frame.sample_rate, + expected: first.sample_rate, + }, + } + } else { + first = Some(frame); + } + } + + let Frame { + channels, + sample_rate, + .. + } = first.context(mp3_error::Empty)?; + + ensure!(samples > 0, mp3_error::Empty); + + Ok(AudioProperties { + channels, + sample_rate, + samples, + size, + }) + } + pub(crate) fn read(path: &Utf8Path) -> Result { let data = filesystem::read(path)?; diff --git a/src/mp4_decoder.rs b/src/mp4_decoder.rs index 10740aef..6f8c7244 100644 --- a/src/mp4_decoder.rs +++ b/src/mp4_decoder.rs @@ -3,7 +3,70 @@ use super::*; pub(crate) struct Mp4Decoder; impl Mp4Decoder { - fn decode(reader: T, size: u64) -> Result { + fn h264_color_info(sps: &[u8]) -> Option { + let mut rbsp = Vec::new(); + + // skip NAL unit header + for &byte in sps.get(1..)? { + // remove emulation prevention bytes + if byte == 3 && rbsp.ends_with(&[0, 0]) { + continue; + } + + rbsp.push(byte); + } + + let mut reader = BitReader::new(&rbsp); + + // profile_idc + let profile_idc = reader.bits(8)?; + + // constraint flags + reader.bits(8)?; + + // level_idc + reader.bits(8)?; + + // seq_parameter_set_id + reader.ue()?; + + if !Self::h264_high_profile(profile_idc) { + return Some(ColorInfo { + bit_depth: 8, + chroma_subsampling: ChromaSubsampling::Yuv420, + }); + } + + // chroma_format_idc + let chroma_subsampling = match reader.ue()? { + 0 => ChromaSubsampling::Yuv400, + 1 => ChromaSubsampling::Yuv420, + 2 => ChromaSubsampling::Yuv422, + 3 => { + // separate_colour_plane_flag + reader.bit()?; + ChromaSubsampling::Yuv444 + } + _ => return None, + }; + + // bit_depth_luma_minus8 + let bit_depth = 8 + reader.ue()?; + + Some(ColorInfo { + bit_depth, + chroma_subsampling, + }) + } + + fn h264_high_profile(profile_idc: u64) -> bool { + matches!( + profile_idc, + 44 | 83 | 86 | 100 | 110 | 118 | 122 | 128 | 134 | 135 | 138 | 139 | 244 + ) + } + + fn metadata(reader: T, size: u64) -> Result { use re_mp4::{Mp4, Mp4aBox, StsdBoxContent, TkhdBox}; fn mp4a_codec(mp4a: &Mp4aBox) -> Option { @@ -185,75 +248,12 @@ impl Mp4Decoder { Ok(VideoMetadata { duration, tracks }) } - fn h264_color_info(sps: &[u8]) -> Option { - let mut rbsp = Vec::new(); - - // skip NAL unit header - for &byte in sps.get(1..)? { - // remove emulation prevention bytes - if byte == 3 && rbsp.ends_with(&[0, 0]) { - continue; - } - - rbsp.push(byte); - } - - let mut reader = BitReader::new(&rbsp); - - // profile_idc - let profile_idc = reader.bits(8)?; - - // constraint flags - reader.bits(8)?; - - // level_idc - reader.bits(8)?; - - // seq_parameter_set_id - reader.ue()?; - - if !Self::h264_high_profile(profile_idc) { - return Some(ColorInfo { - bit_depth: 8, - chroma_subsampling: ChromaSubsampling::Yuv420, - }); - } - - // chroma_format_idc - let chroma_subsampling = match reader.ue()? { - 0 => ChromaSubsampling::Yuv400, - 1 => ChromaSubsampling::Yuv420, - 2 => ChromaSubsampling::Yuv422, - 3 => { - // separate_colour_plane_flag - reader.bit()?; - ChromaSubsampling::Yuv444 - } - _ => return None, - }; - - // bit_depth_luma_minus8 - let bit_depth = 8 + reader.ue()?; - - Some(ColorInfo { - bit_depth, - chroma_subsampling, - }) - } - - fn h264_high_profile(profile_idc: u64) -> bool { - matches!( - profile_idc, - 44 | 83 | 86 | 100 | 110 | 118 | 122 | 128 | 134 | 135 | 138 | 139 | 244 - ) - } - pub(crate) fn read(path: &Utf8Path) -> Result { let file = filesystem::open(path)?; let size = file.metadata().context(error::FilesystemIo { path })?.len(); - Self::decode(file, size).context(error::Video { path }) + Self::metadata(file, size).context(error::Video { path }) } } @@ -262,12 +262,55 @@ mod tests { use super::*; #[test] - fn decode() { + fn h264_color_info() { + #[track_caller] + fn case(sps: &[u8], expected: Option) { + assert_eq!(Mp4Decoder::h264_color_info(sps), expected); + } + + fn config(bit_depth: u64, chroma_subsampling: ChromaSubsampling) -> ColorInfo { + ColorInfo { + bit_depth, + chroma_subsampling, + } + } + + case( + &[0x67, 66, 0, 30, 0x80], + Some(config(8, ChromaSubsampling::Yuv420)), + ); + case( + &[0x67, 100, 0, 31, 0xA6], + Some(config(10, ChromaSubsampling::Yuv420)), + ); + case( + &[0x67, 100, 0, 31, 0xB8], + Some(config(8, ChromaSubsampling::Yuv422)), + ); + case( + &[0x67, 100, 0, 31, 0x91], + Some(config(8, ChromaSubsampling::Yuv444)), + ); + case( + &[0x67, 100, 0, 31, 0xE0], + Some(config(8, ChromaSubsampling::Yuv400)), + ); + case( + &[0x67, 100, 0, 0, 0x03, 0xA6], + Some(config(10, ChromaSubsampling::Yuv420)), + ); + case(&[0x67, 100, 0, 31], None); + case(&[0x67], None); + case(&[], None); + } + + #[test] + fn metadata() { #[track_caller] fn case(builder: Mp4Builder) -> Result { let bytes = builder.build(); let size = bytes.len().try_into().unwrap(); - Mp4Decoder::decode(io::Cursor::new(bytes), size) + Mp4Decoder::metadata(io::Cursor::new(bytes), size) } #[track_caller] @@ -528,53 +571,10 @@ mod tests { ); assert_eq!( - Mp4Decoder::decode(io::Cursor::new(b"foo"), 3) + Mp4Decoder::metadata(io::Cursor::new(b"foo"), 3) .unwrap_err() .to_string(), "failed to decode MP4", ); } - - #[test] - fn h264_color_info() { - #[track_caller] - fn case(sps: &[u8], expected: Option) { - assert_eq!(Mp4Decoder::h264_color_info(sps), expected); - } - - fn config(bit_depth: u64, chroma_subsampling: ChromaSubsampling) -> ColorInfo { - ColorInfo { - bit_depth, - chroma_subsampling, - } - } - - case( - &[0x67, 66, 0, 30, 0x80], - Some(config(8, ChromaSubsampling::Yuv420)), - ); - case( - &[0x67, 100, 0, 31, 0xA6], - Some(config(10, ChromaSubsampling::Yuv420)), - ); - case( - &[0x67, 100, 0, 31, 0xB8], - Some(config(8, ChromaSubsampling::Yuv422)), - ); - case( - &[0x67, 100, 0, 31, 0x91], - Some(config(8, ChromaSubsampling::Yuv444)), - ); - case( - &[0x67, 100, 0, 31, 0xE0], - Some(config(8, ChromaSubsampling::Yuv400)), - ); - case( - &[0x67, 100, 0, 0, 0x03, 0xA6], - Some(config(10, ChromaSubsampling::Yuv420)), - ); - case(&[0x67, 100, 0, 31], None); - case(&[0x67], None); - case(&[], None); - } } diff --git a/src/webm_decoder.rs b/src/webm_decoder.rs index d5f838b1..0e9ecded 100644 --- a/src/webm_decoder.rs +++ b/src/webm_decoder.rs @@ -3,7 +3,7 @@ use super::*; pub(crate) struct WebmDecoder; impl WebmDecoder { - fn decode(reader: T) -> Result { + fn metadata(reader: T) -> Result { use matroska_demuxer::{Frame, MatroskaFile, TrackType}; let mut file = MatroskaFile::open(BufReader::new(reader)).context(video_error::DecodeWebm)?; @@ -184,7 +184,7 @@ impl WebmDecoder { pub(crate) fn read(path: &Utf8Path) -> Result { let file = filesystem::open(path)?; - Self::decode(file).context(error::Video { path }) + Self::metadata(file).context(error::Video { path }) } fn vp9_color_info(data: &[u8]) -> Option { @@ -284,10 +284,10 @@ mod tests { const VP9_FRAME: &[u8] = &[0x82, 0x49, 0x83, 0x42, 0x00]; #[test] - fn decode() { + fn metadata() { #[track_caller] fn case(builder: WebmBuilder) -> Result { - WebmDecoder::decode(io::Cursor::new(builder.build())) + WebmDecoder::metadata(io::Cursor::new(builder.build())) } #[track_caller] @@ -586,7 +586,7 @@ mod tests { ); assert_eq!( - WebmDecoder::decode(io::Cursor::new(b"foo")) + WebmDecoder::metadata(io::Cursor::new(b"foo")) .unwrap_err() .to_string(), "failed to decode WebM",