Support the ALS codec for MP4; Fix MP4 property reading

This commit is contained in:
Serial 2022-03-08 23:08:04 -05:00
parent fb0b6e0c1f
commit 768fc8d875
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
4 changed files with 136 additions and 20 deletions

View file

@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `TagItem::{into_key, into_value, consume}`
- **MP4**: `Mp4Codec::ALS`
### Changed
- **MP4**: Sample rates are now retrieved from the [audio specific config](https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config) (if possible).
If the information is invalid or unavailable, the existing value from the `mp4a` box will be used instead.
### Fixed
- **MP4**: Non-full `meta` atoms are now properly handled.
@ -19,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- https://leo-van-stee.github.io/
- https://github.com/axiomatic-systems/Bento4/blob/v1.6.0-639/Source/C%2B%2B/Core/Ap4ContainerAtom.cpp#L60
- https://github.com/taglib/taglib/issues/1041
- **MP4**: Properly search for `soun` atom
- The search wasn't adding read bytes correctly, but tests passed due to the atom being immediately available.
It would attempt to read until it reached an EOF if it managed to make it through multiple iterations.
## [0.5.3] - 2022-03-03

View file

@ -18,6 +18,7 @@ use byteorder::{BigEndian, ReadBytesExt};
pub enum Mp4Codec {
AAC,
ALAC,
ALS,
Unknown(String),
}
@ -112,9 +113,9 @@ where
data.seek(SeekFrom::Start(mdia.start + 8))?;
let mut read = 8;
while read < mdia.len {
let atom = AtomInfo::read(data)?;
read += atom.len;
if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc {
@ -138,7 +139,6 @@ where
b"minf" => minf = Some(atom),
_ => {
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len;
},
}
@ -146,7 +146,6 @@ where
}
skip_unneeded(data, atom.extended, atom.len)?;
read += atom.len;
}
}
@ -240,6 +239,16 @@ fn mp4a_properties<R>(stsd: &mut R, properties: &mut Mp4Properties, file_length:
where
R: Read + Seek,
{
const ELEMENTARY_DESCRIPTOR_TAG: u8 = 0x03;
const DECODER_CONFIG_TAG: u8 = 0x04;
const DECODER_SPECIFIC_DESCRIPTOR_TAG: u8 = 0x05;
// https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies
const SAMPLE_RATES: [u32; 15] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0,
0,
];
properties.codec = Mp4Codec::AAC;
// Skipping 16 bytes
@ -267,34 +276,89 @@ where
// Version (1)
// Flags (3)
if esds.ident == AtomIdent::Fourcc(*b"esds") && stsd.read_u32::<BigEndian>()? == 0 {
let mut descriptor = [0; 4];
stsd.read_exact(&mut descriptor)?;
// [0x03, 0x80, 0x80, 0x80] marks the start of the elementary stream descriptor.
// 0x03 being the object descriptor
if descriptor == [0x03, 0x80, 0x80, 0x80] {
// Skipping 4 bytes
// Descriptor length (1)
let descriptor = Descriptor::read(stsd)?;
if descriptor.tag == ELEMENTARY_DESCRIPTOR_TAG {
// Skipping 3 bytes
// Elementary stream ID (2)
// Flags (1)
stsd.seek(SeekFrom::Current(4))?;
stsd.seek(SeekFrom::Current(3))?;
// There is another descriptor embedded in the previous one
let mut specific_config = [0; 4];
stsd.read_exact(&mut specific_config)?;
// [0x04, 0x80, 0x80, 0x80] marks the start of the descriptor configuration
if specific_config == [0x04, 0x80, 0x80, 0x80] {
// Skipping 10 bytes
// Descriptor length (1)
let descriptor = Descriptor::read(stsd)?;
if descriptor.tag == DECODER_CONFIG_TAG {
// Skipping 9 bytes
// Codec (1)
// Stream type (1)
// Buffer size (3)
// Max bitrate (4)
stsd.seek(SeekFrom::Current(10))?;
stsd.seek(SeekFrom::Current(9))?;
let average_bitrate = stsd.read_u32::<BigEndian>()?;
// Yet another descriptor to check
let descriptor = Descriptor::read(stsd)?;
if descriptor.tag == DECODER_SPECIFIC_DESCRIPTOR_TAG {
// We just check for ALS here, might extend it for more codes eventually
// https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
//
// 5 bits: object type (profile)
// if (object type == 31)
// 6 bits + 32: object type
// 4 bits: frequency index
// if (frequency index == 15)
// 24 bits: frequency
// 4 bits: channel configuration
let mut profile = stsd.read_u8()?;
let byte_b = stsd.read_u8()?;
let mut frequency_index = (profile << 5) | (byte_b >> 7);
let mut extended_frequency_byte = None;
if (profile >> 3) == 31 {
profile = ((profile & 7) | (byte_b >> 5)) + 32;
let frequency_ext = stsd.read_u8()?;
frequency_index = (byte_b & 0x0F) | (frequency_ext & 1);
extended_frequency_byte = Some(frequency_ext);
}
// TODO: Channels
match frequency_index {
// 15 means the sample rate is stored in the next 24 bits
0x0F => {
if let Some(byte) = extended_frequency_byte {
let remaining_sample_rate =
u32::from(stsd.read_u16::<BigEndian>()?);
properties.sample_rate =
u32::from(byte >> 1) | remaining_sample_rate;
} else {
properties.sample_rate = stsd.read_uint::<BigEndian>(3)? as u32
}
},
i if i < SAMPLE_RATES.len() as u8 => {
properties.sample_rate = SAMPLE_RATES[i as usize]
},
// Keep the sample rate we read above
_ => {},
}
// https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types
if profile == 36 {
let mut ident = [0; 5];
stsd.read_exact(&mut ident)?;
if &ident == b"\0ALS\0" {
properties.codec = Mp4Codec::ALS;
properties.sample_rate = stsd.read_u32::<BigEndian>()?;
// Sample count
stsd.seek(SeekFrom::Current(4))?;
properties.channels = stsd.read_u16::<BigEndian>()? as u8 + 1;
}
}
}
let overall_bitrate =
u128::from(file_length * 8) / properties.duration.as_millis();
@ -366,3 +430,26 @@ where
Ok(())
}
struct Descriptor {
tag: u8,
_size: u32,
}
impl Descriptor {
fn read<R: Read>(reader: &mut R) -> Result<Descriptor> {
let tag = reader.read_u8()?;
// https://github.com/FFmpeg/FFmpeg/blob/84f5583078699e96b040f4f41b39720b683326d0/libavformat/isom.c#L283
let mut size: u32 = 0;
for _ in 0..4 {
let b = reader.read_u8()?;
size = (size << 7) | u32::from(b & 0x7F);
if b & 0x80 == 0 {
break;
}
}
Ok(Descriptor { tag, _size: size })
}
}

View file

@ -72,6 +72,9 @@ mod tests {
use std::fs::File;
use std::time::Duration;
// These values are taken from FFmpeg's ffprobe
// They may be *slightly* different due to how ffprobe rounds
const AIFF_PROPERTIES: FileProperties = FileProperties {
duration: Duration::from_millis(1428),
overall_bitrate: Some(1542),
@ -135,6 +138,16 @@ mod tests {
channels: 2,
};
const MP4_ALS_PROPERTIES: Mp4Properties = Mp4Properties {
codec: Mp4Codec::ALS,
duration: Duration::from_millis(1429),
overall_bitrate: 1083,
audio_bitrate: 1078,
sample_rate: 48000,
bit_depth: None,
channels: 2,
};
const OPUS_PROPERTIES: OpusProperties = OpusProperties {
duration: Duration::from_millis(1428),
overall_bitrate: 120,
@ -238,6 +251,14 @@ mod tests {
)
}
#[test]
fn mp4_als_properties() {
assert_eq!(
get_properties::<Mp4File>("tests/files/assets/minimal/mp4_codec_als.mp4"),
MP4_ALS_PROPERTIES
)
}
#[test]
fn opus_properties() {
assert_eq!(

Binary file not shown.