rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
//! Silence detection: trim and split, matching `librosa.effects`.

use crate::dsp::db::amplitude_to_db;

/// Convert a frame index to a sample index.
fn frame_to_sample(frame: usize, hop_length: usize) -> usize {
    frame * hop_length
}

/// Trim leading and trailing silence from an audio signal.
///
/// Matches `librosa.effects.trim(y, top_db=top_db, frame_length=frame_length, hop_length=hop_length)`.
///
/// Returns `(start_sample, end_sample)` — the non-silent region boundaries.
///
/// # Examples
///
/// ```
/// let signal = vec![0.0, 0.0, 1.0, 0.5, 0.0, 0.0];
/// let (start, end) = rosa::effects::trim(&signal, 20.0, 2048, 512);
/// ```
pub fn trim(y: &[f64], top_db: f64, frame_length: usize, hop_length: usize) -> (usize, usize) {
    let non_silent = signal_to_frame_nonsilent(y, top_db, frame_length, hop_length);

    // Find first and last non-silent frames
    let first = non_silent.iter().position(|&b| b);
    let last = non_silent.iter().rposition(|&b| b);

    match (first, last) {
        (Some(f), Some(l)) => {
            let start = frame_to_sample(f, hop_length);
            let end = frame_to_sample(l + 1, hop_length).min(y.len());
            (start, end)
        }
        _ => (0, 0),
    }
}

/// Split an audio signal into non-silent intervals.
///
/// Matches `librosa.effects.split(y, top_db=top_db, frame_length=frame_length, hop_length=hop_length)`.
///
/// Returns a list of `(start_sample, end_sample)` intervals.
pub fn split(
    y: &[f64],
    top_db: f64,
    frame_length: usize,
    hop_length: usize,
) -> Vec<(usize, usize)> {
    let non_silent = signal_to_frame_nonsilent(y, top_db, frame_length, hop_length);

    if non_silent.is_empty() {
        return Vec::new();
    }

    // Find edges: points where non_silent changes value
    let mut edges: Vec<usize> = Vec::new();

    // If first frame is non-silent, start at 0
    if non_silent[0] {
        edges.push(0);
    }

    // Find transitions (diff of non_silent as int)
    for i in 0..non_silent.len() - 1 {
        if non_silent[i] != non_silent[i + 1] {
            edges.push(i + 1);
        }
    }

    // If last frame is non-silent, close the interval
    if non_silent[non_silent.len() - 1] {
        edges.push(non_silent.len());
    }

    // Convert frames to samples and clip to signal length
    let edges_samples: Vec<usize> = edges
        .iter()
        .map(|&e| frame_to_sample(e, hop_length).min(y.len()))
        .collect();

    // Pair up edges into intervals
    edges_samples
        .chunks(2)
        .filter(|chunk| chunk.len() == 2)
        .map(|chunk| (chunk[0], chunk[1]))
        .collect()
}

/// Compute per-frame non-silent indicator.
///
/// Matches `librosa.effects._signal_to_frame_nonsilent`.
///
/// 1. Compute RMS per frame
/// 2. Convert to dB with ref=max (amplitude_to_db)
/// 3. Non-silent where dB > -top_db
fn signal_to_frame_nonsilent(
    y: &[f64],
    top_db: f64,
    frame_length: usize,
    hop_length: usize,
) -> Vec<bool> {
    // Compute RMS per frame
    let rms = crate::spectral::rms(y, frame_length, hop_length, true);

    if rms.is_empty() {
        return Vec::new();
    }

    // Convert RMS (amplitude) to dB with ref=max
    // librosa uses amplitude_to_db(rms, ref=np.max, top_db=None)
    // ref=np.max means ref_val = max(rms)
    let ref_val = rms.iter().copied().fold(0.0_f64, f64::max);
    let db = amplitude_to_db(&rms, ref_val, 1e-5, None);

    // Non-silent where dB > -top_db
    db.iter().map(|&d| d > -top_db).collect()
}