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};
13
14struct CallbackData {
16 header: Vec<u8>,
17 frame_buf: Vec<u8>,
18 encoded_samples: u32,
19}
20
21pub 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 data.header.extend_from_slice(slice);
43 } else {
44 data.frame_buf.extend_from_slice(slice);
46 data.encoded_samples += samples;
47 }
48
49 0 }
51
52#[allow(unsafe_code)]
53impl FlacEncoder {
54 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, None, None, 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 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 (*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]; let result = enc.encode(&pcm).unwrap();
207 if !result.data.is_empty() {
208 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 for _ in 0..100 {
223 let pcm = vec![42u8; 960 * 4];
224 let result = enc.encode(&pcm).unwrap();
225 if result.data.len() >= 4 {
227 assert_ne!(&result.data[..4], b"fLaC", "got header in frame data");
228 }
229 }
230 }
231}