use crate::error::{OxiError, OxiResult};
use crate::types::SampleFormat;
pub fn interleaved_to_planar(interleaved: &[f32], channels: usize) -> (Vec<f32>, Vec<f32>) {
let len = interleaved.len();
let samples = len.checked_div(channels).unwrap_or(0);
let mut planes: Vec<Vec<f32>> = (0..channels).map(|_| Vec::with_capacity(samples)).collect();
for frame in 0..samples {
for ch in 0..channels {
planes[ch].push(interleaved[frame * channels + ch]);
}
}
if channels < 2 {
let left = planes.into_iter().next().unwrap_or_default();
(left, Vec::new())
} else {
let mut iter = planes.into_iter();
let left = iter.next().unwrap_or_default();
let right = iter.next().unwrap_or_default();
(left, right)
}
}
#[must_use]
pub fn planar_to_interleaved(planes: &[Vec<f32>]) -> Vec<f32> {
if planes.is_empty() {
return Vec::new();
}
let sample_count = planes[0].len();
let channels = planes.len();
let mut out = Vec::with_capacity(sample_count * channels);
for s in 0..sample_count {
for plane in planes {
if s < plane.len() {
out.push(plane[s]);
}
}
}
out
}
#[inline]
#[must_use]
pub fn s16_to_f32(s: i16) -> f32 {
f32::from(s) / 32_768.0
}
#[inline]
#[must_use]
pub fn f32_to_s16(s: f32) -> i16 {
let clamped = s.clamp(-1.0, 1.0);
(clamped * 32_767.0).round() as i16
}
#[inline]
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn s32_to_f32(s: i32) -> f32 {
s as f32 / (i32::MAX as f32 + 1.0)
}
#[inline]
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn f32_to_s32(s: f32) -> i32 {
let clamped = s.clamp(-1.0, 1.0);
(clamped * i32::MAX as f32).round() as i32
}
#[inline]
#[must_use]
pub fn s24_bytes_to_f32(bytes: [u8; 3]) -> f32 {
let raw: i32 = i32::from(bytes[0]) | (i32::from(bytes[1]) << 8) | (i32::from(bytes[2]) << 16);
let signed = if raw & 0x00_80_00_00 != 0 {
raw | -0x01_00_00_00i32
} else {
raw
};
signed as f32 / 8_388_608.0 }
#[inline]
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn f32_to_s24_bytes(s: f32) -> [u8; 3] {
let clamped = s.clamp(-1.0, 1.0);
let raw = (clamped * 8_388_607.0).round() as i32;
[
(raw & 0xFF) as u8,
((raw >> 8) & 0xFF) as u8,
((raw >> 16) & 0xFF) as u8,
]
}
#[inline]
#[must_use]
pub fn u8_to_f32(s: u8) -> f32 {
(f32::from(s) - 128.0) / 128.0
}
#[inline]
#[must_use]
pub fn f32_to_u8(s: f32) -> u8 {
let clamped = s.clamp(-1.0, 1.0);
((clamped * 127.0) + 128.0).round() as u8
}
#[must_use]
pub fn s16_slice_to_f32(src: &[i16]) -> Vec<f32> {
src.iter().map(|&s| s16_to_f32(s)).collect()
}
#[must_use]
pub fn f32_slice_to_s16(src: &[f32]) -> Vec<i16> {
src.iter().map(|&s| f32_to_s16(s)).collect()
}
#[must_use]
pub fn s32_slice_to_f32(src: &[i32]) -> Vec<f32> {
src.iter().map(|&s| s32_to_f32(s)).collect()
}
#[must_use]
pub fn f32_slice_to_s32(src: &[f32]) -> Vec<i32> {
src.iter().map(|&s| f32_to_s32(s)).collect()
}
#[must_use]
pub fn u8_slice_to_f32(src: &[u8]) -> Vec<f32> {
src.iter().map(|&s| u8_to_f32(s)).collect()
}
#[must_use]
pub fn f32_slice_to_u8(src: &[f32]) -> Vec<u8> {
src.iter().map(|&s| f32_to_u8(s)).collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConversionPath {
pub src: SampleFormat,
pub dst: SampleFormat,
}
impl ConversionPath {
#[must_use]
pub fn is_identity(self) -> bool {
self.src == self.dst
}
#[must_use]
pub fn is_supported(self) -> bool {
if self.src == self.dst {
return true;
}
if self.src.to_packed() == self.dst.to_packed() {
return true;
}
let src_packed = self.src.to_packed();
let dst_packed = self.dst.to_packed();
matches!(
(src_packed, dst_packed),
(SampleFormat::S16, SampleFormat::F32)
| (SampleFormat::F32, SampleFormat::S16)
| (SampleFormat::S32, SampleFormat::F32)
| (SampleFormat::F32, SampleFormat::S32)
| (SampleFormat::U8, SampleFormat::F32)
| (SampleFormat::F32, SampleFormat::U8)
| (SampleFormat::S24, SampleFormat::F32)
| (SampleFormat::F32, SampleFormat::S24)
| (SampleFormat::F64, SampleFormat::F32)
| (SampleFormat::F32, SampleFormat::F64)
)
}
pub fn validate(self) -> OxiResult<()> {
if self.is_supported() {
Ok(())
} else {
Err(OxiError::Unsupported(format!(
"sample format conversion {src} → {dst} is not supported",
src = self.src,
dst = self.dst,
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-5;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < EPS
}
#[test]
fn test_s16_to_f32_zero() {
assert!(approx(s16_to_f32(0), 0.0));
}
#[test]
fn test_s16_to_f32_max() {
let v = s16_to_f32(i16::MAX);
assert!(v > 0.9999 && v <= 1.0, "got {v}");
}
#[test]
fn test_s16_to_f32_min() {
let v = s16_to_f32(i16::MIN);
assert!(v >= -1.0 && v < -0.9999, "got {v}");
}
#[test]
fn test_f32_s16_roundtrip() {
for x in [-1.0_f32, -0.5, 0.0, 0.5, 0.9999] {
let encoded = f32_to_s16(x);
let decoded = s16_to_f32(encoded);
assert!(
(decoded - x).abs() < 4e-5,
"x={x} encoded={encoded} decoded={decoded}"
);
}
}
#[test]
fn test_f32_to_s16_clamp_high() {
let out = f32_to_s16(1.5);
assert_eq!(out, i16::MAX);
}
#[test]
fn test_f32_to_s16_clamp_low() {
let out = f32_to_s16(-1.5);
assert_eq!(out, i16::MIN + 1); }
#[test]
fn test_u8_to_f32_midpoint() {
assert!(approx(u8_to_f32(128), 0.0));
}
#[test]
fn test_u8_f32_roundtrip() {
for &b in &[0u8, 64, 128, 192, 255] {
let f = u8_to_f32(b);
let back = f32_to_u8(f);
assert!((back as i16 - b as i16).abs() <= 1, "b={b} back={back}");
}
}
#[test]
fn test_s32_to_f32_zero() {
assert!(approx(s32_to_f32(0), 0.0));
}
#[test]
fn test_s32_f32_roundtrip() {
let orig = i32::MAX / 2;
let f = s32_to_f32(orig);
let back = f32_to_s32(f);
let err = (back as i64 - orig as i64).unsigned_abs();
assert!(err < 500, "orig={orig} back={back} err={err}");
}
#[test]
fn test_s24_to_f32_zero() {
assert!(approx(s24_bytes_to_f32([0, 0, 0]), 0.0));
}
#[test]
fn test_s24_f32_roundtrip_positive() {
let orig = 0.5_f32;
let bytes = f32_to_s24_bytes(orig);
let back = s24_bytes_to_f32(bytes);
assert!((back - orig).abs() < 1e-5, "back={back}");
}
#[test]
fn test_s24_f32_roundtrip_negative() {
let orig = -0.75_f32;
let bytes = f32_to_s24_bytes(orig);
let back = s24_bytes_to_f32(bytes);
assert!((back - orig).abs() < 1e-5, "back={back}");
}
#[test]
fn test_bulk_s16_roundtrip() {
let orig: Vec<i16> = (-100..=100).collect();
let f = s16_slice_to_f32(&orig);
let back = f32_slice_to_s16(&f);
for (o, b) in orig.iter().zip(back.iter()) {
assert_eq!(o, b, "sample mismatch at {o}");
}
}
#[test]
fn test_stereo_layout_roundtrip() {
let interleaved: Vec<f32> = (0..8).map(|i| i as f32 * 0.1).collect();
let (left, right) = interleaved_to_planar(&interleaved, 2);
assert_eq!(left.len(), 4);
assert_eq!(right.len(), 4);
let back = planar_to_interleaved(&[left, right]);
for (a, b) in interleaved.iter().zip(back.iter()) {
assert!((a - b).abs() < 1e-6, "a={a} b={b}");
}
}
#[test]
fn test_mono_layout() {
let mono = vec![0.1_f32, 0.2, 0.3];
let (left, right) = interleaved_to_planar(&mono, 1);
assert_eq!(left, mono);
assert!(right.is_empty());
}
#[test]
fn test_planar_to_interleaved_empty() {
let out = planar_to_interleaved(&[]);
assert!(out.is_empty());
}
#[test]
fn test_conversion_path_identity() {
let path = ConversionPath {
src: SampleFormat::F32,
dst: SampleFormat::F32,
};
assert!(path.is_identity());
assert!(path.is_supported());
}
#[test]
fn test_conversion_path_packed_planar() {
let path = ConversionPath {
src: SampleFormat::F32,
dst: SampleFormat::F32p,
};
assert!(!path.is_identity());
assert!(path.is_supported());
assert!(path.validate().is_ok());
}
#[test]
fn test_conversion_path_s16_to_f32() {
let path = ConversionPath {
src: SampleFormat::S16,
dst: SampleFormat::F32,
};
assert!(path.is_supported());
assert!(path.validate().is_ok());
}
#[test]
fn test_conversion_path_unsupported() {
let path = ConversionPath {
src: SampleFormat::S16,
dst: SampleFormat::S32,
};
assert!(!path.is_supported());
assert!(path.validate().is_err());
}
#[test]
fn test_f32_to_u8_clamp() {
assert_eq!(f32_to_u8(1.5), 255);
assert_eq!(f32_to_u8(-1.5), 1); }
#[test]
fn test_bulk_s32_roundtrip() {
let orig: Vec<i32> = (0..20).map(|i| i * 100_000).collect();
let f = s32_slice_to_f32(&orig);
let back = f32_slice_to_s32(&f);
for (o, b) in orig.iter().zip(back.iter()) {
let err = ((*b as i64) - (*o as i64)).unsigned_abs();
assert!(err < 1000, "orig={o} back={b} err={err}");
}
}
#[test]
fn test_bulk_u8_roundtrip() {
let orig: Vec<u8> = (0u8..=255u8).collect();
let f = u8_slice_to_f32(&orig);
let back = f32_slice_to_u8(&f);
for (o, b) in orig.iter().zip(back.iter()) {
assert!((*b as i16 - *o as i16).unsigned_abs() <= 1, "o={o} b={b}");
}
}
#[test]
fn test_s24_max_positive() {
let bytes: [u8; 3] = [0xFF, 0xFF, 0x7F];
let v = s24_bytes_to_f32(bytes);
assert!(v > 0.999, "v={v}");
}
#[test]
fn test_conversion_path_s24_to_f32() {
let path = ConversionPath {
src: SampleFormat::S24,
dst: SampleFormat::F32,
};
assert!(path.is_supported());
}
}