librespot_playback/
convert.rs

1use crate::dither::{Ditherer, DithererBuilder};
2use zerocopy::{Immutable, IntoBytes};
3
4#[derive(Immutable, IntoBytes, Copy, Clone, Debug)]
5#[allow(non_camel_case_types)]
6#[repr(transparent)]
7pub struct i24([u8; 3]);
8impl i24 {
9    fn from_s24(sample: i32) -> Self {
10        // trim the padding in the most significant byte
11        #[allow(unused_variables)]
12        let [a, b, c, d] = sample.to_ne_bytes();
13        #[cfg(target_endian = "little")]
14        return Self([a, b, c]);
15        #[cfg(target_endian = "big")]
16        return Self([b, c, d]);
17    }
18}
19
20pub struct Converter {
21    ditherer: Option<Box<dyn Ditherer>>,
22}
23
24impl Converter {
25    pub fn new(dither_config: Option<DithererBuilder>) -> Self {
26        match dither_config {
27            Some(ditherer_builder) => {
28                let ditherer = (ditherer_builder)();
29                info!("Converting with ditherer: {}", ditherer.name());
30                Self {
31                    ditherer: Some(ditherer),
32                }
33            }
34            None => Self { ditherer: None },
35        }
36    }
37
38    /// Base bit positions for PCM format scaling. These represent the position
39    /// of the most significant bit in each format's full-scale representation.
40    /// For signed integers in two's complement, full scale is 2^(bits-1).
41    const SHIFT_S16: u8 = 15; // 16-bit: 2^15 = 32768
42    const SHIFT_S24: u8 = 23; // 24-bit: 2^23 = 8388608  
43    const SHIFT_S32: u8 = 31; // 32-bit: 2^31 = 2147483648
44
45    /// Additional bit shifts needed to scale from 16-bit to higher bit depths.
46    /// These are the differences between the base shift amounts above.
47    const SHIFT_16_TO_24: u8 = Self::SHIFT_S24 - Self::SHIFT_S16; // 23 - 15 = 8
48    const SHIFT_16_TO_32: u8 = Self::SHIFT_S32 - Self::SHIFT_S16; // 31 - 15 = 16
49
50    /// Pre-calculated scale factor for 24-bit clamping bounds
51    const SCALE_S24: f64 = (1_u64 << Self::SHIFT_S24) as f64;
52
53    /// Scale audio samples with optimal dithering strategy for Spotify's 16-bit source material.
54    ///
55    /// Since Spotify audio is always 16-bit depth, this function:
56    /// 1. When dithering: applies noise at 16-bit level, preserves fractional precision,
57    ///    then scales to target format and rounds once at the end
58    /// 2. When not dithering: scales directly from normalized float to target format
59    ///
60    /// The `shift` parameter specifies how many extra bits to shift beyond
61    /// the base 16-bit scaling (0 for 16-bit, 8 for 24-bit, 16 for 32-bit).
62    #[inline]
63    pub fn scale(&mut self, sample: f64, shift: u8) -> f64 {
64        match self.ditherer.as_mut() {
65            Some(d) => {
66                // With dithering: Apply noise at 16-bit level to address original quantization,
67                // then scale up to target format while preserving sub-LSB information
68                let dithered_16bit = sample * (1_u64 << Self::SHIFT_S16) as f64 + d.noise();
69                let scaled = dithered_16bit * (1_u64 << shift) as f64;
70                scaled.round()
71            }
72            None => {
73                // No dithering: Scale directly from normalized float to target format
74                // using a single bit shift operation (base 16-bit shift + additional shift)
75                let total_shift = Self::SHIFT_S16 + shift;
76                (sample * (1_u64 << total_shift) as f64).round()
77            }
78        }
79    }
80
81    /// Clamping scale specifically for 24-bit output to prevent MSB overflow.
82    /// Only used for S24 formats where samples are packed in 32-bit words.
83    /// Ensures the most significant byte is zero to prevent overflow during dithering.
84    #[inline]
85    pub fn clamping_scale_s24(&mut self, sample: f64) -> f64 {
86        let int_value = self.scale(sample, Self::SHIFT_16_TO_24);
87
88        // In two's complement, there are more negative than positive values.
89        let min = -Self::SCALE_S24;
90        let max = Self::SCALE_S24 - 1.0;
91
92        int_value.clamp(min, max)
93    }
94
95    #[inline]
96    pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {
97        samples.iter().map(|sample| *sample as f32).collect()
98    }
99
100    #[inline]
101    pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec<i32> {
102        samples
103            .iter()
104            .map(|sample| self.scale(*sample, Self::SHIFT_16_TO_32) as i32)
105            .collect()
106    }
107
108    /// S24 is 24-bit PCM packed in an upper 32-bit word
109    #[inline]
110    pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
111        samples
112            .iter()
113            .map(|sample| self.clamping_scale_s24(*sample) as i32)
114            .collect()
115    }
116
117    /// S24_3 is 24-bit PCM in a 3-byte array
118    #[inline]
119    pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
120        samples
121            .iter()
122            .map(|sample| i24::from_s24(self.clamping_scale_s24(*sample) as i32))
123            .collect()
124    }
125
126    #[inline]
127    pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
128        samples
129            .iter()
130            .map(|sample| self.scale(*sample, 0) as i16)
131            .collect()
132    }
133}