Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions SUPPORTED_FORMATS.md
Original file line number Diff line number Diff line change
@@ -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`\* |
Expand Down
1 change: 1 addition & 0 deletions benches/read_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
6 changes: 5 additions & 1 deletion fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -84,4 +88,4 @@ path = "fuzz_targets/picture_from_flac_bytes.rs"

[[bin]]
name = "picture_from_ape_bytes"
path = "fuzz_targets/picture_from_ape_bytes.rs"
path = "fuzz_targets/picture_from_ape_bytes.rs"
13 changes: 13 additions & 0 deletions fuzz/fuzz_targets/aacfile_read_from.rs
Original file line number Diff line number Diff line change
@@ -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),
);
});
4 changes: 2 additions & 2 deletions lofty_attr/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
123 changes: 123 additions & 0 deletions src/aac/header.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions src/aac/mod.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 108 additions & 0 deletions src/aac/properties.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading