geedback

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

lib.rs (7713B)


      1 use wasm_bindgen::prelude::*;
      2 
      3 #[wasm_bindgen]
      4 #[derive(PartialEq, Clone, Copy)]
      5 pub enum Waveform {
      6     Sine,
      7     Triangle,
      8     TriangleSawtooth,
      9     Sawtooth,
     10     ReverseSawtooth,
     11     Square,
     12     WidePulse,
     13     NarrowPulse,
     14 }
     15 
     16 #[wasm_bindgen]
     17 pub struct GeedbackProcessor {
     18     // Primary inputs
     19     orient_a: f64, // Z (Alpha)
     20     orient_b: f64, // X (Beta)
     21     orient_g: f64, // Y (Gamma)
     22     linear_accel_x: f64,
     23     linear_accel_y: f64,
     24     linear_accel_z: f64,
     25 
     26     // Touch inputs (Kaoss Pad)
     27     touch_x: f64,
     28     touch_y: f64,
     29 
     30     // Smoothed parameters for all 3 axes (sin/cos pairs)
     31     s_a_sin: f64,
     32     s_a_cos: f64,
     33     s_b_sin: f64,
     34     s_b_cos: f64,
     35     s_g_sin: f64,
     36     s_g_cos: f64,
     37 
     38     // Smoothed linear acceleration per axis
     39     s_la_x: f64,
     40     s_la_y: f64,
     41     s_la_z: f64,
     42 
     43     // Oscillator States
     44     phases: [f64; 3],
     45     phases_mod: [f64; 3], // Dedicated modulator phases
     46     waveforms: [Waveform; 3],
     47     mixes: [f64; 3],
     48 
     49     sample_rate: f64,
     50     latest_output: f64,
     51 
     52     // Filter state
     53     s_filter_cutoff: f64,
     54     s_filter_res: f64,
     55     filter_mem: [f64; 4], // x1, x2, y1, y2
     56 }
     57 
     58 #[wasm_bindgen]
     59 impl GeedbackProcessor {
     60     #[wasm_bindgen(constructor)]
     61     pub fn new() -> Self {
     62         Self {
     63             orient_a: 0.0,
     64             orient_b: 0.0,
     65             orient_g: 0.0,
     66             linear_accel_x: 0.0,
     67             linear_accel_y: 0.0,
     68             linear_accel_z: 0.0,
     69             touch_x: 0.5,
     70             touch_y: 0.5,
     71             s_a_sin: 0.0,
     72             s_a_cos: 1.0,
     73             s_b_sin: 0.0,
     74             s_b_cos: 1.0,
     75             s_g_sin: 0.0,
     76             s_g_cos: 1.0,
     77             s_la_x: 0.0,
     78             s_la_y: 0.0,
     79             s_la_z: 0.0,
     80             s_filter_cutoff: 0.5,
     81             s_filter_res: 0.1,
     82             filter_mem: [0.0; 4],
     83             phases: [0.0; 3],
     84             phases_mod: [0.0; 3],
     85             waveforms: [Waveform::Sine, Waveform::Sine, Waveform::Sine],
     86             mixes: [0.3, 0.3, 0.3],
     87             sample_rate: 44100.0,
     88             latest_output: 0.0,
     89         }
     90     }
     91 
     92     pub fn set_waveform(&mut self, index: usize, wave: Waveform) {
     93         if index < 3 {
     94             self.waveforms[index] = wave;
     95         }
     96     }
     97 
     98     pub fn set_orient(&mut self, a: f64, b: f64, g: f64) {
     99         self.orient_a = a;
    100         self.orient_b = b;
    101         self.orient_g = g;
    102     }
    103 
    104     pub fn set_linear_accel(&mut self, x: f64, y: f64, z: f64) {
    105         self.linear_accel_x = x;
    106         self.linear_accel_y = y;
    107         self.linear_accel_z = z;
    108     }
    109 
    110     pub fn set_touch(&mut self, x: f64, y: f64) {
    111         self.touch_x = x.clamp(0.0, 1.0);
    112         self.touch_y = y.clamp(0.0, 1.0);
    113     }
    114 
    115     pub fn set_sample_rate(&mut self, sample_rate: f64) {
    116         self.sample_rate = sample_rate;
    117     }
    118 
    119     fn calculate_wave(&self, phase: f64, wave: Waveform, morph: f64) -> f64 {
    120         let m = (morph + 1.0) * 0.5;
    121         let p = phase.fract();
    122 
    123         match wave {
    124             Waveform::Sine => {
    125                 let raw = (p * 2.0 * std::f64::consts::PI).sin();
    126                 if m > 0.5 {
    127                     let drive = 1.0 + (m - 0.5) * 2.0;
    128                     (raw * drive).tanh()
    129                 } else {
    130                     raw
    131                 }
    132             }
    133             Waveform::Triangle => 1.0 - 2.0 * (p + 0.75).fract().abs(),
    134             Waveform::TriangleSawtooth => {
    135                 let tri = 1.0 - 2.0 * (p + 0.75).fract().abs();
    136                 let saw = 2.0 * p - 1.0;
    137                 tri * (1.0 - m) + saw * m
    138             }
    139             Waveform::Sawtooth => 2.0 * p - 1.0,
    140             Waveform::ReverseSawtooth => 1.0 - p * 2.0,
    141             Waveform::Square | Waveform::WidePulse | Waveform::NarrowPulse => {
    142                 let width = 0.02 + m * 0.48;
    143                 if p < width { 1.0 } else { -1.0 }
    144             }
    145         }
    146     }
    147 
    148     pub fn process(&mut self) -> f64 {
    149         let rad_a = (self.orient_a / 2.0).to_radians();
    150         let rad_b = (self.orient_b / 2.0).to_radians();
    151         let rad_g = (self.orient_g / 2.0).to_radians();
    152 
    153         let lpf_val = 0.9995;
    154         let gain_val = 0.0005;
    155 
    156         // Smooth Orientation
    157         self.s_a_sin = self.s_a_sin * lpf_val + rad_a.sin() * gain_val;
    158         self.s_a_cos = self.s_a_cos * lpf_val + rad_a.cos() * gain_val;
    159         self.s_b_sin = self.s_b_sin * lpf_val + rad_b.sin() * gain_val;
    160         self.s_b_cos = self.s_b_cos * lpf_val + rad_b.cos() * gain_val;
    161         self.s_g_sin = self.s_g_sin * lpf_val + rad_g.sin() * gain_val;
    162         self.s_g_cos = self.s_g_cos * lpf_val + rad_g.cos() * gain_val;
    163 
    164         // Smooth Linear Accel
    165         let la_lpf = 0.995;
    166         let la_gain = 0.005;
    167         self.s_la_x = self.s_la_x * la_lpf + self.linear_accel_x * la_gain;
    168         self.s_la_y = self.s_la_y * la_lpf + self.linear_accel_y * la_gain;
    169         self.s_la_z = self.s_la_z * la_lpf + self.linear_accel_z * la_gain;
    170 
    171         self.s_filter_cutoff = self.s_filter_cutoff * 0.9995 + self.touch_x * 0.0005;
    172         self.s_filter_res = self.s_filter_res * 0.9995 + (1.0 - self.touch_y) * 0.0005;
    173 
    174         let freqs = [
    175             110.0 * 2.0_f64.powf(self.s_g_sin * 2.5), // Osc 1: Y
    176             164.8 * 2.0_f64.powf(self.s_a_sin * 4.0), // Osc 2: Z
    177             220.0 * 2.0_f64.powf(self.s_b_sin * 2.5), // Osc 3: X
    178         ];
    179 
    180         let morphs = [self.s_g_cos, self.s_a_cos, self.s_b_cos];
    181 
    182         let fm_depths = [
    183             self.s_la_y.abs().powi(3) * 1.0, // Non-linear: quiet when slow, aggressive when shaken
    184             self.s_la_z.abs().powi(3) * 1.0,
    185             self.s_la_x.abs().powi(3) * 1.0,
    186         ];
    187 
    188         let mut mixed_output = 0.0;
    189 
    190         for i in 0..3 {
    191             self.phases_mod[i] = (self.phases_mod[i] + freqs[i] / self.sample_rate) % 1.0;
    192             let modulation = (self.phases_mod[i] * 2.0 * std::f64::consts::PI).sin() * fm_depths[i];
    193             let osc_out =
    194                 self.calculate_wave(self.phases[i] + modulation, self.waveforms[i], morphs[i]);
    195             mixed_output += osc_out * self.mixes[i];
    196             self.phases[i] = (self.phases[i] + freqs[i] / self.sample_rate) % 1.0;
    197         }
    198 
    199         let mut cutoff_hz = 200.0 * 180.0_f64.powf(self.s_filter_cutoff);
    200 
    201         // Safety: Clamp cutoff to slightly below Nyquist to prevent NaN
    202         let max_cutoff = self.sample_rate * 0.45;
    203         if cutoff_hz > max_cutoff {
    204             cutoff_hz = max_cutoff;
    205         }
    206 
    207         let q = 0.707 + self.s_filter_res * 14.3;
    208 
    209         let omega = 2.0 * std::f64::consts::PI * cutoff_hz / self.sample_rate;
    210         let alpha = omega.sin() / (2.0 * q);
    211         let cos_w = omega.cos();
    212 
    213         let b0 = (1.0 - cos_w) / 2.0;
    214         let b1 = 1.0 - cos_w;
    215         let b2 = (1.0 - cos_w) / 2.0;
    216         let a0 = 1.0 + alpha;
    217         let a1 = -2.0 * cos_w;
    218         let a2 = 1.0 - alpha;
    219 
    220         // Denormal prevention: add a tiny offset to the input
    221         let filter_input = mixed_output + 1e-20;
    222 
    223         let filtered = (b0 / a0) * filter_input
    224             + (b1 / a0) * self.filter_mem[0]
    225             + (b2 / a0) * self.filter_mem[1]
    226             - (a1 / a0) * self.filter_mem[2]
    227             - (a2 / a0) * self.filter_mem[3];
    228 
    229         self.filter_mem[1] = self.filter_mem[0];
    230         self.filter_mem[0] = filter_input;
    231         self.filter_mem[3] = self.filter_mem[2];
    232         self.filter_mem[2] = filtered;
    233 
    234         self.latest_output = filtered;
    235 
    236         // "Metal Zone" Mode: Scale master gain with acceleration for heavy saturation
    237         let shake_intensity = (self.s_la_x.abs() + self.s_la_y.abs() + self.s_la_z.abs()) * 4.0;
    238         let master_gain = 0.6 + shake_intensity;
    239         (filtered * master_gain).tanh()
    240     }
    241 
    242     pub fn get_latest_output(&self) -> f64 {
    243         self.latest_output
    244     }
    245 }