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.