From abb497b19c1e9ce41af3c129d8f0a9d93a126c73 Mon Sep 17 00:00:00 2001 From: Ian Hobson Date: Tue, 16 Jan 2024 14:43:51 +0100 Subject: [PATCH] caf: Add initial support for Core Audio Format / CAF files (#232) --- CONTRIBUTORS | 1 + README.md | 2 + symphonia-core/src/sample.rs | 2 +- symphonia-format-caf/Cargo.toml | 18 + symphonia-format-caf/README.md | 15 + symphonia-format-caf/src/chunks.rs | 589 ++++++++++++++++++++++++++++ symphonia-format-caf/src/demuxer.rs | 373 ++++++++++++++++++ symphonia-format-caf/src/lib.rs | 20 + symphonia/Cargo.toml | 9 +- symphonia/README.md | 2 + symphonia/src/lib.rs | 6 + 11 files changed, 1035 insertions(+), 2 deletions(-) create mode 100644 symphonia-format-caf/Cargo.toml create mode 100644 symphonia-format-caf/README.md create mode 100644 symphonia-format-caf/src/chunks.rs create mode 100644 symphonia-format-caf/src/demuxer.rs create mode 100644 symphonia-format-caf/src/lib.rs diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 8521175a..7616dd41 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -8,6 +8,7 @@ Philip Deljanov # Please keep this section sorted in ascending order. Akiyuki Okayasu +Ian Hobson Kostya Shishkov Thom Chiovoloni diff --git a/README.md b/README.md index 45848a0f..122928d3 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ A status of *Excellent* is only assigned after the feature passes all compliance | Format | Status | Gapless* | Feature Flag | Default | Crate | |----------|-----------|----------|--------------|---------|-----------------------------| | AIFF | Great | Yes | `aiff` | No | [`symphonia-format-riff`] | +| CAF | Good | No | `caf` | No | [`symphonia-format-caf`] | | ISO/MP4 | Great | No | `isomp4` | No | [`symphonia-format-isomp4`] | | MKV/WebM | Good | No | `mkv` | Yes | [`symphonia-format-mkv`] | | OGG | Great | Yes | `ogg` | Yes | [`symphonia-format-ogg`] | @@ -95,6 +96,7 @@ A status of *Excellent* is only assigned after the feature passes all compliance \* Gapless playback requires support from both the demuxer and decoder. +[`symphonia-format-caf`]: https://docs.rs/symphonia-format-caf [`symphonia-format-isomp4`]: https://docs.rs/symphonia-format-isomp4 [`symphonia-format-mkv`]: https://docs.rs/symphonia-format-mkv [`symphonia-format-ogg`]: https://docs.rs/symphonia-format-ogg diff --git a/symphonia-core/src/sample.rs b/symphonia-core/src/sample.rs index eafb87af..4563baa8 100644 --- a/symphonia-core/src/sample.rs +++ b/symphonia-core/src/sample.rs @@ -30,7 +30,7 @@ pub enum SampleFormat { S24, /// Signed 32-bit integer. S32, - /// Single prevision (32-bit) floating point. + /// Single precision (32-bit) floating point. F32, /// Double precision (64-bit) floating point. F64, diff --git a/symphonia-format-caf/Cargo.toml b/symphonia-format-caf/Cargo.toml new file mode 100644 index 00000000..46979afc --- /dev/null +++ b/symphonia-format-caf/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "symphonia-format-caf" +version = "0.5.2" +description = "Pure Rust CAF demuxer (a part of project Symphonia)." +homepage = "https://github.com/pdeljanov/Symphonia" +repository = "https://github.com/pdeljanov/Symphonia" +authors = ["Ian Hobson ", "Philip Deljanov "] +license = "MPL-2.0" +readme = "README.md" +categories = ["multimedia", "multimedia::audio", "multimedia::encoding"] +keywords = ["audio", "media", "demuxer", "caf"] +edition = "2018" +rust-version = "1.53" + +[dependencies] +log = "0.4" +symphonia-core = { version = "0.5.2", path = "../symphonia-core" } +symphonia-metadata = { version = "0.5.2", path = "../symphonia-metadata" } diff --git a/symphonia-format-caf/README.md b/symphonia-format-caf/README.md new file mode 100644 index 00000000..7a648db2 --- /dev/null +++ b/symphonia-format-caf/README.md @@ -0,0 +1,15 @@ +# Symphonia Core Audio Format demuxer + +CAF decoder for Project Symphonia. + +**Note:** This crate is part of Symphonia. Please use the [`symphonia`](https://crates.io/crates/symphonia) crate instead of this one directly. + +## License + +Symphonia is provided under the MPL v2.0 license. Please refer to the LICENSE file for more details. + +## Contributing + +Symphonia is an open-source project and contributions are very welcome! If you would like to make a large contribution, please raise an issue ahead of time to make sure your efforts fit into the project goals, and that no duplication of efforts occurs. + +All contributors will be credited within the CONTRIBUTORS file. diff --git a/symphonia-format-caf/src/chunks.rs b/symphonia-format-caf/src/chunks.rs new file mode 100644 index 00000000..733dbb15 --- /dev/null +++ b/symphonia-format-caf/src/chunks.rs @@ -0,0 +1,589 @@ +// Symphonia +// Copyright (c) 2019-2024 The Project Symphonia Developers. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use log::{debug, error, info, warn}; +use std::{convert::TryFrom, fmt, mem::size_of, str}; +use symphonia_core::{ + audio::{Channels, Layout}, + codecs::*, + errors::{decode_error, unsupported_error, Error, Result}, + io::{MediaSourceStream, ReadBytes}, +}; + +#[derive(Debug)] +pub enum Chunk { + AudioDescription(AudioDescription), + AudioData(AudioData), + ChannelLayout(ChannelLayout), + PacketTable(PacketTable), + MagicCookie(Box<[u8]>), + Free, +} + +impl Chunk { + /// Reads a chunk + /// + /// After calling this function the reader's position will be: + /// - at the start of the next chunk, + /// - or, at the end of the file, + /// - or, if the chunk is the audio data chunk and the size is unknown, + /// then at the start of the audio data. + /// + /// The first chunk read will be the AudioDescription chunk. Once it's been read, the caller + /// should pass it in to subsequent read calls. + pub fn read( + reader: &mut MediaSourceStream, + audio_description: &Option, + ) -> Result> { + let chunk_type = reader.read_quad_bytes()?; + let chunk_size = reader.read_be_i64()?; + + let result = match &chunk_type { + b"desc" => Chunk::AudioDescription(AudioDescription::read(reader, chunk_size)?), + b"data" => Chunk::AudioData(AudioData::read(reader, chunk_size)?), + b"chan" => Chunk::ChannelLayout(ChannelLayout::read(reader, chunk_size)?), + b"pakt" => { + Chunk::PacketTable(PacketTable::read(reader, audio_description, chunk_size)?) + } + b"kuki" => { + if let Ok(chunk_size) = usize::try_from(chunk_size) { + Chunk::MagicCookie(reader.read_boxed_slice_exact(chunk_size)?) + } + else { + return invalid_chunk_size_error("Magic Cookie", chunk_size); + } + } + b"free" => { + if chunk_size < 0 { + return invalid_chunk_size_error("Free", chunk_size); + } + reader.ignore_bytes(chunk_size as u64)?; + Chunk::Free + } + other => { + // Log unsupported chunk types but don't return an error + info!( + "unsupported chunk type ('{}')", + str::from_utf8(other.as_slice()).unwrap_or("????") + ); + + if chunk_size >= 0 { + reader.ignore_bytes(chunk_size as u64)?; + return Ok(None); + } + else { + return invalid_chunk_size_error("unsupported", chunk_size); + } + } + }; + + debug!("chunk: {result:?} - size: {chunk_size}"); + Ok(Some(result)) + } +} + +#[derive(Debug)] +pub struct AudioDescription { + pub sample_rate: f64, + pub format_id: AudioDescriptionFormatId, + pub bytes_per_packet: u32, + pub frames_per_packet: u32, + pub channels_per_frame: u32, + pub bits_per_channel: u32, +} + +impl AudioDescription { + pub fn read(reader: &mut MediaSourceStream, chunk_size: i64) -> Result { + if chunk_size != 32 { + return invalid_chunk_size_error("Audio Description", chunk_size); + } + + let sample_rate = reader.read_be_f64()?; + if sample_rate == 0.0 { + return decode_error("caf: sample rate must be not be zero"); + } + + let format_id = AudioDescriptionFormatId::read(reader)?; + + let bytes_per_packet = reader.read_be_u32()?; + let frames_per_packet = reader.read_be_u32()?; + + let channels_per_frame = reader.read_be_u32()?; + if channels_per_frame == 0 { + return decode_error("caf: channels per frame must be not be zero"); + } + + let bits_per_channel = reader.read_be_u32()?; + + Ok(Self { + sample_rate, + format_id, + bytes_per_packet, + frames_per_packet, + channels_per_frame, + bits_per_channel, + }) + } + + pub fn codec_type(&self) -> Result { + use AudioDescriptionFormatId::*; + + let result = match &self.format_id { + LinearPCM { floating_point, little_endian } => { + if *floating_point { + match (self.bits_per_channel, *little_endian) { + (32, true) => CODEC_TYPE_PCM_F32LE, + (32, false) => CODEC_TYPE_PCM_F32BE, + (64, true) => CODEC_TYPE_PCM_F64LE, + (64, false) => CODEC_TYPE_PCM_F64BE, + (bits, _) => { + error!("unsupported PCM floating point format (bits: {})", bits); + return unsupported_error("caf: unsupported bits per channel"); + } + } + } + else { + match (self.bits_per_channel, *little_endian) { + (16, true) => CODEC_TYPE_PCM_S16LE, + (16, false) => CODEC_TYPE_PCM_S16BE, + (24, true) => CODEC_TYPE_PCM_S24LE, + (24, false) => CODEC_TYPE_PCM_S24BE, + (32, true) => CODEC_TYPE_PCM_S32LE, + (32, false) => CODEC_TYPE_PCM_S32BE, + (bits, _) => { + error!("unsupported PCM integer format (bits: {})", bits); + return unsupported_error("caf: unsupported bits per channel"); + } + } + } + } + AppleIMA4 => CODEC_TYPE_ADPCM_IMA_WAV, + MPEG4AAC => CODEC_TYPE_AAC, + ULaw => CODEC_TYPE_PCM_MULAW, + ALaw => CODEC_TYPE_PCM_ALAW, + MPEGLayer1 => CODEC_TYPE_MP1, + MPEGLayer2 => CODEC_TYPE_MP2, + MPEGLayer3 => CODEC_TYPE_MP3, + AppleLossless => CODEC_TYPE_ALAC, + Flac => CODEC_TYPE_FLAC, + Opus => CODEC_TYPE_OPUS, + unsupported => { + error!("unsupported codec ({:?})", unsupported); + return unsupported_error("caf: unsupported codec"); + } + }; + + Ok(result) + } + + pub fn format_is_compressed(&self) -> bool { + self.bits_per_channel == 0 + } +} + +#[derive(Debug)] +pub struct AudioData { + pub _edit_count: u32, + pub start_pos: u64, + pub data_len: Option, +} + +impl AudioData { + pub fn read(reader: &mut MediaSourceStream, chunk_size: i64) -> Result { + let edit_count_offset = size_of::() as i64; + + if chunk_size != -1 && chunk_size < edit_count_offset { + return invalid_chunk_size_error("Audio Data", chunk_size); + } + + let edit_count = reader.read_be_u32()?; + let start_pos = reader.pos(); + + if chunk_size == -1 { + return Ok(Self { _edit_count: edit_count, start_pos, data_len: None }); + } + + let data_len = (chunk_size - edit_count_offset) as u64; + debug!("data_len: {}", data_len); + reader.ignore_bytes(data_len)?; + Ok(Self { _edit_count: edit_count, start_pos, data_len: Some(data_len) }) + } +} + +#[derive(Debug)] +pub enum AudioDescriptionFormatId { + LinearPCM { floating_point: bool, little_endian: bool }, + AppleIMA4, + MPEG4AAC, + MACE3, + MACE6, + ULaw, + ALaw, + MPEGLayer1, + MPEGLayer2, + MPEGLayer3, + AppleLossless, + Flac, + Opus, +} + +impl AudioDescriptionFormatId { + pub fn read(reader: &mut MediaSourceStream) -> Result { + use AudioDescriptionFormatId::*; + + let format_id = reader.read_quad_bytes()?; + let format_flags = reader.read_be_u32()?; + + let result = match &format_id { + // Formats mentioned in the spec + b"lpcm" => { + let floating_point = format_flags & (1 << 0) != 0; + let little_endian = format_flags & (1 << 1) != 0; + return Ok(LinearPCM { floating_point, little_endian }); + } + b"ima4" => AppleIMA4, + b"aac " => { + if format_flags != 2 { + warn!("undocumented AAC object type ({})", format_flags); + } + return Ok(MPEG4AAC); + } + b"MAC3" => MACE3, + b"MAC6" => MACE6, + b"ulaw" => ULaw, + b"alaw" => ALaw, + b".mp1" => MPEGLayer1, + b".mp2" => MPEGLayer2, + b".mp3" => MPEGLayer3, + b"alac" => AppleLossless, + // Additional formats from CoreAudioBaseTypes.h + b"flac" => Flac, + b"opus" => Opus, + other => { + error!("unsupported format id ({:?})", other); + return unsupported_error("caf: unsupported format id"); + } + }; + + if format_flags != 0 { + info!("non-zero format flags ({})", format_flags); + } + + Ok(result) + } +} + +#[derive(Debug)] +pub struct ChannelLayout { + pub channel_layout: u32, + pub channel_bitmap: u32, + pub channel_descriptions: Vec, +} + +impl ChannelLayout { + pub fn read(reader: &mut MediaSourceStream, chunk_size: i64) -> Result { + if chunk_size < 12 { + return invalid_chunk_size_error("Channel Layout", chunk_size); + } + + let channel_layout = reader.read_be_u32()?; + let channel_bitmap = reader.read_be_u32()?; + let channel_description_count = reader.read_be_u32()?; + let channel_descriptions: Vec = (0..channel_description_count) + .map(|_| ChannelDescription::read(reader)) + .collect::>()?; + + Ok(Self { channel_layout, channel_bitmap, channel_descriptions }) + } + + pub fn channels(&self) -> Option { + let channels = match self.channel_layout { + // Use channel descriptions + 0 => { + let mut channels: u32 = 0; + for channel in self.channel_descriptions.iter() { + match channel.channel_label { + 1 => channels |= Channels::FRONT_LEFT.bits(), + 2 => channels |= Channels::FRONT_RIGHT.bits(), + 3 => channels |= Channels::FRONT_CENTRE.bits(), + 4 => channels |= Channels::LFE1.bits(), + 5 => channels |= Channels::REAR_LEFT.bits(), + 6 => channels |= Channels::REAR_RIGHT.bits(), + 7 => channels |= Channels::FRONT_LEFT_CENTRE.bits(), + 8 => channels |= Channels::FRONT_RIGHT_CENTRE.bits(), + 9 => channels |= Channels::REAR_CENTRE.bits(), + 10 => channels |= Channels::SIDE_LEFT.bits(), + 11 => channels |= Channels::SIDE_RIGHT.bits(), + 12 => channels |= Channels::TOP_CENTRE.bits(), + 13 => channels |= Channels::TOP_FRONT_LEFT.bits(), + 14 => channels |= Channels::TOP_FRONT_CENTRE.bits(), + 15 => channels |= Channels::TOP_FRONT_RIGHT.bits(), + 16 => channels |= Channels::TOP_REAR_LEFT.bits(), + 17 => channels |= Channels::TOP_REAR_CENTRE.bits(), + 18 => channels |= Channels::TOP_REAR_RIGHT.bits(), + unsupported => { + info!("unsupported channel label: {}", unsupported); + return None; + } + } + } + return Channels::from_bits(channels); + } + // Use the channel bitmap + LAYOUT_TAG_USE_CHANNEL_BITMAP => return Channels::from_bits(self.channel_bitmap), + // Layout tags which have channel roles that match the standard channel layout + LAYOUT_TAG_MONO => Layout::Mono.into_channels(), + LAYOUT_TAG_STEREO | LAYOUT_TAG_STEREO_HEADPHONES => Layout::Stereo.into_channels(), + LAYOUT_TAG_MPEG_3_0_A => { + Channels::FRONT_LEFT | Channels::FRONT_RIGHT | Channels::FRONT_CENTRE + } + LAYOUT_TAG_MPEG_5_1_A => Layout::FivePointOne.into_channels(), + LAYOUT_TAG_MPEG_7_1_A => { + Channels::FRONT_LEFT + | Channels::FRONT_RIGHT + | Channels::FRONT_CENTRE + | Channels::LFE1 + | Channels::REAR_LEFT + | Channels::REAR_RIGHT + | Channels::FRONT_LEFT_CENTRE + | Channels::FRONT_RIGHT_CENTRE + } + LAYOUT_TAG_DVD_10 => { + Channels::FRONT_LEFT + | Channels::FRONT_RIGHT + | Channels::FRONT_CENTRE + | Channels::LFE1 + } + unsupported => { + debug!("unsupported channel layout: {}", unsupported); + return None; + } + }; + + Some(channels) + } +} + +#[derive(Debug)] +pub struct ChannelDescription { + pub channel_label: u32, + pub channel_flags: u32, + pub coordinates: [f32; 3], +} + +impl ChannelDescription { + pub fn read(reader: &mut MediaSourceStream) -> Result { + Ok(Self { + channel_label: reader.read_be_u32()?, + channel_flags: reader.read_be_u32()?, + coordinates: [reader.read_be_f32()?, reader.read_be_f32()?, reader.read_be_f32()?], + }) + } +} + +const LAYOUT_TAG_USE_CHANNEL_BITMAP: u32 = 1 << 16; +// Layout tags from the CAF spec that match the first N channels of a standard layout +const LAYOUT_TAG_MONO: u32 = (100 << 16) | 1; +const LAYOUT_TAG_STEREO: u32 = (101 << 16) | 2; +const LAYOUT_TAG_STEREO_HEADPHONES: u32 = (102 << 16) | 2; +const LAYOUT_TAG_MPEG_3_0_A: u32 = (113 << 16) | 3; // L R C +const LAYOUT_TAG_MPEG_5_1_A: u32 = (121 << 16) | 6; // L R C LFE Ls Rs +const LAYOUT_TAG_MPEG_7_1_A: u32 = (126 << 16) | 8; // L R C LFE Ls Rs Lc Rc +const LAYOUT_TAG_DVD_10: u32 = (136 << 16) | 4; // L R C LFE + +pub struct PacketTable { + pub valid_frames: i64, + pub priming_frames: i32, + pub remainder_frames: i32, + pub packets: Vec, +} + +impl PacketTable { + pub fn read( + reader: &mut MediaSourceStream, + desc: &Option, + chunk_size: i64, + ) -> Result { + if chunk_size < 24 { + return invalid_chunk_size_error("Packet Table", chunk_size); + } + + let desc = desc.as_ref().ok_or_else(|| { + error!("missing audio description"); + Error::DecodeError("caf: missing audio descripton") + })?; + + let total_packets = reader.read_be_i64()?; + if total_packets < 0 { + error!("invalid number of packets in the packet table ({})", total_packets); + return decode_error("caf: invalid number of packets in the packet table"); + } + + let valid_frames = reader.read_be_i64()?; + if valid_frames < 0 { + error!("invalid number of frames in the packet table ({})", valid_frames); + return decode_error("caf: invalid number of frames in the packet table"); + } + + let priming_frames = reader.read_be_i32()?; + let remainder_frames = reader.read_be_i32()?; + + let mut packets = Vec::with_capacity(total_packets as usize); + let mut current_frame = 0; + let mut packet_offset = 0; + + match (desc.bytes_per_packet, desc.frames_per_packet) { + // Variable bytes per packet, variable number of frames + (0, 0) => { + for _ in 0..total_packets { + let size = read_variable_length_integer(reader)?; + let frames = read_variable_length_integer(reader)?; + packets.push(CafPacket { + size, + frames, + start_frame: current_frame, + data_offset: packet_offset, + }); + current_frame += frames; + packet_offset += size; + } + } + // Variable bytes per packet, constant number of frames + (0, frames_per_packet) => { + for _ in 0..total_packets { + let size = read_variable_length_integer(reader)?; + let frames = frames_per_packet as u64; + packets.push(CafPacket { + size, + frames, + start_frame: current_frame, + data_offset: packet_offset, + }); + current_frame += frames; + packet_offset += size; + } + } + // Constant bytes per packet, variable number of frames + (bytes_per_packet, 0) => { + for _ in 0..total_packets { + let size = bytes_per_packet as u64; + let frames = read_variable_length_integer(reader)?; + packets.push(CafPacket { + size, + frames, + start_frame: current_frame, + data_offset: packet_offset, + }); + current_frame += frames; + packet_offset += size; + } + } + // Constant bit rate format + (_, _) => { + if total_packets > 0 { + error!( + "unexpected packet table for constant bit rate ({} packets)", + total_packets + ); + return decode_error( + "caf: unexpected packet table for constant bit rate format", + ); + } + } + } + + Ok(Self { valid_frames, priming_frames, remainder_frames, packets }) + } +} + +impl fmt::Debug for PacketTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PacketTable")?; + write!( + f, + "{{ valid_frames: {}, priming_frames: {}, remainder_frames: {}, packet count: {}}}", + self.valid_frames, + self.priming_frames, + self.remainder_frames, + self.packets.len() + ) + } +} + +#[derive(Debug)] +pub struct CafPacket { + // The packet's offset in bytes from the start of the data + pub data_offset: u64, + // The index of the first frame in the packet + pub start_frame: u64, + // The number of frames in the packet + // For files with a constant frames per packet this value will match frames_per_packet + pub frames: u64, + // The size in bytes of the packet + // For constant bit-rate files this value will match bytes_per_packet + pub size: u64, +} + +fn invalid_chunk_size_error(chunk_type: &str, chunk_size: i64) -> Result { + error!("invalid {} chunk size ({})", chunk_type, chunk_size); + decode_error("caf: invalid chunk size") +} + +fn read_variable_length_integer(reader: &mut MediaSourceStream) -> Result { + let mut result = 0; + + for _ in 0..9 { + let byte = reader.read_byte()?; + + result |= (byte & 0x7f) as u64; + + if byte & 0x80 == 0 { + return Ok(result); + } + + result <<= 7; + } + + decode_error("caf: unterminated variable-length integer") +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + fn variable_length_integer_test(bytes: &[u8], expected: u64) -> Result<()> { + let cursor = Cursor::new(Vec::from(bytes)); + let mut source = MediaSourceStream::new(Box::new(cursor), Default::default()); + + assert_eq!(read_variable_length_integer(&mut source)?, expected); + + Ok(()) + } + + #[test] + fn variable_length_integers() -> Result<()> { + variable_length_integer_test(&[0x01], 1)?; + variable_length_integer_test(&[0x11], 17)?; + variable_length_integer_test(&[0x7f], 127)?; + variable_length_integer_test(&[0x81, 0x00], 128)?; + variable_length_integer_test(&[0x81, 0x02], 130)?; + variable_length_integer_test(&[0x82, 0x01], 257)?; + variable_length_integer_test(&[0xff, 0x7f], 16383)?; + variable_length_integer_test(&[0x81, 0x80, 0x00], 16384)?; + Ok(()) + } + + #[test] + fn unterminated_variable_length_integer() { + let cursor = Cursor::new(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + let mut source = MediaSourceStream::new(Box::new(cursor), Default::default()); + + assert!(read_variable_length_integer(&mut source).is_err()); + } +} diff --git a/symphonia-format-caf/src/demuxer.rs b/symphonia-format-caf/src/demuxer.rs new file mode 100644 index 00000000..2a879c4d --- /dev/null +++ b/symphonia-format-caf/src/demuxer.rs @@ -0,0 +1,373 @@ +// Symphonia +// Copyright (c) 2019-2024 The Project Symphonia Developers. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::chunks::*; +use log::{debug, error, info}; +use std::io::{Seek, SeekFrom}; +use symphonia_core::{ + audio::Channels, + codecs::*, + errors::{ + decode_error, end_of_stream_error, seek_error, unsupported_error, Result, SeekErrorKind, + }, + formats::{Cue, FormatOptions, FormatReader, Packet, SeekMode, SeekTo, SeekedTo, Track}, + io::{MediaSource, MediaSourceStream, ReadBytes}, + meta::{Metadata, MetadataLog}, + probe::{Descriptor, Instantiate, QueryDescriptor}, + support_format, + units::{TimeBase, TimeStamp}, +}; + +const MAX_FRAMES_PER_PACKET: u64 = 1152; + +/// Core Audio Format (CAF) format reader. +/// +/// `CafReader` implements a demuxer for Core Audio Format containers. +pub struct CafReader { + reader: MediaSourceStream, + tracks: Vec, + cues: Vec, + metadata: MetadataLog, + data_start_pos: u64, + data_len: Option, + packet_info: PacketInfo, +} + +enum PacketInfo { + Unknown, + Uncompressed { bytes_per_frame: u32 }, + Compressed { packets: Vec, current_packet_index: usize }, +} + +impl QueryDescriptor for CafReader { + fn query() -> &'static [Descriptor] { + &[support_format!("caf", "Core Audio Format", &["caf"], &["audio/x-caf"], &[b"caff"])] + } + + fn score(_context: &[u8]) -> u8 { + 255 + } +} + +impl FormatReader for CafReader { + fn try_new(source: MediaSourceStream, _options: &FormatOptions) -> Result { + let mut reader = Self { + reader: source, + tracks: vec![], + cues: vec![], + metadata: MetadataLog::default(), + data_start_pos: 0, + data_len: None, + packet_info: PacketInfo::Unknown, + }; + + reader.check_file_header()?; + let codec_params = reader.read_chunks()?; + + reader.tracks.push(Track::new(0, codec_params)); + + Ok(reader) + } + + fn next_packet(&mut self) -> Result { + match &mut self.packet_info { + PacketInfo::Uncompressed { bytes_per_frame } => { + let pos = self.reader.pos(); + let data_pos = pos - self.data_start_pos; + + let bytes_per_frame = *bytes_per_frame as u64; + let max_bytes_to_read = bytes_per_frame * MAX_FRAMES_PER_PACKET; + + let bytes_remaining = if let Some(data_len) = self.data_len { + data_len - data_pos + } + else { + max_bytes_to_read + }; + + if bytes_remaining == 0 { + return end_of_stream_error(); + } + + let bytes_to_read = max_bytes_to_read.min(bytes_remaining); + let packet_duration = bytes_to_read / bytes_per_frame; + let packet_timestamp = data_pos / bytes_per_frame; + let buffer = self.reader.read_boxed_slice(bytes_to_read as usize)?; + Ok(Packet::new_from_boxed_slice(0, packet_timestamp, packet_duration, buffer)) + } + PacketInfo::Compressed { packets, ref mut current_packet_index } => { + if let Some(packet) = packets.get(*current_packet_index) { + *current_packet_index += 1; + let buffer = self.reader.read_boxed_slice(packet.size as usize)?; + Ok(Packet::new_from_boxed_slice(0, packet.start_frame, packet.frames, buffer)) + } + else if *current_packet_index == packets.len() { + end_of_stream_error() + } + else { + decode_error("caf: invalid packet index") + } + } + PacketInfo::Unknown => decode_error("caf: missing packet info"), + } + } + + fn metadata(&mut self) -> Metadata<'_> { + self.metadata.metadata() + } + + fn cues(&self) -> &[Cue] { + &self.cues + } + + fn tracks(&self) -> &[Track] { + &self.tracks + } + + fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> Result { + let required_ts = match to { + SeekTo::TimeStamp { ts, .. } => ts, + SeekTo::Time { time, .. } => { + if let Some(time_base) = self.time_base() { + time_base.calc_timestamp(time) + } + else { + return seek_error(SeekErrorKind::Unseekable); + } + } + }; + + match &mut self.packet_info { + PacketInfo::Uncompressed { bytes_per_frame } => { + // Packetization for PCM data is performed by chunking the stream into + // packets of MAX_FRAMES_PER_PACKET frames each. + // To allow for determinstic packet timestamps, we want the seek to jump to the + // packet boundary before the requested seek time. + let actual_ts = (required_ts / MAX_FRAMES_PER_PACKET) * MAX_FRAMES_PER_PACKET; + let seek_pos = self.data_start_pos + actual_ts * (*bytes_per_frame as u64); + + if self.reader.is_seekable() { + self.reader.seek(SeekFrom::Start(seek_pos))?; + } + else { + let current_pos = self.reader.pos(); + if seek_pos >= current_pos { + self.reader.ignore_bytes(seek_pos - current_pos)?; + } + else { + return seek_error(SeekErrorKind::ForwardOnly); + } + } + + debug!( + "seek required_ts: {}, actual_ts: {}, (difference: {})", + actual_ts, + required_ts, + actual_ts as i64 - required_ts as i64, + ); + + Ok(SeekedTo { track_id: 0, actual_ts, required_ts }) + } + PacketInfo::Compressed { packets, current_packet_index } => { + let current_ts = if let Some(packet) = packets.get(*current_packet_index) { + TimeStamp::from(packet.start_frame) + } + else { + error!("invalid packet index: {}", current_packet_index); + return decode_error("caf: invalid packet index"); + }; + + let search_range = if current_ts < required_ts { + *current_packet_index..packets.len() + } + else { + 0..*current_packet_index + }; + + let packet_after_ts = packets[search_range] + .partition_point(|packet| packet.start_frame < required_ts); + let seek_packet_index = packet_after_ts.saturating_sub(1); + let seek_packet = &packets[seek_packet_index]; + + let seek_pos = self.data_start_pos + seek_packet.data_offset; + + if self.reader.is_seekable() { + self.reader.seek(SeekFrom::Start(seek_pos))?; + } + else { + let current_pos = self.reader.pos(); + if seek_pos >= current_pos { + self.reader.ignore_bytes(seek_pos - current_pos)?; + } + else { + return seek_error(SeekErrorKind::ForwardOnly); + } + } + + *current_packet_index = seek_packet_index; + let actual_ts = TimeStamp::from(seek_packet.start_frame); + + debug!( + "seek required_ts: {}, actual_ts: {}, (difference: {}, packet: {})", + required_ts, + actual_ts, + actual_ts as i64 - required_ts as i64, + seek_packet_index, + ); + + Ok(SeekedTo { track_id: 0, actual_ts, required_ts }) + } + PacketInfo::Unknown => decode_error("caf: missing packet info"), + } + } + + fn into_inner(self: Box) -> MediaSourceStream { + self.reader + } +} + +impl CafReader { + fn time_base(&self) -> Option { + self.tracks.first().and_then(|track| { + track.codec_params.sample_rate.map(|sample_rate| TimeBase::new(1, sample_rate)) + }) + } + + fn check_file_header(&mut self) -> Result<()> { + let file_type = self.reader.read_quad_bytes()?; + if file_type != *b"caff" { + return unsupported_error("caf: missing 'caff' stream marker"); + } + + let file_version = self.reader.read_be_u16()?; + if file_version != 1 { + error!("unsupported file version ({})", file_version); + return unsupported_error("caf: unsupported file version"); + } + + // Ignored in CAF v1 + let _file_flags = self.reader.read_be_u16()?; + + Ok(()) + } + + fn read_audio_description_chunk( + &mut self, + desc: &AudioDescription, + codec_params: &mut CodecParameters, + ) -> Result<()> { + codec_params + .for_codec(desc.codec_type()?) + .with_sample_rate(desc.sample_rate as u32) + .with_time_base(TimeBase::new(1, desc.sample_rate as u32)) + .with_bits_per_sample(desc.bits_per_channel) + .with_bits_per_coded_sample((desc.bytes_per_packet * 8) / desc.channels_per_frame); + + match desc.channels_per_frame { + 0 => { + // A channel count of zero should have been rejected by the AudioDescription parser + unreachable!("Invalid channel count"); + } + 1 => { + codec_params.with_channels(Channels::FRONT_LEFT); + } + 2 => { + codec_params.with_channels(Channels::FRONT_LEFT | Channels::FRONT_RIGHT); + } + n => { + // When the channel count is >2 then enable the first N channels. + // This can/should be overridden when parsing the channel layout chunk. + match Channels::from_bits(((1u64 << n as u64) - 1) as u32) { + Some(channels) => { + codec_params.with_channels(channels); + } + None => { + return unsupported_error("caf: unsupported channel count"); + } + } + } + } + + if desc.format_is_compressed() { + self.packet_info = + PacketInfo::Compressed { packets: Vec::new(), current_packet_index: 0 }; + } + else { + codec_params.with_max_frames_per_packet(MAX_FRAMES_PER_PACKET).with_frames_per_block(1); + self.packet_info = PacketInfo::Uncompressed { bytes_per_frame: desc.bytes_per_packet } + }; + + Ok(()) + } + + fn read_chunks(&mut self) -> Result { + use Chunk::*; + + let mut codec_params = CodecParameters::new(); + let mut audio_description = None; + + loop { + match Chunk::read(&mut self.reader, &audio_description)? { + Some(AudioDescription(desc)) => { + if audio_description.is_some() { + return decode_error("caf: additional Audio Description chunk"); + } + self.read_audio_description_chunk(&desc, &mut codec_params)?; + audio_description = Some(desc); + } + Some(AudioData(data)) => { + self.data_start_pos = data.start_pos; + self.data_len = data.data_len; + if let Some(data_len) = self.data_len { + if let PacketInfo::Uncompressed { bytes_per_frame } = &self.packet_info { + codec_params.with_n_frames(data_len / *bytes_per_frame as u64); + } + } + } + Some(ChannelLayout(layout)) => { + if let Some(channels) = layout.channels() { + codec_params.channels = Some(channels); + } + else { + // Don't error if the layout doesn't correspond directly to a Symphonia + // layout, the channels bitmap was set after the audio description was read + // to match the number of channels, and that's probably OK. + info!("couldn't convert the channel layout into a channel bitmap"); + } + } + Some(PacketTable(table)) => { + if let PacketInfo::Compressed { ref mut packets, .. } = &mut self.packet_info { + codec_params.with_n_frames(table.valid_frames as u64); + *packets = table.packets; + } + } + Some(MagicCookie(data)) => { + codec_params.with_extra_data(data); + } + Some(Free) | None => {} + } + + if audio_description.is_none() { + error!("missing audio description chunk"); + return decode_error("caf: missing audio description chunk"); + } + + if let Some(byte_len) = self.reader.byte_len() { + if self.reader.pos() == byte_len { + // If we've reached the end of the file, then the Audio Data chunk should have + // had a defined size, and we should seek to the start of the audio data. + if self.data_len.is_some() { + self.reader.seek(SeekFrom::Start(self.data_start_pos))?; + } + break; + } + } + } + + Ok(codec_params) + } +} diff --git a/symphonia-format-caf/src/lib.rs b/symphonia-format-caf/src/lib.rs new file mode 100644 index 00000000..15320ac7 --- /dev/null +++ b/symphonia-format-caf/src/lib.rs @@ -0,0 +1,20 @@ +// Symphonia +// Copyright (c) 2019-2024 The Project Symphonia Developers. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![warn(rust_2018_idioms)] +#![forbid(unsafe_code)] +// The following lints are allowed in all Symphonia crates. Please see clippy.toml for their +// justification. +#![allow(clippy::comparison_chain)] +#![allow(clippy::excessive_precision)] +#![allow(clippy::identity_op)] +#![allow(clippy::manual_range_contains)] + +mod chunks; +mod demuxer; + +pub use demuxer::CafReader; diff --git a/symphonia/Cargo.toml b/symphonia/Cargo.toml index 8ea4f218..e55420e4 100644 --- a/symphonia/Cargo.toml +++ b/symphonia/Cargo.toml @@ -23,6 +23,7 @@ aac = ["symphonia-codec-aac"] adpcm = ["symphonia-codec-adpcm"] alac = ["symphonia-codec-alac"] flac = ["symphonia-bundle-flac"] +caf = ["symphonia-format-caf"] isomp4 = ["symphonia-format-isomp4"] mkv = ["symphonia-format-mkv"] mp1 = ["symphonia-bundle-mp3/mp1"] @@ -52,6 +53,7 @@ all-codecs = [ # Enable all supported formats. all-formats = [ + "caf", "isomp4", "mkv", "ogg", @@ -147,6 +149,11 @@ version = "0.5.2" path = "../symphonia-format-mkv" optional = true +[dependencies.symphonia-format-caf] +version = "0.5.2" +path = "../symphonia-format-caf" +optional = true + # Show documentation with all features enabled on docs.rs [package.metadata.docs.rs] -all-features = true \ No newline at end of file +all-features = true diff --git a/symphonia/README.md b/symphonia/README.md index 42cd5be7..226852b2 100644 --- a/symphonia/README.md +++ b/symphonia/README.md @@ -87,6 +87,7 @@ A status of *Excellent* is only assigned after the feature passes all compliance | Format | Status | Gapless* | Feature Flag | Default | Crate | |----------|-----------|----------|--------------|---------|-----------------------------| | AIFF | Great | Yes | `aiff` | No | [`symphonia-format-riff`] | +| CAF | Good | No | `caf` | No | [`symphonia-format-caf`] | | ISO/MP4 | Great | No | `isomp4` | No | [`symphonia-format-isomp4`] | | MKV/WebM | Good | No | `mkv` | Yes | [`symphonia-format-mkv`] | | OGG | Great | Yes | `ogg` | Yes | [`symphonia-format-ogg`] | @@ -94,6 +95,7 @@ A status of *Excellent* is only assigned after the feature passes all compliance \* Gapless playback requires support from both the demuxer and decoder. +[`symphonia-format-caf`]: https://docs.rs/symphonia-format-caf [`symphonia-format-isomp4`]: https://docs.rs/symphonia-format-isomp4 [`symphonia-format-mkv`]: https://docs.rs/symphonia-format-mkv [`symphonia-format-ogg`]: https://docs.rs/symphonia-format-ogg diff --git a/symphonia/src/lib.rs b/symphonia/src/lib.rs index ae3107f3..e16fddfe 100644 --- a/symphonia/src/lib.rs +++ b/symphonia/src/lib.rs @@ -27,6 +27,7 @@ //! | Format | Feature Flag | Gapless* | Default | //! |----------|--------------|----------|---------| //! | AIFF | `aiff` | Yes | No | +//! | CAF | `caf` | No | No | //! | ISO/MP4 | `isomp4` | No | No | //! | MKV/WebM | `mkv` | No | Yes | //! | OGG | `ogg` | Yes | Yes | @@ -170,6 +171,8 @@ pub mod default { pub use symphonia_bundle_mp3::MpaReader; #[cfg(feature = "aac")] pub use symphonia_codec_aac::AdtsReader; + #[cfg(feature = "caf")] + pub use symphonia_format_caf::CafReader; #[cfg(feature = "isomp4")] pub use symphonia_format_isomp4::IsoMp4Reader; #[cfg(feature = "mkv")] @@ -267,6 +270,9 @@ pub mod default { #[cfg(feature = "aac")] probe.register_all::(); + #[cfg(feature = "caf")] + probe.register_all::(); + #[cfg(feature = "flac")] probe.register_all::();