From 4c508b2371b13d6b66511531b9fbdfb3f3998658 Mon Sep 17 00:00:00 2001 From: Johannes Hackel Date: Tue, 8 Nov 2022 17:30:52 +0100 Subject: [PATCH] adpcm: Add ADPCM Decoder (#160) Add an ADPCM decoder supporting Microsoft and IMA ADPCM codecs. --- CONTRIBUTORS | 1 + Cargo.toml | 1 + README.md | 2 + symphonia-codec-adpcm/Cargo.toml | 17 +++ symphonia-codec-adpcm/README.md | 26 ++++ symphonia-codec-adpcm/src/codec_ima.rs | 99 ++++++++++++++ symphonia-codec-adpcm/src/codec_ms.rs | 139 +++++++++++++++++++ symphonia-codec-adpcm/src/common.rs | 36 +++++ symphonia-codec-adpcm/src/lib.rs | 182 +++++++++++++++++++++++++ symphonia-core/src/codecs.rs | 10 ++ symphonia-format-wav/src/chunks.rs | 100 +++++++++++++- symphonia-format-wav/src/lib.rs | 128 ++++++++++++----- symphonia/Cargo.toml | 4 +- symphonia/src/lib.rs | 5 + 14 files changed, 713 insertions(+), 37 deletions(-) create mode 100644 symphonia-codec-adpcm/Cargo.toml create mode 100644 symphonia-codec-adpcm/README.md create mode 100644 symphonia-codec-adpcm/src/codec_ima.rs create mode 100644 symphonia-codec-adpcm/src/codec_ms.rs create mode 100644 symphonia-codec-adpcm/src/common.rs create mode 100644 symphonia-codec-adpcm/src/lib.rs diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 1160292d..7848b42f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -20,6 +20,7 @@ BlackHoleFox [https://github.com/blackholefox] darksv [https://github.com/darksv] djugei [https://github.com/djugei] FelixMcFelix [https://github.com/FelixMcFelix] +geckoxx [https://github.com/geckoxx] Herohtar [https://github.com/herohtar] nicholaswyoung [https://github.com/nicholaswyoung] richardmitic [https://github.com/richardmitic] diff --git a/Cargo.toml b/Cargo.toml index 40326bef..bdc8480c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "symphonia-bundle-flac", "symphonia-bundle-mp3", "symphonia-codec-aac", + "symphonia-codec-adpcm", "symphonia-codec-alac", "symphonia-codec-opus", "symphonia-codec-pcm", diff --git a/README.md b/README.md index aed9e447..4818575c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ A status of *great* indicates that major development is complete and that the fe | Codec | Status | Gapless | Feature Flag | Default | Crate | |------------------------------|-----------|---------|--------------|---------|----------------------------| | AAC-LC | Great | No | `aac` | No | [`symphonia-codec-aac`] | +| ADPCM | Good | Yes | `adpcm` | Yes | [`symphonia-codec-adpcm`] | | ALAC | Great | Yes | `alac` | No | [`symphonia-codec-alac`] | | HE-AAC (AAC+, aacPlus) | - | - | `aac` | No | [`symphonia-codec-aac`] | | HE-AACv2 (eAAC+, aacPlus v2) | - | - | `aac` | No | [`symphonia-codec-aac`] | @@ -113,6 +114,7 @@ A status of *great* indicates that major development is complete and that the fe A `symphonia-bundle-*` package is a combination of a decoder and a native demuxer. [`symphonia-codec-aac`]: https://docs.rs/symphonia-codec-aac +[`symphonia-codec-adpcm`]: https://docs.rs/symphonia-codec-adpcm [`symphonia-codec-alac`]: https://docs.rs/symphonia-codec-alac [`symphonia-bundle-flac`]: https://docs.rs/symphonia-bundle-flac [`symphonia-bundle-mp3`]: https://docs.rs/symphonia-bundle-mp3 diff --git a/symphonia-codec-adpcm/Cargo.toml b/symphonia-codec-adpcm/Cargo.toml new file mode 100644 index 00000000..11bf81fc --- /dev/null +++ b/symphonia-codec-adpcm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "symphonia-codec-adpcm" +version = "0.5.1" +description = "Pure Rust ADPCM audio decoder (a part of project Symphonia)." +homepage = "https://github.com/pdeljanov/Symphonia" +repository = "https://github.com/pdeljanov/Symphonia" +authors = ["Philip Deljanov ", "Johannes Hackel "] +license = "MPL-2.0" +readme = "README.md" +categories = ["multimedia", "multimedia::audio", "multimedia::encoding"] +keywords = ["audio", "codec", "decoder", "adpcm"] +edition = "2018" +rust-version = "1.53" + +[dependencies] +log = "0.4" +symphonia-core = { version = "0.5", path = "../symphonia-core" } \ No newline at end of file diff --git a/symphonia-codec-adpcm/README.md b/symphonia-codec-adpcm/README.md new file mode 100644 index 00000000..bc8bb37b --- /dev/null +++ b/symphonia-codec-adpcm/README.md @@ -0,0 +1,26 @@ +# Symphonia ADPCM Codec + +[![Docs](https://docs.rs/symphonia-codec-adpcm/badge.svg)](https://docs.rs/symphonia-codec-adpcm) + +ADPCM audio decoders 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. + +## Support + +The following ADPCM encodings are supported: + +* Microsoft ADPCM +* ADPCM IMA WAV + +Only 4 bits per sample and only mono and stereo channels are supported. + +## 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-codec-adpcm/src/codec_ima.rs b/symphonia-codec-adpcm/src/codec_ima.rs new file mode 100644 index 00000000..29d61792 --- /dev/null +++ b/symphonia-codec-adpcm/src/codec_ima.rs @@ -0,0 +1,99 @@ +// Symphonia +// Copyright (c) 2019-2022 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; +use symphonia_core::util::clamp::clamp_i16; + +use crate::common::{from_i16_shift, u16_to_i32, Nibble}; + +#[rustfmt::skip] +const IMA_INDEX_TABLE: [i32; 16] = [ + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8, +]; + +#[rustfmt::skip] +const IMA_STEP_TABLE: [i32; 89] = [ + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767, +]; + +/// `AdpcmImaBlockStatus` contains values to decode a block +struct AdpcmImaBlockStatus { + predictor: i32, + step_index: i32, +} + +impl AdpcmImaBlockStatus { + fn read_preamble(stream: &mut B) -> Result { + let predictor = u16_to_i32!(stream.read_u16()?); + let step_index = stream.read_byte()? as i32; + //reserved byte + let _ = stream.read_byte()?; + let status = Self { predictor, step_index }; + Ok(status) + } + + fn expand_nibble(&mut self, byte: u8, nibble: Nibble) -> i32 { + let nibble = nibble.get_nibble(byte); + let step = IMA_STEP_TABLE[self.step_index as usize]; + let sign = (nibble & 0x08) != 0; + let delta = (nibble & 0x07) as i32; + let diff = ((2 * delta + 1) * step) >> 3; + let predictor = if sign { self.predictor - diff } else { self.predictor + diff }; + self.predictor = clamp_i16(predictor) as i32; + self.step_index = (self.step_index + IMA_INDEX_TABLE[nibble as usize]).min(88).max(0); + from_i16_shift!(self.predictor) + } +} + +pub(crate) fn decode_mono( + stream: &mut B, + buffer: &mut [i32], + frames_per_block: usize, +) -> Result<()> { + let data_bytes_per_channel = (frames_per_block - 1) / 2; + let mut status = AdpcmImaBlockStatus::read_preamble(stream)?; + buffer[0] = from_i16_shift!(status.predictor); + for byte in 0..data_bytes_per_channel { + let nibbles = stream.read_u8()?; + buffer[1 + byte * 2] = status.expand_nibble(nibbles, Nibble::Lower); + buffer[1 + byte * 2 + 1] = status.expand_nibble(nibbles, Nibble::Upper); + } + Ok(()) +} + +pub(crate) fn decode_stereo( + stream: &mut B, + buffers: [&mut [i32]; 2], + frames_per_block: usize, +) -> Result<()> { + let data_bytes_per_channel = frames_per_block - 1; + let mut status = + [AdpcmImaBlockStatus::read_preamble(stream)?, AdpcmImaBlockStatus::read_preamble(stream)?]; + buffers[0][0] = from_i16_shift!(status[0].predictor); + buffers[1][0] = from_i16_shift!(status[1].predictor); + for index in 0..data_bytes_per_channel { + let channel = (index / 4) & 1; + let offset = (index / 8) * 8; + let byte = index % 4; + let nibbles = stream.read_u8()?; + buffers[channel][1 + offset + byte * 2] = + status[channel].expand_nibble(nibbles, Nibble::Lower); + buffers[channel][1 + offset + byte * 2 + 1] = + status[channel].expand_nibble(nibbles, Nibble::Upper); + } + Ok(()) +} diff --git a/symphonia-codec-adpcm/src/codec_ms.rs b/symphonia-codec-adpcm/src/codec_ms.rs new file mode 100644 index 00000000..81415e0b --- /dev/null +++ b/symphonia-codec-adpcm/src/codec_ms.rs @@ -0,0 +1,139 @@ +// Symphonia +// Copyright (c) 2019-2022 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 symphonia_core::errors::{unsupported_error, Result}; +use symphonia_core::io::ReadBytes; +use symphonia_core::util::clamp::clamp_i16; + +use crate::common::{from_i16_shift, u16_to_i32, Nibble}; + +#[rustfmt::skip] +const MS_ADAPTATION_TABLE: [i32; 16] = [ + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230, +]; + +const MS_ADAPT_COEFFS1: [i32; 7] = [256, 512, 0, 192, 240, 460, 392]; +const MS_ADAPT_COEFFS2: [i32; 7] = [0, -256, 0, 64, 0, -208, -232]; + +const DELTA_MIN: i32 = 16; + +macro_rules! check_block_predictor { + ($block_predictor:ident, $max:expr) => { + if $block_predictor > $max { + return unsupported_error("adpcm: block predictor exceeds range."); + } + }; +} + +pub fn signed_nibble(nibble: u8) -> i8 { + if (nibble & 0x08) != 0 { + nibble as i8 - 0x10 + } + else { + nibble as i8 + } +} + +/// `AdpcmMsBlockStatus` contains values to decode a block +struct AdpcmMsBlockStatus { + coeff1: i32, + coeff2: i32, + delta: i32, + sample1: i32, + sample2: i32, +} + +impl AdpcmMsBlockStatus { + fn read_mono_preamble(stream: &mut B) -> Result { + let block_predictor = stream.read_byte()? as usize; + check_block_predictor!(block_predictor, 6); + let status = Self { + coeff1: MS_ADAPT_COEFFS1[block_predictor], + coeff2: MS_ADAPT_COEFFS2[block_predictor], + delta: u16_to_i32!(stream.read_u16()?), + sample1: u16_to_i32!(stream.read_u16()?), + sample2: u16_to_i32!(stream.read_u16()?), + }; + Ok(status) + } + + fn read_stereo_preamble(stream: &mut B) -> Result<(Self, Self)> { + let left_block_predictor = stream.read_byte()? as usize; + check_block_predictor!(left_block_predictor, 6); + let right_block_predictor = stream.read_byte()? as usize; + check_block_predictor!(right_block_predictor, 6); + let left_delta = u16_to_i32!(stream.read_u16()?); + let right_delta = u16_to_i32!(stream.read_u16()?); + let left_sample1 = u16_to_i32!(stream.read_u16()?); + let right_sample1 = u16_to_i32!(stream.read_u16()?); + let left_sample2 = u16_to_i32!(stream.read_u16()?); + let right_sample2 = u16_to_i32!(stream.read_u16()?); + Ok(( + Self { + coeff1: MS_ADAPT_COEFFS1[left_block_predictor], + coeff2: MS_ADAPT_COEFFS2[left_block_predictor], + delta: left_delta, + sample1: left_sample1, + sample2: left_sample2, + }, + Self { + coeff1: MS_ADAPT_COEFFS1[right_block_predictor], + coeff2: MS_ADAPT_COEFFS2[right_block_predictor], + delta: right_delta, + sample1: right_sample1, + sample2: right_sample2, + }, + )) + } + + fn expand_nibble(&mut self, byte: u8, nibble: Nibble) -> i32 { + let nibble = nibble.get_nibble(byte); + let signed_nibble = signed_nibble(nibble) as i32; + let predictor = ((self.sample1 * self.coeff1) + (self.sample2 * self.coeff2)) / 256 + + signed_nibble * self.delta; + self.sample2 = self.sample1; + self.sample1 = clamp_i16(predictor) as i32; + self.delta = (MS_ADAPTATION_TABLE[nibble as usize] * self.delta) / 256; + self.delta = self.delta.max(DELTA_MIN); + from_i16_shift!(self.sample1) + } +} + +pub(crate) fn decode_mono( + stream: &mut B, + buffer: &mut [i32], + frames_per_block: usize, +) -> Result<()> { + let mut status = AdpcmMsBlockStatus::read_mono_preamble(stream)?; + buffer[0] = from_i16_shift!(status.sample2); + buffer[1] = from_i16_shift!(status.sample1); + for byte in 1..(frames_per_block / 2) { + let nibbles = stream.read_u8()?; + buffer[byte * 2] = status.expand_nibble(nibbles, Nibble::Upper); + buffer[byte * 2 + 1] = status.expand_nibble(nibbles, Nibble::Lower); + } + Ok(()) +} + +pub(crate) fn decode_stereo( + stream: &mut B, + buffers: [&mut [i32]; 2], + frames_per_block: usize, +) -> Result<()> { + let (mut left_status, mut right_status) = AdpcmMsBlockStatus::read_stereo_preamble(stream)?; + buffers[0][0] = from_i16_shift!(left_status.sample2); + buffers[0][1] = from_i16_shift!(left_status.sample1); + buffers[1][0] = from_i16_shift!(right_status.sample2); + buffers[1][1] = from_i16_shift!(right_status.sample1); + for frame in 2..frames_per_block { + let nibbles = stream.read_u8()?; + buffers[0][frame] = left_status.expand_nibble(nibbles, Nibble::Upper); + buffers[1][frame] = right_status.expand_nibble(nibbles, Nibble::Lower); + } + Ok(()) +} diff --git a/symphonia-codec-adpcm/src/common.rs b/symphonia-codec-adpcm/src/common.rs new file mode 100644 index 00000000..d81a563a --- /dev/null +++ b/symphonia-codec-adpcm/src/common.rs @@ -0,0 +1,36 @@ +// Symphonia +// Copyright (c) 2019-2022 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/. + +/// `Nibble` represents the lower or upper 4 bits of a byte +pub(crate) enum Nibble { + Upper, + Lower, +} + +impl Nibble { + pub fn get_nibble(&self, byte: u8) -> u8 { + match self { + Nibble::Upper => byte >> 4, + Nibble::Lower => byte & 0x0F, + } + } +} + +macro_rules! u16_to_i32 { + ($input:expr) => { + $input as i16 as i32 + }; +} + +macro_rules! from_i16_shift { + ($input:expr) => { + ($input as i32) << 16 + }; +} + +pub(crate) use from_i16_shift; +pub(crate) use u16_to_i32; diff --git a/symphonia-codec-adpcm/src/lib.rs b/symphonia-codec-adpcm/src/lib.rs new file mode 100644 index 00000000..4d69fe3e --- /dev/null +++ b/symphonia-codec-adpcm/src/lib.rs @@ -0,0 +1,182 @@ +// Symphonia +// Copyright (c) 2019-2022 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)] + +use symphonia_core::support_codec; + +use symphonia_core::audio::{AsAudioBufferRef, AudioBuffer, AudioBufferRef, Signal, SignalSpec}; +use symphonia_core::codecs::{CodecDescriptor, CodecParameters, CodecType}; +use symphonia_core::codecs::{Decoder, DecoderOptions, FinalizeResult}; +use symphonia_core::codecs::{CODEC_TYPE_ADPCM_IMA_WAV, CODEC_TYPE_ADPCM_MS}; +use symphonia_core::errors::{unsupported_error, Result}; +use symphonia_core::formats::Packet; +use symphonia_core::io::ReadBytes; + +mod codec_ima; +mod codec_ms; +mod common; + +fn is_supported_adpcm_codec(codec_type: CodecType) -> bool { + matches!(codec_type, CODEC_TYPE_ADPCM_MS | CODEC_TYPE_ADPCM_IMA_WAV) +} + +enum InnerDecoder { + AdpcmMs, + AdpcmIma, +} + +impl InnerDecoder { + fn decode_mono_fn(&self) -> impl Fn(&mut B, &mut [i32], usize) -> Result<()> { + match *self { + InnerDecoder::AdpcmMs => codec_ms::decode_mono, + InnerDecoder::AdpcmIma => codec_ima::decode_mono, + } + } + + fn decode_stereo_fn( + &self, + ) -> impl Fn(&mut B, [&mut [i32]; 2], usize) -> Result<()> { + match *self { + InnerDecoder::AdpcmMs => codec_ms::decode_stereo, + InnerDecoder::AdpcmIma => codec_ima::decode_stereo, + } + } +} + +/// Adaptive Differential Pulse Code Modulation (ADPCM) decoder. +pub struct AdpcmDecoder { + params: CodecParameters, + inner_decoder: InnerDecoder, + buf: AudioBuffer, +} + +impl AdpcmDecoder { + fn decode_inner(&mut self, packet: &Packet) -> Result<()> { + let mut stream = packet.as_buf_reader(); + + let frames_per_block = self.params.frames_per_block.unwrap() as usize; + + let block_count = packet.block_dur() as usize / frames_per_block; + + self.buf.clear(); + self.buf.render_reserved(Some(block_count * frames_per_block)); + + let channel_count = self.buf.spec().channels.count(); + match channel_count { + 1 => { + let buffer = self.buf.chan_mut(0); + let decode_mono = self.inner_decoder.decode_mono_fn(); + for block_id in 0..block_count { + let offset = frames_per_block * block_id; + let buffer_range = offset..(offset + frames_per_block); + let buffer = &mut buffer[buffer_range]; + decode_mono(&mut stream, buffer, frames_per_block)?; + } + } + 2 => { + let buffers = self.buf.chan_pair_mut(0, 1); + let decode_stereo = self.inner_decoder.decode_stereo_fn(); + for block_id in 0..block_count { + let offset = frames_per_block * block_id; + let buffer_range = offset..(offset + frames_per_block); + let buffers = + [&mut buffers.0[buffer_range.clone()], &mut buffers.1[buffer_range]]; + decode_stereo(&mut stream, buffers, frames_per_block)?; + } + } + _ => unreachable!(), + } + + Ok(()) + } +} + +impl Decoder for AdpcmDecoder { + fn try_new(params: &CodecParameters, _options: &DecoderOptions) -> Result { + // This decoder only supports certain ADPCM codecs. + if !is_supported_adpcm_codec(params.codec) { + return unsupported_error("adpcm: invalid codec type"); + } + + let frames = match params.max_frames_per_packet { + Some(frames) => frames, + _ => return unsupported_error("adpcm: maximum frames per packet is required"), + }; + + if params.frames_per_block.is_none() || params.frames_per_block.unwrap() == 0 { + return unsupported_error("adpcm: valid frames per block is required"); + } + + let rate = match params.sample_rate { + Some(rate) => rate, + _ => return unsupported_error("adpcm: sample rate is required"), + }; + + let spec = if let Some(channels) = params.channels { + SignalSpec::new(rate, channels) + } + else if let Some(layout) = params.channel_layout { + SignalSpec::new_with_layout(rate, layout) + } + else { + return unsupported_error("adpcm: channels or channel_layout is required"); + }; + + let inner_decoder = match params.codec { + CODEC_TYPE_ADPCM_MS => InnerDecoder::AdpcmMs, + CODEC_TYPE_ADPCM_IMA_WAV => InnerDecoder::AdpcmIma, + _ => return unsupported_error("adpcm: codec is unsupported"), + }; + + Ok(AdpcmDecoder { + params: params.clone(), + inner_decoder, + buf: AudioBuffer::new(frames, spec), + }) + } + + fn supported_codecs() -> &'static [CodecDescriptor] { + &[ + support_codec!(CODEC_TYPE_ADPCM_MS, "adpcm_ms", "Microsoft ADPCM"), + support_codec!(CODEC_TYPE_ADPCM_IMA_WAV, "adpcm_ima_wav", "ADPCM IMA WAV"), + ] + } + + fn reset(&mut self) { + // No state is stored between packets, therefore do nothing. + } + + fn codec_params(&self) -> &CodecParameters { + &self.params + } + + fn decode(&mut self, packet: &Packet) -> Result> { + if let Err(e) = self.decode_inner(packet) { + self.buf.clear(); + Err(e) + } + else { + Ok(self.buf.as_audio_buffer_ref()) + } + } + + fn finalize(&mut self) -> FinalizeResult { + Default::default() + } + + fn last_decoded(&self) -> AudioBufferRef<'_> { + self.buf.as_audio_buffer_ref() + } +} diff --git a/symphonia-core/src/codecs.rs b/symphonia-core/src/codecs.rs index 195d0705..31feb1de 100644 --- a/symphonia-core/src/codecs.rs +++ b/symphonia-core/src/codecs.rs @@ -300,6 +300,9 @@ pub struct CodecParameters { /// A method and expected value that may be used to perform verification on the decoded audio. pub verification_check: Option, + /// The number of frames per block, in case packets are seperated in multiple blocks. + pub frames_per_block: Option, + /// Extra data (defined by the codec). pub extra_data: Option>, } @@ -322,6 +325,7 @@ impl CodecParameters { max_frames_per_packet: None, packet_data_integrity: false, verification_check: None, + frames_per_block: None, extra_data: None, } } @@ -410,6 +414,12 @@ impl CodecParameters { self } + /// Provide the maximum number of frames per packet. + pub fn with_frames_per_block(&mut self, len: u64) -> &mut Self { + self.frames_per_block = Some(len); + self + } + /// Provide codec extra data. pub fn with_extra_data(&mut self, data: Box<[u8]>) -> &mut Self { self.extra_data = Some(data); diff --git a/symphonia-format-wav/src/chunks.rs b/symphonia-format-wav/src/chunks.rs index 034c3879..2952f361 100644 --- a/symphonia-format-wav/src/chunks.rs +++ b/symphonia-format-wav/src/chunks.rs @@ -11,8 +11,9 @@ use std::marker::PhantomData; use symphonia_core::audio::Channels; use symphonia_core::codecs::CodecType; use symphonia_core::codecs::{ - CODEC_TYPE_PCM_ALAW, CODEC_TYPE_PCM_F32LE, CODEC_TYPE_PCM_F64LE, CODEC_TYPE_PCM_MULAW, - CODEC_TYPE_PCM_S16LE, CODEC_TYPE_PCM_S24LE, CODEC_TYPE_PCM_S32LE, CODEC_TYPE_PCM_U8, + CODEC_TYPE_ADPCM_IMA_WAV, CODEC_TYPE_ADPCM_MS, CODEC_TYPE_PCM_ALAW, CODEC_TYPE_PCM_F32LE, + CODEC_TYPE_PCM_F64LE, CODEC_TYPE_PCM_MULAW, CODEC_TYPE_PCM_S16LE, CODEC_TYPE_PCM_S24LE, + CODEC_TYPE_PCM_S32LE, CODEC_TYPE_PCM_U8, }; use symphonia_core::errors::{decode_error, unsupported_error, Result}; use symphonia_core::io::ReadBytes; @@ -21,6 +22,8 @@ use symphonia_metadata::riff; use log::info; +use crate::PacketInfo; + /// `ParseChunkTag` implements `parse_tag` to map between the 4-byte chunk identifier and the /// enumeration pub trait ParseChunkTag: Sized { @@ -142,6 +145,7 @@ impl ChunkParser

{ pub enum WaveFormatData { Pcm(WaveFormatPcm), + Adpcm(WaveFormatAdpcm), IeeeFloat(WaveFormatIeeeFloat), Extensible(WaveFormatExtensible), ALaw(WaveFormatALaw), @@ -157,6 +161,15 @@ pub struct WaveFormatPcm { pub codec: CodecType, } +pub struct WaveFormatAdpcm { + /// The number of bits per sample. At the moment only 4bit is supported. + pub bits_per_sample: u16, + /// Channel bitmask. + pub channels: Channels, + /// Codec type. + pub codec: CodecType, +} + pub struct WaveFormatIeeeFloat { /// Channel bitmask. pub channels: Channels, @@ -267,6 +280,45 @@ impl WaveFormatChunk { Ok(WaveFormatData::Pcm(WaveFormatPcm { bits_per_sample, channels, codec })) } + fn read_adpcm_fmt( + reader: &mut B, + bits_per_sample: u16, + n_channels: u16, + len: u32, + codec: CodecType, + ) -> Result { + if bits_per_sample != 4 { + return decode_error("wav: bits per sample for fmt_adpcm must be 4 bits"); + } + + // WaveFormatEx with extension data length field present and with atleast frames per block data. + if len < 20 { + return decode_error("wav: malformed fmt_adpcm chunk"); + } + + let extra_size = reader.read_u16()? as u64; + + match codec { + CODEC_TYPE_ADPCM_MS if extra_size < 32 => { + return decode_error("wav: malformed fmt_adpcm chunk"); + } + CODEC_TYPE_ADPCM_IMA_WAV if extra_size != 2 => { + return decode_error("wav: malformed fmt_adpcm chunk"); + } + _ => (), + } + reader.ignore_bytes(extra_size)?; + + // The ADPCM format only supports 1 or 2 channels, for mono and stereo channel layouts, + // respectively. + let channels = match n_channels { + 1 => Channels::FRONT_LEFT, + 2 => Channels::FRONT_LEFT | Channels::FRONT_RIGHT, + _ => return decode_error("wav: channel layout is not stereo or mono for fmt_adpcm"), + }; + Ok(WaveFormatData::Adpcm(WaveFormatAdpcm { bits_per_sample, channels, codec })) + } + fn read_ieee_fmt( reader: &mut B, bits_per_sample: u16, @@ -486,6 +538,29 @@ impl WaveFormatChunk { Ok(WaveFormatData::MuLaw(WaveFormatMuLaw { codec: CODEC_TYPE_PCM_MULAW, channels })) } + + pub(crate) fn packet_info(&self) -> Result { + match self.format_data { + WaveFormatData::Adpcm(WaveFormatAdpcm { codec, bits_per_sample, .. }) + //| WaveFormatData::Extensible(WaveFormatExtensible { codec, bits_per_sample, .. }) + if codec == CODEC_TYPE_ADPCM_MS => + { + let frames_per_block = ((((self.block_align - (7 * self.n_channels)) * 8) + / (bits_per_sample * self.n_channels)) + + 2) as u64; + PacketInfo::with_blocks(self.block_align, frames_per_block) + } + WaveFormatData::Adpcm(WaveFormatAdpcm { codec, bits_per_sample, .. }) + if codec == CODEC_TYPE_ADPCM_IMA_WAV => + { + let frames_per_block = (((self.block_align - (4 * self.n_channels)) * 8) + / (bits_per_sample * self.n_channels) + + 1) as u64; + PacketInfo::with_blocks(self.block_align, frames_per_block) + } + _ => Ok(PacketInfo::without_blocks(self.block_align)), + } + } } impl ParseChunk for WaveFormatChunk { @@ -506,15 +581,20 @@ impl ParseChunk for WaveFormatChunk { // The definition of these format identifiers can be found in mmreg.h of the Microsoft // Windows Platform SDK. const WAVE_FORMAT_PCM: u16 = 0x0001; - // const WAVE_FORMAT_ADPCM: u16 = 0x0002; + const WAVE_FORMAT_ADPCM: u16 = 0x0002; const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003; const WAVE_FORMAT_ALAW: u16 = 0x0006; const WAVE_FORMAT_MULAW: u16 = 0x0007; + const WAVE_FORMAT_ADPCM_IMA: u16 = 0x0011; const WAVE_FORMAT_EXTENSIBLE: u16 = 0xfffe; let format_data = match format { // The PCM Wave Format WAVE_FORMAT_PCM => Self::read_pcm_fmt(reader, bits_per_sample, n_channels, len), + // The Microsoft ADPCM Format + WAVE_FORMAT_ADPCM => { + Self::read_adpcm_fmt(reader, bits_per_sample, n_channels, len, CODEC_TYPE_ADPCM_MS) + } // The IEEE Float Wave Format WAVE_FORMAT_IEEE_FLOAT => Self::read_ieee_fmt(reader, bits_per_sample, n_channels, len), // The Extensible Wave Format @@ -523,6 +603,14 @@ impl ParseChunk for WaveFormatChunk { WAVE_FORMAT_ALAW => Self::read_alaw_pcm_fmt(reader, n_channels, len), // The MuLaw Wave Format. WAVE_FORMAT_MULAW => Self::read_mulaw_pcm_fmt(reader, n_channels, len), + // The IMA ADPCM Format + WAVE_FORMAT_ADPCM_IMA => Self::read_adpcm_fmt( + reader, + bits_per_sample, + n_channels, + len, + CODEC_TYPE_ADPCM_IMA_WAV, + ), // Unsupported format. _ => return unsupported_error("wav: unsupported wave format"), }?; @@ -546,6 +634,12 @@ impl fmt::Display for WaveFormatChunk { writeln!(f, "\t\tchannels: {},", pcm.channels)?; writeln!(f, "\t\tcodec: {},", pcm.codec)?; } + WaveFormatData::Adpcm(ref adpcm) => { + writeln!(f, "\tformat_data: Adpcm {{")?; + writeln!(f, "\t\tbits_per_sample: {},", adpcm.bits_per_sample)?; + writeln!(f, "\t\tchannels: {},", adpcm.channels)?; + writeln!(f, "\t\tcodec: {},", adpcm.codec)?; + } WaveFormatData::IeeeFloat(ref ieee) => { writeln!(f, "\tformat_data: IeeeFloat {{")?; writeln!(f, "\t\tchannels: {},", ieee.channels)?; diff --git a/symphonia-format-wav/src/lib.rs b/symphonia-format-wav/src/lib.rs index aa1adffa..281f9567 100644 --- a/symphonia-format-wav/src/lib.rs +++ b/symphonia-format-wav/src/lib.rs @@ -17,7 +17,7 @@ use std::io::{Seek, SeekFrom}; use symphonia_core::codecs::CodecParameters; -use symphonia_core::errors::{end_of_stream_error, seek_error, unsupported_error}; +use symphonia_core::errors::{decode_error, end_of_stream_error, seek_error, unsupported_error}; use symphonia_core::errors::{Result, SeekErrorKind}; use symphonia_core::formats::prelude::*; use symphonia_core::io::*; @@ -40,6 +40,53 @@ const WAVE_RIFF_FORM: [u8; 4] = *b"WAVE"; /// The maximum number of frames that will be in a packet. const WAVE_MAX_FRAMES_PER_PACKET: u64 = 1152; +/// `PacketInfo` helps to simulate packetization over a number of blocks of data. +/// In case the codec is blockless the block size equals one full audio frame in bytes. +pub(crate) struct PacketInfo { + block_size: u64, + frames_per_block: u64, + max_blocks_per_packet: u64, +} + +impl PacketInfo { + fn with_blocks(block_size: u16, frames_per_block: u64) -> Result { + if frames_per_block == 0 { + return decode_error("wav: frames per block is 0"); + } + Ok(Self { + block_size: u64::from(block_size), + frames_per_block, + max_blocks_per_packet: frames_per_block.max(WAVE_MAX_FRAMES_PER_PACKET) + / frames_per_block, + }) + } + + fn without_blocks(frame_len: u16) -> Self { + Self { + block_size: u64::from(frame_len), + frames_per_block: 1, + max_blocks_per_packet: WAVE_MAX_FRAMES_PER_PACKET, + } + } + + fn is_empty(&self) -> bool { + self.block_size == 0 + } + + fn get_max_frames_per_packet(&self) -> u64 { + self.max_blocks_per_packet * self.frames_per_block + } + + fn get_frames(&self, data_len: u64) -> u64 { + data_len / self.block_size * self.frames_per_block + } + + fn get_actual_ts(&self, ts: u64) -> u64 { + let max_frames_per_packet = self.get_max_frames_per_packet(); + ts / max_frames_per_packet * max_frames_per_packet + } +} + /// WAVE (WAV) format reader. /// /// `WavReader` implements a demuxer for the WAVE container format. @@ -48,7 +95,7 @@ pub struct WavReader { tracks: Vec, cues: Vec, metadata: MetadataLog, - frame_len: u16, + packet_info: PacketInfo, data_start_pos: u64, data_end_pos: u64, } @@ -98,7 +145,7 @@ impl FormatReader for WavReader { let mut codec_params = CodecParameters::new(); let mut metadata: MetadataLog = Default::default(); - let mut frame_len = 0; + let mut packet_info = PacketInfo::without_blocks(0); loop { let chunk = riff_chunks.next(&mut source)?; @@ -113,13 +160,15 @@ impl FormatReader for WavReader { RiffWaveChunks::Format(fmt) => { let format = fmt.parse(&mut source)?; - // The Format chunk contains the block_align field which indicates the size - // of one full audio frame in bytes, atleast for the codecs supported by - // WavReader. This value is stored to support seeking. - frame_len = format.block_align; + // The Format chunk contains the block_align field and possible additional information + // to handle packetization and seeking. + packet_info = format.packet_info()?; + codec_params + .with_max_frames_per_packet(packet_info.get_max_frames_per_packet()) + .with_frames_per_block(packet_info.frames_per_block); // Append Format chunk fields to codec parameters. - append_format_params(&mut codec_params, &format); + append_format_params(&mut codec_params, format); } RiffWaveChunks::Fact(fct) => { let fact = fct.parse(&mut source)?; @@ -145,7 +194,7 @@ impl FormatReader for WavReader { let data_end_pos = data_start_pos + u64::from(data.len); // Append Data chunk fields to codec parameters. - append_data_params(&mut codec_params, &data, frame_len); + append_data_params(&mut codec_params, &data, &packet_info); // Add a new track using the collected codec parameters. return Ok(WavReader { @@ -153,7 +202,7 @@ impl FormatReader for WavReader { tracks: vec![Track::new(0, codec_params)], cues: Vec::new(), metadata, - frame_len, + packet_info, data_start_pos, data_end_pos, }); @@ -166,29 +215,36 @@ impl FormatReader for WavReader { fn next_packet(&mut self) -> Result { let pos = self.reader.pos(); + if self.tracks.is_empty() { + return decode_error("wav: no tracks"); + } + if self.packet_info.is_empty() { + return decode_error("wav: block size is 0"); + } - // Determine the number of complete frames remaining in the data chunk. - let num_frames_left = if pos < self.data_end_pos { - (self.data_end_pos - pos) / u64::from(self.frame_len) + // Determine the number of complete blocks remaining in the data chunk. + let num_blocks_left = if pos < self.data_end_pos { + (self.data_end_pos - pos) / self.packet_info.block_size } else { 0 }; - if num_frames_left == 0 { + if num_blocks_left == 0 { return end_of_stream_error(); } - // Limit the duration of a packet to WAVE_MAX_FRAMES_PER_PACKET frames. - let dur = num_frames_left.min(WAVE_MAX_FRAMES_PER_PACKET); + let blocks_per_packet = num_blocks_left.min(self.packet_info.max_blocks_per_packet); + + let dur = blocks_per_packet * self.packet_info.frames_per_block; + let packet_len = blocks_per_packet * self.packet_info.block_size; // Copy the frames. - let packet_len = dur * u64::from(self.frame_len); let packet_buf = self.reader.read_boxed_slice(packet_len as usize)?; // The packet timestamp is the position of the first byte of the first frame in the // packet relative to the start of the data chunk divided by the length per frame. - let pts = (pos - self.data_start_pos) / u64::from(self.frame_len); + let pts = self.packet_info.get_frames(pos - self.data_start_pos); Ok(Packet::new_from_boxed_slice(0, pts, dur, packet_buf)) } @@ -206,7 +262,7 @@ impl FormatReader for WavReader { } fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> Result { - if self.tracks.is_empty() || self.frame_len == 0 { + if self.tracks.is_empty() || self.packet_info.is_empty() { return seek_error(SeekErrorKind::Unseekable); } @@ -239,15 +295,15 @@ impl FormatReader for WavReader { debug!("seeking to frame_ts={}", ts); // WAVE is not internally packetized for PCM codecs. Packetization is simulated by trying to - // read a constant number of samples every call to next_packet. Therefore, a packet begins + // read a constant number of samples or blocks every call to next_packet. Therefore, a packet begins // wherever the data stream is currently positioned. Since timestamps on packets should be // determinstic, instead of seeking to the exact timestamp requested and starting the next // packet there, seek to a packet boundary. In this way, packets will have have the same // timestamps regardless if the stream was seeked or not. - let actual_ts = (ts / WAVE_MAX_FRAMES_PER_PACKET) * WAVE_MAX_FRAMES_PER_PACKET; + let actual_ts = self.packet_info.get_actual_ts(ts); // Calculate the absolute byte offset of the desired audio frame. - let seek_pos = self.data_start_pos + (actual_ts * u64::from(self.frame_len)); + let seek_pos = self.data_start_pos + (actual_ts * self.packet_info.block_size); // If the reader supports seeking we can seek directly to the frame's offset wherever it may // be. @@ -298,34 +354,36 @@ fn read_info_chunk(source: &mut MediaSourceStream, len: u32) -> Result { + WaveFormatData::Pcm(pcm) => { codec_params .for_codec(pcm.codec) .with_bits_per_coded_sample(u32::from(pcm.bits_per_sample)) .with_bits_per_sample(u32::from(pcm.bits_per_sample)) .with_channels(pcm.channels); } - WaveFormatData::IeeeFloat(ref ieee) => { + WaveFormatData::Adpcm(adpcm) => { + codec_params.for_codec(adpcm.codec).with_channels(adpcm.channels); + } + WaveFormatData::IeeeFloat(ieee) => { codec_params.for_codec(ieee.codec).with_channels(ieee.channels); } - WaveFormatData::Extensible(ref ext) => { + WaveFormatData::Extensible(ext) => { codec_params .for_codec(ext.codec) .with_bits_per_coded_sample(u32::from(ext.bits_per_coded_sample)) .with_bits_per_sample(u32::from(ext.bits_per_sample)) .with_channels(ext.channels); } - WaveFormatData::ALaw(ref alaw) => { + WaveFormatData::ALaw(alaw) => { codec_params.for_codec(alaw.codec).with_channels(alaw.channels); } - WaveFormatData::MuLaw(ref mulaw) => { + WaveFormatData::MuLaw(mulaw) => { codec_params.for_codec(mulaw.codec).with_channels(mulaw.channels); } } @@ -335,9 +393,13 @@ fn append_fact_params(codec_params: &mut CodecParameters, fact: &FactChunk) { codec_params.with_n_frames(u64::from(fact.n_frames)); } -fn append_data_params(codec_params: &mut CodecParameters, data: &DataChunk, frame_len: u16) { - if frame_len > 0 { - let n_frames = data.len / u32::from(frame_len); - codec_params.with_n_frames(u64::from(n_frames)); +fn append_data_params( + codec_params: &mut CodecParameters, + data: &DataChunk, + packet_info: &PacketInfo, +) { + if !packet_info.is_empty() { + let n_frames = packet_info.get_frames(u64::from(data.len)); + codec_params.with_n_frames(n_frames); } } diff --git a/symphonia/Cargo.toml b/symphonia/Cargo.toml index 463b438a..e98579fe 100644 --- a/symphonia/Cargo.toml +++ b/symphonia/Cargo.toml @@ -13,8 +13,9 @@ edition = "2018" rust-version = "1.53" [features] -default = ["flac", "mkv", "ogg", "pcm", "vorbis", "wav"] +default = ["adpcm", "flac", "mkv", "ogg", "pcm", "vorbis", "wav"] aac = ["symphonia-codec-aac"] +adpcm = ["symphonia-codec-adpcm"] alac = ["symphonia-codec-alac"] flac = ["symphonia-bundle-flac"] isomp4 = ["symphonia-format-isomp4"] @@ -32,6 +33,7 @@ symphonia-metadata = { version = "0.5", path = "../symphonia-metadata" } symphonia-bundle-flac = { version = "0.5", path = "../symphonia-bundle-flac", optional = true } symphonia-bundle-mp3 = { version = "0.5", path = "../symphonia-bundle-mp3", optional = true } symphonia-codec-aac = { version = "0.5", path = "../symphonia-codec-aac", optional = true } +symphonia-codec-adpcm = { version = "0.5", path = "../symphonia-codec-adpcm", optional = true } symphonia-codec-alac = { version = "0.5", path = "../symphonia-codec-alac", optional = true } symphonia-codec-pcm = { version = "0.5", path = "../symphonia-codec-pcm", optional = true } symphonia-codec-vorbis = { version = "0.5", path = "../symphonia-codec-vorbis", optional = true } diff --git a/symphonia/src/lib.rs b/symphonia/src/lib.rs index c0e20459..48aa9a36 100644 --- a/symphonia/src/lib.rs +++ b/symphonia/src/lib.rs @@ -122,6 +122,8 @@ pub mod default { pub use symphonia_bundle_mp3::Mp3Decoder; #[cfg(feature = "aac")] pub use symphonia_codec_aac::AacDecoder; + #[cfg(feature = "adpcm")] + pub use symphonia_codec_adpcm::AdpcmDecoder; #[cfg(feature = "alac")] pub use symphonia_codec_alac::AlacDecoder; #[cfg(feature = "pcm")] @@ -199,6 +201,9 @@ pub mod default { #[cfg(feature = "aac")] registry.register_all::(); + #[cfg(feature = "adpcm")] + registry.register_all::(); + #[cfg(feature = "alac")] registry.register_all::();