| 1 | // js/effects.js |
| 2 | |
| 3 | export class Effects { |
| 4 | constructor(audioContext) { |
| 5 | this.context = audioContext; |
| 6 | this.setupEffects(); |
| 7 | } |
| 8 | |
| 9 | setupEffects() { |
| 10 | // Mid/Side processing |
| 11 | this.setupMidSide(); |
| 12 | |
| 13 | // Bitcrusher |
| 14 | this.bitcrusher = new AudioWorkletNode(this.context, 'bitcrusher-processor'); |
| 15 | |
| 16 | // Filters with EXTREME settings |
| 17 | this.lowpass = this.context.createBiquadFilter(); |
| 18 | this.lowpass.type = 'lowpass'; |
| 19 | this.lowpass.frequency.value = 1000; |
| 20 | |
| 21 | this.highpass = this.context.createBiquadFilter(); |
| 22 | this.highpass.type = 'highpass'; |
| 23 | this.highpass.frequency.value = 1000; |
| 24 | |
| 25 | // Delay with modulation |
| 26 | this.setupDelay(); |
| 27 | |
| 28 | // Add LFO for delay modulation |
| 29 | this.lfo = this.context.createOscillator(); |
| 30 | this.lfoGain = this.context.createGain(); |
| 31 | this.lfoGain.gain.value = 0.002; // Subtle modulation |
| 32 | this.lfo.frequency.value = 0.5; |
| 33 | this.lfo.connect(this.lfoGain); |
| 34 | this.lfoGain.connect(this.delay.delayTime); |
| 35 | this.lfo.start(); |
| 36 | |
| 37 | // Convolution reverb with filter |
| 38 | this.setupReverb(); |
| 39 | |
| 40 | // Add reverb filter for character |
| 41 | this.reverbFilter = this.context.createBiquadFilter(); |
| 42 | this.reverbFilter.type = 'lowpass'; |
| 43 | this.reverbFilter.frequency.value = 5000; |
| 44 | this.convolver.connect(this.reverbFilter); |
| 45 | this.reverbFilter.connect(this.reverbMix); |
| 46 | |
| 47 | // Spectral freeze |
| 48 | this.spectralFreeze = new AudioWorkletNode(this.context, 'spectral-freeze-processor'); |
| 49 | |
| 50 | // Pitch shift (placeholder - uses playback rate in AudioEngine) |
| 51 | this.pitchShift = this.context.createGain(); |
| 52 | |
| 53 | // Add a limiter to prevent clipping from extreme effects |
| 54 | this.limiter = this.context.createDynamicsCompressor(); |
| 55 | this.limiter.threshold.value = -3; |
| 56 | this.limiter.knee.value = 0; |
| 57 | this.limiter.ratio.value = 20; |
| 58 | this.limiter.attack.value = 0.001; |
| 59 | this.limiter.release.value = 0.1; |
| 60 | } |
| 61 | |
| 62 | setupMidSide() { |
| 63 | this.midSideIn = this.context.createChannelSplitter(2); |
| 64 | this.midSideOut = this.context.createChannelMerger(2); |
| 65 | this.midGain = this.context.createGain(); |
| 66 | this.sideGain = this.context.createGain(); |
| 67 | this.sideGain.gain.value = 1.0; |
| 68 | |
| 69 | // Create mid/side matrix |
| 70 | // Mid = (L + R) / 2 |
| 71 | // Side = (L - R) / 2 |
| 72 | this.midSideIn.connect(this.midGain, 0); |
| 73 | this.midSideIn.connect(this.midGain, 1); |
| 74 | this.midSideIn.connect(this.sideGain, 0); |
| 75 | this.midSideIn.connect(this.sideGain, 1); |
| 76 | |
| 77 | // Reconstruct L/R from M/S |
| 78 | this.midGain.connect(this.midSideOut, 0, 0); |
| 79 | this.midGain.connect(this.midSideOut, 0, 1); |
| 80 | this.sideGain.connect(this.midSideOut, 0, 0); |
| 81 | |
| 82 | // Invert phase for right channel side |
| 83 | const sideInverter = this.context.createGain(); |
| 84 | sideInverter.gain.value = -1; |
| 85 | this.sideGain.connect(sideInverter); |
| 86 | sideInverter.connect(this.midSideOut, 0, 1); |
| 87 | } |
| 88 | |
| 89 | setupDelay() { |
| 90 | this.delay = this.context.createDelay(2); |
| 91 | this.delay.delayTime.value = 0.3; |
| 92 | this.delayFeedback = this.context.createGain(); |
| 93 | this.delayFeedback.gain.value = 0.4; |
| 94 | this.delayMix = this.context.createGain(); |
| 95 | this.delayMix.gain.value = 0.5; |
| 96 | this.delayDry = this.context.createGain(); |
| 97 | |
| 98 | // Feedback loop |
| 99 | this.delay.connect(this.delayFeedback); |
| 100 | this.delayFeedback.connect(this.delay); |
| 101 | this.delay.connect(this.delayMix); |
| 102 | } |
| 103 | |
| 104 | setupReverb() { |
| 105 | this.convolver = this.context.createConvolver(); |
| 106 | this.reverbMix = this.context.createGain(); |
| 107 | this.reverbMix.gain.value = 0.5; |
| 108 | this.reverbDry = this.context.createGain(); |
| 109 | |
| 110 | // Create default impulse response |
| 111 | this.createDefaultIR(); |
| 112 | } |
| 113 | |
| 114 | createDefaultIR() { |
| 115 | const length = this.context.sampleRate * 2; // 2 seconds |
| 116 | const impulse = this.context.createBuffer(2, length, this.context.sampleRate); |
| 117 | |
| 118 | for (let channel = 0; channel < 2; channel++) { |
| 119 | const channelData = impulse.getChannelData(channel); |
| 120 | for (let i = 0; i < length; i++) { |
| 121 | channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | this.convolver.buffer = impulse; |
| 126 | } |
| 127 | |
| 128 | setImpulseResponse(buffer) { |
| 129 | this.convolver.buffer = buffer; |
| 130 | } |
| 131 | |
| 132 | getEffect(name) { |
| 133 | const effects = { |
| 134 | 'mid_side': this.midSideOut, |
| 135 | 'bitcrush': this.bitcrusher, |
| 136 | 'lowpass': this.lowpass, |
| 137 | 'highpass': this.highpass, |
| 138 | 'delay': this.delayMix, |
| 139 | 'reverb': this.reverbMix, |
| 140 | 'spectral_freeze': this.spectralFreeze, |
| 141 | 'pitch_shift': this.pitchShift |
| 142 | }; |
| 143 | |
| 144 | return effects[name]; |
| 145 | } |
| 146 | |
| 147 | connectEffect(name, input, output) { |
| 148 | switch (name) { |
| 149 | case 'mid_side': |
| 150 | input.connect(this.midSideIn); |
| 151 | this.midSideOut.connect(output); |
| 152 | break; |
| 153 | |
| 154 | case 'delay': |
| 155 | input.connect(this.delay); |
| 156 | input.connect(this.delayDry); |
| 157 | this.delayMix.connect(output); |
| 158 | this.delayDry.connect(output); |
| 159 | break; |
| 160 | |
| 161 | case 'reverb': |
| 162 | input.connect(this.convolver); |
| 163 | input.connect(this.reverbDry); |
| 164 | this.convolver.connect(this.reverbMix); |
| 165 | this.reverbMix.connect(output); |
| 166 | this.reverbDry.connect(output); |
| 167 | break; |
| 168 | |
| 169 | default: |
| 170 | const effect = this.getEffect(name); |
| 171 | input.connect(effect); |
| 172 | effect.connect(output); |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | updateParameter(effectName, value) { |
| 177 | switch (effectName) { |
| 178 | case 'mid_side': |
| 179 | // EXTREME stereo effects - from completely mono to psychedelic width |
| 180 | this.sideGain.gain.value = value * 8.0; // SUPER WIDE |
| 181 | // Also mess with the mid gain for more extreme effect |
| 182 | this.midGain.gain.value = 1 - (value * 0.8); // Reduce mid as side increases |
| 183 | break; |
| 184 | |
| 185 | case 'bitcrush': |
| 186 | // EXTREME bit reduction with sample rate reduction too! |
| 187 | const bitDepth = Math.max(1, 8 - (value * 7)); // 8-bit down to 1-bit |
| 188 | this.bitcrusher.parameters.get('bitDepth').value = bitDepth; |
| 189 | // TODO: Add sample rate reduction for more lofi effect |
| 190 | break; |
| 191 | |
| 192 | case 'lowpass': |
| 193 | // INSANE filter sweep from sub-bass to almost nothing |
| 194 | this.lowpass.frequency.value = 20 * Math.pow(500, value); // 20Hz to 10kHz |
| 195 | this.lowpass.Q.value = 0.5 + (value * value * 30); // Exponential resonance - SCREAMING at high values |
| 196 | // Add some gain compensation for the resonance |
| 197 | const lpGain = 1 - (value * value * 0.5); |
| 198 | if (this.lowpass.gain) this.lowpass.gain.value = lpGain; |
| 199 | break; |
| 200 | |
| 201 | case 'highpass': |
| 202 | // CRAZY high pass that can remove everything |
| 203 | this.highpass.frequency.value = 10 * Math.pow(1000, value); // 10Hz to 10kHz |
| 204 | this.highpass.Q.value = 0.5 + (value * value * 30); // SCREAMING resonance |
| 205 | break; |
| 206 | |
| 207 | case 'delay': |
| 208 | // CHAOS delay - multiple taps, extreme feedback |
| 209 | const delayTime = 0.001 + (value * value * 1.5); // 1ms to 1.5 seconds (exponential) |
| 210 | this.delay.delayTime.value = delayTime; |
| 211 | this.delayFeedback.gain.value = Math.min(0.98, value * 1.1); // Can go over 100%! |
| 212 | this.delayMix.gain.value = value * 1.5; // Delay can be louder than dry |
| 213 | |
| 214 | // Modulate delay time slightly for chorus/flanger effects at low values |
| 215 | if (value < 0.3 && this.lfo) { |
| 216 | this.lfo.frequency.value = 2 + value * 10; |
| 217 | } |
| 218 | break; |
| 219 | |
| 220 | case 'reverb': |
| 221 | // MASSIVE reverb with pre-delay and filtering |
| 222 | this.reverbMix.gain.value = value * value * 2; // Exponential curve, can be 200% wet |
| 223 | this.reverbDry.gain.value = 1 - value; |
| 224 | |
| 225 | // Add some filtering to the reverb for more character |
| 226 | if (this.reverbFilter) { |
| 227 | this.reverbFilter.frequency.value = 200 + (1 - value) * 5000; // Darker reverb as it gets wetter |
| 228 | } |
| 229 | break; |
| 230 | |
| 231 | case 'spectral_freeze': |
| 232 | // Multiple freeze modes based on position |
| 233 | if (value < 0.2) { |
| 234 | this.spectralFreeze.parameters.get('freeze').value = 0; |
| 235 | } else if (value < 0.5) { |
| 236 | // Stutter freeze |
| 237 | this.spectralFreeze.parameters.get('freeze').value = |
| 238 | Math.sin(Date.now() * 0.01) > 0 ? 1 : 0; |
| 239 | } else { |
| 240 | // Full freeze |
| 241 | this.spectralFreeze.parameters.get('freeze').value = 1; |
| 242 | } |
| 243 | break; |
| 244 | |
| 245 | case 'pitch_shift': |
| 246 | // EXTREME pitch shifting with formant effects |
| 247 | // This is handled in AudioEngine but let's add a note |
| 248 | break; |
| 249 | } |
| 250 | } |
| 251 | } |