Skip to content

Commit

Permalink
AIFF: Expose the compression type for AIFC
Browse files Browse the repository at this point in the history
  • Loading branch information
Serial-ATA committed Jul 10, 2023
1 parent 36a3561 commit c3c0178
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 28 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ParseOptions**: `ParseOptions::max_junk_bytes`, allowing the parser to sift through junk bytes to find required information, rather than
immediately declare a file invalid. ([discussion](https://github.com/Serial-ATA/lofty-rs/discussions/219)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/227))
- **WavPack**: `WavPackProperties` now contains the channel mask, accessible through `WavPackProperties::channel_mask()` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/230))
- **AIFF**:
- `AiffProperties` to hold additional AIFF-specific information
- AIFC compression types are now exposed through `AiffCompressionType`

## Changed
- **ID3v2**:
Expand All @@ -21,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **APE**: The default track/disk number is now `0` to line up with ID3v2.
This is only used when `set_{track, disk}_total` is used without a corresponding `set_{track, disk}`.
- **VorbisComments**: When writing, items larger than `u32::MAX` will throw `ErrorKind::TooMuchData`, rather than be silently discarded.
- **AIFF**: `AiffFile` will no longer use `FileProperties`. It now uses `AiffProperties`.

## Fixed
- **APE**: Track/Disk number pairs are properly converted when writing ([issue](https://github.com/Serial-ATA/lofty-rs/issues/159)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/216))
Expand Down
4 changes: 2 additions & 2 deletions src/iff/aiff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ mod read;
pub(crate) mod tag;

use crate::id3::v2::tag::Id3v2Tag;
use crate::properties::FileProperties;

use lofty_attr::LoftyFile;

// Exports

pub use properties::{AiffCompressionType, AiffProperties};
pub use tag::{AIFFTextChunks, Comment};

/// An AIFF file
Expand All @@ -25,5 +25,5 @@ pub struct AiffFile {
#[lofty(tag_type = "Id3v2")]
pub(crate) id3v2_tag: Option<Id3v2Tag>,
/// The file's audio properties
pub(crate) properties: FileProperties,
pub(crate) properties: AiffProperties,
}
205 changes: 194 additions & 11 deletions src/iff/aiff/properties.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,162 @@
use super::read::CompressionPresent;
use crate::error::Result;
use crate::macros::decode_err;
use crate::macros::{decode_err, try_vec};
use crate::properties::FileProperties;

use std::borrow::Cow;
use std::io::Read;
use std::time::Duration;

use byteorder::{BigEndian, ReadBytesExt};

/// The AIFC compression type
///
/// This contains a non-exhaustive list of compression types
#[allow(non_camel_case_types)]
#[derive(Clone, Eq, PartialEq, Default, Debug)]
pub enum AiffCompressionType {
#[default]
/// PCM
None,
/// 2-to-1 IIGS ACE (Audio Compression / Expansion)
ACE2,
/// 8-to-3 IIGS ACE (Audio Compression / Expansion)
ACE8,
/// 3-to-1 Macintosh Audio Compression / Expansion
MAC3,
/// 6-to-1 Macintosh Audio Compression / Expansion
MAC6,
/// PCM (byte swapped)
sowt,
/// IEEE 32-bit float
fl32,
/// IEEE 64-bit float
fl64,
/// 8-bit ITU-T G.711 A-law
alaw,
/// 8-bit ITU-T G.711 µ-law
ulaw,
/// 8-bit ITU-T G.711 µ-law (64 kb/s)
ULAW,
/// 8-bit ITU-T G.711 A-law (64 kb/s)
ALAW,
/// IEEE 32-bit float (From SoundHack & Csound)
FL32,
/// Catch-all for unknown compression algorithms
Other {
/// Identifier from the compression algorithm
compression_type: [u8; 4],
/// Human-readable description of the compression algorithm
compression_name: String,
},
}

impl AiffCompressionType {
/// Get the compression name for a compression type
///
/// For variants other than [`AiffCompressionType::Other`], this will use statically known names.
///
/// # Examples
///
/// ```rust
/// use lofty::iff::aiff::AiffCompressionType;
///
/// let compression_type = AiffCompressionType::alaw;
/// assert_eq!(compression_type.compression_name(), "ALaw 2:1");
/// ```
pub fn compression_name(&self) -> Cow<'_, str> {
match self {
AiffCompressionType::None => Cow::Borrowed("not compressed"),
AiffCompressionType::ACE2 => Cow::Borrowed("ACE 2-to-1"),
AiffCompressionType::ACE8 => Cow::Borrowed("ACE 8-to-3"),
AiffCompressionType::MAC3 => Cow::Borrowed("MACE 3-to-1"),
AiffCompressionType::MAC6 => Cow::Borrowed("MACE 6-to-1"),
AiffCompressionType::sowt => Cow::Borrowed(""), // Has no compression name
AiffCompressionType::fl32 => Cow::Borrowed("32-bit floating point"),
AiffCompressionType::fl64 => Cow::Borrowed("64-bit floating point"),
AiffCompressionType::alaw => Cow::Borrowed("ALaw 2:1"),
AiffCompressionType::ulaw => Cow::Borrowed("µLaw 2:1"),
AiffCompressionType::ULAW => Cow::Borrowed("CCITT G.711 u-law"),
AiffCompressionType::ALAW => Cow::Borrowed("CCITT G.711 A-law"),
AiffCompressionType::FL32 => Cow::Borrowed("Float 32"),
AiffCompressionType::Other {
compression_name, ..
} => Cow::from(compression_name),
}
}
}

/// A AIFF file's audio properties
#[derive(Debug, PartialEq, Eq, Clone, Default)]
#[non_exhaustive]
pub struct AiffProperties {
pub(crate) duration: Duration,
pub(crate) overall_bitrate: u32,
pub(crate) audio_bitrate: u32,
pub(crate) sample_rate: u32,
pub(crate) sample_size: u16,
pub(crate) channels: u16,
pub(crate) compression_type: Option<AiffCompressionType>,
}

impl From<AiffProperties> for FileProperties {
fn from(value: AiffProperties) -> Self {
Self {
duration: value.duration,
overall_bitrate: Some(value.overall_bitrate),
audio_bitrate: Some(value.audio_bitrate),
sample_rate: Some(value.sample_rate),
bit_depth: Some(value.sample_size as u8),
channels: Some(value.channels as u8),
channel_mask: None,
}
}
}

impl AiffProperties {
/// Duration of the audio
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
}

/// Bits per sample
pub fn sample_size(&self) -> u16 {
self.sample_size
}

/// Channel count
pub fn channels(&self) -> u16 {
self.channels
}

/// AIFC compression type, if an AIFC file was read
pub fn compression_type(&self) -> Option<&AiffCompressionType> {
self.compression_type.as_ref()
}
}

