pub fn power_to_db(s: &[f64], ref_val: f64, amin: f64, top_db: Option<f64>) -> Vec<f64> {
let ref_val = ref_val.abs().max(amin);
let log_ref = 10.0 * ref_val.log10();
let mut result: Vec<f64> = s
.iter()
.map(|&val| 10.0 * val.max(amin).log10() - log_ref)
.collect();
if let Some(top_db) = top_db {
assert!(top_db >= 0.0, "top_db must be non-negative");
let max_val = result.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let threshold = max_val - top_db;
for val in result.iter_mut() {
*val = val.max(threshold);
}
}
result
}
pub fn db_to_power(s_db: &[f64], ref_val: f64) -> Vec<f64> {
s_db.iter()
.map(|&val| ref_val * 10.0_f64.powf(val * 0.1))
.collect()
}
pub fn amplitude_to_db(s: &[f64], ref_val: f64, amin: f64, top_db: Option<f64>) -> Vec<f64> {
let power: Vec<f64> = s.iter().map(|&val| val.abs().max(amin).powi(2)).collect();
let ref_power = ref_val.abs().max(amin).powi(2);
power_to_db(&power, ref_power, amin * amin, top_db)
}
pub fn db_to_amplitude(s_db: &[f64], ref_val: f64) -> Vec<f64> {
let ref_power = ref_val * ref_val;
db_to_power(s_db, ref_power)
.iter()
.map(|&val| val.sqrt())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct DbTestCase {
name: String,
function: String,
input: Vec<f64>,
#[serde(default = "default_ref")]
ref_val: f64,
#[serde(default)]
amin: Option<f64>,
#[serde(default)]
top_db: Option<f64>,
expected: Vec<f64>,
}
fn default_ref() -> f64 {
1.0
}
fn load_db_references() -> Vec<DbTestCase> {
let data = include_str!("tests/references/db_conversions.json");
serde_json::from_str(data).expect("Failed to parse db_conversions.json")
}
fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{name}: length mismatch: got {} expected {}",
actual.len(),
expected.len()
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
let diff = (a - e).abs();
assert!(
diff < tol,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
);
}
}
#[test]
fn test_power_to_db_matches_librosa() {
let refs = load_db_references();
for case in refs.iter().filter(|c| c.function == "power_to_db") {
let amin = case.amin.unwrap_or(1e-10);
let result = power_to_db(&case.input, case.ref_val, amin, case.top_db);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[test]
fn test_db_to_power_matches_librosa() {
let refs = load_db_references();
for case in refs.iter().filter(|c| c.function == "db_to_power") {
let result = db_to_power(&case.input, case.ref_val);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[test]
fn test_amplitude_to_db_matches_librosa() {
let refs = load_db_references();
for case in refs.iter().filter(|c| c.function == "amplitude_to_db") {
let amin = case.amin.unwrap_or(1e-5);
let result = amplitude_to_db(&case.input, case.ref_val, amin, case.top_db);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[test]
fn test_db_to_amplitude_matches_librosa() {
let refs = load_db_references();
for case in refs.iter().filter(|c| c.function == "db_to_amplitude") {
let result = db_to_amplitude(&case.input, case.ref_val);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[test]
fn test_power_roundtrip() {
let original = vec![0.001, 0.01, 0.1, 1.0, 10.0];
let db = power_to_db(&original, 1.0, 1e-10, None);
let recovered = db_to_power(&db, 1.0);
assert_approx_eq(&recovered, &original, 1e-10, "power_roundtrip");
}
#[test]
fn test_amplitude_roundtrip() {
let original = vec![0.01, 0.1, 1.0, 10.0];
let db = amplitude_to_db(&original, 1.0, 1e-10, None);
let recovered = db_to_amplitude(&db, 1.0);
assert_approx_eq(&recovered, &original, 1e-10, "amplitude_roundtrip");
}
}