rosa/effects/silence.rs
1//! Silence detection: trim and split, matching `librosa.effects`.
2
3use crate::dsp::db::amplitude_to_db;
4
5/// Convert a frame index to a sample index.
6fn frame_to_sample(frame: usize, hop_length: usize) -> usize {
7 frame * hop_length
8}
9
10/// Trim leading and trailing silence from an audio signal.
11///
12/// Matches `librosa.effects.trim(y, top_db=top_db, frame_length=frame_length, hop_length=hop_length)`.
13///
14/// Returns `(start_sample, end_sample)` — the non-silent region boundaries.
15///
16/// # Examples
17///
18/// ```
19/// let signal = vec![0.0, 0.0, 1.0, 0.5, 0.0, 0.0];
20/// let (start, end) = rosa::effects::trim(&signal, 20.0, 2048, 512);
21/// ```
22pub fn trim(y: &[f64], top_db: f64, frame_length: usize, hop_length: usize) -> (usize, usize) {
23 let non_silent = signal_to_frame_nonsilent(y, top_db, frame_length, hop_length);
24
25 // Find first and last non-silent frames
26 let first = non_silent.iter().position(|&b| b);
27 let last = non_silent.iter().rposition(|&b| b);
28
29 match (first, last) {
30 (Some(f), Some(l)) => {
31 let start = frame_to_sample(f, hop_length);
32 let end = frame_to_sample(l + 1, hop_length).min(y.len());
33 (start, end)
34 }
35 _ => (0, 0),
36 }
37}
38
39/// Split an audio signal into non-silent intervals.
40///
41/// Matches `librosa.effects.split(y, top_db=top_db, frame_length=frame_length, hop_length=hop_length)`.
42///
43/// Returns a list of `(start_sample, end_sample)` intervals.
44pub fn split(
45 y: &[f64],
46 top_db: f64,
47 frame_length: usize,
48 hop_length: usize,
49) -> Vec<(usize, usize)> {
50 let non_silent = signal_to_frame_nonsilent(y, top_db, frame_length, hop_length);
51
52 if non_silent.is_empty() {
53 return Vec::new();
54 }
55
56 // Find edges: points where non_silent changes value
57 let mut edges: Vec<usize> = Vec::new();
58
59 // If first frame is non-silent, start at 0
60 if non_silent[0] {
61 edges.push(0);
62 }
63
64 // Find transitions (diff of non_silent as int)
65 for i in 0..non_silent.len() - 1 {
66 if non_silent[i] != non_silent[i + 1] {
67 edges.push(i + 1);
68 }
69 }
70
71 // If last frame is non-silent, close the interval
72 if non_silent[non_silent.len() - 1] {
73 edges.push(non_silent.len());
74 }
75
76 // Convert frames to samples and clip to signal length
77 let edges_samples: Vec<usize> = edges
78 .iter()
79 .map(|&e| frame_to_sample(e, hop_length).min(y.len()))
80 .collect();
81
82 // Pair up edges into intervals
83 edges_samples
84 .chunks(2)
85 .filter(|chunk| chunk.len() == 2)
86 .map(|chunk| (chunk[0], chunk[1]))
87 .collect()
88}
89
90/// Compute per-frame non-silent indicator.
91///
92/// Matches `librosa.effects._signal_to_frame_nonsilent`.
93///
94/// 1. Compute RMS per frame
95/// 2. Convert to dB with ref=max (amplitude_to_db)
96/// 3. Non-silent where dB > -top_db
97fn signal_to_frame_nonsilent(
98 y: &[f64],
99 top_db: f64,
100 frame_length: usize,
101 hop_length: usize,
102) -> Vec<bool> {
103 // Compute RMS per frame
104 let rms = crate::spectral::rms(y, frame_length, hop_length, true);
105
106 if rms.is_empty() {
107 return Vec::new();
108 }
109
110 // Convert RMS (amplitude) to dB with ref=max
111 // librosa uses amplitude_to_db(rms, ref=np.max, top_db=None)
112 // ref=np.max means ref_val = max(rms)
113 let ref_val = rms.iter().copied().fold(0.0_f64, f64::max);
114 let db = amplitude_to_db(&rms, ref_val, 1e-5, None);
115
116 // Non-silent where dB > -top_db
117 db.iter().map(|&d| d > -top_db).collect()
118}