1use smallvec::SmallVec;
2
3use crate::{GaplessInfo, PcmChunk, duration_for_frames, gapless::heuristic::SilenceTrimParams};
4
5pub type GaplessOutput = SmallVec<[PcmChunk; 2]>;
7type TailBuffer = SmallVec<[PcmChunk; 4]>;
8
9struct Consts;
10impl Consts {
11 const FADE_IN_DURATION_MS: u64 = 3;
16
17 const FADE_OUT_DURATION_MS: u64 = 3;
23
24 const TRAILING_SILENCE_WINDOW_MS: u64 = 10;
35}
36
37#[derive(Debug, Default)]
39pub struct GaplessTrimmer {
40 mode: GaplessMode,
41 tail_buffer: TailBuffer,
42 tail_buffered_frames: u64,
43 trailing_frames: u64,
53}
54
55#[derive(Debug, Default)]
56enum GaplessMode {
57 #[default]
58 Disabled,
59 Fixed {
60 leading_remaining: u64,
61 fade_in: Option<FadeInState>,
65 },
66 Heuristic(Box<HeuristicState>),
67}
68
69#[derive(Debug)]
70struct HeuristicState {
71 fade_in: Option<FadeInState>,
74 params: SilenceTrimParams,
75 leading_buffer: TailBuffer,
79 leading_enabled: bool,
80 trim_trailing: bool,
83 silence_threshold_amp: f32,
87 leading_buffered_frames: u64,
88}
89
90impl HeuristicState {
91 fn new(params: SilenceTrimParams) -> Self {
92 let silence_threshold_amp = params.threshold_amplitude();
93 let trim_trailing = params.trim_trailing;
94 Self {
95 params,
96 silence_threshold_amp,
97 trim_trailing,
98 leading_buffer: TailBuffer::new(),
99 leading_buffered_frames: 0,
100 leading_enabled: true,
101 fade_in: None,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
113struct FadeInState {
114 applied_frames: u16,
115 total_frames: u16,
116}
117
118impl FadeInState {
119 fn apply(&mut self, chunk: &mut PcmChunk) {
123 if self.is_done() {
124 return;
125 }
126 let frames = chunk_frames(chunk);
127 if frames == 0 {
128 return;
129 }
130 let channels = usize::from(chunk.spec().channels.max(1));
131 let remaining = self.total_frames.saturating_sub(self.applied_frames);
132 let to_shape = remaining.min(u16::try_from(frames).unwrap_or(u16::MAX));
133 let total = f32::from(self.total_frames.max(1));
134 let start_frame = self.applied_frames;
135 let shape_samples = usize::from(to_shape) * channels;
136 let prefix_len = shape_samples.min(chunk.pcm.len());
137 let prefix = &mut chunk.pcm[..prefix_len];
138 for (frame_offset, frame_samples) in prefix.chunks_exact_mut(channels).enumerate() {
139 let frame = start_frame.saturating_add(u16::try_from(frame_offset).unwrap_or(u16::MAX));
140 let position = f32::from(frame) / total;
141 let gain = 0.5 - 0.5 * (std::f32::consts::PI * position).cos();
142 for sample in frame_samples {
143 *sample *= gain;
144 }
145 }
146 self.applied_frames = self.applied_frames.saturating_add(to_shape);
147 }
148
149 fn for_sample_rate(sample_rate: u32) -> Self {
150 let total_frames =
151 u64::from(sample_rate.max(1)).saturating_mul(Consts::FADE_IN_DURATION_MS) / 1000;
152 let total_frames = u16::try_from(total_frames.clamp(1, 65_535)).unwrap_or(u16::MAX);
153 Self {
154 total_frames,
155 applied_frames: 0,
156 }
157 }
158
159 fn is_done(self) -> bool {
161 self.applied_frames >= self.total_frames
162 }
163}
164
165impl GaplessTrimmer {
166 #[must_use]
173 pub fn codec_priming(leading_frames: u64, sample_rate: u32) -> Self {
174 if leading_frames == 0 {
175 return Self::disabled();
176 }
177 Self {
178 mode: GaplessMode::Fixed {
179 leading_remaining: leading_frames,
180 fade_in: Some(FadeInState::for_sample_rate(sample_rate)),
181 },
182 trailing_frames: 0,
183 tail_buffer: TailBuffer::new(),
184 tail_buffered_frames: 0,
185 }
186 }
187
188 #[must_use]
189 pub fn disabled() -> Self {
190 Self::default()
191 }
192
193 #[must_use]
194 pub fn flush(&mut self) -> GaplessOutput {
195 match &mut self.mode {
196 GaplessMode::Disabled => GaplessOutput::new(),
197 GaplessMode::Fixed { .. } => {
198 trim_tail_frames(
199 &mut self.tail_buffer,
200 &mut self.tail_buffered_frames,
201 self.trailing_frames,
202 );
203 drain_tail(&mut self.tail_buffer, &mut self.tail_buffered_frames)
204 }
205 GaplessMode::Heuristic(state) => flush_heuristic(
206 state,
207 &mut self.tail_buffer,
208 &mut self.tail_buffered_frames,
209 self.trailing_frames,
210 ),
211 }
212 }
213
214 pub fn notify_seek(&mut self) {
219 match &mut self.mode {
220 GaplessMode::Disabled => {}
221 GaplessMode::Fixed {
222 leading_remaining,
223 fade_in,
224 } => {
225 *leading_remaining = 0;
226 *fade_in = None;
227 }
228 GaplessMode::Heuristic(state) => {
229 state.leading_buffer.clear();
230 state.leading_buffered_frames = 0;
231 state.leading_enabled = false;
232 state.fade_in = None;
233 }
234 }
235 clear_tail_buffer(&mut self.tail_buffer, &mut self.tail_buffered_frames);
236 }
237
238 #[must_use]
239 pub fn push(&mut self, chunk: PcmChunk) -> GaplessOutput {
240 match &mut self.mode {
241 GaplessMode::Disabled => output_with(chunk),
242 GaplessMode::Fixed {
243 leading_remaining,
244 fade_in,
245 } => {
246 let Some(mut chunk) = trim_leading(chunk, leading_remaining) else {
247 return SmallVec::new();
248 };
249 apply_fade_in(fade_in, &mut chunk);
250
251 buffer_tail(&mut self.tail_buffer, &mut self.tail_buffered_frames, chunk);
252 release_ready_chunks(
253 &mut self.tail_buffer,
254 &mut self.tail_buffered_frames,
255 self.trailing_frames,
256 )
257 }
258 GaplessMode::Heuristic(state) => push_heuristic(
259 state,
260 &mut self.tail_buffer,
261 &mut self.tail_buffered_frames,
262 self.trailing_frames,
263 chunk,
264 ),
265 }
266 }
267
268 #[must_use]
272 pub fn silence_trim(params: SilenceTrimParams) -> Self {
273 Self {
274 trailing_frames: params.scan_window_frames,
275 mode: GaplessMode::Heuristic(Box::new(HeuristicState::new(params))),
276 tail_buffer: TailBuffer::new(),
277 tail_buffered_frames: 0,
278 }
279 }
280}
281
282impl From<GaplessInfo> for GaplessTrimmer {
283 fn from(info: GaplessInfo) -> Self {
284 let enabled = info.leading_frames > 0 || info.trailing_frames > 0;
285 Self {
286 mode: if enabled {
287 GaplessMode::Fixed {
288 leading_remaining: info.leading_frames,
289 fade_in: None,
290 }
291 } else {
292 GaplessMode::Disabled
293 },
294 trailing_frames: info.trailing_frames,
295 tail_buffer: TailBuffer::new(),
296 tail_buffered_frames: 0,
297 }
298 }
299}
300
301fn push_heuristic(
302 state: &mut HeuristicState,
303 tail_buffer: &mut TailBuffer,
304 tail_buffered_frames: &mut u64,
305 trailing_frames: u64,
306 chunk: PcmChunk,
307) -> GaplessOutput {
308 if !state.leading_enabled {
309 return forward_post_leading(
310 state,
311 tail_buffer,
312 tail_buffered_frames,
313 trailing_frames,
314 chunk,
315 );
316 }
317
318 state.leading_buffered_frames = state
319 .leading_buffered_frames
320 .saturating_add(chunk_frames(&chunk));
321 state.leading_buffer.push(chunk);
322
323 if let Some(trim_frames) = find_leading_trim_frames(
324 &state.leading_buffer,
325 &state.params,
326 state.silence_threshold_amp,
327 ) {
328 state.leading_enabled = false;
329 if trim_frames > 0 {
330 arm_fade_in(state);
331 }
332 return drain_leading_buffer(
333 state,
334 tail_buffer,
335 tail_buffered_frames,
336 trailing_frames,
337 trim_frames,
338 );
339 }
340
341 if state.leading_buffered_frames >= state.params.scan_window_frames {
342 state.leading_enabled = false;
343 return drain_leading_buffer(state, tail_buffer, tail_buffered_frames, trailing_frames, 0);
344 }
345
346 GaplessOutput::new()
347}
348
349fn forward_post_leading(
350 state: &mut HeuristicState,
351 tail_buffer: &mut TailBuffer,
352 tail_buffered_frames: &mut u64,
353 trailing_frames: u64,
354 mut chunk: PcmChunk,
355) -> GaplessOutput {
356 apply_fade_in(&mut state.fade_in, &mut chunk);
357 buffer_tail(tail_buffer, tail_buffered_frames, chunk);
358 release_ready_chunks(tail_buffer, tail_buffered_frames, trailing_frames)
359}
360
361fn flush_heuristic(
362 state: &mut HeuristicState,
363 tail_buffer: &mut TailBuffer,
364 tail_buffered_frames: &mut u64,
365 trailing_frames: u64,
366) -> GaplessOutput {
367 let mut ready = GaplessOutput::new();
368
369 if state.leading_enabled {
370 let trim_frames = find_leading_trim_frames(
371 &state.leading_buffer,
372 &state.params,
373 state.silence_threshold_amp,
374 )
375 .unwrap_or(0);
376 state.leading_enabled = false;
377 if trim_frames > 0 {
378 arm_fade_in(state);
379 }
380 ready.extend(drain_leading_buffer(
381 state,
382 tail_buffer,
383 tail_buffered_frames,
384 trailing_frames,
385 trim_frames,
386 ));
387 }
388
389 if state.trim_trailing {
390 let silent_suffix = trailing_silent_frames(tail_buffer, state.silence_threshold_amp);
391 if silent_suffix > 0
392 && silent_suffix < *tail_buffered_frames
393 && silent_suffix >= state.params.min_trim_frames
394 {
395 trim_tail_frames(tail_buffer, tail_buffered_frames, silent_suffix);
396 let sample_rate = tail_buffer
397 .last()
398 .map_or(0, |chunk| chunk.spec().sample_rate);
399 apply_trailing_fade_out(tail_buffer, sample_rate);
400 }
401 }
402
403 ready.extend(drain_tail(tail_buffer, tail_buffered_frames));
404 ready
405}
406
407fn apply_trailing_fade_out(tail_buffer: &mut TailBuffer, sample_rate: u32) {
413 if tail_buffer.is_empty() {
414 return;
415 }
416 let total_frames_u64 =
417 u64::from(sample_rate.max(1)).saturating_mul(Consts::FADE_OUT_DURATION_MS) / 1000;
418 let total_frames = usize_from_u64_saturating(total_frames_u64).max(1);
419 let denom = u32::try_from(total_frames.saturating_sub(1).max(1)).unwrap_or(u32::MAX);
420 let denom = f32::from(u16::try_from(denom).unwrap_or(u16::MAX));
421
422 let mut faded_so_far: usize = 0;
423 for chunk in tail_buffer.iter_mut().rev() {
424 if faded_so_far >= total_frames {
425 break;
426 }
427 let channels = usize::from(chunk.spec().channels.max(1));
428 let chunk_total_frames = usize_from_u64_saturating(chunk_frames(chunk));
429 if chunk_total_frames == 0 {
430 continue;
431 }
432 let in_window = (total_frames - faded_so_far).min(chunk_total_frames);
433 let first_to_shape = chunk_total_frames - in_window;
434
435 let pcm_end = (chunk_total_frames * channels).min(chunk.pcm.len());
436 let pcm_start = (first_to_shape * channels).min(pcm_end);
437 let window = &mut chunk.pcm[pcm_start..pcm_end];
438 for (frame_in_chunk, frame_samples) in window.chunks_exact_mut(channels).enumerate() {
439 let frames_to_end = in_window - 1 - frame_in_chunk + faded_so_far;
440 let frame_in_fade = total_frames - 1 - frames_to_end;
441 let position = f32::from(u16::try_from(frame_in_fade).unwrap_or(u16::MAX)) / denom;
442 let gain = 0.5 + 0.5 * (std::f32::consts::PI * position).cos();
443 for sample in frame_samples {
444 *sample *= gain;
445 }
446 }
447
448 faded_so_far += in_window;
449 }
450}
451
452fn arm_fade_in(state: &mut HeuristicState) {
453 let sample_rate = state
454 .leading_buffer
455 .first()
456 .map_or(0, |chunk| chunk.spec().sample_rate);
457 state.fade_in = Some(FadeInState::for_sample_rate(sample_rate));
458}
459
460fn apply_fade_in(fade: &mut Option<FadeInState>, chunk: &mut PcmChunk) {
461 let Some(state) = fade.as_mut() else {
462 return;
463 };
464 state.apply(chunk);
465 if state.is_done() {
466 *fade = None;
467 }
468}
469
470fn trim_leading(mut chunk: PcmChunk, leading_remaining: &mut u64) -> Option<PcmChunk> {
471 if *leading_remaining > 0 {
472 let chunk_frames = chunk_frames(&chunk);
473 if chunk_frames <= *leading_remaining {
474 *leading_remaining -= chunk_frames;
475 return None;
476 }
477
478 let trim_frames = usize_from_u64_saturating(*leading_remaining);
479 *leading_remaining = 0;
480 trim_chunk_start(&mut chunk, trim_frames);
481 }
482
483 (chunk_frames(&chunk) > 0).then_some(chunk)
484}
485
486fn drain_leading_buffer(
487 state: &mut HeuristicState,
488 tail_buffer: &mut TailBuffer,
489 tail_buffered_frames: &mut u64,
490 trailing_frames: u64,
491 trim_frames: u64,
492) -> GaplessOutput {
493 let mut buffer = std::mem::take(&mut state.leading_buffer);
494 state.leading_buffered_frames = 0;
495
496 let mut remaining_trim = trim_frames;
497 for mut chunk in buffer.drain(..) {
498 if remaining_trim > 0 {
499 let chunk_frames = chunk_frames(&chunk);
500 if chunk_frames <= remaining_trim {
501 remaining_trim -= chunk_frames;
502 continue;
503 }
504
505 let trim = usize_from_u64_saturating(remaining_trim);
506 remaining_trim = 0;
507 trim_chunk_start(&mut chunk, trim);
508 }
509
510 apply_fade_in(&mut state.fade_in, &mut chunk);
511 buffer_tail(tail_buffer, tail_buffered_frames, chunk);
512 }
513
514 release_ready_chunks(tail_buffer, tail_buffered_frames, trailing_frames)
515}
516
517fn buffer_tail(tail_buffer: &mut TailBuffer, tail_buffered_frames: &mut u64, chunk: PcmChunk) {
518 *tail_buffered_frames = (*tail_buffered_frames).saturating_add(chunk_frames(&chunk));
519 tail_buffer.push(chunk);
520}
521
522fn release_ready_chunks(
523 tail_buffer: &mut TailBuffer,
524 tail_buffered_frames: &mut u64,
525 trailing_frames: u64,
526) -> GaplessOutput {
527 let mut ready = GaplessOutput::new();
528 while can_release_front(tail_buffer, *tail_buffered_frames, trailing_frames) {
529 if let Some(chunk) = pop_front_chunk(tail_buffer, tail_buffered_frames) {
530 ready.push(chunk);
531 }
532 }
533 ready
534}
535
536fn can_release_front(
537 tail_buffer: &TailBuffer,
538 tail_buffered_frames: u64,
539 trailing_frames: u64,
540) -> bool {
541 let Some(front) = tail_buffer.first() else {
542 return false;
543 };
544
545 tail_buffered_frames.saturating_sub(chunk_frames(front)) >= trailing_frames
546}
547
548fn trim_tail_frames(
549 tail_buffer: &mut TailBuffer,
550 tail_buffered_frames: &mut u64,
551 trim_frames: u64,
552) {
553 let mut drop_frames = trim_frames.min(*tail_buffered_frames);
554 while drop_frames > 0 {
555 let Some(back) = tail_buffer.last_mut() else {
556 break;
557 };
558
559 let back_frames = chunk_frames(back);
560 if back_frames <= drop_frames {
561 drop_frames -= back_frames;
562 *tail_buffered_frames = (*tail_buffered_frames).saturating_sub(back_frames);
563 tail_buffer.pop();
564 continue;
565 }
566
567 trim_chunk_end(back, drop_frames);
568 *tail_buffered_frames = (*tail_buffered_frames).saturating_sub(drop_frames);
569 drop_frames = 0;
570 }
571}
572
573fn trailing_silent_frames(tail_buffer: &TailBuffer, threshold_amp: f32) -> u64 {
590 use num_traits::AsPrimitive;
591
592 if tail_buffer.is_empty() {
593 return 0;
594 }
595
596 let sample_rate = tail_buffer
597 .first()
598 .map_or(48_000, |chunk| chunk.spec().sample_rate)
599 .max(1);
600 let window_frames =
601 (u64::from(sample_rate).saturating_mul(Consts::TRAILING_SILENCE_WINDOW_MS) / 1000).max(1);
602 let threshold = f64::from(threshold_amp);
603
604 let mut silent_frames = 0u64;
605 let mut window_sum_abs = 0.0_f64;
606 let mut window_count: u64 = 0;
607
608 for chunk in tail_buffer.iter().rev() {
609 let chunk_total_frames = chunk_frames(chunk);
610 let samples = chunk.samples();
611 let channels = usize::from(chunk.spec().channels.max(1));
612 let channels_f64: f64 = channels.max(1).as_();
613 for frame in (0..chunk_total_frames).rev() {
614 let frame_start = usize_from_u64_saturating(frame).saturating_mul(channels);
615 let frame_end = frame_start.saturating_add(channels).min(samples.len());
616 if frame_end <= frame_start {
617 continue;
618 }
619 let mut frame_sum_abs = 0.0_f64;
620 for &sample in &samples[frame_start..frame_end] {
621 frame_sum_abs += f64::from(sample.abs());
622 }
623 let frame_mean_abs = frame_sum_abs / channels_f64;
624 window_sum_abs += frame_mean_abs;
625 window_count = window_count.saturating_add(1);
626
627 if window_count >= window_frames {
628 let window_count_f64: f64 = window_count.as_();
629 let mean_abs = window_sum_abs / window_count_f64;
630 if mean_abs <= threshold {
631 silent_frames = silent_frames.saturating_add(window_count);
632 window_sum_abs = 0.0;
633 window_count = 0;
634 } else {
635 return silent_frames;
636 }
637 }
638 }
639 }
640
641 if window_count > 0 {
642 let window_count_f64: f64 = window_count.as_();
643 let mean_abs = window_sum_abs / window_count_f64;
644 if mean_abs <= threshold {
645 silent_frames = silent_frames.saturating_add(window_count);
646 }
647 }
648
649 silent_frames
650}
651
652fn find_leading_trim_frames(
664 buffer: &[PcmChunk],
665 params: &SilenceTrimParams,
666 threshold_amp: f32,
667) -> Option<u64> {
668 let mut scanned_frames = 0_u64;
669 let mut trim_frames = 0_u64;
670
671 for chunk in buffer {
672 let chunk_frames = chunk_frames(chunk);
673 let samples = chunk.samples();
674 let channels = usize::from(chunk.spec().channels.max(1));
675 for frame in 0..chunk_frames {
676 if scanned_frames >= params.scan_window_frames {
677 return None;
678 }
679
680 let frame_start = usize_from_u64_saturating(frame).saturating_mul(channels);
681 let frame_end = frame_start.saturating_add(channels).min(samples.len());
682 if frame_end <= frame_start {
683 scanned_frames = scanned_frames.saturating_add(1);
684 trim_frames = trim_frames.saturating_add(1);
685 continue;
686 }
687
688 if !frame_is_silent(&samples[frame_start..frame_end], threshold_amp) {
689 return (trim_frames >= params.min_trim_frames).then_some(trim_frames);
690 }
691
692 scanned_frames = scanned_frames.saturating_add(1);
693 trim_frames = trim_frames.saturating_add(1);
694 }
695 }
696
697 None
698}
699
700fn drain_tail(tail_buffer: &mut TailBuffer, tail_buffered_frames: &mut u64) -> GaplessOutput {
701 let mut ready = GaplessOutput::new();
702 while let Some(chunk) = pop_front_chunk(tail_buffer, tail_buffered_frames) {
703 ready.push(chunk);
704 }
705 ready
706}
707
708fn clear_tail_buffer(tail_buffer: &mut TailBuffer, tail_buffered_frames: &mut u64) {
709 tail_buffer.clear();
710 *tail_buffered_frames = 0;
711}
712
713fn pop_front_chunk(
714 tail_buffer: &mut TailBuffer,
715 tail_buffered_frames: &mut u64,
716) -> Option<PcmChunk> {
717 if tail_buffer.is_empty() {
718 return None;
719 }
720
721 let chunk = tail_buffer.remove(0);
722 *tail_buffered_frames = tail_buffered_frames.saturating_sub(chunk_frames(&chunk));
723 Some(chunk)
724}
725
726fn frame_is_silent(samples: &[f32], threshold_amp: f32) -> bool {
727 samples.iter().all(|sample| sample.abs() <= threshold_amp)
728}
729
730fn trim_chunk_start(chunk: &mut PcmChunk, trim_frames: usize) {
731 let spec = chunk.spec();
732 let channels = usize::from(spec.channels.max(1));
733 let trim_samples = trim_frames.saturating_mul(channels);
734 let len = chunk.pcm.len();
735 chunk.pcm.copy_within(trim_samples..len, 0);
736 chunk.pcm.truncate(len.saturating_sub(trim_samples));
737 chunk.meta.frame_offset = chunk.meta.frame_offset.saturating_add(trim_frames as u64);
738 chunk.meta.frames = u32::try_from(chunk.pcm.len() / channels.max(1)).unwrap_or(u32::MAX);
739 chunk.meta.timestamp = chunk
740 .meta
741 .timestamp
742 .saturating_add(duration_for_frames(spec.sample_rate, trim_frames as u64));
743}
744
745fn trim_chunk_end(chunk: &mut PcmChunk, trim_frames: u64) {
746 let channels = usize::from(chunk.spec().channels.max(1));
747 let keep_frames = usize_from_u64_saturating(chunk_frames(chunk).saturating_sub(trim_frames));
748 let keep_samples = keep_frames.saturating_mul(channels);
749 chunk.pcm.truncate(keep_samples);
750 chunk.meta.frames = u32::try_from(keep_frames).unwrap_or(u32::MAX);
751}
752
753fn output_with(chunk: PcmChunk) -> GaplessOutput {
754 let mut ready = GaplessOutput::new();
755 ready.push(chunk);
756 ready
757}
758
759fn chunk_frames(chunk: &PcmChunk) -> u64 {
760 u64::try_from(chunk.frames()).unwrap_or(u64::MAX)
761}
762
763fn usize_from_u64_saturating(value: u64) -> usize {
764 usize::try_from(value).unwrap_or(usize::MAX)
765}
766
767#[cfg(test)]
768#[path = "tests.rs"]
769mod tests;