JavaScript · 9844 bytes Raw Blame History
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 }