Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID3v2: Support audio-text accessibility (ATXT) frame #188

Merged
merged 1 commit into from
Apr 16, 2023
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
4 changes: 2 additions & 2 deletions src/id3/v2/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ pub enum FrameValue {
///
/// NOTES:
///
/// * This is used for "GEOB" and "SYLT" frames, see
/// [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse) and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively
/// * This is used for rare frames, such as GEOB, SYLT, and ATXT to skip additional unnecessary work.
/// See [`GeneralEncapsulatedObject::parse`](crate::id3::v2::GeneralEncapsulatedObject::parse), [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse), and [`AudioTextFrame::parse`](crate::id3::v2::AudioTextFrame::parse) respectively
/// * This is used for **all** frames with an ID of [`FrameID::Outdated`]
/// * This is used for unknown frames
Binary(Vec<u8>),
Expand Down
213 changes: 213 additions & 0 deletions src/id3/v2/items/audio_text_frame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use crate::error::{ErrorKind, ID3v2Error, ID3v2ErrorKind, LoftyError, Result};
use crate::util::text::{decode_text, encode_text, TextEncoding};

use byteorder::ReadBytesExt as _;

/// Flags for an ID3v2 audio-text flag
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct AudioTextFrameFlags {
/// This flag shall be set if the scrambling method defined in [Section 5] has been applied
/// to the audio data, or not set if no scrambling has been applied.
///
/// [Section 5]: https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2-accessibility-1.0.html#scrambling-scheme-for-non-mpeg-audio-formats
pub scrambling: bool,
}

impl AudioTextFrameFlags {
/// Get ID3v2 ATXT frame flags from a byte
///
/// The flag byte layout is defined here: <https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2-accessibility-1.0.html#proposed-audio-text-frame>
pub fn from_u8(byte: u8) -> Self {
Self {
scrambling: byte & 0x01 > 0,
}
}

/// Convert an [`AudioTextFrameFlags`] to an ATXT frame flag byte
///
/// The flag byte layout is defined here: <https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2-accessibility-1.0.html#proposed-audio-text-frame>
pub fn as_u8(&self) -> u8 {
let mut byte = 0_u8;

if self.scrambling {
byte |= 0x01
}

byte
}
}

/// An `ID3v2` audio-text frame
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AudioTextFrame {
/// The encoding of the description
pub encoding: TextEncoding,
/// The MIME type of the audio data
pub mime_type: String,
/// Flags for the
pub flags: AudioTextFrameFlags,
/// The equivalent text for the audio clip
///
/// This text must be semantically equivalent to the spoken narrative in the audio clip and
/// should match the text and encoding used by another ID3v2 frame in the tag.
pub equivalent_text: String,
/// The audio clip
///
/// The Audio data carries an audio clip which provides the audio description. The encoding
/// of the audio data shall match the MIME type field and the data shall be scrambled if
/// the scrambling flag is set.
///
/// To unscramble the data, see [`scramble()`].
///
/// NOTE: Do not replace this field with the unscrambled data unless the [`AudioTextFrameFlags::scrambling`] flag
/// has been unset. Otherwise, this frame will no longer be readable.
pub audio_data: Vec<u8>,
}

impl AudioTextFrame {
/// Get an [`AudioTextFrame`] from ID3v2 ATXT bytes:
///
/// NOTE: This expects *only* the frame content
///
/// # Errors
///
/// * Not enough data
/// * Improperly encoded text
pub fn parse(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 4 {
return Err(ID3v2Error::new(ID3v2ErrorKind::BadFrameLength).into());
}

let content = &mut &bytes[..];
let encoding = TextEncoding::from_u8(content.read_u8()?)
.ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?;

let mime_type = decode_text(content, TextEncoding::Latin1, true)?.unwrap_or_default();

let flags = AudioTextFrameFlags::from_u8(content.read_u8()?);

let equivalent_text = decode_text(content, encoding, true)?.unwrap_or_default();

Ok(Self {
encoding,
mime_type,
flags,
equivalent_text,
audio_data: content.to_vec(),
})
}

/// Convert an [`AudioTextFrame`] to a ID3v2 A/PIC byte Vec
///
/// NOTE: This does not include the frame header
pub fn as_bytes(&self) -> Vec<u8> {
let mut content = vec![self.encoding as u8];

content.extend(encode_text(
self.mime_type.as_str(),
TextEncoding::Latin1,
true,
));
content.push(self.flags.as_u8());
content.extend(encode_text(&self.equivalent_text, self.encoding, true));
content.extend(&self.audio_data);
content
}
}

const SCRAMBLING_TABLE: [u8; 127] = {
let mut scrambling_table = [0_u8; 127];
scrambling_table[0] = 0xFE;

let mut i = 0;
loop {
let byte = scrambling_table[i];

let bit7 = (byte >> 7) & 0x01;
let bit6 = (byte >> 6) & 0x01;
let bit5 = (byte >> 5) & 0x01;
let bit4 = (byte >> 4) & 0x01;
let bit3 = (byte >> 3) & 0x01;
let bit2 = (byte >> 2) & 0x01;
let bit1 = (byte >> 1) & 0x01;
let bit0 = byte & 0x01;

let new_byte = ((bit6 ^ bit5) << 7)
+ ((bit5 ^ bit4) << 6)
+ ((bit4 ^ bit3) << 5)
+ ((bit3 ^ bit2) << 4)
+ ((bit2 ^ bit1) << 3)
+ ((bit1 ^ bit0) << 2)
+ ((bit7 ^ bit5) << 1)
+ (bit6 ^ bit4);

if new_byte == 0xFE {
break;
}

i += 1;
scrambling_table[i] = new_byte;
}

scrambling_table
};

/// Scramble/Unscramble the audio clip from an ATXT frame in place
///
/// The scrambling scheme is defined here: <https://mutagen-specs.readthedocs.io/en/latest/id3/id3v2-accessibility-1.0.html#scrambling-scheme-for-non-mpeg-audio-formats>
pub fn scramble(audio_data: &mut [u8]) {
let mut idx = 0;
for b in audio_data.iter_mut() {
*b ^= SCRAMBLING_TABLE[idx];
if idx == 126 {
idx = 0;
} else {
idx += 1;
}
}
}

#[cfg(test)]
mod tests {
use crate::id3::v2::{AudioTextFrame, AudioTextFrameFlags};
use crate::TextEncoding;

#[test]
fn atxt_decode() {
let expected = AudioTextFrame {
encoding: TextEncoding::Latin1,
mime_type: String::from("audio/mpeg"),
flags: AudioTextFrameFlags { scrambling: false },
equivalent_text: String::from("foo bar baz"),
audio_data: crate::tag::utils::test_utils::read_path(
"tests/files/assets/minimal/full_test.mp3",
),
};

let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.atxt");

let parsed_atxt = AudioTextFrame::parse(&cont).unwrap();

assert_eq!(parsed_atxt, expected);
}

#[test]
fn atxt_encode() {
let to_encode = AudioTextFrame {
encoding: TextEncoding::Latin1,
mime_type: String::from("audio/mpeg"),
flags: AudioTextFrameFlags { scrambling: false },
equivalent_text: String::from("foo bar baz"),
audio_data: crate::tag::utils::test_utils::read_path(
"tests/files/assets/minimal/full_test.mp3",
),
};

let encoded = to_encode.as_bytes();

let expected_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.atxt");

assert_eq!(encoded, expected_bytes);
}
}
2 changes: 2 additions & 0 deletions src/id3/v2/items/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod attached_picture_frame;
mod audio_text_frame;
mod encapsulated_object;
mod extended_text_frame;
mod extended_url_frame;
Expand All @@ -10,6 +11,7 @@ mod text_information_frame;
mod url_link_frame;

pub use attached_picture_frame::AttachedPictureFrame;
pub use audio_text_frame::{scramble, AudioTextFrame, AudioTextFrameFlags};
pub use encapsulated_object::{GEOBInformation, GeneralEncapsulatedObject};
pub use extended_text_frame::ExtendedTextFrame;
pub use extended_url_frame::ExtendedUrlFrame;
Expand Down
Binary file added tests/tags/assets/id3v2/test.atxt
Binary file not shown.