use libm::Libm;
pub const C4_HZ: f64 = 261.625_565_300_598_6;
pub const GATE_HIGH_V: f64 = 5.0;
pub const GATE_THRESHOLD_V: f64 = 2.5;
pub const MAX_ABS_VOCT: f64 = 32.0;
#[inline]
pub fn voct_to_hz(voct: f64) -> f64 {
C4_HZ * Libm::<f64>::pow(2.0, voct.clamp(-MAX_ABS_VOCT, MAX_ABS_VOCT))
}
#[inline]
pub fn env_coef(time_seconds: f64, sample_rate: f64) -> f64 {
let denom = time_seconds * sample_rate;
if denom <= 0.0 {
return 0.0;
}
Libm::<f64>::exp(-1.0 / denom)
}
#[inline]
pub fn read_interpolated(buffer: &[f64], write_pos: usize, delay_samples: f64) -> f64 {
let buffer_len = buffer.len();
let delay_int = delay_samples as usize;
let frac = delay_samples - delay_int as f64;
let read_pos1 = (write_pos + buffer_len - delay_int) % buffer_len;
let read_pos2 = (write_pos + buffer_len - delay_int - 1) % buffer_len;
let sample1 = buffer[read_pos1];
let sample2 = buffer[read_pos2];
sample1 * (1.0 - frac) + sample2 * frac
}
#[inline]
pub fn db_to_gain(db: f64) -> f64 {
Libm::<f64>::pow(10.0, db / 20.0)
}
#[inline]
pub fn gain_to_db(gain: f64) -> f64 {
20.0 * Libm::<f64>::log10(gain)
}
#[inline]
pub fn sanitize_audio(x: f64) -> f64 {
if x.is_finite() {
x
} else {
0.0
}
}
#[allow(dead_code)]
#[inline]
pub fn flush_denorm(x: f64) -> f64 {
if Libm::<f64>::fabs(x) < 1e-20 {
0.0
} else {
x
}
}
#[inline]
pub fn wrap_phase(x: f64) -> f64 {
if !x.is_finite() {
return 0.0;
}
let wrapped = x - Libm::<f64>::floor(x);
if wrapped >= 1.0 {
0.0
} else {
wrapped
}
}
#[inline]
pub fn polyblep(t: f64, dt: f64) -> f64 {
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 polyblamp(t: f64, dt: f64) -> f64 {
if t < dt {
let t = t / dt - 1.0;
-1.0 / 3.0 * t * t * t
} else if t > 1.0 - dt {
let t = (t - 1.0) / dt + 1.0;
1.0 / 3.0 * t * t * t
} else {
0.0
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Memo<const N: usize, V> {
keys: [u64; N],
val: V,
valid: bool,
#[cfg(test)]
recomputes: u64,
}
impl<const N: usize, V: Copy> Memo<N, V> {
pub(crate) fn new(placeholder: V) -> Self {
Self {
keys: [0; N],
val: placeholder,
valid: false,
#[cfg(test)]
recomputes: 0,
}
}
#[inline]
pub(crate) fn get_or_compute(&mut self, keys: [f64; N], compute: impl FnOnce() -> V) -> V {
let bits = keys.map(f64::to_bits);
if !self.valid || bits != self.keys {
self.val = compute();
self.keys = bits;
self.valid = true;
#[cfg(test)]
{
self.recomputes += 1;
}
}
self.val
}
#[cfg(test)]
pub(crate) fn invalidate(&mut self) {
self.valid = false;
}
#[cfg(test)]
pub(crate) fn recompute_count(&self) -> u64 {
self.recomputes
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct EdgeDetector {
prev: f64,
}
impl EdgeDetector {
pub fn new() -> Self {
Self { prev: 0.0 }
}
#[inline]
pub fn rising(&mut self, v: f64) -> bool {
self.rising_above(v, GATE_THRESHOLD_V)
}
#[inline]
pub fn rising_above(&mut self, v: f64, threshold: f64) -> bool {
let rising = v > threshold && self.prev <= threshold;
self.prev = v;
rising
}
#[inline]
pub fn rising_frac(&mut self, v: f64) -> Option<f64> {
self.rising_frac_above(v, GATE_THRESHOLD_V)
}
#[inline]
pub fn rising_frac_above(&mut self, v: f64, threshold: f64) -> Option<f64> {
let prev = self.prev;
self.prev = v;
if v > threshold && prev <= threshold {
let denom = v - prev;
let frac = if denom > 0.0 {
((threshold - prev) / denom).clamp(0.0, 1.0)
} else {
1.0
};
Some(frac)
} else {
None
}
}
#[inline]
pub fn reset(&mut self) {
self.prev = 0.0;
}
}
#[cfg(test)]
pub(crate) const SAFE_AUDIO_LIMIT: f64 = 10.0;
#[cfg(test)]
pub(crate) fn measure_max_output<F>(samples: usize, mut tick_fn: F) -> f64
where
F: FnMut() -> f64,
{
let mut max_abs = 0.0f64;
for _ in 0..samples {
let out = tick_fn();
max_abs = max_abs.max(out.abs());
}
max_abs
}
#[cfg(test)]
mod tests {
use super::{voct_to_hz, Memo, C4_HZ};
use libm::Libm;
#[test]
fn test_memo_first_call_always_computes() {
let mut memo: Memo<1, f64> = Memo::new(123.456);
let v = memo.get_or_compute([0.0], || 7.0);
assert_eq!(v.to_bits(), 7.0f64.to_bits());
assert_eq!(memo.recompute_count(), 1);
}
#[test]
fn test_memo_hit_and_miss() {
let mut memo: Memo<2, f64> = Memo::new(0.0);
let v1 = memo.get_or_compute([1.0, 2.0], || 3.0);
let v2 = memo.get_or_compute([1.0, 2.0], || unreachable!("must hit"));
assert_eq!(v1.to_bits(), v2.to_bits());
assert_eq!(memo.recompute_count(), 1);
let v3 = memo.get_or_compute([1.0, 2.5], || 4.0);
assert_eq!(v3.to_bits(), 4.0f64.to_bits());
assert_eq!(memo.recompute_count(), 2);
}
#[test]
fn test_memo_signed_zero_keys_are_distinct() {
let mut memo: Memo<1, f64> = Memo::new(0.0);
memo.get_or_compute([0.0], || 1.0);
let v = memo.get_or_compute([-0.0], || 2.0);
assert_eq!(v.to_bits(), 2.0f64.to_bits());
assert_eq!(memo.recompute_count(), 2);
}
#[test]
fn test_memo_nan_key_hits_identical_nan() {
let mut memo: Memo<1, f64> = Memo::new(0.0);
let v1 = memo.get_or_compute([f64::NAN], || 9.0);
let v2 = memo.get_or_compute([f64::NAN], || unreachable!("must hit"));
assert_eq!(v1.to_bits(), v2.to_bits());
assert_eq!(memo.recompute_count(), 1);
}
#[test]
fn test_memo_invalidate_forces_recompute() {
let mut memo: Memo<1, f64> = Memo::new(0.0);
memo.get_or_compute([5.0], || 1.0);
memo.invalidate();
let v = memo.get_or_compute([5.0], || 2.0);
assert_eq!(v.to_bits(), 2.0f64.to_bits());
assert_eq!(memo.recompute_count(), 2);
}
#[test]
fn voct_to_hz_is_finite_for_any_finite_input() {
for &v in &[
0.0, 1.0, -1.0, 8.5, -14.7, 32.0, -32.0, 1100.0, -1100.0, 1e300, -1e300,
] {
let hz = voct_to_hz(v);
assert!(hz.is_finite(), "voct_to_hz({v}) = {hz}");
assert!(
hz > 0.0,
"voct_to_hz({v}) = {hz} is not a positive frequency"
);
}
}
#[test]
fn voct_to_hz_is_untouched_across_the_reachable_range() {
for step in -110..=110 {
let v = step as f64 / 10.0;
let expected = C4_HZ * Libm::<f64>::pow(2.0, v);
assert_eq!(
voct_to_hz(v).to_bits(),
expected.to_bits(),
"voct_to_hz({v}) moved"
);
}
}
#[test]
fn voct_to_hz_passes_nan_through() {
assert!(voct_to_hz(f64::NAN).is_nan());
}
}