Skip to main content

snapcast_server/encoder/
pcm.rs

1//! PCM encoder — passthrough with WAV header.
2
3use anyhow::Result;
4use snapcast_proto::SampleFormat;
5
6use super::{EncodedChunk, Encoder};
7use crate::AudioData;
8
9/// PCM passthrough encoder. Header is a 44-byte WAV header.
10pub struct PcmEncoder {
11    format: SampleFormat,
12    header: Vec<u8>,
13    warned: bool,
14}
15
16impl PcmEncoder {
17    /// Create a new PCM encoder for the given sample format.
18    pub fn new(format: SampleFormat) -> Self {
19        let header = build_wav_header(format);
20        Self {
21            format,
22            header,
23            warned: false,
24        }
25    }
26}
27
28impl Encoder for PcmEncoder {
29    fn name(&self) -> &str {
30        "pcm"
31    }
32
33    fn header(&self) -> &[u8] {
34        &self.header
35    }
36
37    fn encode(&mut self, input: &AudioData) -> Result<EncodedChunk> {
38        let data = match input {
39            AudioData::Pcm(pcm) => pcm.clone(),
40            AudioData::F32(samples) => {
41                if !self.warned {
42                    self.warned = true;
43                    tracing::warn!(
44                        codec = "pcm",
45                        bits = self.format.bits(),
46                        "F32 input → {}-bit PCM quantization",
47                        self.format.bits()
48                    );
49                }
50                super::f32_to_pcm(samples, self.format.bits())
51            }
52        };
53        Ok(EncodedChunk { data })
54    }
55}
56
57fn build_wav_header(fmt: SampleFormat) -> Vec<u8> {
58    let mut h = vec![0u8; 44];
59    let channels = fmt.channels();
60    let rate = fmt.rate();
61    let bits = fmt.bits();
62    let block_align = channels * bits.div_ceil(8);
63    let byte_rate = rate * block_align as u32;
64
65    h[0..4].copy_from_slice(b"RIFF");
66    h[4..8].copy_from_slice(&36u32.to_le_bytes());
67    h[8..12].copy_from_slice(b"WAVE");
68    h[12..16].copy_from_slice(b"fmt ");
69    h[16..20].copy_from_slice(&16u32.to_le_bytes());
70    h[20..22].copy_from_slice(&1u16.to_le_bytes()); // PCM
71    h[22..24].copy_from_slice(&channels.to_le_bytes());
72    h[24..28].copy_from_slice(&rate.to_le_bytes());
73    h[28..32].copy_from_slice(&byte_rate.to_le_bytes());
74    h[32..34].copy_from_slice(&block_align.to_le_bytes());
75    h[34..36].copy_from_slice(&bits.to_le_bytes());
76    h[36..40].copy_from_slice(b"data");
77    h[40..44].copy_from_slice(&0u32.to_le_bytes());
78    h
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn pcm_passthrough() {
87        let fmt = SampleFormat::new(48000, 16, 2);
88        let mut enc = PcmEncoder::new(fmt);
89        assert_eq!(enc.name(), "pcm");
90        assert_eq!(enc.header().len(), 44);
91        assert_eq!(&enc.header()[0..4], b"RIFF");
92
93        let pcm = vec![0u8; 960 * 4];
94        let result = enc.encode(&AudioData::Pcm(pcm.clone())).unwrap();
95        assert_eq!(result.data.len(), pcm.len());
96    }
97
98    #[test]
99    fn f32_converts_to_pcm() {
100        let fmt = SampleFormat::new(48000, 16, 2);
101        let mut enc = PcmEncoder::new(fmt);
102        let samples = vec![0.0f32; 960];
103        let result = enc.encode(&AudioData::F32(samples)).unwrap();
104        assert_eq!(result.data.len(), 960 * 2); // 16-bit = 2 bytes/sample
105    }
106}