extern crate alloc;
use alloc::string::String;
use core::f64::consts;
use crate::address::Address;
use crate::float::Float;
use crate::vm::{GenericVm, VmError};
use crate::word::Word;
pub fn register_audio_natives<'a, 'arena, W: Word, A: Address, F: Float>(
vm: &mut GenericVm<'a, 'arena, W, A, F>,
) {
vm.register_fn("audio::midi_to_freq", |note: i64| -> f64 {
440.0 * libm::pow(2.0, (note - 69) as f64 / 12.0)
});
vm.register_fn_fallible("audio::freq_to_midi", |freq: f64| -> Result<i64, VmError> {
if freq <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::freq_to_midi: frequency must be positive",
)));
}
let note = 69.0 + 12.0 * libm::log2(freq / 440.0);
Ok(libm::round(note) as i64)
});
vm.register_fn("audio::cents_to_ratio", |cents: f64| -> f64 {
libm::pow(2.0, cents / 1200.0)
});
vm.register_fn_fallible(
"audio::ratio_to_cents",
|ratio: f64| -> Result<f64, VmError> {
if ratio <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::ratio_to_cents: ratio must be strictly positive",
)));
}
Ok(1200.0 * libm::log2(ratio))
},
);
vm.register_fn("audio::semitones_to_ratio", |semitones: f64| -> f64 {
libm::pow(2.0, semitones / 12.0)
});
vm.register_fn_fallible(
"audio::ratio_to_semitones",
|ratio: f64| -> Result<f64, VmError> {
if ratio <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::ratio_to_semitones: ratio must be strictly positive",
)));
}
Ok(12.0 * libm::log2(ratio))
},
);
vm.register_fn("audio::db_to_linear", |db: f64| -> f64 {
libm::pow(10.0, db / 20.0)
});
vm.register_fn_fallible(
"audio::linear_to_db",
|linear: f64| -> Result<f64, VmError> {
if linear <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::linear_to_db: amplitude must be positive",
)));
}
Ok(20.0 * libm::log10(linear))
},
);
vm.register_fn_fallible(
"audio::ms_to_samples",
|ms: f64, sample_rate: f64| -> Result<f64, VmError> {
if sample_rate <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::ms_to_samples: sample_rate must be strictly positive",
)));
}
Ok(ms * sample_rate / 1000.0)
},
);
vm.register_fn_fallible(
"audio::samples_to_ms",
|samples: f64, sample_rate: f64| -> Result<f64, VmError> {
if sample_rate <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::samples_to_ms: sample_rate must be strictly positive",
)));
}
Ok(samples * 1000.0 / sample_rate)
},
);
vm.register_fn_fallible(
"audio::onepole_lpf_alpha",
|cutoff_hz: f64, sample_rate: f64| -> Result<f64, VmError> {
if sample_rate <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::onepole_lpf_alpha: sample_rate must be strictly positive",
)));
}
if cutoff_hz < 0.0 {
return Err(VmError::NativeError(String::from(
"audio::onepole_lpf_alpha: cutoff_hz must be non-negative",
)));
}
Ok(1.0 - libm::exp(-2.0 * consts::PI * cutoff_hz / sample_rate))
},
);
vm.register_fn_fallible(
"audio::onepole_hpf_alpha",
|cutoff_hz: f64, sample_rate: f64| -> Result<f64, VmError> {
if sample_rate <= 0.0 {
return Err(VmError::NativeError(String::from(
"audio::onepole_hpf_alpha: sample_rate must be strictly positive",
)));
}
if cutoff_hz < 0.0 {
return Err(VmError::NativeError(String::from(
"audio::onepole_hpf_alpha: cutoff_hz must be non-negative",
)));
}
Ok(libm::exp(-2.0 * consts::PI * cutoff_hz / sample_rate))
},
);
vm.register_fn("audio::pan_law", |pos: f64| -> (f64, f64) {
let clamped = pos.clamp(-1.0, 1.0);
let theta = (clamped + 1.0) * consts::FRAC_PI_4;
(libm::cos(theta), libm::sin(theta))
});
}
#[cfg(all(test, feature = "compile", feature = "verify"))]
mod tests {
use super::*;
use crate::bytecode::Value;
use crate::compiler::compile;
use crate::lexer::tokenize;
use crate::parser::parse;
use crate::vm::{DEFAULT_ARENA_CAPACITY, Vm, VmState};
fn run_with_audio(src: &str) -> Value {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
let module = compile(&program).expect("compile error");
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
register_audio_natives(&mut vm);
match vm.call(&[]).unwrap() {
VmState::Finished(v) => v,
VmState::Yielded(v) => panic!("unexpected yield: {:?}", v),
VmState::Reset => panic!("unexpected reset"),
}
}
fn assert_close(val: Value, expected: f64, tol: f64) {
match val {
Value::Float(f) => assert!(
(f - expected).abs() < tol,
"expected ~{}, got {}",
expected,
f
),
other => panic!("expected Float, got {:?}", other),
}
}
#[test]
fn midi_to_freq_a4() {
assert_close(
run_with_audio(
"use audio::midi_to_freq\nfn main() -> Float { audio::midi_to_freq(69) }",
),
440.0,
0.01,
);
}
#[test]
fn midi_to_freq_c4() {
assert_close(
run_with_audio(
"use audio::midi_to_freq\nfn main() -> Float { audio::midi_to_freq(60) }",
),
261.6256,
0.01,
);
}
#[test]
fn freq_to_midi_440() {
let val = run_with_audio(
"use audio::freq_to_midi\nfn main() -> Word { audio::freq_to_midi(440.0) }",
);
assert_eq!(val, Value::Int(69));
}
#[test]
fn cents_to_ratio_one_octave() {
assert_close(
run_with_audio(
"use audio::cents_to_ratio\nfn main() -> Float { audio::cents_to_ratio(1200.0) }",
),
2.0,
1e-9,
);
}
#[test]
fn ratio_to_cents_octave() {
assert_close(
run_with_audio(
"use audio::ratio_to_cents\nfn main() -> Float { audio::ratio_to_cents(2.0) }",
),
1200.0,
1e-9,
);
}
#[test]
fn semitones_to_ratio_octave() {
assert_close(
run_with_audio(
"use audio::semitones_to_ratio\nfn main() -> Float { audio::semitones_to_ratio(12.0) }",
),
2.0,
1e-9,
);
}
#[test]
fn ratio_to_semitones_octave() {
assert_close(
run_with_audio(
"use audio::ratio_to_semitones\nfn main() -> Float { audio::ratio_to_semitones(2.0) }",
),
12.0,
1e-9,
);
}
#[test]
fn db_to_linear_zero() {
assert_close(
run_with_audio(
"use audio::db_to_linear\nfn main() -> Float { audio::db_to_linear(0.0) }",
),
1.0,
1e-9,
);
}
#[test]
fn db_to_linear_minus6() {
assert_close(
run_with_audio(
"use audio::db_to_linear\nfn main() -> Float { audio::db_to_linear(-6.0) }",
),
0.501187,
1e-4,
);
}
#[test]
fn linear_to_db_one() {
assert_close(
run_with_audio(
"use audio::linear_to_db\nfn main() -> Float { audio::linear_to_db(1.0) }",
),
0.0,
1e-9,
);
}
#[test]
fn ms_to_samples_round_trip() {
assert_close(
run_with_audio(
"use audio::ms_to_samples\nfn main() -> Float { audio::ms_to_samples(1000.0, 48000.0) }",
),
48000.0,
1e-9,
);
}
#[test]
fn samples_to_ms_round_trip() {
assert_close(
run_with_audio(
"use audio::samples_to_ms\nfn main() -> Float { audio::samples_to_ms(48000.0, 48000.0) }",
),
1000.0,
1e-9,
);
}
#[test]
fn onepole_lpf_alpha_at_nyquist_approaches_one() {
let val = run_with_audio(
"use audio::onepole_lpf_alpha\nfn main() -> Float { audio::onepole_lpf_alpha(10000.0, 48000.0) }",
);
match val {
Value::Float(f) => {
assert!(f > 0.0 && f < 1.0, "alpha out of range: {}", f);
}
other => panic!("expected Float, got {:?}", other),
}
}
#[test]
fn onepole_lpf_alpha_zero_cutoff_is_zero() {
assert_close(
run_with_audio(
"use audio::onepole_lpf_alpha\nfn main() -> Float { audio::onepole_lpf_alpha(0.0, 48000.0) }",
),
0.0,
1e-12,
);
}
#[test]
fn onepole_hpf_alpha_zero_cutoff_is_one() {
assert_close(
run_with_audio(
"use audio::onepole_hpf_alpha\nfn main() -> Float { audio::onepole_hpf_alpha(0.0, 48000.0) }",
),
1.0,
1e-12,
);
}
#[test]
fn pan_law_centre() {
let val = run_with_audio(
"use audio::pan_law\nfn main() -> (Float, Float) { audio::pan_law(0.0) }",
);
match val {
Value::Tuple(elems) => {
assert_eq!(elems.len(), 2);
let l = match elems[0] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
let r = match elems[1] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
let target = core::f64::consts::FRAC_1_SQRT_2;
assert!(
(l - target).abs() < 1e-9,
"left expected ~{}, got {}",
target,
l
);
assert!(
(r - target).abs() < 1e-9,
"right expected ~{}, got {}",
target,
r
);
}
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn pan_law_full_left() {
let val = run_with_audio(
"use audio::pan_law\nfn main() -> (Float, Float) { audio::pan_law(-1.0) }",
);
match val {
Value::Tuple(elems) => {
let l = match elems[0] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
let r = match elems[1] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
assert!((l - 1.0).abs() < 1e-9, "left expected 1.0, got {}", l);
assert!(r.abs() < 1e-9, "right expected 0.0, got {}", r);
}
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn pan_law_full_right() {
let val = run_with_audio(
"use audio::pan_law\nfn main() -> (Float, Float) { audio::pan_law(1.0) }",
);
match val {
Value::Tuple(elems) => {
let l = match elems[0] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
let r = match elems[1] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
assert!(l.abs() < 1e-9, "left expected 0.0, got {}", l);
assert!((r - 1.0).abs() < 1e-9, "right expected 1.0, got {}", r);
}
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn pan_law_clamps_out_of_range() {
let val = run_with_audio(
"use audio::pan_law\nfn main() -> (Float, Float) { audio::pan_law(2.0) }",
);
match val {
Value::Tuple(elems) => {
let l = match elems[0] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
let r = match elems[1] {
Value::Float(f) => f,
_ => panic!("expected Float"),
};
assert!(l.abs() < 1e-9, "left expected 0.0, got {}", l);
assert!((r - 1.0).abs() < 1e-9, "right expected 1.0, got {}", r);
}
other => panic!("expected tuple, got {:?}", other),
}
}
#[test]
fn freq_to_midi_nonpositive_error() {
let tokens =
tokenize("use audio::freq_to_midi\nfn main() -> Word { audio::freq_to_midi(0.0) }")
.unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
register_audio_natives(&mut vm);
assert!(vm.call(&[]).is_err());
}
#[test]
fn ms_to_samples_bad_sample_rate_error() {
let tokens = tokenize(
"use audio::ms_to_samples\nfn main() -> Float { audio::ms_to_samples(10.0, 0.0) }",
)
.unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
register_audio_natives(&mut vm);
assert!(vm.call(&[]).is_err());
}
#[test]
fn onepole_lpf_alpha_negative_cutoff_error() {
let tokens = tokenize(
"use audio::onepole_lpf_alpha\nfn main() -> Float { audio::onepole_lpf_alpha(-1.0, 48000.0) }",
)
.unwrap();
let program = parse(&tokens).unwrap();
let module = compile(&program).unwrap();
let arena = keleusma_arena::Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).unwrap();
register_audio_natives(&mut vm);
assert!(vm.call(&[]).is_err());
}
}