Skip to main content

snapcast_server/encoder/
flac.rs

1//! FLAC encoder using libflac-sys directly (raw FFI with streaming callback).
2//!
3//! Uses the raw C callback API to distinguish header writes from frame writes,
4//! matching the C++ server's FlacEncoder exactly.
5
6use std::ffi::c_void;
7
8use anyhow::{Result, bail};
9use libflac_sys::*;
10use snapcast_proto::SampleFormat;
11
12use super::{EncodedChunk, Encoder};
13
14/// Client data passed to the libflac write callback.
15struct CallbackData {
16    header: Vec<u8>,
17    frame_buf: Vec<u8>,
18    encoded_samples: u32,
19}
20
21/// Streaming FLAC encoder using raw libflac FFI.
22pub struct FlacEncoder {
23    format: SampleFormat,
24    encoder: *mut FLAC__StreamEncoder,
25    callback_data: *mut CallbackData,
26}
27
28#[allow(unsafe_code)]
29unsafe extern "C" fn write_callback(
30    _encoder: *const FLAC__StreamEncoder,
31    buffer: *const FLAC__byte,
32    bytes: usize,
33    samples: u32,
34    current_frame: u32,
35    client_data: *mut c_void,
36) -> FLAC__StreamEncoderWriteStatus {
37    let data = unsafe { &mut *(client_data as *mut CallbackData) };
38    let slice = unsafe { std::slice::from_raw_parts(buffer, bytes) };
39
40    if current_frame == 0 && samples == 0 {
41        // Header/metadata — goes to header buffer
42        data.header.extend_from_slice(slice);
43    } else {
44        // Frame data — goes to frame buffer
45        data.frame_buf.extend_from_slice(slice);
46        data.encoded_samples += samples;
47    }
48
49    0 // FLAC__STREAM_ENCODER_WRITE_STATUS_OK
50}
51
52#[allow(unsafe_code)]
53impl FlacEncoder {
54    /// Create a new FLAC encoder. Options: compression level 0-8 (default: 2).
55    pub fn new(format: SampleFormat, options: &str) -> Result<Self> {
56        let level: u32 = if options.is_empty() {
57            2
58        } else {
59            options
60                .parse()
61                .map_err(|_| anyhow::anyhow!("invalid FLAC compression level: {options}"))?
62        };
63        if level > 8 {
64            bail!("FLAC compression level must be 0-8, got {level}");
65        }
66
67        unsafe {
68            let encoder = FLAC__stream_encoder_new();
69            if encoder.is_null() {
70                bail!("failed to create FLAC encoder");
71            }
72
73            FLAC__stream_encoder_set_verify(encoder, 1);
74            FLAC__stream_encoder_set_compression_level(encoder, level);
75            FLAC__stream_encoder_set_channels(encoder, format.channels() as u32);
76            FLAC__stream_encoder_set_bits_per_sample(encoder, format.bits() as u32);
77            FLAC__stream_encoder_set_sample_rate(encoder, format.rate());
78
79            let callback_data = Box::into_raw(Box::new(CallbackData {
80                header: Vec::new(),
81                frame_buf: Vec::new(),
82                encoded_samples: 0,
83            }));
84
85            let status = FLAC__stream_encoder_init_stream(
86                encoder,
87                Some(write_callback),
88                None, // seek
89                None, // tell
90                None, // metadata
91                callback_data as *mut c_void,
92            );
93
94            if status != 0 {
95                FLAC__stream_encoder_delete(encoder);
96                let _ = Box::from_raw(callback_data);
97                bail!("FLAC encoder init failed with status {status}");
98            }
99
100            tracing::info!(
101                compression_level = level,
102                header_bytes = (*callback_data).header.len(),
103                "FLAC streaming encoder initialized"
104            );
105
106            Ok(Self {
107                format,
108                encoder,
109                callback_data,
110            })
111        }
112    }
113}
114
115#[allow(unsafe_code)]
116impl Encoder for FlacEncoder {
117    fn name(&self) -> &str {
118        "flac"
119    }
120
121    fn header(&self) -> &[u8] {
122        unsafe { &(*self.callback_data).header }
123    }
124
125    fn encode(&mut self, pcm: &[u8]) -> Result<EncodedChunk> {
126        let sample_size = self.format.sample_size() as usize;
127        let channels = self.format.channels() as usize;
128        let samples = pcm.len() / sample_size;
129        let frames = samples / channels;
130
131        // Convert to interleaved i32 (libflac process_interleaved format)
132        let mut i32_buf: Vec<i32> = Vec::with_capacity(samples);
133        match sample_size {
134            2 => {
135                for chunk in pcm.chunks_exact(2) {
136                    i32_buf.push(i16::from_le_bytes([chunk[0], chunk[1]]) as i32);
137                }
138            }
139            4 => {
140                for chunk in pcm.chunks_exact(4) {
141                    i32_buf.push(i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
142                }
143            }
144            _ => bail!("unsupported sample size: {sample_size}"),
145        }
146
147        unsafe {
148            // Clear frame buffer
149            (*self.callback_data).frame_buf.clear();
150            (*self.callback_data).encoded_samples = 0;
151
152            let ok = FLAC__stream_encoder_process_interleaved(
153                self.encoder,
154                i32_buf.as_ptr(),
155                frames as u32,
156            );
157
158            if ok == 0 {
159                bail!("FLAC encode failed");
160            }
161
162            let data = (*self.callback_data).frame_buf.clone();
163            let duration_ms = self.format.frames_to_ms(frames);
164            Ok(EncodedChunk { data, duration_ms })
165        }
166    }
167}
168
169#[allow(unsafe_code)]
170impl Drop for FlacEncoder {
171    fn drop(&mut self) {
172        unsafe {
173            if !self.encoder.is_null() {
174                FLAC__stream_encoder_finish(self.encoder);
175                FLAC__stream_encoder_delete(self.encoder);
176            }
177            if !self.callback_data.is_null() {
178                let _ = Box::from_raw(self.callback_data);
179            }
180        }
181    }
182}
183
184#[allow(unsafe_code)]
185unsafe impl Send for FlacEncoder {}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn header_starts_with_flac() {
193        let fmt = SampleFormat::new(48000, 16, 2);
194        let enc = FlacEncoder::new(fmt, "").unwrap();
195        assert!(!enc.header().is_empty());
196        assert_eq!(&enc.header()[..4], b"fLaC");
197    }
198
199    #[test]
200    fn encode_produces_frames() {
201        let fmt = SampleFormat::new(48000, 16, 2);
202        let mut enc = FlacEncoder::new(fmt, "").unwrap();
203        let mut total = 0;
204        for _ in 0..10 {
205            let pcm = vec![0u8; 960 * 4]; // 20ms chunks
206            let result = enc.encode(&pcm).unwrap();
207            if !result.data.is_empty() {
208                // FLAC frame sync code
209                assert_eq!(result.data[0], 0xFF);
210                assert!(result.data[1] == 0xF8 || result.data[1] == 0xF9);
211            }
212            total += result.data.len();
213        }
214        assert!(total > 0, "expected FLAC output");
215    }
216
217    #[test]
218    fn persistent_across_chunks() {
219        let fmt = SampleFormat::new(48000, 16, 2);
220        let mut enc = FlacEncoder::new(fmt, "").unwrap();
221        // Encode 100 chunks — should not crash or produce header data
222        for _ in 0..100 {
223            let pcm = vec![42u8; 960 * 4];
224            let result = enc.encode(&pcm).unwrap();
225            // Frame data only — no fLaC header bytes
226            if result.data.len() >= 4 {
227                assert_ne!(&result.data[..4], b"fLaC", "got header in frame data");
228            }
229        }
230    }
231}