Skip to main content

sesh_sdk/
vec.rs

1#![allow(unused_variables)]
2//! Vector operations for batch audio processing.
3//!
4//! Each op has two code paths: an inline Rust fallback (always available) and a
5//! host-accelerated import (used when `sesh_vec_version() > 0`). The SDK selects
6//! the path at runtime. Plugin authors call the same functions regardless of platform.
7
8#[cfg(target_arch = "wasm32")]
9use std::sync::atomic::{AtomicU32, Ordering};
10
11// ---------------------------------------------------------------------------
12// Host capability detection
13// ---------------------------------------------------------------------------
14
15#[cfg(target_arch = "wasm32")]
16extern "C" {
17    fn sesh_vec_version() -> u32;
18}
19
20/// Cached host vec version. 0 = not yet queried, u32::MAX = stubs (web).
21#[cfg(target_arch = "wasm32")]
22static HOST_VEC_VERSION: AtomicU32 = AtomicU32::new(0);
23
24#[cfg(target_arch = "wasm32")]
25fn host_version() -> u32 {
26    let v = HOST_VEC_VERSION.load(Ordering::Relaxed);
27    if v != 0 {
28        return v;
29    }
30    let v = unsafe { sesh_vec_version() };
31    // Store non-zero so we don't re-query. If host returns 0, store a sentinel.
32    let store = if v == 0 { u32::MAX } else { v };
33    HOST_VEC_VERSION.store(store, Ordering::Relaxed);
34    v
35}
36
37#[inline]
38#[allow(dead_code)]
39fn use_host_ops() -> bool {
40    #[cfg(target_arch = "wasm32")]
41    { host_version() > 0 && host_version() != u32::MAX }
42    #[cfg(not(target_arch = "wasm32"))]
43    { false }
44}
45
46// ---------------------------------------------------------------------------
47// Dispatch: host-accelerated in production, inline fallback in tests
48// ---------------------------------------------------------------------------
49
50macro_rules! dispatch {
51    ($host:expr, $fallback:expr) => {{
52        #[cfg(target_arch = "wasm32")]
53        {
54            if use_host_ops() { $host } else { $fallback }
55        }
56        #[cfg(not(target_arch = "wasm32"))]
57        { $fallback }
58    }};
59}
60
61// ---------------------------------------------------------------------------
62// Host imports (C ABI, raw pointers) — not linked in test builds
63// ---------------------------------------------------------------------------
64
65#[cfg(target_arch = "wasm32")]
66extern "C" {
67    fn sesh_vec_copy_host(dst: *mut f32, src: *const f32, len: u32);
68    fn sesh_vec_fill_host(dst: *mut f32, value: f32, len: u32);
69    fn sesh_vec_add_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
70    fn sesh_vec_add_scalar_host(dst: *mut f32, value: f32, len: u32);
71    fn sesh_vec_mul_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
72    fn sesh_vec_mul_scalar_host(dst: *mut f32, value: f32, len: u32);
73    fn sesh_vec_mul_add_host(dst: *mut f32, src: *const f32, gain: f32, len: u32);
74    fn sesh_vec_clamp_host(dst: *mut f32, src: *const f32, min: f32, max: f32, len: u32);
75    fn sesh_vec_ring_write_host(
76        buf: *mut f32, buf_len: u32, pos: *mut u32, src: *const f32, len: u32,
77    );
78    fn sesh_vec_ring_read_host(
79        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, offset: u32, len: u32,
80    );
81    fn sesh_vec_delay_read_host(
82        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, time: *const f32, len: u32,
83    );
84    fn sesh_vec_osc_host(
85        phase: *mut f32, dst: *mut f32, freq: f32, waveform: u32, sample_rate: f32, len: u32,
86    );
87    fn sesh_vec_biquad_host(
88        state: *mut f32, dst: *mut f32, src: *const f32,
89        cutoff: *const f32, q: *const f32, gain: *const f32,
90        filter_type: u32, sample_rate: f32, len: u32,
91    );
92    fn sesh_vec_envelope_host(
93        state: *mut f32, dst: *mut f32, src: *const f32,
94        attack: *const f32, release: *const f32,
95        mode: u32, sample_rate: f32, len: u32,
96    );
97    fn sesh_vec_tanh_host(dst: *mut f32, src: *const f32, drive: *const f32, len: u32);
98    fn sesh_vec_hard_clip_host(dst: *mut f32, src: *const f32, threshold: *const f32, len: u32);
99    fn sesh_vec_abs_host(dst: *mut f32, src: *const f32, len: u32);
100    fn sesh_vec_neg_host(dst: *mut f32, src: *const f32, len: u32);
101    fn sesh_vec_sqrt_host(dst: *mut f32, src: *const f32, len: u32);
102    fn sesh_vec_recip_host(dst: *mut f32, src: *const f32, len: u32);
103    fn sesh_vec_div_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
104    fn sesh_vec_pow_host(dst: *mut f32, src: *const f32, exp: *const f32, len: u32);
105    fn sesh_vec_schroeder_allpass_host(
106        buf: *mut f32, buf_len: u32, pos: *mut u32,
107        dst: *mut f32, src: *const f32,
108        delay: u32, g: f32, len: u32,
109    );
110}
111
112// ---------------------------------------------------------------------------
113// Enums and state types
114// ---------------------------------------------------------------------------
115
116/// Oscillator waveform shape.
117#[repr(u32)]
118#[derive(Clone, Copy)]
119pub enum Waveform {
120    Sine = 0,
121    Triangle = 1,
122    Saw = 2,
123    Square = 3,
124}
125
126/// Biquad filter type.
127#[repr(u32)]
128#[derive(Clone, Copy)]
129pub enum FilterType {
130    Lowpass = 0,
131    Highpass = 1,
132    Bandpass = 2,
133    Notch = 3,
134    /// Parametric EQ band — boost/cut at cutoff frequency.
135    Peak = 4,
136    /// Boost/cut below cutoff frequency.
137    LowShelf = 5,
138    /// Boost/cut above cutoff frequency.
139    HighShelf = 6,
140    /// Phase shift without changing amplitude — used in phasers.
141    Allpass = 7,
142}
143
144/// Internal state for a biquad filter (two-sample history).
145#[derive(Clone, Copy)]
146#[repr(C)]
147pub struct BiquadState {
148    pub x1: f32,
149    pub x2: f32,
150    pub y1: f32,
151    pub y2: f32,
152}
153
154impl BiquadState {
155    pub const fn new() -> Self {
156        Self { x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
157    }
158}
159
160/// Envelope follower detection mode.
161#[repr(u32)]
162#[derive(Clone, Copy)]
163pub enum EnvelopeMode {
164    /// Track instantaneous peaks.
165    Peak = 0,
166    /// Track root-mean-square level.
167    Rms = 1,
168}
169
170/// Internal state for an envelope follower.
171#[derive(Clone, Copy)]
172#[repr(C)]
173pub struct EnvelopeState {
174    pub current: f32,
175}
176
177impl EnvelopeState {
178    pub const fn new() -> Self {
179        Self { current: 0.0 }
180    }
181}
182
183/// Internal state for a one-pole filter (single-sample history).
184#[derive(Clone, Copy)]
185#[repr(C)]
186pub struct OnePoleState {
187    pub y1: f32,
188}
189
190impl OnePoleState {
191    pub const fn new() -> Self {
192        Self { y1: 0.0 }
193    }
194}
195
196// ===========================================================================
197// Math ops
198// ===========================================================================
199
200/// Copy `src` into `dst`.
201pub fn vec_copy(dst: &mut [f32], src: &[f32]) {
202    let len = dst.len().min(src.len());
203    dispatch!(
204        unsafe { sesh_vec_copy_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) },
205        dst[..len].copy_from_slice(&src[..len])
206    );
207}
208
209/// Fill `dst` with a constant value.
210pub fn vec_fill(dst: &mut [f32], value: f32) {
211    let len = dst.len();
212    dispatch!(
213        unsafe { sesh_vec_fill_host(dst.as_mut_ptr(), value, len as u32) },
214        for s in dst.iter_mut() { *s = value; }
215    );
216}
217
218/// Element-wise addition: `dst[i] = a[i] + b[i]`.
219pub fn vec_add(dst: &mut [f32], a: &[f32], b: &[f32]) {
220    let len = dst.len().min(a.len()).min(b.len());
221    dispatch!(
222        unsafe { sesh_vec_add_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) },
223        for i in 0..len { dst[i] = a[i] + b[i]; }
224    );
225}
226
227/// Add scalar to every element: `dst[i] += value`.
228pub fn vec_add_scalar(dst: &mut [f32], value: f32) {
229    let len = dst.len();
230    dispatch!(
231        unsafe { sesh_vec_add_scalar_host(dst.as_mut_ptr(), value, len as u32) },
232        for s in dst.iter_mut() { *s += value; }
233    );
234}
235
236/// Element-wise multiplication: `dst[i] = a[i] * b[i]`.
237pub fn vec_mul(dst: &mut [f32], a: &[f32], b: &[f32]) {
238    let len = dst.len().min(a.len()).min(b.len());
239    dispatch!(
240        unsafe { sesh_vec_mul_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) },
241        for i in 0..len { dst[i] = a[i] * b[i]; }
242    );
243}
244
245/// Multiply every element by scalar: `dst[i] *= value`.
246pub fn vec_mul_scalar(dst: &mut [f32], value: f32) {
247    let len = dst.len();
248    dispatch!(
249        unsafe { sesh_vec_mul_scalar_host(dst.as_mut_ptr(), value, len as u32) },
250        for s in dst.iter_mut() { *s *= value; }
251    );
252}
253
254/// Multiply and accumulate: `dst[i] += src[i] * gain`.
255pub fn vec_mul_add(dst: &mut [f32], src: &[f32], gain: f32) {
256    let len = dst.len().min(src.len());
257    dispatch!(
258        unsafe { sesh_vec_mul_add_host(dst.as_mut_ptr(), src.as_ptr(), gain, len as u32) },
259        for i in 0..len { dst[i] += src[i] * gain; }
260    );
261}
262
263/// Clamp: `dst[i] = clamp(src[i], min, max)`.
264pub fn vec_clamp(dst: &mut [f32], src: &[f32], min: f32, max: f32) {
265    let len = dst.len().min(src.len());
266    dispatch!(
267        unsafe { sesh_vec_clamp_host(dst.as_mut_ptr(), src.as_ptr(), min, max, len as u32) },
268        for i in 0..len { dst[i] = src[i].clamp(min, max); }
269    );
270}
271
272/// In-place clamp: `dst[i] = clamp(dst[i], min, max)`.
273pub fn vec_clamp_assign(dst: &mut [f32], min: f32, max: f32) {
274    let len = dst.len();
275    dispatch!(
276        unsafe { sesh_vec_clamp_host(dst.as_mut_ptr(), dst.as_ptr(), min, max, len as u32) },
277        for i in 0..len { dst[i] = dst[i].clamp(min, max); }
278    );
279}
280
281// ===========================================================================
282// Circular buffer ops
283// ===========================================================================
284
285/// Write `src` into circular buffer `buf` starting at `*pos`, wrapping at `buf.len()`.
286/// Advances `*pos` by `src.len()`.
287pub fn vec_ring_write(buf: &mut [f32], pos: &mut usize, src: &[f32]) {
288    let buf_len = buf.len();
289    let frames = src.len();
290    dispatch!(
291        {
292            let mut pos32 = *pos as u32;
293            unsafe {
294                sesh_vec_ring_write_host(
295                    buf.as_mut_ptr(), buf_len as u32, &mut pos32, src.as_ptr(), frames as u32,
296                );
297            }
298            *pos = pos32 as usize;
299        },
300        {
301            for i in 0..frames {
302                buf[(*pos + i) % buf_len] = src[i];
303            }
304            *pos = (*pos + frames) % buf_len;
305        }
306    );
307}
308
309/// Read `dst.len()` contiguous samples from circular buffer at `pos - offset`, wrapping.
310pub fn vec_ring_read(buf: &[f32], pos: usize, dst: &mut [f32], offset: usize) {
311    let buf_len = buf.len();
312    let frames = dst.len();
313    dispatch!(
314        unsafe {
315            sesh_vec_ring_read_host(
316                buf.as_ptr(), buf_len as u32, pos as u32,
317                dst.as_mut_ptr(), offset as u32, frames as u32,
318            );
319        },
320        {
321            let start = (pos + buf_len - offset) % buf_len;
322            for i in 0..frames {
323                dst[i] = buf[(start + i) % buf_len];
324            }
325        }
326    );
327}
328
329// ===========================================================================
330// Delay op
331// ===========================================================================
332
333/// Per-sample modulated delay read with linear interpolation.
334///
335/// For each sample `i`, reads from circular buffer at a fractional offset
336/// `time[i]` samples behind where the write head was at sample `i`.
337/// `pos` should be the write head position *after* the most recent `vec_ring_write`.
338pub fn vec_delay_read(buf: &[f32], pos: usize, dst: &mut [f32], time: &[f32]) {
339    let buf_len = buf.len();
340    let frames = dst.len().min(time.len());
341    dispatch!(
342        unsafe {
343            sesh_vec_delay_read_host(
344                buf.as_ptr(), buf_len as u32, pos as u32,
345                dst.as_mut_ptr(), time.as_ptr(), frames as u32,
346            );
347        },
348        {
349            for i in 0..frames {
350                // The write head was at (pos - frames + i) when sample i was written.
351                let write_pos_at_i = (pos + buf_len - frames + i) % buf_len;
352
353                let delay_int = time[i] as usize;
354                let delay_frac = time[i] - delay_int as f32;
355
356                let idx1 = (write_pos_at_i + buf_len - delay_int) % buf_len;
357                let idx2 = (idx1 + buf_len - 1) % buf_len;
358
359                dst[i] = buf[idx1] + delay_frac * (buf[idx2] - buf[idx1]);
360            }
361        }
362    );
363}
364
365// ===========================================================================
366// Schroeder allpass diffuser
367// ===========================================================================
368
369/// Schroeder allpass filter operating on a circular buffer.
370///
371/// Unity-gain allpass: smears transients without changing frequency balance.
372/// Used in series for reverb diffusion. Each allpass needs its own buffer and
373/// write position (like a delay line).
374///
375/// `g` is the allpass coefficient (typically 0.5–0.7). `delay` is in samples.
376pub fn vec_schroeder_allpass(
377    buf: &mut [f32],
378    pos: &mut usize,
379    dst: &mut [f32],
380    src: &[f32],
381    delay: usize,
382    g: f32,
383) {
384    let buf_len = buf.len();
385    let frames = dst.len().min(src.len());
386    dispatch!(
387        {
388            let mut pos32 = *pos as u32;
389            unsafe {
390                sesh_vec_schroeder_allpass_host(
391                    buf.as_mut_ptr(), buf_len as u32, &mut pos32,
392                    dst.as_mut_ptr(), src.as_ptr(),
393                    delay as u32, g, frames as u32,
394                );
395            }
396            *pos = pos32 as usize;
397        },
398        {
399            let mut wp = *pos;
400            for i in 0..frames {
401                let read_idx = (wp + buf_len - delay) % buf_len;
402                let buf_out = buf[read_idx];
403
404                let v = src[i] + g * buf_out;
405                dst[i] = buf_out - g * v;
406
407                buf[wp] = v;
408                wp = (wp + 1) % buf_len;
409            }
410            *pos = wp;
411        }
412    );
413}
414
415// ===========================================================================
416// One-pole filter
417// ===========================================================================
418
419/// One-pole lowpass filter (6 dB/oct). Processes a block of samples.
420///
421/// `coefficient` controls the cutoff: 0.0 = no filtering (pass-through),
422/// approaching 1.0 = heavy lowpass. Compute from cutoff frequency:
423/// `coefficient = exp(-2π * cutoff_hz / sample_rate)`.
424pub fn vec_one_pole(
425    state: &mut OnePoleState,
426    dst: &mut [f32],
427    src: &[f32],
428    coefficient: f32,
429) {
430    let frames = dst.len().min(src.len());
431    let mut y = state.y1;
432    for i in 0..frames {
433        y = src[i] + coefficient * (y - src[i]);
434        dst[i] = y;
435    }
436    state.y1 = y;
437}
438
439// ===========================================================================
440// Comb filters (delay with feedback)
441// ===========================================================================
442//
443// Three variants forming a hierarchy:
444//
445// - `vec_comb`          — single delay line with feedback. The primitive
446//                         building block: echo, flanger, karplus-strong.
447//
448// - `vec_comb_parallel` — N independent delay lines, same input, no
449//                         cross-feedback. Schroeder/Moorer reverb topology:
450//                         parallel combs → series allpasses.
451//
452// - `vec_comb_coupled`  — N delay lines with an N×N mixing matrix coupling
453//                         their feedback paths. Feedback delay network (FDN)
454//                         topology: the matrix (Hadamard, householder, etc.)
455//                         controls diffusion density.
456//
457// All three internalize the per-sample feedback loop, so the plugin never
458// needs to write sample-by-sample code for delay-with-feedback effects.
459
460/// Maximum number of lines for coupled/parallel comb operations.
461const MAX_COMB_LINES: usize = 16;
462
463/// Single comb filter: one delay line with feedback and damping.
464///
465/// Reads from the delay line with interpolated modulated delay time,
466/// applies one-pole damping, outputs the result, and writes
467/// `input + feedback * damped` back into the buffer.
468///
469/// Use for: echo, flanger, chorus with feedback, karplus-strong strings,
470/// single delay with feedback.
471pub fn vec_comb(
472    buf: &mut [f32],
473    pos: &mut usize,
474    damp: &mut OnePoleState,
475    dst: &mut [f32],
476    src: &[f32],
477    time: &[f32],
478    feedback: f32,
479    damping: f32,
480) {
481    let buf_len = buf.len();
482    let frames = dst.len().min(src.len()).min(time.len());
483    let mut wp = *pos;
484    let mut y = damp.y1;
485    for i in 0..frames {
486        let delay_int = time[i] as usize;
487        let delay_frac = time[i] - delay_int as f32;
488        let idx1 = (wp + buf_len - delay_int) % buf_len;
489        let idx2 = (idx1 + buf_len - 1) % buf_len;
490        let tap = buf[idx1] + delay_frac * (buf[idx2] - buf[idx1]);
491
492        y = tap + damping * (y - tap);
493
494        dst[i] = y;
495        buf[wp] = src[i] + feedback * y;
496        wp = (wp + 1) % buf_len;
497    }
498    *pos = wp;
499    damp.y1 = y;
500}
501
502/// Parallel comb filter: N independent delay lines, same input, no cross-feedback.
503///
504/// Each line gets the full `src` input and feeds back only into itself.
505/// Outputs are written to separate buffers so the caller can sum/weight
506/// them however they like (e.g. different weights for L/R stereo width).
507///
508/// Use for: Schroeder reverb (parallel combs → series allpasses), Moorer
509/// reverb, any topology with independent delay+feedback lines.
510pub fn vec_comb_parallel(
511    bufs: &mut [&mut [f32]],
512    positions: &mut [usize],
513    damp: &mut [OnePoleState],
514    dst: &mut [&mut [f32]],
515    src: &[f32],
516    time: &[&[f32]],
517    feedback: f32,
518    damping: f32,
519) {
520    let n = bufs.len();
521    for line in 0..n {
522        vec_comb(
523            bufs[line], &mut positions[line], &mut damp[line],
524            dst[line], src, time[line], feedback, damping,
525        );
526    }
527}
528
529/// Coupled comb filter (FDN): N delay lines with an N×N mixing matrix.
530///
531/// Each sample: reads N taps with interpolated modulated delay, applies
532/// one-pole damping, multiplies through the mixing matrix, then writes
533/// `src[line] + mixed[line]` back into each buffer. Outputs the damped
534/// taps (pre-matrix) so the caller can weight them for stereo.
535///
536/// The mixing matrix encodes both the cross-coupling pattern and feedback
537/// gain. For a standard FDN, use a normalized Hadamard matrix scaled by
538/// the desired feedback coefficient.
539///
540/// Use for: FDN reverbs (Hadamard, householder), Dattorro plate reverb,
541/// any architecture where delay lines cross-feed.
542///
543/// Maximum N is 16. Panics if `bufs.len()` exceeds this.
544pub fn vec_comb_coupled(
545    bufs: &mut [&mut [f32]],
546    positions: &mut [usize],
547    damp: &mut [OnePoleState],
548    dst: &mut [&mut [f32]],
549    src: &[&[f32]],
550    time: &[&[f32]],
551    matrix: &[f32],
552    damping: f32,
553) {
554    let n = bufs.len();
555    assert!(n <= MAX_COMB_LINES, "vec_comb_coupled: max {MAX_COMB_LINES} lines");
556    assert!(matrix.len() >= n * n, "vec_comb_coupled: matrix must be N×N");
557
558    let frames = dst[0].len();
559
560    for i in 0..frames {
561        let mut taps = [0.0f32; MAX_COMB_LINES];
562        let mut mixed = [0.0f32; MAX_COMB_LINES];
563
564        // Read N taps with interpolation.
565        for line in 0..n {
566            let buf_len = bufs[line].len();
567            let wp = positions[line];
568            let t = time[line][i];
569            let delay_int = t as usize;
570            let delay_frac = t - delay_int as f32;
571            let idx1 = (wp + buf_len - delay_int) % buf_len;
572            let idx2 = (idx1 + buf_len - 1) % buf_len;
573            taps[line] = bufs[line][idx1] + delay_frac * (bufs[line][idx2] - bufs[line][idx1]);
574        }
575
576        // One-pole damping per line.
577        for line in 0..n {
578            damp[line].y1 = taps[line] + damping * (damp[line].y1 - taps[line]);
579            taps[line] = damp[line].y1;
580        }
581
582        // Matrix multiply: mixed = matrix × taps.
583        for row in 0..n {
584            let mut sum = 0.0;
585            for col in 0..n {
586                sum += matrix[row * n + col] * taps[col];
587            }
588            mixed[row] = sum;
589        }
590
591        // Output damped taps; write input + mixed feedback.
592        for line in 0..n {
593            dst[line][i] = taps[line];
594            bufs[line][positions[line]] = src[line][i] + mixed[line];
595            positions[line] = (positions[line] + 1) % bufs[line].len();
596        }
597    }
598}
599
600// ===========================================================================
601// Oscillator
602// ===========================================================================
603
604/// Fill `dst` with oscillator output. Advances `*phase`. `freq` is in Hz.
605pub fn vec_osc(
606    phase: &mut f32,
607    dst: &mut [f32],
608    freq: f32,
609    waveform: Waveform,
610    sample_rate: f32,
611) {
612    let frames = dst.len();
613    dispatch!(
614        unsafe {
615            sesh_vec_osc_host(
616                phase as *mut f32, dst.as_mut_ptr(),
617                freq, waveform as u32, sample_rate, frames as u32,
618            );
619        },
620        {
621            let phase_inc = freq / sample_rate;
622            for i in 0..frames {
623                dst[i] = match waveform {
624                    Waveform::Sine => (*phase * std::f32::consts::TAU).sin(),
625                    Waveform::Triangle => 4.0 * (*phase - (*phase + 0.5).floor()).abs() - 1.0,
626                    Waveform::Saw => 2.0 * (*phase - (*phase + 0.5).floor()),
627                    Waveform::Square => if *phase % 1.0 < 0.5 { 1.0 } else { -1.0 },
628                };
629                *phase += phase_inc;
630                if *phase >= 1.0 {
631                    *phase -= 1.0;
632                }
633            }
634        }
635    );
636}
637
638// ===========================================================================
639// Filter
640// ===========================================================================
641
642/// Biquad filter with per-sample modulation of cutoff, Q, and gain.
643///
644/// `cutoff` is in Hz, `q` is the Q factor, `gain` is in dB (used for Peak/Shelf types).
645/// Coefficients are recomputed each sample from the parameter buffers.
646pub fn vec_biquad(
647    state: &mut BiquadState,
648    dst: &mut [f32],
649    src: &[f32],
650    cutoff: &[f32],
651    q: &[f32],
652    gain: &[f32],
653    filter_type: FilterType,
654    sample_rate: f32,
655) {
656    let frames = dst.len().min(src.len()).min(cutoff.len()).min(q.len()).min(gain.len());
657    dispatch!(
658        unsafe {
659            sesh_vec_biquad_host(
660                state as *mut BiquadState as *mut f32,
661                dst.as_mut_ptr(), src.as_ptr(),
662                cutoff.as_ptr(), q.as_ptr(), gain.as_ptr(),
663                filter_type as u32, sample_rate, frames as u32,
664            );
665        },
666        {
667            for i in 0..frames {
668                let w0 = std::f32::consts::TAU * cutoff[i] / sample_rate;
669                let cos_w0 = w0.cos();
670                let sin_w0 = w0.sin();
671                let alpha = sin_w0 / (2.0 * q[i]);
672                let a_db = gain[i];
673                let a_lin = 10.0f32.powf(a_db / 40.0);
674
675                let (b0, b1, b2, a0, a1, a2) = match filter_type {
676                    FilterType::Lowpass => {
677                        let b1 = 1.0 - cos_w0;
678                        let b0 = b1 / 2.0;
679                        (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
680                    }
681                    FilterType::Highpass => {
682                        let b1 = -(1.0 + cos_w0);
683                        let b0 = (1.0 + cos_w0) / 2.0;
684                        (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
685                    }
686                    FilterType::Bandpass => {
687                        (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
688                    }
689                    FilterType::Notch => {
690                        (1.0, -2.0 * cos_w0, 1.0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
691                    }
692                    FilterType::Peak => {
693                        (
694                            1.0 + alpha * a_lin,
695                            -2.0 * cos_w0,
696                            1.0 - alpha * a_lin,
697                            1.0 + alpha / a_lin,
698                            -2.0 * cos_w0,
699                            1.0 - alpha / a_lin,
700                        )
701                    }
702                    FilterType::LowShelf => {
703                        let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
704                        (
705                            a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
706                            2.0 * a_lin * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
707                            a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
708                            (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
709                            -2.0 * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
710                            (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
711                        )
712                    }
713                    FilterType::HighShelf => {
714                        let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
715                        (
716                            a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
717                            -2.0 * a_lin * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
718                            a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
719                            (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
720                            2.0 * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
721                            (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
722                        )
723                    }
724                    FilterType::Allpass => {
725                        (1.0 - alpha, -2.0 * cos_w0, 1.0 + alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
726                    }
727                };
728
729                // Normalize coefficients.
730                let b0 = b0 / a0;
731                let b1 = b1 / a0;
732                let b2 = b2 / a0;
733                let a1 = a1 / a0;
734                let a2 = a2 / a0;
735
736                let x0 = src[i];
737                let y0 = b0 * x0 + b1 * state.x1 + b2 * state.x2
738                    - a1 * state.y1 - a2 * state.y2;
739
740                state.x2 = state.x1;
741                state.x1 = x0;
742                state.y2 = state.y1;
743                state.y1 = y0;
744
745                dst[i] = y0;
746            }
747        }
748    );
749}
750
751// ===========================================================================
752// Dynamics
753// ===========================================================================
754
755/// Envelope follower. Tracks amplitude of `src` with attack/release smoothing.
756///
757/// `attack` and `release` are in seconds (per-sample buffers for modulation).
758/// Output in `dst` is the smoothed envelope value.
759pub fn vec_envelope(
760    state: &mut EnvelopeState,
761    dst: &mut [f32],
762    src: &[f32],
763    attack: &[f32],
764    release: &[f32],
765    mode: EnvelopeMode,
766    sample_rate: f32,
767) {
768    let frames = dst.len().min(src.len()).min(attack.len()).min(release.len());
769    dispatch!(
770        unsafe {
771            sesh_vec_envelope_host(
772                state as *mut EnvelopeState as *mut f32,
773                dst.as_mut_ptr(), src.as_ptr(),
774                attack.as_ptr(), release.as_ptr(),
775                mode as u32, sample_rate, frames as u32,
776            );
777        },
778        {
779            for i in 0..frames {
780                let input_level = match mode {
781                    EnvelopeMode::Peak => src[i].abs(),
782                    EnvelopeMode::Rms => src[i] * src[i],
783                };
784
785                let att_coeff = (-1.0 / (attack[i] * sample_rate)).exp();
786                let rel_coeff = (-1.0 / (release[i] * sample_rate)).exp();
787
788                let coeff = if input_level > state.current { att_coeff } else { rel_coeff };
789                state.current = coeff * state.current + (1.0 - coeff) * input_level;
790
791                dst[i] = match mode {
792                    EnvelopeMode::Peak => state.current,
793                    EnvelopeMode::Rms => state.current.sqrt(),
794                };
795            }
796        }
797    );
798}
799
800// ===========================================================================
801// Waveshaping
802// ===========================================================================
803
804/// Soft saturation: `dst[i] = tanh(src[i] * drive[i])`.
805pub fn vec_tanh(dst: &mut [f32], src: &[f32], drive: &[f32]) {
806    let len = dst.len().min(src.len()).min(drive.len());
807    dispatch!(
808        unsafe { sesh_vec_tanh_host(dst.as_mut_ptr(), src.as_ptr(), drive.as_ptr(), len as u32) },
809        for i in 0..len { dst[i] = (src[i] * drive[i]).tanh(); }
810    );
811}
812
813/// Hard clipping: clamp `src` to `±threshold[i]`.
814pub fn vec_hard_clip(dst: &mut [f32], src: &[f32], threshold: &[f32]) {
815    let len = dst.len().min(src.len()).min(threshold.len());
816    dispatch!(
817        unsafe { sesh_vec_hard_clip_host(dst.as_mut_ptr(), src.as_ptr(), threshold.as_ptr(), len as u32) },
818        for i in 0..len { dst[i] = src[i].clamp(-threshold[i], threshold[i]); }
819    );
820}
821
822// ===========================================================================
823// Unary / additional math ops
824// ===========================================================================
825
826/// Absolute value: `dst[i] = |src[i]|`.
827pub fn vec_abs(dst: &mut [f32], src: &[f32]) {
828    let len = dst.len().min(src.len());
829    dispatch!(
830        unsafe { sesh_vec_abs_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) },
831        for i in 0..len { dst[i] = src[i].abs(); }
832    );
833}
834
835/// Negate: `dst[i] = -src[i]`. Phase inversion.
836pub fn vec_neg(dst: &mut [f32], src: &[f32]) {
837    let len = dst.len().min(src.len());
838    dispatch!(
839        unsafe { sesh_vec_neg_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) },
840        for i in 0..len { dst[i] = -src[i]; }
841    );
842}
843
844/// Square root: `dst[i] = sqrt(src[i])`.
845pub fn vec_sqrt(dst: &mut [f32], src: &[f32]) {
846    let len = dst.len().min(src.len());
847    dispatch!(
848        unsafe { sesh_vec_sqrt_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) },
849        for i in 0..len { dst[i] = src[i].sqrt(); }
850    );
851}
852
853/// Reciprocal: `dst[i] = 1.0 / src[i]`.
854pub fn vec_recip(dst: &mut [f32], src: &[f32]) {
855    let len = dst.len().min(src.len());
856    dispatch!(
857        unsafe { sesh_vec_recip_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) },
858        for i in 0..len { dst[i] = 1.0 / src[i]; }
859    );
860}
861
862/// Element-wise division: `dst[i] = a[i] / b[i]`.
863pub fn vec_div(dst: &mut [f32], a: &[f32], b: &[f32]) {
864    let len = dst.len().min(a.len()).min(b.len());
865    dispatch!(
866        unsafe { sesh_vec_div_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) },
867        for i in 0..len { dst[i] = a[i] / b[i]; }
868    );
869}
870
871/// Element-wise power: `dst[i] = src[i].powf(exp[i])`.
872pub fn vec_pow(dst: &mut [f32], src: &[f32], exp: &[f32]) {
873    let len = dst.len().min(src.len()).min(exp.len());
874    dispatch!(
875        unsafe { sesh_vec_pow_host(dst.as_mut_ptr(), src.as_ptr(), exp.as_ptr(), len as u32) },
876        for i in 0..len { dst[i] = src[i].powf(exp[i]); }
877    );
878}
879
880// ===========================================================================
881// In-place (_assign) variants
882// ===========================================================================
883//
884// These are Rust convenience wrappers that call the same host imports with
885// dst aliased as src. Raw pointer aliasing is fine — this is purely a Rust
886// borrow-checker workaround. No additional C/host API surface.
887
888/// In-place element-wise addition: `dst[i] += src[i]`.
889pub fn vec_add_assign(dst: &mut [f32], src: &[f32]) {
890    let len = dst.len().min(src.len());
891    dispatch!(
892        unsafe { sesh_vec_add_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) },
893        for i in 0..len { dst[i] += src[i]; }
894    );
895}
896
897/// In-place element-wise multiplication: `dst[i] *= src[i]`.
898pub fn vec_mul_assign(dst: &mut [f32], src: &[f32]) {
899    let len = dst.len().min(src.len());
900    dispatch!(
901        unsafe { sesh_vec_mul_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) },
902        for i in 0..len { dst[i] *= src[i]; }
903    );
904}
905
906/// In-place soft saturation: `dst[i] = tanh(dst[i] * drive[i])`.
907pub fn vec_tanh_assign(dst: &mut [f32], drive: &[f32]) {
908    let len = dst.len().min(drive.len());
909    dispatch!(
910        unsafe { sesh_vec_tanh_host(dst.as_mut_ptr(), dst.as_ptr(), drive.as_ptr(), len as u32) },
911        for i in 0..len { dst[i] = (dst[i] * drive[i]).tanh(); }
912    );
913}
914
915/// In-place hard clipping: clamp `dst` to `±threshold[i]`.
916pub fn vec_hard_clip_assign(dst: &mut [f32], threshold: &[f32]) {
917    let len = dst.len().min(threshold.len());
918    dispatch!(
919        unsafe { sesh_vec_hard_clip_host(dst.as_mut_ptr(), dst.as_ptr(), threshold.as_ptr(), len as u32) },
920        for i in 0..len { dst[i] = dst[i].clamp(-threshold[i], threshold[i]); }
921    );
922}
923
924/// In-place absolute value: `dst[i] = |dst[i]|`.
925pub fn vec_abs_assign(dst: &mut [f32]) {
926    let len = dst.len();
927    dispatch!(
928        unsafe { sesh_vec_abs_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) },
929        for i in 0..len { dst[i] = dst[i].abs(); }
930    );
931}
932
933/// In-place negate: `dst[i] = -dst[i]`.
934pub fn vec_neg_assign(dst: &mut [f32]) {
935    let len = dst.len();
936    dispatch!(
937        unsafe { sesh_vec_neg_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) },
938        for i in 0..len { dst[i] = -dst[i]; }
939    );
940}
941
942/// In-place square root: `dst[i] = sqrt(dst[i])`.
943pub fn vec_sqrt_assign(dst: &mut [f32]) {
944    let len = dst.len();
945    dispatch!(
946        unsafe { sesh_vec_sqrt_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) },
947        for i in 0..len { dst[i] = dst[i].sqrt(); }
948    );
949}
950
951/// In-place reciprocal: `dst[i] = 1.0 / dst[i]`.
952pub fn vec_recip_assign(dst: &mut [f32]) {
953    let len = dst.len();
954    dispatch!(
955        unsafe { sesh_vec_recip_host(dst.as_mut_ptr(), dst.as_ptr(), len as u32) },
956        for i in 0..len { dst[i] = 1.0 / dst[i]; }
957    );
958}
959
960/// In-place element-wise division: `dst[i] /= src[i]`.
961pub fn vec_div_assign(dst: &mut [f32], src: &[f32]) {
962    let len = dst.len().min(src.len());
963    dispatch!(
964        unsafe { sesh_vec_div_host(dst.as_mut_ptr(), dst.as_ptr(), src.as_ptr(), len as u32) },
965        for i in 0..len { dst[i] /= src[i]; }
966    );
967}
968
969/// In-place element-wise power: `dst[i] = dst[i].powf(exp[i])`.
970pub fn vec_pow_assign(dst: &mut [f32], exp: &[f32]) {
971    let len = dst.len().min(exp.len());
972    dispatch!(
973        unsafe { sesh_vec_pow_host(dst.as_mut_ptr(), dst.as_ptr(), exp.as_ptr(), len as u32) },
974        for i in 0..len { dst[i] = dst[i].powf(exp[i]); }
975    );
976}