PageSourceSearch

https://pupwife.com/scripts/daw/daw.js?v=citrus-synth-flyout-3

js pupwife.com collected 2026-09-30 15:06:41 UTC 246,018 bytes, 5,182 lines download raw bytes

1// ============================================
2// DAW - Main Controller
3// ============================================
4// This is the main entry point for the Digital Audio Workstation.
5// It initializes all modules and coordinates between them.
6
7import { MIDI } from './midi.js';
8import { Synth } from './synth.js';
9import { Effects } from './effects.js';
10import { Soundfonts } from './soundfonts.js';
11import { SynesthesiaVisualizer } from './synesthesia-visualizer.js';
12import { SpectrogramVisualizer } from './spectrogram-visualizer.js';
13import { enhanceRangeSliders } from './rangeSliders.js';
14import { SYNTH_PRESET_LIST } from './synthPresets.js';
15
16// Verify all imports
17if (!MIDI) {
18    console.error('MIDI module failed to import');
19}
20if (!Synth) {
21    console.error('Synth module failed to import');
22}
23if (!Effects) {
24    console.error('Effects module failed to import');
25}
26
27class DAW {
28    constructor() {
29        try {
30            this.audioContext = null;
31            this.masterGain = null;
32            this.masterAnalyser = null;
33            this.isInitialized = false;
34            this.uiSetupComplete = false;
35            
36            // Initialize modules with error checking
37            if (!MIDI) throw new Error('MIDI is undefined');
38            if (!Synth) throw new Error('Synth is undefined');
39            if (!Effects) throw new Error('Effects is undefined');
40            
41            this.midi = new MIDI();
42            this.synth = new Synth();
43            this.effects = new Effects();
44            this.soundfonts = null; // Will be initialized after audio context is ready
45            this.visualizer = new SynesthesiaVisualizer();
46            this.spectrogramVisualizer = new SpectrogramVisualizer();
47            
48            // Bind methods
49            this.init = this.init.bind(this);
50            this.initializeAudio = this.initializeAudio.bind(this);
51        } catch (error) {
52            console.error('Error in DAW constructor:', error);
53            throw error;
54        }
55    }
56    
57    // Debounced save to avoid too many localStorage writes
58    saveSettingsDebounced() {
59        if (this.saveTimeout) {
60            clearTimeout(this.saveTimeout);
61        }
62        this.saveTimeout = setTimeout(() => {
63            this.saveSettings();
64        }, 300);
65    }
66    
67    // Save all settings to localStorage
68    saveSettings() {
69        try {
70            const settings = {
71                soundfont: document.getElementById('soundfont-select')?.value || 'sf2:Piano',
72                showSynthesized: document.getElementById('show-synthesized')?.checked || false,
73                a4Frequency: document.getElementById('a4-freq')?.value || 440,
74                tuningSystem: document.getElementById('tuning-system')?.value || 'equal',
75                maxPolyphony: document.getElementById('polyphony')?.value || 8,
76                velocityCurve: document.getElementById('velocity-curve')?.value || 1.0,
77                velocityMultiplier: document.getElementById('velocity-multiplier')?.value || 1.0,
78                notePriority: document.getElementById('note-priority')?.value || 'oldest',
79                arpEnabled: document.getElementById('arp-enabled')?.checked || false,
80                arpBpm: document.getElementById('arp-bpm')?.value || 120,
81                arpChord: document.getElementById('arp-chord')?.value || 'major',
82                arpPattern: document.getElementById('arp-pattern')?.value || 'up',
83                arpOctaves: document.getElementById('arp-octaves')?.value || 1,
84                showKeyLabels: document.getElementById('show-key-labels')?.checked || false,
85                borderColor: document.getElementById('border-color')?.value || '#ffffff',
86                rgbBorderMode: document.getElementById('rgb-border-mode')?.checked || false,
87                rgbSpeed: document.getElementById('rgb-speed-slider')?.value || 6,
88                masterVolume: document.getElementById('master-volume')?.value || 0.8,
89                waveform: document.getElementById('synth-waveform')?.value || 'sine',
90                pulseWidth: document.getElementById('synth-pulse-width')?.value || 0.5,
91                attack: document.getElementById('synth-attack')?.value || 0.01,
92                decay: document.getElementById('synth-decay')?.value || 0.1,
93                sustain: document.getElementById('synth-sustain')?.value || 0.7,
94                release: document.getElementById('synth-release')?.value || 0.3,
95                filterCutoff: document.getElementById('synth-filter-cutoff')?.value || 8000,
96                filterResonance: document.getElementById('synth-filter-resonance')?.value || 1,
97                filterDecay: document.getElementById('synth-filter-decay')?.value || 0.2,
98                filterOctaves: document.getElementById('synth-filter-octaves')?.value || 2,
99                effects: this.getEffectsSettings(),
100                backgroundVisualizerMode:
101                    document.querySelector('input[name="background-visualizer-mode"]:checked')?.value || 'none',
102                synesthesiaIntensity: document.getElementById('synesthesia-intensity')?.value || 50,
103                synesthesiaFade: document.getElementById('synesthesia-fade')?.value || 5
104            };
105            localStorage.setItem('daw-settings', JSON.stringify(settings));
106        } catch (error) {
107            console.error('Error saving settings:', error);
108        }
109    }
110
111    getBackgroundVisualizerModeFromDOM() {
112        const el = document.querySelector('input[name="background-visualizer-mode"]:checked');
113        return el ? el.value : 'none';
114    }
115
116    applyBackgroundVisualizerMode(mode) {
117        if (!['none', 'synesthesia', 'spectrogram'].includes(mode)) {
118            mode = 'none';
119        }
120        if (this.visualizer) {
121            if (mode === 'synesthesia') {
122                this.visualizer.enable();
123            } else {
124                this.visualizer.disable();
125            }
126        }
127        if (this.spectrogramVisualizer) {
128            if (mode === 'spectrogram') {
129                this.spectrogramVisualizer.enable();
130            } else {
131                this.spectrogramVisualizer.disable();
132            }
133        }
134        const synControls = document.getElementById('synesthesia-controls');
135        if (synControls) {
136            synControls.style.display = mode === 'synesthesia' ? 'block' : 'none';
137        }
138    }
139
140    syncBackgroundVisualizerAfterAudioReady() {
141        this.applyBackgroundVisualizerMode(this.getBackgroundVisualizerModeFromDOM());
142    }
143    
144    // Load all settings from localStorage
145    loadSettings() {
146        try {
147            const saved = localStorage.getItem('daw-settings');
148            if (!saved) return;
149            
150            const settings = JSON.parse(saved);
151            
152            // Load basic settings
153            if (settings.a4Frequency !== undefined) {
154                const slider = document.getElementById('a4-freq');
155                const valueSpan = document.getElementById('a4-freq-value');
156                if (slider) {
157                    slider.value = settings.a4Frequency;
158                    if (valueSpan) valueSpan.textContent = settings.a4Frequency;
159                    if (this.synth) this.synth.setA4Frequency(parseInt(settings.a4Frequency));
160                }
161            }
162            
163            if (settings.tuningSystem !== undefined) {
164                const select = document.getElementById('tuning-system');
165                if (select) {
166                    select.value = settings.tuningSystem;
167                    if (this.synth) this.synth.setTuningSystem(settings.tuningSystem);
168                }
169            }
170            
171            if (settings.maxPolyphony !== undefined) {
172                const slider = document.getElementById('polyphony');
173                const valueSpan = document.getElementById('polyphony-value');
174                if (slider) {
175                    slider.value = settings.maxPolyphony;
176                    if (valueSpan) valueSpan.textContent = settings.maxPolyphony;
177                    if (this.synth) this.synth.setMaxPolyphony(parseInt(settings.maxPolyphony));
178                }
179            }
180            
181            if (settings.velocityCurve !== undefined) {
182                const slider = document.getElementById('velocity-curve');
183                const valueSpan = document.getElementById('velocity-curve-value');
184                if (slider) {
185                    slider.value = settings.velocityCurve;
186                    if (valueSpan) valueSpan.textContent = parseFloat(settings.velocityCurve).toFixed(1);
187                    if (this.synth) this.synth.setVelocityCurve(parseFloat(settings.velocityCurve));
188                }
189            }
190            
191            if (settings.velocityMultiplier !== undefined) {
192                const slider = document.getElementById('velocity-multiplier');
193                const valueSpan = document.getElementById('velocity-multiplier-value');
194                if (slider) {
195                    slider.value = settings.velocityMultiplier;
196                    if (valueSpan) valueSpan.textContent = parseFloat(settings.velocityMultiplier).toFixed(1);
197                    if (this.synth) this.synth.setVelocityMultiplier(parseFloat(settings.velocityMultiplier));
198                }
199            }
200            
201            if (settings.notePriority !== undefined) {
202                const select = document.getElementById('note-priority');
203                if (select) {
204                    select.value = settings.notePriority;
205                    if (this.synth) this.synth.setNotePriority(settings.notePriority);
206                }
207            }
208
209            this.applyArpeggiatorSettings(settings);
210            
211            if (settings.showKeyLabels !== undefined) {
212                const checkbox = document.getElementById('show-key-labels');
213                if (checkbox) {
214                    checkbox.checked = settings.showKeyLabels;
215                    if (this.midi) {
216                        this.midi.setShowKeyLabels(settings.showKeyLabels);
217                    }
218                }
219            }
220            
221            // Load RGB border mode setting
222            const rgbBorderModeCheckbox = document.getElementById('rgb-border-mode');
223            const rgbSpeedSlider = document.getElementById('rgb-speed-sli
223der');
224            const rgbSpeedValue = document.getElementById('rgb-speed-value');
225            const rgbSpeedControls = document.getElementById('rgb-speed-controls');
226            
227            if (rgbBorderModeCheckbox) {
228                rgbBorderModeCheckbox.checked = settings.rgbBorderMode || false;
229                this.toggleRgbBorderMode(settings.rgbBorderMode || false);
230            }
231            
232            // Load RGB speed setting
233            if (rgbSpeedSlider && rgbSpeedValue) {
234                const speed = settings.rgbSpeed || 6;
235                rgbSpeedSlider.value = speed;
236                rgbSpeedValue.textContent = speed;
237                this.setRgbSpeed(speed);
238            }
239            
240            // Show/hide speed controls based on RGB mode
241            if (rgbSpeedControls && rgbBorderModeCheckbox) {
242                rgbSpeedControls.style.display = rgbBorderModeCheckbox.checked ? 'block' : 'none';
243            }
244            
245            // Load border color setting (only if RGB mode is off)
246            const colorInput = document.getElementById('border-color');
247            const colorTextInput = document.getElementById('border-color-text');
248            if (colorInput && colorTextInput) {
249                const borderColor = settings.borderColor || '#ffffff'; // Default to white if not set
250                colorInput.value = borderColor;
251                colorTextInput.value = borderColor;
252                if (!(settings.rgbBorderMode || false)) {
253                    this.applyBorderColor(borderColor);
254                }
255            }
256            
257            // Background visualizer (synesthesia / spectrogram / off)
258            let bgMode = settings.backgroundVisualizerMode;
259            if (bgMode === undefined || bgMode === null) {
260                bgMode = settings.synesthesiaEnabled ? 'synesthesia' : 'none';
261            }
262            if (!['none', 'synesthesia', 'spectrogram'].includes(bgMode)) {
263                bgMode = 'none';
264            }
265            const bgRadios = document.querySelectorAll('input[name="background-visualizer-mode"]');
266            if (bgRadios.length) {
267                bgRadios.forEach((r) => {
268                    r.checked = r.value === bgMode;
269                });
270            }
271            this.applyBackgroundVisualizerMode(bgMode);
272
273            const synesthesiaIntensitySlider = document.getElementById('synesthesia-intensity');
274            const synesthesiaIntensityValue = document.getElementById('synesthesia-intensity-value');
275            const synesthesiaFadeSlider = document.getElementById('synesthesia-fade');
276            const synesthesiaFadeValue = document.getElementById('synesthesia-fade-value');
277            
278            if (synesthesiaIntensitySlider && synesthesiaIntensityValue && this.visualizer) {
279                const intensity = settings.synesthesiaIntensity || 50;
280                synesthesiaIntensitySlider.value = intensity;
281                synesthesiaIntensityValue.textContent = intensity;
282                this.visualizer.setIntensity(intensity / 100);
283            }
284            
285            if (synesthesiaFadeSlider && synesthesiaFadeValue && this.visualizer) {
286                const fade = settings.synesthesiaFade || 5;
287                synesthesiaFadeSlider.value = fade;
288                synesthesiaFadeValue.textContent = fade;
289                this.visualizer.setFadeSpeed(fade / 100);
290            }
291            
292            // Master volume will be loaded after audio context is initialized
293            // Store it for later loading
294            if (settings.masterVolume !== undefined) {
295                const slider = document.getElementById('master-volume');
296                if (slider) {
297                    slider.value = settings.masterVolume;
298                    // Set master gain if it exists, otherwise it will be set after audio init
299                    if (this.masterGain) {
300                        this.masterGain.gain.value = parseFloat(settings.masterVolume);
301                    }
302                }
303            }
304            
305            if (settings.waveform !== undefined) {
306                const select = document.getElementById('synth-waveform');
307                if (select) {
308                    select.value = settings.waveform;
309                    if (this.synth) this.synth.setWaveform(settings.waveform);
310                }
311            }
312            
313            if (settings.pulseWidth !== undefined) {
314                const slider = document.getElementById('synth-pulse-width');
315                const valueSpan = document.getElementById('pulse-width-value');
316                if (slider) {
317                    slider.value = settings.pulseWidth;
318                    if (valueSpan) valueSpan.textContent = parseFloat(settings.pulseWidth).toFixed(2);
319                    if (this.synth) this.synth.setPulseWidth(parseFloat(settings.pulseWidth));
320                }
321            }
322            
323            if (settings.attack !== undefined) {
324                const slider = document.getElementById('synth-attack');
325                const valueSpan = document.getElementById('attack-value');
326                if (slider) {
327                    slider.value = settings.attack;
328                    if (valueSpan) valueSpan.textContent = parseFloat(settings.attack).toFixed(2);
329                }
330            }
331            
332            if (settings.decay !== undefined) {
333                const slider = document.getElementById('synth-decay');
334                const valueSpan = document.getElementById('decay-value');
335                if (slider) {
336                    slider.value = settings.decay;
337                    if (valueSpan) valueSpan.textContent = parseFloat(settings.decay).toFixed(2);
338                }
339            }
340            
341            if (settings.sustain !== undefined) {
342                const slider = document.getElementById('synth-sustain');
343                const valueSpan = document.getElementById('sustain-value');
344                if (slider) {
345                    slider.value = settings.sustain;
346                    if (valueSpan) valueSpan.textContent = parseFloat(settings.sustain).toFixed(2);
347                }
348            }
349            
350            if (settings.release !== undefined) {
351                const slider = document.getElementById('synth-release');
352                const valueSpan = document.getElementById('release-value');
353                if (slider) {
354                    slider.value = settings.release;
355                    if (valueSpan) valueSpan.textContent = parseFloat(settings.release).toFixed(2);
356                }
357            }
358            
359            if (settings.filterCutoff !== undefined) {
360                const slider = document.getElementById('synth-filter-cutoff');
361                const valueSpan = document.getElementById('filter-cutoff-value');
362                if (slider) {
363                    slider.value = settings.filterCutoff;
364                    if (valueSpan) valueSpan.textContent = Math.round(settings.filterCutoff);
365                }
366            }
367            if (settings.filterResonance !== undefined) {
368                const slider = document.getElementById('synth-filter-resonance');
369                const valueSpan = document.getElementById('filter-resonance-value');
370                if (slider) {
371                    slider.value = settings.filterResonance;
372                    if (valueSpan) valueSpan.textContent = parseFloat(settings.filterResonance).toFixed(1);
373                }
374            }
375            if (settings.filterDecay !== undefined) {
376                const slider = document.getElementById('synth-filter-decay');
377                const valueSpan = document.getElementById('filter-decay-value');
378                if (slider) {
379                    slider.value = settings.filterDecay;
380                    if (valueSpan) valueSpan.textContent = parseFloat(settings.filterDecay).toFixed(2);
381                }
382            }
383            if (settings.filterOctaves !== undefined) {
384                const slider = document.getElementById('synth-filter-octaves');
385                const valueSpan = document.getElementById('filter-octaves-value');
386                if (slider) {
387                    slider.value = settings.filterOctaves;
388                    if (valueSpan) valueSpan.textContent = parseFloat(settings.filterOctaves).toFixed(1);
389                }
390            }
391
392            if (this.synth && settings.filterCutoff !== undefined) {
393                this.synth.setFilter(
394                    parseFloat(settings.filterCutoff),
395                    parseFloat(settings.filterResonance ?? 1)
396                );
397                this.synth.setFilterEnvelope({
398                    decay: parseFloat(settings.filterDecay ?? 0.2),
399                    octaves: parseFloat(settings.filterOctaves ?? 2)
400                });
401            }
402            
403            // Update envelope if all values are loaded
404            if (settings.attack !== undefined && settings.decay !== undefined && 
405                settings.sustain !== undefined && settings.release !== undefined && this.synth) {
406                this.synth.setEnvelope(
407                    parseFloat(settings.attack),
408                    parseFloat(settings.decay),
409                    parseFloat(settings.sustain),
410                    parseFloat(settings.release)
411                );
412            }
413            
414            // Effects settings will be loaded after audio context is initialized
415            // Store them for later loading
416            if (settings.effects) {
417                this.pendingEffectsSettings = settings.effects;
418                // Try to load now if audio is already initialized
419                if (this.audioContext && this.audioContext.state === 'running' && this.effects) {
420                    this.loadEffectsSettings(settings.effects);
421                }
422            }
423        } catch (error) {
424            console.error('Error loading settings:', error);
425        }
426    }
427    
428    // Get current effects settings from DOM
429    getEffectsSettings() {
430        const effects = {};
431        
432        // Helper to get effect settings
433        const getEffect = (name, params) => {
434            const checkbox = document.getElementById(`effect-${name}`);
435            if (!checkbox) return null;
436            
437            const result = { enabled: checkbox.checked };
438            params.forEach(param => {
439                const slider = document.getElementById(`${name}-${param}`);
440                if (slider) {
441                    result[param] = parseFloat(slider.value);
442                }
443            });
444            return result;
445        };
446        
447        effects.reverb = getEffect('reverb', ['room', 'damp']);
448        effects.delay = getEffect('delay', ['time', 'feedback', 'mix']);
449        effects.distortion = getEffect('distortion', ['amount']);
450        effects.chorus = getEffect('chorus', ['rate', 'depth', 'feedback']);
451        effects.phaser = getEffect('phaser', ['rate', 'depth', 'feedback']);
452        effects.filter = getEffect('filter', ['freq', 'res']);
453        effects.compressor = getEffect('compressor', ['threshold', 'ratio', 'knee', 'attack', 'release']);
454        effects.tremolo = getEffect('tremolo', ['rate', 'depth']);
455        effects.vibrato = getEffect('vibrato', ['rate', 'depth']);
456        effects.bitcrusher = getEffect('bitcrusher', ['bits', 'sr', 'mix']);
457        effects.ringmod = getEffect('ringmod', ['frequency', 'mix']);
458        effects.flanger = getEffect('flanger', ['rate', 'depth', 'feedback', 'delay']);
459        effects.autowah = getEffect('autowah', ['sensitivity', 'range', 'resonance', 'attack', 'release']);
460        effects.pitchshift = getEffect('pitchshift', ['semitones', 'cents']);
461        effects.echo = getEffect('echo', ['time', 'feedback', 'mix']);
462        effects.overdrive = getEffect('overdrive', ['drive', 'tone', 'level']);
463        effects.fuzz = getEffect('fuzz', ['intensity', 'gate', 'mix']);
464        effects.stereowidener = getEffect('stereowidener', ['width', 'balance']);
465        effects.exciter = getEffect('exciter', ['amount', 'freq']);
466        effects.noisegate = getEffect('noisegate', ['threshold', 'attack', 'release']);
467        effects.limiter = getEffect('limiter', ['threshold', 'release']);
468        
469        // Get filter type
470        const filterTypeSelect = document.getElementById('filter-type');
471        if (filterTypeSelect && effects.filter) {
472            effects.filter.type = filterTypeSelect.value;
473        }
474        
475        // Get tremolo waveform
476        const tremoloWaveformSelect = document.getElementById('tremolo-waveform');
477        if (tremoloWaveformSelect && effects.tremolo) {
478            effects.tremolo.waveform = tremoloWaveformSelect.value;
479        }
480        
481        return effects;
482    }
483    
484    // Load effects settings from saved data
485    loadEffectsSettings(effects) {
486        if (!this.effects) return;
487        
488        // Ensure audio context is ready
489        if (!this.audioContext || this.audioContext.state !== 'running') {
490            console.warn('Audio context not ready, deferring effects loading');
491            this.pendingEffectsSettings = effects;
492            return;
493        }
494        
495        // Helper to set effect
496        const setEffect = (name, params, methodName) => {
497            const effect = effects[name];
498            if (!effect) return;
499            
500            const checkbox = document.getElementById(`effect-${name}`);
501            const controls = document.getElementById(`${name}-controls`);
502            
503            if (checkbox) {
504                checkbox.checked = effect.enabled || false;
505                if (controls) {
506                    controls.style.display = effect.enabled ? 'block' : 'none';
507                }
508            }
509            
510            const values = params.map(param => {
511                const slider = document.getElementById(`${name}-${param}`);
512                if (slider && effect[param] !== undefined) {
513                    slider.value = effect[param];
514                    const valueSpan = document.getElementById(`${name}-${param}-value`);
515                    if (valueSpan) {
516                        const val = parseFloat(effect[param]);
517                        // Format based on the parameter type
518                        if (param === 'bits' || param === 'sample-rate' || param === 'range' || param === 'freq') {
519                            valueSpan.textContent = Math.round(val);
520                        } else if (param === 'threshold' || param === 'ratio' || param === 'knee') {
521                            valueSpan.textContent = val % 1 === 0 ? val : val.toFixed(1);
522                        } else if (param === 'attack' || param === 'release') {
523                            valueSpan.textContent = val.toFixed(3);
524                        } else {
525                            valueSpan.textContent = val % 1 === 0 ? val : val.toFixed(2);
526                        }
527                    }
528                }
529                return effect[param] !== undefined ? parseFloat(effect[param]) : undefined;
530            });
531            
532            // Call the effect setter method - ensure all values are present
533            if (this.effects[methodName]) {
534                // For effects that need all parameters, check if we have them
535                const hasAllValues = values.every(v => v !== undefined);
536                if (hasAllValues) {
537                    // Apply the effect with saved values
538                    this.effects[methodName](effect.enabled, ...values);
539                } else if (effect.enabled) {
540                    // If enabled but missing some values, use defaults for missing ones
541                    const defaultValues = values.map((v, i) => v !== undefined ? v : this.getDefaultEffectValue(name, params[i]));
542                    this.effects[methodName](true, ...defaultValues);
543                }
544            }
545        };
546        
547        setEffect('reverb', ['room', 'damp'], 'setReverb');
548        setEffect('delay', ['time', 'feedback', 'mix'], 'setDelay');
549        setEffect('distortion', ['amount'], 'setDistortion');
550        setEffect('chorus', ['rate', 'depth', 'feedback'], 'setChorus');
551        setEffect('phaser', ['rate', 'depth', 'feedback'], 'setPhaser');
552        // Filter needs special handling for type parameter
553        const filterEffect = effects.filter;
554        if (filterEffect) {
555            const checkbox = document.getElementById('effect-filter');
556            const controls = document.getElementById('filter-controls');
557            const filterTypeSelect = document.getElementById('filter-type');
558            const filterFreqSlider = document.getElementById('filter-freq');
559            const filterResSlider = document.getElementById('filter-res');
560            
561            if (checkbox) {
562                checkbox.checked = filterEffect.enabled || false;
563                if (controls) {
564                    controls.style.display = filterEffect.enabled ? 'block' : 'none';
565                }
566            }
567            
568            if (filterTypeSelect && filterEffect.type) {
569                filterTypeSelect.value = filterEffect.type;
570            }
571            
572            if (filterFreqSlider && filterEffect.freq !== undefined) {
573                filterFreqSlider.value = filterEffect.freq;
574                const valueSpan = document.getElementById('filter-freq-value');
575                if (valueSpan) valueSpan.textContent = Math.round(filterEffect.freq);
576            }
577            
578            if (filterResSlider && filterEffect.res !== undefined) {
579                filterResSlider.value = filterEffect.res;
580                const valueSpan = document.getElementById('filter-res-value');
581                if (valueSpan) valueSpan.textContent = parseFloat(filterEffect.res).toFixed(1);
582            }
583            
584            if (this.effects.setFilter && filterEffect.freq !== undefined && filterEffect.res !== undefined) {
585                const type = filterEffect.type || filterTypeSelect?.value || 'lowpass';
586                this.effects.setFilter(filterEffect.enabled || false, t
586ype, parseFloat(filterEffect.freq), parseFloat(filterEffect.res));
587            }
588        }
589        
590        // Tremolo needs special handling for waveform parameter
591        const tremoloEffect = effects.tremolo;
592        if (tremoloEffect) {
593            const checkbox = document.getElementById('effect-tremolo');
594            const controls = document.getElementById('tremolo-controls');
595            const tremoloWaveformSelect = document.getElementById('tremolo-waveform');
596            const tremoloRateSlider = document.getElementById('tremolo-rate');
597            const tremoloDepthSlider = document.getElementById('tremolo-depth');
598            
599            if (checkbox) {
600                checkbox.checked = tremoloEffect.enabled || false;
601                if (controls) {
602                    controls.style.display = tremoloEffect.enabled ? 'block' : 'none';
603                }
604            }
605            
606            if (tremoloWaveformSelect && tremoloEffect.waveform) {
607                tremoloWaveformSelect.value = tremoloEffect.waveform;
608            }
609            
610            if (tremoloRateSlider && tremoloEffect.rate !== undefined) {
611                tremoloRateSlider.value = tremoloEffect.rate;
612                const valueSpan = document.getElementById('tremolo-rate-value');
613                if (valueSpan) valueSpan.textContent = parseFloat(tremoloEffect.rate).toFixed(1);
614            }
615            
616            if (tremoloDepthSlider && tremoloEffect.depth !== undefined) {
617                tremoloDepthSlider.value = tremoloEffect.depth;
618                const valueSpan = document.getElementById('tremolo-depth-value');
619                if (valueSpan) valueSpan.textContent = parseFloat(tremoloEffect.depth).toFixed(2);
620            }
621            
622            if (this.effects.setTremolo && tremoloEffect.rate !== undefined && tremoloEffect.depth !== undefined) {
623                const waveform = tremoloEffect.waveform || tremoloWaveformSelect?.value || 'sine';
624                this.effects.setTremolo(tremoloEffect.enabled || false, parseFloat(tremoloEffect.rate), parseFloat(tremoloEffect.depth), waveform);
625            }
626        }
627        
628        // Regular effects (without special parameters)
629        setEffect('reverb', ['room', 'damp'], 'setReverb');
630        setEffect('delay', ['time', 'feedback', 'mix'], 'setDelay');
631        setEffect('distortion', ['amount'], 'setDistortion');
632        setEffect('chorus', ['rate', 'depth', 'feedback'], 'setChorus');
633        setEffect('phaser', ['rate', 'depth', 'feedback'], 'setPhaser');
634        setEffect('compressor', ['threshold', 'ratio', 'knee', 'attack', 'release'], 'setCompressor');
635        setEffect('vibrato', ['rate', 'depth'], 'setVibrato');
636        setEffect('bitcrusher', ['bits', 'sr', 'mix'], 'setBitcrusher');
637        setEffect('ringmod', ['frequency', 'mix'], 'setRingModulator');
638        setEffect('flanger', ['rate', 'depth', 'feedback', 'delay'], 'setFlanger');
639        setEffect('autowah', ['sensitivity', 'range', 'resonance', 'attack', 'release'], 'setAutoWah');
640        setEffect('pitchshift', ['semitones', 'cents'], 'setPitchShifter');
641        setEffect('echo', ['time', 'feedback', 'mix'], 'setEcho');
642        setEffect('overdrive', ['drive', 'tone', 'level'], 'setOverdrive');
643        setEffect('fuzz', ['intensity', 'gate', 'mix'], 'setFuzz');
644        setEffect('stereowidener', ['width', 'balance'], 'setStereoWidener');
645        setEffect('exciter', ['amount', 'freq'], 'setExciter');
646        setEffect('noisegate', ['threshold', 'attack', 'release'], 'setNoiseGate');
647        setEffect('limiter', ['threshold', 'release'], 'setLimiter');
648        
649        // Re-route effects if synth is ready - this ensures audio graph is properly connected
650        if (this.synth) {
651            this.synth.rerouteEffects();
652        }
653    }
654    
655    // Get default value for an effect parameter
656    getDefaultEffectValue(effectName, paramName) {
657        const defaults = {
658            'reverb': { 'room': 0.5, 'damp': 0.5 },
659            'delay': { 'time': 0.3, 'feedback': 0.3, 'mix': 0.3 },
660            'distortion': { 'amount': 0.5 },
661            'chorus': { 'rate': 1.5, 'depth': 0.3, 'feedback': 0.2 },
662            'phaser': { 'rate': 0.5, 'depth': 0.6, 'feedback': 0.3 },
663            'filter': { 'freq': 2000, 'res': 1 },
664            'compressor': { 'threshold': -24, 'ratio': 4, 'knee': 30, 'attack': 0.003, 'release': 0.25 },
665            'tremolo': { 'rate': 5, 'depth': 0.5 },
666            'vibrato': { 'rate': 5, 'depth': 0.1 },
667            'bitcrusher': { 'bits': 8, 'sr': 1, 'mix': 1.0 },
668            'ringmod': { 'frequency': 100, 'mix': 0.5 },
669            'flanger': { 'rate': 0.5, 'depth': 0.3, 'feedback': 0.3, 'delay': 0.005 },
670            'autowah': { 'sensitivity': 0.5, 'range': 2000, 'resonance': 5, 'attack': 0.01, 'release': 0.1 },
671            'pitchshift': { 'semitones': 0, 'cents': 0 }
672        };
673        return defaults[effectName]?.[paramName] || 0;
674    }
675    
676    // Helper to get effect method name
677    getEffectMethodName(effectName) {
678        const methodMap = {
679            'reverb': 'setReverb',
680            'delay': 'setDelay',
681            'distortion': 'setDistortion',
682            'chorus': 'setChorus',
683            'phaser': 'setPhaser',
684            'filter': 'setFilter',
685            'compressor': 'setCompressor',
686            'tremolo': 'setTremolo',
687            'vibrato': 'setVibrato',
688            'bitcrusher': 'setBitcrusher',
689            'ringmod': 'setRingModulator',
690            'flanger': 'setFlanger',
691            'autowah': 'setAutoWah',
692            'pitchshift': 'setPitchShifter'
693        };
694        return methodMap[effectName];
695    }
696    
697    // Helper to get effect parameter names
698    getEffectParams(effectName) {
699        const paramMap = {
700            'reverb': ['room', 'damp'],
701            'delay': ['time', 'feedback', 'mix'],
702            'distortion': ['amount'],
703            'chorus': ['rate', 'depth', 'feedback'],
704            'phaser': ['rate', 'depth', 'feedback'],
705            'filter': ['freq', 'res'],
706            'compressor': ['threshold', 'ratio', 'knee', 'attack', 'release'],
707            'tremolo': ['rate', 'depth'],
708            'vibrato': ['rate', 'depth'],
709            'bitcrusher': ['bits', 'sample-rate', 'mix'],
710            'ringmod': ['frequency', 'mix'],
711            'flanger': ['rate', 'depth', 'feedback', 'delay'],
712            'autowah': ['sensitivity', 'range', 'resonance', 'attack', 'release'],
713            'pitchshift': ['semitones', 'cents']
714        };
715        return paramMap[effectName] || [];
716    }
717    
718    async init() {
719        this._destroyed = false;
720        if (this.isInitialized) return;
721        
722        // Set up UI immediately (doesn't require audio context)
723        this.setupUI();
724        
725        // Load saved settings after UI is set up
726        setTimeout(() => {
727            this.loadSettings();
728        }, 100);
729        
730        // Set up click handlers to initialize audio on any interaction
731        const initOnInteraction = async () => {
732            if (!this.audioContext || this.audioContext.state === 'closed') {
733                console.log('Initializing audio context...');
734                await this.initializeAudio();
735            } else if (this.audioContext.state === 'suspended') {
736                // Just resume if suspended
737                try {
738                    await this.audioContext.resume();
739                    console.log('Audio context resumed from suspended state');
740                } catch (error) {
741                    console.error('Error resuming audio:', error);
742                }
743            }
744        };
745        
746        // Add listeners that can fire multiple times
747        document.addEventListener('click', initOnInteraction);
748        document.addEventListener('keydown', initOnInteraction);
749        document.addEventListener('touchstart', initOnInteraction);
750        
751        console.log('DAW UI initialized. Audio will initialize on first interaction...');
752    }
753    
754    async initializeAudio() {
755        // Don't reinitialize if already running
756        if (this.audioContext && this.audioContext.state === 'running') {
757            console.log('Audio context already running');
758            return true;
759        }
760        
761        // Resume if suspended
762        if (this.audioContext && this.audioContext.state === 'suspended') {
763            try {
764                await this.audioContext.resume();
765                console.log('Audio context resumed');
766                return true;
767            } catch (error) {
768                console.error('Error resuming audio context:', error);
769            }
770        }
771        
772        try {
773            // Create AudioContext (requires user interaction in some browsers)
774            this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
775            console.log('Audio context created, state:', this.audioContext.state);
776            
777            // Resume if suspended (some browsers start suspended)
778            if (this.audioContext.state === 'suspended') {
779                await this.audioContext.resume();
780                console.log('Audio context resumed after creation');
781            }
782            
783            // Create master gain node → analyser → destination (spectrogram taps analyser)
784            this.masterGain = this.audioContext.createGain();
785            this.masterAnalyser = this.audioContext.createAnalyser();
786            this.masterAnalyser.fftSize = 8192;
787            this.masterAnalyser.minDecibels = -100;
788            this.masterAnalyser.maxDecibels = -5;
789            this.masterAnalyser.smoothingTimeConstant = 0.65;
790            this.masterGain.connect(this.masterAnalyser);
791            this.masterAnalyser.connect(this.audioContext.destination);
792            this.masterGain.gain.value = 0.8; // Default volume
793
794            if (this.spectrogramVisualizer) {
795                this.spectrogramVisualizer.setAnalyser(this.masterAnalyser, this.audioContext.sampleRate);
796            }
797            
798            // Initialize modules with audio context (in proper order)
799            try {
800                await this.effects.init(this.audioContext, this.masterGain);
801            } catch (error) {
802                console.error('Error initializing effects:', error);
803                // Continue even if effects fail
804            }
805            
806            // Initialize soundfonts
807            try {
808                this.soundfonts = new Soundfonts(this.audioContext, this.masterGain);
809            } catch (error) {
810                console.error('Error initializing soundfonts:', error);
811            }
812            
813            try {
814                await this.synth.init(this.audioContext, this.masterGain, this.effects, this.soundfonts);
815            } catch (error) {
816                console.error('Error initializing synth:', error);
817                // This is critical, but try to continue
818            }
819            await this.midi.init(this.audioContext, this.synth);
820            
821            // Re-setup UI now that audio context is ready
822            this.setupAudioUI();
823            
824            // Load master volume now that masterGain exists
825            const saved = localStorage.getItem('daw-settings');
826            if (saved) {
827                try {
828                    const settings = JSON.parse(saved);
829                    if (settings.masterVolume !== undefined && this.masterGain) {
830                        const slider = document.getElementById('master-volume');
831                        if (slider) {
832                            slider.value = settings.masterVolume;
833                            this.masterGain.gain.value = parseFloat(settings.masterVolume);
834                            console.log('Master volume loaded:', settings.masterVolume);
835                        }
836                    }
837                } catch (error) {
838                    console.error('Error loading master volume:', error);
839                }
840            }
841            
842            // Load effects settings now that audio context is ready
843            // Wait a bit to ensure effects are fully initialized
844            setTimeout(() => {
845                if (this.pendingEffectsSettings) {
846                    console.log('Loading pending effects settings...');
847                    this.loadEffectsSettings(this.pendingEffectsSettings);
848                    this.pendingEffectsSettings = null;
849                } else {
850                    // Try loading from localStorage if not already loaded
851                    const saved = localStorage.getItem('daw-settings');
852                    if (saved) {
853                        try {
854                            const settings = JSON.parse(saved);
855                            if (settings.effects) {
856                                console.log('Loading effects from localStorage...');
857                                this.loadEffectsSettings(settings.effects);
858                            }
859                            
860                            // Load saved soundfont category
861                            // Load saved soundfont (new format: "sf2:Name" or "category:instrument")
862                            if (settings.soundfont && this.soundfonts && this.synth) {
863                                const soundfontSelect = document.getElementById('soundfont-select');
864                                if (soundfontSelect) {
865                                    soundfontSelect.value = settings.soundfont;
866                                    // Trigger change event to load the soundfont
867                                    soundfontSelect.dispatchEvent(new Event('change'));
868                                }
869                            } else if (settings.soundfontCategory && this.soundfonts && this.synth) {
870                                // Legacy format support
871                                const value = settings.soundfontInstrument 
872                                    ? `${settings.soundfontCategory}:${settings.soundfontInstrument}`
873                                    : `${settings.soundfontCategory}:null`;
874                                const soundfontSelect = document.getElementById('soundfont-select');
875                                if (soundfontSelect) {
876                                    soundfontSelect.value = value;
877                                    soundfontSelect.dispatchEvent(new Event('change'));
878                                }
879                            } else if (settings.soundfont && settings.soundfont.startsWith('brass-')) {
880                                // Very old format
881                                const instrument = settings.soundfont.replace('brass-', '');
882                                const value = `brass:${instrument}`;
883                                const soundfontSelect = document.getElementById('soundfont-select');
884                                if (soundfontSelect) {
885                                    soundfontSelect.value = value;
886                                    soundfontSelect.dispatchEvent(new Event('change'));
887                                }
888                            }
889                        } catch (error) {
890                            console.error('Error loading effects:', error);
891                        }
892                    }
893                }
894            }, 100);
895
896            this.syncBackgroundVisualizerAfterAudioReady();
897            
898            this.isInitialized = true;
899            console.log('DAW fully initialized and ready! Audio context state:', this.audioContext.state);
900            return true;
901        } catch (error) {
902            console.error('Error initializing DAW:', error);
903            console.error('Error details:', error.message, error.stack);
904            // Don't return false immediately - try to continue with what we have
905            if (this.audioContext && this.audioContext.state === 'running') {
906                // Audio context is working, continue initialization
907                console.log('Audio context is running, continuing despite error...');
908            } else {
909                return false;
910            }
911        }
912    }
913    
914    setupUI() {
915        if (this.uiSetupComplete) return;
916        console.log('Setting up DAW UI...');
917
918        try {
919            enhanceRangeSliders(document.getElementById('daw-container'));
920        } catch (error) {
921            console.error('Error enhancing range sliders:', error);
922        }
923        
924        // Check if required elements exist
925        const virtualKeyboard = document.getElementById('virtual-keyboard');
926        
927        if (!virtualKeyboard) {
928            console.error('Virtual keyboard element not found!');
929        }
930        
931        // Initialize MIDI UI (doesn't need audio context for keyboard display)
932        try {
933            if (this.midi && typeof this.midi.setupUI === 'function') {
934                this.midi.setupUI();
935                console.log('MIDI UI setup complete');
936            } else {
937                console.error('MIDI or setupUI method not available');
938            }
939        } catch (error) {
940            console.error('Error setting up MIDI UI:', error);
941        }
942        
943        // Setup synth controls
944        try {
945            this.setupSynthControls();
946        } catch (error) {
947            console.error('Error setting up synth controls:', error);
948        }
949        
950        // Setup effects controls
951        try {
952            this.setupEffectsControls();
953        } catch (error) {
954            console.error('Error setting up effects controls:', error);
955        }
956        
957        try {
958            this.setupEffectsOrdering();
959        } catch (error) {
960            console.error('Error setting up effects ordering:', error);
961        }
962        
963        // Setup effects dropdown toggle
964        try {
965            this.setupEffectsDropdown();
966        } catch (error) {
967            console.error('Error setting up effects dropdown:', error);
968        }
969        
970        // Setup scales dropdown toggle
971        try {
972            this.setupSoundsDropdown();
973        } catch (error) {
974            console.error('Error setting up sounds dropdown:', error);
975        }
976        
977        try {
978            this.setupScalesDropdown();
979        } catch (error) {
980            console.error('Error setting up scales dropdown:', error);
981        }
982        
983        // Setup settings dropdown toggle
984        try {
985            this.setupSettingsDropdown();
986        } catch (error) {
987            console.error('Error setting up settings dropdown:', error);
988        }
989
990        try {
991            this.setupArpeggiatorControls();
992        } catch (error) {
993            console.error('Error setting up arpeggiator controls:', error);
994        }
995        
996        // Setup sidebar toggle
997        try {
998            this.setupSidebarToggle();
999        } catch (error) {
1000            console.error('Error setting up sidebar toggle:', error);
1001        }
1002        
1003        // Visualizer no longer needs a separate element - it targets keyboard-container directly
1004        
1005        try {
1006            enhanceRangeSliders(document.getElementById('daw-container'));
1007        } catch (error) {
1008            console.error('Error enhancing range sliders (final pass):', error);
1009        }
1010
1011        this.uiSetupComplete = true;
1012        if (document.body.classList.contains('page-citrusos') &&
1013            window.DawSynthFlyout &&
1014            typeof window.DawSynthFlyout.scheduleHeightSync === 'function') {
1015            window.requestAnimationFrame(function () {
1016                window.DawSynthFlyout.scheduleHeightSync();
1017            });
1018        }
1019        console.log('DAW UI setup complete');
1020    }
1021    
1022    setupEffectsDropdown() {
1023        const dropdownButton = document.getElementById('effects-dropdown-button');
1024        const dropdown = document.getElementById('effects-dropdown');
1025        
1026        if (dropdownButton && dropdown) {
1027            // Use a flag to prevent multiple listeners
1028            if (dropdownButton.dataset.listenerAdded) {
1029                return; // Already set up
1030            }
1031            dropdownButton.dataset.listenerAdded = 'true';
1032            
1033            dropdownButton.addEventListener('click', (e) => {
1034                e.stopPropagation();
1035                e.preventDefault();
1036                const isShowing = dropdown.classList.contains('show');
1037                dropdown.classList.toggle('show');
1038                // Close other dropdowns if open
1039                const settingsDropdown = document.getElementById('settings-dropdown');
1040                const scalesDropdown = document.getElementById('scales-dropdown');
1041                if (settingsDropdown) settingsDropdown.classList.remove('show');
1042                if (scalesDropdown) scalesDropdown.classList.remove('show');
1043            });
1044            
1045            // Close dropdown when clicking outside (only add once)
1046            if (!document.effectsDropdownListenerAdded) {
1047                document.addEventListener('click', (e) => {
1048                    if (dropdown && dropdownButton) {
1049                        if (!dropdown.contains(e.target) && !dropdownButton.contains(e.target)) {
1050                            dropdown.classList.remove('show');
1051                        }
1052                    }
1053                });
1054                document.effectsDropdownListenerAdded = true;
1055            }
1056        }
1057    }
1058    
1059    setupSoundsDropdown() {
1060        const dropdownButton = document.getElementById('sounds-dropdown-button');
1061        const dropdown = document.getElementById('sounds-dropdown');
1062        const soundfontSelect = document.getElementById('soundfont-select');
1063        const showSynthesizedCheckbox = document.getElementById('show-synthesized');
1064        
1065        if (!dropdownButton || !dropdown || !soundfontSelect) {
1066            console.error('Sounds dropdown elements not found');
1067            return;
1068        }
1069        
1070        // Use a flag to prevent multiple listeners
1071        if (dropdownButton.dataset.listenerAdded) {
1072            return; // Already set up
1073        }
1074        dropdownButton.dataset.listenerAdded = 'true';
1075        
1076        // Toggle dropdown
1077        const toggleDropdown = (e) => {
1078            e.preventDefault();
1079            e.stopPropagation();
1080            
1081            // Close other dropdowns
1082            document.getElementById('settings-dropdown')?.classList.remove('show');
1083            document.getElementById('scales-dropdown')?.classList.remove('show');
1084            document.getElementById('effects-dropdown')?.classList.remove('show');
1085            
1086            const isOpening = !dropdown.classList.contains('show');
1087            dropdown.classList.toggle('show');
1088            
1089            // Populate dropdown when opening (in case it wasn't ready before)
1090            if (isOpening && this.soundfonts) {
1091                // Small delay to ensure dropdown is visible, then populate
1092                setTimeout(() => {
1093                    const showSynthesized = showSynthesizedCheckbox ? showSynthesizedCheckbox.checked : false;
1094                    this.populateSoundfontDropdown(showSynthesized);
1095                }, 50);
1096            }
1097        };
1098        
1099        dropdownButton.addEventListener('click', toggleDropdown);
1100        
1101        // Close dropdown when clicking outside
1102        if (!document.soundsDropdownListenerAdded) {
1103            document.soundsDropdownListenerAdded = true;
1104            document.addEventListener('click', (e) => {
1105                if (dropdown && dropdownButton) {
1106                    if (!dropdown.contains(e.target) && !dropdownButton.contains(e.target)) {
1107                        dropdown.classList.remove('show');
1108                    }
1109                }
1110            });
1111        }
1112        
1113        // Populate dropdown with SF2 soundfonts first, then synthesized
1114        // Wait for soundfonts to be initialized, including SF2 loader
1115        const populateDropdown = () => {
1116            if (this.soundfonts) {
1117                // Check if SF2 loader is ready (it initializes asynchronously)
1118                const sf2Ready = this.soundfonts.sf2Loader !== null && this.soundfonts.sf2Loader !== undefined;
1119                const showSynthesized = showSynthesizedCheckbox ? showSynthesizedCheckbox.checked : false;
1120                
1121                // Populate immediately, even if SF2 loader isn't ready yet
1122                // It will be populated again when the loader is ready
1123                this.populateSoundfontDropdown(showSynthesized);
1124                
1125                // If SF2 loader isn't ready yet, wait and repopulate when it is
1126                if (!sf2Ready && this.soundfonts.sf2Loader === null) {
1127                    // Check again after SF2 loader initialization delay (1.5s + buffer)
1128                    setTimeout(() => {
1129                        if (this.soundfonts && this.soundfonts.sf2Loader) {
1130                            const showSynthesized = showSynthesizedCheckbox ? showSynthesizedCheckbox.checked : false;
1131                            this.populateSoundfontDropdown(showSynthesized);
1132                        }
1133                    }, 2000);
1134                }
1135            } else {
1136                setTimeout(populateDropdown, 100);
1137            }
1138        };
1139        populateDropdown();
1140        
1141        
1142        // Handle soundfont selection
1143        soundfontSelect.addEventListener('change', async (e) => {
1144            const selectedValue = e.target.value;
1145            if (!selectedValue) return;
1146            
1147            console.log('Soundfont selected:', selectedValue);
1148            
1149            // Parse the value to determine if it's SF2 or synthesized
1150            if (selectedValue.startsWith('sf2:')) {
1151                // SF2 soundfont
1152                const soundfontName = selectedValue.replace('sf2:', '');
1153                await this.handleSF2Selection(soundfontName);
1154            } else {
1155                // Synthesized soundfont (category:instrument format)
1156                const parts = selectedValue.split(':');
1157                if (parts.length === 2) {
1158                    const category = parts[0];
1159                    const instrument = parts[1] === 'null' ? null : parts[1];
1160                    this.handleCategoryChange(category, instrument);
1161                }
1162            }
1163            this.saveSettingsDebounced();
1164        });
1165        
1166        // Listen for SF2 loader ready event to repopulate dropdown
1167        window.addEventListener('sf2LoaderReady', () => {
1168            if (this.soundfonts && this.soundfonts.sf2Loader) {
1169                const showSynthesized = showSynthesizedCheckbox ? showSynthesizedCheckbox.checked : false;
1170                this.populateSoundfontDropdown(showSynthesized);
1171                console.log('✓ SF2 loader ready - dropdown repopulated');
1172            }
1173        });
1174        
1175        // Handle show synthesized checkbox
1176        if (showSynthesizedCheckbox) {
1177            showSynthesizedCheckbox.addEventListener('change', (e) => {
1178                this.populateSoundfontDropdown(e.target.checked);
1179                // Restore selection if it was set
1180                const saved = localStorage.getItem('daw-settings');
1181                if (saved) {
1182                    try {
1183                        const settings = JSON.parse(saved);
1184                        if (settings.soundfont && soundfontSelect) {
1185                            soundfontSelect.value = settings.soundfont;
1186                        }
1187                    } catch (error) {
1188                        // Ignore
1189                    }
1190                }
1191            });
1192            
1193            // Load checkbox state
1194            const saved = localStorage.getItem('daw-settings');
1195            if (saved) {
1196                try {
1197                    const settings = JSON.parse(saved);
1198                    if (settings.showSynthesized !== undefined) {
1199                        showSynthesizedCheckbox.checked = settings.showSynthesized;
1200                    }
1201                } catch (error) {
1202                    // Ignore
1203                }
1204            }
1205        }
1206        
1207        // Load saved soundfont
1208        const saved = localStorage.getItem('daw-settings');
1209        if (saved) {
1210            try {
1211                const settings = JSON.parse(saved);
1212                if (settings.soundfont) {
1213                    soundfontSelect.value = settings.soundfont;
1214                    // Trigger change to load the soundfont
1215                    soundfontSelect.dispatchEvent(new Event('change'));
1216                } else if (settings.soundfontCategory) {
1217                    // Legacy format - convert to new format
1218                    const value = settings.soundfontInstrument 
1219                        ? `${settings.soundfontCategory}:${settings.soundfontInstrument}`
1220                        : `${settings.soundfontCategory}:null`;
1221                    soundfontSelect.value = value;
1222                    soundfontSelect.dispatchEvent(new Event('change'));
1223                }
1224            } catch (error) {
1225                console.error('Error loading soundfont:', error);
1226            }
1227        }
1228        
1229        // Handle custom SF2 file loading
1230        const customSF2Input = document.getElementById('custom-sf2-input');
1231        const clearSF2Input = document.getElementById('clear-sf2-input');
1232        
1233        // Add clear button handler
1234        if (clearSF2Input) {
1235            clearSF2Input.addEventListener('click', () => {
1236                if (customSF2Input) {
1237                    customSF2Input.value = '';
1238                    console.log('Custom SF2 file input cleared');
1239                }
1240            });
1241        }
1242        
1243        if (customSF2Input) {
1244            customSF2Input.addEventListener('change', async (e) => {
1245                const file = e.target.files[0];
1246                if (!file) return;
1247                
1248                if (!file.name.endsWith('.sf2')) {
1249                    alert('Please select a .sf2 SoundFont file');
1250                    e.target.value = ''; // Clear the input
1251                    return;
1252                }
1253                
1254                if (!this.soundfonts || !this.soundfonts.sf2Loader) {
1255                    alert('SF2 library (FluidSynth) not loaded. Please refresh the page and try again.');
1256                    e.target.value = '';
1257                    return;
1258                }
1259                
1260                // Check if FluidSynth is available
1261                const hasFluidSynth = (typeof Synthesizer !== 'undefined' && Synthesizer !== null) ||
1262                                     (typeof window.JSSynth !== 'undefined' && window.JSSynth && window.JSSynth.Synthesizer);
1263                
1264                if (!hasFluidSynth) {
1265                    alert('SF2 library (FluidSynth/js-synthesizer) not available. Please refresh the page and try again.');
1266                    console.error('FluidSynth check failed. Synthesizer:', typeof Synthesizer, 'JSSynth:', typeof window.JSSynth);
1267                    e.target.value = '';
1268                    return;
1269                }
1270                
1271                try {
1272                    console.log('Loading custom SF2 file:', file.name);
1273                    console.log('File size:', file.size, 'bytes');
1274                    console.log('File type:', file.type);
1275                    
1276                    // Extract instrument name from filename (without .sf2 extension)
1277                    const instrumentName = file.name.replace('.sf2', '').replace(/^JR_/, '');
1278                    
1279                    // Read file as ArrayBuffer
1280                    // Use FileReader instead of file.arrayBuffer() for better compatibility
1281                    const arrayBuffer = await new Promise((resolve, reject) => {
1282                        const reader = new FileReader();
1283                        reader.onload = (e) => resolve(e.target.result);
1284                        reader.onerror = (e) => reject(new Error('Failed to read file'));
1285                        reader.readAsArrayBuffer(file);
1286                    });
1287                    
1288                    // Validate file size (SF2 files should be at least a few KB)
1289                    if (arrayBuffer.byteLength < 1024) {
1290                        throw new Error('File is too small to be a valid SF2 file');
1291                    }
1292                    
1293                    // Validate SF2 file header (should start with "RIFF")
1294                    const headerView = new Uint8Array(arrayBuffer, 0, 4);
1295                    const header = String.fromCharCode(headerView[0], headerView[1], headerView[2], headerView[3]);
1296                    
1297                    if (header !== 'RIFF') {
1298                        console.warn('File header does not match SF2 format. Expected "RIFF", got:', header);
1299                        // Continue anyway - some SF2 files might have different headers
1300                    }
1301                    
1302                    console.log('File read successfully:', {
1303                        size: arrayBuffer.byteLength,
1304                        header: header,
1305                        isArrayBuffer: arrayBuffer instanceof ArrayBuffer
1306                    });
1307                    
1308                    // Load the soundfont using the SF2 loader's buffer method
1309                    // This now returns soundfontData with presets included
1310                    const soundfontData = await this.soundfonts.sf2Loader.loadSoundfontFromBuffer(arrayBuffer, instrumentName);
1311                    
1312                    // Store in the loader's loaded soundfonts map (already done by loadSoundfontFromBuffer, but ensure it's there)
1313                    if (!this.soundfonts.sf2Loader.loadedSoundfonts.has(instrumentName)) {
1314                        this.soundfonts.sf2Loader.loadedSoundfonts.set(instrumentName, soundfontData);
1315                    }
1316                    
1317                    // Clear the file input so the same file can be selected again if needed
1318                    // Clear immediately and also after a delay to ensure it works
1319                    e.target.value = '';
1320                    setTimeout(() => {
1321                        if (customSF2Input) {
1322                            customSF2Input.value = '';
1323                        }
1324                    }, 100);
1325                    
1326                    // Always repopulate dropdown to ensure the custom soundfont appears in the Custom category
1327                    const soundfontSelect = document.getElementById('soundfont-select');
1328                    if (soundfontSelect) {
1329                        const showSynthesized = document.getElementById('show-synthesized')?.checked || false;
1330                        this.populateSoundfontDropdown(showSynthesized);
1331                        
1332                        // Re-select the custom soundfont after repopulating
1333                        // Use setTimeout to ensure dropdown is fully populated
1334                        setTimeout(() => {
1335                            soundfontSelect.value = `sf2:${instrumentName}`;
1336                            soundfontSelect.dispatchEvent(new Event('change'));
1337                        }, 50);
1338                    }
1339                    
1340                    console.log(`✓ Custom SF2 file loaded: ${instrumentName}`);
1341                    alert(`Custom SF2 file "${instrumentName}" loaded successfully!`);
1342                } catch (error) {
1343                    console.error('Error loading custom SF2 file:', error);
1344                    alert(`Failed to load SF2 file: ${error.message || error}`);
1345                    e.target.value = ''; // Clear the input
1346                }
1347            });
1348        }
1349    }
1350    
1351    // Populate the soundfont dropdown
1352    populateSoundfontDropdown(showSynthesized = false) {
1353        const soundfontSelect = document.getElementById('soundfont-select');
1354        if (!soundfontSelect) return;
1355        
1356        // Clear existing options
1357        soundfontSelect.innerHTML = '';
1358        
1359        // Add SF2 soundfonts first (prioritized)
1360        // Check if SF2 loader exists and is ready
1361        if (this.soundfonts && this.soundfonts.sf2Loader !== null && this.soundfonts.sf2Loader !== undefined) {
1362            let sf2List = [];
1363            try {
1364                sf2List = this.soundfonts.getAllSF2Soundfonts();
1365            } catch (error) {
1366                console.warn('Error getting SF2 soundfonts list:', error);
1367                sf2List = [];
1368            }
1369            
1370            // Group by category
1371            const grouped = {};
1372            sf2List.forEach(sf => {
1373                if (!grouped[sf.category]) {
1374                    grouped[sf.category] = [];
1375                }
1376                grouped[sf.category].push(sf);
1377            });
1378            
1379            // Add SF2 instruments grouped by category
1380            // Sort categories so "Custom" appears last
1381            const sortedCategories = Object.keys(grouped).sort((a, b) => {
1382                if (a === 'custom') return 1;
1383                if (b === 'custom') return -1;
1384                return a.localeCompare(b);
1385            });
1386            
1387            sortedCategories.forEach(category => {
1388                const categoryName = category === 'custom' ? 'Custom' : category.charAt(0).toUpperCase() + category.slice(1);
1389                const optgroup = document.createElement('optgroup');
1390                optgroup.label = category === 'custom' ? 'Custom (SF2)' : `${categoryName} (SF2)`;
1391                
1392                grouped[category].forEach(sf => {
1393                    const option = document.createElement('option');
1394                    option.value = `sf2:${sf.displayName}`;
1395                    option.textContent = sf.displayName;
1396                    optgroup.appendChild(option);
1397                });
1398                
1399                soundfontSelect.appendChild(optgroup);
1400            });
1401        }
1402        
1403        // Add synthesized instruments if checkbox is checked
1404        if (showSynthesized) {
1405            const presetOptgroup = document.createElement('optgroup');
1406            presetOptgroup.label = 'Synth Presets';
1407            SYNTH_PRESET_LIST.forEach(({ id, label }) => {
1408                const option = document.createElement('option');
1409                option.value = `synth:${id}`;
1410                option.textContent = label;
1411                presetOptgroup.appendChild(option);
1412            });
1413            soundfontSelect.appendChild(presetOptgroup);
1414
1415            const synthesizedOptgroup = document.createElement('optgroup');
1416            synthesizedOptgroup.label = 'Synthesized Instruments';
1417            
1418            // Custom manual synthesizer
1419            const synthOption = document.createElement('option');
1420            synthOption.value = 'synthesizer:null';
1421            synthOption.textContent = 'Custom Synthesizer';
1422            synthesizedOptgroup.appendChild(synthOption);
1423            
1424            // Brass
1425            const brassInstruments = ['trumpet', 'french-horn', 'trombone', 'tuba'];
1426            brassInstruments.forEach(inst => {
1427                const option = document.createElement('option');
1428                option.value = `brass:${inst}`;
1429                option.textContent = `Brass - ${inst.charAt(0).toUpperCase() + inst.slice(1).replace('-', ' ')}`;
1430                synthesizedOptgroup.appendChild(option);
1431            });
1432            
1433            // Woodwinds
1434            const woodwindInstruments = ['flute', 'saxophone', 'clarinet', 'oboe', 'bassoon'];
1435            woodwindInstruments.forEach(inst => {
1436                const option = document.createElement('option');
1437                option.value = `woodwind:${inst}`;
1438                option.textContent = `Woodwind - ${inst.charAt(0).toUpperCase() + inst.slice(1)}`;
1439                synthesizedOptgroup.appendChild(option);
1440            });
1441            
1442            // Strings
1443            const stringInstruments = ['violin', 'viola', 'cello', 'double-bass'];
1444            stringInstruments.forEach(inst => {
1445                const option = document.createElement('option');
1446                option.value = `string:${inst}`;
1447                option.textContent = `String - ${inst.charAt(0).toUpperCase() + inst.slice(1).replace('-', ' ')}`;
1448                synthesizedOptgroup.appendChild(option);
1449            });
1450            
1451            // Vocal
1452            const vocalOptions = ['male-ah', 'male-oh', 'male-oo', 'male-ee', 'female-ah', 'female-oh', 'female-oo', 'female-ee', 'mixed-ah', 'mixed-oh', 'mixed-oo', 'mixed-ee'];
1453            vocalOptions.forEach(inst => {
1454                const option = document.createElement('option');
1455                option.value = `vocal:${inst}`;
1456                const parts = inst.split('-');
1457                option.textContent = `Vocal - ${parts[0].charAt(0).toUpperCase() + parts[0].slice(1)} "${parts[1].toUpperCase()}"`;
1458                synthesizedOptgroup.appendChild(option);
1459            });
1460            
1461            // Percussion
1462            const percOption = document.createElement('option');
1463            percOption.value = 'percussion:drums';
1464            percOption.textContent = 'Percussion - Drums';
1465            synthesizedOptgroup.appendChild(percOption);
1466            
1467            soundfontSelect.appendChild(synthesizedOptgroup);
1468        }
1469    }
1470    
1471    // Handle SF2 soundfont selection
1472    async handleSF2Selection(soundfontName) {
1473        if (!this.soundfonts || !this.soundfonts.sf2Loader) {
1474            console.error('SF2 loader not available');
1475            // Show error in sidebar
1476            this.updateSynthSidebar('sf2', soundfontName);
1477            return;
1478        }
1479        
1480        // CRITICAL: Cancel any in-progress loads before starting a new one
1481        // This prevents the library from getting stuck with multiple simultaneous loads
1482        if (this.soundfonts.sf2Loader._loadingSoundfont && 
1483            this.soundfonts.sf2Loader._loadingSoundfont !== soundfontName) {
1484            console.log(`Cancelling previous load of ${this.soundfonts.sf2Loader._loadingSoundfont} to switch to ${soundfontName}`);
1485            this.soundfonts.sf2Loader.cancelAllLoads();
1486        }
1487        
1488        // Set the soundfont first
1489        this.soundfonts.setSoundfont(soundfontName);
1490        if (this.synth) {
1491            this.synth.setSoundfont(soundfontName, this.soundfonts);
1492        }
1493        
1494        // Update sidebar to show SF2 controls (with loading state)
1495        this.updateSynthSidebar('sf2', soundfontName);
1496        
1497        // Check if already loaded
1498        if (this.soundfonts.isSF2Loaded(soundfontName)) {
1499            // Already loaded, update status and refresh preset selector
1500            const statusEl = document.getElementById('sf2-status');
1501            if (statusEl) {
1502                statusEl.innerHTML = `<label style="color: #8f8; font-size: 11px;">✓ Loaded</label>`;
1503            }
1504            // Refresh preset selector if it exists, or add it if it doesn't
1505            const sidebar = document.getElementById('synth-sidebar');
1506            if (sidebar) {
1507                const existingPresetGroup = document.getElementById('sf2-preset-selector');
1508                if (existingPresetGroup) {
1509                    existingPresetGroup.remove();
1510                }
1511                this.addPresetSelector(sidebar.querySelector('.sidebar-section'), soundfontName);
1512            }
1513            return;
1514        }
1515        
1516        // Show loading indicator
1517        const statusEl = document.getElementById('sf2-status');
1518        if (statusEl) {
1519            statusEl.innerHTML = `<label style="color: #ff8; font-size: 11px;">Loading ${soundfontName}...</label>`;
1520        }
1521        
1522        // Load the soundfont immediately when selected
1523        // Clear any previous loading state to prevent stuck loads
1524        try {
1525            console.log(`Loading SF2 soundfont ${soundfontName} on selection...`);
1526            
1527            // Add a timeout to prevent infinite loading
1528            const loadTimeout = setTimeout(() => {
1529                if (statusEl && statusEl.innerHTML.includes('Loading')) {
1530                    statusEl.innerHTML = `<label style="color: #f44; font-size: 11px;">✗ Load timeout (30s)</label>`;
1531                    console.error(`SF2 soundfont ${soundfontName} load timeout`);
1532                    // Cancel the stuck load
1533                    this.soundfonts.sf2Loader.cancelAllLoads();
1534                }
1535            }, 30000); // 30 second timeout
1536            
1537            const loaded = await this.soundfonts.loadSF2Soundfont(soundfontName);
1538            clearTimeout(loadTimeout);
1539            
1540            if (loaded) {
1541                // Update status
1542                if (statusEl) {
1543                    statusEl.innerHTML = `<label style="color: #8f8; font-size: 11px;">✓ Loaded</label>`;
1544                }
1545                console.log(`✓ SF2 soundfont ${soundfontName} loaded successfully`);
1546                
1547                // Refresh preset selector after loading (with delay to ensure presets are loaded)
1548                const sidebar = document.getElementById('synth-sidebar');
1549                if (sidebar) {
1550                    const existingPresetGroup = document.getElementById('sf2-preset-selector');
1551                    if (existingPresetGroup) {
1552                        existingPresetGroup.remove();
1553                    }
1554                    // Wait a bit for presets to be fully loaded
1555                    setTimeout(() => {
1556                        const section = sidebar.querySelector('.sidebar-section');
1557                        if (section) {
1558                            this.addPresetSelector(section, soundfontName);
1559                        }
1560                    }, 300);
1561                }
1562            } else {
1563                // Loading failed
1564                if (statusEl) {
1565                    statusEl.innerHTML = `<label style="color: #f44; font-size: 11px;">✗ Failed to load (check console)</label>`;
1566                }
1567                console.error(`✗ Failed to load SF2 soundfont ${soundfontName}`);
1568            }
1569        } catch (error) {
1570            console.error(`Error loading SF2 soundfont ${soundfontName}:`, error);
1571            if (statusEl) {
1572                statusEl.innerHTML = `<label style="color: #f44; font-size: 11px;">✗ Error loading</label>`;
1573            }
1574            // Make sure to clear loading state on error
1575            this.soundfonts.sf2Loader.cancelAllLoads();
1576        }
1577    }
1578    
1579    // Handle category change
1580    handleCategoryChange(category, instrumentName = null) {
1581        if (!this.soundfonts) return;
1582        
1583        // Set default instrument for category if not provided
1584        if (!instrumentName) {
1585            const defaults = {
1586                'brass': 'trumpet',
1587                'woodwind': 'flute',
1588                'string': 'violin',
1589                'vocal': 'male-ah',
1590                'percussion': 'drums',
1591                'sf2': null // Will be set based on first available SF2
1592            };
1593            instrumentName = defaults[category] || null;
1594        }
1595        
1596        // Build full soundfont name
1597        let soundfontName = category;
1598        if (category === 'sf2') {
1599            // For SF2, use the display name directly
1600            soundfontName = instrumentName || 'Piano';
1601        } else if (category === 'synth' && instrumentName) {
1602            soundfontName = `synth-${instrumentName}`;
1603        } else if (category !== 'synthesizer' && category !== 'percussion' && instrumentName) {
1604            soundfontName = `${category}-${instrumentName}`;
1605        } else if (category === 'percussion') {
1606            soundfontName = 'percussion-drums';
1607        }
1608        
1609        this.soundfonts.setSoundfont(soundfontName);
1610        if (this.synth) {
1611            this.synth.setSoundfont(soundfontName, this.soundfonts);
1612        }
1613        this.updateSynthSidebar(category, instrumentName);
1614        
1615        // Update synth engine from preset after sidebar is rendered
1616        setTimeout(() => {
1617            if (this.synth && this.soundfonts) {
1618                const instrumentSettings = this.soundfonts.getCurrentSettings();
1619                if (instrumentSettings) {
1620                    this.synth.applySoundfontSettings(instrumentSettings);
1621                    this.syncSynthSidebarFromSettings(instrumentSettings);
1622                }
1623            }
1624        }, 50);
1625    }
1626    
1627    // Update synth sidebar based on selected soundfont category
1628    updateSynthSidebar(category, instrumentName = null) {
1629        const sidebar = document.getElementById('synth-sidebar');
1630        if (!sidebar) {
1631            console.warn('Synth sidebar not found');
1632            return;
1633        }
1634        
1635        // Wait for soundfonts to be initialized
1636        if (!this.soundfonts) {
1637            console.warn('Soundfonts not initialized, retrying...');
1638            setTimeout(() => {
1639                if (this.soundfonts) {
1640                    this.updateSynthSidebar(category, instrumentName);
1641                }
1642            }, 100);
1643            return;
1644        }
1645        
1646        // Clear existing content
1647        sidebar.innerHTML = '';
1648        
1649        // Also remove any orphaned preset selectors that might be outside the sidebar
1650        // (in case they were added to a different section)
1651        const orphanedPresetSelectors = document.querySelectorAll('#sf2-preset-selector');
1652        orphanedPresetSelectors.forEach(el => {
1653            if (el && el.parentNode && !sidebar.contains(el)) {
1654                el.parentNode.removeChild(el);
1655            }
1656        });
1657        
1658        // Create title
1659        const titleSection = document.createElement('div');
1660        titleSection.className = 'sidebar-section';
1661        const title = document.createElement('h3');
1662        title.textContent = this.getCategoryDisplayName(category, instrumentName);
1663        titleSection.appendChild(title);
1664        sidebar.appendChild(titleSection);
1665        
1666        // Add controls based on category
1667        if (category === 'synthesizer' || category === 'synth') {
1668            this.createSynthesizerControls(sidebar);
1669        } else if (category === 'brass') {
1670            this.createBrassControls(sidebar, instrumentName);
1671        } else if (category === 'woodwind') {
1672            this.createWoodwindControls(sidebar, instrumentName);
1673        } else if (category === 'string') {
1674            this.createStringControls(sidebar, instrumentName);
1675        } else if (category === 'vocal') {
1676            this.createVocalControls(sidebar, instrumentName);
1677        } else if (category === 'percussion') {
1678            this.createPercussionControls(sidebar);
1679        } else if (category === 'sf2') {
1680            this.createSF2Controls(sidebar, instrumentName);
1681        }
1682    }
1683    
1684    // Get display name for category
1685    getCategoryDisplayName(category, instrumentName = null) {
1686        if (category === 'synth' && instrumentName) {
1687            const preset = SYNTH_PRESET_LIST.find((p) => p.id === instrumentName);
1688            if (preset) return preset.label;
1689        }
1690        const names = {
1691            'synthesizer': 'Synthesizer',
1692            'synth': 'Synth Preset',
1693            'brass': 'Brass',
1694            'woodwind': 'Woodwinds',
1695            'string': 'Strings',
1696            'vocal': 'Vocal Choral',
1697            'percussion': 'Percussion',
1698            'sf2': 'SF2 Soundfonts'
1699        };
1700        return names[category] || 'Synthesizer';
1701    }
1702    
1703    // Create synthesizer controls
1704    createSynthesizerControls(sidebar) {
1705        const section = document.createElement('div');
1706        section.className = 'sidebar-section';
1707        const settings = this.soundfonts ? this.soundfonts.getCurrentSettings() : null;
1708        
1709        // Waveform select
1710        const waveformGroup = document.createElement('div');
1711        waveformGroup.className = 'control-group';
1712        waveformGroup.innerHTML = `
1713            <label>Waveform:</label>
1714            <select id="synth-waveform">
1715                <option value="sine">Sine</option>
1716                <option value="square">Square</option>
1717                <option value="sawtooth">Sawtooth</option>
1718                <option value="triangle">Triangle</option>
1719                <option value="pulse">Pulse</option>
1720                <option value="pwm">PWM (Pulse Width Modulation)</option>
1721            </select>
1722        `;
1723        section.appendChild(waveformGroup);
1724        
1725        // Pulse width (hidden by default)
1726        const pulseWidthGroup = document.createElement('div');
1727        pulseWidthGroup.className = 'control-group';
1728        pulseWidthGroup.id = 'pulse-width-control';
1729        pulseWidthGroup.style.display = 'none';
1730        pulseWidthGroup.innerHTML = `
1731            <label>Pulse Width: <span id="pulse-width-value">0.5</span></label>
1732            <input type="range" id="synth-pulse-width" min="0.01" max="0.99" step="0.01" value="
17320.5">
1733        `;
1734        section.appendChild(pulseWidthGroup);
1735
1736        if (settings?.waveform) {
1737            const waveformSelect = section.querySelector('#synth-waveform');
1738            if (waveformSelect) waveformSelect.value = settings.waveform;
1739            if (settings.waveform === 'pulse' || settings.waveform === 'pwm') {
1740                pulseWidthGroup.style.display = 'block';
1741            }
1742        }
1743        
1744        if (settings?.pulseWidth !== undefined) {
1745            const pulseSlider = pulseWidthGroup.querySelector('#synth-pulse-width');
1746            const pulseVal = pulseWidthGroup.querySelector('#pulse-width-value');
1747            if (pulseSlider) pulseSlider.value = settings.pulseWidth;
1748            if (pulseVal) pulseVal.textContent = parseFloat(settings.pulseWidth).toFixed(2);
1749        }
1750        
1751        this.addEnvelopeControls(section, settings || { attack: 0.01, decay: 0.1, sustain: 0.7, release: 0.3 });
1752        this.addFilterControls(section, settings);
1753        
1754        sidebar.appendChild(section);
1755        this.setupSynthControls();
1756    }
1757
1758    syncSynthSidebarFromSettings(settings) {
1759        if (!settings) return;
1760
1761        const waveformSelect = document.getElementById('synth-waveform');
1762        if (waveformSelect && settings.waveform) {
1763            waveformSelect.value = settings.waveform;
1764        }
1765
1766        const pulseWidthControl = document.getElementById('pulse-width-control');
1767        if (pulseWidthControl && settings.waveform) {
1768            pulseWidthControl.style.display = (settings.waveform === 'pulse' || settings.waveform === 'pwm') ? 'block' : 'none';
1769        }
1770
1771        const pulseWidthSlider = document.getElementById('synth-pulse-width');
1772        if (pulseWidthSlider && settings.pulseWidth !== undefined) {
1773            pulseWidthSlider.value = settings.pulseWidth;
1774            const pulseVal = document.getElementById('pulse-width-value');
1775            if (pulseVal) pulseVal.textContent = parseFloat(settings.pulseWidth).toFixed(2);
1776        }
1777
1778        const map = [
1779            ['synth-attack', 'attack-value', settings.attack, (v) => v.toFixed(2)],
1780            ['synth-decay', 'decay-value', settings.decay, (v) => v.toFixed(2)],
1781            ['synth-sustain', 'sustain-value', settings.sustain, (v) => v.toFixed(2)],
1782            ['synth-release', 'release-value', settings.release, (v) => v.toFixed(2)],
1783            ['synth-filter-cutoff', 'filter-cutoff-value', settings.filterCutoff, (v) => String(Math.round(v))],
1784            ['synth-filter-resonance', 'filter-resonance-value', settings.filterResonance, (v) => v.toFixed(1)],
1785            ['synth-filter-decay', 'filter-decay-value', settings.filterDecay, (v) => v.toFixed(2)],
1786            ['synth-filter-octaves', 'filter-octaves-value', settings.filterOctaves, (v) => v.toFixed(1)]
1787        ];
1788
1789        map.forEach(([sliderId, valueId, val, fmt]) => {
1790            if (val === undefined) return;
1791            const slider = document.getElementById(sliderId);
1792            const valueSpan = document.getElementById(valueId);
1793            if (slider) slider.value = val;
1794            if (valueSpan) valueSpan.textContent = fmt(parseFloat(val));
1795        });
1796    }
1797
1798    addFilterControls(section, settings) {
1799        const s = settings || {};
1800        const cutoff = s.filterCutoff ?? 8000;
1801        const resonance = s.filterResonance ?? 1;
1802        const fDecay = s.filterDecay ?? 0.2;
1803        const octaves = s.filterOctaves ?? 2;
1804
1805        const cutoffGroup = document.createElement('div');
1806        cutoffGroup.className = 'control-group';
1807        cutoffGroup.innerHTML = `
1808            <label>Filter Cutoff: <span id="filter-cutoff-value">${Math.round(cutoff)}</span> Hz</label>
1809            <input type="range" id="synth-filter-cutoff" min="80" max="12000" step="10" value="${cutoff}">
1810        `;
1811        section.appendChild(cutoffGroup);
1812
1813        const resonanceGroup = document.createElement('div');
1814        resonanceGroup.className = 'control-group';
1815        resonanceGroup.innerHTML = `
1816            <label>Filter Resonance: <span id="filter-resonance-value">${parseFloat(resonance).toFixed(1)}</span></label>
1817            <input type="range" id="synth-filter-resonance" min="0.1" max="20" step="0.1" value="${resonance}">
1818        `;
1819        section.appendChild(resonanceGroup);
1820
1821        const filterDecayGroup = document.createElement('div');
1822        filterDecayGroup.className = 'control-group';
1823        filterDecayGroup.innerHTML = `
1824            <label>Filter Envelope Decay: <span id="filter-decay-value">${parseFloat(fDecay).toFixed(2)}</span>s</label>
1825            <input type="range" id="synth-filter-decay" min="0.01" max="1" step="0.01" value="${fDecay}">
1826        `;
1827        section.appendChild(filterDecayGroup);
1828
1829        const octavesGroup = document.createElement('div');
1830        octavesGroup.className = 'control-group';
1831        octavesGroup.innerHTML = `
1832            <label>Filter Envelope Amount: <span id="filter-octaves-value">${parseFloat(octaves).toFixed(1)}</span> oct</label>
1833            <input type="range" id="synth-filter-octaves" min="0" max="6" step="0.1" value="${octaves}">
1834        `;
1835        section.appendChild(octavesGroup);
1836    }
1837    
1838    // Create brass controls
1839    createBrassControls(sidebar, instrumentName) {
1840        const section = document.createElement('div');
1841        section.className = 'sidebar-section';
1842        
1843        // Instrument selector
1844        const instrumentGroup = document.createElement('div');
1845        instrumentGroup.className = 'control-group';
1846        instrumentGroup.innerHTML = `
1847            <label>Instrument:</label>
1848            <select id="soundfont-instrument-select">
1849                <option value="trumpet">Trumpet</option>
1850                <option value="french-horn">French Horn</option>
1851                <option value="trombone">Trombone</option>
1852                <option value="tuba">Tuba</option>
1853            </select>
1854        `;
1855        section.appendChild(instrumentGroup);
1856        
1857        // Set selected instrument
1858        const instrumentSelect = instrumentGroup.querySelector('select');
1859        if (instrumentSelect && instrumentName) {
1860            instrumentSelect.value = instrumentName;
1861        }
1862        
1863        // Instrument change handler
1864        instrumentSelect.addEventListener('change', (e) => {
1865            const newInstrument = e.target.value;
1866            this.handleCategoryChange('brass', newInstrument);
1867            this.saveSettingsDebounced();
1868        });
1869        
1870        // Waveform select (category-specific for brass)
1871        const waveformGroup = document.createElement('div');
1872        waveformGroup.className = 'control-group';
1873        waveformGroup.innerHTML = `
1874            <label>Waveform:</label>
1875            <select id="synth-waveform">
1876                <option value="sawtooth">Sawtooth</option>
1877                <option value="square">Square</option>
1878                <option value="triangle">Triangle</option>
1879                <option value="sine">Sine</option>
1880            </select>
1881        `;
1882        section.appendChild(waveformGroup);
1883        
1884        // Set waveform based on selected instrument
1885        const instrumentSettings = this.soundfonts ? this.soundfonts.getCurrentSettings() : null;
1886        const waveformSelect = waveformGroup.querySelector('select');
1887        if (waveformSelect && instrumentSettings && instrumentSettings.waveform) {
1888            waveformSelect.value = instrumentSettings.waveform;
1889            if (this.synth) {
1890                this.synth.setWaveform(instrumentSettings.waveform);
1891            }
1892        } else if (this.synth) {
1893            // Default for brass
1894            this.synth.setWaveform('sawtooth');
1895        }
1896        
1897        // Brightness
1898        const settings = instrumentSettings;
1899        const brightnessGroup = document.createElement('div');
1900        brightnessGroup.className = 'control-group';
1901        brightnessGroup.innerHTML = `
1902            <label>Brightness: <span id="brass-brightness-value">${settings?.brightness?.toFixed(2) || '0.70'}</span></label>
1903            <input type="range" id="brass-brightness" min="0" max="1" step="0.01" value="${settings?.brightness || 0.7}">
1904        `;
1905        section.appendChild(brightnessGroup);
1906        
1907        this.addEnvelopeControls(section, settings || { attack: 0.05, decay: 0.1, sustain: 0.8, release: 0.2 });
1908        this.addVibratoControls(section, settings || { vibratoRate: 5, vibratoDepth: 0.1 });
1909        
1910        sidebar.appendChild(section);
1911        this.setupBrassControls();
1912    }
1913    
1914    // Create woodwind controls
1915    createWoodwindControls(sidebar, instrumentName) {
1916        const section = document.createElement('div');
1917        section.className = 'sidebar-section';
1918        
1919        // Instrument selector
1920        const instrumentGroup = document.createElement('div');
1921        instrumentGroup.className = 'control-group';
1922        instrumentGroup.innerHTML = `
1923            <label>Instrument:</label>
1924            <select id="soundfont-instrument-select">
1925                <option value="flute">Flute</option>
1926                <option value="saxophone">Saxophone</option>
1927                <option value="clarinet">Clarinet</option>
1928                <option value="oboe">Oboe</option>
1929                <option value="bassoon">Bassoon</option>
1930            </select>
1931        `;
1932        section.appendChild(instrumentGroup);
1933        
1934        const instrumentSelect = instrumentGroup.querySelector('select');
1935        if (instrumentSelect && instrumentName) {
1936            instrumentSelect.value = instrumentName;
1937        }
1938        
1939        instrumentSelect.addEventListener('change', (e) => {
1940            const newInstrument = e.target.value;
1941            this.handleCategoryChange('woodwind', newInstrument);
1942            this.saveSettingsDebounced();
1943        });
1944        
1945        // Waveform select (category-specific for woodwind)
1946        const waveformGroup = document.createElement('div');
1947        waveformGroup.className = 'control-group';
1948        waveformGroup.innerHTML = `
1949            <label>Waveform:</label>
1950            <select id="synth-waveform">
1951                <option value="triangle">Triangle</option>
1952                <option value="sine">Sine</option>
1953                <option value="sawtooth">Sawtooth</option>
1954                <option value="square">Square</option>
1955            </select>
1956        `;
1957        section.appendChild(waveformGroup);
1958        
1959        // Set waveform based on selected instrument
1960        const woodwindSettings = this.soundfonts ? this.soundfonts.getCurrentSettings() : null;
1961        const waveformSelect = waveformGroup.querySelector('select');
1962        if (waveformSelect && woodwindSettings && woodwindSettings.waveform) {
1963            waveformSelect.value = woodwindSettings.waveform;
1964            if (this.synth) {
1965                this.synth.setWaveform(woodwindSettings.waveform);
1966            }
1967        } else if (this.synth) {
1968            // Default for woodwind
1969            this.synth.setWaveform('triangle');
1970        }
1971        
1972        const settings = woodwindSettings;
1973        const breathinessGroup = document.createElement('div');
1974        breathinessGroup.className = 'control-group';
1975        breathinessGroup.innerHTML = `
1976            <label>Breathiness: <span id="woodwind-breathiness-value">${settings?.breathiness?.toFixed(2) || '0.30'}</span></label>
1977            <input type="range" id="woodwind-breathiness" min="0" max="1" step="0.01" value="${settings?.breathiness || 0.3}">
1978        `;
1979        section.appendChild(breathinessGroup);
1980        
1981        this.addEnvelopeControls(section, settings || { attack: 0.02, decay: 0.05, sustain: 0.9, release: 0.15 });
1982        this.addVibratoControls(section, settings || { vibratoRate: 6, vibratoDepth: 0.12 });
1983        
1984        sidebar.appendChild(section);
1985        this.setupWoodwindControls();
1986    }
1987    
1988    // Create string controls
1989    createStringControls(sidebar, instrumentName) {
1990        const section = document.createElement('div');
1991        section.className = 'sidebar-section';
1992        
1993        // Instrument selector
1994        const instrumentGroup = document.createElement('div');
1995        instrumentGroup.className = 'control-group';
1996        instrumentGroup.innerHTML = `
1997            <label>Instrument:</label>
1998            <select id="soundfont-instrument-select">
1999                <option value="violin">Violin</option>
2000                <option value="viola">Viola</option>
2001                <option value="cello">Cello</option>
2002                <option value="double-bass">Double Bass</option>
2003            </select>
2004        `;
2005        section.appendChild(instrumentGroup);
2006        
2007        const instrumentSelect = instrumentGroup.querySelector('select');
2008        if (instrumentSelect && instrumentName) {
2009            instrumentSelect.value = instrumentName;
2010        }
2011        
2012        instrumentSelect.addEventListener('change', (e) => {
2013            const newInstrument = e.target.value;
2014            this.handleCategoryChange('string', newInstrument);
2015            this.saveSettingsDebounced();
2016        });
2017        
2018        // Waveform select (category-specific for strings)
2019        const waveformGroup = document.createElement('div');
2020        waveformGroup.className = 'control-group';
2021        waveformGroup.innerHTML = `
2022            <label>Waveform:</label>
2023            <select id="synth-waveform">
2024                <option value="sawtooth">Sawtooth</option>
2025                <option value="triangle">Triangle</option>
2026                <option value="square">Square</option>
2027                <option value="sine">Sine</option>
2028            </select>
2029        `;
2030        section.appendChild(waveformGroup);
2031        
2032        // Set waveform based on selected instrument
2033        const stringSettings = this.soundfonts ? this.soundfonts.getCurrentSettings() : null;
2034        const waveformSelect = waveformGroup.querySelector('select');
2035        if (waveformSelect && stringSettings && stringSettings.waveform) {
2036            waveformSelect.value = stringSettings.waveform;
2037            if (this.synth) {
2038                this.synth.setWaveform(stringSettings.waveform);
2039            }
2040        } else if (this.synth) {
2041            // Default for strings
2042            this.synth.setWaveform('sawtooth');
2043        }
2044        
2045        const settings = stringSettings;
2046        const bowPressureGroup = document.createElement('div');
2047        bowPressureGroup.className = 'control-group';
2048        bowPressureGroup.innerHTML = `
2049            <label>Bow Pressure: <span id="string-bow-pressure-value">${settings?.bowPressure?.toFixed(2) || '0.70'}</span></label>
2050            <input type="range" id="string-bow-pressure" min="0" max="1" step="0.01" value="${settings?.bowPressure || 0.7}">
2051        `;
2052        section.appendChild(bowPressureGroup);
2053        
2054        this.addEnvelopeControls(section, settings || { attack: 0.01, decay: 0.05, sustain: 0.9, release: 0.25 });
2055        this.addVibratoControls(section, settings || { vibratoRate: 6, vibratoDepth: 0.2 });
2056        
2057        sidebar.appendChild(section);
2058        this.setupStringControls();
2059    }
2060    
2061    // Create vocal controls
2062    createVocalControls(sidebar, instrumentName) {
2063        const section = document.createElement('div');
2064        section.className = 'sidebar-section';
2065        
2066        // Instrument selector (gender + vowel)
2067        const instrumentGroup = document.createElement('div');
2068        instrumentGroup.className = 'control-group';
2069        instrumentGroup.innerHTML = `
2070            <label>Voice:</label>
2071            <select id="soundfont-instrument-select">
2072                <option value="male-ah">Male - "Ah"</option>
2073                <option value="male-oh">Male - "Oh"</option>
2074                <option value="male-oo">Male - "Oo"</option>
2075                <option value="male-ee">Male - "Ee"</option>
2076                <option value="female-ah">Female - "Ah"</option>
2077                <option value="female-oh">Female - "Oh"</option>
2078                <option value="female-oo">Female - "Oo"</option>
2079                <option value="female-ee">Female - "Ee"</option>
2080                <option value="mixed-ah">Mixed - "Ah"</option>
2081                <option value="mixed-oh">Mixed - "Oh"</option>
2082                <option value="mixed-oo">Mixed - "Oo"</option>
2083                <option value="mixed-ee">Mixed - "Ee"</option>
2084            </select>
2085        `;
2086        section.appendChild(instrumentGroup);
2087        
2088        const instrumentSelect = instrumentGroup.querySelector('select');
2089        if (instrumentSelect && instrumentName) {
2090            instrumentSelect.value = instrumentName;
2091        }
2092        
2093        instrumentSelect.addEventListener('change', (e) => {
2094            const newInstrument = e.target.value;
2095            this.handleCategoryChange('vocal', newInstrument);
2096            this.saveSettingsDebounced();
2097        });
2098        
2099        // Waveform select (category-specific for vocal)
2100        const waveformGroup = document.createElement('div');
2101        waveformGroup.className = 'control-group';
2102        waveformGroup.innerHTML = `
2103            <label>Waveform:</label>
2104            <select id="synth-waveform">
2105                <option value="sine">Sine</option>
2106                <option value="triangle">Triangle</option>
2107                <option value="sawtooth">Sawtooth</option>
2108                <option value="square">Square</option>
2109            </select>
2110        `;
2111        section.appendChild(waveformGroup);
2112        
2113        // Set waveform based on selected instrument
2114        const vocalSettings = this.soundfonts ? this.soundfonts.getCurrentSettings() : null;
2115        const waveformSelect = waveformGroup.querySelector('select');
2116        if (waveformSelect && vocalSettings && vocalSettings.waveform) {
2117            waveformSelect.value = vocalSettings.waveform;
2118            if (this.synth) {
2119                this.synth.setWaveform(vocalSettings.waveform);
2120            }
2121        } else if (this.synth) {
2122            // Default for vocal
2123            this.synth.setWaveform('sine');
2124        }
2125        
2126        const settings = vocalSettings;
2127        const formantGroup = document.createElement('div');
2128        formantGroup.className = 'control-group';
2129        formantGroup.innerHTML = `
2130            <label>Formant Shift: <span id="vocal-formant-value">${settings?.formantShift?.toFixed(2) || '0.00'}</span></label>
2131            <input type="range" id="vocal-formant" min="-0.5" max="0.5" step="0.01" value="${settings?.formantShift || 0}">
2132        `;
2133        section.appendChild(formantGroup);
2134        
2135        this.addEnvelopeControls(section, settings || { attack: 0.05, decay: 0.1, sustain: 0.85, release: 0.3 });
2136        this.addVibratoControls(section, settings || { vibratoRate: 5, vibratoDepth: 0.1 });
2137        
2138        sidebar.appendChild(section);
2139        this.setupVocalControls();
2140    }
2141    
2142    // Create percussion controls
2143    createPercussionControls(sidebar) {
2144        const section = document.createElement('div');
2145        section.className = 'sidebar-section';
2146        
2147        const infoGroup = document.createElement('div');
2148        infoGroup.className = 'control-group';
2149        infoGroup.innerHTML = `
2150            <label style="text-align: center; color: #999; font-size: 12px; font-weight: normal;">
2151                Each key plays a different drum sound:<br>
2152                C = Kick, D = Snare, F# = Hi-Hat,<br>
2153                C# = Crash, D# = Ride, etc.
2154            </label>
2155        `;
2156        section.appendChild(infoGroup);
2157        
2158        sidebar.appendChild(section);
2159    }
2160    
2161    // Create SF2 controls
2162    createSF2Controls(sidebar, instrumentName) {
2163        const section = document.createElement('div');
2164        section.className = 'sidebar-section';
2165        
2166        // Check if SF2 loader exists (it may be null initially while loading)
2167        if (!this.soundfonts || this.soundfonts.sf2Loader === null || this.soundfonts.sf2Loader === undefined) {
2168            // Check if FluidSynth library is available but loader just hasn't initialized yet
2169            const hasFluidSynth = (typeof Synthesizer !== 'undefined' && Synthesizer !== null) ||
2170                                 (typeof window.JSSynth !== 'undefined' && window.JSSynth && window.JSSynth.Synthesizer);
2171            
2172            if (hasFluidSynth) {
2173                // Library is available, loader just needs time to initialize
2174                const loadingGroup = document.createElement('div');
2175                loadingGroup.className = 'control-group';
2176                loadingGroup.innerHTML = `
2177                    <label style="text-align: center; color: #8f8; font-size: 12px; font-weight: bold;">
2178                        ⏳ Loading FluidSynth...
2179                    </label>
2180                    <label style="text-align: center; color: #999; font-size: 11px; display: block; margin-top: 10px;">
2181                        Initializing SF2 loader...<br>
2182                        This should only take a moment.
2183                    </label>
2184                `;
2185                section.appendChild(loadingGroup);
2186                sidebar.appendChild(section);
2187                
2188                // Retry after SF2 loader is ready
2189                const retryUpdate = () => {
2190                    if (this.soundfonts && this.soundfonts.sf2Loader) {
2191                        // Recreate the sidebar with proper controls
2192                        this.updateSynthSidebar('sf2', instrumentName);
2193                    } else {
2194                        // Check again in 500ms
2195                        setTimeout(retryUpdate, 500);
2196                    }
2197                };
2198                
2199                // Listen for SF2 loader ready event
2200                const onSF2Ready = () => {
2201                    if (this.soundfonts && this.soundfonts.sf2Loader) {
2202                        this.updateSynthSidebar('sf2', instrumentName);
2203                    }
2204                };
2205                window.addEventListener('sf2LoaderReady', onSF2Ready, { once: true });
2206                
2207                // Also try polling as backup
2208                setTimeout(retryUpdate, 500);
2209                return;
2210            } else {
2211                // Library not available at all
2212                const errorGroup = document.createElement('div');
2213                errorGroup.className = 'control-group';
2214                errorGroup.innerHTML = `
2215                    <label style="text-align: center; color: #f44; font-size: 12px; font-weight: bold;">
2216                        ⚠ FluidSynth Not Available
2217                    </label>
2218                    <label style="text-align: center; color: #ff8; font-size: 11px; display: block; margin-top: 10px;">
2219                        The FluidSynth (js-synthesizer) library failed to load.<br>
2220                        SF2 soundfonts are disabled.<br>
2221                        Please use synthesized sounds instead.
2222                    </label>
2223                    <label style="text-align: center; color: #999; font-size: 10px; display: block; margin-top: 10px;">
2224                        Check the browser console for details.<br>
2225                        Download from: https://github.com/jet2jet/js-synthesizer
2226                    </label>
2227                `;
2228                section.appendChild(errorGroup);
2229                sidebar.appendChild(section);
2230                return;
2231            }
2232        }
2233        
2234        // Double-check that FluidSynth is actually available
2235        const hasFluidSynth = (typeof Synthesizer !== 'undefined' && Synthesizer !== null) ||
2236                             (typeof window.JSSynth !== 'undefined' && window.JSSynth && window.JSSynth.Synthesizer);
2237        
2238        if (!hasFluidSynth) {
2239            const errorGroup = document.createElement('div');
2240            errorGroup.className = 'control-group';
2241            errorGroup.innerHTML = `
2242                <label style="text-align: center; color: #f44; font-size: 12px; font-weight: bold;">
2243                    ⚠ FluidSynth Not Available
2244                </label>
2245                <label style="text-align: center; color: #ff8; font-size: 11px; display: block; margin-top: 10px;">
2246                    The FluidSynth (js-synthesizer) library is not available.<br>
2247                    SF2 soundfonts are disabled.<br>
2248                    Please use synthesized sounds instead.
2249                </label>
2250            `;
2251            section.appendChild(errorGroup);
2252            sidebar.appendChild(section);
2253            return;
2254        }
2255        
2256        // Show current instrument info
2257        const infoGroup = document.createElement('div');
2258        infoGroup.className = 'control-group';
2259        infoGroup.innerHTML = `
2260            <label style="text-align: center; color: #8f8; font-size: 14px; font-weight: bold;">
2261                ${instrumentName || 'No instrument selected'}
2262            </label>
2263            <label style="text-align: center; color: #999; font-size: 11px; font-weight: normal; margin-top: 5px; display: block;">
2264                High-quality sampled instrument<br>
2265                from SoundFont file (.sf2)
2266            </label>
2267        `;
2268        section.appendChild(infoGroup);
2269        
2270        // Loading status and manual load button
2271        const statusGroup = document.createElement('div');
2272        statusGroup.className = 'control-group';
2273        statusGroup.id = 'sf2-status';
2274        if (instrumentName) {
2275            if (this.soundfonts.isSF2Loaded(instrumentName)) {
2276                statusGroup.innerHTML = `<label style="color: #8f8; font-size: 11px;">✓ Loaded</label>`;
2277                
2278                // Check for presets and add preset selector
2279                // Note: addPresetSelector will remove any existing selectors before adding a new one
2280                // Use setTimeout to ensure this happens after the section is fully added to DOM
2281                setTimeout(() => {
2282                    this.addPresetSelector(section, instrumentName);
2283                }, 0);
2284            } else {
2285                statusGroup.innerHTML = `
2286                    <label style="color: #999; font-size: 11px; display: block; margin-bottom: 8px;">Not loaded</label>
2287                    <button id="sf2-load-button" style="
2288                        background: #4a4a6a;
2289                        color: #e0e0e0;
2290                        border: 1px solid #6a6a8a;
2291                        padding: 6px 12px;
2292                        border-radius: 4px;
2293                        cursor: pointer;
2294                        font-size: 11px;
2295                        width: 100%;
2296                    ">Load Soundfont</button>
2297                `;
2298                
2299                // Add click handler for load button
2300                setTimeout(() => {
2301                    const loadButton = document.getElementById('sf2-load-button');
2302                    if (loadButton) {
2303                        const loadHandler = async () => {
2304                            loadButton.disabled = true;
2305                            loadButton.textContent = 'Loading...';
2306                            
2307                            try {
2308                                const loaded = await this.soundfonts.loadSF2Soundfont(instrumentName);
2309                                if (loaded) {
2310                                    statusGroup.innerHTML = `<label style="color: #8f8; font-size: 11px;">✓ Loaded</label>`;
2311                                    
2312                                    // Refresh preset selector after loading
2313                                    // Remove existing preset selector if any
2314                                    const existingPresetGroup = document.getElementById('sf2-preset-selector');
2315                                    if (existingPresetGroup) {
2316                                        existingPresetGroup.remove();
2317                                    }
2318                                    // Add new preset selector (with a small delay to ensure soundfont is fully loaded)
2319                                    setTimeout(() => {
2320                                        this.addPresetSelector(section, instrumentName);
2321                                    }, 200);
2322                                } else {
2323                                    statusGroup.innerHTML = `
2324                                        <label style="color: #f44; font-size: 11px; display: block; margin-bottom: 8px;">✗ Failed to load</label>
2325                                        <button id="sf2-load-button" style="
2326                                            background: #4a4a6a;
2327                                            color: #e0e0e0;
2328                                            border: 1px solid #6a6a8a;
2329                                            padding: 6px 12px;
2330                                            border-radius: 4px;
2331                                            cursor: pointer;
2332                                            font-size: 11px;
2333                                            width: 100%;
2334                                        ">Retry Load</button>
2335                                    `;
2336                                    // Re-attach event listener
2337                                    setTimeout(() => {
2338                                        const retryButton = document.getElementById('sf2-load-button');
2339                                        if (retryButton) {
2340                                            retryButton.addEventListener('click', loadHandler);
2341                                        }
2342                                    }, 10);
2343                                }
2344                            } catch (error) {
2345                                console.error('Error loading SF2 soundfont:', error);
2346                                statusGroup.innerHTML = `
2347                                    <label style="color: #f44; font-size: 11px; display: block; margin-bottom: 8px;">✗ Error loading</label>
2348                                    <button id="sf2-load-button" style="
2349                                        background: #4a4a6a;
2350                                        color: #e0e0e0;
2351                                        border: 1px solid #6a6a8a;
2352                                        padding: 6px 12px;
2353                                        border-radius: 4px;
2354                                        cursor: pointer;
2355                                        font-size: 11px;
2356                                        width: 100%;
2357                                    ">Retry Load</button>
2358                                `;
2359                                // Re-attach event listener
2360                                setTimeout(() => {
2361                                    const retryButton = document.getElementById('sf2-load-button');
2362                                    if (retryButton) {
2363                                        retryButton.addEventListener('click', loadHandler);
2364                                    }
2365                                }, 10);
2366                            }
2367                        };
2368                        loadButton.addEventListener('click', loadHandler);
2369                    }
2370                }, 100);
2371            }
2372        }
2373        section.appendChild(statusGroup);
2374        
2375        // Add preset selector if soundfont is loaded and has presets
2376        if (instrumentName && this.soundfonts.isSF2Loaded(instrumentName)) {
2377            this.addPresetSelector(section, instrumentName);
2378        }
2379        
2380        sidebar.appendChild(section);
2381    }
2382    
2383    // Add preset selector for SF2 soundfonts
2384    addPresetSelector(section, soundfontName) {
2385        if (!this.soundfonts || !this.soundfonts.sf2Loader) {
2386            return;
2387        }
2388        
2389        // Check if this is FluidSynth loader
2390        const isFluidSynth = this.soundfonts.sf2Loader._synthesizer !== undefined;
2391        if (!isFluidSynth) {
2392            return; // Old loader doesn't support presets
2393        }
2394        
2395        // CRITICAL: Remove ALL existing preset selectors before creating a new one
2396        // Check both by ID and by class to catch any duplicates
2397        const existingPresetSelectors = document.querySelectorAll('#sf2-preset-selector, [id^="sf2-preset-selector"]');
2398        existingPresetSelectors.forEach(el => {
2399            if (el && el.parentNode) {
2400                el.parentNode.removeChild(el);
2401            }
2402        });
2403        
2404        // Also remove any bank selectors that might be orphaned
2405        const existingBankSelectors = document.querySelectorAll('[id^="sf2-bank-select"]');
2406        existingBankSelectors.forEach(el => {
2407            // Only remove if it's not inside a preset selector (which we just removed)
2408            if (el && el.parentNode && !el.closest('#sf2-preset-selector')) {
2409                // Check if it's orphaned (not inside any preset selector)
2410                const presetSelector = el.closest('[id*="preset"]');
2411                if (!presetSelector) {
2412                    el.parentNode.removeChild(el);
2413                }
2414            }
2415        });
2416        
2417        // Get presets for this soundfont
2418        const presets = this.soundfonts.sf2Loader.getPresetsForSoundfont(soundfontName);
2419        
2420        if (presets.length === 0) {
2421            return; // No presets available
2422        }
2423        
2424        // Group presets by bank
2425        const presetsByBank = {};
2426        presets.forEach(preset => {
2427            if (!presetsByBank[preset.bank]) {
2428                presetsByBank[preset.bank] = [];
2429            }
2430            presetsByBank[preset.bank].push(preset);
2431        });
2432        
2433        // Get unique banks and sort them
2434        const banks = Object.keys(presetsByBank).map(Number).sort((a, b) => a - b);
2435        
2436        // Create preset selector group
2437        const presetGroup = document.createElement('div');
2438        presetGroup.className = 'control-group';
2439        presetGroup.id = 'sf2-preset-selector';
2440        
2441        // Only show bank selector if there are multiple banks
2442        let bankSelect = null;
2443        if (banks.length > 1) {
2444            const bankLabel = document.createElement('label');
2445            bankLabel.textContent = 'Bank:';
2446            bankLabel.style.marginBottom = '8px';
2447            bankLabel.style.display = 'block';
2448            bankLabel.style.color = '#d0d0d0';
2449            bankLabel.style.fontSize = '13px';
2450            presetGroup.appendChild(bankLabel);
2451            
2452            bankSelect = document.createElement('select');
2453            bankSelect.id = `sf2-bank-select-${soundfontName}`;
2454            bankSelect.style.width = '100%';
2455            bankSelect.style.padding = '8px 12px';
2456            bankSelect.style.background = '#252530';
2457            bankSelect.style.border = '1px solid rgba(255, 255, 255, 0.1)';
2458            bankSelect.style.borderRadius = '0';
2459            bankSelect.style.color = '#e8e8e8';
2460            bankSelect.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
2461            bankSelect.style.fontSize = '13px';
2462            bankSelect.style.marginBottom = '15px';
2463            bankSelect.style.transition = 'border-color 0.15s ease';
2464            bankSelect.style.cursor = 'pointer';
2465            
2466            // Add hover effect for bank select
2467            bankSelect.addEventListener('mouseenter', () => {
2468                bankSelect.style.borderColor = 'rgba(255, 255, 255, 0.15)';
2469            });
2470            bankSelect.addEventListener('mouseleave', () => {
2471                bankSelect.style.borderColor = 'rgba(255, 255, 255, 0.1)';
2472            });
2473            bankSelect.addEventListener('focus', () => {
2474                bankSelect.style.borderColor = 'rgba(255, 255, 255, 0.2)';
2475                bankSelect.style.outline = 'none';
2476            });
2477            bankSelect.addEventListener('blur', () => {
2478                bankSelect.style.borderColor = 'rgba(255, 255, 255, 0.1)';
2479            });
2480            
2481            // Add banks to dropdown
2482            banks.forEach(bank => {
2483                const option = document.createElement('option');
2484                option.value = bank;
2485                option.textContent = `Bank ${bank}`;
2486                option.style.background = '#1a1a24';
2487                option.style.color = '#e8e8e8';
2488                if (bank === banks[0]) {
2489                    option.selected = true; // Select first bank by default
2490                }
2491                bankSelect.appendChild(option);
2492            });
2493            
2494            presetGroup.appendChild(bankSelect);
2495        }
2496        
2497        const presetLabel = document.createElement('label');
2498        presetLabel.textContent = 'Preset:';
2499        presetLabel.style.marginBottom = '8px';
2500        presetLabel.style.display = 'block';
2501        presetLabel.style.color = '#d0d0d0';
2502        presetLabel.style.fontSize = '13px';
2503        presetGroup.appendChild(presetLabel);
2504        
2505        const presetSelect = document.createElement('select');
2506        presetSelect.id = `sf2-preset-select-${soundfontName}`;
2507        presetSelect.style.width = '100%';
2508        presetSelect.style.padding = '8px 12px';
2509        presetSelect.style.background = '#252530';
2510        presetSelect.style.border = '1px solid rgba(255, 255, 255, 0.1)';
2511        presetSelect.style.borderRadius = '0';
2512        presetSelect.style.color = '#e8e8e8';
2513        presetSelect.style.fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
2514        presetSelect.style.fontSize = '13px';
2515        presetSelect.style.transition = 'border-color 0.15s ease';
2516        presetSelect.style.cursor = 'pointer';
2517        
2518        // Function to apply preset change (centralized to ensure consistency)
2519        const applyPresetChange = (bank, program, force = false) => {
2520            if (this.soundfonts && this.soundfonts.sf2Loader) {
2521                // Ensure we have valid numbers
2522                const bankNum = Number(bank);
2523                const programNum = Number(program);
2524                
2525                if (!isNaN(bankNum) && !isNaN(programNum)) {
2526                    // Always apply preset change, ensuring soundfont is properly selected first
2527                    try {
2528                        this.soundfonts.sf2Loader.changePreset(soundfontName, bankNum, programNum);
2529                        const preset = presets.find(p => p.bank === bankNum && p.program === programNum);
2530                        console.log(`Changed preset to: Bank ${bankNum}, Program ${programNum} (${preset?.name || 'Unknown'})`);
2531                    } catch (error) {
2532                        console.error('Error applying preset change:', error);
2533                    }
2534                }
2535            }
2536        };
2537        
2538        // Function to update preset dropdown based on selected bank
2539        const updatePresetDropdown = (selectedBank) => {
2540            presetSelect.innerHTML = ''; // Clear existing options
2541            
2542            const bankPresets = presetsByBank[selectedBank] || [];
2543            bankPresets.forEach((preset, index) => {
2544                const option = document.createElement('option');
2545                option.value = `${preset.bank}-${preset.program}`;
2546                option.textContent = `${preset.program}: ${preset.name}`;
2547                option.style.background = '#1a1a24';
2548                option.style.color = '#e8e8e8';
2549                if (index === 0) {
2550                    option.selected = true; // Select first preset by default
2551                }
2552                presetSelect.appendChild(option);
2553            });
2554            
2555            // Trigger preset change when bank changes
2556            if (bankPresets.length > 0) {
2557                const firstPreset = bankPresets[0];
2558                // Apply preset immediately when bank changes (no delay needed)
2559                applyPresetChange(firstPreset.bank, firstPreset.program, true);
2560            }
2561        };
2562        
2563        // Initialize with first bank
2564        const initialBank = banks[0];
2565        updatePresetDropdown(initialBank);
2566        
2567        // Handle bank change (if multiple banks)
2568        if (bankSelect) {
2569            bankSelect.addEventListener('change', (e) => {
2570                const selectedBank = Number(e.target.value);
2571                updatePresetDropdown(selectedBank);
2572            });
2573        }
2574        
2575        // Handle preset change - ensure it always works
2576        presetSelect.addEventListener('change', (e) => {
2577            const [bank, program] = e.target.value.split('-').map(Number);
2578            // Always apply preset change immediately, even if it's the first manual change
2579            applyPresetChange(bank, program, true);
2580        });
2581        
2582        // Add hover effect for preset select
2583        presetSelect.addEventListener('mouseenter', () => {
2584            presetSelect.style.borderColor = 'rgba(255, 255, 255, 0.15)';
2585        });
2586        presetSelect.addEventListener('mouseleave', () => {
2587            presetSelect.style.borderColor = 'rgba(255, 255, 255, 0.1)';
2588        });
2589        presetSelect.addEventListener('focus', () => {
2590            presetSelect.style.borderColor = 'rgba(255, 255, 255, 0.2)';
2591            presetSelect.style.outline = 'none';
2592        });
2593        presetSelect.addEventListener('blur', () => {
2594            presetSelect.style.borderColor = 'rgba(255, 255, 255, 0.1)';
2595        });
2596        
2597        presetGroup.appendChild(presetSelect);
2598        
2599        // Insert after status group
2600        const statusGroup = document.getElementById('sf2-status');
2601        if (statusGroup && statusGroup.parentNode) {
2602            statusGroup.parentNode.insertBefore(presetGroup, statusGroup.nextSibling);
2603        } else {
2604            section.appendChild(presetGroup);
2605        }
2606    }
2607    
2608    // Add envelope controls (shared)
2609    addEnvelopeControls(section, settings) {
2610        const attackGroup = document.createElement('div');
2611        attackGroup.className = 'control-group';
2612        attackGroup.innerHTML = `
2613            <label>Attack: <span id="attack-value">${(settings.attack || 0.01).toFixed(2)}</span>s</label>
2614            <input type="range" id="synth-attack" min="0" max="5" step="0.01" value="${settings.attack || 0.01}">
2615        `;
2616        section.appendChild(attackGroup);
2617        
2618        const decayGroup = document.createElement('div');
2619        decayGroup.className = 'control-group';
2620        decayGroup.innerHTML = `
2621            <label>Decay: <span id="decay-value">${(settings.decay || 0.1).toFixed(2)}</span>s</label>
2622            <input type="range" id="synth-decay" min="0" max="1" step="0.01" value="${settings.decay || 0.1}">
2623        `;
2624        section.appendChild(decayGroup);
2625        
2626        const sustainGroup = document.createElement('div');
2627        sustainGroup.className = 'control-group';
2628        sustainGroup.innerHTML = `
2629            <label>Sustain: <span id="sustain-value">${(settings.sustain || 0.7).toFixed(2)}</span></label>
2630            <input type="range" id="synth-sustain" min="0" max="1" step="0.01" value="${settings.sustain || 0.7}">
2631        `;
2632        section.appendChild(sustainGroup);
2633        
2634        const releaseGroup = document.createElement('div');
2635        releaseGroup.className = 'control-group';
2636        releaseGroup.innerHTML = `
2637            <label>Release: <span id="release-value">${(settings.release || 0.3).toFixed(2)}</span>s</label>
2638            <input type="range" id="synth-release" min="0" max="5" step="0.01" value="${settings.release || 0.3}">
2639        `;
2640        section.appendChild(releaseGroup);
2641    }
2642    
2643    // Add vibrato controls (shared)
2644    addVibratoControls(section, settings) {
2645        const vibratoRateGroup = document.createElement('div');
2646        vibratoRateGroup.className = 'control-group';
2647        vibratoRateGroup.innerHTML = `
2648            <label>Vibrato Rate: <span id="vibrato-rate-value">${(settings.vibratoRate || 5).toFixed(1)}</span> Hz</label>
2649            <input type="range" id="vibrato-rate" min="1" max="10" step="0.1" value="${settings.vibratoRate || 5}">
2650        `;
2651        section.appendChild(vibratoRateGroup);
2652        
2653        const vibratoDepthGroup = document.createElement('div');
2654        vibratoDepthGroup.className = 'control-group';
2655        vibratoDepthGroup.innerHTML = `
2656            <label>Vibrato Depth: <span id="vibrato-depth-value">${(settings.vibratoDepth || 0.1).toFixed(2)}</span></label>
2657            <input type="range" id="vibrato-depth" min="0" max="0.5" step="0.01" value="${settings.vibratoDepth || 0.1}">
2658        `;
2659        section.appendChild(vibratoDepthGroup);
2660    }
2661    
2662    // Setup brass controls
2663    setupBrassControls() {
2664        const brightnessSlider = document.getElementById('brass-brightness');
2665        if (brightnessSlider) {
2666            brightnessSlider.addEventListener('input', (e) => {
2667                const value = parseFloat(e.target.value);
2668                document.getElementById('brass-brightness-value').textContent = value.toFixed(2);
2669                if (this.soundfonts) {
2670                    this.soundfonts.updateSetting('brightness', value);
2671                }
2672                this.saveSettingsDebounced();
2673            });
2674        }
2675        this.setupEnvelopeControls();
2676        this.setupVibratoControls();
2677        this.setupWaveformControl();
2678    }
2679    
2680    // Setup woodwind controls
2681    setupWoodwindControls() {
2682        const breathinessSlider = document.getElementById('woodwind-breathiness');
2683        if (breathinessSlider) {
2684            breathinessSlider.addEventListener('input', (e) => {
2685                const value = parseFloat(e.target.value);
2686                document.getElementById('woodwind-breathiness-value').textContent = value.toFixed(2);
2687                if (this.soundfonts) {
2688                    this.soundfonts.updateSetting('breathiness', value);
2689                }
2690                this.saveSettingsDebounced();
2691            });
2692        }
2693        this.setupEnvelopeControls();
2694        this.setupVibratoControls();
2695        this.setupWaveformControl();
2696    }
2697    
2698    // Setup string controls
2699    setupStringControls() {
2700        const bowPressureSlider = document.getElementById('string-bow-pressure');
2701        if (bowPressureSlider) {
2702            bowPressureSlider.addEventListener('input', (e) => {
2703                const value = parseFloat(e.target.value);
2704                document.getElementById('string-bow-pressure-value').textContent = value.toFixed(2);
2705                if (this.soundfonts) {
2706                    this.soundfonts.updateSetting('bowPressure', value);
2707                }
2708                this.saveSettingsDebounced();
2709            });
2710        }
2711        this.setupEnvelopeControls();
2712        this.setupVibratoControls();
2713        this.setupWaveformControl();
2714    }
2715    
2716    // Setup vocal controls
2717    setupVocalControls() {
2718        const formantSlider = document.getElementById('vocal-formant');
2719        if (formantSlider) {
2720            formantSlider.addEventListener('input', (e) => {
2721                const value = parseFloat(e.target.value);
2722                document.getElementById('vocal-formant-value').textContent = value.toFixed(2);
2723                if (this.soundfonts) {
2724                    this.soundfonts.updateSetting('formantShift', value);
2725                }
2726                this.saveSettingsDebounced();
2727            });
2728        }
2729        this.setupEnvelopeControls();
2730        this.setupVibratoControls();
2731        this.setupWaveformControl();
2732    }
2733    
2734    // Setup waveform control (shared)
2735    setupWaveformControl() {
2736        const waveformSelect = document.getElementById('synth-waveform');
2737        if (waveformSelect) {
2738            waveformSelect.addEventListener('change', (e) => {
2739                const waveform = e.target.value;
2740                if (this.synth) {
2741                    this.synth.setWaveform(waveform);
2742                }
2743                this.saveSettingsDebounced();
2744            });
2745        }
2746    }
2747    
2748    // Setup envelope controls (shared)
2749    setupEnvelopeControls() {
2750        const attackSlider = document.getElementById('synth-attack');
2751        const decaySlider = document.getElementById('synth-decay');
2752        const sustainSlider = document.getElementById('synth-sustain');
2753        const releaseSlider = document.getElementById('synth-release');
2754        
2755        if (attackSlider) {
2756            attackSlider.addEventListener('input', (e) => {
2757                const value = parseFloat(e.target.value);
2758                document.getElementById('attack-value').textContent = value.toFixed(2);
2759                if (this.soundfonts) {
2760                    this.soundfonts.updateSetting('attack', value);
2761                }
2762                if (this.synth) {
2763                    this.synth.setEnvelope(value, 
2764                        parseFloat(decaySlider?.value || 0.1),
2765                        parseFloat(sustainSlider?.value || 0.7),
2766                        parseFloat(releaseSlider?.value || 0.3)
2767                    );
2768                }
2769                this.saveSettingsDebounced();
2770            });
2771        }
2772        
2773        if (decaySlider) {
2774            decaySlider.addEventListener('input', (e) => {
2775                const value = parseFloat(e.target.value);
2776                document.getElementById('decay-value').textContent = value.toFixed(2);
2777                if (this.soundfonts) {
2778                    this.soundfonts.updateSetting('decay', value);
2779                }
2780                if (this.synth) {
2781                    this.synth.setEnvelope(
2782                        parseFloat(attackSlider?.value || 0.01),
2783                        value,
2784                        parseFloat(sustainSlider?.value || 0.7),
2785                        parseFloat(releaseSlider?.value || 0.3)
2786                    );
2787                }
2788                this.saveSettingsDebounced();
2789            });
2790        }
2791        
2792        if (sustainSlider) {
2793            sustainSlider.addEventListener('input', (e) => {
2794                const value = parseFloat(e.target.value);
2795                document.getElementById('sustain-value').textContent = value.toFixed(2);
2796                if (this.soundfonts) {
2797                    this.soundfonts.updateSetting('sustain', value);
2798                }
2799                if (this.synth) {
2800                    this.synth.setEnvelope(
2801                        parseFloat(attackSlider?.value || 0.01),
2802                        parseFloat(decaySlider?.value || 0.1),
2803                        value,
2804                        parseFloat(releaseSlider?.value || 0.3)
2805                    );
2806                }
2807                this.saveSettingsDebounced();
2808            });
2809        }
2810        
2811        if (releaseSlider) {
2812            releaseSlider.addEventListener('input', (e) => {
2813                const value = parseFloat(e.target.value);
2814                document.getElementById('release-value').textContent = value.toFixed(2);
2815                if (this.soundfonts) {
2816                    this.soundfonts.updateSetting('release', value);
2817                }
2818                if (this.synth) {
2819                    this.synth.setEnvelope(
2820                        parseFloat(attackSlider?.value || 0.01),
2821                        parseFloat(decaySlider?.value || 0.1),
2822                        parseFloat(sustainSlider?.value || 0.7),
2823                        value
2824                    );
2825                }
2826                this.saveSettingsDebounced();
2827            });
2828        }
2829    }
2830    
2831    // Setup vibrato controls (shared)
2832    setupVibratoControls() {
2833        const vibratoRateSlider = document.getElementById('vibrato-rate');
2834        const vibratoDepthSlider = document.getElementById('vibrato-depth');
2835        
2836        if (vibratoRateSlider) {
2837            vibratoRateSlider.addEventListener('input', (e) => {
2838                const value = parseFloat(e.target.value);
2839                document.getElementById('vibrato-rate-value').textContent = value.toFixed(1);
2840                if (this.soundfonts) {
2841                    this.soundfonts.updateSetting('vibratoRate', value);
2842                }
2843                this.saveSettingsDebounced();
2844            });
2845        }
2846        
2847        if (vibratoDepthSlider) {
2848            vibratoDepthSlider.addEventListener('input', (e) => {
2849                const value = parseFloat(e.target.value);
2850                document.getElementById('vibrato-depth-value').textContent = value.toFixed(2);
2851                if (this.soundfonts) {
2852                    this.soundfonts.updateSetting('vibratoDepth', value);
2853                }
2854                this.saveSettingsDebounced();
2855            });
2856        }
2857    }
2858    
2859    setupScalesDropdown() {
2860        const dropdownButton = document.getElementById('scales-dropdown-button');
2861        const dropdown = document.getElementById('scales-dropdown');
2862        const scaleSelect = document.getElementById('scale-select');
2863        
2864        if (dropdownButton && dropdown) {
2865            // Use a flag to prevent multiple listeners
2866            if (dropdownButton.dataset.listenerAdded) {
2867                return; // Already set up
2868            }
2869            dropdownButton.dataset.listenerAdded = 'true';
2870            
2871            dropdownButton.addEventListener('click', (e) => {
2872                e.stopPropagation();
2873                e.preventDefault();
2874                const isShowing = dropdown.classList.contains('show');
2875                dropdown.classList.toggle('show');
2876                // Close other dropdowns if open
2877                const settingsDropdown = document.getElementById('settings-dropdown');
2878                const effectsDropdown = document.getElementById('effects-dropdown');
2879                if (settingsDropdown) settingsDropdown.classList.remove('show');
2880                if (effectsDropdown) effectsDropdown.classList.remove('show');
2881            });
2882            
2883            // Scale select change handler
2884            if (scaleSelect) {
2885                scaleSelect.addEventListener('change', (e) => {
2886                    const scaleName = e.target.value;
2887                    if (this.midi) {
2888                        this.midi.setScale(scaleName);
2889                    }
2890                });
2891            }
2892            
2893            // Close dropdown when clicking outside
2894            if (!document.scalesDropdownListenerAdded) {
2895                document.addEventListener('click', (e) => {
2896                    if (dropdown && dropdownButton) {
2897                        if (!dropdown.contains(e.target) && !dropdownButton.contains(e.target)) {
2898                            dropdown.classList.remove('show');
2899                        }
2900                    }
2901                });
2902                document.scalesDropdownListenerAdded = true;
2903            }
2904        }
2905    }
2906    
2907    applyArpeggiatorSettings(settings = {}) {
2908        if (!this.midi?.arpeggiator) return;
2909
2910        const enabledEl = document.getElementById('arp-enabled');
2911        const controlsEl = document.getElementById('arp-controls');
2912        const bpmEl = document.getElementById('arp-bpm');
2913        const bpmValEl = document.getElementById('arp-bpm-value');
2914        const patternEl = document.getElementById('arp-pattern');
2915        const chordEl = document.getElementById('arp-chord');
2916        const octEl = document.getElementById('arp-octaves');
2917        const octValEl = document.getElementById('arp-octaves-value');
2918
2919        const enabled = settings.arpEnabled !== undefined ? !!settings.arpEnabled : (enabledEl?.checked || false);
2920        let bpm = settings.arpBpm !== undefined ? Number(settings.arpBpm) : undefined;
2921        if (bpm === undefined && settings.arpRate !== undefined) {
2922            bpm = Number(settings.arpRate) * 15;
2923        }
2924        if (bpm === undefined) bpm = Number(bpmEl?.value || 120);
2925        const chord = settings.arpChord || chordEl?.value || 'major';
2926        const pattern = settings.arpPattern || patternEl?.value || 'up';
2927        const octaves = settings.arpOctaves !== undefined ? Number(settings.arpOctaves) : Number(octEl?.value || 1);
2928
2929        if (enabledEl) enabledEl.checked = enabled;
2930        if (controlsEl) controlsEl.style.display = enabled ? 'block' : 'none';
2931        if (bpmEl) bpmEl.value = bpm;
2932        if (bpmValEl) bpmValEl.textContent = String(bpm);
2933        if (chordEl) chordEl.value = chord;
2934        if (patternEl) patternEl.value = pattern;
2935        if (octEl) octEl.value = octaves;
2936        if (octValEl) octValEl.textContent = String(octaves);
2937
2938        this.midi.arpeggiator.configure({ enabled, bpm, chord, pattern, octaves });
2939    }
2940
2941    setupArpeggiatorControls() {
2942        if (document.getElementById('arp-enabled')?.dataset.arpBound === '1') return;
2943        const enabledEl = document.getElementById('arp-enabled');
2944        if (!enabledEl) return;
2945        enabledEl.dataset.arpBound = '1';
2946
2947        const controlsEl = document.getElementById('arp-controls');
2948        const bpmEl = document.getElementById('arp-bpm');
2949        const bpmValEl = document.getElementById('arp-bpm-value');
2950        const patternEl = document.getElementById('arp-pattern');
2951        const chordEl = document.getElementById('arp-chord');
2952        const octEl = document.getElementById('arp-octaves');
2953        const octValEl = document.getElementById('arp-octaves-value');
2954
2955        const sync = () => {
2956            if (!this.midi?.arpeggiator) return;
2957            this.midi.arpeggiator.configure({
2958                enabled: enabledEl.checked,
2959                bpm: Number(bpmEl?.value || 120),
2960                chord: chordEl?.value || 'major',
2961                pattern: patternEl?.value || 'up',
2962                octaves: Number(octEl?.value || 1)
2963            });
2964            if (controlsEl) controlsEl.style.display = enabledEl.checked ? 'block' : 'none';
2965            this.saveSettingsDebounced();
2966        };
2967
2968        enabledEl.addEventListener('change', sync);
2969        if (bpmEl) {
2970            bpmEl.addEventListener('input', (e) => {
2971                if (bpmValEl) bpmValEl.textContent = e.target.value;
2972                sync();
2973            });
2974        }
2975        if (chordEl) chordEl.addEventListener('change', sync);
2976        if (patternEl) patternEl.addEventListener('change', sync);
2977        if (octEl) {
2978            octEl.addEventListener('input', (e) => {
2979                if (octValEl) octValEl.textContent = e.target.value;
2980                sync();
2981            });
2982        }
2983    }
2984
2985    setupSettingsDropdown() {
2986        const dropdownButton = document.getElementById('settings-dropdown-button');
2987        const dropdown = document.getElementById('settings-dropdown');
2988        const a4FreqSlider = document.getElementById('a4-freq');
2989        const tuningSystemSelect = document.getElementById('tuning-system');
2990        
2991        if (dropdownButton && dropdown) {
2992            // Use a flag to prevent multiple listeners
2993            if (dropdownButton.dataset.listenerAdded) {
2994                return; // Already set up
2995            }
2996            dropdownButton.dataset.listenerAdded = 'true';
2997            
2998            dropdownButton.addEventListener('click', (e) => {
2999                e.stopPropagation();
3000                e.preventDefault();
3001                const isShowing = dropdown.classList.contains('show');
3002                dropdown.classList.toggle('show');
3003                // Close other dropdowns if open
3004                const scalesDropdown = document.getElementById('scales-dropdown');
3005                const effectsDropdown = document.getElementById('effects-dropdown');
3006                if (scalesDropdown) scalesDropdown.classList.remove('show');
3007                if (effectsDropdown) effectsDropdown.classList.remove('show');
3008            });
3009            
3010            // A4 frequency slider
3011            const a4FreqValue = document.getElementById('a4-freq-value');
3012            if (a4FreqSlider && a4FreqValue) {
3013                this.makeValueEditable(a4FreqValue, a4FreqSlider, (val) => {
3014                    if (this.synth) {
3015                        this.synth.setA4Frequency(val);
3016                    }
3017                });
3018                a4FreqSlider.addEventListener('input', (e) => {
3019                    const freq = parseInt(e.target.value);
3020                    a4FreqValue.textContent = freq;
3021                    if (this.synth) {
3022                        this.synth.setA4Frequency(freq);
3023                    }
3024                    this.saveSettings();
3025                });
3026            }
3027            
3028            // Tuning system select
3029            if (tuningSystemSelect) {
3030                tuningSystemSelect.addEventListener('change', (e) => {
3031                    const system = e.target.value;
3032                    if (this.synth) {
3033                        this.synth.setTuningSystem(system);
3034                    }
3035                    this.saveSettings();
3036                });
3037            }
3038            
3039            // Polyphony slider
3040            const polyphonySlider = document.getElementById('polyphony');
3041            if (polyphonySlider) {
3042                polyphonySlider.addEventListener('input', (e) => {
3043                    const val = parseInt(e.target.value);
3044                    document.getElementById('polyphony-value').textContent = val;
3045                    if (this.synth) {
3046                        this.synth.setMaxPolyphony(val);
3047                    }
3048                    this.saveSettings();
3049                });
3050            }
3051            
3052            // Velocity curve slider
3053            const velocityCurveSlider = document.getElementById('velocity-curve');
3054            if (velocityCurveSlider) {
3055                velocityCurveSlider.addEventListener('input', (e) => {
3056                    const val = parseFloat(e.target.value);
3057                    document.getElementById('velocity-curve-value').textContent = val.toFixed(1);
3058                    if (this.synth) {
3059                        this.synth.setVelocityCurve(val);
3060                    }
3061                    this.saveSettings();
3062                });
3063            }
3064            
3065            // Velocity multiplier slider
3066            const velocityMultiplierSlider = document.getElementById('velocity-multiplier');
3067            if (velocityMultiplierSlider) {
3068                velocityMultiplierSlider.addEventListener('input', (e) => {
3069                    const val = parseFloat(e.target.value);
3070                    document.getElementById('velocity-multiplier-value').textContent = val.toFixed(1);
3071                    if (this.synth) {
3072                        this.synth.setVelocityMultiplier(val);
3073                    }
3074                    this.saveSettings();
3075                });
3076            }
3077            
3078            // Note priority select
3079            const notePrioritySelect = document.getElementById('note-priority');
3080            if (notePrioritySelect) {
3081                notePrioritySelect.addEventListener('change', (e) => {
3082                    const priority = e.target.value;
3083                    if (this.synth) {
3084                        this.synth.setNotePriority(priority);
3085                    }
3086                    this.saveSettings();
3087                });
3088            }
3089
3090            this.setupArpeggiatorControls();
3091            
3092            // Show key labels checkbox
3093            const showKeyLabelsCheckbox = document.getElementById('show-key-labels');
3094            if (showKeyLabelsCheckbox) {
3095                showKeyLabelsCheckbox.addEventListener('change', (e) => {
3096                    const show = e.target.checked;
3097                    if (this.midi) {
3098                        this.midi.setShowKeyLabels(show);
3099                    }
3100                    this.saveSettings();
3101                });
3102            }
3103            
3104            // Background visualizer mode (off / synesthesia / spectrogram)
3105            const bgVisualizerRadios = document.querySelectorAll('input[name="background-visualizer-mode"]');
3106            bgVisualizerRadios.forEach((radio) => {
3107                radio.addEventListener('change', (e) => {
3108                    if (!e.target.checked) return;
3109                    this.applyBackgroundVisualizerMode(e.target.value);
3110                    this.saveSettings();
3111                });
3112            });
3113
3114            const synesthesiaIntensitySlider = document.getElementById('synesthesia-intensity');
3115            const synesthesiaIntensityValue = document.getElementById('synesthesia-intensity-value');
3116            const synesthesiaFadeSlider = document.getElementById('synesthesia-fade');
3117            const synesthesiaFadeValue = document.getElementById('synesthesia-fade-value');
3118            
3119            // Synesthesia intensity slider
3120            if (synesthesiaIntensitySlider && synesthesiaIntensityValue) {
3121                synesthesiaIntensitySlider.addEventListener('input', (e) => {
3122                    const intensity = parseInt(e.target.value) / 100; // Convert to 0-1
3123                    synesthesiaIntensityValue.textContent = e.target.value;
3124                    if (this.visualizer) {
3125                        this.visualizer.setIntensity(intensity);
3126                    }
3127                    this.saveSettings();
3128                });
3129            }
3130            
3131            // Synesthesia fade speed slider
3132            if (synesthesiaFadeSlider && synesthesiaFadeValue) {
3133                synesthesiaFadeSlider.addEventListener('input', (e) => {
3134                    const fadeSpeed = parseInt(e.target.value) / 100; // Convert to 0-1
3135                    synesthesiaFadeValue.textContent = e.target.value;
3136                    if (this.visualizer) {
3137                        this.visualizer.setFadeSpeed(fadeSpeed);
3138                    }
3139                    this.saveSettings();
3140                });
3141            }
3142            
3143            // RGB border mode checkbox
3144            const rgbBorderModeCheckbox = document.getElementById('rgb-border-mode');
3145            const staticColorControls = document.getElementById('static-color-controls');
3146            const rgbSpeedSlider = document.getElementById('rgb-speed-slider');
3147            const rgbSpeedValue = document.getElementById('rgb-speed-value');
3148            const rgbSpeedControls = document.getElementById('rgb-speed-controls');
3149            
3150            if (rgbBorderModeCheckbox) {
3151                rgbBorderModeCheckbox.addEventListener('change', (e) => {
3152                    const enabled = e.target.checked;
3153                    this.toggleRgbBorderMode(enabled);
3154                    // Show/hide speed controls
3155                    if (rgbSpeedControls) {
3156                        rgbSpeedControls.style.display = enabled ? 'block' : 'none';
3157                    }
3158                    this.saveSettings();
3159                });
3160            }
3161            
3162            // RGB speed slider
3163            if (rgbSpeedSlider && rgbSpeedValue) {
3164                rgbSpeedSlider.addEventListener('input', (e) => {
3165                    const speed = parseFloat(e.target.value);
3166                    rgbSpeedValue.textContent = speed;
3167                    this.setRgbSpeed(speed);
3168                    this.saveSettings();
3169                });
3170            }
3171            
3172            // Border color picker
3173            const borderColorInput = document.getElementById('border-color');
3174            const borderColorTextInput = document.getElementById('border-color-text');
3175            const resetBorderColorButton = document.getElementById('reset-border-color');
3176            
3177            if (borderColorInput && borderColorTextInput) {
3178                // Update text input when color picker changes
3179                borderColorInput.addEventListener('input', (e) => {
3180                    if (!rgbBorderModeCheckbox?.checked) {
3181                        const color = e.target.value;
3182                        borderColorTextInput.value = color;
3183                        this.applyBorderColor(color);
3184                        this.saveSettings();
3185                    }
3186                });
3187                
3188                // Update color picker when text input changes
3189                borderColorTextInput.addEventListener('input', (e) => {
3190                    if (!rgbBorderModeCheckbox?.checked) {
3191                        const color = e.target.value;
3192                        // Validate hex color
3193                        if (/^#[0-9A-F]{6}$/i.test(color)) {
3194                            borderColorInput.value = color;
3195                            this.applyBorderColor(color);
3196                            this.saveSettings();
3197                        }
3198                    }
3199                });
3200                
3201                // Reset to default
3202                if (resetBorderColorButton) {
3203                    resetBorderColorButton.addEventListener('click', () => {
3204                        if (!rgbBorderModeCheckbox?.checked) {
3205                            const defaultColor = '#ffffff';
3206                            borderColorInput.value = defaultColor;
3207                            borderColorTextInput.value = defaultColor;
3208                            this.applyBorderColor(defaultColor);
3209                            this.saveSettings();
3210                        }
3211                    });
3212                }
3213            }
3214            
3215            // Close dropdown when clicking outside
3216            if (!document.settingsDropdownListenerAdded) {
3217                document.addEventListener('click', (e) => {
3218                    if (dropdown && dropdownButton) {
3219                        if (!dropdown.contains(e.target) && !dropdownButton.contains(e.target)) {
3220                            dropdown.classList.remove('show');
3221                        }
3222                    }
3223                });
3224                document.settingsDropdownListenerAdded = true;
3225            }
3226        }
3227    }
3228    
3229    setRgbSpeed(speed) {
3230        // Update CSS variable for animation duration
3231        const root = document.documentElement;
3232        root.style.setProperty('--rgb-animation-duration', `${speed}s`);
3233    }
3234    
3235    toggleRgbBorderMode(enabled) {
3236        const body = document.body;
3237        const staticColorControls = document.getElementById('static-color-controls');
3238        
3239        if (enabled) {
3240            // Enable RGB mode - remove inline styles so CSS animations can work
3241            body.classList.add('rgb-border-mode');
3242            
3243            // Apply current speed setting
3244            const speedSlider = document.getElementById('rgb-speed-slider');
3245            if (speedSlider) {
3246                this.setRgbSpeed(parseFloat(speedSlider.value));
3247            }
3248            
3249            // Remove inline styles that would override animations
3250            const elementsToClear = [
3251                document.querySelector('.virtual-keyboard'),
3252                document.getElementById('keyboard-container'),
3253                document.getElementById('synth-sidebar'),
3254                document.getElementById('toolbar'),
3255                ...document.querySelectorAll('.settings-dropdown, .sounds-dropdown, .scales-dropdown, .effects-dropdown, .chord-display')
3256            ];
3257            
3258            elementsToClear.forEach(el => {
3259                if (el) {
3260                    el.style.borderColor = '';
3261                    el.style.borderBottomColor = '';
3262                    el.style.animation = '';
3263                    el.style.backgroundColor = '';
3264                }
3265            });
3266            
3267            // Clear keyboard background inline style
3268            const virtualKeyboard = document.querySelector('.virtual-keyboard');
3269            if (virtualKeyboard) {
3270                virtualKeyboard.style.background = '';
3271                virtualKeyboard.style.backgroundColor = '';
3272            }
3273            
3274            if (staticColorControls) {
3275                staticColorControls.style.opacity = '0.5';
3276                staticColorControls.style.pointerEvents = 'none';
3277            }
3278        } else {
3279            // Disable RGB mode
3280            body.classList.remove('rgb-border-mode');
3281            
3282            // Clear animation styles
3283            const elementsToClear = [
3284                document.querySelector('.virtual-keyboard'),
3285                document.getElementById('keyboard-container'),
3286                document.getElementById('synth-sidebar'),
3287                document.getElementById('toolbar'),
3288                ...document.querySelectorAll('.settings-dropdown, .sounds-dropdown, .scales-dropdown, .effects-dropdown, .chord-display')
3289            ];
3290            
3291            elementsToClear.forEach(el => {
3292                if (el) {
3293                    el.style.animation = '';
3294                }
3295            });
3296            
3297            if (staticColorControls) {
3298                staticColorControls.style.opacity = '1';
3299                staticColorControls.style.pointerEvents = 'auto';
3300            }
3301            
3302            // Apply saved static color
3303            const colorInput = document.getElementById('border-color');
3304            if (colorInput) {
3305                this.applyBorderColor(colorInput.value);
3306            }
3307        }
3308    }
3309    
3310    setupSidebarToggle() {
3311        if (document.body.classList.contains('page-citrusos')) {
3312            const scope = document.getElementById('synth-sidebar')?.closest(
3313                '.citrus-window__content, .citrus-fullscreen__content'
3314            );
3315            if (window.DawSynthFlyout && typeof window.DawSynthFlyout.init === 'function' && scope) {
3316                window.DawSynthFlyout.init(scope);
3317            } else if (!window.DawSynthFlyout) {
3318                console.warn('[DAW] DawSynthFlyout missing on CitrusOS');
3319            }
3320            return;
3321        }
3322
3323        const sidebarToggle = document.getElementById('sidebar-toggle');
3324        const sidebar = document.getElementById('synth-sidebar');
3325
3326        const setSidebarToggleIcon = (collapsed) => {
3327            if (!sidebarToggle) return;
3328            sidebarToggle.innerHTML = collapsed
3329                ? '<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>'
3330                : '<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>';
3331        };
3332        
3333        if (sidebarToggle && sidebar) {
3334            // Prevent duplicate event listeners
3335            if (sidebarToggle.dataset.listenerAdded === 'true') {
3336                return; // Already set up
3337            }
3338            sidebarToggle.dataset.listenerAdded = 'true';
3339            
3340            // Load saved state
3341            const saved = localStorage.getItem('daw-sidebar-collapsed');
3342            const isCollapsed = saved === 'true';
3343            
3344            if (isCollapsed) {
3345                sidebar.classList.add('collapsed');
3346                sidebarToggle.classList.add('collapsed');
3347                setSidebarToggleIcon(true);
3348            } else {
3349                setSidebarToggleIcon(false);
3350            }
3351            
3352            sidebarToggle.addEventListener('click', (e) => {
3353                e.stopPropagation();
3354                e.preventDefault();
3355                
3356                const isCollapsed = sidebar.classList.contains('collapsed');
3357                
3358                if (isCollapsed) {
3359                    // Expand
3360                    sidebar.classList.remove('collapsed');
3361                    sidebarToggle.classList.remove('collapsed');
3362                    setSidebarToggleIcon(false);
3363                    localStorage.setItem('daw-sidebar-collapsed', 'false');
3364                } else {
3365                    // Collapse
3366                    sidebar.classList.add('collapsed');
3367                    sidebarToggle.classList.add('collapsed');
3368                    setSidebarToggleIcon(true);
3369                    localStorage.setItem('daw-sidebar-collapsed', 'true');
3370                }
3371            });
3372        }
3373    }
3374    
3375    applyBorderColor(color) {
3376        // Don't apply if RGB mode is enabled
3377        const rgbModeCheckbox = document.getElementById('rgb-border-mode');
3378        if (rgbModeCheckbox && rgbModeCheckbox.checked) {
3379            return;
3380        }
3381        
3382        // Apply border color to virtual keyboard and UI elements
3383        const root = document.documentElement;
3384        root.style.setProperty('--daw-border-color', color);
3385        
3386        // Apply to virtual keyboard (border + background - exact same color)
3387        const virtualKeyboard = document.querySelector('.virtual-keyboard');
3388        if (virtualKeyboard) {
3389            virtualKeyboard.style.borderColor = color;
3390            virtualKeyboard.style.backgroundColor = color; // Exact same color as border
3391            virtualKeyboard.style.animation = 'none'; // Remove RGB animation if present
3392        }
3393        
3394        // Apply to keyboard container
3395        const keyboardContainer = document.getElementById('keyboard-container');
3396        if (keyboardContainer) {
3397            keyboardContainer.style.borderColor = color;
3398            keyboardContainer.style.animation = 'none'; // Remove RGB animation if present
3399        }
3400        
3401        // Apply to sidebar
3402        const synthSidebar = document.getElementById('synth-sidebar');
3403        if (synthSidebar) {
3404            synthSidebar.style.borderColor = color;
3405            synthSidebar.style.animation = 'none'; // Remove RGB animation if present
3406        }
3407        
3408        // Apply to toolbar
3409        const toolbar = document.getElementById('toolbar');
3410        if (toolbar) {
3411            toolbar.style.borderBottomColor = color;
3412            toolbar.style.animation = 'none'; // Remove RGB animation if present
3413        }
3414        
3415        // Apply to dropdowns
3416        const dropdowns = document.querySelectorAll('.settings-dropdown, .sounds-dropdown, .scales-dropdown, .effects-dropdown');
3417        dropdowns.forEach(dropdown => {
3418            dropdown.style.borderColor = color;
3419            dropdown.style.animation = 'none'; // Remove RGB animation if present
3420        });
3421        
3422        // Apply to chord display
3423        const chordDisplay = document.querySelector('.chord-display');
3424        if (chordDisplay) {
3425            chordDisplay.style.borderColor = color;
3426            chordDisplay.style.animation = 'none'; // Remove RGB animation if present
3427        }
3428    }
3429    
3430    setupAudioUI() {
3431        // Master volume
3432        const volumeSlider = document.getElementById('master-volume');
3433        if (volumeSlider && this.masterGain) {
3434            volumeSlider.addEventListener('input', (e) => {
3435                const volume = parseFloat(e.target.value);
3436                this.masterGain.gain.value = volume;
3437                this.saveSettings();
3438            });
3439        }
3440        
3441        console.log('Audio UI setup complete');
3442    }
3443    
3444    setupSynthControls() {
3445        const waveformSelect = document.getElementById('synth-waveform');
3446        if (waveformSelect) {
3447            waveformSelect.addEventListener('change', (e) => {
3448                const waveform = e.target.value;
3449                this.synth.setWaveform(waveform);
3450                
3451                // Show/hide pulse width control
3452                const pulseWidthControl = document.getElementById('pulse-width-control');
3453                if (pulseWidthControl) {
3454                    pulseWidthControl.style.display = (waveform === 'pulse' || waveform === 'pwm') ? 'block' : 'none';
3455                }
3456                this.saveSettings();
3457            });
3458        }
3459        
3460        const pulseWidthSlider = document.getElementById('synth-pulse-width');
3461        if (pulseWidthSlider) {
3462            pulseWidthSlider.addEventListener('input', (e) => {
3463                const val = parseFloat(e.target.value);
3464                document.getElementById('pulse-width-value').textContent = val.toFixed(2);
3465                this.synth.setPulseWidth(val);
3466                this.saveSettings();
3467            });
3468        }
3469        
3470        const attackSlider = document.getElementById('synth-attack');
3471        const decaySlider = document.getElementById('synth-decay');
3472        const sustainSlider = document.getElementById('synth-sustain');
3473        const releaseSlider = document.getElementById('synth-release');
3474        
3475        if (attackSlider) {
3476            attackSlider.addEventListener('input', (e) => {
3477                const val = parseFloat(e.target.value);
3478                document.getElementById('attack-value').textContent = val.toFixed(2);
3479                this.synth.setEnvelope(
3480                    val,
3481                    parseFloat(decaySlider?.value || 0.1),
3482                    parseFloat(sustainSlider?.value || 0.7),
3483                    parseFloat(releaseSlider?.value || 0.3)
3484                );
3485                this.saveSettings();
3486            });
3487        }
3488        
3489        if (decaySlider) {
3490            decaySlider.addEventListener('input', (e) => {
3491                const val = parseFloat(e.target.value);
3492                document.getElementById('decay-value').textContent = val.toFixed(2);
3493                this.synth.setEnvelope(
3494                    parseFloat(attackSlider?.value || 0.01),
3495                    val,
3496                    parseFloat(sustainSlider?.value || 0.7),
3497                    parseFloat(releaseSlider?.value || 0.3)
3498                );
3499                this.saveSettings();
3500            });
3501        }
3502        
3503        if (sustainSlider) {
3504            sustainSlider.addEventListener('input', (e) => {
3505                const val = parseFloat(e.target.value);
3506                document.getElementById('sustain-value').textContent = val.toFixed(2);
3507                this.synth.setEnvelope(
3508                    parseFloat(attackSlider?.value || 0.01),
3509                    parseFloat(decaySlider?.value || 0.1),
3510                    val,
3511                    parseFloat(releaseSlider?.value || 0.3)
3512                );
3513                this.saveSettings();
3514            });
3515        }
3516        
3517        if (releaseSlider) {
3518            releaseSlider.addEventListener('input', (e) => {
3519                const val = parseFloat(e.target.value);
3520                document.getElementById('release-value').textContent = val.toFixed(2);
3521                this.synth.setEnvelope(
3522                    parseFloat(attackSlider?.value || 0.01),
3523                    parseFloat(decaySlider?.value || 0.1),
3524                    parseFloat(sustainSlider?.value || 0.7),
3525                    val
3526                );
3527                this.saveSettings();
3528            });
3529        }
3530
3531        const filterCutoffSlider = document.getElementById('synth-filter-cutoff');
3532        const filterResonanceSlider = document.getElementById('synth-filter-resonance');
3533        const filterDecaySlider = document.getElementById('synth-filter-decay');
3534        const filterOctavesSlider = document.getElementById('synth-filter-octaves');
3535
3536        if (filterCutoffSlider) {
3537            filterCutoffSlider.addEventListener('input', (e) => {
3538                const val = parseFloat(e.target.value);
3539                document.getElementById('filter-cutoff-value').textContent = String(Math.round(val));
3540                this.synth.setFilter(val, parseFloat(filterResonanceSlider?.value || 1));
3541                this.saveSettings();
3542            });
3543        }
3544
3545        if (filterResonanceSlider) {
3546            filterResonanceSlider.addEventListener('input', (e) => {
3547                const val = parseFloat(e.target.value);
3548                document.getElementById('filter-resonance-value').textContent = val.toFixed(1);
3549                this.synth.setFilter(parseFloat(filterCutoffSlider?.value || 8000), val);
3550                this.saveSettings();
3551            });
3552        }
3553
3554        if (filterDecaySlider) {
3555            filterDecaySlider.addEventListener('input', (e) => {
3556                const val = parseFloat(e.target.value);
3557                document.getElementById('filter-decay-value').textContent = val.toFixed(2);
3558                this.synth.setFilterEnvelope({ decay: val });
3559                this.saveSettings();
3560            });
3561        }
3562
3563        if (filterOctavesSlider) {
3564            filterOctavesSlider.addEventListener('input', (e) => {
3565                const val = parseFloat(e.target.value);
3566                document.getElementById('filter-octaves-value').textContent = val.toFixed(1);
3567                this.synth.setFilterEnvelope({ octaves: val });
3568                this.saveSettings();
3569            });
3570        }
3571
3572        enhanceRangeSliders(document.getElementById('synth-sidebar'));
3573    }
3574    
3575    // Make a single value span editable
3576    makeValueEditable(valueSpan, slider, updateCallback) {
3577        if (!valueSpan || !slider) return;
3578        
3579        valueSpan.style.cursor = 'pointer';
3580        valueSpan.title = 'Click to edit';
3581        
3582        valueSpan.addEventListener('click', () => {
3583            const currentValue = parseFloat(slider.value);
3584            const min = parseFloat(slider.min);
3585            const max = parseFloat(slider.max);
3586            const step = parseFloat(slider.step) || 1;
3587            
3588            // Create input element
3589            const input = document.createElement('input');
3590            input.type = 'number';
3591            input.value = currentValue;
3592            input.min = min;
3593            input.max = max;
3594            input.step = step;
3595            input.style.width = valueSpan.offsetWidth + 'px';
3596            input.style.textAlign = 'center';
3597            input.style.border = '1px solid #c8a8ff';
3598            input.style.background = '#1a1a2e';
3599            input.style.color = '#e0e0e0';
3600            input.style.borderRadius = '3px';
3601            input.style.padding = '2px';
3602            
3603            // Replace span with input
3604            valueSpan.style.display = 'none';
3605            valueSpan.parentNode.insertBefore(input, valueSpan);
3606            input.focus();
3607            input.select();
3608            
3609            // Handle input completion
3610            const finishEdit = () => {
3611                let val = parseFloat(input.value);
3612                
3613                // Validate and clamp
3614                if (isNaN(val)) val = currentValue;
3615                val = Math.max(min, Math.min(max, val));
3616                
3617                // Round to step
3618                val = Math.round(val / step) * step;
3619                
3620                // Update slider
3621                slider.value = val;
3622                
3623                // Update display
3624                const displayValue = step < 1 ? val.toFixed(step.toString().split('.')[1]?.length || 2) : 
3625                                    (val % 1 === 0 ? val : val.toFixed(1));
3626                valueSpan.textContent = displayValue;
3627                
3628                // Restore span
3629                input.remove();
3630                valueSpan.style.display = '';
3631                
3632                // Call update callback
3633                if (updateCallback) {
3634                    updateCallback(val);
3635                }
3636            };
3637            
3638            input.addEventListener('blur', finishEdit);
3639            input.addEventListener('keydown', (e) => {
3640                if (e.key === 'Enter') {
3641                    e.preventDefault();
3642                    finishEdit();
3643                } else if (e.key === 'Escape') {
3644                    e.preventDefault();
3645                    input.remove();
3646                    valueSpan.style.display = '';
3647                }
3648            });
3649        });
3650    }
3651    
3652    // Apply editable functionality to all slider value displays
3653    makeAllValuesEditable() {
3654        // Find all sliders and their corresponding value displays
3655        const sliders = document.querySelectorAll('input[type="range"]');
3656        sliders.forEach(slider => {
3657            const id = slider.id;
3658            if (!id) return;
3659            
3660            // Find corresponding value span (common patterns: id-value, id + '-value')
3661            const valueId = id + '-value';
3662            const valueSpan = document.getElementById(valueId);
3663            if (!valueSpan) return;
3664            
3665            // Default callback: trigger the slider's input event to update everything
3666            // This will call all existing event handlers
3667            let updateCallback = () => {
3668                slider.dispatchEvent(new Event('input', { bubbles: true }));
3669            };
3670            
3671            // Apply editable functionality (callback will trigger existing handlers)
3672            this.makeValueEditable(valueSpan, slider, updateCallback);
3673        });
3674    }
3675    
3676    setupEffectsControls() {
3677        // Reverb controls
3678        const reverbCheckbox = document.getElementById('effect-reverb');
3679        const reverbControls = document.getElementById('reverb-controls');
3680        const reverbRoomSlider = document.getElementById('reverb-room');
3681        const reverbDampSlider = document.getElementById('reverb-damp');
3682        
3683        if (reverbCheckbox) {
3684            reverbCheckbox.addEventListener('change', (e) => {
3685                const enabled = e.target.checked;
3686                if (reverbControls) {
3687                    reverbControls.style.display = enabled ? 'block' : 'none';
3688                }
3689                const roomSize = parseFloat(reverbRoomSlider?.value || 0.5);
3690                const dampening = parseFloat(reverbDampSlider?.value || 0.5);
3691                this.effects.setReverb(enabled, roomSize, dampening);
3692                // Re-route Tone.js if using it
3693                if (this.synth) {
3694                    this.synth.rerouteEffects();
3695                }
3696                this.saveSettingsDebounced();
3697            });
3698        }
3699        
3700        if (reverbRoomSlider) {
3701            reverbRoomSlider.addEventListener('input', (e) => {
3702                const val = parseFloat(e.target.value);
3703                document.getElementById('reverb-room-value').textContent = val.toFixed(2);
3704                if (reverbCheckbox?.checked) {
3705                    const dampening = parseFloat(reverbDampSlider?.value || 0.5);
3706                    this.effects.setReverb(true, val, dampening);
3707                }
3708                this.saveSettingsDebounced();
3709            });
3710        }
3711        
3712        if (reverbDampSlider) {
3713            reverbDampSlider.addEventListener('input', (e) => {
3714                const val = parseFloat(e.target.value);
3715                document.getElementById('reverb-damp-value').textContent = val.toFixed(2);
3716                if (reverbCheckbox?.checked) {
3717                    const roomSize = parseFloat(reverbRoomSlider?.value || 0.5);
3718                    this.effects.setReverb(true, roomSize, val);
3719                }
3720                this.saveSettingsDebounced();
3721            });
3722        }
3723        
3724        // Delay controls
3725        const delayCheckbox = document.getElementById('effect-delay');
3726        const delayControls = document.getElementById('delay-controls');
3727        const delayTimeSlider = document.getElementById('delay-time');
3728        const delayFeedbackSlider = document.getElementById('delay-feedback');
3729        const delayMixSlider = document.getElementById('delay-mix');
3730        
3731        if (delayCheckbox) {
3732            delayCheckbox.addEventListener('change', (e) => {
3733                const enabled = e.target.checked;
3734                if (delayControls) {
3735                    delayControls.style.display = enabled ? 'block' : 'none';
3736                }
3737                const time = parseFloat(delayTimeSlider?.value || 0.3);
3738                const feedback = parseFloat(delayFeedbackSlider?.value || 0.3);
3739                const mix = parseFloat(delayMixSlider?.value || 0.3);
3740                this.effects.setDelay(enabled, time, feedback, mix);
3741                // Re-route Tone.js if using it
3742                if (this.synth) {
3743                    this.synth.rerouteEffects();
3744                }
3745                this.saveSettingsDebounced();
3746            });
3747        }
3748        
3749        if (delayTimeSlider) {
3750            delayTimeSlider.addEventListener('input', (e) => {
3751                const val = parseFloat(e.target.value);
3752                document.getElementById('delay-time-value').textContent = val.toFixed(2);
3753                if (delayCheckbox?.checked) {
3754                    const feedback = parseFloat(delayFeedbackSlider?.value || 0.3);
3755                    const mix = parseFloat(delayMixSlider?.value || 0.3);
3756                    this.effects.setDelay(true, val, feedback, mix);
3757                }
3758                this.saveSettingsDebounced();
3759            });
3760        }
3761        
3762        if (delayFeedbackSlider) {
3763            delayFeedbackSlider.addEventListener('input', (e) => {
3764                const val = parseFloat(e.target.value);
3765                document.getElementById('delay-feedback-value').textContent = val.toFixed(2);
3766                if (delayCheckbox?.checked) {
3767                    const time = parseFloat(delayTimeSlider?.value || 0.3);
3768                    const mix = parseFloat(delayMixSlider?.value || 0.3);
3769                    this.effects.setDelay(true, time, val, mix);
3770                }
3771                this.saveSettingsDebounced();
3772            });
3773        }
3774        
3775        if (delayMixSlider) {
3776            delayMixSlider.addEventListener('input', (e) => {
3777                const val = parseFloat(e.target.value);
3778                document.getElementById('delay-mix-value').textContent = val.toFixed(2);
3779                if (delayCheckbox?.checked) {
3780                    const time = parseFloat(delayTimeSlider?.value || 0.3);
3781                    const feedback = parseFloat(delayFeedbackSlider?.value || 0.3);
3782                    this.effects.setDelay(true, time, feedback, val);
3783                }
3784                this.saveSettingsDebounced();
3785            });
3786        }
3787        
3788        // Distortion controls
3789        const distortionCheckbox = document.getElementById('effect-distortion');
3790        const distortionControls = document.getElementById('distortion-controls');
3791        const distortionAmountSlider = document.getElementById('distortion-amount');
3792        
3793        if (distortionCheckbox) {
3794            distortionCheckbox.addEventListener('change', (e) => {
3795                const enabled = e.target.checked;
3796                if (distortionControls) {
3797                    distortionControls.style.display = enabled ? 'block' : 'none';
3798                }
3799                const amount = parseFloat(distortionAmountSlider?.value || 0.5);
3800                this.effects.setDistortion(enabled, amount);
3801                // Re-route Tone.js if using it
3802                if (this.synth) {
3803                    this.synth.rerouteEffects();
3804                }
3805            });
3806        }
3807        
3808        if (distortionAmountSlider) {
3809            distortionAmountSlider.addEventListener('input', (e) => {
3810                const val = parseFloat(e.target.value);
3811                document.getElementById('distortion-amount-value').textContent = val.toFixed(2);
3812                if (distortionCheckbox?.checked) {
3813                    this.effects.setDistortion(true, val);
3814                }
3815                this.saveSettingsDebounced();
3816            });
3817        }
3818        
3819        // Chorus controls
3820        const chorusCheckbox = document.getElementById('effect-chorus');
3821        const chorusControls = document.getElementById('chorus-controls');
3822        const chorusRateSlider = document.getElementById('chorus-rate');
3823        const chorusDepthSlider = document.getElementById('chorus-depth');
3824        const chorusFeedbackSlider = document.getElementById('chorus-feedback');
3825        
3826        if (chorusCheckbox) {
3827            chorusCheckbox.addEventListener('change', (e) => {
3828                const enabled = e.target.checked;
3829                if (chorusControls) {
3830                    chorusControls.style.display = enabled ? 'block' : 'none';
3831                }
3832                const rate = parseFloat(chorusRateSlider?.value || 1.5);
3833                const depth = parseFloat(chorusDepthSlider?.value || 0.3);
3834                const feedback = parseFloat(chorusFeedbackSlider?.value || 0.2);
3835                this.effects.setChorus(enabled, rate, depth, feedback);
3836                if (this.synth) this.synth.rerouteEffects();
3837                this.saveSettingsDebounced();
3838            });
3839        }
3840        
3841        if (chorusRateSlider) {
3842            chorusRateSlider.addEventListener('input', (e) => {
3843                const val = parseFloat(e.target.value);
3844                document.getElementById('chorus-rate-value').textContent = val.toFixed(1);
3845                if (chorusCheckbox?.checked) {
3846                    const depth = parseFloat(chorusDepthSlider?.value || 0.3);
3847                    const feedback = parseFloat(chorusFeedbackSlider?.value || 0.2);
3848                    this.effects.setChorus(true, val, depth, feedback);
3849                }
3850                this.saveSettingsDebounced();
3851            });
3852        }
3853        
3854        if (chorusDepthSlider) {
3855            chorusDepthSlider.addEventListener('input', (e) => {
3856                const val = parseFloat(e.target.value);
3857                document.getElementById('chorus-depth-value').textContent = val.toFixed(2);
3858                if (chorusCheckbox?.checked) {
3859                    const rate = parseFloat(chorusRateSlider?.value || 1.5);
3860                    const feedback = parseFloat(chorusFeedbackSlider?.value || 0.2);
3861                    this.effects.setChorus(true, rate, val, feedback);
3862                }
3863                this.saveSettingsDebounced();
3864            });
3865        }
3866        
3867        if (chorusFeedbackSlider) {
3868            chorusFeedbackSlider.addEventListener('input', (e) => {
3869                const val = parseFloat(e.target.value);
3870                document.getElementById('chorus-feedback-value').textContent = val.toFixed(2);
3871                if (chorusCheckbox?.checked) {
3872                    const rate = parseFloat(chorusRateSlider?.value || 1.5);
3873                    const depth = parseFloat(chorusDepthSlider?.value || 0.3);
3874                    this.effects.setChorus(true, rate, depth, val);
3875                }
3876                this.saveSettingsDebounced();
3877            });
3878        }
3879        
3880        // Phaser controls
3881        const phaserCheckbox = document.getElementById('effect-phaser');
3882        const phaserControls = document.getElementById('phaser-controls');
3883        const phaserRateSlider = document.getElementById('phaser-rate');
3884        const phaserDepthSlider = document.getElementById('phaser-depth');
3885        const phaserFeedbackSlider = document.getElementById('phaser-feedback');
3886        
3887        if (phaserCheckbox) {
3888            phaserCheckbox.addEventListener('change', (e) => {
3889                const enabled = e.target.checked;
3890                if (phaserControls) {
3891                    phaserControls.style.display = enabled ? 'block' : 'none';
3892                }
3893                const rate = parseFloat(phaserRateSlider?.value || 0.5);
3894                const depth = parseFloat(phaserDepthSlider?.value || 0.6);
3895                const feedback = parseFloat(phaserFeedbackSlider?.value || 0.3);
3896                this.effects.setPhaser(enabled, rate, depth, feedback);
3897                if (this.synth) this.synth.rerouteEffects();
3898                this.saveSettingsDebounced();
3899            });
3900        }
3901        
3902        if (phaserRateSlider) {
3903            phaserRateSlider.addEventListener('input', (e) => {
3904                const val = parseFloat(e.target.value);
3905                document.getElementById('phaser-rate-value').textContent = val.toFixed(1);
3906                if (phaserCheckbox?.checked) {
3907                    const depth = parseFloat(phaserDepthSlider?.value || 0.6);
3908                    const feedback = parseFloat(phaserFeedbackSlider?.value || 0.3);
3909                    this.effects.setPhaser(true, val, depth, feedback);
3910                }
3911                this.saveSettingsDebounced();
3912            });
3913        }
3914        
3915        if (phaserDepthSlider) {
3916            phaserDepthSlider.addEventListener('input', (e) => {
3917                const val = parseFloat(e.target.value);
3918                document.getElementById('phaser-depth-value').textContent = val.toFixed(2);
3919                if (phaserCheckbox?.checked) {
3920                    const rate = parseFloat(phaserRateSlider?.value || 0.5);
3921                    const feedback = parseFloat(phaserFeedbackSlider?.value || 0.3);
3922                    this.effects.setPhaser(true, rate, val, feedback);
3923                }
3924                this.saveSettingsDebounced();
3925            });
3926        }
3927        
3928        if (phaserFeedbackSlider) {
3929            phaserFeedbackSlider.addEventListener('input', (e) => {
3930                const val = parseFloat(e.target.value);
3931                document.getElementById('phaser-feedback-value').textContent = val.toFixed(2);
3932                if (phaserCheckbox?.checked) {
3933                    const rate = parseFloat(phaserRateSlider?.value || 0.5);
3934                    const depth = parseFloat(phaserDepthSlider?.value || 0.6);
3935                    this.effects.setPhaser(true, rate, depth, val);
3936                }
3937                this.saveSettingsDebounced();
3938            });
3939        }
3940        
3941        // Filter controls
3942        const filterCheckbox = document.getElementById('effect-filter');
3943        const filterControls = document.getElementById('filter-control
3943s');
3944        const filterTypeSelect = document.getElementById('filter-type');
3945        const filterFreqSlider = document.getElementById('filter-freq');
3946        const filterResSlider = document.getElementById('filter-res');
3947        
3948        if (filterCheckbox) {
3949            filterCheckbox.addEventListener('change', (e) => {
3950                const enabled = e.target.checked;
3951                if (filterControls) {
3952                    filterControls.style.display = enabled ? 'block' : 'none';
3953                }
3954                const type = filterTypeSelect?.value || 'lowpass';
3955                const freq = parseFloat(filterFreqSlider?.value || 2000);
3956                const res = parseFloat(filterResSlider?.value || 1);
3957                this.effects.setFilter(enabled, type, freq, res);
3958                if (this.synth) this.synth.rerouteEffects();
3959                this.saveSettingsDebounced();
3960            });
3961        }
3962        
3963        if (filterTypeSelect) {
3964            filterTypeSelect.addEventListener('change', (e) => {
3965                if (filterCheckbox?.checked) {
3966                    const freq = parseFloat(filterFreqSlider?.value || 2000);
3967                    const res = parseFloat(filterResSlider?.value || 1);
3968                    this.effects.setFilter(true, e.target.value, freq, res);
3969                }
3970                this.saveSettingsDebounced();
3971            });
3972        }
3973        
3974        if (filterFreqSlider) {
3975            filterFreqSlider.addEventListener('input', (e) => {
3976                const val = parseFloat(e.target.value);
3977                document.getElementById('filter-freq-value').textContent = val;
3978                if (filterCheckbox?.checked) {
3979                    const type = filterTypeSelect?.value || 'lowpass';
3980                    const res = parseFloat(filterResSlider?.value || 1);
3981                    this.effects.setFilter(true, type, val, res);
3982                }
3983                this.saveSettingsDebounced();
3984            });
3985        }
3986        
3987        if (filterResSlider) {
3988            filterResSlider.addEventListener('input', (e) => {
3989                const val = parseFloat(e.target.value);
3990                document.getElementById('filter-res-value').textContent = val.toFixed(1);
3991                if (filterCheckbox?.checked) {
3992                    const type = filterTypeSelect?.value || 'lowpass';
3993                    const freq = parseFloat(filterFreqSlider?.value || 2000);
3994                    this.effects.setFilter(true, type, freq, val);
3995                }
3996                this.saveSettingsDebounced();
3997            });
3998        }
3999        
4000        // Compressor controls
4001        const compressorCheckbox = document.getElementById('effect-compressor');
4002        const compressorControls = document.getElementById('compressor-controls');
4003        const compressorThresholdSlider = document.getElementById('compressor-threshold');
4004        const compressorRatioSlider = document.getElementById('compressor-ratio');
4005        const compressorKneeSlider = document.getElementById('compressor-knee');
4006        const compressorAttackSlider = document.getElementById('compressor-attack');
4007        const compressorReleaseSlider = document.getElementById('compressor-release');
4008        
4009        if (compressorCheckbox) {
4010            compressorCheckbox.addEventListener('change', (e) => {
4011                const enabled = e.target.checked;
4012                if (compressorControls) {
4013                    compressorControls.style.display = enabled ? 'block' : 'none';
4014                }
4015                const threshold = parseFloat(compressorThresholdSlider?.value || -24);
4016                const ratio = parseFloat(compressorRatioSlider?.value || 4);
4017                const knee = parseFloat(compressorKneeSlider?.value || 30);
4018                const attack = parseFloat(compressorAttackSlider?.value || 0.003);
4019                const release = parseFloat(compressorReleaseSlider?.value || 0.25);
4020                this.effects.setCompressor(enabled, threshold, ratio, knee, attack, release);
4021                if (this.synth) this.synth.rerouteEffects();
4022                this.saveSettingsDebounced();
4023            });
4024        }
4025        
4026        if (compressorThresholdSlider) {
4027            compressorThresholdSlider.addEventListener('input', (e) => {
4028                const val = parseFloat(e.target.value);
4029                document.getElementById('compressor-threshold-value').textContent = val;
4030                if (compressorCheckbox?.checked) {
4031                    const ratio = parseFloat(compressorRatioSlider?.value || 4);
4032                    const knee = parseFloat(compressorKneeSlider?.value || 30);
4033                    const attack = parseFloat(compressorAttackSlider?.value || 0.003);
4034                    const release = parseFloat(compressorReleaseSlider?.value || 0.25);
4035                    this.effects.setCompressor(true, val, ratio, knee, attack, release);
4036                }
4037                this.saveSettingsDebounced();
4038            });
4039        }
4040        
4041        if (compressorRatioSlider) {
4042            compressorRatioSlider.addEventListener('input', (e) => {
4043                const val = parseFloat(e.target.value);
4044                document.getElementById('compressor-ratio-value').textContent = val.toFixed(1);
4045                if (compressorCheckbox?.checked) {
4046                    const threshold = parseFloat(compressorThresholdSlider?.value || -24);
4047                    const knee = parseFloat(compressorKneeSlider?.value || 30);
4048                    const attack = parseFloat(compressorAttackSlider?.value || 0.003);
4049                    const release = parseFloat(compressorReleaseSlider?.value || 0.25);
4050                    this.effects.setCompressor(true, threshold, val, knee, attack, release);
4051                }
4052                this.saveSettingsDebounced();
4053            });
4054        }
4055        
4056        if (compressorKneeSlider) {
4057            compressorKneeSlider.addEventListener('input', (e) => {
4058                const val = parseFloat(e.target.value);
4059                document.getElementById('compressor-knee-value').textContent = val;
4060                if (compressorCheckbox?.checked) {
4061                    const threshold = parseFloat(compressorThresholdSlider?.value || -24);
4062                    const ratio = parseFloat(compressorRatioSlider?.value || 4);
4063                    const attack = parseFloat(compressorAttackSlider?.value || 0.003);
4064                    const release = parseFloat(compressorReleaseSlider?.value || 0.25);
4065                    this.effects.setCompressor(true, threshold, ratio, val, attack, release);
4066                }
4067                this.saveSettingsDebounced();
4068            });
4069        }
4070        
4071        if (compressorAttackSlider) {
4072            compressorAttackSlider.addEventListener('input', (e) => {
4073                const val = parseFloat(e.target.value);
4074                document.getElementById('compressor-attack-value').textContent = val.toFixed(3);
4075                if (compressorCheckbox?.checked) {
4076                    const threshold = parseFloat(compressorThresholdSlider?.value || -24);
4077                    const ratio = parseFloat(compressorRatioSlider?.value || 4);
4078                    const knee = parseFloat(compressorKneeSlider?.value || 30);
4079                    const release = parseFloat(compressorReleaseSlider?.value || 0.25);
4080                    this.effects.setCompressor(true, threshold, ratio, knee, val, release);
4081                }
4082                this.saveSettingsDebounced();
4083            });
4084        }
4085        
4086        if (compressorReleaseSlider) {
4087            compressorReleaseSlider.addEventListener('input', (e) => {
4088                const val = parseFloat(e.target.value);
4089                document.getElementById('compressor-release-value').textContent = val.toFixed(2);
4090                if (compressorCheckbox?.checked) {
4091                    const threshold = parseFloat(compressorThresholdSlider?.value || -24);
4092                    const ratio = parseFloat(compressorRatioSlider?.value || 4);
4093                    const knee = parseFloat(compressorKneeSlider?.value || 30);
4094                    const attack = parseFloat(compressorAttackSlider?.value || 0.003);
4095                    this.effects.setCompressor(true, threshold, ratio, knee, attack, val);
4096                }
4097                this.saveSettingsDebounced();
4098            });
4099        }
4100        
4101        // Tremolo controls
4102        const tremoloCheckbox = document.getElementById('effect-tremolo');
4103        const tremoloControls = document.getElementById('tremolo-controls');
4104        const tremoloRateSlider = document.getElementById('tremolo-rate');
4105        const tremoloDepthSlider = document.getElementById('tremolo-depth');
4106        const tremoloWaveformSelect = document.getElementById('tremolo-waveform');
4107        
4108        if (tremoloCheckbox) {
4109            tremoloCheckbox.addEventListener('change', (e) => {
4110                const enabled = e.target.checked;
4111                if (tremoloControls) {
4112                    tremoloControls.style.display = enabled ? 'block' : 'none';
4113                }
4114                const rate = parseFloat(tremoloRateSlider?.value || 5);
4115                const depth = parseFloat(tremoloDepthSlider?.value || 0.5);
4116                const waveform = tremoloWaveformSelect?.value || 'sine';
4117                this.effects.setTremolo(enabled, rate, depth, waveform);
4118                if (this.synth) this.synth.rerouteEffects();
4119                this.saveSettingsDebounced();
4120            });
4121        }
4122        
4123        if (tremoloRateSlider) {
4124            tremoloRateSlider.addEventListener('input', (e) => {
4125                const val = parseFloat(e.target.value);
4126                document.getElementById('tremolo-rate-value').textContent = val.toFixed(1);
4127                if (tremoloCheckbox?.checked) {
4128                    const depth = parseFloat(tremoloDepthSlider?.value || 0.5);
4129                    const waveform = tremoloWaveformSelect?.value || 'sine';
4130                    this.effects.setTremolo(true, val, depth, waveform);
4131                }
4132                this.saveSettingsDebounced();
4133            });
4134        }
4135        
4136        if (tremoloDepthSlider) {
4137            tremoloDepthSlider.addEventListener('input', (e) => {
4138                const val = parseFloat(e.target.value);
4139                document.getElementById('tremolo-depth-value').textContent = val.toFixed(2);
4140                if (tremoloCheckbox?.checked) {
4141                    const rate = parseFloat(tremoloRateSlider?.value || 5);
4142                    const waveform = tremoloWaveformSelect?.value || 'sine';
4143                    this.effects.setTremolo(true, rate, val, waveform);
4144                }
4145                this.saveSettingsDebounced();
4146            });
4147        }
4148        
4149        if (tremoloWaveformSelect) {
4150            tremoloWaveformSelect.addEventListener('change', (e) => {
4151                if (tremoloCheckbox?.checked) {
4152                    const rate = parseFloat(tremoloRateSlider?.value || 5);
4153                    const depth = parseFloat(tremoloDepthSlider?.value || 0.5);
4154                    this.effects.setTremolo(true, rate, depth, e.target.value);
4155                }
4156                this.saveSettingsDebounced();
4157            });
4158        }
4159        
4160        // Vibrato controls
4161        const vibratoCheckbox = document.getElementById('effect-vibrato');
4162        const vibratoControls = document.getElementById('vibrato-controls');
4163        const vibratoRateSlider = document.getElementById('vibrato-rate');
4164        const vibratoDepthSlider = document.getElementById('vibrato-depth');
4165        
4166        if (vibratoCheckbox) {
4167            vibratoCheckbox.addEventListener('change', (e) => {
4168                const enabled = e.target.checked;
4169                if (vibratoControls) {
4170                    vibratoControls.style.display = enabled ? 'block' : 'none';
4171                }
4172                const rate = parseFloat(vibratoRateSlider?.value || 5);
4173                const depth = parseFloat(vibratoDepthSlider?.value || 0.1);
4174                this.effects.setVibrato(enabled, rate, depth);
4175                if (this.synth) this.synth.rerouteEffects();
4176                this.saveSettingsDebounced();
4177            });
4178        }
4179        
4180        if (vibratoRateSlider) {
4181            vibratoRateSlider.addEventListener('input', (e) => {
4182                const val = parseFloat(e.target.value);
4183                document.getElementById('vibrato-rate-value').textContent = val.toFixed(1);
4184                if (vibratoCheckbox?.checked) {
4185                    const depth = parseFloat(vibratoDepthSlider?.value || 0.1);
4186                    this.effects.setVibrato(true, val, depth);
4187                }
4188                this.saveSettingsDebounced();
4189            });
4190        }
4191        
4192        if (vibratoDepthSlider) {
4193            vibratoDepthSlider.addEventListener('input', (e) => {
4194                const val = parseFloat(e.target.value);
4195                document.getElementById('vibrato-depth-value').textContent = val.toFixed(2);
4196                if (vibratoCheckbox?.checked) {
4197                    const rate = parseFloat(vibratoRateSlider?.value || 5);
4198                    this.effects.setVibrato(true, rate, val);
4199                }
4200                this.saveSettingsDebounced();
4201            });
4202        }
4203        
4204        // Bitcrusher controls
4205        const bitcrusherCheckbox = document.getElementById('effect-bitcrusher');
4206        const bitcrusherControls = document.getElementById('bitcrusher-control
4206s');
4207        const bitcrusherBitsSlider = document.getElementById('bitcrusher-bits');
4208        const bitcrusherSRSlider = document.getElementById('bitcrusher-sr');
4209        const bitcrusherMixSlider = document.getElementById('bitcrusher-mix');
4210        
4211        if (bitcrusherCheckbox) {
4212            bitcrusherCheckbox.addEventListener('change', (e) => {
4213                const enabled = e.target.checked;
4214                if (bitcrusherControls) {
4215                    bitcrusherControls.style.display = enabled ? 'block' : 'none';
4216                }
4217                const bits = parseInt(bitcrusherBitsSlider?.value || 8);
4218                const sr = parseInt(bitcrusherSRSlider?.value || 1);
4219                const mix = parseFloat(bitcrusherMixSlider?.value || 1.0);
4220                this.effects.setBitcrusher(enabled, bits, sr, mix);
4221                if (this.synth) this.synth.rerouteEffects();
4222                this.saveSettingsDebounced();
4223            });
4224        }
4225        
4226        if (bitcrusherBitsSlider) {
4227            bitcrusherBitsSlider.addEventListener('input', (e) => {
4228                const val = parseInt(e.target.value);
4229                document.getElementById('bitcrusher-bits-value').textContent = val;
4230                if (bitcrusherCheckbox?.checked) {
4231                    const sr = parseInt(bitcrusherSRSlider?.value || 1);
4232                    const mix = parseFloat(bitcrusherMixSlider?.value || 1.0);
4233                    this.effects.setBitcrusher(true, val, sr, mix);
4234                }
4235                this.saveSettingsDebounced();
4236            });
4237        }
4238        
4239        if (bitcrusherSRSlider) {
4240            bitcrusherSRSlider.addEventListener('input', (e) => {
4241                const val = parseInt(e.target.value);
4242                document.getElementById('bitcrusher-sr-value').textContent = val;
4243                if (bitcrusherCheckbox?.checked) {
4244                    const bits = parseInt(bitcrusherBitsSlider?.value || 8);
4245                    const mix = parseFloat(bitcrusherMixSlider?.value || 1.0);
4246                    this.effects.setBitcrusher(true, bits, val, mix);
4247                }
4248                this.saveSettingsDebounced();
4249            });
4250        }
4251        
4252        if (bitcrusherMixSlider) {
4253            bitcrusherMixSlider.addEventListener('input', (e) => {
4254                const val = parseFloat(e.target.value);
4255                document.getElementById('bitcrusher-mix-value').textContent = val.toFixed(2);
4256                if (bitcrusherCheckbox?.checked) {
4257                    const bits = parseInt(bitcrusherBitsSlider?.value || 8);
4258                    const sr = parseInt(bitcrusherSRSlider?.value || 1);
4259                    this.effects.setBitcrusher(true, bits, sr, val);
4260                }
4261                this.saveSettingsDebounced();
4262            });
4263        }
4264        
4265        // Ring Modulator controls
4266        const ringmodCheckbox = document.getElementById('effect-ringmod');
4267        const ringmodControls = document.getElementById('ringmod-controls');
4268        const ringmodFreqSlider = document.getElementById('ringmod-freq');
4269        const ringmodMixSlider = document.getElementById('ringmod-mix');
4270        
4271        if (ringmodCheckbox) {
4272            ringmodCheckbox.addEventListener('change', (e) => {
4273                const enabled = e.target.checked;
4274                if (ringmodControls) {
4275                    ringmodControls.style.display = enabled ? 'block' : 'none';
4276                }
4277                const freq = parseInt(ringmodFreqSlider?.value || 100);
4278                const mix = parseFloat(ringmodMixSlider?.value || 0.5);
4279                this.effects.setRingModulator(enabled, freq, mix);
4280                if (this.synth) this.synth.rerouteEffects();
4281                this.saveSettingsDebounced();
4282            });
4283        }
4284        
4285        if (ringmodFreqSlider) {
4286            ringmodFreqSlider.addEventListener('input', (e) => {
4287                const val = parseInt(e.target.value);
4288                document.getElementById('ringmod-freq-value').textContent = val;
4289                if (ringmodCheckbox?.checked) {
4290                    const mix = parseFloat(ringmodMixSlider?.value || 0.5);
4291                    this.effects.setRingModulator(true, val, mix);
4292                }
4293                this.saveSettingsDebounced();
4294            });
4295        }
4296        
4297        if (ringmodMixSlider) {
4298            ringmodMixSlider.addEventListener('input', (e) => {
4299                const val = parseFloat(e.target.value);
4300                document.getElementById('ringmod-mix-value').textContent = val.toFixed(2);
4301                if (ringmodCheckbox?.checked) {
4302                    const freq = parseInt(ringmodFreqSlider?.value || 100);
4303                    this.effects.setRingModulator(true, freq, val);
4304                }
4305                this.saveSettingsDebounced();
4306            });
4307        }
4308        
4309        // Flanger controls
4310        const flangerCheckbox = document.getElementById('effect-flanger');
4311        const flangerControls = document.getElementById('flanger-control
4311s');
4312        const flangerRateSlider = document.getElementById('flanger-rate');
4313        const flangerDepthSlider = document.getElementById('flanger-depth');
4314        const flangerFeedbackSlider = document.getElementById('flanger-feedback');
4315        const flangerDelaySlider = document.getElementById('flanger-delay');
4316        
4317        if (flangerCheckbox) {
4318            flangerCheckbox.addEventListener('change', (e) => {
4319                const enabled = e.target.checked;
4320                if (flangerControls) {
4321                    flangerControls.style.display = enabled ? 'block' : 'none';
4322                }
4323                const rate = parseFloat(flangerRateSlider?.value || 0.5);
4324                const depth = parseFloat(flangerDepthSlider?.value || 0.3);
4325                const feedback = parseFloat(flangerFeedbackSlider?.value || 0.3);
4326                const delay = parseFloat(flangerDelaySlider?.value || 0.005);
4327                this.effects.setFlanger(enabled, rate, depth, feedback, delay);
4328                if (this.synth) this.synth.rerouteEffects();
4329                this.saveSettingsDebounced();
4330            });
4331        }
4332        
4333        if (flangerRateSlider) {
4334            flangerRateSlider.addEventListener('input', (e) => {
4335                const val = parseFloat(e.target.value);
4336                document.getElementById('flanger-rate-value').textContent = val.toFixed(1);
4337                if (flangerCheckbox?.checked) {
4338                    const depth = parseFloat(flangerDepthSlider?.value || 0.3);
4339                    const feedback = parseFloat(flangerFeedbackSlider?.value || 0.3);
4340                    const delay = parseFloat(flangerDelaySlider?.value || 0.005);
4341                    this.effects.setFlanger(true, val, depth, feedback, delay);
4342                }
4343                this.saveSettingsDebounced();
4344            });
4345        }
4346        
4347        if (flangerDepthSlider) {
4348            flangerDepthSlider.addEventListener('input', (e) => {
4349                const val = parseFloat(e.target.value);
4350                document.getElementById('flanger-depth-value').textContent = val.toFixed(2);
4351                if (flangerCheckbox?.checked) {
4352                    const rate = parseFloat(flangerRateSlider?.value || 0.5);
4353                    const feedback = parseFloat(flangerFeedbackSlider?.value || 0.3);
4354                    const delay = parseFloat(flangerDelaySlider?.value || 0.005);
4355                    this.effects.setFlanger(true, rate, val, feedback, delay);
4356                }
4357                this.saveSettingsDebounced();
4358            });
4359        }
4360        
4361        if (flangerFeedbackSlider) {
4362            flangerFeedbackSlider.addEventListener('input', (e) => {
4363                const val = parseFloat(e.target.value);
4364                document.getElementById('flanger-feedback-value').textContent = val.toFixed(2);
4365                if (flangerCheckbox?.checked) {
4366                    const rate = parseFloat(flangerRateSlider?.value || 0.5);
4367                    const depth = parseFloat(flangerDepthSlider?.value || 0.3);
4368                    const delay = parseFloat(flangerDelaySlider?.value || 0.005);
4369                    this.effects.setFlanger(true, rate, depth, val, delay);
4370                }
4371                this.saveSettingsDebounced();
4372            });
4373        }
4374        
4375        if (flangerDelaySlider) {
4376            flangerDelaySlider.addEventListener('input', (e) => {
4377                const val = parseFloat(e.target.value);
4378                document.getElementById('flanger-delay-value').textContent = val.toFixed(3);
4379                if (flangerCheckbox?.checked) {
4380                    const rate = parseFloat(flangerRateSlider?.value || 0.5);
4381                    const depth = parseFloat(flangerDepthSlider?.value || 0.3);
4382                    const feedback = parseFloat(flangerFeedbackSlider?.value || 0.3);
4383                    this.effects.setFlanger(true, rate, depth, feedback, val);
4384                }
4385                this.saveSettingsDebounced();
4386            });
4387        }
4388        
4389        // Auto-Wah controls
4390        const autowahCheckbox = document.getElementById('effect-autowah');
4391        const autowahControls = document.getElementById('autowah-controls');
4392        const autowahSensitivitySlider = document.getElementById('autowah-sensitivity');
4393        const autowahRangeSlider = document.getElementById('autowah-range');
4394        const autowahResSlider = document.getElementById('autowah-res');
4395        const autowahAttackSlider = document.getElementById('autowah-attack');
4396        const autowahReleaseSlider = document.getElementById('autowah-release');
4397        
4398        if (autowahCheckbox) {
4399            autowahCheckbox.addEventListener('change', (e) => {
4400                const enabled = e.target.checked;
4401                if (autowahControls) {
4402                    autowahControls.style.display = enabled ? 'block' : 'none';
4403                }
4404                const sensitivity = parseFloat(autowahSensitivitySlider?.value || 0.5);
4405                const range = parseInt(autowahRangeSlider?.value || 2000);
4406                const res = parseFloat(autowahResSlider?.value || 5);
4407                const attack = parseFloat(autowahAttackSlider?.value || 0.01);
4408                const release = parseFloat(autowahReleaseSlider?.value || 0.1);
4409                this.effects.setAutoWah(enabled, sensitivity, range, res, attack, release);
4410                if (this.synth) this.synth.rerouteEffects();
4411                this.saveSettingsDebounced();
4412            });
4413        }
4414        
4415        if (autowahSensitivitySlider) {
4416            autowahSensitivitySlider.addEventListener('input', (e) => {
4417                const val = parseFloat(e.target.value);
4418                document.getElementById('autowah-sensitivity-value').textContent = val.toFixed(2);
4419                if (autowahCheckbox?.checked) {
4420                    const range = parseInt(autowahRangeSlider?.value || 2000);
4421                    const res = parseFloat(autowahResSlider?.value || 5);
4422                    const attack = parseFloat(autowahAttackSlider?.value || 0.01);
4423                    const release = parseFloat(autowahReleaseSlider?.value || 0.1);
4424                    this.effects.setAutoWah(true, val, range, res, attack, release);
4425                }
4426                this.saveSettingsDebounced();
4427            });
4428        }
4429        
4430        if (autowahRangeSlider) {
4431            autowahRangeSlider.addEventListener('input', (e) => {
4432                const val = parseInt(e.target.value);
4433                document.getElementById('autowah-range-value').textContent = val;
4434                if (autowahCheckbox?.checked) {
4435                    const sensitivity = parseFloat(autowahSensitivitySlider?.value || 0.5);
4436                    const res = parseFloat(autowahResSlider?.value || 5);
4437                    const attack = parseFloat(autowahAttackSlider?.value || 0.01);
4438                    const release = parseFloat(autowahReleaseSlider?.value || 0.1);
4439                    this.effects.setAutoWah(true, sensitivity, val, res, attack, release);
4440                }
4441                this.saveSettingsDebounced();
4442            });
4443        }
4444        
4445        if (autowahResSlider) {
4446            autowahResSlider.addEventListener('input', (e) => {
4447                const val = parseFloat(e.target.value);
4448                document.getElementById('autowah-res-value').textContent = val.toFixed(1);
4449                if (autowahCheckbox?.checked) {
4450                    const sensitivity = parseFloat(autowahSensitivitySlider?.value || 0.5);
4451                    const range = parseInt(autowahRangeSlider?.value || 2000);
4452                    const attack = parseFloat(autowahAttackSlider?.value || 0.01);
4453                    const release = parseFloat(autowahReleaseSlider?.value || 0.1);
4454                    this.effects.setAutoWah(true, sensitivity, range, val, attack, release);
4455                }
4456                this.saveSettingsDebounced();
4457            });
4458        }
4459        
4460        if (autowahAttackSlider) {
4461            autowahAttackSlider.addEventListener('input', (e) => {
4462                const val = parseFloat(e.target.value);
4463                document.getElementById('autowah-attack-value').textContent = val.toFixed(3);
4464                if (autowahCheckbox?.checked) {
4465                    const sensitivity = parseFloat(autowahSensitivitySlider?.value || 0.5);
4466                    const range = parseInt(autowahRangeSlider?.value || 2000);
4467                    const res = parseFloat(autowahResSlider?.value || 5);
4468                    const release = parseFloat(autowahReleaseSlider?.value || 0.1);
4469                    this.effects.setAutoWah(true, sensitivity, range, res, val, release);
4470                }
4471                this.saveSettingsDebounced();
4472            });
4473        }
4474        
4475        if (autowahReleaseSlider) {
4476            autowahReleaseSlider.addEventListener('input', (e) => {
4477                const val = parseFloat(e.target.value);
4478                document.getElementById('autowah-release-value').textContent = val.toFixed(2);
4479                if (autowahCheckbox?.checked) {
4480                    const sensitivity = parseFloat(autowahSensitivitySlider?.value || 0.5);
4481                    const range = parseInt(autowahRangeSlider?.value || 2000);
4482                    const res = parseFloat(autowahResSlider?.value || 5);
4483                    const attack = parseFloat(autowahAttackSlider?.value || 0.01);
4484                    this.effects.setAutoWah(true, sensitivity, range, res, attack, val);
4485                }
4486                this.saveSettingsDebounced();
4487            });
4488        }
4489        
4490        // Pitch Shifter controls
4491        const pitchshiftCheckbox = document.getElementById('effect-pitchshift');
4492        const pitchshiftControls = document.getElementById('pitchshift-control
4492s');
4493        const pitchshiftSemitonesSlider = document.getElementById('pitchshift-semitones');
4494        const pitchshiftCentsSlider = document.getElementById('pitchshift-cents');
4495        
4496        if (pitchshiftCheckbox) {
4497            pitchshiftCheckbox.addEventListener('change', (e) => {
4498                const enabled = e.target.checked;
4499                if (pitchshiftControls) {
4500                    pitchshiftControls.style.display = enabled ? 'block' : 'none';
4501                }
4502                const semitones = parseInt(pitchshiftSemitonesSlider?.value || 0);
4503                const cents = parseInt(pitchshiftCentsSlider?.value || 0);
4504                this.effects.setPitchShifter(enabled, semitones, cents, 0);
4505                if (this.synth) this.synth.rerouteEffects();
4506                this.saveSettingsDebounced();
4507            });
4508        }
4509        
4510        if (pitchshiftSemitonesSlider) {
4511            pitchshiftSemitonesSlider.addEventListener('input', (e) => {
4512                const val = parseInt(e.target.value);
4513                document.getElementById('pitchshift-semitones-value').textContent = val;
4514                if (pitchshiftCheckbox?.checked) {
4515                    const cents = parseInt(pitchshiftCentsSlider?.value || 0);
4516                    this.effects.setPitchShifter(true, val, cents, 0);
4517                }
4518                this.saveSettingsDebounced();
4519            });
4520        }
4521        
4522        if (pitchshiftCentsSlider) {
4523            pitchshiftCentsSlider.addEventListener('input', (e) => {
4524                const val = parseInt(e.target.value);
4525                document.getElementById('pitchshift-cents-value').textContent = val;
4526                if (pitchshiftCheckbox?.checked) {
4527                    const semitones = parseInt(pitchshiftSemitonesSlider?.value || 0);
4528                    this.effects.setPitchShifter(true, semitones, val, 0);
4529                }
4530                this.saveSettingsDebounced();
4531            });
4532        }
4533        
4534        // Echo controls
4535        const echoCheckbox = document.getElementById('effect-echo');
4536        const echoControls = document.getElementById('echo-controls');
4537        const echoTimeSlider = document.getElementById('echo-time');
4538        const echoFeedbackSlider = document.getElementById('echo-feedback');
4539        const echoMixSlider = document.getElementById('echo-mix');
4540        
4541        if (echoCheckbox) {
4542            echoCheckbox.addEventListener('change', (e) => {
4543                const enabled = e.target.checked;
4544                if (echoControls) echoControls.style.display = enabled ? 'block' : 'none';
4545                const time = parseFloat(echoTimeSlider?.value || 0.25);
4546                const feedback = parseFloat(echoFeedbackSlider?.value || 0.4);
4547                const mix = parseFloat(echoMixSlider?.value || 0.3);
4548                this.effects.setEcho(enabled, time, feedback, mix);
4549                if (this.synth) this.synth.rerouteEffects();
4550                this.saveSettingsDebounced();
4551            });
4552        }
4553        
4554        if (echoTimeSlider) {
4555            echoTimeSlider.addEventListener('input', (e) => {
4556                const val = parseFloat(e.target.value);
4557                document.getElementById('echo-time-value').textContent = val.toFixed(2);
4558                if (echoCheckbox?.checked) {
4559                    const feedback = parseFloat(echoFeedbackSlider?.value || 0.4);
4560                    const mix = parseFloat(echoMixSlider?.value || 0.3);
4561                    this.effects.setEcho(true, val, feedback, mix);
4562                }
4563                this.saveSettingsDebounced();
4564            });
4565        }
4566        
4567        if (echoFeedbackSlider) {
4568            echoFeedbackSlider.addEventListener('input', (e) => {
4569                const val = parseFloat(e.target.value);
4570                document.getElementById('echo-feedback-value').textContent = val.toFixed(2);
4571                if (echoCheckbox?.checked) {
4572                    const time = parseFloat(echoTimeSlider?.value || 0.25);
4573                    const mix = parseFloat(echoMixSlider?.value || 0.3);
4574                    this.effects.setEcho(true, time, val, mix);
4575                }
4576                this.saveSettingsDebounced();
4577            });
4578        }
4579        
4580        if (echoMixSlider) {
4581            echoMixSlider.addEventListener('input', (e) => {
4582                const val = parseFloat(e.target.value);
4583                document.getElementById('echo-mix-value').textContent = val.toFixed(2);
4584                if (echoCheckbox?.checked) {
4585                    const time = parseFloat(echoTimeSlider?.value || 0.25);
4586                    const feedback = parseFloat(echoFeedbackSlider?.value || 0.4);
4587                    this.effects.setEcho(true, time, feedback, val);
4588                }
4589                this.saveSettingsDebounced();
4590            });
4591        }
4592        
4593        // Overdrive controls
4594        const overdriveCheckbox = document.getElementById('effect-overdrive');
4595        const overdriveControls = document.getElementById('overdrive-control
4595s');
4596        const overdriveDriveSlider = document.getElementById('overdrive-drive');
4597        const overdriveToneSlider = document.getElementById('overdrive-tone');
4598        const overdriveLevelSlider = document.getElementById('overdrive-level');
4599        
4600        if (overdriveCheckbox) {
4601            overdriveCheckbox.addEventListener('change', (e) => {
4602                const enabled = e.target.checked;
4603                if (overdriveControls) overdriveControls.style.display = enabled ? 'block' : 'none';
4604                const drive = parseFloat(overdriveDriveSlider?.value || 0.3);
4605                const tone = parseFloat(overdriveToneSlider?.value || 0.5);
4606                const level = parseFloat(overdriveLevelSlider?.value || 0.7);
4607                this.effects.setOverdrive(enabled, drive, tone, level);
4608                if (this.synth) this.synth.rerouteEffects();
4609                this.saveSettingsDebounced();
4610            });
4611        }
4612        
4613        if (overdriveDriveSlider) {
4614            overdriveDriveSlider.addEventListener('input', (e) => {
4615                const val = parseFloat(e.target.value);
4616                document.getElementById('overdrive-drive-value').textContent = val.toFixed(2);
4617                if (overdriveCheckbox?.checked) {
4618                    const tone = parseFloat(overdriveToneSlider?.value || 0.5);
4619                    const level = parseFloat(overdriveLevelSlider?.value || 0.7);
4620                    this.effects.setOverdrive(true, val, tone, level);
4621                }
4622                this.saveSettingsDebounced();
4623            });
4624        }
4625        
4626        if (overdriveToneSlider) {
4627            overdriveToneSlider.addEventListener('input', (e) => {
4628                const val = parseFloat(e.target.value);
4629                document.getElementById('overdrive-tone-value').textContent = val.toFixed(2);
4630                if (overdriveCheckbox?.checked) {
4631                    const drive = parseFloat(overdriveDriveSlider?.value || 0.3);
4632                    const level = parseFloat(overdriveLevelSlider?.value || 0.7);
4633                    this.effects.setOverdrive(true, drive, val, level);
4634                }
4635                this.saveSettingsDebounced();
4636            });
4637        }
4638        
4639        if (overdriveLevelSlider) {
4640            overdriveLevelSlider.addEventListener('input', (e) => {
4641                const val = parseFloat(e.target.value);
4642                document.getElementById('overdrive-level-value').textContent = val.toFixed(2);
4643                if (overdriveCheckbox?.checked) {
4644                    const drive = parseFloat(overdriveDriveSlider?.value || 0.3);
4645                    const tone = parseFloat(overdriveToneSlider?.value || 0.5);
4646                    this.effects.setOverdrive(true, drive, tone, val);
4647                }
4648                this.saveSettingsDebounced();
4649            });
4650        }
4651        
4652        // Fuzz controls
4653        const fuzzCheckbox = document.getElementById('effect-fuzz');
4654        const fuzzControls = document.getElementById('fuzz-controls');
4655        const fuzzIntensitySlider = document.getElementById('fuzz-intensity');
4656        const fuzzGateSlider = document.getElementById('fuzz-gate');
4657        const fuzzMixSlider = document.getElementById('fuzz-mix');
4658        
4659        if (fuzzCheckbox) {
4660            fuzzCheckbox.addEventListener('change', (e) => {
4661                const enabled = e.target.checked;
4662                if (fuzzControls) fuzzControls.style.display = enabled ? 'block' : 'none';
4663                const intensity = parseFloat(fuzzIntensitySlider?.value || 0.6);
4664                const gate = parseFloat(fuzzGateSlider?.value || 0.3);
4665                const mix = parseFloat(fuzzMixSlider?.value || 0.8);
4666                this.effects.setFuzz(enabled, intensity, gate, mix);
4667                if (this.synth) this.synth.rerouteEffects();
4668                this.saveSettingsDebounced();
4669            });
4670        }
4671        
4672        if (fuzzIntensitySlider) {
4673            fuzzIntensitySlider.addEventListener('input', (e) => {
4674                const val = parseFloat(e.target.value);
4675                document.getElementById('fuzz-intensity-value').textContent = val.toFixed(2);
4676                if (fuzzCheckbox?.checked) {
4677                    const gate = parseFloat(fuzzGateSlider?.value || 0.3);
4678                    const mix = parseFloat(fuzzMixSlider?.value || 0.8);
4679                    this.effects.setFuzz(true, val, gate, mix);
4680                }
4681                this.saveSettingsDebounced();
4682            });
4683        }
4684        
4685        if (fuzzGateSlider) {
4686            fuzzGateSlider.addEventListener('input', (e) => {
4687                const val = parseFloat(e.target.value);
4688                document.getElementById('fuzz-gate-value').textContent = val.toFixed(2);
4689                if (fuzzCheckbox?.checked) {
4690                    const intensity = parseFloat(fuzzIntensitySlider?.value || 0.6);
4691                    const mix = parseFloat(fuzzMixSlider?.value || 0.8);
4692                    this.effects.setFuzz(true, intensity, val, mix);
4693                }
4694                this.saveSettingsDebounced();
4695            });
4696        }
4697        
4698        if (fuzzMixSlider) {
4699            fuzzMixSlider.addEventListener('input', (e) => {
4700                const val = parseFloat(e.target.value);
4701                document.getElementById('fuzz-mix-value').textContent = val.toFixed(2);
4702                if (fuzzCheckbox?.checked) {
4703                    const intensity = parseFloat(fuzzIntensitySlider?.value || 0.6);
4704                    const gate = parseFloat(fuzzGateSlider?.value || 0.3);
4705                    this.effects.setFuzz(true, intensity, gate, val);
4706                }
4707                this.saveSettingsDebounced();
4708            });
4709        }
4710        
4711        // Stereo Widener controls
4712        const stereowidenerCheckbox = document.getElementById('effect-stereowidener');
4713        const stereowidenerControls = document.getElementById('stereowidener-control
4713s');
4714        const stereowidenerWidthSlider = document.getElementById('stereowidener-width');
4715        const stereowidenerBalanceSlider = document.getElementById('stereowidener-balance');
4716        
4717        if (stereowidenerCheckbox) {
4718            stereowidenerCheckbox.addEventListener('change', (e) => {
4719                const enabled = e.target.checked;
4720                if (stereowidenerControls) stereowidenerControls.style.display = enabled ? 'block' : 'none';
4721                const width = parseFloat(stereowidenerWidthSlider?.value || 0.5);
4722                const balance = parseFloat(stereowidenerBalanceSlider?.value || 0);
4723                this.effects.setStereoWidener(enabled, width, balance);
4724                if (this.synth) this.synth.rerouteEffects();
4725                this.saveSettingsDebounced();
4726            });
4727        }
4728        
4729        if (stereowidenerWidthSlider) {
4730            stereowidenerWidthSlider.addEventListener('input', (e) => {
4731                const val = parseFloat(e.target.value);
4732                document.getElementById('stereowidener-width-value').textContent = val.toFixed(2);
4733                if (stereowidenerCheckbox?.checked) {
4734                    const balance = parseFloat(stereowidenerBalanceSlider?.value || 0);
4735                    this.effects.setStereoWidener(true, val, balance);
4736                }
4737                this.saveSettingsDebounced();
4738            });
4739        }
4740        
4741        if (stereowidenerBalanceSlider) {
4742            stereowidenerBalanceSlider.addEventListener('input', (e) => {
4743                const val = parseFloat(e.target.value);
4744                document.getElementById('stereowidener-balance-value').textContent = val.toFixed(2);
4745                if (stereowidenerCheckbox?.checked) {
4746                    const width = parseFloat(stereowidenerWidthSlider?.value || 0.5);
4747                    this.effects.setStereoWidener(true, width, val);
4748                }
4749                this.saveSettingsDebounced();
4750            });
4751        }
4752        
4753        // Exciter controls
4754        const exciterCheckbox = document.getElementById('effect-exciter');
4755        const exciterControls = document.getElementById('exciter-control
4755s');
4756        const exciterAmountSlider = document.getElementById('exciter-amount');
4757        const exciterFreqSlider = document.getElementById('exciter-freq');
4758        
4759        if (exciterCheckbox) {
4760            exciterCheckbox.addEventListener('change', (e) => {
4761                const enabled = e.target.checked;
4762                if (exciterControls) exciterControls.style.display = enabled ? 'block' : 'none';
4763                const amount = parseFloat(exciterAmountSlider?.value || 0.3);
4764                const frequency = parseFloat(exciterFreqSlider?.value || 5000);
4765                this.effects.setExciter(enabled, amount, frequency);
4766                if (this.synth) this.synth.rerouteEffects();
4767                this.saveSettingsDebounced();
4768            });
4769        }
4770        
4771        if (exciterAmountSlider) {
4772            exciterAmountSlider.addEventListener('input', (e) => {
4773                const val = parseFloat(e.target.value);
4774                document.getElementById('exciter-amount-value').textContent = val.toFixed(2);
4775                if (exciterCheckbox?.checked) {
4776                    const frequency = parseFloat(exciterFreqSlider?.value || 5000);
4777                    this.effects.setExciter(true, val, frequency);
4778                }
4779                this.saveSettingsDebounced();
4780            });
4781        }
4782        
4783        if (exciterFreqSlider) {
4784            exciterFreqSlider.addEventListener('input', (e) => {
4785                const val = parseFloat(e.target.value);
4786                document.getElementById('exciter-freq-value').textContent = val;
4787                if (exciterCheckbox?.checked) {
4788                    const amount = parseFloat(exciterAmountSlider?.value || 0.3);
4789                    this.effects.setExciter(true, amount, val);
4790                }
4791                this.saveSettingsDebounced();
4792            });
4793        }
4794        
4795        // Noise Gate controls
4796        const noisegateCheckbox = document.getElementById('effect-noisegate');
4797        const noisegateControls = document.getElementById('noisegate-controls');
4798        const noisegateThresholdSlider = document.getElementById('noisegate-threshold');
4799        const noisegateAttackSlider = document.getElementById('noisegate-attack');
4800        const noisegateReleaseSlider = document.getElementById('noisegate-release');
4801        
4802        if (noisegateCheckbox) {
4803            noisegateCheckbox.addEventListener('change', (e) => {
4804                const enabled = e.target.checked;
4805                if (noisegateControls) noisegateControls.style.display = enabled ? 'block' : 'none';
4806                const threshold = parseFloat(noisegateThresholdSlider?.value || -40);
4807                const attack = parseFloat(noisegateAttackSlider?.value || 0.001);
4808                const release = parseFloat(noisegateReleaseSlider?.value || 0.1);
4809                this.effects.setNoiseGate(enabled, threshold, attack, release);
4810                if (this.synth) this.synth.rerouteEffects();
4811                this.saveSettingsDebounced();
4812            });
4813        }
4814        
4815        if (noisegateThresholdSlider) {
4816            noisegateThresholdSlider.addEventListener('input', (e) => {
4817                const val = parseFloat(e.target.value);
4818                document.getElementById('noisegate-threshold-value').textContent = val;
4819                if (noisegateCheckbox?.checked) {
4820                    const attack = parseFloat(noisegateAttackSlider?.value || 0.001);
4821                    const release = parseFloat(noisegateReleaseSlider?.value || 0.1);
4822                    this.effects.setNoiseGate(true, val, attack, release);
4823                }
4824                this.saveSettingsDebounced();
4825            });
4826        }
4827        
4828        if (noisegateAttackSlider) {
4829            noisegateAttackSlider.addEventListener('input', (e) => {
4830                const val = parseFloat(e.target.value);
4831                document.getElementById('noisegate-attack-value').textContent = val.toFixed(3);
4832                if (noisegateCheckbox?.checked) {
4833                    const threshold = parseFloat(noisegateThresholdSlider?.value || -40);
4834                    const release = parseFloat(noisegateReleaseSlider?.value || 0.1);
4835                    this.effects.setNoiseGate(true, threshold, val, release);
4836                }
4837                this.saveSettingsDebounced();
4838            });
4839        }
4840        
4841        if (noisegateReleaseSlider) {
4842            noisegateReleaseSlider.addEventListener('input', (e) => {
4843                const val = parseFloat(e.target.value);
4844                document.getElementById('noisegate-release-value').textContent = val.toFixed(2);
4845                if (noisegateCheckbox?.checked) {
4846                    const threshold = parseFloat(noisegateThresholdSlider?.value || -40);
4847                    const attack = parseFloat(noisegateAttackSlider?.value || 0.001);
4848                    this.effects.setNoiseGate(true, threshold, attack, val);
4849                }
4850                this.saveSettingsDebounced();
4851            });
4852        }
4853        
4854        // Limiter controls
4855        const limiterCheckbox = document.getElementById('effect-limiter');
4856        const limiterControls = document.getElementById('limiter-control
4856s');
4857        const limiterThresholdSlider = document.getElementById('limiter-threshold');
4858        const limiterReleaseSlider = document.getElementById('limiter-release');
4859        
4860        if (limiterCheckbox) {
4861            limiterCheckbox.addEventListener('change', (e) => {
4862                const enabled = e.target.checked;
4863                if (limiterControls) limiterControls.style.display = enabled ? 'block' : 'none';
4864                const threshold = parseFloat(limiterThresholdSlider?.value || -3);
4865                const release = parseFloat(limiterReleaseSlider?.value || 0.1);
4866                this.effects.setLimiter(enabled, threshold, release);
4867                if (this.synth) this.synth.rerouteEffects();
4868                this.saveSettingsDebounced();
4869            });
4870        }
4871        
4872        if (limiterThresholdSlider) {
4873            limiterThresholdSlider.addEventListener('input', (e) => {
4874                const val = parseFloat(e.target.value);
4875                document.getElementById('limiter-threshold-value').textContent = val;
4876                if (limiterCheckbox?.checked) {
4877                    const release = parseFloat(limiterReleaseSlider?.value || 0.1);
4878                    this.effects.setLimiter(true, val, release);
4879                }
4880                this.saveSettingsDebounced();
4881            });
4882        }
4883        
4884        if (limiterReleaseSlider) {
4885            limiterReleaseSlider.addEventListener('input', (e) => {
4886                const val = parseFloat(e.target.value);
4887                document.getElementById('limiter-release-value').textContent = val.toFixed(2);
4888                if (limiterCheckbox?.checked) {
4889                    const threshold = parseFloat(limiterThresholdSlider?.value || -3);
4890                    this.effects.setLimiter(true, threshold, val);
4891                }
4892                this.saveSettingsDebounced();
4893            });
4894        }
4895        
4896        // Make all slider values editable AFTER all event listeners are set up
4897        this.makeAllValuesEditable();
4898    }
4899    
4900    setupEffectsOrdering() {
4901        // Get default order (all effects in their current HTML order)
4902        const effectItems = Array.from(document.querySelectorAll('.effect-item'));
4903        const defaultOrder = effectItems.map(item => item.dataset.effect);
4904        
4905        // Load saved order or use default
4906        let effectsOrder = defaultOrder;
4907        try {
4908            const saved = localStorage.getItem('daw-effects-order');
4909            if (saved) {
4910                const parsed = JSON.parse(saved);
4911                // Validate: ensure all effects are present and no extras
4912                if (Array.isArray(parsed) && parsed.length === defaultOrder.length) {
4913                    const hasAllEffects = defaultOrder.every(effect => parsed.includes(effect));
4914                    if (hasAllEffects) {
4915                        effectsOrder = parsed;
4916                    }
4917                }
4918            }
4919        } catch (error) {
4920            console.warn('Error loading effects order:', error);
4921        }
4922        
4923        // Reorder DOM elements based on saved order
4924        this.reorderEffectsDOM(effectsOrder);
4925        
4926        // Update effects order in Effects class
4927        if (this.effects) {
4928            this.effects.setEffectsOrder(effectsOrder);
4929            // Re-route effects to apply new order immediately
4930            if (this.synth) {
4931                this.synth.rerouteEffects();
4932            }
4933        }
4934        
4935        // Set up click handlers for up/down buttons (only once)
4936        // Use a data attribute to track if listener is already attached
4937        document.querySelectorAll('.effect-order-btn').forEach(btn => {
4938            if (btn.dataset.listenerAttached === 'true') return;
4939            btn.dataset.listenerAttached = 'true';
4940            
4941            btn.addEventListener('click', (e) => {
4942                e.stopPropagation();
4943                e.preventDefault();
4944                const effect = btn.dataset.effect;
4945                const isUp = btn.classList.contains('effect-order-up');
4946                this.moveEffect(effect, isUp);
4947            });
4948        });
4949        
4950        // Update button states (disable up for first, down for last)
4951        this.updateOrderButtonStates();
4952    }
4953    
4954    reorderEffectsDOM(order) {
4955        const container = document.querySelector('#effects-dropdown .sidebar-section');
4956        if (!container) return;
4957        
4958        const effectItems = Array.from(document.querySelectorAll('.effect-item'));
4959        const itemsByEffect = {};
4960        effectItems.forEach(item => {
4961            itemsByEffect[item.dataset.effect] = item;
4962        });
4963        
4964        // Remove all items (this preserves event listeners since we're not cloning)
4965        effectItems.forEach(item => item.remove());
4966        
4967        // Add them back in the new order
4968        order.forEach(effect => {
4969            const item = itemsByEffect[effect];
4970            if (item) {
4971                container.appendChild(item);
4972            }
4973        });
4974        
4975        // Re-attach event listeners after reordering (they should still be attached, but just in case)
4976        // Actually, remove() and appendChild() preserve event listeners, so we don't need to re-attach
4977    }
4978    
4979    moveEffect(effect, moveUp) {
4980        // Get fresh list of items
4981        const container = document.querySelector('#effects-dropdown .sidebar-section');
4982        if (!container) return;
4983        
4984        const effectItems = Array.from(container.querySelectorAll('.effect-item'));
4985        const currentIndex = effectItems.findIndex(item => item.dataset.effect === effect);
4986        
4987        if (currentIndex === -1) return;
4988        
4989        const newIndex = moveUp ? currentIndex - 1 : currentIndex + 1;
4990        if (newIndex < 0 || newIndex >= effectItems.length) return;
4991        
4992        const currentItem = effectItems[currentIndex];
4993        
4994        // Remove the item first (this is safe - insertBefore will handle re-insertion)
4995        // But actually, insertBefore automatically removes from old position, so we don't need to remove first
4996        
4997        if (moveUp) {
4998            // Moving up: insert before the item that's currently above
4999            const targetItem = effectItems[newIndex];
5000            container.insertBefore(currentItem, targetItem);
5001        } else {
5002            // Moving down: we want to insert currentItem AFTER targetItem
5003            // So we need to find what comes after targetItem and insert before that
5004            const targetItem = effectItems[newIndex];
5005            // Get all children of container (including currentItem)
5006            const allChildren = Array.from(container.children);
5007            const targetIndex = allChildren.indexOf(targetItem);
5008            
5009            // Find the next sibling element after targetItem (skip text nodes)
5010            let insertBeforeNode = null;
5011            for (let i = targetIndex + 1; i < allChildren.length; i++) {
5012                if (allChildren[i] !== currentItem && allChildren[i].classList.contains('effect-item')) {
5013                    insertBeforeNode = allChildren[i];
5014                    break;
5015                }
5016            }
5017            
5018            if (insertBeforeNode) {
5019                container.insertBefore(currentItem, insertBeforeNode);
5020            } else {
5021                // targetItem is the last element (or currentItem is right after it), append to end
5022                container.appendChild(currentItem);
5023            }
5024        }
5025        
5026        // Update order array
5027        const newOrder = Array.from(document.querySelectorAll('.effect-item')).map(item => item.dataset.effect);
5028        
5029        // Save to localStorage
5030        localStorage.setItem('daw-effects-order', JSON.stringify(newOrder));
5031        
5032        // Update effects order in Effects class
5033        if (this.effects) {
5034            this.effects.setEffectsOrder(newOrder);
5035            // Re-route effects to apply new order
5036            if (this.synth) {
5037                this.synth.rerouteEffects();
5038            }
5039        }
5040        
5041        // Update button states
5042        this.updateOrderButtonStates();
5043    }
5044    
5045    updateOrderButtonStates() {
5046        const effectItems = Array.from(document.querySelectorAll('.effect-item'));
5047        effectItems.forEach((item, index) => {
5048            const effect = item.dataset.effect;
5049            const upBtn = item.querySelector(`.effect-order-up[data-effect="${effect}"]`);
5050            const downBtn = item.querySelector(`.effect-order-down[data-effect="${effect}"]`);
5051            
5052            if (upBtn) {
5053                upBtn.disabled = index === 0;
5054            }
5055            if (downBtn) {
5056                downBtn.disabled = index === effectItems.length - 1;
5057            }
5058        });
5059    }
5060    
5061    // Getter methods for modules
5062    getAudioContext() {
5063        return this.audioContext;
5064    }
5065    
5066    getMasterGain() {
5067        return this.masterGain;
5068    }
5069    
5070    // Public method to check/initialize audio
5071    async ensureAudioReady() {
5072        if (!this.audioContext || this.audioContext.state === 'suspended' || this.audioContext.state === 'closed') {
5073            const success = await this.initializeAudio();
5074            if (!success) {
5075                console.warn('Failed to initialize audio');
5076                return false;
5077            }
5078        }
5079        return this.audioContext && this.audioContext.state === 'running';
5080    }
5081
5082    destroy() {
5083        if (this._destroyed) return;
5084        this._destroyed = true;
5085
5086        try {
5087            if (this.midi) {
5088                if (this.midi.arpeggiator) this.midi.arpeggiator.clearHeld();
5089                if (this.midi.setSustainPedal) this.midi.setSustainPedal(false);
5090                if (this.midi.activeNotes) {
5091                    for (const note of Array.from(this.midi.activeNotes.keys())) {
5092                        this.midi.stopSoundingNote(note);
5093                    }
5094                }
5095                if (typeof this.midi.teardownListeners === 'function') {
5096                    this.midi.teardownListeners();
5097                }
5098            }
5099        } catch (e) {
5100            console.warn('DAW MIDI teardown error:', e);
5101        }
5102
5103        try {
5104            if (this.visualizer && typeof this.visualizer.stop === 'function') {
5105                this.visualizer.stop();
5106            }
5107            if (this.spectrogramVisualizer && typeof this.spectrogramVisualizer.stop === 'function') {
5108                this.spectrogramVisualizer.stop();
5109            }
5110        } catch (e) {
5111            console.warn('DAW visualizer teardown error:', e);
5112        }
5113
5114        if (this.rgbAnimationFrame) {
5115            cancelAnimationFrame(this.rgbAnimationFrame);
5116            this.rgbAnimationFrame = null;
5117        }
5118
5119        if (window.DawSynthFlyout && typeof window.DawSynthFlyout.destroy === 'function') {
5120            window.DawSynthFlyout.destroy();
5121        }
5122
5123        if (this.audioContext && this.audioContext.state === 'running') {
5124            this.audioContext.suspend().catch(() => {});
5125        }
5126
5127        this.isInitialized = false;
5128        this.uiSetupComplete = false;
5129    }
5130    
5131}
5132
5133// Initialize DAW when DOM is ready (skipped when DawPage sets __dawDeferBoot)
5134let dawInstance = null;
5135
5136function initializeDAW() {
5137    if (dawInstance && !dawInstance._destroyed) return dawInstance;
5138    if (dawInstance && dawInstance._destroyed) {
5139        dawInstance = null;
5140        window.DAW = null;
5141    }
5142    
5143    try {
5144        console.log('Creating DAW instance...');
5145        dawInstance = new DAW();
5146        window.DAW = dawInstance;
5147        if (window.DawEngine) window.DawEngine.bind(dawInstance);
5148        console.log('DAW instance created successfully');
5149        
5150        setTimeout(() => {
5151            if (!dawInstance.isInitialized && !window.__dawDeferBoot) {
5152                dawInstance.init().catch(error => {
5153                    console.error('Error during DAW initialization:', error);
5154                });
5155            }
5156        }, 100);
5157    } catch (error) {
5158        console.error('Error creating DAW instance:', error);
5159        console.error('Error stack:', error.stack);
5160    }
5161
5162    return dawInstance;
5163}
5164
5165window.initializeDAW = initializeDAW;
5166
5167if (!window.__dawDeferBoot) {
5168    if (document.readyState === 'loading') {
5169        document.addEventListener('DOMContentLoaded', initializeDAW);
5170    } else {
5171        initializeDAW();
5172    }
5173
5174    window.addEventListener('load', () => {
5175        if (!dawInstance) {
5176            console.warn('DAW not initialized yet, trying again...');
5177            initializeDAW();
5178        }
5179    });
5180}
5181
5182export { DAW, initializeDAW };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.