snapcast_server/encoder/
flac.rs1use 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
15struct CallbackData {
17 header: Vec<u8>,
18 frame_buf: Vec<u8>,
19 encoded_samples: u32,
20}
21
22pub 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 data.header.extend_from_slice(slice);
45 } else {
46 data.frame_buf.extend_from_slice(slice);
48 data.encoded_samples += samples;
49 }
50
51 0 }
53
54#[allow(unsafe_code)]
55impl FlacEncoder {
56 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, None, None, 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 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 (*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]; let result = enc.encode(&AudioData::Pcm(pcm)).unwrap();
224 if !result.data.is_empty() {
225 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}