Skip to main content

libflo_audio/lossy/
encoder.rs

1use super::mdct::{
2    BlockSize, Mdct, WindowType, LONG_BLOCK_SIZE, SHORTS_PER_GROUP, SHORT_BLOCK_SIZE,
3    SHORT_GROUP_OFFSET,
4};
5use super::psychoacoustic::{PsychoacousticModel, NUM_BARK_BANDS};
6use crate::core::{ChannelData, Frame, FrameType, ResidualEncoding, I16_MAX_F32, I16_MIN_F32};
7use alloc::vec;
8use alloc::vec::Vec;
9
10/// Loudness spike threshold (energy ratio) between adjacent short windows that
11/// marks a transient.
12const ATTACK_RATIO: f32 = 8.0;
13/// Relative floor for the transient detector baseline: short-window energy must
14/// clear a fraction of the frame's average energy to count as an attack.
15const DETECTOR_FLOOR_FRACTION: f32 = 0.05;
16/// Hop size in samples between frames (LONG_BLOCK_SIZE/2, the frame cadence)
17const HOP_SIZE: usize = LONG_BLOCK_SIZE / 2;
18
19/// Transform lossy encoder
20pub struct TransformEncoder {
21    /// Sample rate
22    sample_rate: u32,
23    /// Number of channels
24    channels: u8,
25    /// MDCT processor
26    mdct: Mdct,
27    /// Psychoacoustic model for long blocks (one per channel)
28    psy_models: Vec<PsychoacousticModel>,
29    /// Psychoacoustic model for short blocks (one per channel)
30    short_psy_models: Vec<PsychoacousticModel>,
31    /// Quality setting (0.0 = lowest, 1.0 = transparent)
32    quality: f32,
33}
34
35/// Encoded frame data
36#[derive(Debug, Clone)]
37pub struct TransformFrame {
38    /// Quantized MDCT coefficients per channel (as i16)
39    pub coefficients: Vec<Vec<i16>>,
40    /// Scale factors per Bark band per channel
41    pub scale_factors: Vec<Vec<f32>>,
42    /// Block size used
43    pub block_size: BlockSize,
44    /// Number of samples this frame represents (after overlap-add)
45    pub num_samples: usize,
46}
47
48impl TransformEncoder {
49    /// Create a new transform encoder
50    pub fn new(sample_rate: u32, channels: u8, quality: f32) -> Self {
51        let mdct = Mdct::new(channels as usize, WindowType::Vorbis);
52        let psy_models: Vec<_> = (0..channels)
53            .map(|_| PsychoacousticModel::new(sample_rate, LONG_BLOCK_SIZE))
54            .collect();
55        let short_psy_models: Vec<_> = (0..channels)
56            .map(|_| PsychoacousticModel::new(sample_rate, SHORT_BLOCK_SIZE))
57            .collect();
58
59        Self {
60            sample_rate,
61            channels,
62            mdct,
63            psy_models,
64            short_psy_models,
65            quality: quality.clamp(0.0, 1.0),
66        }
67    }
68
69    /// Set quality (0.0-1.0)
70    pub fn set_quality(&mut self, quality: f32) {
71        self.quality = quality.clamp(0.0, 1.0);
72    }
73
74    /// Encode a frame of audio
75    /// Input: interleaved samples covering one frame grid (block_size * channels)
76    /// Returns encoded frame
77    pub fn encode_frame(&mut self, samples: &[f32], block_size: BlockSize) -> TransformFrame {
78        let frame_input = block_size.frame_input_size();
79
80        // Deinterleave channels
81        let mut channel_data: Vec<Vec<f32>> = (0..self.channels as usize)
82            .map(|_| Vec::with_capacity(samples.len() / self.channels as usize))
83            .collect();
84
85        for (i, &s) in samples.iter().enumerate() {
86            channel_data[i % self.channels as usize].push(s);
87        }
88
89        let mut all_coefficients = Vec::with_capacity(self.channels as usize);
90        let mut all_scale_factors = Vec::with_capacity(self.channels as usize);
91
92        for (ch, data) in channel_data.iter().enumerate() {
93            // Pad to the frame grid if needed
94            let mut frame_data = data.clone();
95            if frame_data.len() < frame_input {
96                frame_data.resize(frame_input, 0.0);
97            }
98
99            // MDCT transform
100            let coeffs = self.mdct.forward(&frame_data, block_size);
101
102            // Quantize based on perceptual importance
103            let (quantized, scale_factors) = if block_size.is_short_group() {
104                self.quantize_short_group(&coeffs, ch)
105            } else {
106                self.quantize_long_block(&coeffs, ch)
107            };
108
109            all_coefficients.push(quantized);
110            all_scale_factors.push(scale_factors);
111        }
112
113        TransformFrame {
114            coefficients: all_coefficients,
115            scale_factors: all_scale_factors,
116            block_size,
117            num_samples: block_size.frame_coefficients(),
118        }
119    }
120
121    /// Quantize a long-block coefficient set (Long/Start/Stop) per channel
122    fn quantize_long_block(&mut self, coeffs: &[f32], ch: usize) -> (Vec<i16>, Vec<f32>) {
123        // Psychoacoustic analysis
124        let smr = self.psy_models[ch].calculate_smr(coeffs);
125
126        // Bit budget grows with quality: 1..16 nominal bits per coefficient.
127        let bits_per_coeff = 1.0 + self.quality * 15.0;
128        let total_bits = (coeffs.len() as f32 * bits_per_coeff) as usize;
129        let allocation = self.psy_models[ch].allocate_bits(&smr, total_bits);
130
131        self.quantize_coefficients(coeffs, &smr, &allocation)
132    }
133
134    /// Quantize a short-group frame (8 short blocks) with a shared scale-factor set.
135    fn quantize_short_group(&mut self, coeffs: &[f32], ch: usize) -> (Vec<i16>, Vec<f32>) {
136        let coeffs_per_window = SHORT_BLOCK_SIZE / 2;
137        let bits_per_coeff = 1.0 + self.quality * 15.0;
138        let per_window_bits = (coeffs_per_window as f32 * bits_per_coeff) as usize;
139
140        let mut smr = Vec::with_capacity(coeffs.len());
141        let mut allocation = Vec::with_capacity(coeffs.len());
142        for w in 0..SHORTS_PER_GROUP {
143            let window_coeffs = &coeffs[w * coeffs_per_window..(w + 1) * coeffs_per_window];
144            let model = &mut self.short_psy_models[ch];
145            let window_smr = model.calculate_smr(window_coeffs);
146            smr.extend(window_smr.iter());
147            let window_allocation = model.allocate_bits(&window_smr, per_window_bits);
148            allocation.extend(window_allocation.iter());
149        }
150
151        let freq_resolution = self.sample_rate as f32 / SHORT_BLOCK_SIZE as f32;
152        let scale_factors = self.compute_band_scale_factors(
153            coeffs,
154            &allocation,
155            coeffs_per_window,
156            freq_resolution,
157        );
158        let smr_threshold = self.smr_threshold();
159        let quantized = self.quantize_with_scale(
160            coeffs,
161            &smr,
162            &scale_factors,
163            coeffs_per_window,
164            freq_resolution,
165            smr_threshold,
166        );
167
168        (quantized, scale_factors)
169    }
170
171    /// Quantize MDCT coefficients based on SMR and per-band bit allocation
172    pub fn quantize_coefficients(
173        &self,
174        coeffs: &[f32],
175        smr: &[f32],
176        allocation: &[u8],
177    ) -> (Vec<i16>, Vec<f32>) {
178        let freq_resolution = self.sample_rate as f32 / LONG_BLOCK_SIZE as f32;
179        let scale_factors = self.compute_band_scale_factors(
180            coeffs,
181            allocation,
182            LONG_BLOCK_SIZE / 2,
183            freq_resolution,
184        );
185        let smr_threshold = self.smr_threshold();
186        let quantized = self.quantize_with_scale(
187            coeffs,
188            smr,
189            &scale_factors,
190            LONG_BLOCK_SIZE / 2,
191            freq_resolution,
192            smr_threshold,
193        );
194
195        (quantized, scale_factors)
196    }
197
198    /// Frequency of MDCT coefficient `k` when a frame holds `coeffs_per_window`
199    /// coefficients per analysis window.
200    fn freq_for_coeff(k: usize, coeffs_per_window: usize, freq_resolution: f32) -> f32 {
201        let bin = k % coeffs_per_window;
202        (bin as f32 + 0.5) * freq_resolution
203    }
204
205    /// Per-band scale factors computed from coefficient magnitudes and the
206    /// per-coefficient bit allocation
207    fn compute_band_scale_factors(
208        &self,
209        coeffs: &[f32],
210        allocation: &[u8],
211        coeffs_per_window: usize,
212        freq_resolution: f32,
213    ) -> Vec<f32> {
214        // Calculate scale factors per Bark band
215        let mut band_max = [0.0f32; NUM_BARK_BANDS];
216        for (k, &c) in coeffs.iter().enumerate() {
217            let freq = Self::freq_for_coeff(k, coeffs_per_window, freq_resolution);
218            let band = PsychoacousticModel::freq_to_bark_band(freq);
219            band_max[band] = band_max[band].max(c.abs());
220        }
221
222        // Per-band bit allocation from the psychoacoustic model
223        let mut band_bits_max = [0u8; NUM_BARK_BANDS];
224        for (k, &bits) in allocation.iter().enumerate() {
225            let freq = Self::freq_for_coeff(k, coeffs_per_window, freq_resolution);
226            let band = PsychoacousticModel::freq_to_bark_band(freq);
227            band_bits_max[band] = band_bits_max[band].max(bits);
228        }
229        // The most-precisely-allocated band in this frame is the reference.
230        let max_bits = band_bits_max.iter().copied().max().unwrap_or(0);
231
232        // Calculate scale factors (to fit i16 range without clipping)
233        let mut scale_factors = vec![1.0f32; NUM_BARK_BANDS];
234        for (sf, &max_val) in scale_factors.iter_mut().zip(band_max.iter()) {
235            if max_val > 1e-10 {
236                // Use 30000 as max to leave some headroom
237                *sf = 30000.0 / max_val;
238            }
239        }
240
241        // Refine per-band precision
242        if max_bits > 0 {
243            for (sf, &bits) in scale_factors.iter_mut().zip(band_bits_max.iter()) {
244                let weight = 0.25 + 0.75 * (bits as f32 / max_bits as f32);
245                *sf *= weight;
246            }
247        }
248
249        scale_factors
250    }
251
252    /// Quality-dependent masking threshold (dB). At max quality keep everything.
253    fn smr_threshold(&self) -> f32 {
254        if self.quality >= 0.99 {
255            -100.0
256        } else {
257            // Exponential decay from 0 dB at quality=0 to -60 dB at quality=1
258            let t = (1.0 - self.quality).max(0.001);
259            -60.0 * (1.0 - libm::powf(t, 0.5))
260        }
261    }
262
263    /// Quantize coefficients above the masking threshold against per-band scale factors
264    fn quantize_with_scale(
265        &self,
266        coeffs: &[f32],
267        smr: &[f32],
268        scale_factors: &[f32],
269        coeffs_per_window: usize,
270        freq_resolution: f32,
271        smr_threshold: f32,
272    ) -> Vec<i16> {
273        let mut quantized = vec![0i16; coeffs.len()];
274
275        for (k, (q, &c)) in quantized.iter_mut().zip(coeffs.iter()).enumerate() {
276            let freq = Self::freq_for_coeff(k, coeffs_per_window, freq_resolution);
277            let band = PsychoacousticModel::freq_to_bark_band(freq);
278
279            if smr[k] > smr_threshold {
280                // Above masking threshold, quantize with appropriate precision
281                let scaled = c * scale_factors[band];
282                *q = libm::roundf(scaled).clamp(I16_MIN_F32, I16_MAX_F32) as i16;
283            }
284            // else: below threshold, leave as 0
285        }
286
287        quantized
288    }
289
290    /// Reset encoder state
291    pub fn reset(&mut self) {
292        self.mdct.reset();
293        for model in &mut self.psy_models {
294            model.reset();
295        }
296        for model in &mut self.short_psy_models {
297            model.reset();
298        }
299    }
300
301    /// Detect transient hops and build the BlockSize plan for an entire buffer.
302    fn plan_block_types(&self, mono: &[f32], num_hops: usize) -> Vec<BlockSize> {
303        let mut plan = vec![BlockSize::Long; num_hops];
304
305        for h in 1..num_hops {
306            // Hop 0 is always Long
307            if self.hop_has_transient(mono, h) {
308                plan[h] = BlockSize::Short;
309            }
310        }
311
312        // Merge short runs and insert Start/Stop transitions.
313        let mut h = 0;
314        while h < num_hops {
315            if plan[h] == BlockSize::Short {
316                let mut run_end = h;
317                while run_end + 1 < num_hops && plan[run_end + 1] == BlockSize::Short {
318                    run_end += 1;
319                }
320                // The frame before the first short group transitions with a
321                // Start window
322                if h > 0 {
323                    plan[h - 1] = BlockSize::Start;
324                    plan[run_end] = BlockSize::Stop;
325                }
326                h = run_end + 1;
327            } else {
328                h += 1;
329            }
330        }
331
332        plan
333    }
334
335    /// Whether an attack sits in hop `h`'s short-window guard region.
336    fn hop_has_transient(&self, mono: &[f32], h: usize) -> bool {
337        let grid_start = h * HOP_SIZE;
338
339        // Per-window RMS energy over the guard region
340        let mut window_energy = [0.0f32; SHORTS_PER_GROUP];
341        for w in 0..SHORTS_PER_GROUP {
342            let off = grid_start + SHORT_GROUP_OFFSET + w * (SHORT_BLOCK_SIZE / 2);
343            let mut sum = 0.0f32;
344            for i in 0..SHORT_BLOCK_SIZE {
345                let v = mono[off + i];
346                sum += v * v;
347            }
348            window_energy[w] = sum / SHORT_BLOCK_SIZE as f32;
349        }
350
351        // Baseline floor derived from the whole grid's average energy
352        let mut grid_sum = 0.0f32;
353        for i in 0..LONG_BLOCK_SIZE {
354            let v = mono[grid_start + i];
355            grid_sum += v * v;
356        }
357        let floor = grid_sum / LONG_BLOCK_SIZE as f32 * DETECTOR_FLOOR_FRACTION;
358
359        // An attack is a window whose energy jumps well above its predecessor
360        for w in 0..SHORTS_PER_GROUP {
361            let baseline = window_energy[w.saturating_sub(1)].max(floor);
362            if window_energy[w] > ATTACK_RATIO * baseline {
363                return true;
364            }
365        }
366
367        false
368    }
369
370    /// Encode audio samples to flo file format
371    ///
372    /// This produces a complete flo file with transform-based frames
373    pub fn encode_to_flo(&mut self, samples: &[f32], metadata: &[u8]) -> crate::FloResult<Vec<u8>> {
374        let block_samples = LONG_BLOCK_SIZE;
375        let hop_size = HOP_SIZE;
376
377        // For proper MDCT overlap-add reconstruction, we need:
378        // - A priming frame at the start (silence) to initialize overlap buffer
379        // - Proper number of frames to cover all samples
380        let num_samples_per_channel = samples.len() / self.channels as usize;
381
382        // Add hop_size samples of pre-roll (zeros) at start for proper reconstruction
383        let pre_roll = hop_size;
384        let total_samples = num_samples_per_channel + pre_roll;
385        let num_hops = total_samples.div_ceil(hop_size);
386        let total_samples_needed = (num_hops + 1) * hop_size;
387
388        // Create padded buffer with pre-roll zeros at start
389        let mut padded = vec![0.0f32; total_samples_needed * self.channels as usize];
390
391        // Copy original samples after pre-roll
392        for ch in 0..self.channels as usize {
393            for i in 0..num_samples_per_channel.min(total_samples_needed - pre_roll) {
394                let src_idx = i * self.channels as usize + ch;
395                let dst_idx = (i + pre_roll) * self.channels as usize + ch;
396                if src_idx < samples.len() && dst_idx < padded.len() {
397                    padded[dst_idx] = samples[src_idx];
398                }
399            }
400        }
401
402        // Mono mix for transient detection over the whole buffer
403        let mono: Vec<f32> = (0..total_samples_needed)
404            .map(|i| {
405                let mut sum = 0.0f32;
406                for ch in 0..self.channels as usize {
407                    sum += padded[i * self.channels as usize + ch];
408                }
409                sum / self.channels as f32
410            })
411            .collect();
412
413        // Block-switching plan with look-ahead over the whole buffer
414        let plan = self.plan_block_types(&mono, num_hops);
415
416        // Encode frames
417        let mut encoded_frames: Vec<Frame> = Vec::new();
418
419        // Process overlapping blocks
420        for hop_idx in 0..num_hops {
421            let start = hop_idx * hop_size * self.channels as usize;
422            let end = start + block_samples * self.channels as usize;
423
424            if end > padded.len() {
425                break;
426            }
427
428            let frame_samples = &padded[start..end];
429            let transform_frame = self.encode_frame(frame_samples, plan[hop_idx]);
430
431            // Serialize the transform frame
432            let frame_data = serialize_frame(&transform_frame);
433
434            // Create a flo Frame with transform type
435            let mut flo_frame = Frame::new(FrameType::Transform as u8, hop_size as u32);
436            flo_frame.channels.push(ChannelData {
437                predictor_coeffs: vec![],
438                shift_bits: 0,
439                residual_encoding: ResidualEncoding::Raw,
440                rice_parameter: 0,
441                residuals: frame_data,
442            });
443
444            encoded_frames.push(flo_frame);
445        }
446
447        // Write using the standard Writer
448        let writer = crate::Writer::new();
449        writer.write_ex(
450            self.sample_rate,
451            self.channels,
452            16,                                              // bit_depth for lossy
453            5,    // compression level (not used for transform)
454            true, // is_lossy
455            (libm::roundf(self.quality * 4.0) as u8).min(4), // quality as 0-4
456            &encoded_frames,
457            metadata,
458        )
459    }
460}
461
462/// Serialize a transform frame to bytes (optimized)
463pub fn serialize_frame(frame: &TransformFrame) -> Vec<u8> {
464    let mut data = Vec::new();
465
466    // Block size (1 byte)
467    data.push(match frame.block_size {
468        BlockSize::Long => 0,
469        BlockSize::Short => 1,
470        BlockSize::Start => 2,
471        BlockSize::Stop => 3,
472    });
473
474    // Number of channels (1 byte)
475    data.push(frame.coefficients.len() as u8);
476
477    // Scale factors per channel (25 bands * 2 bytes * channels)
478    // Encode as log scale u16 instead of f32 to save space
479    for sf in &frame.scale_factors {
480        for &s in sf {
481            // Convert to log scale: log2(sf) * 256 + 32768
482            let log_sf = if s > 1e-10 {
483                ((libm::log2f(s) * 256.0) + 32768.0).clamp(0.0, 65535.0) as u16
484            } else {
485                0
486            };
487            data.extend_from_slice(&log_sf.to_le_bytes());
488        }
489    }
490
491    // Coefficients per channel (sparse encoding for mostly-zeros)
492    for quantized in &frame.coefficients {
493        let encoded = serialize_sparse(quantized);
494        let len = encoded.len() as u32;
495        data.extend_from_slice(&len.to_le_bytes());
496        data.extend_from_slice(&encoded);
497    }
498
499    data
500}
501
502/// Encode coefficients using sparse run-length encoding
503/// Format: [zero_count_varint] [non_zero_count] [values...]
504pub fn serialize_sparse(coeffs: &[i16]) -> Vec<u8> {
505    let mut output = Vec::new();
506    let mut i = 0;
507
508    while i < coeffs.len() {
509        // Count leading zeros
510        let zero_start = i;
511        while i < coeffs.len() && coeffs[i] == 0 {
512            i += 1;
513        }
514        let zero_count = i - zero_start;
515
516        // Count non-zeros (up to 255)
517        let non_zero_start = i;
518        while i < coeffs.len() && coeffs[i] != 0 && (i - non_zero_start) < 255 {
519            i += 1;
520        }
521        let non_zero_count = i - non_zero_start;
522
523        // Encode run: [zero_count_varint] [non_zero_count] [values...]
524        encode_varint(&mut output, zero_count as u32);
525        output.push(non_zero_count as u8);
526
527        // Write non-zero values as i16 LE
528        for j in non_zero_start..non_zero_start + non_zero_count {
529            output.extend_from_slice(&coeffs[j].to_le_bytes());
530        }
531    }
532
533    output
534}
535
536/// Encode a u32 as varint (1-5 bytes)
537fn encode_varint(output: &mut Vec<u8>, mut value: u32) {
538    loop {
539        let mut byte = (value & 0x7F) as u8;
540        value >>= 7;
541        if value != 0 {
542            byte |= 0x80;
543        }
544        output.push(byte);
545        if value == 0 {
546            break;
547        }
548    }
549}