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 }