pub(super) fn read_properties(
comm: &mut &[u8],
compression_present: CompressionPresent,
stream_len: u32,
file_length: u64,
) -> Result<FileProperties> {
let channels = comm.read_u16::<BigEndian>()? as u8;
) -> Result<AiffProperties> {
let channels = comm.read_u16::<BigEndian>()?;

if channels == 0 {
decode_err!(@BAIL Aiff, "File contains 0 channels");
Expand Down Expand Up @@ -55,20 +199,59 @@ pub(super) fn read_properties(

(
Duration::from_millis(length as u64),
Some(((file_length as f64) * 8.0 / length + 0.5) as u32),
Some((f64::from(stream_len) * 8.0 / length + 0.5) as u32),
((file_length as f64) * 8.0 / length + 0.5) as u32,
(f64::from(stream_len) * 8.0 / length + 0.5) as u32,
)
} else {
(Duration::ZERO, None, None)
(Duration::ZERO, 0, 0)
};

Ok(FileProperties {
let mut compression = None;
if comm.len() >= 5 && compression_present == CompressionPresent::Yes {
let mut compression_type = [0u8; 4];
comm.read_exact(&mut compression_type)?;

compression = Some(match &compression_type {
b"NONE" => AiffCompressionType::None,
b"ACE2" => AiffCompressionType::ACE2,
b"ACE8" => AiffCompressionType::ACE8,
b"MAC3" => AiffCompressionType::MAC3,
b"MAC6" => AiffCompressionType::MAC6,
b"sowt" => AiffCompressionType::sowt,
b"fl32" => AiffCompressionType::fl32,
b"fl64" => AiffCompressionType::fl64,
b"alaw" => AiffCompressionType::alaw,
b"ulaw" => AiffCompressionType::ulaw,
b"ULAW" => AiffCompressionType::ULAW,
b"ALAW" => AiffCompressionType::ALAW,
b"FL32" => AiffCompressionType::FL32,
_ => {
// We have to read the compression name string
let mut compression_name = String::new();

let compression_name_size = comm.read_u8()?;
if compression_name_size > 0 {
let mut compression_name_bytes = try_vec![0u8; compression_name_size as usize];
comm.read_exact(&mut compression_name_bytes)?;

compression_name = String::from_utf8(compression_name_bytes)?;
}

AiffCompressionType::Other {
compression_type,
compression_name,
}
},
});
}

Ok(AiffProperties {
duration,
overall_bitrate,
audio_bitrate,
sample_rate: Some(sample_rate),
bit_depth: Some(sample_size as u8),
channels: Some(channels),
channel_mask: None,
sample_rate,
sample_size,
channels,
compression_type: compression,
})
}
24 changes: 18 additions & 6 deletions src/iff/aiff/read.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
use super::properties::AiffProperties;
use super::tag::{AIFFTextChunks, Comment};
use super::AiffFile;
use crate::error::Result;
use crate::id3::v2::tag::Id3v2Tag;
use crate::iff::chunk::Chunks;
use crate::macros::{decode_err, err};
use crate::probe::ParseOptions;
use crate::properties::FileProperties;

use std::io::{Read, Seek, SeekFrom};

use byteorder::{BigEndian, ReadBytesExt};

pub(in crate::iff) fn verify_aiff<R>(data: &mut R) -> Result<()>
/// Whether we are dealing with an AIFC file
#[derive(Eq, PartialEq)]
pub(in crate::iff) enum CompressionPresent {
Yes,
No,
}

pub(in crate::iff) fn verify_aiff<R>(data: &mut R) -> Result<CompressionPresent>
where
R: Read + Seek,
{
let mut id = [0; 12];
data.read_exact(&mut id)?;

if !(&id[..4] == b"FORM" && (&id[8..] == b"AIFF" || &id[8..] == b"AIFC")) {
if !(&id[..4] == b"FORM") {
err!(UnknownFormat);
}

Ok(())
match &id[8..] {
b"AIFF" => Ok(CompressionPresent::No),
b"AIFC" => Ok(CompressionPresent::Yes),
_ => err!(UnknownFormat),
}
}

pub(crate) fn read_from<R>(data: &mut R, parse_options: ParseOptions) -> Result<AiffFile>
Expand All @@ -31,7 +42,7 @@ where
{
// TODO: Maybe one day the `Seek` bound can be removed?
// let file_size = verify_aiff(data)?;
verify_aiff(data)?;
let compression_present = verify_aiff(data)?;

let current_pos = data.stream_position()?;
let file_len = data.seek(SeekFrom::End(0))?;
Expand Down Expand Up @@ -134,14 +145,15 @@ where

properties = super::properties::read_properties(
&mut &*comm,
compression_present,
stream_len,
data.stream_position()?,
)?;
},
None => decode_err!(@BAIL Aiff, "File does not contain a \"COMM\" chunk"),
}
} else {
properties = FileProperties::default();
properties = AiffProperties::default();
};

Ok(AiffFile {
Expand Down
18 changes: 9 additions & 9 deletions src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ mod tests {
use crate::aac::{AACProperties, AacFile};
use crate::ape::{ApeFile, ApeProperties};
use crate::flac::{FlacFile, FlacProperties};
use crate::iff::aiff::AiffFile;
use crate::iff::aiff::{AiffFile, AiffProperties};
use crate::iff::wav::{WavFile, WavFormat, WavProperties};
use crate::mp4::{AudioObjectType, Mp4Codec, Mp4File, Mp4Properties};
use crate::mpeg::{ChannelMode, Emphasis, Layer, MpegFile, MpegProperties, MpegVersion};
Expand All @@ -136,7 +136,7 @@ mod tests {
};
use crate::probe::ParseOptions;
use crate::wavpack::{WavPackFile, WavPackProperties};
use crate::{AudioFile, ChannelMask, FileProperties};
use crate::{AudioFile, ChannelMask};

use std::fs::File;
use std::time::Duration;
Expand All @@ -157,14 +157,14 @@ mod tests {
original: false,
};

const AIFF_PROPERTIES: FileProperties = FileProperties {
const AIFF_PROPERTIES: AiffProperties = AiffProperties {
duration: Duration::from_millis(1428),
overall_bitrate: Some(1542),
audio_bitrate: Some(1536),
sample_rate: Some(48000),
bit_depth: Some(16),
channels: Some(2),
channel_mask: None,
overall_bitrate: 1542,
audio_bitrate: 1536,
sample_rate: 48000,
sample_size: 16,
channels: 2,
compression_type: None,
};

const APE_PROPERTIES: ApeProperties = ApeProperties {
Expand Down

0 comments on commit c3c0178

Please sign in to comment.