use std::f64::consts::TAU;
const DB_SCALE_F32: f32 = 20.0 / std::f32::consts::LN_10; const DB_EXP_F32: f32 = std::f32::consts::LN_10 / 20.0; const DB_SCALE_F64: f64 = 20.0 / std::f64::consts::LN_10;
const DB_EXP_F64: f64 = std::f64::consts::LN_10 / 20.0;
const DB_GAIN_EXP_F64: f64 = std::f64::consts::LN_10 / 40.0;
#[inline]
pub fn amplitude_to_db(amplitude: f32) -> f32 {
if amplitude <= 0.0 {
return f32::NEG_INFINITY;
}
DB_SCALE_F32 * amplitude.ln()
}
#[inline]
pub fn db_to_amplitude(db: f32) -> f32 {
(db * DB_EXP_F32).exp()
}
#[inline]
pub fn amplitude_to_db_f64(amplitude: f64) -> f64 {
if amplitude <= 0.0 {
return f64::NEG_INFINITY;
}
DB_SCALE_F64 * amplitude.ln()
}
#[inline]
pub fn db_to_amplitude_f64(db: f64) -> f64 {
(db * DB_EXP_F64).exp()
}
#[inline]
pub fn db_gain_factor(db: f64) -> f64 {
(db * DB_GAIN_EXP_F64).exp()
}
pub const A4_FREQUENCY: f64 = 440.0;
pub const A4_MIDI_NOTE: f64 = 69.0;
pub const SEMITONES_PER_OCTAVE: f64 = 12.0;
#[inline]
pub fn midi_to_freq(note: f64) -> f64 {
A4_FREQUENCY * ((note - A4_MIDI_NOTE) / SEMITONES_PER_OCTAVE).exp2()
}
#[inline]
pub fn freq_to_midi(freq: f64) -> f64 {
if freq <= 0.0 {
return f64::NEG_INFINITY;
}
A4_MIDI_NOTE + SEMITONES_PER_OCTAVE * (freq / A4_FREQUENCY).log2()
}
#[inline]
pub fn time_constant(time_ms: f32, sample_rate: u32) -> f32 {
let samples = (time_ms * 0.001 * sample_rate as f32).max(1.0);
(-1.0f32 / samples).exp()
}
#[inline]
pub fn sanitize_sample(s: f32) -> f32 {
if s.is_finite() { s } else { 0.0 }
}
#[inline]
pub fn angular_frequency(freq_hz: f64, sample_rate: f64) -> f64 {
TAU * freq_hz / sample_rate
}
#[inline]
pub fn poly_blep(t: f64, dt: f64) -> f64 {
if dt <= 0.0 {
return 0.0;
}
if t < dt {
let t = t / dt;
2.0 * t - t * t - 1.0
} else if t > 1.0 - dt {
let t = (t - 1.0) / dt;
t * t + 2.0 * t + 1.0
} else {
0.0
}
}
#[inline]
pub fn constant_power_pan(pan: f32) -> (f32, f32) {
let pan = pan.clamp(-1.0, 1.0);
let theta = (pan + 1.0) * std::f32::consts::FRAC_PI_4;
let (sin, cos) = theta.sin_cos();
(cos, sin)
}
#[inline]
pub fn equal_power_crossfade(t: f32) -> (f32, f32) {
let angle = t.clamp(0.0, 1.0) * std::f32::consts::FRAC_PI_2;
let (sin, cos) = angle.sin_cos();
(cos, sin)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn db_unity() {
assert!(amplitude_to_db(1.0).abs() < 0.01);
assert!((db_to_amplitude(0.0) - 1.0).abs() < 0.01);
}
#[test]
fn db_half_amplitude() {
assert!((amplitude_to_db(0.5) - (-6.02)).abs() < 0.1);
assert!((db_to_amplitude(-6.02) - 0.5).abs() < 0.01);
}
#[test]
fn db_zero_amplitude() {
assert!(amplitude_to_db(0.0).is_infinite());
assert!(amplitude_to_db(-1.0).is_infinite());
}
#[test]
fn db_f64_matches_f32() {
for amp in [0.1, 0.5, 1.0, 2.0] {
let db32 = amplitude_to_db(amp as f32) as f64;
let db64 = amplitude_to_db_f64(amp);
assert!((db32 - db64).abs() < 0.01);
}
}
#[test]
fn db_gain_factor_peaking() {
let f = db_gain_factor(12.0);
assert!((f - 1.995).abs() < 0.01);
}
#[test]
fn midi_a4() {
assert!((midi_to_freq(69.0) - 440.0).abs() < 0.01);
}
#[test]
fn midi_c4() {
assert!((midi_to_freq(60.0) - 261.63).abs() < 0.1);
}
#[test]
fn midi_roundtrip() {
for note in [0.0, 36.0, 60.0, 69.0, 84.0, 127.0] {
let freq = midi_to_freq(note);
let back = freq_to_midi(freq);
assert!(
(back - note).abs() < 1e-10,
"roundtrip failed for note {note}"
);
}
}
#[test]
fn freq_to_midi_zero() {
assert!(freq_to_midi(0.0).is_infinite());
}
#[test]
fn time_constant_fast() {
let c = time_constant(0.01, 44100); assert!(c < 0.5, "near-instant should have low coefficient");
}
#[test]
fn time_constant_slow() {
let c = time_constant(1000.0, 44100); assert!(c > 0.99, "slow release should have high coefficient");
}
#[test]
fn sanitize_finite() {
assert_eq!(sanitize_sample(0.5), 0.5);
}
#[test]
fn sanitize_nan() {
assert_eq!(sanitize_sample(f32::NAN), 0.0);
}
#[test]
fn sanitize_inf() {
assert_eq!(sanitize_sample(f32::INFINITY), 0.0);
}
#[test]
fn angular_freq_1khz() {
let w0 = angular_frequency(1000.0, 44100.0);
let expected = TAU * 1000.0 / 44100.0;
assert!((w0 - expected).abs() < 1e-12);
}
#[test]
fn poly_blep_mid_phase_zero() {
assert_eq!(poly_blep(0.5, 0.01), 0.0);
}
#[test]
fn poly_blep_near_discontinuity() {
let dt = 0.01;
let v = poly_blep(dt * 0.5, dt);
assert!(v != 0.0, "should correct near discontinuity");
}
#[test]
fn poly_blep_zero_dt() {
assert_eq!(poly_blep(0.0, 0.0), 0.0);
}
#[test]
fn pan_center() {
let (l, r) = constant_power_pan(0.0);
assert!((l - r).abs() < 0.01);
assert!((l - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.01);
}
#[test]
fn pan_full_left() {
let (l, r) = constant_power_pan(-1.0);
assert!((l - 1.0).abs() < 0.01);
assert!(r.abs() < 0.01);
}
#[test]
fn pan_full_right() {
let (l, r) = constant_power_pan(1.0);
assert!(l.abs() < 0.01);
assert!((r - 1.0).abs() < 0.01);
}
#[test]
fn pan_constant_power() {
for p in [-1.0, -0.5, 0.0, 0.25, 0.5, 1.0] {
let (l, r) = constant_power_pan(p);
let power = l * l + r * r;
assert!((power - 1.0).abs() < 0.01, "power={power} at pan={p}");
}
}
#[test]
fn crossfade_endpoints() {
let (a, b) = equal_power_crossfade(0.0);
assert!((a - 1.0).abs() < 0.01);
assert!(b.abs() < 0.01);
let (a, b) = equal_power_crossfade(1.0);
assert!(a.abs() < 0.01);
assert!((b - 1.0).abs() < 0.01);
}
#[test]
fn crossfade_constant_energy() {
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
let (a, b) = equal_power_crossfade(t);
let energy = a * a + b * b;
assert!((energy - 1.0).abs() < 0.01, "energy={energy} at t={t}");
}
}
#[test]
fn amplitude_to_db_f64_zero() {
assert!(amplitude_to_db_f64(0.0).is_infinite());
assert!(amplitude_to_db_f64(-1.0).is_infinite());
}
#[test]
fn db_to_amplitude_f64_roundtrip() {
let amp = db_to_amplitude_f64(-6.02);
assert!((amp - 0.5).abs() < 0.01);
let db = amplitude_to_db_f64(amp);
assert!((db - (-6.02)).abs() < 0.01);
}
#[test]
fn pan_clamps_out_of_range() {
let (l, r) = constant_power_pan(-2.0);
let (l2, r2) = constant_power_pan(-1.0);
assert!((l - l2).abs() < 0.01);
assert!((r - r2).abs() < 0.01);
let (l, r) = constant_power_pan(5.0);
let (l2, r2) = constant_power_pan(1.0);
assert!((l - l2).abs() < 0.01);
assert!((r - r2).abs() < 0.01);
}
#[test]
fn crossfade_clamps_out_of_range() {
let (a, b) = equal_power_crossfade(-1.0);
let (a2, b2) = equal_power_crossfade(0.0);
assert!((a - a2).abs() < 0.01);
assert!((b - b2).abs() < 0.01);
let (a, b) = equal_power_crossfade(2.0);
let (a2, b2) = equal_power_crossfade(1.0);
assert!((a - a2).abs() < 0.01);
assert!((b - b2).abs() < 0.01);
}
#[test]
fn freq_to_midi_negative() {
assert!(freq_to_midi(-100.0).is_infinite());
}
}