Skip to main content

sesh_sdk/
vec.rs

1//! Vector operations for batch audio processing.
2//!
3//! Each op has two code paths: an inline Rust fallback (always available) and a
4//! host-accelerated import (used when `sesh_vec_version() > 0`). The SDK selects
5//! the path at runtime. Plugin authors call the same functions regardless of platform.
6
7use std::sync::atomic::{AtomicU32, Ordering};
8
9// ---------------------------------------------------------------------------
10// Host capability detection
11// ---------------------------------------------------------------------------
12
13extern "C" {
14    fn sesh_vec_version() -> u32;
15}
16
17/// Cached host vec version. 0 = not yet queried, u32::MAX = stubs (web).
18static HOST_VEC_VERSION: AtomicU32 = AtomicU32::new(0);
19
20fn host_version() -> u32 {
21    let v = HOST_VEC_VERSION.load(Ordering::Relaxed);
22    if v != 0 {
23        return v;
24    }
25    let v = unsafe { sesh_vec_version() };
26    // Store non-zero so we don't re-query. If host returns 0, store a sentinel.
27    let store = if v == 0 { u32::MAX } else { v };
28    HOST_VEC_VERSION.store(store, Ordering::Relaxed);
29    v
30}
31
32#[inline]
33fn use_host_ops() -> bool {
34    host_version() > 0 && host_version() != u32::MAX
35}
36
37// ---------------------------------------------------------------------------
38// Host imports (C ABI, raw pointers)
39// ---------------------------------------------------------------------------
40
41extern "C" {
42    fn sesh_vec_copy_host(dst: *mut f32, src: *const f32, len: u32);
43    fn sesh_vec_fill_host(dst: *mut f32, value: f32, len: u32);
44    fn sesh_vec_add_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
45    fn sesh_vec_add_scalar_host(dst: *mut f32, value: f32, len: u32);
46    fn sesh_vec_mul_host(dst: *mut f32, a: *const f32, b: *const f32, len: u32);
47    fn sesh_vec_mul_scalar_host(dst: *mut f32, value: f32, len: u32);
48    fn sesh_vec_mul_add_host(dst: *mut f32, src: *const f32, gain: f32, len: u32);
49    fn sesh_vec_clamp_host(dst: *mut f32, src: *const f32, min: f32, max: f32, len: u32);
50    fn sesh_vec_ring_write_host(
51        buf: *mut f32, buf_len: u32, pos: *mut u32, src: *const f32, len: u32,
52    );
53    fn sesh_vec_ring_read_host(
54        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, offset: u32, len: u32,
55    );
56    fn sesh_vec_delay_read_host(
57        buf: *const f32, buf_len: u32, pos: u32, dst: *mut f32, time: *const f32, len: u32,
58    );
59    fn sesh_vec_osc_host(
60        phase: *mut f32, dst: *mut f32, freq: f32, waveform: u32, sample_rate: f32, len: u32,
61    );
62    fn sesh_vec_biquad_host(
63        state: *mut f32, dst: *mut f32, src: *const f32,
64        cutoff: *const f32, q: *const f32, gain: *const f32,
65        filter_type: u32, sample_rate: f32, len: u32,
66    );
67    fn sesh_vec_envelope_host(
68        state: *mut f32, dst: *mut f32, src: *const f32,
69        attack: *const f32, release: *const f32,
70        mode: u32, sample_rate: f32, len: u32,
71    );
72    fn sesh_vec_tanh_host(dst: *mut f32, src: *const f32, drive: *const f32, len: u32);
73    fn sesh_vec_hard_clip_host(dst: *mut f32, src: *const f32, threshold: *const f32, len: u32);
74}
75
76// ---------------------------------------------------------------------------
77// Enums and state types
78// ---------------------------------------------------------------------------
79
80/// Oscillator waveform shape.
81#[repr(u32)]
82#[derive(Clone, Copy)]
83pub enum Waveform {
84    Sine = 0,
85    Triangle = 1,
86    Saw = 2,
87    Square = 3,
88}
89
90/// Biquad filter type.
91#[repr(u32)]
92#[derive(Clone, Copy)]
93pub enum FilterType {
94    Lowpass = 0,
95    Highpass = 1,
96    Bandpass = 2,
97    Notch = 3,
98    /// Parametric EQ band — boost/cut at cutoff frequency.
99    Peak = 4,
100    /// Boost/cut below cutoff frequency.
101    LowShelf = 5,
102    /// Boost/cut above cutoff frequency.
103    HighShelf = 6,
104}
105
106/// Internal state for a biquad filter (two-sample history).
107#[repr(C)]
108pub struct BiquadState {
109    pub x1: f32,
110    pub x2: f32,
111    pub y1: f32,
112    pub y2: f32,
113}
114
115impl BiquadState {
116    pub const fn new() -> Self {
117        Self { x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 }
118    }
119}
120
121/// Envelope follower detection mode.
122#[repr(u32)]
123#[derive(Clone, Copy)]
124pub enum EnvelopeMode {
125    /// Track instantaneous peaks.
126    Peak = 0,
127    /// Track root-mean-square level.
128    Rms = 1,
129}
130
131/// Internal state for an envelope follower.
132#[repr(C)]
133pub struct EnvelopeState {
134    pub current: f32,
135}
136
137impl EnvelopeState {
138    pub const fn new() -> Self {
139        Self { current: 0.0 }
140    }
141}
142
143// ===========================================================================
144// Math ops
145// ===========================================================================
146
147/// Copy `src` into `dst`.
148pub fn vec_copy(dst: &mut [f32], src: &[f32]) {
149    let len = dst.len().min(src.len());
150    if use_host_ops() {
151        unsafe { sesh_vec_copy_host(dst.as_mut_ptr(), src.as_ptr(), len as u32) }
152    } else {
153        dst[..len].copy_from_slice(&src[..len]);
154    }
155}
156
157/// Fill `dst` with a constant value.
158pub fn vec_fill(dst: &mut [f32], value: f32) {
159    let len = dst.len();
160    if use_host_ops() {
161        unsafe { sesh_vec_fill_host(dst.as_mut_ptr(), value, len as u32) }
162    } else {
163        for s in dst.iter_mut() {
164            *s = value;
165        }
166    }
167}
168
169/// Element-wise addition: `dst[i] = a[i] + b[i]`.
170pub fn vec_add(dst: &mut [f32], a: &[f32], b: &[f32]) {
171    let len = dst.len().min(a.len()).min(b.len());
172    if use_host_ops() {
173        unsafe { sesh_vec_add_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) }
174    } else {
175        for i in 0..len {
176            dst[i] = a[i] + b[i];
177        }
178    }
179}
180
181/// Add scalar to every element: `dst[i] += value`.
182pub fn vec_add_scalar(dst: &mut [f32], value: f32) {
183    let len = dst.len();
184    if use_host_ops() {
185        unsafe { sesh_vec_add_scalar_host(dst.as_mut_ptr(), value, len as u32) }
186    } else {
187        for s in dst.iter_mut() {
188            *s += value;
189        }
190    }
191}
192
193/// Element-wise multiplication: `dst[i] = a[i] * b[i]`.
194pub fn vec_mul(dst: &mut [f32], a: &[f32], b: &[f32]) {
195    let len = dst.len().min(a.len()).min(b.len());
196    if use_host_ops() {
197        unsafe { sesh_vec_mul_host(dst.as_mut_ptr(), a.as_ptr(), b.as_ptr(), len as u32) }
198    } else {
199        for i in 0..len {
200            dst[i] = a[i] * b[i];
201        }
202    }
203}
204
205/// Multiply every element by scalar: `dst[i] *= value`.
206pub fn vec_mul_scalar(dst: &mut [f32], value: f32) {
207    let len = dst.len();
208    if use_host_ops() {
209        unsafe { sesh_vec_mul_scalar_host(dst.as_mut_ptr(), value, len as u32) }
210    } else {
211        for s in dst.iter_mut() {
212            *s *= value;
213        }
214    }
215}
216
217/// Multiply and accumulate: `dst[i] += src[i] * gain`.
218pub fn vec_mul_add(dst: &mut [f32], src: &[f32], gain: f32) {
219    let len = dst.len().min(src.len());
220    if use_host_ops() {
221        unsafe { sesh_vec_mul_add_host(dst.as_mut_ptr(), src.as_ptr(), gain, len as u32) }
222    } else {
223        for i in 0..len {
224            dst[i] += src[i] * gain;
225        }
226    }
227}
228
229/// Clamp: `dst[i] = clamp(src[i], min, max)`.
230pub fn vec_clamp(dst: &mut [f32], src: &[f32], min: f32, max: f32) {
231    let len = dst.len().min(src.len());
232    if use_host_ops() {
233        unsafe { sesh_vec_clamp_host(dst.as_mut_ptr(), src.as_ptr(), min, max, len as u32) }
234    } else {
235        for i in 0..len {
236            dst[i] = src[i].clamp(min, max);
237        }
238    }
239}
240
241// ===========================================================================
242// Circular buffer ops
243// ===========================================================================
244
245/// Write `src` into circular buffer `buf` starting at `*pos`, wrapping at `buf.len()`.
246/// Advances `*pos` by `src.len()`.
247pub fn vec_ring_write(buf: &mut [f32], pos: &mut usize, src: &[f32]) {
248    let buf_len = buf.len();
249    let frames = src.len();
250    if use_host_ops() {
251        let mut pos32 = *pos as u32;
252        unsafe {
253            sesh_vec_ring_write_host(
254                buf.as_mut_ptr(), buf_len as u32, &mut pos32, src.as_ptr(), frames as u32,
255            );
256        }
257        *pos = pos32 as usize;
258    } else {
259        for i in 0..frames {
260            buf[(*pos + i) % buf_len] = src[i];
261        }
262        *pos = (*pos + frames) % buf_len;
263    }
264}
265
266/// Read `dst.len()` contiguous samples from circular buffer at `pos - offset`, wrapping.
267pub fn vec_ring_read(buf: &[f32], pos: usize, dst: &mut [f32], offset: usize) {
268    let buf_len = buf.len();
269    let frames = dst.len();
270    if use_host_ops() {
271        unsafe {
272            sesh_vec_ring_read_host(
273                buf.as_ptr(), buf_len as u32, pos as u32,
274                dst.as_mut_ptr(), offset as u32, frames as u32,
275            );
276        }
277    } else {
278        let start = (pos + buf_len - offset) % buf_len;
279        for i in 0..frames {
280            dst[i] = buf[(start + i) % buf_len];
281        }
282    }
283}
284
285// ===========================================================================
286// Delay op
287// ===========================================================================
288
289/// Per-sample modulated delay read with linear interpolation.
290///
291/// For each sample `i`, reads from circular buffer at a fractional offset
292/// `time[i]` samples behind where the write head was at sample `i`.
293/// `pos` should be the write head position *after* the most recent `vec_ring_write`.
294pub fn vec_delay_read(buf: &[f32], pos: usize, dst: &mut [f32], time: &[f32]) {
295    let buf_len = buf.len();
296    let frames = dst.len().min(time.len());
297    if use_host_ops() {
298        unsafe {
299            sesh_vec_delay_read_host(
300                buf.as_ptr(), buf_len as u32, pos as u32,
301                dst.as_mut_ptr(), time.as_ptr(), frames as u32,
302            );
303        }
304    } else {
305        for i in 0..frames {
306            // The write head was at (pos - frames + i) when sample i was written.
307            let write_pos_at_i = (pos + buf_len - frames + i) % buf_len;
308
309            let delay_int = time[i] as usize;
310            let delay_frac = time[i] - delay_int as f32;
311
312            let idx1 = (write_pos_at_i + buf_len - delay_int) % buf_len;
313            let idx2 = (idx1 + buf_len - 1) % buf_len;
314
315            dst[i] = buf[idx1] + delay_frac * (buf[idx2] - buf[idx1]);
316        }
317    }
318}
319
320// ===========================================================================
321// Oscillator
322// ===========================================================================
323
324/// Fill `dst` with oscillator output. Advances `*phase`. `freq` is in Hz.
325pub fn vec_osc(
326    phase: &mut f32,
327    dst: &mut [f32],
328    freq: f32,
329    waveform: Waveform,
330    sample_rate: f32,
331) {
332    let frames = dst.len();
333    if use_host_ops() {
334        unsafe {
335            sesh_vec_osc_host(
336                phase as *mut f32, dst.as_mut_ptr(),
337                freq, waveform as u32, sample_rate, frames as u32,
338            );
339        }
340    } else {
341        let phase_inc = freq / sample_rate;
342        for i in 0..frames {
343            dst[i] = match waveform {
344                Waveform::Sine => (*phase * std::f32::consts::TAU).sin(),
345                Waveform::Triangle => 4.0 * (*phase - (*phase + 0.5).floor()).abs() - 1.0,
346                Waveform::Saw => 2.0 * (*phase - (*phase + 0.5).floor()),
347                Waveform::Square => if *phase % 1.0 < 0.5 { 1.0 } else { -1.0 },
348            };
349            *phase += phase_inc;
350            if *phase >= 1.0 {
351                *phase -= 1.0;
352            }
353        }
354    }
355}
356
357// ===========================================================================
358// Filter
359// ===========================================================================
360
361/// Biquad filter with per-sample modulation of cutoff, Q, and gain.
362///
363/// `cutoff` is in Hz, `q` is the Q factor, `gain` is in dB (used for Peak/Shelf types).
364/// Coefficients are recomputed each sample from the parameter buffers.
365pub fn vec_biquad(
366    state: &mut BiquadState,
367    dst: &mut [f32],
368    src: &[f32],
369    cutoff: &[f32],
370    q: &[f32],
371    gain: &[f32],
372    filter_type: FilterType,
373    sample_rate: f32,
374) {
375    let frames = dst.len().min(src.len()).min(cutoff.len()).min(q.len()).min(gain.len());
376    if use_host_ops() {
377        unsafe {
378            sesh_vec_biquad_host(
379                state as *mut BiquadState as *mut f32,
380                dst.as_mut_ptr(), src.as_ptr(),
381                cutoff.as_ptr(), q.as_ptr(), gain.as_ptr(),
382                filter_type as u32, sample_rate, frames as u32,
383            );
384        }
385    } else {
386        for i in 0..frames {
387            let w0 = std::f32::consts::TAU * cutoff[i] / sample_rate;
388            let cos_w0 = w0.cos();
389            let sin_w0 = w0.sin();
390            let alpha = sin_w0 / (2.0 * q[i]);
391            let a_db = gain[i];
392            let a_lin = 10.0f32.powf(a_db / 40.0);
393
394            let (b0, b1, b2, a0, a1, a2) = match filter_type {
395                FilterType::Lowpass => {
396                    let b1 = 1.0 - cos_w0;
397                    let b0 = b1 / 2.0;
398                    (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
399                }
400                FilterType::Highpass => {
401                    let b1 = -(1.0 + cos_w0);
402                    let b0 = (1.0 + cos_w0) / 2.0;
403                    (b0, b1, b0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
404                }
405                FilterType::Bandpass => {
406                    (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
407                }
408                FilterType::Notch => {
409                    (1.0, -2.0 * cos_w0, 1.0, 1.0 + alpha, -2.0 * cos_w0, 1.0 - alpha)
410                }
411                FilterType::Peak => {
412                    (
413                        1.0 + alpha * a_lin,
414                        -2.0 * cos_w0,
415                        1.0 - alpha * a_lin,
416                        1.0 + alpha / a_lin,
417                        -2.0 * cos_w0,
418                        1.0 - alpha / a_lin,
419                    )
420                }
421                FilterType::LowShelf => {
422                    let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
423                    (
424                        a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
425                        2.0 * a_lin * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
426                        a_lin * ((a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
427                        (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
428                        -2.0 * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
429                        (a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
430                    )
431                }
432                FilterType::HighShelf => {
433                    let two_sqrt_a_alpha = 2.0 * a_lin.sqrt() * alpha;
434                    (
435                        a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha),
436                        -2.0 * a_lin * ((a_lin - 1.0) + (a_lin + 1.0) * cos_w0),
437                        a_lin * ((a_lin + 1.0) + (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha),
438                        (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 + two_sqrt_a_alpha,
439                        2.0 * ((a_lin - 1.0) - (a_lin + 1.0) * cos_w0),
440                        (a_lin + 1.0) - (a_lin - 1.0) * cos_w0 - two_sqrt_a_alpha,
441                    )
442                }
443            };
444
445            // Normalize coefficients.
446            let b0 = b0 / a0;
447            let b1 = b1 / a0;
448            let b2 = b2 / a0;
449            let a1 = a1 / a0;
450            let a2 = a2 / a0;
451
452            let x0 = src[i];
453            let y0 = b0 * x0 + b1 * state.x1 + b2 * state.x2
454                - a1 * state.y1 - a2 * state.y2;
455
456            state.x2 = state.x1;
457            state.x1 = x0;
458            state.y2 = state.y1;
459            state.y1 = y0;
460
461            dst[i] = y0;
462        }
463    }
464}
465
466// ===========================================================================
467// Dynamics
468// ===========================================================================
469
470/// Envelope follower. Tracks amplitude of `src` with attack/release smoothing.
471///
472/// `attack` and `release` are in seconds (per-sample buffers for modulation).
473/// Output in `dst` is the smoothed envelope value.
474pub fn vec_envelope(
475    state: &mut EnvelopeState,
476    dst: &mut [f32],
477    src: &[f32],
478    attack: &[f32],
479    release: &[f32],
480    mode: EnvelopeMode,
481    sample_rate: f32,
482) {
483    let frames = dst.len().min(src.len()).min(attack.len()).min(release.len());
484    if use_host_ops() {
485        unsafe {
486            sesh_vec_envelope_host(
487                state as *mut EnvelopeState as *mut f32,
488                dst.as_mut_ptr(), src.as_ptr(),
489                attack.as_ptr(), release.as_ptr(),
490                mode as u32, sample_rate, frames as u32,
491            );
492        }
493    } else {
494        for i in 0..frames {
495            let input_level = match mode {
496                EnvelopeMode::Peak => src[i].abs(),
497                EnvelopeMode::Rms => src[i] * src[i],
498            };
499
500            let att_coeff = (-1.0 / (attack[i] * sample_rate)).exp();
501            let rel_coeff = (-1.0 / (release[i] * sample_rate)).exp();
502
503            let coeff = if input_level > state.current { att_coeff } else { rel_coeff };
504            state.current = coeff * state.current + (1.0 - coeff) * input_level;
505
506            dst[i] = match mode {
507                EnvelopeMode::Peak => state.current,
508                EnvelopeMode::Rms => state.current.sqrt(),
509            };
510        }
511    }
512}
513
514// ===========================================================================
515// Waveshaping
516// ===========================================================================
517
518/// Soft saturation: `dst[i] = tanh(src[i] * drive[i])`.
519pub fn vec_tanh(dst: &mut [f32], src: &[f32], drive: &[f32]) {
520    let len = dst.len().min(src.len()).min(drive.len());
521    if use_host_ops() {
522        unsafe { sesh_vec_tanh_host(dst.as_mut_ptr(), src.as_ptr(), drive.as_ptr(), len as u32) }
523    } else {
524        for i in 0..len {
525            dst[i] = (src[i] * drive[i]).tanh();
526        }
527    }
528}
529
530/// Hard clipping: clamp `src` to `±threshold[i]`.
531pub fn vec_hard_clip(dst: &mut [f32], src: &[f32], threshold: &[f32]) {
532    let len = dst.len().min(src.len()).min(threshold.len());
533    if use_host_ops() {
534        unsafe {
535            sesh_vec_hard_clip_host(dst.as_mut_ptr(), src.as_ptr(), threshold.as_ptr(), len as u32)
536        }
537    } else {
538        for i in 0..len {
539            dst[i] = src[i].clamp(-threshold[i], threshold[i]);
540        }
541    }
542}