mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-12 05:32:38 +00:00
Add support for AAC (#71)
This commit is contained in:
parent
78723a7dd0
commit
02f1314005
28 changed files with 686 additions and 32 deletions
|
@ -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
|
||||
`read_properties`, specified with a `bool` in `read_from{_path}`. This will now default to `true`,
|
||||
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`
|
||||
- Debug logging via the [log](https://crates.io/crates/log) crate for exposing recoverable errors.
|
||||
- **Error**: `ErrorKind::SizeMismatch`
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
| File Format | Metadata Format(s) |
|
||||
|-------------|--------------------------------------|
|
||||
| AAC (ADTS) | `ID3v2`, `ID3v1` |
|
||||
| Ape | `APEv2`, `APEv1`, `ID3v2`\*, `ID3v1` |
|
||||
| AIFF | `ID3v2`, `Text Chunks` |
|
||||
| FLAC | `Vorbis Comments`, `ID3v2`\* |
|
||||
|
|
|
@ -30,6 +30,7 @@ fn content_infer_read(c: &mut Criterion) {
|
|||
test_read_file!(
|
||||
c,
|
||||
[
|
||||
(AAC, "../tests/files/assets/minimal/full_test.aac"),
|
||||
(AIFF, "../tests/files/assets/minimal/full_test.aiff"),
|
||||
(APE, "../tests/files/assets/minimal/full_test.ape"),
|
||||
(FLAC, "../tests/files/assets/minimal/full_test.flac"),
|
||||
|
|
|
@ -26,6 +26,10 @@ path = "fuzz_targets/filetype_from_buffer.rs"
|
|||
name = "mp3file_read_from"
|
||||
path = "fuzz_targets/mpegfile_read_from.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "aacfile_read_from"
|
||||
path = "fuzz_targets/aacfile_read_from.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "aifffile_read_from"
|
||||
path = "fuzz_targets/aifffile_read_from.rs"
|
||||
|
|
13
fuzz/fuzz_targets/aacfile_read_from.rs
Normal file
13
fuzz/fuzz_targets/aacfile_read_from.rs
Normal 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),
|
||||
);
|
||||
});
|
|
@ -9,8 +9,8 @@ use quote::quote;
|
|||
pub(crate) fn opt_internal_file_type(
|
||||
struct_name: String,
|
||||
) -> Option<(proc_macro2::TokenStream, bool)> {
|
||||
const LOFTY_FILE_TYPES: [&str; 10] = [
|
||||
"AIFF", "APE", "FLAC", "MPEG", "MP4", "Opus", "Vorbis", "Speex", "WAV", "WavPack",
|
||||
const LOFTY_FILE_TYPES: [&str; 11] = [
|
||||
"AAC", "AIFF", "APE", "FLAC", "MPEG", "MP4", "Opus", "Vorbis", "Speex", "WAV", "WavPack",
|
||||
];
|
||||
|
||||
const ID3V2_STRIPPABLE: [&str; 1] = ["APE"];
|
||||
|
|
123
src/aac/header.rs
Normal file
123
src/aac/header.rs
Normal 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
30
src/aac/mod.rs
Normal 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
108
src/aac/properties.rs
Normal 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
169
src/aac/read.rs
Normal 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)
|
||||
}
|
49
src/file.rs
49
src/file.rs
|
@ -482,6 +482,7 @@ impl AudioFile for TaggedFile {
|
|||
#[non_exhaustive]
|
||||
/// The type of file read
|
||||
pub enum FileType {
|
||||
AAC,
|
||||
AIFF,
|
||||
APE,
|
||||
FLAC,
|
||||
|
@ -500,7 +501,7 @@ impl FileType {
|
|||
///
|
||||
/// | [`FileType`] | [`TagType`] |
|
||||
/// |-----------------------------------|------------------|
|
||||
/// | `AIFF`, `MP3`, `WAV` | `ID3v2` |
|
||||
/// | `AAC`, `AIFF`, `MP3`, `WAV` | `ID3v2` |
|
||||
/// | `APE` , `WavPack` | `APE` |
|
||||
/// | `FLAC`, `Opus`, `Vorbis`, `Speex` | `VorbisComments` |
|
||||
/// | `MP4` | `MP4ilst` |
|
||||
|
@ -519,7 +520,7 @@ impl FileType {
|
|||
/// ```
|
||||
pub fn primary_tag_type(&self) -> TagType {
|
||||
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::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => {
|
||||
TagType::VorbisComments
|
||||
|
@ -553,7 +554,7 @@ impl FileType {
|
|||
pub fn supports_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match self {
|
||||
#[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 =>
|
||||
{
|
||||
true
|
||||
|
@ -561,7 +562,11 @@ impl FileType {
|
|||
#[cfg(feature = "aiff_text_chunks")]
|
||||
FileType::AIFF if tag_type == TagType::AIFFText => true,
|
||||
#[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")]
|
||||
FileType::APE | FileType::MPEG | FileType::WavPack if tag_type == TagType::APE => true,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
|
@ -597,6 +602,7 @@ impl FileType {
|
|||
let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
|
||||
|
||||
match ext.as_str() {
|
||||
"aac" => Some(Self::AAC),
|
||||
"ape" => Some(Self::APE),
|
||||
"aiff" | "aif" | "afc" | "aifc" => Some(Self::AIFF),
|
||||
"mp3" | "mp2" | "mp1" => Some(Self::MPEG),
|
||||
|
@ -716,7 +722,40 @@ impl FileType {
|
|||
// Safe to index, since we return early on an empty buffer
|
||||
match buf[0] {
|
||||
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" => {
|
||||
let id = &buf[8..12];
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ macro_rules! impl_accessor {
|
|||
/// * [`GENRES`] contains the string
|
||||
/// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8`
|
||||
#[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 {
|
||||
/// Track title, 30 bytes max
|
||||
pub title: Option<String>,
|
||||
|
|
|
@ -92,7 +92,7 @@ macro_rules! impl_accessor {
|
|||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
#[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 {
|
||||
flags: ID3v2TagFlags,
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::id3::find_id3v2;
|
|||
use crate::id3::v2::frame::FrameRef;
|
||||
use crate::id3::v2::tag::Id3v2TagRef;
|
||||
use crate::id3::v2::util::synch_u32;
|
||||
use crate::id3::v2::ID3v2Tag;
|
||||
use crate::macros::err;
|
||||
use crate::probe::Probe;
|
||||
|
||||
|
@ -15,7 +16,6 @@ use std::fs::File;
|
|||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::ops::Not;
|
||||
|
||||
use crate::id3::v2::ID3v2Tag;
|
||||
use byteorder::{BigEndian, LittleEndian, WriteBytesExt};
|
||||
|
||||
// In the very rare chance someone wants to write a CRC in their extended header
|
||||
|
|
|
@ -166,6 +166,7 @@
|
|||
extern crate self as lofty;
|
||||
pub(crate) mod _this_is_internal {}
|
||||
|
||||
pub mod aac;
|
||||
pub mod ape;
|
||||
pub mod error;
|
||||
pub(crate) mod file;
|
||||
|
|
|
@ -31,6 +31,8 @@ cfg_if::cfg_if! {
|
|||
|
||||
pub use crate::mp4::properties::{AudioObjectType, Mp4Codec, Mp4Properties};
|
||||
|
||||
pub(crate) use properties::SAMPLE_RATES;
|
||||
|
||||
/// An MP4 file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
|
|
|
@ -360,6 +360,11 @@ where
|
|||
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<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
|
@ -368,12 +373,6 @@ where
|
|||
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,
|
||||
];
|
||||
|
||||
// Set the codec to AAC, which is a good guess if we fail before reaching the `esds`
|
||||
properties.codec = Mp4Codec::AAC;
|
||||
|
||||
|
|
|
@ -70,39 +70,49 @@ where
|
|||
ret
|
||||
}
|
||||
|
||||
pub(super) enum HeaderCmpResult {
|
||||
pub(crate) enum HeaderCmpResult {
|
||||
Equal,
|
||||
Undetermined,
|
||||
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,
|
||||
header_size: u32,
|
||||
first_header_len: u32,
|
||||
first_header_bytes: u32,
|
||||
header_mask: u32,
|
||||
) -> HeaderCmpResult
|
||||
where
|
||||
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
|
||||
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() {
|
||||
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)
|
||||
if first_header_bytes & HEADER_MASK == second_header_data & HEADER_MASK =>
|
||||
if first_header_bytes & header_mask == second_header_data & header_mask =>
|
||||
{
|
||||
HeaderCmpResult::Equal
|
||||
},
|
||||
Err(_) => HeaderCmpResult::Undetermined,
|
||||
_ => HeaderCmpResult::NotEqual,
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +124,8 @@ pub enum MpegVersion {
|
|||
V1,
|
||||
V2,
|
||||
V2_5,
|
||||
/// Exclusive to AAC
|
||||
V4,
|
||||
}
|
||||
|
||||
impl Default for MpegVersion {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, XingHeader};
|
||||
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 std::io::{Read, Seek, SeekFrom};
|
||||
|
@ -156,7 +156,13 @@ where
|
|||
let last_frame_data = reader.read_u32::<BigEndian>()?;
|
||||
|
||||
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 => {
|
||||
last_frame = Some(last_frame_header);
|
||||
break;
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::id3::v2::read::parse_id3v2;
|
|||
use crate::id3::v2::read_id3v2_header;
|
||||
use crate::id3::{find_id3v1, find_lyrics3v2, ID3FindResults};
|
||||
use crate::macros::{decode_err, err};
|
||||
use crate::mpeg::header::HEADER_MASK;
|
||||
use crate::probe::ParseOptions;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
@ -187,7 +188,7 @@ where
|
|||
let first_header_data = reader.read_u32::<BigEndian>()?;
|
||||
|
||||
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 => {
|
||||
return Ok(Some((first_header, first_mp3_frame_start_absolute)))
|
||||
},
|
||||
|
|
25
src/probe.rs
25
src/probe.rs
|
@ -1,3 +1,4 @@
|
|||
use crate::aac::AACFile;
|
||||
use crate::ape::ApeFile;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
|
@ -432,7 +433,18 @@ impl<R: Read + Seek> Probe<R> {
|
|||
b"fLaC" => Ok(Some(FileType::FLAC)),
|
||||
// Search for a frame sync, which may be preceded by junk
|
||||
_ 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),
|
||||
};
|
||||
|
@ -493,6 +505,7 @@ impl<R: Read + Seek> Probe<R> {
|
|||
|
||||
match self.f_ty {
|
||||
Some(f_type) => Ok(match f_type {
|
||||
FileType::AAC => AACFile::read_from(reader, options)?.into(),
|
||||
FileType::AIFF => AiffFile::read_from(reader, options)?.into(),
|
||||
FileType::APE => ApeFile::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));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn probe_aiff() {
|
||||
test_probe("tests/files/assets/minimal/full_test.aiff", FileType::AIFF);
|
||||
|
|
|
@ -78,6 +78,7 @@ impl FileProperties {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::aac::{AACFile, AACProperties};
|
||||
use crate::ape::{ApeFile, ApeProperties};
|
||||
use crate::flac::FlacFile;
|
||||
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)
|
||||
// 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 {
|
||||
duration: Duration::from_millis(1428),
|
||||
overall_bitrate: Some(1542),
|
||||
|
@ -281,6 +294,14 @@ mod tests {
|
|||
audio_file.properties().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aac_properties() {
|
||||
assert_eq!(
|
||||
get_properties::<AACFile>("tests/files/assets/minimal/full_test.aac"),
|
||||
AAC_PROPERTIES
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aiff_properties() {
|
||||
assert_eq!(
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::error::Result;
|
|||
use crate::file::FileType;
|
||||
use crate::macros::err;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::{ape, flac, iff, mpeg, wavpack};
|
||||
use crate::{aac, ape, flac, iff, mpeg, wavpack};
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
use crate::id3::v1::tag::Id3v1TagRef;
|
||||
|
@ -25,6 +25,7 @@ use std::io::Write;
|
|||
#[allow(unreachable_patterns)]
|
||||
pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Result<()> {
|
||||
match file_type {
|
||||
FileType::AAC => aac::write::write_to(file, tag),
|
||||
FileType::AIFF => iff::aiff::write::write_to(file, tag),
|
||||
FileType::APE => ape::write::write_to(file, tag),
|
||||
FileType::FLAC => flac::write::write_to(file, tag),
|
||||
|
|
95
tests/files/aac.rs
Normal file
95
tests/files/aac.rs
Normal 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);
|
||||
}
|
BIN
tests/files/assets/junk_between_id3_and_adts.aac
Normal file
BIN
tests/files/assets/junk_between_id3_and_adts.aac
Normal file
Binary file not shown.
BIN
tests/files/assets/minimal/full_test.aac
Normal file
BIN
tests/files/assets/minimal/full_test.aac
Normal file
Binary file not shown.
BIN
tests/files/assets/minimal/untagged.aac
Normal file
BIN
tests/files/assets/minimal/untagged.aac
Normal file
Binary file not shown.
|
@ -1,3 +1,4 @@
|
|||
mod aac;
|
||||
mod aiff;
|
||||
mod ape;
|
||||
mod mp4;
|
||||
|
|
Loading…
Reference in a new issue