diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb66e2d..3b9d50d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Unreleased +- Add packed 24-bit sample types `I24LE3`, `I24BE3`, `U24LE3` and `U24BE3` to + `dasp_sample::types`, corresponding to the `S24_3LE`, `S24_3BE`, `U24_3LE` + and `U24_3BE` formats. Unlike `I24`/`U24`, which store their value in a + four-byte container, these store it in exactly three bytes with an alignment + of one, so a buffer of packed 24-bit PCM can be reinterpreted as a slice of + samples without a copy. They implement `Sample`, `Frame` and the full set of + `FromSample`/`ToSample` conversions. `Sample::Signed` and `Sample::Float` are + `I24LE3`/`i32` and `f32`, mirroring the padded types, so `I24LE3` and `I24BE3` + are `SignedSample` as every other signed sample type is, while `U24LE3` and + `U24BE3` are not, as no unsigned type is. They carry the same arithmetic + operators (`Add`, `Sub`, `Mul`, `Div`, `Rem`, and `Neg` on the signed pair) + with the same debug-panic/release-wrap contract, the same `From` conversions, + and `I48`/`U48` accept them as sources. The bitwise and shift operators are + not mirrored, being bit manipulation whose meaning does not carry from a + four-byte container to a three-byte one. + + Having no spare byte to store one, they wrap out-of-range values modulo 2^24 + where `I24`/`U24` keep them. Only conversions from `f32`/`f64` outside the + documented `-1.0 <= v < 1.0` range can reach this: every integer conversion + into 24 bits is a shift that fits by construction. This is the same wrapping + `impl From for I24` already does, and no attempt is made here to settle + the wider question of overflow behaviour raised in #39 and #73. +- Bump `dasp_sample` and `dasp_frame` to 0.11.1, and raise `dasp_frame`'s + requirement on `dasp_sample` to `0.11.1`, as it now references the packed + types by name and would not build against 0.11.0. - Renamed `window-hanning` to `window-hann` - Made `IntoInterleavedSamples` and `IntoInterleavedSamplesIterator` stop yielding samples when the underlying signal gets exhausted. This is a breaking diff --git a/README.md b/README.md index b486dced..f667ef2a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ Use the **Sample** trait to convert between and remain generic over any bit-depth in an optimal, performance-sensitive manner. Implementations are provided for all signed integer, unsigned integer and floating point primitive types along with some custom types including 11, 20, 24 and 48-bit signed and -unsigned unpacked integers. For example: +unsigned unpacked integers, and 24-bit signed and unsigned integers packed into +exactly three bytes in either byte order. For example: ```rust assert_eq!((-1.0).to_sample::(), 0); diff --git a/dasp_frame/Cargo.toml b/dasp_frame/Cargo.toml index ce3ff5a7..0b2a639b 100644 --- a/dasp_frame/Cargo.toml +++ b/dasp_frame/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dasp_frame" description = "An abstraction for audio PCM DSP frames, along with useful conversions and operations." -version = "0.11.0" +version = "0.11.1" authors = ["mitchmindtree "] readme = "../README.md" keywords = ["dsp", "frame", "channel", "pcm", "audio"] @@ -11,7 +11,7 @@ homepage = "https://github.com/rustaudio/dasp" edition = "2018" [dependencies] -dasp_sample = { version = "0.11", path = "../dasp_sample", default-features = false } +dasp_sample = { version = "0.11.1", path = "../dasp_sample", default-features = false } [features] default = ["std"] diff --git a/dasp_frame/src/lib.rs b/dasp_frame/src/lib.rs index 104a8501..4952e194 100644 --- a/dasp_frame/src/lib.rs +++ b/dasp_frame/src/lib.rs @@ -569,6 +569,12 @@ impl_frame_for_sample! { dasp_sample::types::U24 dasp_sample::types::U48 } +impl_frame_for_sample! { + dasp_sample::types::I24LE3 + dasp_sample::types::I24BE3 + dasp_sample::types::U24LE3 + dasp_sample::types::U24BE3 +} impl Iterator for Channels where diff --git a/dasp_graph/Cargo.toml b/dasp_graph/Cargo.toml index 85e9cfc1..766e718f 100644 --- a/dasp_graph/Cargo.toml +++ b/dasp_graph/Cargo.toml @@ -21,10 +21,10 @@ node-signal = ["dasp_frame", "dasp_signal"] node-sum = ["dasp_slice"] [dependencies] -dasp_frame = { version = "0.11", default-features = false, features = ["std"], optional = true } -dasp_ring_buffer = { version = "0.11", default-features = false, features = ["std"], optional = true } -dasp_signal = { version = "0.11", default-features = false, features = ["std"], optional = true } -dasp_slice = { version = "0.11", default-features = false, features = ["std"], optional = true } +dasp_frame = { version = "0.11", path = "../dasp_frame", default-features = false, features = ["std"], optional = true } +dasp_ring_buffer = { version = "0.11", path = "../dasp_ring_buffer", default-features = false, features = ["std"], optional = true } +dasp_signal = { version = "0.11", path = "../dasp_signal", default-features = false, features = ["std"], optional = true } +dasp_slice = { version = "0.11", path = "../dasp_slice", default-features = false, features = ["std"], optional = true } petgraph = { version = "0.5", default-features = false } [dev-dependencies] diff --git a/dasp_sample/Cargo.toml b/dasp_sample/Cargo.toml index 1849abe7..aef0ad2d 100644 --- a/dasp_sample/Cargo.toml +++ b/dasp_sample/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dasp_sample" description = "An abstraction for audio PCM DSP samples, along with useful conversions and operations." -version = "0.11.0" +version = "0.11.1" authors = ["mitchmindtree "] readme = "../README.md" keywords = ["dsp", "bit-depth", "sample", "pcm", "audio"] diff --git a/dasp_sample/src/conv.rs b/dasp_sample/src/conv.rs index f1ed3f90..65b3cb59 100644 --- a/dasp_sample/src/conv.rs +++ b/dasp_sample/src/conv.rs @@ -1,19 +1,48 @@ -//! Pure functions and traits for converting between i8, i16, I24, i32, I48, i64, u8, u16, U24, -//! u32, U48, u64, f32 and f64. +//! Pure functions and traits for converting between i8, i16, I24, I24LE3, I24BE3, i32, I48, i64, +//! u8, u16, U24, U24LE3, U24BE3, u32, U48, u64, f32 and f64. //! //! Each conversion function is performance focused, memory-sensitive and expects that the user has //! validated their input prior to the function call. //! //! No conversion function will ever cast to a type with a size in bytes larger than the largest -//! between the source and target sample types. +//! between the source and target sample types, with one exception: the packed 24-bit types are +//! three bytes wide but convert via their four-byte padded equivalents, since packing is a +//! re-arrangement of the same 24 bits and there is no shorter path to any other width. //! //! The conversion functions do *not* check the range of incoming values for floating point values -//! or any of the custom `I24`, `U24`, `I48` and `U48` types. +//! or any of the custom `I24`, `U24`, `I48`, `U48`, `I24LE3`, `I24BE3`, `U24LE3` and `U24BE3` +//! types. //! //! Note that floating point conversions use the range -1.0 <= v < 1.0: //! `(1.0 as f64).to_sample::()` will overflow! +//! +//! # Out-of-range input and the packed types +//! +//! For every value within the documented input range, a conversion to a packed 24-bit type +//! (`I24LE3`, `I24BE3`, `U24LE3`, `U24BE3`) and one to its padded equivalent (`I24`, `U24`) +//! agree exactly. They differ only for input that is already out of range, and only from a +//! floating point source: +//! +//! - Every *integer* conversion into 24 bits is a shift that fits by construction — `i8 << 16` +//! and `i16 << 8` widen into range, `i32 >> 8` yields exactly `-8_388_608..=8_388_607`, and +//! `i64 >> 40` and `I48 >> 24` likewise — so no integer source can overflow. +//! - A *floating point* source outside `-1.0 <= v < 1.0` can. `I24` and `U24` absorb the +//! overshoot in the spare byte of their `i32` container and hold a numerically out-of-range +//! value; a packed type has no spare byte, so the value wraps modulo 2^24. `1.0f32` becomes +//! `I24(8_388_608)` but `I24LE3(-8_388_608)`. +//! +//! The padded types' tolerance here is a property of their storage rather than a deliberate +//! policy: see [#73], which reports the same overflow against the primitive types and notes in +//! passing that `I24` and `I48` happen not to be affected. Overflow behaviour across this crate +//! is a longstanding open question — see also [#39] on saturating arithmetic — and the packed +//! types deliberately do not attempt to settle it. They wrap, which is what +//! `impl From for I24` already does, and callers who need out-of-range input handled must +//! clamp before converting. +//! +//! [#39]: https://github.com/RustAudio/dasp/issues/39 +//! [#73]: https://github.com/RustAudio/dasp/issues/73 -use crate::types::{I24, I48, U24, U48}; +use crate::types::{I24, I24BE3, I24LE3, I48, U24, U24BE3, U24LE3, U48}; macro_rules! conversion_fn { ($Rep:ty, $s:ident to_i8 { $body:expr }) => { @@ -37,6 +66,20 @@ macro_rules! conversion_fn { } }; + ($Rep:ty, $s:ident to_i24_le3 { $body:expr }) => { + #[inline] + pub fn to_i24_le3($s: $Rep) -> I24LE3 { + $body + } + }; + + ($Rep:ty, $s:ident to_i24_be3 { $body:expr }) => { + #[inline] + pub fn to_i24_be3($s: $Rep) -> I24BE3 { + $body + } + }; + ($Rep:ty, $s:ident to_i32 { $body:expr }) => { #[inline] pub fn to_i32($s: $Rep) -> i32 { @@ -79,6 +122,20 @@ macro_rules! conversion_fn { } }; + ($Rep:ty, $s:ident to_u24_le3 { $body:expr }) => { + #[inline] + pub fn to_u24_le3($s: $Rep) -> U24LE3 { + $body + } + }; + + ($Rep:ty, $s:ident to_u24_be3 { $body:expr }) => { + #[inline] + pub fn to_u24_be3($s: $Rep) -> U24BE3 { + $body + } + }; + ($Rep:ty, $s:ident to_u32 { $body:expr }) => { #[inline] pub fn to_u32($s: $Rep) -> u32 { @@ -126,7 +183,7 @@ macro_rules! conversion_fns { macro_rules! conversions { ($T:ident, $mod_name:ident { $($rest:tt)* }) => { pub mod $mod_name { - use $crate::types::{I24, U24, I48, U48}; + use $crate::types::{I24, I24BE3, I24LE3, U24, U24BE3, U24LE3, I48, U48}; conversion_fns!($T, $($rest)*); } }; @@ -135,6 +192,10 @@ macro_rules! conversions { conversions!(i8, i8 { s to_i16 { (s as i16) << 8 } s to_i24 { I24::new_unchecked((s as i32) << 16) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s as i32) << 24 } s to_i48 { I48::new_unchecked((s as i64) << 40) } s to_i64 { (s as i64) << 56 } @@ -184,6 +245,10 @@ conversions!(i8, i8 { conversions!(i16, i16 { s to_i8 { (s >> 8) as i8 } s to_i24 { I24::new_unchecked((s as i32) << 8) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s as i32) << 16 } s to_i48 { I48::new_unchecked((s as i64) << 32) } s to_i64 { (s as i64) << 48 } @@ -237,6 +302,10 @@ conversions!(i16, i16 { conversions!(I24, i24 { s to_i8 { (s.inner() >> 16) as i8 } s to_i16 { (s.inner() >> 8) as i16 } + s to_i24_le3 { I24LE3::from(s) } + s to_i24_be3 { I24BE3::from(s) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { s.inner() << 8 } s to_i48 { I48::new_unchecked((s.inner() as i64) << 24) } s to_i64 { (s.inner() as i64) << 40 } @@ -266,10 +335,56 @@ conversions!(I24, i24 { } }); +// The packed 24-bit conversions all unpack to `I24` first. This is not a detour: packing is a +// pure re-arrangement of the same 24 bits, so there is no shorter path to any other width. +conversions!(I24LE3, i24_le3 { + s to_i8 { super::i24::to_i8(to_i24(s)) } + s to_i16 { super::i24::to_i16(to_i24(s)) } + s to_i24 { I24::from(s) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } + s to_i32 { super::i24::to_i32(to_i24(s)) } + s to_i48 { super::i24::to_i48(to_i24(s)) } + s to_i64 { super::i24::to_i64(to_i24(s)) } + s to_u8 { super::i24::to_u8(to_i24(s)) } + s to_u16 { super::i24::to_u16(to_i24(s)) } + s to_u24 { super::i24::to_u24(to_i24(s)) } + s to_u32 { super::i24::to_u32(to_i24(s)) } + s to_u48 { super::i24::to_u48(to_i24(s)) } + s to_u64 { super::i24::to_u64(to_i24(s)) } + s to_f32 { super::i24::to_f32(to_i24(s)) } + s to_f64 { super::i24::to_f64(to_i24(s)) } +}); + +conversions!(I24BE3, i24_be3 { + s to_i8 { super::i24::to_i8(to_i24(s)) } + s to_i16 { super::i24::to_i16(to_i24(s)) } + s to_i24 { I24::from(s) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } + s to_i32 { super::i24::to_i32(to_i24(s)) } + s to_i48 { super::i24::to_i48(to_i24(s)) } + s to_i64 { super::i24::to_i64(to_i24(s)) } + s to_u8 { super::i24::to_u8(to_i24(s)) } + s to_u16 { super::i24::to_u16(to_i24(s)) } + s to_u24 { super::i24::to_u24(to_i24(s)) } + s to_u32 { super::i24::to_u32(to_i24(s)) } + s to_u48 { super::i24::to_u48(to_i24(s)) } + s to_u64 { super::i24::to_u64(to_i24(s)) } + s to_f32 { super::i24::to_f32(to_i24(s)) } + s to_f64 { super::i24::to_f64(to_i24(s)) } +}); + conversions!(i32, i32 { s to_i8 { (s >> 24) as i8 } s to_i16 { (s >> 16) as i16 } s to_i24 { I24::new_unchecked(s >> 8) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i48 { I48::new_unchecked((s as i64) << 16) } s to_i64 { (s as i64) << 32 } s to_u8 { @@ -310,6 +425,10 @@ conversions!(I48, i48 { s to_i8 { (s.inner() >> 40) as i8 } s to_i16 { (s.inner() >> 32) as i16 } s to_i24 { I24::new_unchecked((s.inner() >> 24) as i32) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s.inner() >> 16) as i32 } s to_i64 { s.inner() << 16 } s to_u8 { @@ -342,6 +461,10 @@ conversions!(i64, i64 { s to_i8 { (s >> 56) as i8 } s to_i16 { (s >> 48) as i16 } s to_i24 { I24::new_unchecked((s >> 40) as i32) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s >> 32) as i32 } s to_i48 { I48::new_unchecked(s >> 16) } s to_u8 { @@ -388,6 +511,10 @@ conversions!(u8, u8 { s to_i24 { I24::new_unchecked((s as i32 - 128) << 16) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s as i32 - 128) << 24 } @@ -418,6 +545,10 @@ conversions!(u16, u16 { s to_i24 { I24::new_unchecked((s as i32 - 32_768) << 8) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s as i32 - 32_768) << 16 } @@ -442,6 +573,10 @@ conversions!(U24, u24 { s to_i24 { I24::new_unchecked(s.inner() - 8_388_608) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(s) } + s to_u24_be3 { U24BE3::from(s) } s to_i32 { (s.inner() - 8_388_608) << 8 } @@ -460,10 +595,54 @@ conversions!(U24, u24 { s to_f64 { super::i24::to_f64(to_i24(s)) } }); +conversions!(U24LE3, u24_le3 { + s to_i8 { super::u24::to_i8(to_u24(s)) } + s to_i16 { super::u24::to_i16(to_u24(s)) } + s to_i24 { super::u24::to_i24(to_u24(s)) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } + s to_i32 { super::u24::to_i32(to_u24(s)) } + s to_i48 { super::u24::to_i48(to_u24(s)) } + s to_i64 { super::u24::to_i64(to_u24(s)) } + s to_u8 { super::u24::to_u8(to_u24(s)) } + s to_u16 { super::u24::to_u16(to_u24(s)) } + s to_u24 { U24::from(s) } + s to_u32 { super::u24::to_u32(to_u24(s)) } + s to_u48 { super::u24::to_u48(to_u24(s)) } + s to_u64 { super::u24::to_u64(to_u24(s)) } + s to_f32 { super::u24::to_f32(to_u24(s)) } + s to_f64 { super::u24::to_f64(to_u24(s)) } +}); + +conversions!(U24BE3, u24_be3 { + s to_i8 { super::u24::to_i8(to_u24(s)) } + s to_i16 { super::u24::to_i16(to_u24(s)) } + s to_i24 { super::u24::to_i24(to_u24(s)) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_i32 { super::u24::to_i32(to_u24(s)) } + s to_i48 { super::u24::to_i48(to_u24(s)) } + s to_i64 { super::u24::to_i64(to_u24(s)) } + s to_u8 { super::u24::to_u8(to_u24(s)) } + s to_u16 { super::u24::to_u16(to_u24(s)) } + s to_u24 { U24::from(s) } + s to_u32 { super::u24::to_u32(to_u24(s)) } + s to_u48 { super::u24::to_u48(to_u24(s)) } + s to_u64 { super::u24::to_u64(to_u24(s)) } + s to_f32 { super::u24::to_f32(to_u24(s)) } + s to_f64 { super::u24::to_f64(to_u24(s)) } +}); + conversions!(u32, u32 { s to_i8 { super::u8::to_i8(to_u8(s)) } s to_i16 { super::u16::to_i16(to_u16(s)) } s to_i24 { super::u24::to_i24(to_u24(s)) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { if s < 2_147_483_648 { s as i32 - 2_147_483_647 - 1 @@ -490,6 +669,10 @@ conversions!(U48, u48 { s to_i8 { super::u8::to_i8(to_u8(s)) } s to_i16 { super::u16::to_i16(to_u16(s)) } s to_i24 { super::u24::to_i24(to_u24(s)) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { super::u32::to_i32(to_u32(s)) } s to_i48 { I48::new_unchecked(s.inner() - 140_737_488_355_328) @@ -510,6 +693,10 @@ conversions!(u64, u64 { s to_i8 { super::u8::to_i8(to_u8(s)) } s to_i16 { super::u16::to_i16(to_u16(s)) } s to_i24 { super::u24::to_i24(to_u24(s)) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { super::u32::to_i32(to_u32(s)) } s to_i48 { super::u48::to_i48(to_u48(s)) } s to_i64 { @@ -534,6 +721,10 @@ conversions!(f32, f32 { s to_i8 { (s * 128.0) as i8 } s to_i16 { (s * 32_768.0) as i16 } s to_i24 { I24::new_unchecked((s * 8_388_608.0) as i32) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s * 2_147_483_648.0) as i32 } s to_i48 { I48::new_unchecked((s * 140_737_488_355_328.0) as i64) } s to_i64 { (s * 9_223_372_036_854_775_808.0) as i64 } @@ -552,6 +743,10 @@ conversions!(f64, f64 { s to_i8 { (s * 128.0) as i8 } s to_i16 { (s * 32_768.0) as i16 } s to_i24 { I24::new_unchecked((s * 8_388_608.0) as i32) } + s to_i24_le3 { I24LE3::from(to_i24(s)) } + s to_i24_be3 { I24BE3::from(to_i24(s)) } + s to_u24_le3 { U24LE3::from(to_u24(s)) } + s to_u24_be3 { U24BE3::from(to_u24(s)) } s to_i32 { (s * 2_147_483_648.0) as i32 } s to_i48 { I48::new_unchecked((s * 140_737_488_355_328.0) as i64) } s to_i64 { (s * 9_223_372_036_854_775_808.0) as i64 } @@ -595,87 +790,129 @@ macro_rules! impl_from_sample { impl_from_sample! {i8, to_i8 from {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {i16, to_i16 from {i8:i8} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {I24, to_i24 from {i8:i8} {i16:i16} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {i32, to_i32 from {i8:i8} {i16:i16} {I24:i24} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {I48, to_i48 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {i64, to_i64 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {u8, to_u8 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {u16, to_u16 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {U24, to_u24 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {u32, to_u32 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {U48, to_u48 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {u64, to_u64 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} {f64:f64} } impl_from_sample! {f32, to_f32 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f64:f64} } impl_from_sample! {f64, to_f64 from {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} {f32:f32} } +impl_from_sample! {I24LE3, to_i24_le3 from + {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} + {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24BE3:i24_be3} {U24LE3:u24_le3} {U24BE3:u24_be3} + {f32:f32} {f64:f64} +} + +impl_from_sample! {I24BE3, to_i24_be3 from + {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} + {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {U24LE3:u24_le3} {U24BE3:u24_be3} + {f32:f32} {f64:f64} +} + +impl_from_sample! {U24LE3, to_u24_le3 from + {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} + {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24BE3:u24_be3} + {f32:f32} {f64:f64} +} + +impl_from_sample! {U24BE3, to_u24_be3 from + {i8:i8} {i16:i16} {I24:i24} {i32:i32} {I48:i48} {i64:i64} + {u8:u8} {u16:u16} {U24:u24} {u32:u32} {U48:u48} {u64:u64} + {I24LE3:i24_le3} {I24BE3:i24_be3} {U24LE3:u24_le3} + {f32:f32} {f64:f64} +} + /// Similar to the std `Into` trait, but specifically for converting between sample types. /// /// This trait has a blanket implementation for all types that implement diff --git a/dasp_sample/src/lib.rs b/dasp_sample/src/lib.rs index 0c82ebe6..3992acea 100644 --- a/dasp_sample/src/lib.rs +++ b/dasp_sample/src/lib.rs @@ -10,7 +10,7 @@ extern crate alloc; pub use conv::{Duplex, FromSample, ToSample}; -pub use types::{I24, I48, U24, U48}; +pub use types::{I24, I24BE3, I24LE3, I48, U24, U24BE3, U24LE3, U48}; pub mod conv; mod ops; @@ -261,12 +261,18 @@ impl_sample! { i8: Signed: i8, Float: f32, EQUILIBRIUM: 0, i16: Signed: i16, Float: f32, EQUILIBRIUM: 0, I24: Signed: I24, Float: f32, EQUILIBRIUM: types::i24::EQUILIBRIUM, + // The packed types share the `Signed` and `Float` types of their padded equivalents, so all + // arithmetic on them unpacks first rather than operating three bytes at a time. + I24LE3: Signed: I24LE3, Float: f32, EQUILIBRIUM: types::i24_le3::EQUILIBRIUM, + I24BE3: Signed: I24BE3, Float: f32, EQUILIBRIUM: types::i24_be3::EQUILIBRIUM, i32: Signed: i32, Float: f32, EQUILIBRIUM: 0, I48: Signed: I48, Float: f64, EQUILIBRIUM: types::i48::EQUILIBRIUM, i64: Signed: i64, Float: f64, EQUILIBRIUM: 0, u8: Signed: i8, Float: f32, EQUILIBRIUM: 128, u16: Signed: i16, Float: f32, EQUILIBRIUM: 32_768, U24: Signed: i32, Float: f32, EQUILIBRIUM: types::u24::EQUILIBRIUM, + U24LE3: Signed: i32, Float: f32, EQUILIBRIUM: types::u24_le3::EQUILIBRIUM, + U24BE3: Signed: i32, Float: f32, EQUILIBRIUM: types::u24_be3::EQUILIBRIUM, u32: Signed: i32, Float: f32, EQUILIBRIUM: 2_147_483_648, U48: Signed: i64, Float: f64, EQUILIBRIUM: types::u48::EQUILIBRIUM, u64: Signed: i64, Float: f64, EQUILIBRIUM: 9_223_372_036_854_775_808, @@ -286,7 +292,7 @@ pub trait SignedSample: { } macro_rules! impl_signed_sample { ($($T:ty)*) => { $( impl SignedSample for $T {} )* } } -impl_signed_sample!(i8 i16 I24 i32 I48 i64 f32 f64); +impl_signed_sample!(i8 i16 I24 I24LE3 I24BE3 i32 I48 i64 f32 f64); /// Sample format types represented as floating point numbers. /// diff --git a/dasp_sample/src/types.rs b/dasp_sample/src/types.rs index 6c48b821..4172157e 100644 --- a/dasp_sample/src/types.rs +++ b/dasp_sample/src/types.rs @@ -1,12 +1,65 @@ //! A collection of custom, non-std **Sample** types. +//! +//! Most types here are "padded", i.e. they store their value in the smallest primitive integer +//! that can contain it. `I24`, for example, is a newtype around an `i32` and is therefore four +//! bytes wide with an alignment of four. +//! +//! The `*LE3` and `*BE3` types are the "packed" counterparts: a 24-bit value stored in exactly +//! three bytes with an alignment of one, in the given byte order regardless of the byte order of +//! the host. These correspond to the `S24_3LE`, `S24_3BE`, `U24_3LE` and `U24_3BE` formats as +//! named by ALSA. +//! +//! # Packed types +//! +//! All four are `#[repr(transparent)]` over `[u8; 3]`, with `size_of` exactly 3, `align_of` +//! exactly 1, and every one of the 2^24 byte patterns a valid value. That is what they exist +//! for: a buffer of packed 24-bit PCM can be reinterpreted as a slice of them without a copy, +//! which a four-byte container cannot do. rustdoc does not render `repr(transparent)` for a type +//! with a private field, so the guarantee is stated here rather than being visible on each type. +//! +//! ``` +//! use dasp_sample::I24LE3; +//! +//! let raw: [u8; 9] = [0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x7F]; +//! // Sound because the alignment is 1, every byte pattern is valid, and the length is divided +//! // by the stride rather than assumed. +//! let samples: &[I24LE3] = unsafe { +//! core::slice::from_raw_parts(raw.as_ptr() as *const I24LE3, raw.len() / 3) +//! }; +//! assert_eq!(samples[0].inner(), -8_388_608); +//! assert_eq!(samples[2].inner(), 8_388_607); +//! ``` +//! +//! They carry the same arithmetic as their padded equivalents — `Add`, `Sub`, `Mul`, `Div`, +//! `Rem`, and `Neg` on the signed pair — with the same contract: `Add`, `Sub` and `Mul` panic on +//! overflow in debug and wrap in release, while `Div` and `Rem` are unchecked. The release wrap +//! costs nothing extra here, because truncating a value to three bytes *is* reduction modulo +//! 2^24, where the padded types need an explicit compare. `I24LE3` and `I24BE3` are therefore +//! `SignedSample`, like every other signed sample type; `U24LE3` and `U24BE3` are not, like every +//! other unsigned one. +//! +//! The bitwise and shift operators are not mirrored. They are bit manipulation rather than +//! arithmetic, and do not carry over: `!x` on a padded type inverts the 32 bits of its container, +//! including the byte holding no sample data. +//! +//! Two behaviours differ from the padded types and are easy to trip over: +//! +//! - Out-of-range values wrap modulo 2^24, because there is no spare byte to hold them. See the +//! `conv` module docs. +//! - `Default` is all-zero bytes, matching `I24`/`U24`. For the signed types that is equilibrium, +//! but for the unsigned ones it is `MIN`, the negative rail. Fill silence with `EQUILIBRIUM`. pub use self::i11::I11; pub use self::i20::I20; pub use self::i24::I24; +pub use self::i24_be3::I24BE3; +pub use self::i24_le3::I24LE3; pub use self::i48::I48; pub use self::u11::U11; pub use self::u20::U20; pub use self::u24::U24; +pub use self::u24_be3::U24BE3; +pub use self::u24_le3::U24LE3; pub use self::u48::U48; macro_rules! impl_from { @@ -58,6 +111,130 @@ macro_rules! impl_neg { }; } +// The packed counterparts of `impl_from!`/`impl_froms!`/`impl_neg!`. Separate because the value +// has to be encoded into three bytes rather than stored directly. +macro_rules! impl_packed_from { + ($T:ident from {$U:ident}) => { + impl From<$U> for $T { + #[inline] + fn from(other: $U) -> Self { + $T::new_unchecked(other.inner() as i32) + } + } + }; + ($T:ident from $U:ident) => { + impl From<$U> for $T { + #[inline] + fn from(other: $U) -> Self { + $T::new_unchecked(other as i32) + } + } + }; +} + +macro_rules! impl_packed_froms { + ($T:ident, {$U:ident}, $($rest:tt)*) => { + impl_packed_from!($T from {$U}); + impl_packed_froms!($T, $($rest)*); + }; + ($T:ident, {$U:ident}) => { + impl_packed_from!($T from {$U}); + }; + ($T:ident, $U:ident, $($rest:tt)*) => { + impl_packed_from!($T from $U); + impl_packed_froms!($T, $($rest)*); + }; + ($T:ident, $U:ident) => { + impl_packed_from!($T from $U); + }; + ($T:ident,) => {}; +} + +macro_rules! impl_packed_neg { + ($T:ident) => { + impl ::core::ops::Neg for $T { + type Output = $T; + /// Negation wraps for `MIN`, which has no positive counterpart in 24 bits, in + /// keeping with the rest of these types' out-of-range behaviour. + #[inline] + fn neg(self) -> $T { + $T::new_unchecked(-self.inner()) + } + } + }; +} + +// The arithmetic operators, mirroring what `new_sample_type!` gives the padded types: `Add`, +// `Sub` and `Mul` panic on overflow in debug and wrap in release, while `Div` and `Rem` are +// unchecked, exactly as the padded ones are. The release arms need no explicit wrap, because +// truncating to three bytes *is* reduction modulo 2^24 -- it lands on the same value the padded +// types reach through `wrap_overflow_once` (for `Add`/`Sub`) or `wrap_overflow` (for `Mul`). +// +// The bitwise and shift operators are deliberately not mirrored. They are bit manipulation +// rather than arithmetic, and their meaning does not carry over: `!x` on a padded type inverts +// the 32 bits of its container, including the byte that holds no sample data. +macro_rules! impl_packed_arithmetic { + ($T:ident) => { + impl ::core::ops::Add<$T> for $T { + type Output = $T; + #[inline] + fn add(self, other: Self) -> Self { + let sum = self.inner() + other.inner(); + if cfg!(debug_assertions) { + $T::new(sum).expect("arithmetic operation overflowed") + } else { + $T::new_unchecked(sum) + } + } + } + + impl ::core::ops::Sub<$T> for $T { + type Output = $T; + #[inline] + fn sub(self, other: Self) -> Self { + let difference = self.inner() - other.inner(); + if cfg!(debug_assertions) { + $T::new(difference).expect("arithmetic operation overflowed") + } else { + $T::new_unchecked(difference) + } + } + } + + impl ::core::ops::Mul<$T> for $T { + type Output = $T; + #[inline] + fn mul(self, other: Self) -> Self { + // As on the padded types, the product is formed in the same width the values + // unpack to, so a large enough pair overflows that before the sample range is + // ever considered. + let product = self.inner() * other.inner(); + if cfg!(debug_assertions) { + $T::new(product).expect("arithmetic operation overflowed") + } else { + $T::new_unchecked(product) + } + } + } + + impl ::core::ops::Div<$T> for $T { + type Output = $T; + #[inline] + fn div(self, other: Self) -> Self { + $T::new_unchecked(self.inner() / other.inner()) + } + } + + impl ::core::ops::Rem<$T> for $T { + type Output = $T; + #[inline] + fn rem(self, other: Self) -> Self { + $T::new_unchecked(self.inner() % other.inner()) + } + } + }; +} + macro_rules! new_sample_type { ($T:ident: $Rep:ident, eq: $EQ:expr, min: $MIN:expr, max: $MAX:expr, total: $TOTAL:expr, from: $($rest:tt)*) => { pub const MIN: $T = $T($MIN); @@ -234,6 +411,128 @@ macro_rules! new_sample_type { }; } +// Expands to a packed 24-bit sample type; see the module docs. `$decode` and `$encode` carry the +// byte order and signedness, and are the only things that differ between the four types. +macro_rules! new_packed_sample_type { + ($T:ident: $Unpacked:ident, eq: $EQ:expr, min: $MIN:expr, max: $MAX:expr, + endian: $ENDIAN:expr, alsa: $ALSA:expr, + decode: |$b:ident| $decode:expr, encode: |$v:ident| $encode:expr, + from: $($rest:tt)*) => { + pub const MIN: $T = $T::new_unchecked($MIN); + pub const MAX: $T = $T::new_unchecked($MAX); + pub const EQUILIBRIUM: $T = $T::new_unchecked($EQ); + const MIN_VALUE: i32 = $MIN; + const MAX_VALUE: i32 = $MAX; + + #[doc = concat!("A 24-bit sample packed into exactly three ", $ENDIAN, " bytes.")] + /// + #[doc = concat!( + "Valid range `", stringify!($MIN), "..=", stringify!($MAX), + "`, with equilibrium at `", stringify!($EQ), "`. Known to ALSA as `", $ALSA, "`." + )] + /// + #[doc = concat!( + "This type is ", $ENDIAN, " on every target; see the [`types`](crate::types) module docs for the \ + layout guarantees, which are what make the zero-copy reinterpretation possible." + )] + #[derive(Copy, Clone, PartialEq, Eq, Default)] + #[repr(transparent)] + pub struct $T([u8; 3]); + + impl $T { + #[doc = concat!( + "Construct a sample from its three packed bytes, read as ", $ENDIAN, ". Every byte \ + triple is a valid sample, so this cannot fail." + )] + #[inline] + pub const fn from_bytes(bytes: [u8; 3]) -> Self { + $T(bytes) + } + + #[doc = concat!("The three packed bytes representing the sample, in ", $ENDIAN, " order.")] + #[inline] + pub const fn to_bytes(self) -> [u8; 3] { + self.0 + } + + /// Construct a new sample if the given value is within range. + /// + /// Returns `None` if `val` is out of range. + #[inline] + pub fn new(val: i32) -> Option { + if (MIN_VALUE..=MAX_VALUE).contains(&val) { + Some($T::new_unchecked(val)) + } else { + None + } + } + + /// Constructs a new sample without checking for overflowing. + /// + /// Unlike `I24::new_unchecked`, which stores an out-of-range value verbatim, there is + /// nowhere to put the overshoot here, so `val` wraps modulo 2^24. Use [`Self::new`] + /// where the value is not known to be in range. + /// + /// ``` + /// use dasp_sample::{I24, I24LE3}; + /// + /// assert_eq!(I24::new_unchecked(8_388_608).inner(), 8_388_608); // stored verbatim + /// assert_eq!(I24LE3::new_unchecked(8_388_608).inner(), -8_388_608); // wrapped + /// ``` + #[inline] + pub const fn new_unchecked($v: i32) -> Self { + $T($encode) + } + + /// The value represented by the sample, unpacked into an `i32`. + #[inline] + pub const fn inner(self) -> i32 { + let $b = self.0; + $decode + } + } + + impl From<$Unpacked> for $T { + #[inline] + fn from(other: $Unpacked) -> Self { + $T::new_unchecked(other.inner()) + } + } + + impl From<$T> for $Unpacked { + #[inline] + fn from(other: $T) -> Self { + $Unpacked::new_unchecked(other.inner()) + } + } + + // Derived comparison would compare the bytes in storage order, which is neither the + // numeric order nor even consistent between the `LE3` and `BE3` types, so we decode. + impl ::core::cmp::Ord for $T { + #[inline] + fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { + self.inner().cmp(&other.inner()) + } + } + + impl ::core::cmp::PartialOrd for $T { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> { + Some(self.cmp(other)) + } + } + + // Show the value rather than the storage, to match the padded sample types. + impl ::core::fmt::Debug for $T { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + f.debug_tuple(stringify!($T)).field(&self.inner()).finish() + } + } + + impl_packed_froms!($T, $($rest)*); + }; +} + pub mod i11 { new_sample_type!(I11: i16, eq: 0, min: -1024, max: 1023, total: 2048, from: i8, u8); @@ -253,10 +552,34 @@ pub mod i24 { impl_neg!(I24); } +pub mod i24_le3 { + use super::{I20, I24, U20}; + new_packed_sample_type!(I24LE3: I24, eq: 0, min: -8_388_608, max: 8_388_607, + endian: "little-endian", alsa: "S24_3LE", + // The most significant byte is cast via `i8` to sign extend. + decode: |b| (b[0] as i32) | ((b[1] as i32) << 8) | ((b[2] as i8 as i32) << 16), + encode: |v| [v as u8, (v >> 8) as u8, (v >> 16) as u8], + from: i8, i16, {I20}, i32, u8, u16, {U20}); + impl_packed_neg!(I24LE3); + impl_packed_arithmetic!(I24LE3); +} + +pub mod i24_be3 { + use super::{I20, I24, U20}; + new_packed_sample_type!(I24BE3: I24, eq: 0, min: -8_388_608, max: 8_388_607, + endian: "big-endian", alsa: "S24_3BE", + decode: |b| (b[2] as i32) | ((b[1] as i32) << 8) | ((b[0] as i8 as i32) << 16), + encode: |v| [(v >> 16) as u8, (v >> 8) as u8, v as u8], + from: i8, i16, {I20}, i32, u8, u16, {U20}); + impl_packed_neg!(I24BE3); + impl_packed_arithmetic!(I24BE3); +} + pub mod i48 { - use super::{I20, I24, U20, U24}; + use super::{I20, I24, I24BE3, I24LE3, U20, U24, U24BE3, U24LE3}; new_sample_type!(I48: i64, eq: 0, min: -140_737_488_355_328, max: 140_737_488_355_327, total: 281_474_976_710_656, - from: i8, i16, {I20:i32}, {I24:i32}, i32, u8, u16, {U20:i32}, {U24:i32}, u32); + from: i8, i16, {I20:i32}, {I24:i32}, {I24LE3:i32}, {I24BE3:i32}, i32, + u8, u16, {U20:i32}, {U24:i32}, {U24LE3:i32}, {U24BE3:i32}, u32); impl_neg!(I48); } @@ -277,8 +600,28 @@ pub mod u24 { from: u8, u16, {U20:i32}); } -pub mod u48 { +pub mod u24_le3 { + use super::{U20, U24}; + new_packed_sample_type!(U24LE3: U24, eq: 8_388_608, min: 0, max: 16_777_215, + endian: "little-endian", alsa: "U24_3LE", + decode: |b| (b[0] as i32) | ((b[1] as i32) << 8) | ((b[2] as i32) << 16), + encode: |v| [v as u8, (v >> 8) as u8, (v >> 16) as u8], + from: i32, u8, u16, {U20}); + impl_packed_arithmetic!(U24LE3); +} + +pub mod u24_be3 { use super::{U20, U24}; + new_packed_sample_type!(U24BE3: U24, eq: 8_388_608, min: 0, max: 16_777_215, + endian: "big-endian", alsa: "U24_3BE", + decode: |b| (b[2] as i32) | ((b[1] as i32) << 8) | ((b[0] as i32) << 16), + encode: |v| [(v >> 16) as u8, (v >> 8) as u8, v as u8], + from: i32, u8, u16, {U20}); + impl_packed_arithmetic!(U24BE3); +} + +pub mod u48 { + use super::{U20, U24, U24BE3, U24LE3}; new_sample_type!(U48: i64, eq: 140_737_488_355_328, min: 0, max: 281_474_976_710_655, total: 281_474_976_710_656, - from: u8, u16, {U20:i32}, {U24:i32}, u32); + from: u8, u16, {U20:i32}, {U24:i32}, {U24LE3:i32}, {U24BE3:i32}, u32); } diff --git a/dasp_sample/tests/conv.rs b/dasp_sample/tests/conv.rs index b442d246..5f7ca0df 100644 --- a/dasp_sample/tests/conv.rs +++ b/dasp_sample/tests/conv.rs @@ -6,21 +6,33 @@ /// Expands to an `assert_eq` for each pre-conversion and post-conversion pair. /// -/// Literals that must be wrapped by a custom sample type are wrapped using $T/$U::new_unchecked. +/// Literals that must be wrapped by a custom sample type go through `new`, not `new_unchecked`, +/// so that an out-of-range literal fails the test rather than being silently mangled into a +/// value the assertion then agrees with. That distinction only became load-bearing with the +/// packed types: `new_unchecked` stores out-of-range values verbatim on the padded types but +/// wraps modulo 2^24 on the packed ones, which would make +/// `I24LE3::new_unchecked(8_388_608) == I24LE3::new_unchecked(-8_388_608)` and let a wrong +/// expectation pass. macro_rules! conv_cmp { ($fn_name:ident, $pre_conv:expr, $post_conv:expr) => { assert_eq!($fn_name($pre_conv), $post_conv); }; ($fn_name:ident: $U:ident, $pre_conv:expr, $post_conv:expr) => { - assert_eq!($fn_name($pre_conv), $U::new_unchecked($post_conv)); + assert_eq!( + $fn_name($pre_conv), + $U::new($post_conv).expect("expected value is out of range for the target type") + ); }; ($T:ident; $fn_name:ident, $pre_conv:expr, $post_conv:expr) => { - assert_eq!($fn_name($T::new_unchecked($pre_conv)), $post_conv); + assert_eq!( + $fn_name($T::new($pre_conv).expect("input value is out of range for the source type")), + $post_conv + ); }; ($T:ident; $fn_name:ident: $U:ident, $pre_conv:expr, $post_conv:expr) => { assert_eq!( - $fn_name($T::new_unchecked($pre_conv)), - $U::new_unchecked($post_conv) + $fn_name($T::new($pre_conv).expect("input value is out of range for the source type")), + $U::new($post_conv).expect("expected value is out of range for the target type") ); }; } @@ -78,6 +90,34 @@ macro_rules! test_fn { } }; + (to_i24_le3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_i24_le3() { + conv_cmps!(to_i24_le3: I24LE3, $($conv_cmps)*); + } + }; + + (to_i24_be3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_i24_be3() { + conv_cmps!(to_i24_be3: I24BE3, $($conv_cmps)*); + } + }; + + (to_u24_le3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_u24_le3() { + conv_cmps!(to_u24_le3: U24LE3, $($conv_cmps)*); + } + }; + + (to_u24_be3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_u24_be3() { + conv_cmps!(to_u24_be3: U24BE3, $($conv_cmps)*); + } + }; + (to_i32 { $($conv_cmps:tt)* }) => { #[test] fn test_to_i32() { @@ -178,6 +218,34 @@ macro_rules! test_fn { } }; + ($T:ident: to_i24_le3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_i24_le3() { + conv_cmps!($T; to_i24_le3: I24LE3, $($conv_cmps)*); + } + }; + + ($T:ident: to_i24_be3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_i24_be3() { + conv_cmps!($T; to_i24_be3: I24BE3, $($conv_cmps)*); + } + }; + + ($T:ident: to_u24_le3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_u24_le3() { + conv_cmps!($T; to_u24_le3: U24LE3, $($conv_cmps)*); + } + }; + + ($T:ident: to_u24_be3 { $($conv_cmps:tt)* }) => { + #[test] + fn test_to_u24_be3() { + conv_cmps!($T; to_u24_be3: U24BE3, $($conv_cmps)*); + } + }; + ($T:ident: to_i32 { $($conv_cmps:tt)* }) => { #[test] fn test_to_i32() { @@ -275,14 +343,14 @@ macro_rules! tests { ($T:ident { $($rest:tt)* }) => { pub mod $T { use dasp_sample::conv::$T::*; - use dasp_sample::types::{I24, U24, I48, U48}; + use dasp_sample::types::{I24, I24BE3, I24LE3, U24, U24BE3, U24LE3, I48, U48}; test_fns!($($rest)*); } }; ($T:ident: $mod_name:ident { $($rest:tt)* }) => { pub mod $mod_name { use dasp_sample::conv::$mod_name::*; - use dasp_sample::types::{I24, U24, I48, U48}; + use dasp_sample::types::{I24, I24BE3, I24LE3, U24, U24BE3, U24LE3, I48, U48}; test_fns!($T: $($rest)*); } }; @@ -300,6 +368,10 @@ tests!(i8 { to_u32 { -128, 0; 0, 2_147_483_648; 127, 4_278_190_080; } to_u48 { -128, 0; 0, 140_737_488_355_328; 127, 280_375_465_082_880; } to_u64 { -128, 0; 0, 9_223_372_036_854_775_808; 127, 18_374_686_479_671_623_680; } + to_i24_le3 { -128, -8_388_608; 0, 0; 127, 8_323_072; } + to_i24_be3 { -128, -8_388_608; 0, 0; 127, 8_323_072; } + to_u24_le3 { -128, 0; 0, 8_388_608; 127, 16_711_680; } + to_u24_be3 { -128, 0; 0, 8_388_608; 127, 16_711_680; } to_f32 { -128, -1.0; 0, 0.0; } to_f64 { -128, -1.0; 0, 0.0; } }); @@ -316,6 +388,10 @@ tests!(i16 { to_u32 { -32_768, 0; 0, 2_147_483_648; 32_767, 4_294_901_760; } to_u48 { -32_768, 0; 0, 140_737_488_355_328; 32_767, 281_470_681_743_360; } to_u64 { -32_768, 0; 0, 9_223_372_036_854_775_808; 32_767, 18_446_462_598_732_840_960; } + to_i24_le3 { -32_768, -8_388_608; 0, 0; 32_767, 8_388_352; } + to_i24_be3 { -32_768, -8_388_608; 0, 0; 32_767, 8_388_352; } + to_u24_le3 { -32_768, 0; 0, 8_388_608; 32_767, 16_776_960; } + to_u24_be3 { -32_768, 0; 0, 8_388_608; 32_767, 16_776_960; } to_f32 { -32_768, -1.0; 0, 0.0; } to_f64 { -32_768, -1.0; 0, 0.0; } }); @@ -332,6 +408,52 @@ tests!(I24: i24 { to_u32 { -8_388_608, 0; 0, 2_147_483_648; 8_388_607, 4_294_967_040; } to_u48 { -8_388_608, 0; 0, 140_737_488_355_328; 8_388_607, 281_474_959_933_440; } to_u64 { -8_388_608, 0; 0, 9_223_372_036_854_775_808; 8_388_607, 18_446_742_974_197_923_840; } + to_i24_le3 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_i24_be3 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_u24_le3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_u24_be3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_f32 { -8_388_608, -1.0; 0, 0.0; } + to_f64 { -8_388_608, -1.0; 0, 0.0; } +}); + +// The packed 24-bit types cover the same range as `I24`, so their expectations match the `i24` +// block above. What is under test here is that packing and unpacking preserves the value. +tests!(I24LE3: i24_le3 { + to_i8 { -8_388_608, -128; 0, 0; 8_388_607, 127; } + to_i16 { -8_388_608, -32_768; 0, 0; 8_388_607, 32_767; } + to_i24 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_i32 { -8_388_608, -2_147_483_648; 0, 0; 8_388_607, 2_147_483_392; } + to_i48 { -8_388_608, -140_737_488_355_328; 0, 0; 8_388_607, 140_737_471_578_112; } + to_i64 { -8_388_608, -9_223_372_036_854_775_808; 0, 0; 8_388_607, 9_223_370_937_343_148_032; } + to_u8 { -8_388_608, 0; 0, 128; 8_388_607, 255; } + to_u16 { -8_388_608, 0; 0, 32_768; 8_388_607, 65_535; } + to_u24 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_u32 { -8_388_608, 0; 0, 2_147_483_648; 8_388_607, 4_294_967_040; } + to_u48 { -8_388_608, 0; 0, 140_737_488_355_328; 8_388_607, 281_474_959_933_440; } + to_u64 { -8_388_608, 0; 0, 9_223_372_036_854_775_808; 8_388_607, 18_446_742_974_197_923_840; } + to_i24_be3 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_u24_le3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_u24_be3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_f32 { -8_388_608, -1.0; 0, 0.0; } + to_f64 { -8_388_608, -1.0; 0, 0.0; } +}); + +tests!(I24BE3: i24_be3 { + to_i8 { -8_388_608, -128; 0, 0; 8_388_607, 127; } + to_i16 { -8_388_608, -32_768; 0, 0; 8_388_607, 32_767; } + to_i24 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_i32 { -8_388_608, -2_147_483_648; 0, 0; 8_388_607, 2_147_483_392; } + to_i48 { -8_388_608, -140_737_488_355_328; 0, 0; 8_388_607, 140_737_471_578_112; } + to_i64 { -8_388_608, -9_223_372_036_854_775_808; 0, 0; 8_388_607, 9_223_370_937_343_148_032; } + to_u8 { -8_388_608, 0; 0, 128; 8_388_607, 255; } + to_u16 { -8_388_608, 0; 0, 32_768; 8_388_607, 65_535; } + to_u24 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_u32 { -8_388_608, 0; 0, 2_147_483_648; 8_388_607, 4_294_967_040; } + to_u48 { -8_388_608, 0; 0, 140_737_488_355_328; 8_388_607, 281_474_959_933_440; } + to_u64 { -8_388_608, 0; 0, 9_223_372_036_854_775_808; 8_388_607, 18_446_742_974_197_923_840; } + to_i24_le3 { -8_388_608, -8_388_608; 0, 0; 8_388_607, 8_388_607; } + to_u24_le3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } + to_u24_be3 { -8_388_608, 0; 0, 8_388_608; 8_388_607, 16_777_215; } to_f32 { -8_388_608, -1.0; 0, 0.0; } to_f64 { -8_388_608, -1.0; 0, 0.0; } }); @@ -348,6 +470,10 @@ tests!(i32 { to_u32 { -2_147_483_648, 0; 0, 2_147_483_648; 2_147_483_647, 4_294_967_295; } to_u48 { -2_147_483_648, 0; 0, 140_737_488_355_328; 2_147_483_647, 281_474_976_645_120; } to_u64 { -2_147_483_648, 0; 0, 9_223_372_036_854_775_808; 2_147_483_647, 18_446_744_069_414_584_320; } + to_i24_le3 { -2_147_483_648, -8_388_608; 0, 0; 2_147_483_647, 8_388_607; } + to_i24_be3 { -2_147_483_648, -8_388_608; 0, 0; 2_147_483_647, 8_388_607; } + to_u24_le3 { -2_147_483_648, 0; 0, 8_388_608; 2_147_483_647, 16_777_215; } + to_u24_be3 { -2_147_483_648, 0; 0, 8_388_608; 2_147_483_647, 16_777_215; } to_f32 { -2_147_483_648, -1.0; 0, 0.0; } to_f64 { -2_147_483_648, -1.0; 0, 0.0; } }); @@ -364,6 +490,10 @@ tests!(I48: i48 { to_u32 { -140_737_488_355_328, 0; 0, 2_147_483_648; 140_737_488_355_327, 4_294_967_295; } to_u48 { -140_737_488_355_328, 0; 0, 140_737_488_355_328; 140_737_488_355_327, 281_474_976_710_655; } to_u64 { -140_737_488_355_328, 0; 0, 9_223_372_036_854_775_808; 140_737_488_355_327, 18_446_744_073_709_486_080; } + to_i24_le3 { -140_737_488_355_328, -8_388_608; 0, 0; 140_737_488_355_327, 8_388_607; } + to_i24_be3 { -140_737_488_355_328, -8_388_608; 0, 0; 140_737_488_355_327, 8_388_607; } + to_u24_le3 { -140_737_488_355_328, 0; 0, 8_388_608; 140_737_488_355_327, 16_777_215; } + to_u24_be3 { -140_737_488_355_328, 0; 0, 8_388_608; 140_737_488_355_327, 16_777_215; } }); tests!(i64 { @@ -378,6 +508,10 @@ tests!(i64 { to_u32 { -9_223_372_036_854_775_808, 0; 0, 2_147_483_648; 9_223_372_036_854_775_807, 4_294_967_295; } to_u48 { -9_223_372_036_854_775_808, 0; 0, 140_737_488_355_328; 9_223_372_036_854_775_807, 281_474_976_710_655; } to_u64 { -9_223_372_036_854_775_808, 0; 0, 9_223_372_036_854_775_808; 9_223_372_036_854_775_807, 18_446_744_073_709_551_615; } + to_i24_le3 { -9_223_372_036_854_775_808, -8_388_608; 0, 0; 9_223_372_036_854_775_807, 8_388_607; } + to_i24_be3 { -9_223_372_036_854_775_808, -8_388_608; 0, 0; 9_223_372_036_854_775_807, 8_388_607; } + to_u24_le3 { -9_223_372_036_854_775_808, 0; 0, 8_388_608; 9_223_372_036_854_775_807, 16_777_215; } + to_u24_be3 { -9_223_372_036_854_775_808, 0; 0, 8_388_608; 9_223_372_036_854_775_807, 16_777_215; } to_f32 { -9_223_372_036_854_775_808, -1.0; 0, 0.0; } to_f64 { -9_223_372_036_854_775_808, -1.0; 0, 0.0; } }); @@ -394,6 +528,10 @@ tests!(u8 { to_u32 { 0, 0; 128, 2_147_483_648; 255, 4_278_190_080; } to_u48 { 0, 0; 128, 140_737_488_355_328; 255, 280_375_465_082_880; } to_u64 { 0, 0; 128, 9_223_372_036_854_775_808; 255, 18_374_686_479_671_623_680; } + to_i24_le3 { 0, -8_388_608; 128, 0; 255, 8_323_072; } + to_i24_be3 { 0, -8_388_608; 128, 0; 255, 8_323_072; } + to_u24_le3 { 0, 0; 128, 8_388_608; 255, 16_711_680; } + to_u24_be3 { 0, 0; 128, 8_388_608; 255, 16_711_680; } to_f32 { 0, -1.0; 128, 0.0; } to_f64 { 0, -1.0; 128, 0.0; } }); @@ -410,6 +548,10 @@ tests!(u16 { to_u32 { 0, 0; 32_768, 2_147_483_648; 65_535, 4_294_901_760; } to_u48 { 0, 0; 32_768, 140_737_488_355_328; 65_535, 281_470_681_743_360; } to_u64 { 0, 0; 32_768, 9_223_372_036_854_775_808; 65_535, 18_446_462_598_732_840_960; } + to_i24_le3 { 0, -8_388_608; 32_768, 0; 65_535, 8_388_352; } + to_i24_be3 { 0, -8_388_608; 32_768, 0; 65_535, 8_388_352; } + to_u24_le3 { 0, 0; 32_768, 8_388_608; 65_535, 16_776_960; } + to_u24_be3 { 0, 0; 32_768, 8_388_608; 65_535, 16_776_960; } to_f32 { 0, -1.0; 32_768, 0.0; } to_f64 { 0, -1.0; 32_768, 0.0; } }); @@ -426,6 +568,50 @@ tests!(U24: u24 { to_u32 { 0, 0; 8_388_608, 2_147_483_648; 16_777_215, 4_294_967_040; } to_u48 { 0, 0; 8_388_608, 140_737_488_355_328; 16_777_215, 281_474_959_933_440; } to_u64 { 0, 0; 8_388_608, 9_223_372_036_854_775_808; 16_777_215, 18_446_742_974_197_923_840; } + to_i24_le3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_u24_le3 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } + to_u24_be3 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } + to_f32 { 0, -1.0; 8_388_608, 0.0; } + to_f64 { 0, -1.0; 8_388_608, 0.0; } +}); + +tests!(U24LE3: u24_le3 { + to_i8 { 0, -128; 8_388_608, 0; 16_777_215, 127; } + to_i16 { 0, -32_768; 8_388_608, 0; 16_777_215, 32_767; } + to_i24 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_i32 { 0, -2_147_483_648; 8_388_608, 0; 16_777_215, 2_147_483_392; } + to_i48 { 0, -140_737_488_355_328; 8_388_608, 0; 16_777_215, 140_737_471_578_112; } + to_i64 { 0, -9_223_372_036_854_775_808; 8_388_608, 0; 16_777_215, 9_223_370_937_343_148_032; } + to_u8 { 0, 0; 8_388_608, 128; 16_777_215, 255; } + to_u16 { 0, 0; 8_388_608, 32_768; 16_777_215, 65_535; } + to_u24 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } + to_u32 { 0, 0; 8_388_608, 2_147_483_648; 16_777_215, 4_294_967_040; } + to_u48 { 0, 0; 8_388_608, 140_737_488_355_328; 16_777_215, 281_474_959_933_440; } + to_u64 { 0, 0; 8_388_608, 9_223_372_036_854_775_808; 16_777_215, 18_446_742_974_197_923_840; } + to_i24_le3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_u24_be3 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } + to_f32 { 0, -1.0; 8_388_608, 0.0; } + to_f64 { 0, -1.0; 8_388_608, 0.0; } +}); + +tests!(U24BE3: u24_be3 { + to_i8 { 0, -128; 8_388_608, 0; 16_777_215, 127; } + to_i16 { 0, -32_768; 8_388_608, 0; 16_777_215, 32_767; } + to_i24 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_i32 { 0, -2_147_483_648; 8_388_608, 0; 16_777_215, 2_147_483_392; } + to_i48 { 0, -140_737_488_355_328; 8_388_608, 0; 16_777_215, 140_737_471_578_112; } + to_i64 { 0, -9_223_372_036_854_775_808; 8_388_608, 0; 16_777_215, 9_223_370_937_343_148_032; } + to_u8 { 0, 0; 8_388_608, 128; 16_777_215, 255; } + to_u16 { 0, 0; 8_388_608, 32_768; 16_777_215, 65_535; } + to_u24 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } + to_u32 { 0, 0; 8_388_608, 2_147_483_648; 16_777_215, 4_294_967_040; } + to_u48 { 0, 0; 8_388_608, 140_737_488_355_328; 16_777_215, 281_474_959_933_440; } + to_u64 { 0, 0; 8_388_608, 9_223_372_036_854_775_808; 16_777_215, 18_446_742_974_197_923_840; } + to_i24_le3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 8_388_608, 0; 16_777_215, 8_388_607; } + to_u24_le3 { 0, 0; 8_388_608, 8_388_608; 16_777_215, 16_777_215; } to_f32 { 0, -1.0; 8_388_608, 0.0; } to_f64 { 0, -1.0; 8_388_608, 0.0; } }); @@ -442,6 +628,10 @@ tests!(u32 { to_u24 { 0, 0; 2_147_483_648, 8_388_608; 4_294_967_295, 16_777_215; } to_u48 { 0, 0; 2_147_483_648, 140_737_488_355_328; 4_294_967_295, 281_474_976_645_120; } to_u64 { 0, 0; 2_147_483_648, 9_223_372_036_854_775_808; 4_294_967_295, 18_446_744_069_414_584_320; } + to_i24_le3 { 0, -8_388_608; 2_147_483_648, 0; 4_294_967_295, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 2_147_483_648, 0; 4_294_967_295, 8_388_607; } + to_u24_le3 { 0, 0; 2_147_483_648, 8_388_608; 4_294_967_295, 16_777_215; } + to_u24_be3 { 0, 0; 2_147_483_648, 8_388_608; 4_294_967_295, 16_777_215; } to_f32 { 0, -1.0; 2_147_483_648, 0.0; } to_f64 { 0, -1.0; 2_147_483_648, 0.0; } }); @@ -458,6 +648,10 @@ tests!(U48: u48 { to_u24 { 0, 0; 140_737_488_355_328, 8_388_608; 281_474_976_710_655, 16_777_215; } to_u32 { 0, 0; 140_737_488_355_328, 2_147_483_648; 281_474_976_710_655, 4_294_967_295; } to_u64 { 0, 0; 140_737_488_355_328, 9_223_372_036_854_775_808; 281_474_976_710_655, 18_446_744_073_709_486_080; } + to_i24_le3 { 0, -8_388_608; 140_737_488_355_328, 0; 281_474_976_710_655, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 140_737_488_355_328, 0; 281_474_976_710_655, 8_388_607; } + to_u24_le3 { 0, 0; 140_737_488_355_328, 8_388_608; 281_474_976_710_655, 16_777_215; } + to_u24_be3 { 0, 0; 140_737_488_355_328, 8_388_608; 281_474_976_710_655, 16_777_215; } to_f32 { 0, -1.0; 140_737_488_355_328, 0.0; } to_f64 { 0, -1.0; 140_737_488_355_328, 0.0; } }); @@ -474,6 +668,10 @@ tests!(u64 { to_u24 { 0, 0; 9_223_372_036_854_775_808, 8_388_608; 18_446_744_073_709_551_615, 16_777_215; } to_u32 { 0, 0; 9_223_372_036_854_775_808, 2_147_483_648; 18_446_744_073_709_551_615, 4_294_967_295; } to_u48 { 0, 0; 9_223_372_036_854_775_808, 140_737_488_355_328; 18_446_744_073_709_551_615, 281_474_976_710_655; } + to_i24_le3 { 0, -8_388_608; 9_223_372_036_854_775_808, 0; 18_446_744_073_709_551_615, 8_388_607; } + to_i24_be3 { 0, -8_388_608; 9_223_372_036_854_775_808, 0; 18_446_744_073_709_551_615, 8_388_607; } + to_u24_le3 { 0, 0; 9_223_372_036_854_775_808, 8_388_608; 18_446_744_073_709_551_615, 16_777_215; } + to_u24_be3 { 0, 0; 9_223_372_036_854_775_808, 8_388_608; 18_446_744_073_709_551_615, 16_777_215; } to_f32 { 0, -1.0; 9_223_372_036_854_775_808, 0.0; } to_f64 { 0, -1.0; 9_223_372_036_854_775_808, 0.0; } }); @@ -491,6 +689,10 @@ tests!(f32 { to_u32 { -1.0, 0; 0.0, 2_147_483_648; } to_u48 { -1.0, 0; 0.0, 140_737_488_355_328; } to_u64 { -1.0, 0; 0.0, 9_223_372_036_854_775_808; } + to_i24_le3 { -1.0, -8_388_608; 0.0, 0; } + to_i24_be3 { -1.0, -8_388_608; 0.0, 0; } + to_u24_le3 { -1.0, 0; 0.0, 8_388_608; } + to_u24_be3 { -1.0, 0; 0.0, 8_388_608; } to_f64 { -1.0, -1.0; 0.0, 0.0; } }); @@ -507,5 +709,74 @@ tests!(f64 { to_u32 { -1.0, 0; 0.0, 2_147_483_648; } to_u48 { -1.0, 0; 0.0, 140_737_488_355_328; } to_u64 { -1.0, 0; 0.0, 9_223_372_036_854_775_808; } + to_i24_le3 { -1.0, -8_388_608; 0.0, 0; } + to_i24_be3 { -1.0, -8_388_608; 0.0, 0; } + to_u24_le3 { -1.0, 0; 0.0, 8_388_608; } + to_u24_be3 { -1.0, 0; 0.0, 8_388_608; } to_f32 { -1.0, -1.0; 0.0, 0.0; } }); + +/// A completeness check over the whole conversion matrix. +/// +/// The blocks above are written out by hand, so a missing `FromSample` impl for some pair of +/// types would simply go untested rather than fail. This asserts that every ordered pair of +/// sample types converts at all (a missing impl is a compile error) and that equilibrium is +/// preserved in every direction, which is the one expectation that holds for all of them. +/// +/// Add any new sample type to the single list in `every_pair_converts_and_preserves_equilibrium`. +mod matrix { + use dasp_sample::{Sample, I24, I24BE3, I24LE3, I48, U24, U24BE3, U24LE3, U48}; + + macro_rules! conv_row { + ($n:ident; $A:ty; $($B:ty),*) => {$({ + let converted: $B = <$A as Sample>::EQUILIBRIUM.to_sample::<$B>(); + assert_eq!( + converted, + <$B as Sample>::EQUILIBRIUM, + "{} -> {}: equilibrium was not preserved", + stringify!($A), + stringify!($B), + ); + $n += 1; + })*}; + } + + /// Expands to the conversions for every ordered pair, and evaluates to the number of types + /// in the list. + /// + /// The list is given once and expanded along *both* dimensions, which is the point: an + /// earlier version of this test wrote the target types out a second time, and comparing the + /// pair count against `types * types` then only proved the two lists were the same length. + /// Swapping one type for another in just one of them passed while leaving that type + /// untested and its replacement tested twice. With a single list that cannot happen. + macro_rules! conv_matrix { + ($n:ident; $($T:ty),*) => {{ + conv_matrix!(@rows $n; [$($T),*]; $($T),*); + [$(stringify!($T)),*].len() + }}; + // Peel one type off the outer list at a time so the whole inner list can be expanded + // again for each row. A single `$( conv_row!(.. $A .. $($B),*); )*` is not legal: both + // lists sit at the same repetition depth, so rustc tries to iterate them in lockstep. + (@rows $n:ident; [$($B:ty),*]; $A:ty $(, $rest:ty)*) => { + conv_row!($n; $A; $($B),*); + conv_matrix!(@rows $n; [$($B),*]; $($rest),*); + }; + (@rows $n:ident; [$($B:ty),*];) => {}; + } + + #[test] + fn every_pair_converts_and_preserves_equilibrium() { + let mut n = 0usize; + let types = conv_matrix!(n; i8, i16, I24, I24LE3, I24BE3, i32, I48, i64, + u8, u16, U24, U24LE3, U24BE3, u32, U48, u64, f32, f64); + assert_eq!( + n, + types * types, + "expected every ordered pair of {types} sample types" + ); + assert_eq!( + types, 18, + "a sample type was added or removed without updating this count" + ); + } +} diff --git a/dasp_sample/tests/packed.rs b/dasp_sample/tests/packed.rs new file mode 100644 index 00000000..7700d155 --- /dev/null +++ b/dasp_sample/tests/packed.rs @@ -0,0 +1,451 @@ +//! Tests for the packed 24-bit sample types. +//! +//! The conversion matrix in `conv.rs` already covers the *values* these types produce. What is +//! tested here is everything that is specific to their being packed: their memory layout, their +//! byte order, and the traits whose derived implementations would have been wrong. + +use dasp_sample::types::{i24_be3, i24_le3, u24_be3, u24_le3, I20, U20}; +use dasp_sample::{Sample, SignedSample, I24, I24BE3, I24LE3, I48, U24, U24BE3, U24LE3, U48}; +use std::mem; + +/// The packed types offer the same `From` conversions and `Neg` as their padded equivalents. +/// +/// These are value-widening conversions rather than amplitude-preserving ones — `I24::from(3i8)` +/// is `I24(3)`, not `I24(3 << 16)` — and the packed types match that exactly. +#[test] +fn from_and_neg_match_the_padded_types() { + // Signed packed: the same list `I24` gets. + assert_eq!(I24LE3::from(3i8).inner(), I24::from(3i8).inner()); + assert_eq!(I24LE3::from(3i16).inner(), I24::from(3i16).inner()); + assert_eq!(I24LE3::from(3i32).inner(), I24::from(3i32).inner()); + assert_eq!(I24LE3::from(3u8).inner(), I24::from(3u8).inner()); + assert_eq!(I24LE3::from(3u16).inner(), I24::from(3u16).inner()); + assert_eq!(I24LE3::from(I20::new_unchecked(3)).inner(), 3); + assert_eq!(I24LE3::from(U20::new_unchecked(3)).inner(), 3); + assert_eq!(I24BE3::from(3i16).inner(), 3); + + // Unsigned packed: the same list `U24` gets, which has no signed sources and no `Neg`. + assert_eq!(U24LE3::from(3u8).inner(), U24::from(3u8).inner()); + assert_eq!(U24LE3::from(3u16).inner(), U24::from(3u16).inner()); + assert_eq!(U24BE3::from(3u16).inner(), 3); + assert_eq!(U24LE3::from(U20::new_unchecked(3)).inner(), 3); + + // `From` wraps for both, which is the one place they agree *because* both wrap rather + // than because neither overflows. + assert_eq!( + I24LE3::from(8_388_608i32).inner(), + I24::from(8_388_608i32).inner() + ); + + assert_eq!((-I24LE3::new_unchecked(5)).inner(), -5); + assert_eq!((-I24BE3::new_unchecked(5)).inner(), -5); + // `MIN` has no positive counterpart in 24 bits, so negating it wraps back to itself. + assert_eq!((-i24_le3::MIN).inner(), i24_le3::MIN.inner()); + + // `I48`/`U48` already accepted `I24`/`U24`; they now accept the packed types too. + assert_eq!(I48::from(I24LE3::new_unchecked(-7)).inner(), -7); + assert_eq!(I48::from(I24BE3::new_unchecked(-7)).inner(), -7); + assert_eq!(I48::from(U24LE3::new_unchecked(7)).inner(), 7); + assert_eq!(U48::from(U24LE3::new_unchecked(7)).inner(), 7); + assert_eq!(U48::from(U24BE3::new_unchecked(7)).inner(), 7); +} + +/// The signed packed types are `SignedSample`, as every other signed sample type is, and the +/// arithmetic operators behave exactly as the padded ones do. +/// +/// Between this, the `From` conversions and `Neg`, a call site can be retargeted from `I24` to +/// `I24LE3` by changing the type name — for everything except the bitwise and shift operators, +/// which the packed types deliberately do not have. +/// +/// The release-mode wrap is free here: truncating to three bytes *is* reduction modulo 2^24, so +/// it lands on the same value the padded types reach through `wrap_overflow_once`. +#[test] +fn arithmetic_matches_the_padded_types() { + fn assert_signed_sample() {} + assert_signed_sample::(); + assert_signed_sample::(); + // The unsigned pair is not, exactly as `U24` is not. + + // The same operations `tests/types.rs` checks on the padded types, with the same answers. + macro_rules! check_arithmetic { + ($T:ident) => {{ + let n = |v| $T::new_unchecked(v); + assert_eq!((n(8) + n(12)).inner(), 20); + assert_eq!((n(12) - n(4)).inner(), 8); + assert_eq!((n(2) * n(2)).inner(), 4); + assert_eq!((n(3) * n(3)).inner(), 9); + assert_eq!((n(5) * n(10)).inner(), 50); + assert_eq!((n(16) / n(8)).inner(), 2); + assert_eq!((n(8) % n(3)).inner(), 2); + }}; + } + check_arithmetic!(I24LE3); + check_arithmetic!(I24BE3); + check_arithmetic!(U24LE3); + check_arithmetic!(U24BE3); + + // Identical to the padded types for the same inputs. + assert_eq!( + (I24LE3::new_unchecked(8) + I24LE3::new_unchecked(12)).inner(), + (I24::new_unchecked(8) + I24::new_unchecked(12)).inner() + ); + assert_eq!( + (I24LE3::new_unchecked(5) * I24LE3::new_unchecked(10)).inner(), + (I24::new_unchecked(5) * I24::new_unchecked(10)).inner() + ); + + // The bitwise and shift operators are deliberately absent, so these do not compile: + // let _ = I24LE3::new_unchecked(1) & I24LE3::new_unchecked(1); + // let _ = !I24LE3::new_unchecked(1); +} + +/// Overflow panics in debug, matching `I24`'s `Add`. In release it wraps instead, which is +/// checked in `overflow_wraps_in_release`. +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "arithmetic operation overflowed")] +fn add_panics_on_overflow_in_debug() { + let _ = i24_le3::MAX + I24LE3::new_unchecked(1); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "arithmetic operation overflowed")] +fn sub_panics_on_underflow_in_debug() { + let _ = i24_le3::MIN - I24LE3::new_unchecked(1); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "arithmetic operation overflowed")] +fn mul_panics_on_overflow_in_debug() { + let _ = i24_le3::MAX * I24LE3::new_unchecked(2); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "arithmetic operation overflowed")] +fn unsigned_sub_panics_on_underflow_in_debug() { + let _ = u24_le3::MIN - U24LE3::new_unchecked(1); +} + +/// In release the packed types wrap, landing on the same value the padded types reach through +/// `wrap_overflow_once`. +#[cfg(not(debug_assertions))] +#[test] +fn overflow_wraps_in_release() { + assert_eq!( + (i24_le3::MAX + I24LE3::new_unchecked(1)).inner(), + -8_388_608 + ); + assert_eq!( + (i24_le3::MAX + I24LE3::new_unchecked(1)).inner(), + (dasp_sample::types::i24::MAX + I24::new_unchecked(1)).inner() + ); +} + +/// The whole reason these types exist: a buffer of packed 24-bit PCM must be reinterpretable as a +/// slice of samples, which requires a stride of exactly three bytes and no alignment requirement. +#[test] +fn layout() { + assert_eq!(mem::size_of::(), 3); + assert_eq!(mem::size_of::(), 3); + assert_eq!(mem::size_of::(), 3); + assert_eq!(mem::size_of::(), 3); + + assert_eq!(mem::align_of::(), 1); + assert_eq!(mem::align_of::(), 1); + assert_eq!(mem::align_of::(), 1); + assert_eq!(mem::align_of::(), 1); +} + +/// The byte order is a property of the type, not of the host, so these assertions hold on a +/// big-endian target too. +#[test] +fn byte_order() { + let n = 0x123456; + assert_eq!(I24LE3::new_unchecked(n).to_bytes(), [0x56, 0x34, 0x12]); + assert_eq!(I24BE3::new_unchecked(n).to_bytes(), [0x12, 0x34, 0x56]); + assert_eq!(U24LE3::new_unchecked(n).to_bytes(), [0x56, 0x34, 0x12]); + assert_eq!(U24BE3::new_unchecked(n).to_bytes(), [0x12, 0x34, 0x56]); + + assert_eq!(I24LE3::from_bytes([0x56, 0x34, 0x12]).inner(), 0x123456); + assert_eq!(I24BE3::from_bytes([0x12, 0x34, 0x56]).inner(), 0x123456); + assert_eq!(U24LE3::from_bytes([0x56, 0x34, 0x12]).inner(), 0x123456); + assert_eq!(U24BE3::from_bytes([0x12, 0x34, 0x56]).inner(), 0x123456); +} + +/// The signed types must sign extend from bit 23; the unsigned types must not. +#[test] +fn sign_extension() { + assert_eq!(I24LE3::from_bytes([0x00, 0x00, 0x80]).inner(), -8_388_608); + assert_eq!(I24BE3::from_bytes([0x80, 0x00, 0x00]).inner(), -8_388_608); + assert_eq!(I24LE3::from_bytes([0xFF, 0xFF, 0xFF]).inner(), -1); + assert_eq!(I24BE3::from_bytes([0xFF, 0xFF, 0xFF]).inner(), -1); + + assert_eq!(U24LE3::from_bytes([0x00, 0x00, 0x80]).inner(), 8_388_608); + assert_eq!(U24BE3::from_bytes([0x80, 0x00, 0x00]).inner(), 8_388_608); + assert_eq!(U24LE3::from_bytes([0xFF, 0xFF, 0xFF]).inner(), 16_777_215); + assert_eq!(U24BE3::from_bytes([0xFF, 0xFF, 0xFF]).inner(), 16_777_215); +} + +#[test] +fn consts() { + assert_eq!(i24_le3::MIN.inner(), -8_388_608); + assert_eq!(i24_le3::MAX.inner(), 8_388_607); + assert_eq!(i24_le3::EQUILIBRIUM.inner(), 0); + assert_eq!(i24_be3::MIN.inner(), -8_388_608); + assert_eq!(i24_be3::MAX.inner(), 8_388_607); + assert_eq!(i24_be3::EQUILIBRIUM.inner(), 0); + + assert_eq!(u24_le3::MIN.inner(), 0); + assert_eq!(u24_le3::MAX.inner(), 16_777_215); + assert_eq!(u24_le3::EQUILIBRIUM.inner(), 8_388_608); + assert_eq!(u24_be3::MIN.inner(), 0); + assert_eq!(u24_be3::MAX.inner(), 16_777_215); + assert_eq!(u24_be3::EQUILIBRIUM.inner(), 8_388_608); + + // The `Sample::EQUILIBRIUM` associated consts must agree with the module consts. + assert_eq!(::EQUILIBRIUM, i24_le3::EQUILIBRIUM); + assert_eq!(::EQUILIBRIUM, i24_be3::EQUILIBRIUM); + assert_eq!(::EQUILIBRIUM, u24_le3::EQUILIBRIUM); + assert_eq!(::EQUILIBRIUM, u24_be3::EQUILIBRIUM); + + assert_eq!(I24LE3::from(I24::EQUILIBRIUM), i24_le3::EQUILIBRIUM); + assert_eq!(U24BE3::from(U24::EQUILIBRIUM), u24_be3::EQUILIBRIUM); +} + +#[test] +fn new_checks_range() { + assert!(I24LE3::new(-8_388_609).is_none()); + assert!(I24LE3::new(8_388_608).is_none()); + assert_eq!(I24LE3::new(-8_388_608), Some(i24_le3::MIN)); + assert_eq!(I24LE3::new(8_388_607), Some(i24_le3::MAX)); + + assert!(U24BE3::new(-1).is_none()); + assert!(U24BE3::new(16_777_216).is_none()); + assert_eq!(U24BE3::new(0), Some(u24_be3::MIN)); + assert_eq!(U24BE3::new(16_777_215), Some(u24_be3::MAX)); +} + +/// A packed type has nowhere to put an out-of-range value, so `new_unchecked` wraps modulo 2^24. +/// This is the one place where a packed type does not agree with its padded equivalent, because +/// the padded one can park an out-of-range value in the spare byte of its container. +#[test] +fn out_of_range_wraps() { + assert_eq!(I24LE3::new_unchecked(8_388_608).inner(), -8_388_608); + assert_eq!(I24LE3::new_unchecked(-8_388_609).inner(), 8_388_607); + assert_eq!(I24BE3::new_unchecked(8_388_608).inner(), -8_388_608); + assert_eq!(U24LE3::new_unchecked(16_777_216).inner(), 0); + assert_eq!(U24BE3::new_unchecked(-1).inner(), 16_777_215); + + // The padded `From` wraps in exactly the same way... + assert_eq!(I24::from(8_388_608i32).inner(), -8_388_608); + // ...but the padded `new_unchecked` keeps the out-of-range value verbatim. + assert_eq!(I24::new_unchecked(8_388_608).inner(), 8_388_608); + + // Which is why converting a float from outside the documented -1.0 <= v < 1.0 range gives + // different answers for the padded and packed types. + assert_eq!(1.0f32.to_sample::().inner(), 8_388_608); + assert_eq!(1.0f32.to_sample::().inner(), -8_388_608); + assert_eq!(1.0f32.to_sample::().inner(), 16_777_216); + assert_eq!(1.0f32.to_sample::().inner(), 0); +} + +/// Asserts `$lo < $hi` numerically *and* that a derived `Ord` would have got it wrong. +/// +/// The second half is the point. Most orderings one might reach for happen to come out the same +/// either way, so an assertion like `MIN < MAX` passes under the very implementation it is +/// supposed to rule out. Requiring the byte-wise comparison to disagree makes each case prove it +/// is actually discriminating. +macro_rules! assert_derived_ord_would_fail { + ($lo:expr, $hi:expr) => {{ + assert!($lo < $hi, "numeric ordering is wrong"); + assert!( + $lo.to_bytes() > $hi.to_bytes(), + "this case is not discriminating: byte order agrees with numeric order here, so it \ + would pass under a derived Ord and proves nothing" + ); + }}; +} + +/// A derived `Ord` compares `[u8; 3]` lexicographically, which is wrong for three of the four +/// types: little-endian orders the *least* significant byte first, and the signed types read +/// their sign byte as unsigned. +#[test] +fn ordering_is_numeric_not_lexicographic() { + // Signed, either byte order: the sign byte read as unsigned puts negatives above positives. + assert_derived_ord_would_fail!(i24_le3::MIN, i24_le3::EQUILIBRIUM); + assert_derived_ord_would_fail!(i24_be3::MIN, i24_be3::EQUILIBRIUM); + assert_derived_ord_would_fail!(I24LE3::new_unchecked(-1), I24LE3::new_unchecked(1)); + assert_derived_ord_would_fail!(I24BE3::new_unchecked(-1), I24BE3::new_unchecked(1)); + + // Little-endian, either signedness: 1 is [01,00,00] and 256 is [00,01,00], so the smaller + // value sorts higher byte-wise even though both are positive. + assert_derived_ord_would_fail!(I24LE3::new_unchecked(1), I24LE3::new_unchecked(256)); + assert_derived_ord_would_fail!(U24LE3::new_unchecked(1), U24LE3::new_unchecked(256)); + + // `U24BE3` can have no discriminating case: for a big-endian *unsigned* value, + // lexicographic byte order and numeric order are the same relation. Its manual `Ord` is + // kept for consistency, not because a test could catch its absence. + assert!(u24_be3::MIN < u24_be3::EQUILIBRIUM); + assert!(u24_be3::EQUILIBRIUM < u24_be3::MAX); + assert!(U24BE3::new_unchecked(1) < U24BE3::new_unchecked(256)); + + // Ordering must agree with the derived `PartialEq`, which does compare bytes. + let a = I24LE3::new_unchecked(42); + assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal); + assert_eq!(a, I24LE3::new_unchecked(42)); +} + +/// Packing and unpacking is a pure re-arrangement of bits and so must be lossless for every value +/// in range, in both directions. +#[test] +fn round_trip_every_value() { + for v in -8_388_608..=8_388_607i32 { + assert_eq!(I24LE3::new_unchecked(v).inner(), v); + assert_eq!(I24BE3::new_unchecked(v).inner(), v); + assert_eq!(I24::from(I24LE3::new_unchecked(v)).inner(), v); + assert_eq!(I24::from(I24BE3::new_unchecked(v)).inner(), v); + } + for v in 0..=16_777_215i32 { + assert_eq!(U24LE3::new_unchecked(v).inner(), v); + assert_eq!(U24BE3::new_unchecked(v).inner(), v); + assert_eq!(U24::from(U24LE3::new_unchecked(v)).inner(), v); + assert_eq!(U24::from(U24BE3::new_unchecked(v)).inner(), v); + } +} + +/// Every one of the 2^24 byte patterns must decode to a distinct value and re-encode to itself, +/// which is what makes the byte-wise `PartialEq` consistent with the value-wise `Ord`. +#[test] +fn every_byte_pattern_survives_a_round_trip() { + for raw in 0..=0xFF_FFFFu32 { + let bytes = [raw as u8, (raw >> 8) as u8, (raw >> 16) as u8]; + assert_eq!( + I24LE3::new_unchecked(I24LE3::from_bytes(bytes).inner()).to_bytes(), + bytes + ); + assert_eq!( + I24BE3::new_unchecked(I24BE3::from_bytes(bytes).inner()).to_bytes(), + bytes + ); + assert_eq!( + U24LE3::new_unchecked(U24LE3::from_bytes(bytes).inner()).to_bytes(), + bytes + ); + assert_eq!( + U24BE3::new_unchecked(U24BE3::from_bytes(bytes).inner()).to_bytes(), + bytes + ); + } +} + +/// `add_amp` and `mul_amp` go via `Sample::Signed` and `Sample::Float`, so they must give the same +/// answers as the padded types. +#[test] +fn sample_ops() { + let half = I24LE3::from_sample(0.5f32); + assert_eq!(half.inner(), 4_194_304); + assert_eq!(half.mul_amp(0.5).inner(), 2_097_152); + assert_eq!(half.mul_amp(0.0), i24_le3::EQUILIBRIUM); + assert_eq!(half.add_amp(I24LE3::new_unchecked(-4_194_304)).inner(), 0); + + // The unsigned pair takes its `Signed` from `U24`, which is `i32`, so the amplitude offset + // is on the `i32` scale rather than the 24-bit one. The signed pair is its own `Signed`. + let half = U24BE3::from_sample(0.5f32); + assert_eq!(half.inner(), 12_582_912); + assert_eq!(half.mul_amp(0.0), u24_be3::EQUILIBRIUM); + assert_eq!(half.to_signed_sample(), 1_073_741_824i32); + assert_eq!(half.add_amp(-1_073_741_824), u24_be3::EQUILIBRIUM); + + assert_eq!( + I24LE3::from_sample(-1.0f32).to_signed_sample(), + i24_le3::MIN + ); + assert_eq!(I24LE3::from_sample(-1.0f32).to_float_sample(), -1.0f32); + assert_eq!(U24LE3::from_sample(-1.0f32).to_float_sample(), -1.0f32); +} + +/// Pins `Sample::Signed` and `Sample::Float` for all four types. +/// +/// Without this, two of the four were unasserted: changing `I24BE3`'s `Signed` to `i32` +/// compiled and passed the entire workspace suite, despite making `add_amp` read its argument +/// on a scale 256x off. The associated types are only observable through the scale `add_amp` +/// works on, so that is what is checked here — for every type, not just one. +#[test] +fn signed_and_float_associated_types() { + fn assert_signed, Expected>() {} + fn assert_float, Expected>() {} + + assert_signed::(); + assert_signed::(); + assert_signed::(); + assert_signed::(); + + assert_float::(); + assert_float::(); + assert_float::(); + assert_float::(); + + // Half scale offset back to equilibrium, in the units of that type's `Signed` -- the + // scale is exactly what a wrong associated type changes. + assert_eq!( + I24LE3::from_sample(0.5f32).add_amp(I24LE3::new_unchecked(-4_194_304)), + i24_le3::EQUILIBRIUM + ); + assert_eq!( + I24BE3::from_sample(0.5f32).add_amp(I24BE3::new_unchecked(-4_194_304)), + i24_be3::EQUILIBRIUM + ); + assert_eq!( + U24LE3::from_sample(0.5f32).add_amp(-1_073_741_824i32), + u24_le3::EQUILIBRIUM + ); + assert_eq!( + U24BE3::from_sample(0.5f32).add_amp(-1_073_741_824i32), + u24_be3::EQUILIBRIUM + ); + + assert_eq!(I24LE3::from_sample(0.5f32).mul_amp(0.5).inner(), 2_097_152); + assert_eq!(I24BE3::from_sample(0.5f32).mul_amp(0.5).inner(), 2_097_152); + assert_eq!( + U24LE3::from_sample(0.5f32).mul_amp(0.0), + u24_le3::EQUILIBRIUM + ); + assert_eq!( + U24BE3::from_sample(0.5f32).mul_amp(0.0), + u24_be3::EQUILIBRIUM + ); +} + +/// The payoff: a byte buffer straight off the wire, reinterpreted without a copy. +#[test] +fn reinterpret_byte_buffer() { + // Three S24_3LE samples: min, equilibrium, max. + let raw: [u8; 9] = [ + 0x00, 0x00, 0x80, // -8_388_608 + 0x00, 0x00, 0x00, // 0 + 0xFF, 0xFF, 0x7F, // 8_388_607 + ]; + + // Sound because `I24LE3` is `repr(transparent)` over `[u8; 3]`: every byte pattern is a valid + // value, the alignment is 1, and the length is an exact multiple of the stride. + assert_eq!(raw.len() % mem::size_of::(), 0); + let samples: &[I24LE3] = unsafe { + std::slice::from_raw_parts( + raw.as_ptr() as *const I24LE3, + raw.len() / mem::size_of::(), + ) + }; + + assert_eq!(samples.len(), 3); + assert_eq!(samples[0], i24_le3::MIN); + assert_eq!(samples[1], i24_le3::EQUILIBRIUM); + assert_eq!(samples[2], i24_le3::MAX); + + let floats: Vec = samples.iter().map(|s| s.to_sample::()).collect(); + assert_eq!(floats[0], -1.0); + assert_eq!(floats[1], 0.0); + assert!(floats[2] > 0.999_999 && floats[2] < 1.0); +}