Skip to main content

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}