Add support for AAC (#71)

This commit is contained in:
Alex 2022-10-14 09:06:27 -04:00 committed by GitHub
parent 78723a7dd0
commit 02f1314005
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 686 additions and 32 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
eagerness and other settings. Previously, when reading a file the only option available was eagerness and other settings. Previously, when reading a file the only option available was
`read_properties`, specified with a `bool` in `read_from{_path}`. This will now default to `true`, `read_properties`, specified with a `bool` in `read_from{_path}`. This will now default to `true`,
and can be overridden when using `Probe`. and can be overridden when using `Probe`.
- **🎉 Support for AAC (ADTS) files** ([issue](https://github.com/Serial-ATA/lofty-rs/issues/58))
- **FileProperties**: `FileProperties::new` - **FileProperties**: `FileProperties::new`
- Debug logging via the [log](https://crates.io/crates/log) crate for exposing recoverable errors. - Debug logging via the [log](https://crates.io/crates/log) crate for exposing recoverable errors.
- **Error**: `ErrorKind::SizeMismatch` - **Error**: `ErrorKind::SizeMismatch`

View file

@ -1,5 +1,6 @@
| File Format | Metadata Format(s) | | File Format | Metadata Format(s) |
|-------------|--------------------------------------| |-------------|--------------------------------------|
| AAC (ADTS) | `ID3v2`, `ID3v1` |
| Ape | `APEv2`, `APEv1`, `ID3v2`\*, `ID3v1` | | Ape | `APEv2`, `APEv1`, `ID3v2`\*, `ID3v1` |
| AIFF | `ID3v2`, `Text Chunks` | | AIFF | `ID3v2`, `Text Chunks` |
| FLAC | `Vorbis Comments`, `ID3v2`\* | | FLAC | `Vorbis Comments`, `ID3v2`\* |

View file

@ -30,6 +30,7 @@ fn content_infer_read(c: &mut Criterion) {
test_read_file!( test_read_file!(
c, c,
[ [
(AAC, "../tests/files/assets/minimal/full_test.aac"),
(AIFF, "../tests/files/assets/minimal/full_test.aiff"), (AIFF, "../tests/files/assets/minimal/full_test.aiff"),
(APE, "../tests/files/assets/minimal/full_test.ape"), (APE, "../tests/files/assets/minimal/full_test.ape"),
(FLAC, "../tests/files/assets/minimal/full_test.flac"), (FLAC, "../tests/files/assets/minimal/full_test.flac"),

View file

@ -26,6 +26,10 @@ path = "fuzz_targets/filetype_from_buffer.rs"
name = "mp3file_read_from" name = "mp3file_read_from"
path = "fuzz_targets/mpegfile_read_from.rs" path = "fuzz_targets/mpegfile_read_from.rs"
[[bin]]
name = "aacfile_read_from"
path = "fuzz_targets/aacfile_read_from.rs"
[[bin]] [[bin]]
name = "aifffile_read_from" name = "aifffile_read_from"
path = "fuzz_targets/aifffile_read_from.rs" path = "fuzz_targets/aifffile_read_from.rs"

View file

@ -0,0 +1,13 @@
#![no_main]
use std::io::Cursor;
use libfuzzer_sys::fuzz_target;
use lofty::{AudioFile, ParseOptions};
fuzz_target!(|data: Vec<u8>| {
let _ = lofty::aac::AACFile::read_from(
&mut Cursor::new(data),
ParseOptions::new().read_properties(false),
);
});

View file

@ -9,8 +9,8 @@ use quote::quote;
pub(crate) fn opt_internal_file_type( pub(crate) fn opt_internal_file_type(
struct_name: String, struct_name: String,
) -> Option<(proc_macro2::TokenStream, bool)> { ) -> Option<(proc_macro2::TokenStream, bool)> {
const LOFTY_FILE_TYPES: [&str; 10] = [ const LOFTY_FILE_TYPES: [&str; 11] = [
"AIFF", "APE", "FLAC", "MPEG", "MP4", "Opus", "Vorbis", "Speex", "WAV", "WavPack", "AAC", "AIFF", "APE", "FLAC", "MPEG", "MP4", "Opus", "Vorbis", "Speex", "WAV", "WavPack",
]; ];
const ID3V2_STRIPPABLE: [&str; 1] = ["APE"]; const ID3V2_STRIPPABLE: [&str; 1] = ["APE"];

123
src/aac/header.rs Normal file
View file

@ -0,0 +1,123 @@
use crate::error::Result;
use crate::macros::decode_err;
use crate::mp4::{AudioObjectType, SAMPLE_RATES};
use crate::mpeg::MpegVersion;
use crate::probe::ParsingMode;
use std::io::{Read, Seek, SeekFrom};
// Used to compare the headers up to the home bit.
// If they aren't equal, something is broken.
pub(super) const HEADER_MASK: u32 = 0xFFFF_FFE0;
#[derive(Copy, Clone)]
pub(crate) struct ADTSHeader {
pub(crate) version: MpegVersion,
pub(crate) audio_object_ty: AudioObjectType,
pub(crate) sample_rate: u32,
pub(crate) channels: u8,
pub(crate) copyright: bool,
pub(crate) original: bool,
pub(crate) len: u16,
pub(crate) bitrate: u32,
pub(crate) bytes: [u8; 7],
pub(crate) has_crc: bool,
}
impl ADTSHeader {
pub(super) fn read<R>(reader: &mut R, _parse_mode: ParsingMode) -> Result<Option<Self>>
where
R: Read + Seek,
{
// The ADTS header consists of 7 bytes, or 9 bytes with a CRC
let mut needs_crc_skip = false;
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
let mut header = [0; 7];
reader.read_exact(&mut header)?;
// Letter Length (bits) Description
// A 12 Syncword, all bits must be set to 1.
// B 1 MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2.
// C 2 Layer, always set to 0.
// D 1 Protection absence, set to 1 if there is no CRC and 0 if there is CRC.
// E 2 Profile, the MPEG-4 Audio Object Type minus 1.
// F 4 MPEG-4 Sampling Frequency Index (15 is forbidden).
// G 1 Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding.
// H 3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE (Program Config Element)).
// I 1 Originality, set to 1 to signal originality of the audio and 0 otherwise.
// J 1 Home, set to 1 to signal home usage of the audio and 0 otherwise.
// K 1 Copyright ID bit, the next bit of a centrally registered copyright identifier. This is transmitted by sliding over the bit-string in LSB-first order and putting the current bit value in this field and wrapping to start if reached end (circular buffer).
// L 1 Copyright ID start, signals that this frame's Copyright ID bit is the first one by setting 1 and 0 otherwise.
// M 13 Frame length, length of the ADTS frame including headers and CRC check.
// O 11 Buffer fullness, states the bit-reservoir per frame.
// P 2 Number of AAC frames (RDBs (Raw Data Blocks)) in ADTS frame minus 1. For maximum compatibility always use one AAC frame per ADTS frame.
// Q 16 CRC check (as of ISO/IEC 11172-3, subclause 2.4.3.1), if Protection absent is 0.
// AAAABCCD
let byte2 = header[1];
let version = match (byte2 >> 3) & 0b1 {
0 => MpegVersion::V4,
1 => MpegVersion::V2,
_ => unreachable!(),
};
if byte2 & 0b1 == 0 {
needs_crc_skip = true;
}
// EEFFFFGH
let byte3 = header[2];
let audio_object_ty = match ((byte3 >> 6) & 0b11) + 1 {
1 => AudioObjectType::AacMain,
2 => AudioObjectType::AacLowComplexity,
3 => AudioObjectType::AacScalableSampleRate,
4 => AudioObjectType::AacLongTermPrediction,
_ => unreachable!(),
};
let sample_rate_idx = (byte3 >> 2) & 0b1111;
if sample_rate_idx == 15 {
// 15 is forbidden
decode_err!(@BAIL AAC, "File contains an invalid sample frequency index");
}
let sample_rate = SAMPLE_RATES[sample_rate_idx as usize];
// HHIJKLMM
let byte4 = header[3];
let channel_configuration = ((byte3 & 0b1) << 2) | ((byte4 >> 6) & 0b11);
let original = (byte4 >> 5) & 0b1 == 1;
let copyright = (byte4 >> 4) & 0b1 == 1;
// MMMMMMMM
let byte5 = header[4];
// MMMOOOOO
let byte6 = header[5];
let len = (u16::from(byte4 & 0b11) << 11) | u16::from(byte5) << 3 | u16::from(byte6) >> 5;
let bitrate = ((u32::from(len) * sample_rate / 1024) * 8) / 1024;
if needs_crc_skip {
reader.seek(SeekFrom::Current(2))?;
}
Ok(Some(ADTSHeader {
version,
audio_object_ty,
sample_rate,
channels: channel_configuration,
copyright,
original,
len,
bitrate,
bytes: header,
has_crc: needs_crc_skip,
}))
}
}

30
src/aac/mod.rs Normal file
View file

@ -0,0 +1,30 @@
//! AAC (ADTS) specific items
// TODO: Currently we only support ADTS, might want to look into ADIF in the future.
mod header;
mod properties;
mod read;
use crate::id3::v1::tag::ID3v1Tag;
use crate::id3::v2::tag::ID3v2Tag;
use lofty_attr::LoftyFile;
// Exports
pub use properties::AACProperties;
/// An AAC (ADTS) file
#[derive(LoftyFile, Default)]
#[lofty(read_fn = "read::read_from")]
#[lofty(internal_write_module_do_not_use_anywhere_else)]
pub struct AACFile {
#[cfg(feature = "id3v2")]
#[lofty(tag_type = "ID3v2")]
pub(crate) id3v2_tag: Option<ID3v2Tag>,
#[cfg(feature = "id3v1")]
#[lofty(tag_type = "ID3v1")]
pub(crate) id3v1_tag: Option<ID3v1Tag>,
pub(crate) properties: AACProperties,
}

108
src/aac/properties.rs Normal file
View file

@ -0,0 +1,108 @@
use crate::aac::header::ADTSHeader;
use crate::mp4::AudioObjectType;
use crate::mpeg::header::MpegVersion;
use crate::properties::FileProperties;
use std::time::Duration;
/// An AAC file's audio properties
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AACProperties {
pub(crate) version: MpegVersion,
pub(crate) audio_object_type: AudioObjectType,
pub(crate) duration: Duration,
pub(crate) overall_bitrate: u32,
pub(crate) audio_bitrate: u32,
pub(crate) sample_rate: u32,
pub(crate) channels: u8,
pub(crate) copyright: bool,
pub(crate) original: bool,
}
impl AACProperties {
/// MPEG version
pub fn version(&self) -> MpegVersion {
self.version
}
/// Audio object type
///
/// The only possible variants are:
///
/// * [AudioObjectType::AacMain]
/// * [AudioObjectType::AacLowComplexity]
/// * [AudioObjectType::AacScalableSampleRate]
/// * [AudioObjectType::AacLongTermPrediction]
pub fn audio_object_type(&self) -> AudioObjectType {
self.audio_object_type
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Whether the audio is copyrighted
pub fn copyright(&self) -> bool {
self.copyright
}
/// Whether the media is original or a copy
pub fn original(&self) -> bool {
self.original
}
}
impl From<AACProperties> for FileProperties {
fn from(input: AACProperties) -> Self {
FileProperties {
duration: input.duration,
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
bit_depth: None,
channels: Some(input.channels),
}
}
}
pub(super) fn read_properties(
properties: &mut AACProperties,
first_frame: ADTSHeader,
stream_len: u64,
) {
properties.version = first_frame.version;
properties.audio_object_type = first_frame.audio_object_ty;
properties.sample_rate = first_frame.sample_rate;
properties.channels = first_frame.channels;
properties.copyright = first_frame.copyright;
properties.original = first_frame.original;
let bitrate = first_frame.bitrate;
if bitrate > 0 {
properties.audio_bitrate = bitrate;
properties.overall_bitrate = bitrate;
properties.duration = Duration::from_millis((stream_len * 8) / u64::from(bitrate));
}
}

169
src/aac/read.rs Normal file
View file

@ -0,0 +1,169 @@
use super::header::{ADTSHeader, HEADER_MASK};
use super::AACFile;
use crate::error::Result;
use crate::id3::v2::read::parse_id3v2;
use crate::id3::v2::read_id3v2_header;
use crate::id3::{find_id3v1, ID3FindResults};
use crate::macros::{decode_err, parse_mode_choice};
use crate::mpeg::header::{cmp_header, search_for_frame_sync, HeaderCmpResult};
use crate::probe::{ParseOptions, ParsingMode};
use std::io::{Read, Seek, SeekFrom};
use byteorder::ReadBytesExt;
#[allow(clippy::unnecessary_wraps)]
pub(super) fn read_from<R>(reader: &mut R, parse_options: ParseOptions) -> Result<AACFile>
where
R: Read + Seek,
{
let parse_mode = parse_options.parsing_mode;
let mut file = AACFile::default();
let mut first_frame_header = None;
let mut first_frame_end = 0;
// Skip any invalid padding
while reader.read_u8()? == 0 {}
reader.seek(SeekFrom::Current(-1))?;
let pos = reader.stream_position()?;
let mut stream_len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(pos))?;
let mut header = [0; 4];
while let Ok(()) = reader.read_exact(&mut header) {
match header {
// [I, D, 3, ver_major, ver_minor, flags, size (4 bytes)]
[b'I', b'D', b'3', ..] => {
// Seek back to read the tag in full
reader.seek(SeekFrom::Current(-4))?;
let header = read_id3v2_header(reader)?;
let skip_footer = header.flags.footer;
stream_len -= u64::from(header.size);
#[cfg(feature = "id3v2")]
{
let id3v2 = parse_id3v2(reader, header)?;
file.id3v2_tag = Some(id3v2);
}
// Skip over the footer
if skip_footer {
stream_len -= 10;
reader.seek(SeekFrom::Current(10))?;
}
continue;
},
// Tags might be followed by junk bytes before the first ADTS frame begins
_ => {
// Seek back the length of the temporary header buffer, to include them
// in the frame sync search
#[allow(clippy::neg_multiply)]
reader.seek(SeekFrom::Current(-1 * header.len() as i64))?;
if let Some((first_frame_header_, first_frame_end_)) =
find_next_frame(reader, parse_mode)?
{
first_frame_header = Some(first_frame_header_);
first_frame_end = first_frame_end_;
break;
}
},
}
}
#[allow(unused_variables)]
let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?;
#[cfg(feature = "id3v1")]
if header.is_some() {
stream_len -= 128;
file.id3v1_tag = id3v1;
}
if parse_options.read_properties {
let mut first_frame_header = match first_frame_header {
Some(header) => header,
// The search for sync bits was unsuccessful
None => decode_err!(@BAIL MPEG, "File contains an invalid frame"),
};
if first_frame_header.sample_rate == 0 {
parse_mode_choice!(
parse_mode,
STRICT: decode_err!(@BAIL MPEG, "Sample rate is 0"),
);
}
if first_frame_header.bitrate == 0 {
parse_mode_choice!(parse_mode, STRICT: decode_err!(@BAIL MPEG, "Bitrate is 0"),);
}
// Read as many frames as we can to try and fine the average bitrate
reader.seek(SeekFrom::Start(first_frame_end))?;
let mut frame_count = 1;
while let Some((header, _)) = find_next_frame(reader, parse_mode)? {
first_frame_header.bitrate += header.bitrate;
frame_count += 1u32;
}
first_frame_header.bitrate /= frame_count;
super::properties::read_properties(&mut file.properties, first_frame_header, stream_len);
}
Ok(file)
}
// Searches for the next frame, comparing it to the following one
fn find_next_frame<R>(
reader: &mut R,
parsing_mode: ParsingMode,
) -> Result<Option<(ADTSHeader, u64)>>
where
R: Read + Seek,
{
let mut pos = reader.stream_position()?;
while let Ok(Some(first_adts_frame_start_relative)) = search_for_frame_sync(reader) {
let first_adts_frame_start_absolute = pos + first_adts_frame_start_relative;
// Seek back to the start of the frame and read the header
reader.seek(SeekFrom::Start(first_adts_frame_start_absolute))?;
if let Some(first_header) = ADTSHeader::read(reader, parsing_mode)? {
let header_len = if first_header.has_crc { 9 } else { 7 };
match cmp_header(
reader,
header_len,
u32::from(first_header.len),
u32::from_be_bytes(first_header.bytes[..4].try_into().unwrap()),
HEADER_MASK,
) {
HeaderCmpResult::Equal => {
return Ok(Some((
first_header,
first_adts_frame_start_absolute + u64::from(header_len),
)))
},
HeaderCmpResult::Undetermined => return Ok(None),
HeaderCmpResult::NotEqual => {},
}
}
pos = reader.stream_position()?;
}
Ok(None)
}

View file

@ -482,6 +482,7 @@ impl AudioFile for TaggedFile {
#[non_exhaustive] #[non_exhaustive]
/// The type of file read /// The type of file read
pub enum FileType { pub enum FileType {
AAC,
AIFF, AIFF,
APE, APE,
FLAC, FLAC,
@ -500,7 +501,7 @@ impl FileType {
/// ///
/// | [`FileType`] | [`TagType`] | /// | [`FileType`] | [`TagType`] |
/// |-----------------------------------|------------------| /// |-----------------------------------|------------------|
/// | `AIFF`, `MP3`, `WAV` | `ID3v2` | /// | `AAC`, `AIFF`, `MP3`, `WAV` | `ID3v2` |
/// | `APE` , `WavPack` | `APE` | /// | `APE` , `WavPack` | `APE` |
/// | `FLAC`, `Opus`, `Vorbis`, `Speex` | `VorbisComments` | /// | `FLAC`, `Opus`, `Vorbis`, `Speex` | `VorbisComments` |
/// | `MP4` | `MP4ilst` | /// | `MP4` | `MP4ilst` |
@ -519,7 +520,7 @@ impl FileType {
/// ``` /// ```
pub fn primary_tag_type(&self) -> TagType { pub fn primary_tag_type(&self) -> TagType {
match self { match self {
FileType::AIFF | FileType::MPEG | FileType::WAV => TagType::ID3v2, FileType::AIFF | FileType::MPEG | FileType::WAV | FileType::AAC => TagType::ID3v2,
FileType::APE | FileType::WavPack => TagType::APE, FileType::APE | FileType::WavPack => TagType::APE,
FileType::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => { FileType::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => {
TagType::VorbisComments TagType::VorbisComments
@ -553,7 +554,7 @@ impl FileType {
pub fn supports_tag_type(&self, tag_type: TagType) -> bool { pub fn supports_tag_type(&self, tag_type: TagType) -> bool {
match self { match self {
#[cfg(feature = "id3v2")] #[cfg(feature = "id3v2")]
FileType::AIFF | FileType::APE | FileType::MPEG | FileType::WAV FileType::AIFF | FileType::APE | FileType::MPEG | FileType::WAV | FileType::AAC
if tag_type == TagType::ID3v2 => if tag_type == TagType::ID3v2 =>
{ {
true true
@ -561,7 +562,11 @@ impl FileType {
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
FileType::AIFF if tag_type == TagType::AIFFText => true, FileType::AIFF if tag_type == TagType::AIFFText => true,
#[cfg(feature = "id3v1")] #[cfg(feature = "id3v1")]
FileType::APE | FileType::MPEG | FileType::WavPack if tag_type == TagType::ID3v1 => true, FileType::APE | FileType::MPEG | FileType::WavPack | FileType::AAC
if tag_type == TagType::ID3v1 =>
{
true
},
#[cfg(feature = "ape")] #[cfg(feature = "ape")]
FileType::APE | FileType::MPEG | FileType::WavPack if tag_type == TagType::APE => true, FileType::APE | FileType::MPEG | FileType::WavPack if tag_type == TagType::APE => true,
#[cfg(feature = "vorbis_comments")] #[cfg(feature = "vorbis_comments")]
@ -597,6 +602,7 @@ impl FileType {
let ext = ext.as_ref().to_str()?.to_ascii_lowercase(); let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
match ext.as_str() { match ext.as_str() {
"aac" => Some(Self::AAC),
"ape" => Some(Self::APE), "ape" => Some(Self::APE),
"aiff" | "aif" | "afc" | "aifc" => Some(Self::AIFF), "aiff" | "aif" | "afc" | "aifc" => Some(Self::AIFF),
"mp3" | "mp2" | "mp1" => Some(Self::MPEG), "mp3" | "mp2" | "mp1" => Some(Self::MPEG),
@ -716,7 +722,40 @@ impl FileType {
// Safe to index, since we return early on an empty buffer // Safe to index, since we return early on an empty buffer
match buf[0] { match buf[0] {
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::MPEG), 255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => {
// ADTS and MPEG frame headers are way too similar
// ADTS (https://wiki.multimedia.cx/index.php/ADTS#Header):
//
// AAAAAAAA AAAABCCX
//
// Letter Length (bits) Description
// A 12 Syncword, all bits must be set to 1.
// B 1 MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2.
// C 2 Layer, always set to 0.
// MPEG (http://www.mp3-tech.org/programmer/frame_header.html):
//
// AAAAAAAA AAABBCCX
//
// Letter Length (bits) Description
// A 11 Syncword, all bits must be set to 1.
// B 2 MPEG Audio version ID
// C 2 Layer description
// The subtle overlap in the ADTS header's frame sync and MPEG's version ID
// is the first condition to check. However, since 0b10 and 0b11 are valid versions
// in MPEG, we have to also check the layer.
// So, if we have a version 1 (0b11) or version 2 (0b10) MPEG frame AND a layer of 0b00,
// we can assume we have an ADTS header. Awesome!
if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 {
return Some(Self::AAC);
}
Some(Self::MPEG)
},
70 if buf.len() >= 12 && &buf[..4] == b"FORM" => { 70 if buf.len() >= 12 && &buf[..4] == b"FORM" => {
let id = &buf[8..12]; let id = &buf[8..12];

View file

@ -47,7 +47,10 @@ macro_rules! impl_accessor {
/// * [`GENRES`] contains the string /// * [`GENRES`] contains the string
/// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8` /// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8`
#[derive(Default, Debug, PartialEq, Eq, Clone)] #[derive(Default, Debug, PartialEq, Eq, Clone)]
#[tag(description = "An ID3v1 tag", supported_formats(APE, MPEG, WavPack))] #[tag(
description = "An ID3v1 tag",
supported_formats(AAC, APE, MPEG, WavPack)
)]
pub struct ID3v1Tag { pub struct ID3v1Tag {
/// Track title, 30 bytes max /// Track title, 30 bytes max
pub title: Option<String>, pub title: Option<String>,

View file

@ -92,7 +92,7 @@ macro_rules! impl_accessor {
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
#[tag( #[tag(
description = "An `ID3v2` tag", description = "An `ID3v2` tag",
supported_formats(AIFF, MPEG, WAV, read_only(FLAC, APE)) supported_formats(AAC, AIFF, MPEG, WAV, read_only(FLAC, APE))
)] )]
pub struct ID3v2Tag { pub struct ID3v2Tag {
flags: ID3v2TagFlags, flags: ID3v2TagFlags,

View file

@ -8,6 +8,7 @@ use crate::id3::find_id3v2;
use crate::id3::v2::frame::FrameRef; use crate::id3::v2::frame::FrameRef;
use crate::id3::v2::tag::Id3v2TagRef; use crate::id3::v2::tag::Id3v2TagRef;
use crate::id3::v2::util::synch_u32; use crate::id3::v2::util::synch_u32;
use crate::id3::v2::ID3v2Tag;
use crate::macros::err; use crate::macros::err;
use crate::probe::Probe; use crate::probe::Probe;
@ -15,7 +16,6 @@ use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::ops::Not; use std::ops::Not;
use crate::id3::v2::ID3v2Tag;
use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; use byteorder::{BigEndian, LittleEndian, WriteBytesExt};
// In the very rare chance someone wants to write a CRC in their extended header // In the very rare chance someone wants to write a CRC in their extended header

View file

@ -166,6 +166,7 @@
extern crate self as lofty; extern crate self as lofty;
pub(crate) mod _this_is_internal {} pub(crate) mod _this_is_internal {}
pub mod aac;
pub mod ape; pub mod ape;
pub mod error; pub mod error;
pub(crate) mod file; pub(crate) mod file;

View file

@ -31,6 +31,8 @@ cfg_if::cfg_if! {
pub use crate::mp4::properties::{AudioObjectType, Mp4Codec, Mp4Properties}; pub use crate::mp4::properties::{AudioObjectType, Mp4Codec, Mp4Properties};
pub(crate) use properties::SAMPLE_RATES;
/// An MP4 file /// An MP4 file
#[derive(LoftyFile)] #[derive(LoftyFile)]
#[lofty(read_fn = "read::read_from")] #[lofty(read_fn = "read::read_from")]

View file

@ -360,6 +360,11 @@ where
Ok(properties) Ok(properties)
} }
// https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies
pub(crate) const SAMPLE_RATES: [u32; 15] = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0, 0,
];
fn mp4a_properties<R>(stsd: &mut AtomReader<R>, properties: &mut Mp4Properties) -> Result<()> fn mp4a_properties<R>(stsd: &mut AtomReader<R>, properties: &mut Mp4Properties) -> Result<()>
where where
R: Read + Seek, R: Read + Seek,
@ -368,12 +373,6 @@ where
const DECODER_CONFIG_TAG: u8 = 0x04; const DECODER_CONFIG_TAG: u8 = 0x04;
const DECODER_SPECIFIC_DESCRIPTOR_TAG: u8 = 0x05; 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,
];
// Set the codec to AAC, which is a good guess if we fail before reaching the `esds` // Set the codec to AAC, which is a good guess if we fail before reaching the `esds`
properties.codec = Mp4Codec::AAC; properties.codec = Mp4Codec::AAC;

View file

@ -70,39 +70,49 @@ where
ret ret
} }
pub(super) enum HeaderCmpResult { pub(crate) enum HeaderCmpResult {
Equal, Equal,
Undetermined, Undetermined,
NotEqual, NotEqual,
} }
pub(super) fn cmp_header<R>( // Used to compare the versions, layers, and sample rates of two frame headers.
// If they aren't equal, something is broken.
pub(super) const HEADER_MASK: u32 = 0xFFFE_0C00;
pub(crate) fn cmp_header<R>(
reader: &mut R, reader: &mut R,
header_size: u32,
first_header_len: u32, first_header_len: u32,
first_header_bytes: u32, first_header_bytes: u32,
header_mask: u32,
) -> HeaderCmpResult ) -> HeaderCmpResult
where where
R: Read + Seek, R: Read + Seek,
{ {
// Used to compare the versions, layers, and sample rates of two frame headers.
// If they aren't equal, something is broken.
const HEADER_MASK: u32 = 0xFFFE_0C00;
// Read the next header and see if they are the same // Read the next header and see if they are the same
let res = reader.seek(SeekFrom::Current(i64::from( let res = reader.seek(SeekFrom::Current(i64::from(
first_header_len.saturating_sub(4), first_header_len.saturating_sub(header_size),
))); )));
if res.is_err() { if res.is_err() {
return HeaderCmpResult::Undetermined; return HeaderCmpResult::Undetermined;
} }
match reader.read_u32::<BigEndian>() { let second_header_data = reader.read_u32::<BigEndian>();
if second_header_data.is_err() {
return HeaderCmpResult::Undetermined;
}
if reader.seek(SeekFrom::Current(-4)).is_err() {
return HeaderCmpResult::Undetermined;
}
match second_header_data {
Ok(second_header_data) Ok(second_header_data)
if first_header_bytes & HEADER_MASK == second_header_data & HEADER_MASK => if first_header_bytes & header_mask == second_header_data & header_mask =>
{ {
HeaderCmpResult::Equal HeaderCmpResult::Equal
}, },
Err(_) => HeaderCmpResult::Undetermined,
_ => HeaderCmpResult::NotEqual, _ => HeaderCmpResult::NotEqual,
} }
} }
@ -114,6 +124,8 @@ pub enum MpegVersion {
V1, V1,
V2, V2,
V2_5, V2_5,
/// Exclusive to AAC
V4,
} }
impl Default for MpegVersion { impl Default for MpegVersion {

View file

@ -1,6 +1,6 @@
use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, XingHeader}; use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, XingHeader};
use crate::error::Result; use crate::error::Result;
use crate::mpeg::header::{cmp_header, rev_search_for_frame_sync, HeaderCmpResult}; use crate::mpeg::header::{cmp_header, rev_search_for_frame_sync, HeaderCmpResult, HEADER_MASK};
use crate::properties::FileProperties; use crate::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom}; use std::io::{Read, Seek, SeekFrom};
@ -156,7 +156,13 @@ where
let last_frame_data = reader.read_u32::<BigEndian>()?; let last_frame_data = reader.read_u32::<BigEndian>()?;
if let Some(last_frame_header) = Header::read(last_frame_data) { if let Some(last_frame_header) = Header::read(last_frame_data) {
match cmp_header(reader, last_frame_header.len, last_frame_data) { match cmp_header(
reader,
4,
last_frame_header.len,
last_frame_data,
HEADER_MASK,
) {
HeaderCmpResult::Equal | HeaderCmpResult::Undetermined => { HeaderCmpResult::Equal | HeaderCmpResult::Undetermined => {
last_frame = Some(last_frame_header); last_frame = Some(last_frame_header);
break; break;

View file

@ -10,6 +10,7 @@ use crate::id3::v2::read::parse_id3v2;
use crate::id3::v2::read_id3v2_header; use crate::id3::v2::read_id3v2_header;
use crate::id3::{find_id3v1, find_lyrics3v2, ID3FindResults}; use crate::id3::{find_id3v1, find_lyrics3v2, ID3FindResults};
use crate::macros::{decode_err, err}; use crate::macros::{decode_err, err};
use crate::mpeg::header::HEADER_MASK;
use crate::probe::ParseOptions; use crate::probe::ParseOptions;
use std::io::{Read, Seek, SeekFrom}; use std::io::{Read, Seek, SeekFrom};
@ -187,7 +188,7 @@ where
let first_header_data = reader.read_u32::<BigEndian>()?; let first_header_data = reader.read_u32::<BigEndian>()?;
if let Some(first_header) = Header::read(first_header_data) { if let Some(first_header) = Header::read(first_header_data) {
match cmp_header(reader, first_header.len, first_header_data) { match cmp_header(reader, 4, first_header.len, first_header_data, HEADER_MASK) {
HeaderCmpResult::Equal => { HeaderCmpResult::Equal => {
return Ok(Some((first_header, first_mp3_frame_start_absolute))) return Ok(Some((first_header, first_mp3_frame_start_absolute)))
}, },

View file

@ -1,3 +1,4 @@
use crate::aac::AACFile;
use crate::ape::ApeFile; use crate::ape::ApeFile;
use crate::error::Result; use crate::error::Result;
use crate::file::{AudioFile, FileType, TaggedFile}; use crate::file::{AudioFile, FileType, TaggedFile};
@ -432,7 +433,18 @@ impl<R: Read + Seek> Probe<R> {
b"fLaC" => Ok(Some(FileType::FLAC)), b"fLaC" => Ok(Some(FileType::FLAC)),
// Search for a frame sync, which may be preceded by junk // Search for a frame sync, which may be preceded by junk
_ if search_for_frame_sync(&mut self.inner)?.is_some() => { _ if search_for_frame_sync(&mut self.inner)?.is_some() => {
Ok(Some(FileType::MPEG)) // Seek back to the start of the frame sync to check if we are dealing with
// an AAC or MPEG file. See `FileType::quick_type_guess` for explanation.
self.inner.seek(SeekFrom::Current(-2))?;
let mut buf = [0; 2];
self.inner.read_exact(&mut buf)?;
if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 {
Ok(Some(FileType::AAC))
} else {
Ok(Some(FileType::MPEG))
}
}, },
_ => Ok(None), _ => Ok(None),
}; };
@ -493,6 +505,7 @@ impl<R: Read + Seek> Probe<R> {
match self.f_ty { match self.f_ty {
Some(f_type) => Ok(match f_type { Some(f_type) => Ok(match f_type {
FileType::AAC => AACFile::read_from(reader, options)?.into(),
FileType::AIFF => AiffFile::read_from(reader, options)?.into(), FileType::AIFF => AiffFile::read_from(reader, options)?.into(),
FileType::APE => ApeFile::read_from(reader, options)?.into(), FileType::APE => ApeFile::read_from(reader, options)?.into(),
FileType::FLAC => FlacFile::read_from(reader, options)?.into(), FileType::FLAC => FlacFile::read_from(reader, options)?.into(),
@ -621,6 +634,16 @@ mod tests {
assert_eq!(probe.file_type(), Some(expected_file_type_guess)); assert_eq!(probe.file_type(), Some(expected_file_type_guess));
} }
#[test]
fn probe_aac() {
test_probe("tests/files/assets/minimal/untagged.aac", FileType::AAC);
}
#[test]
fn probe_aac_with_id3v2() {
test_probe("tests/files/assets/minimal/full_test.aac", FileType::AAC);
}
#[test] #[test]
fn probe_aiff() { fn probe_aiff() {
test_probe("tests/files/assets/minimal/full_test.aiff", FileType::AIFF); test_probe("tests/files/assets/minimal/full_test.aiff", FileType::AIFF);

View file

@ -78,6 +78,7 @@ impl FileProperties {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::aac::{AACFile, AACProperties};
use crate::ape::{ApeFile, ApeProperties}; use crate::ape::{ApeFile, ApeProperties};
use crate::flac::FlacFile; use crate::flac::FlacFile;
use crate::iff::aiff::AiffFile; use crate::iff::aiff::AiffFile;
@ -98,6 +99,18 @@ mod tests {
// There is a chance they will be +/- 1, anything greater (for real world files) // There is a chance they will be +/- 1, anything greater (for real world files)
// is an issue. // is an issue.
const AAC_PROPERTIES: AACProperties = AACProperties {
version: MpegVersion::V4,
audio_object_type: AudioObjectType::AacLowComplexity,
duration: Duration::from_millis(1474), /* TODO: This is ~100ms greater than FFmpeg's report, can we do better? */
overall_bitrate: 117, // 9 less than FFmpeg reports
audio_bitrate: 117, // 9 less than FFmpeg reports
sample_rate: 48000,
channels: 2,
copyright: false,
original: false,
};
const AIFF_PROPERTIES: FileProperties = FileProperties { const AIFF_PROPERTIES: FileProperties = FileProperties {
duration: Duration::from_millis(1428), duration: Duration::from_millis(1428),
overall_bitrate: Some(1542), overall_bitrate: Some(1542),
@ -281,6 +294,14 @@ mod tests {
audio_file.properties().clone() audio_file.properties().clone()
} }
#[test]
fn aac_properties() {
assert_eq!(
get_properties::<AACFile>("tests/files/assets/minimal/full_test.aac"),
AAC_PROPERTIES
);
}
#[test] #[test]
fn aiff_properties() { fn aiff_properties() {
assert_eq!( assert_eq!(

View file

@ -2,7 +2,7 @@ use crate::error::Result;
use crate::file::FileType; use crate::file::FileType;
use crate::macros::err; use crate::macros::err;
use crate::tag::{Tag, TagType}; use crate::tag::{Tag, TagType};
use crate::{ape, flac, iff, mpeg, wavpack}; use crate::{aac, ape, flac, iff, mpeg, wavpack};
#[cfg(feature = "id3v1")] #[cfg(feature = "id3v1")]
use crate::id3::v1::tag::Id3v1TagRef; use crate::id3::v1::tag::Id3v1TagRef;
@ -25,6 +25,7 @@ use std::io::Write;
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> { pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> {
match file_type { match file_type {
FileType::AAC => aac::write::write_to(file, tag),
FileType::AIFF => iff::aiff::write::write_to(file, tag), FileType::AIFF => iff::aiff::write::write_to(file, tag),
FileType::APE => ape::write::write_to(file, tag), FileType::APE => ape::write::write_to(file, tag),
FileType::FLAC => flac::write::write_to(file, tag), FileType::FLAC => flac::write::write_to(file, tag),

95
tests/files/aac.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::{set_artist, temp_file, verify_artist};
use lofty::{
Accessor, FileType, ItemKey, ItemValue, ParseOptions, Probe, TagExt, TagItem, TagType,
};
use std::io::{Seek, Write};
#[test]
fn read() {
// Here we have an AAC file with an ID3v2, and an ID3v1 tag
let file = Probe::open("tests/files/assets/minimal/full_test.aac")
.unwrap()
.options(ParseOptions::new().read_properties(false))
.read()
.unwrap();
assert_eq!(file.file_type(), FileType::AAC);
// Verify the ID3v2 tag first
crate::verify_artist!(file, primary_tag, "Foo artist", 1);
// Now verify ID3v1
crate::verify_artist!(file, tag, TagType::ID3v1, "Bar artist", 1);
}
#[test]
fn read_with_junk_bytes_between_frames() {
// Read a file that includes an ID3v2.3 data block followed by four bytes of junk data (0x20)
// This is the same test as MP3, but it uses the same byte skipping logic, so it should be tested
// here too :).
let file = Probe::open("tests/files/assets/junk_between_id3_and_adts.aac")
.unwrap()
.read()
.unwrap();
// note that the file contains ID3v2 and ID3v1 data
assert_eq!(file.file_type(), FileType::AAC);
let id3v2_tag = &file.tags()[0];
assert_eq!(id3v2_tag.artist(), Some("artist test"));
assert_eq!(id3v2_tag.album(), Some("album test"));
assert_eq!(id3v2_tag.title(), Some("title test"));
assert_eq!(
id3v2_tag.get_string(&ItemKey::EncoderSettings),
Some("Lavf58.62.100")
);
let id3v1_tag = &file.tags()[1];
assert_eq!(id3v1_tag.artist(), Some("artist test"));
assert_eq!(id3v1_tag.album(), Some("album test"));
assert_eq!(id3v1_tag.title(), Some("title test"));
}
#[test]
fn write() {
let mut file = temp_file!("tests/files/assets/minimal/full_test.aac");
let mut tagged_file = Probe::new(&mut file)
.options(ParseOptions::new().read_properties(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
assert_eq!(tagged_file.file_type(), FileType::AAC);
// ID3v2
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
// ID3v1
crate::set_artist!(tagged_file, tag_mut, TagType::ID3v1, "Bar artist", 1 => file, "Baz artist");
// Now reread the file
file.rewind().unwrap();
let mut tagged_file = Probe::new(&mut file)
.options(ParseOptions::new().read_properties(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::ID3v1, "Baz artist", 1 => file, "Bar artist");
}
#[test]
fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/minimal/full_test.aac", TagType::ID3v2);
}
#[test]
fn remove_id3v1() {
crate::remove_tag!("tests/files/assets/minimal/full_test.aac", TagType::ID3v1);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,3 +1,4 @@
mod aac;
mod aiff; mod aiff;
mod ape; mod ape;
mod mp4; mod mp4;