Skip to main content

snapcast_client/decoder/
f32lz4.rs

1//! F32 LZ4 decoder — lossless compressed f32 audio.
2//!
3//! Wire format:
4//! - Header: "F32L" + sample_rate(u32) + channels(u16) + bits(u16)
5//! - Chunks: LZ4-compressed f32 samples
6//!
7//! Output: raw f32 bytes (4 bytes per sample, little-endian).
8//! The audio_pump converts these to AudioFrame without i16 quantization.
9
10use anyhow::{Result, bail};
11use snapcast_proto::SampleFormat;
12use snapcast_proto::message::codec_header::CodecHeader;
13
14use crate::decoder::Decoder;
15
16const MAGIC: &[u8; 4] = b"F32L";
17
18/// F32 LZ4 decoder.
19pub struct F32Lz4Decoder {
20    sample_format: SampleFormat,
21    #[cfg(feature = "encryption")]
22    decryptor: Option<crate::crypto::ChunkDecryptor>,
23    #[cfg(feature = "encryption")]
24    encryption_psk: Option<String>,
25}
26
27impl Decoder for F32Lz4Decoder {
28    fn set_header(&mut self, header: &CodecHeader) -> Result<SampleFormat> {
29        if header.payload.len() < 12 {
30            bail!("F32LZ4 header too small");
31        }
32        if &header.payload[..4] != MAGIC {
33            bail!("not an F32LZ4 header");
34        }
35        let rate = u32::from_le_bytes(header.payload[4..8].try_into().unwrap());
36        let channels = u16::from_le_bytes(header.payload[8..10].try_into().unwrap());
37        let bits = u16::from_le_bytes(header.payload[10..12].try_into().unwrap());
38        self.sample_format = SampleFormat::new(rate, bits, channels);
39
40        // Check for encryption marker after the 12-byte base header
41        #[cfg(feature = "encryption")]
42        if header.payload.len() >= 32 && &header.payload[12..16] == b"ENC\0" {
43            let salt = &header.payload[16..32];
44            if let Some(ref psk) = self.encryption_psk {
45                self.decryptor = Some(crate::crypto::ChunkDecryptor::new(psk, salt));
46                tracing::info!("F32LZ4 decryption enabled");
47            } else {
48                bail!("Server requires encryption but no encryption_psk configured");
49            }
50        }
51
52        tracing::info!(rate, channels, bits, "F32LZ4 decoder initialized");
53        Ok(self.sample_format)
54    }
55
56    fn decode(&mut self, data: &mut Vec<u8>) -> Result<bool> {
57        if data.is_empty() {
58            return Ok(false);
59        }
60
61        // Decrypt if encryption is active
62        #[cfg(feature = "encryption")]
63        if let Some(ref dec) = self.decryptor {
64            match dec.decrypt(data) {
65                Ok(decrypted) => *data = decrypted,
66                Err(e) => {
67                    tracing::warn!(error = %e, "F32LZ4 decryption failed");
68                    return Ok(false);
69                }
70            }
71        }
72
73        match lz4_flex::decompress_size_prepended(data) {
74            Ok(decompressed) => {
75                tracing::trace!(
76                    compressed = data.len(),
77                    decompressed = decompressed.len(),
78                    "F32LZ4 decoded"
79                );
80                *data = decompressed;
81                Ok(true)
82            }
83            Err(e) => {
84                tracing::warn!(error = %e, "F32LZ4 decompress failed");
85                Ok(false)
86            }
87        }
88    }
89}
90
91/// Create an F32Lz4Decoder.
92#[cfg(feature = "encryption")]
93pub fn create(encryption_psk: Option<&str>) -> F32Lz4Decoder {
94    F32Lz4Decoder {
95        sample_format: SampleFormat::default(),
96        decryptor: None,
97        encryption_psk: encryption_psk.map(String::from),
98    }
99}
100
101/// Create an F32Lz4Decoder.
102#[cfg(not(feature = "encryption"))]
103pub fn create() -> F32Lz4Decoder {
104    F32Lz4Decoder {
105        sample_format: SampleFormat::default(),
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn roundtrip() {
115        let _fmt = SampleFormat::new(48000, 16, 2);
116
117        // Encode
118        let f32_samples: Vec<f32> = (0..960).map(|i| (i as f32 / 960.0) * 2.0 - 1.0).collect();
119        let f32_bytes: Vec<u8> = f32_samples.iter().flat_map(|s| s.to_le_bytes()).collect();
120        let compressed = lz4_flex::compress_prepend_size(&f32_bytes);
121
122        // Decode
123        #[cfg(feature = "encryption")]
124        let mut dec = create(None);
125        #[cfg(not(feature = "encryption"))]
126        let mut dec = create();
127        let header = CodecHeader {
128            codec: "f32lz4".into(),
129            payload: {
130                let mut h = Vec::new();
131                h.extend_from_slice(MAGIC);
132                h.extend_from_slice(&48000u32.to_le_bytes());
133                h.extend_from_slice(&2u16.to_le_bytes());
134                h.extend_from_slice(&32u16.to_le_bytes());
135                h
136            },
137        };
138        let sf = dec.set_header(&header).unwrap();
139        assert_eq!(sf.rate(), 48000);
140
141        let mut data = compressed;
142        assert!(dec.decode(&mut data).unwrap());
143        assert_eq!(data, f32_bytes);
144    }
145}