mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-03-04 14:57:17 +00:00
Merge branch 'main' into wave-depth
This commit is contained in:
commit
6325665ab2
10 changed files with 115 additions and 69 deletions
|
@ -74,7 +74,7 @@ impl ApeProperties {
|
||||||
self.sample_rate
|
self.sample_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bits per sample
|
/// Bits per sample
|
||||||
pub fn bit_depth(&self) -> u8 {
|
pub fn bit_depth(&self) -> u8 {
|
||||||
self.bit_depth
|
self.bit_depth
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,6 @@ where
|
||||||
return Err(LoftyError::Ape("File contains no frames"));
|
return Err(LoftyError::Ape("File contains no frames"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unused
|
|
||||||
let bits_per_sample = header_read.read_u16::<LittleEndian>()?;
|
let bits_per_sample = header_read.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
let channels = header_read.read_u16::<LittleEndian>()?;
|
let channels = header_read.read_u16::<LittleEndian>()?;
|
||||||
|
@ -212,7 +211,6 @@ where
|
||||||
|
|
||||||
let compression_level = header_first.read_u16::<LittleEndian>()?;
|
let compression_level = header_first.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
// Unused
|
|
||||||
let format_flags = header_first.read_u16::<LittleEndian>()?;
|
let format_flags = header_first.read_u16::<LittleEndian>()?;
|
||||||
// https://github.com/fernandotcl/monkeys-audio/blob/5fe956c7e67c13daa80518a4cc7001e9fa185297/src/MACLib/MACLib.h#L74
|
// https://github.com/fernandotcl/monkeys-audio/blob/5fe956c7e67c13daa80518a4cc7001e9fa185297/src/MACLib/MACLib.h#L74
|
||||||
let bit_depth = if (format_flags & 0b1) == 1 {
|
let bit_depth = if (format_flags & 0b1) == 1 {
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub enum LoftyError {
|
||||||
|
|
||||||
// File data related errors
|
// File data related errors
|
||||||
/// Provided an empty file
|
/// Provided an empty file
|
||||||
EmptyFile,
|
EmptyFile, // TODO: Remove this
|
||||||
/// Attempting to read/write an abnormally large amount of data
|
/// Attempting to read/write an abnormally large amount of data
|
||||||
TooMuchData,
|
TooMuchData,
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ impl WavProperties {
|
||||||
self.sample_rate
|
self.sample_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bits per sample
|
/// Bits per sample
|
||||||
pub fn bit_depth(&self) -> u8 {
|
pub fn bit_depth(&self) -> u8 {
|
||||||
self.bit_depth
|
self.bit_depth
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,11 @@ pub(crate) fn verify_frame_sync(frame_sync: [u8; 2]) -> bool {
|
||||||
frame_sync[0] == 0xFF && frame_sync[1] >> 5 == 0b111
|
frame_sync[0] == 0xFF && frame_sync[1] >> 5 == 0b111
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Searches for a frame sync (11 bits with the value 1 like `0b1111_1111_111`) in the input reader.
|
// Searches for a frame sync (11 set bits) in the reader.
|
||||||
/// The search starts at the beginning of the reader and returns the index relative to this beginning.
|
// The search starts at the beginning of the reader and returns the index relative to this beginning.
|
||||||
/// Only the first match is returned and on no match, [`None`] is returned instead.
|
// This will return the first match, if one is found.
|
||||||
///
|
//
|
||||||
/// Note that the search searches in 8 bit steps, i.e. the first 8 bits need to be byte aligned.
|
// Note that the search searches in 8 bit steps, i.e. the first 8 bits need to be byte aligned.
|
||||||
pub(crate) fn search_for_frame_sync<R>(input: &mut R) -> std::io::Result<Option<u64>>
|
pub(crate) fn search_for_frame_sync<R>(input: &mut R) -> std::io::Result<Option<u64>>
|
||||||
where
|
where
|
||||||
R: Read,
|
R: Read,
|
||||||
|
@ -25,16 +25,18 @@ where
|
||||||
if let Some(byte) = iterator.next() {
|
if let Some(byte) = iterator.next() {
|
||||||
buffer[0] = byte?;
|
buffer[0] = byte?;
|
||||||
}
|
}
|
||||||
// create a stream of overlapping 2 byte pairs
|
// Create a stream of overlapping 2 byte pairs
|
||||||
// example: [0x01, 0x02, 0x03, 0x04] should be analyzed as
|
//
|
||||||
|
// Example:
|
||||||
|
// [0x01, 0x02, 0x03, 0x04] should be analyzed as
|
||||||
// [0x01, 0x02], [0x02, 0x03], [0x03, 0x04]
|
// [0x01, 0x02], [0x02, 0x03], [0x03, 0x04]
|
||||||
for (index, byte) in iterator.enumerate() {
|
for (index, byte) in iterator.enumerate() {
|
||||||
buffer[1] = byte?;
|
buffer[1] = byte?;
|
||||||
// check the two bytes in the buffer
|
// Check the two bytes in the buffer
|
||||||
if verify_frame_sync(buffer) {
|
if verify_frame_sync(buffer) {
|
||||||
return Ok(Some(index as u64));
|
return Ok(Some(index as u64));
|
||||||
}
|
}
|
||||||
// if they do not match, copy the last byte in the buffer to the front for the next iteration
|
// If they do not match, copy the last byte in the buffer to the front for the next iteration
|
||||||
buffer[0] = buffer[1];
|
buffer[0] = buffer[1];
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
|
@ -80,17 +80,18 @@ where
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// metadata blocks might be followed by junk bytes before the first MP3 frame begins
|
// Tags might be followed by junk bytes before the first MP3 frame begins
|
||||||
_ => {
|
_ => {
|
||||||
// seek back the length of the temporary header buffer
|
// seek back the length of the temporary header buffer, to include them
|
||||||
// so that all bytes are included in the search for a frame sync
|
// in the frame sync search
|
||||||
#[allow(clippy::neg_multiply)]
|
#[allow(clippy::neg_multiply)]
|
||||||
let start_of_search_area = reader.seek(SeekFrom::Current(-1 * header.len() as i64))?;
|
let start_of_search_area = reader.seek(SeekFrom::Current(-1 * header.len() as i64))?;
|
||||||
|
|
||||||
if let Some(first_mp3_frame_start_relative) = search_for_frame_sync(reader)? {
|
if let Some(first_mp3_frame_start_relative) = search_for_frame_sync(reader)? {
|
||||||
let first_mp3_frame_start_absolute =
|
let first_mp3_frame_start_absolute =
|
||||||
start_of_search_area + first_mp3_frame_start_relative;
|
start_of_search_area + first_mp3_frame_start_relative;
|
||||||
|
|
||||||
// read the first four bytes of the found frame
|
// Seek back to the start of the frame and read the header
|
||||||
reader.seek(SeekFrom::Start(first_mp3_frame_start_absolute))?;
|
reader.seek(SeekFrom::Start(first_mp3_frame_start_absolute))?;
|
||||||
let header = Header::read(reader.read_u32::<BigEndian>()?)?;
|
let header = Header::read(reader.read_u32::<BigEndian>()?)?;
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub struct Mp4Properties {
|
||||||
overall_bitrate: u32,
|
overall_bitrate: u32,
|
||||||
audio_bitrate: u32,
|
audio_bitrate: u32,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
|
bit_depth: Option<u8>,
|
||||||
channels: u8,
|
channels: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ impl From<Mp4Properties> for FileProperties {
|
||||||
overall_bitrate: Some(input.overall_bitrate),
|
overall_bitrate: Some(input.overall_bitrate),
|
||||||
audio_bitrate: Some(input.audio_bitrate),
|
audio_bitrate: Some(input.audio_bitrate),
|
||||||
sample_rate: Some(input.sample_rate),
|
sample_rate: Some(input.sample_rate),
|
||||||
bit_depth: None,
|
bit_depth: input.bit_depth,
|
||||||
channels: Some(input.channels),
|
channels: Some(input.channels),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +57,7 @@ impl Mp4Properties {
|
||||||
overall_bitrate: u32,
|
overall_bitrate: u32,
|
||||||
audio_bitrate: u32,
|
audio_bitrate: u32,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
|
bit_depth: Option<u8>,
|
||||||
channels: u8,
|
channels: u8,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -64,6 +66,7 @@ impl Mp4Properties {
|
||||||
overall_bitrate,
|
overall_bitrate,
|
||||||
audio_bitrate,
|
audio_bitrate,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
|
bit_depth,
|
||||||
channels,
|
channels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +91,11 @@ impl Mp4Properties {
|
||||||
self.sample_rate
|
self.sample_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bits per sample
|
||||||
|
pub fn bit_depth(&self) -> Option<u8> {
|
||||||
|
self.bit_depth
|
||||||
|
}
|
||||||
|
|
||||||
/// Channel count
|
/// Channel count
|
||||||
pub fn channels(&self) -> u8 {
|
pub fn channels(&self) -> u8 {
|
||||||
self.channels
|
self.channels
|
||||||
|
@ -201,6 +209,7 @@ where
|
||||||
overall_bitrate: 0,
|
overall_bitrate: 0,
|
||||||
audio_bitrate: 0,
|
audio_bitrate: 0,
|
||||||
sample_rate: 0,
|
sample_rate: 0,
|
||||||
|
bit_depth: None,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -281,7 +290,7 @@ where
|
||||||
// Descriptor length (1)
|
// Descriptor length (1)
|
||||||
// Elementary stream ID (2)
|
// Elementary stream ID (2)
|
||||||
// Flags (1)
|
// Flags (1)
|
||||||
let _info = stsd.read_u32::<BigEndian>()?;
|
stsd.seek(SeekFrom::Current(4))?;
|
||||||
|
|
||||||
// There is another descriptor embedded in the previous one
|
// There is another descriptor embedded in the previous one
|
||||||
let mut specific_config = [0; 4];
|
let mut specific_config = [0; 4];
|
||||||
|
@ -291,12 +300,11 @@ where
|
||||||
if specific_config == [0x04, 0x80, 0x80, 0x80] {
|
if specific_config == [0x04, 0x80, 0x80, 0x80] {
|
||||||
// Skipping 10 bytes
|
// Skipping 10 bytes
|
||||||
// Descriptor length (1)
|
// Descriptor length (1)
|
||||||
// MPEG4 Audio (1)
|
// Codec (1)
|
||||||
// Stream type (1)
|
// Stream type (1)
|
||||||
// Buffer size (3)
|
// Buffer size (3)
|
||||||
// Max bitrate (4)
|
// Max bitrate (4)
|
||||||
let mut info = [0; 10];
|
stsd.seek(SeekFrom::Current(10))?;
|
||||||
stsd.read_exact(&mut info)?;
|
|
||||||
|
|
||||||
let average_bitrate = stsd.read_u32::<BigEndian>()?;
|
let average_bitrate = stsd.read_u32::<BigEndian>()?;
|
||||||
|
|
||||||
|
@ -337,15 +345,20 @@ where
|
||||||
if alac.ident == AtomIdent::Fourcc(*b"alac") {
|
if alac.ident == AtomIdent::Fourcc(*b"alac") {
|
||||||
properties.codec = Mp4Codec::ALAC;
|
properties.codec = Mp4Codec::ALAC;
|
||||||
|
|
||||||
// Skipping 13 bytes
|
// Skipping 9 bytes
|
||||||
// Version (4)
|
// Version (4)
|
||||||
// Samples per frame (4)
|
// Samples per frame (4)
|
||||||
// Compatible version (1)
|
// Compatible version (1)
|
||||||
|
data.seek(SeekFrom::Current(9))?;
|
||||||
|
|
||||||
// Sample size (1)
|
// Sample size (1)
|
||||||
|
properties.bit_depth = Some(data.read_u8()?);
|
||||||
|
|
||||||
|
// Skipping 3 bytes
|
||||||
// Rice history mult (1)
|
// Rice history mult (1)
|
||||||
// Rice initial history (1)
|
// Rice initial history (1)
|
||||||
// Rice parameter limit (1)
|
// Rice parameter limit (1)
|
||||||
data.seek(SeekFrom::Current(13))?;
|
data.seek(SeekFrom::Current(3))?;
|
||||||
|
|
||||||
properties.channels = data.read_u8()?;
|
properties.channels = data.read_u8()?;
|
||||||
|
|
||||||
|
|
55
src/probe.rs
55
src/probe.rs
|
@ -153,48 +153,43 @@ impl<R: Read + Seek> Probe<R> {
|
||||||
// (36 is just a guess as to how long the data for estimating the file type might be)
|
// (36 is just a guess as to how long the data for estimating the file type might be)
|
||||||
let mut buf = [0; 36];
|
let mut buf = [0; 36];
|
||||||
|
|
||||||
// read the first 36 bytes and seek back to the starting position
|
|
||||||
let starting_position = self.inner.stream_position()?;
|
let starting_position = self.inner.stream_position()?;
|
||||||
|
// Read (up to) 36 bytes
|
||||||
let buf_len = std::io::copy(
|
let buf_len = std::io::copy(
|
||||||
&mut self.inner.by_ref().take(buf.len() as u64),
|
&mut self.inner.by_ref().take(buf.len() as u64),
|
||||||
&mut Cursor::new(&mut buf[..]),
|
&mut Cursor::new(&mut buf[..]),
|
||||||
)? as usize;
|
)? as usize;
|
||||||
|
|
||||||
self.inner.seek(SeekFrom::Start(starting_position))?;
|
self.inner.seek(SeekFrom::Start(starting_position))?;
|
||||||
|
|
||||||
// estimate the file type by using these 36 bytes
|
// Guess the file type by using these 36 bytes
|
||||||
// note that any error from `from_buffer_inner` are suppressed, as it returns an error on unknown format
|
// Note that any error from `from_buffer_inner` are suppressed, as it returns an error on unknown format
|
||||||
match FileType::from_buffer_inner(&buf[..buf_len]) {
|
match FileType::from_buffer_inner(&buf[..buf_len]) {
|
||||||
// the file type was guessed based on these bytes
|
// We were able to determine a file type
|
||||||
Ok((Some(f_ty), _)) => Ok(Some(f_ty)),
|
(Some(f_ty), _) => Ok(Some(f_ty)),
|
||||||
// the first data block is ID3 data; this means other data can follow (e.g. APE or MP3 frames)
|
// The file starts with an ID3v2 tag; this means other data can follow (e.g. APE or MP3 frames)
|
||||||
Ok((None, id3_len)) => {
|
(None, Some(id3_len)) => {
|
||||||
// the position right after the ID3 block is the internal size value (id3_len)
|
// `id3_len` is the size of the tag, not including the header (10 bytes)
|
||||||
// added to the length of the ID3 header (which is 10 bytes),
|
|
||||||
// as the size does not include the header itself
|
|
||||||
let position_after_id3_block = self
|
let position_after_id3_block = self
|
||||||
.inner
|
.inner
|
||||||
.seek(SeekFrom::Current(i64::from(10 + id3_len)))?;
|
.seek(SeekFrom::Current(i64::from(10 + id3_len)))?;
|
||||||
|
|
||||||
let file_type_after_id3_block = {
|
// try to guess the file type after the ID3 block by inspecting the first 3 bytes
|
||||||
// try to guess the file type after the ID3 block by inspecting the first 3 bytes
|
let mut ident = [0; 3];
|
||||||
let mut ident = [0; 3];
|
std::io::copy(
|
||||||
std::io::copy(
|
&mut self.inner.by_ref().take(ident.len() as u64),
|
||||||
&mut self.inner.by_ref().take(ident.len() as u64),
|
&mut Cursor::new(&mut ident[..]),
|
||||||
&mut Cursor::new(&mut ident[..]),
|
)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
if &ident == b"MAC" {
|
self.inner.seek(SeekFrom::Start(position_after_id3_block))?;
|
||||||
Ok(Some(FileType::APE))
|
|
||||||
} else {
|
let file_type_after_id3_block = match &ident {
|
||||||
// potentially some junk bytes are between the ID3 block and the following MP3 block
|
b"MAC" => Ok(Some(FileType::APE)),
|
||||||
// search for any possible sync bits after the ID3 block
|
// Search for a frame sync, which may be preceded by junk
|
||||||
self.inner.seek(SeekFrom::Start(position_after_id3_block))?;
|
_ if search_for_frame_sync(&mut self.inner)?.is_some() => {
|
||||||
if search_for_frame_sync(&mut self.inner)?.is_some() {
|
Ok(Some(FileType::MP3))
|
||||||
Ok(Some(FileType::MP3))
|
},
|
||||||
} else {
|
_ => Ok(None),
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// before returning any result for a file type, seek back to the front
|
// before returning any result for a file type, seek back to the front
|
||||||
|
@ -270,7 +265,7 @@ mod tests {
|
||||||
use crate::Probe;
|
use crate::Probe;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mp3_file_id3v2_3() {
|
fn mp3_id3v2_trailing_junk() {
|
||||||
// test data that contains 4 bytes of junk (0x20) between the ID3 portion and the first MP3 frame
|
// test data that contains 4 bytes of junk (0x20) between the ID3 portion and the first MP3 frame
|
||||||
let data: [&[u8]; 4] = [
|
let data: [&[u8]; 4] = [
|
||||||
// ID3v2.3 header (10 bytes)
|
// ID3v2.3 header (10 bytes)
|
||||||
|
|
|
@ -286,41 +286,59 @@ impl FileType {
|
||||||
|
|
||||||
/// Attempts to extract a [`FileType`] from a buffer
|
/// Attempts to extract a [`FileType`] from a buffer
|
||||||
///
|
///
|
||||||
/// NOTE: This is for use in [`Probe::guess_file_type`](crate::Probe::guess_file_type), it
|
/// NOTES:
|
||||||
|
///
|
||||||
|
/// * This is for use in [`Probe::guess_file_type`], it
|
||||||
/// is recommended to use it that way
|
/// is recommended to use it that way
|
||||||
|
/// * This **will not** search past tags at the start of the buffer.
|
||||||
|
/// For this behavior, use [`Probe::guess_file_type`].
|
||||||
|
///
|
||||||
|
/// [`Probe::guess_file_type`]: crate::Probe::guess_file_type
|
||||||
pub fn from_buffer(buf: &[u8]) -> Option<Self> {
|
pub fn from_buffer(buf: &[u8]) -> Option<Self> {
|
||||||
match Self::from_buffer_inner(buf) {
|
match Self::from_buffer_inner(buf) {
|
||||||
Ok((Some(f_ty), _)) => Some(f_ty),
|
(Some(f_ty), _) => Some(f_ty),
|
||||||
|
// We make no attempt to search past an ID3v2 tag here, since
|
||||||
|
// we only provided a fixed-sized buffer to search from.
|
||||||
|
//
|
||||||
|
// That case is handled in `Probe::guess_file_type`
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_buffer_inner(buf: &[u8]) -> Result<(Option<Self>, u32)> {
|
// TODO: APE tags in the beginning of the file
|
||||||
|
pub(crate) fn from_buffer_inner(buf: &[u8]) -> (Option<Self>, Option<u32>) {
|
||||||
use crate::id3::v2::unsynch_u32;
|
use crate::id3::v2::unsynch_u32;
|
||||||
|
|
||||||
|
// Start out with an empty return: (File type, id3 size)
|
||||||
|
// Only one can be set
|
||||||
|
let mut ret = (None, None);
|
||||||
|
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
return Err(LoftyError::EmptyFile);
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
match Self::quick_type_guess(buf) {
|
match Self::quick_type_guess(buf) {
|
||||||
Some(f_ty) => Ok((Some(f_ty), 0)),
|
Some(f_ty) => ret.0 = Some(f_ty),
|
||||||
// Special case for ID3, gets checked in `Probe::guess_file_type`
|
// Special case for ID3, gets checked in `Probe::guess_file_type`
|
||||||
None if buf.len() >= 11 && &buf[..3] == b"ID3" => {
|
// The bare minimum size for an ID3v2 header is 10 bytes
|
||||||
let size = unsynch_u32(u32::from_be_bytes(
|
None if buf.len() >= 10 && &buf[..3] == b"ID3" => {
|
||||||
buf[6..10]
|
// This is infallible, but preferable to an unwrap
|
||||||
.try_into()
|
if let Ok(arr) = buf[6..10].try_into() {
|
||||||
.map_err(|_| LoftyError::UnknownFormat)?,
|
// Set the ID3v2 size
|
||||||
));
|
ret.1 = Some(unsynch_u32(u32::from_be_bytes(arr)));
|
||||||
|
}
|
||||||
Ok((None, size))
|
|
||||||
},
|
},
|
||||||
None => Err(LoftyError::UnknownFormat),
|
// We aren't able to determine a format
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quick_type_guess(buf: &[u8]) -> Option<Self> {
|
fn quick_type_guess(buf: &[u8]) -> Option<Self> {
|
||||||
use crate::mp3::header::verify_frame_sync;
|
use crate::mp3::header::verify_frame_sync;
|
||||||
|
|
||||||
|
// Safe to unwrap, since we return early on an empty buffer
|
||||||
match buf.first().unwrap() {
|
match buf.first().unwrap() {
|
||||||
77 if buf.starts_with(b"MAC") => Some(Self::APE),
|
77 if buf.starts_with(b"MAC") => Some(Self::APE),
|
||||||
255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => Some(Self::MP3),
|
255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => Some(Self::MP3),
|
||||||
|
|
BIN
tests/files/assets/b.m4a
Normal file
BIN
tests/files/assets/b.m4a
Normal file
Binary file not shown.
|
@ -46,6 +46,17 @@ const MP4_PROPERTIES: Mp4Properties = Mp4Properties::new(
|
||||||
135,
|
135,
|
||||||
124,
|
124,
|
||||||
48000,
|
48000,
|
||||||
|
None,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ALAC_PROPERTIES: Mp4Properties = Mp4Properties::new(
|
||||||
|
Mp4Codec::ALAC,
|
||||||
|
Duration::from_millis(1428),
|
||||||
|
331,
|
||||||
|
124,
|
||||||
|
48000,
|
||||||
|
Some(16),
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -126,6 +137,14 @@ fn mp4_properties() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alac_properties() {
|
||||||
|
assert_eq!(
|
||||||
|
get_properties::<Mp4File>("tests/files/assets/b.m4a").bit_depth(),
|
||||||
|
ALAC_PROPERTIES.bit_depth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opus_properties() {
|
fn opus_properties() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
Loading…
Add table
Reference in a new issue