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