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