use crate::canvas::{AudioFingerprint, AudioParams, WaveType};
use deno_core::op2;
use rustfft::{num_complex::Complex32, FftPlanner};
use std::f64::consts::PI;
use std::sync::OnceLock;
#[allow(
clippy::too_many_arguments,
reason = "audio op takes many args; struct-wrapping adds churn without clarity"
)]
#[op2]
#[buffer]
pub fn op_offline_audio_render(
#[smi] seed: i32,
#[smi] sample_rate: i32,
#[smi] length: i32,
frequency: f64,
wave_type_id: i32, threshold_db: f64,
knee_db: f64,
ratio: f64,
attack_seconds: f64,
release_seconds: f64,
) -> Vec<u8> {
let wave_type = match wave_type_id {
0 => WaveType::Sine,
2 => WaveType::Square,
3 => WaveType::Sawtooth,
_ => WaveType::Triangle,
};
let seed_u64: u64 = seed as u32 as u64;
let seed_f = seed_u64 as f64;
let threshold_jitter = (seed_f * 0.31).sin() * 0.005; let release_jitter = (seed_f * 0.71).sin() * 0.0001;
let params = AudioParams {
sample_rate: sample_rate.max(0) as u32,
length: length.max(0) as u32,
frequency,
wave_type,
threshold: threshold_db + threshold_jitter,
knee: knee_db,
ratio,
attack: attack_seconds,
release: (release_seconds + release_jitter).max(0.0),
};
let fp = AudioFingerprint::from_params(seed_u64, params);
let mut bytes = Vec::with_capacity(fp.data.len() * 4);
for s in &fp.data {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes
}
fn fft_planner() -> &'static std::sync::Mutex<FftPlanner<f32>> {
static PLANNER: OnceLock<std::sync::Mutex<FftPlanner<f32>>> = OnceLock::new();
PLANNER.get_or_init(|| std::sync::Mutex::new(FftPlanner::<f32>::new()))
}
fn blackman_window(samples: &mut [f32]) {
let n = samples.len();
if n == 0 {
return;
}
let n_f = n as f64;
for (i, s) in samples.iter_mut().enumerate() {
let i_f = i as f64;
let w = 0.42 - 0.5 * (2.0 * PI * i_f / n_f).cos() + 0.08 * (4.0 * PI * i_f / n_f).cos();
*s *= w as f32;
}
}
#[op2]
#[buffer]
pub fn op_audio_analyser_freq_data(
#[buffer] time_domain_bytes: &[u8],
#[smi] fft_size: i32,
#[smi] smoothing_x100: i32,
#[buffer] prev_freq_bytes: &[u8],
) -> Vec<u8> {
let n = fft_size.clamp(32, 32768) as usize;
if !n.is_power_of_two() || time_domain_bytes.len() < n * 4 {
return Vec::new();
}
let mut samples: Vec<f32> = (0..n)
.map(|i| {
let off = i * 4;
f32::from_le_bytes([
time_domain_bytes[off],
time_domain_bytes[off + 1],
time_domain_bytes[off + 2],
time_domain_bytes[off + 3],
])
})
.collect();
blackman_window(&mut samples);
let mut buf: Vec<Complex32> = samples.iter().map(|&s| Complex32::new(s, 0.0)).collect();
{
let mut planner = fft_planner().lock().unwrap();
let fft = planner.plan_fft_forward(n);
fft.process(&mut buf);
}
let half = n / 2;
let n_f = n as f32;
let mut mags: Vec<f32> = buf[..half]
.iter()
.map(|c| (c.re * c.re + c.im * c.im).sqrt() / n_f)
.collect();
let smoothing = (smoothing_x100.clamp(0, 100) as f32) / 100.0;
if prev_freq_bytes.len() >= half * 4 && smoothing > 0.0 {
for (i, m) in mags.iter_mut().enumerate() {
let off = i * 4;
let prev = f32::from_le_bytes([
prev_freq_bytes[off],
prev_freq_bytes[off + 1],
prev_freq_bytes[off + 2],
prev_freq_bytes[off + 3],
]);
*m = prev * smoothing + *m * (1.0 - smoothing);
}
}
let db_to_bytes: Vec<u8> = mags
.iter()
.flat_map(|&m| {
let db = if m > 0.0 {
20.0 * m.log10()
} else {
-f32::INFINITY
};
let clamped = db.clamp(-100.0, -30.0);
clamped.to_le_bytes()
})
.collect();
db_to_bytes
}
#[derive(Clone, Copy)]
enum BiquadType {
Lowpass = 0,
Highpass = 1,
Bandpass = 2,
Lowshelf = 3,
Highshelf = 4,
Peaking = 5,
Notch = 6,
Allpass = 7,
}
impl BiquadType {
fn from_id(id: u32) -> Self {
match id {
0 => Self::Lowpass,
1 => Self::Highpass,
2 => Self::Bandpass,
3 => Self::Lowshelf,
4 => Self::Highshelf,
5 => Self::Peaking,
6 => Self::Notch,
7 => Self::Allpass,
_ => Self::Lowpass,
}
}
}
#[allow(
clippy::eq_op,
reason = "(1/1 - 1) is the spec's (1/S - 1) with shelf-slope S=1, written verbatim to mirror the W3C formula"
)]
fn biquad_coeffs(
kind: BiquadType,
frequency: f64,
q: f64,
gain: f64,
sample_rate: f64,
) -> (f64, f64, f64, f64, f64) {
let w0 = 2.0 * PI * frequency / sample_rate;
let cos_w0 = w0.cos();
let sin_w0 = w0.sin();
let alpha_q = sin_w0 / (2.0 * q);
let a_factor = 10f64.powf(gain / 40.0);
let (b0, b1, b2, a0, a1, a2) = match kind {
BiquadType::Lowpass => {
let b1 = 1.0 - cos_w0;
let b0 = b1 / 2.0;
(b0, b1, b0, 1.0 + alpha_q, -2.0 * cos_w0, 1.0 - alpha_q)
}
BiquadType::Highpass => {
let b1 = -(1.0 + cos_w0);
let b0 = (1.0 + cos_w0) / 2.0;
(b0, b1, b0, 1.0 + alpha_q, -2.0 * cos_w0, 1.0 - alpha_q)
}
BiquadType::Bandpass => (
alpha_q,
0.0,
-alpha_q,
1.0 + alpha_q,
-2.0 * cos_w0,
1.0 - alpha_q,
),
BiquadType::Notch => (
1.0,
-2.0 * cos_w0,
1.0,
1.0 + alpha_q,
-2.0 * cos_w0,
1.0 - alpha_q,
),
BiquadType::Allpass => (
1.0 - alpha_q,
-2.0 * cos_w0,
1.0 + alpha_q,
1.0 + alpha_q,
-2.0 * cos_w0,
1.0 - alpha_q,
),
BiquadType::Peaking => (
1.0 + alpha_q * a_factor,
-2.0 * cos_w0,
1.0 - alpha_q * a_factor,
1.0 + alpha_q / a_factor,
-2.0 * cos_w0,
1.0 - alpha_q / a_factor,
),
BiquadType::Lowshelf => {
let two_sqrt = 2.0 * a_factor.sqrt() * alpha_q;
let b0 = a_factor * ((a_factor + 1.0) - (a_factor - 1.0) * cos_w0 + two_sqrt);
let b1 = 2.0 * a_factor * ((a_factor - 1.0) - (a_factor + 1.0) * cos_w0);
let b2 = a_factor * ((a_factor + 1.0) - (a_factor - 1.0) * cos_w0 - two_sqrt);
let a0 = (a_factor + 1.0) + (a_factor - 1.0) * cos_w0 + two_sqrt;
let a1 = -2.0 * ((a_factor - 1.0) + (a_factor + 1.0) * cos_w0);
let a2 = (a_factor + 1.0) + (a_factor - 1.0) * cos_w0 - two_sqrt;
(b0, b1, b2, a0, a1, a2)
}
BiquadType::Highshelf => {
let two_sqrt = 2.0 * a_factor.sqrt() * alpha_q;
let b0 = a_factor * ((a_factor + 1.0) + (a_factor - 1.0) * cos_w0 + two_sqrt);
let b1 = -2.0 * a_factor * ((a_factor - 1.0) + (a_factor + 1.0) * cos_w0);
let b2 = a_factor * ((a_factor + 1.0) + (a_factor - 1.0) * cos_w0 - two_sqrt);
let a0 = (a_factor + 1.0) - (a_factor - 1.0) * cos_w0 + two_sqrt;
let a1 = 2.0 * ((a_factor - 1.0) - (a_factor + 1.0) * cos_w0);
let a2 = (a_factor + 1.0) - (a_factor - 1.0) * cos_w0 - two_sqrt;
(b0, b1, b2, a0, a1, a2)
}
};
(b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0)
}
#[allow(
clippy::too_many_arguments,
reason = "audio op takes many args; struct-wrapping adds churn without clarity"
)]
#[op2]
#[buffer]
pub fn op_audio_biquad_response(
#[buffer] freq_in_bytes: &[u8],
#[smi] filter_type_id: i32,
frequency: f64,
q: f64,
gain: f64,
sample_rate: f64,
) -> Vec<u8> {
let n = freq_in_bytes.len() / 4;
if n == 0 {
return Vec::new();
}
let kind = BiquadType::from_id(filter_type_id.max(0) as u32);
let (b0, b1, b2, a1, a2) = biquad_coeffs(kind, frequency, q, gain, sample_rate);
let mut out = Vec::with_capacity(n * 8);
let mut mags = Vec::with_capacity(n);
let mut phases = Vec::with_capacity(n);
for i in 0..n {
let off = i * 4;
let f = f32::from_le_bytes([
freq_in_bytes[off],
freq_in_bytes[off + 1],
freq_in_bytes[off + 2],
freq_in_bytes[off + 3],
]) as f64;
let w = 2.0 * PI * f / sample_rate;
let (cw, sw) = (w.cos(), w.sin());
let num_re = b0 + b1 * cw + b2 * (2.0 * w).cos();
let num_im = -b1 * sw - b2 * (2.0 * w).sin();
let den_re = 1.0 + a1 * cw + a2 * (2.0 * w).cos();
let den_im = -a1 * sw - a2 * (2.0 * w).sin();
let den_mag2 = den_re * den_re + den_im * den_im;
let h_re = (num_re * den_re + num_im * den_im) / den_mag2;
let h_im = (num_im * den_re - num_re * den_im) / den_mag2;
let mag = (h_re * h_re + h_im * h_im).sqrt();
let phase = h_im.atan2(h_re);
mags.push(mag as f32);
phases.push(phase as f32);
}
for m in mags {
out.extend_from_slice(&m.to_le_bytes());
}
for p in phases {
out.extend_from_slice(&p.to_le_bytes());
}
out
}
deno_core::extension!(
audio_extension,
ops = [
op_offline_audio_render,
op_audio_analyser_freq_data,
op_audio_biquad_response,
],
);
#[cfg(test)]
mod tests {
use super::*;
fn f32_bytes(samples: &[f32]) -> Vec<u8> {
samples.iter().flat_map(|s| s.to_le_bytes()).collect()
}
fn bytes_to_f32(bytes: &[u8]) -> Vec<f32> {
bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
#[test]
fn analyser_returns_db_clamped_to_minus_100_to_minus_30() {
let samples = vec![0.0_f32; 256];
let bytes = op_audio_analyser_freq_data_inner(&f32_bytes(&samples), 256, 0, &[]);
let mags = bytes_to_f32(&bytes);
assert_eq!(mags.len(), 128);
for m in mags {
assert_eq!(m, -100.0);
}
}
#[test]
fn analyser_pure_sine_peaks_at_expected_bin() {
let n = 1024;
let mut samples = vec![0.0_f32; n];
for (i, s) in samples.iter_mut().enumerate() {
*s = (2.0 * std::f32::consts::PI * 1000.0 * i as f32 / 44100.0).sin();
}
let bytes = op_audio_analyser_freq_data_inner(&f32_bytes(&samples), n as i32, 0, &[]);
let mags = bytes_to_f32(&bytes);
let max_idx = mags
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0;
assert!(
(21..=25).contains(&max_idx),
"expected peak near bin 23, got {max_idx}"
);
}
#[test]
fn analyser_smoothing_with_prev_data() {
let samples = vec![0.0_f32; 256];
let prev = vec![0.5_f32; 128];
let bytes = op_audio_analyser_freq_data_inner(
&f32_bytes(&samples),
256,
100, &f32_bytes(&prev),
);
let dbs = bytes_to_f32(&bytes);
assert!(dbs.iter().all(|&d| d == -30.0), "got {:?}", &dbs[..5]);
}
fn op_audio_analyser_freq_data_inner(
time_domain_bytes: &[u8],
fft_size: i32,
smoothing_x100: i32,
prev_freq_bytes: &[u8],
) -> Vec<u8> {
let n = fft_size.clamp(32, 32768) as usize;
if !n.is_power_of_two() || time_domain_bytes.len() < n * 4 {
return Vec::new();
}
let mut samples: Vec<f32> = (0..n)
.map(|i| {
let off = i * 4;
f32::from_le_bytes([
time_domain_bytes[off],
time_domain_bytes[off + 1],
time_domain_bytes[off + 2],
time_domain_bytes[off + 3],
])
})
.collect();
blackman_window(&mut samples);
let mut buf: Vec<Complex32> = samples.iter().map(|&s| Complex32::new(s, 0.0)).collect();
{
let mut planner = fft_planner().lock().unwrap();
let fft = planner.plan_fft_forward(n);
fft.process(&mut buf);
}
let half = n / 2;
let n_f = n as f32;
let mut mags: Vec<f32> = buf[..half]
.iter()
.map(|c| (c.re * c.re + c.im * c.im).sqrt() / n_f)
.collect();
let smoothing = (smoothing_x100.clamp(0, 100) as f32) / 100.0;
if prev_freq_bytes.len() >= half * 4 && smoothing > 0.0 {
for (i, m) in mags.iter_mut().enumerate() {
let off = i * 4;
let prev = f32::from_le_bytes([
prev_freq_bytes[off],
prev_freq_bytes[off + 1],
prev_freq_bytes[off + 2],
prev_freq_bytes[off + 3],
]);
*m = prev * smoothing + *m * (1.0 - smoothing);
}
}
mags.iter()
.flat_map(|&m| {
let db = if m > 0.0 {
20.0 * m.log10()
} else {
-f32::INFINITY
};
db.clamp(-100.0, -30.0).to_le_bytes()
})
.collect()
}
#[test]
#[allow(
clippy::approx_constant,
reason = "0.7071 = Butterworth Q (1/sqrt(2)) filter arg"
)]
fn biquad_lowpass_dc_passes_unity() {
let freqs = vec![0.0_f32];
let bytes = op_audio_biquad_response_inner(
&f32_bytes(&freqs),
BiquadType::Lowpass as i32,
1000.0,
0.7071,
0.0,
44100.0,
);
let out = bytes_to_f32(&bytes);
let (mag, phase) = (out[0], out[1]);
assert!((mag - 1.0).abs() < 1e-3, "DC mag {mag}");
assert!(phase.abs() < 1e-3, "DC phase {phase}");
}
#[test]
#[allow(
clippy::approx_constant,
reason = "0.7071 = Butterworth Q (1/sqrt(2)) filter arg"
)]
fn biquad_highpass_dc_blocks() {
let freqs = vec![0.0_f32];
let bytes = op_audio_biquad_response_inner(
&f32_bytes(&freqs),
BiquadType::Highpass as i32,
1000.0,
0.7071,
0.0,
44100.0,
);
let out = bytes_to_f32(&bytes);
assert!(
out[0] < 1e-3,
"highpass DC mag should be ~0, got {}",
out[0]
);
}
#[test]
fn biquad_returns_2n_floats_for_n_input() {
let freqs = vec![100.0_f32, 200.0, 1000.0, 5000.0, 20000.0];
let bytes = op_audio_biquad_response_inner(
&f32_bytes(&freqs),
BiquadType::Bandpass as i32,
1000.0,
1.0,
0.0,
44100.0,
);
let out = bytes_to_f32(&bytes);
assert_eq!(out.len(), 2 * freqs.len());
for &m in &out[..freqs.len()] {
assert!(m >= 0.0 && m.is_finite());
}
for &p in &out[freqs.len()..] {
assert!(p.is_finite());
assert!(p.abs() <= std::f32::consts::PI + 1e-3);
}
}
fn op_audio_biquad_response_inner(
freq_in_bytes: &[u8],
filter_type_id: i32,
frequency: f64,
q: f64,
gain: f64,
sample_rate: f64,
) -> Vec<u8> {
let n = freq_in_bytes.len() / 4;
if n == 0 {
return Vec::new();
}
let kind = BiquadType::from_id(filter_type_id.max(0) as u32);
let (b0, b1, b2, a1, a2) = biquad_coeffs(kind, frequency, q, gain, sample_rate);
let mut mags = Vec::with_capacity(n);
let mut phases = Vec::with_capacity(n);
for i in 0..n {
let off = i * 4;
let f = f32::from_le_bytes([
freq_in_bytes[off],
freq_in_bytes[off + 1],
freq_in_bytes[off + 2],
freq_in_bytes[off + 3],
]) as f64;
let w = 2.0 * PI * f / sample_rate;
let (cw, sw) = (w.cos(), w.sin());
let num_re = b0 + b1 * cw + b2 * (2.0 * w).cos();
let num_im = -b1 * sw - b2 * (2.0 * w).sin();
let den_re = 1.0 + a1 * cw + a2 * (2.0 * w).cos();
let den_im = -a1 * sw - a2 * (2.0 * w).sin();
let den_mag2 = den_re * den_re + den_im * den_im;
let h_re = (num_re * den_re + num_im * den_im) / den_mag2;
let h_im = (num_im * den_re - num_re * den_im) / den_mag2;
mags.push((h_re * h_re + h_im * h_im).sqrt() as f32);
phases.push((h_im.atan2(h_re)) as f32);
}
let mut out = Vec::with_capacity(n * 8);
for m in mags {
out.extend_from_slice(&m.to_le_bytes());
}
for p in phases {
out.extend_from_slice(&p.to_le_bytes());
}
out
}
}