// Initialize Tone.js synth const synth = new Tone.PolySynth(Tone.Synth).toDestination(); // Initialize effects const reverb = new Tone.Reverb({ decay: 2, wet: 0.3 }).toDestination(); const delay = new Tone.FeedbackDelay({ delayTime: 0.5, feedback: 0.3, wet: 0.2 }).toDestination(); const filter = new Tone.Filter({ type: "lowpass", frequency: 1000, Q: 1 }).toDestination(); const distortion = new Tone.Distortion({ distortion: 0.5, wet: 0.3 }).toDestination(); const chorus = new Tone.Chorus({ frequency: 2, depth: 0.5, wet: 0.3 }).toDestination(); const phaser = new Tone.Phaser({ frequency: 2, depth: 0.5, wet: 0.3 }).toDestination(); // Connect synth to effects chain synth.chain(filter, distortion, chorus, phaser, delay, reverb, Tone.Destination); // Oscillator controls document.getElementById('osc1-waveform').addEventListener('change', (e) => { synth.set({ oscillator: { type: e.target.value } }); }); document.getElementById('osc1-frequency').addEventListener('input', (e) => { const freq = parseFloat(e.target.value); document.getElementById('osc1-freq-value').textContent = `${freq} Hz`; // In a real implementation, this would update oscillator frequency }); document.getElementById('osc1-detune').addEventListener('input', (e) => { const detune = parseFloat(e.target.value); document.getElementById('osc1-detune-value').textContent = `${detune} cents`; // In a real implementation, this would update oscillator detune }); document.getElementById('osc1-volume').addEventListener('input', (e) => { const vol = parseFloat(e.target.value); document.getElementById('osc1-vol-value').textContent = `${vol} dB`; // In a real implementation, this would update oscillator volume }); // Similar event listeners for osc2 and osc3 would go here // ADSR Controls document.getElementById('attack').addEventListener('input', (e) => { const attack = parseFloat(e.target.value); document.getElementById('attack-value').textContent = `${attack.toFixed(2)} s`; synth.set({ envelope: { attack } }); }); document.getElementById('decay').addEventListener('input', (e) => { const decay = parseFloat(e.target.value); document.getElementById('decay-value').textContent = `${decay.toFixed(2)} s`; synth.set({ envelope: { decay } }); }); document.getElementById('sustain').addEventListener('input', (e) => { const sustain = parseFloat(e.target.value); document.getElementById('sustain-value').textContent = `${Math.round(sustain * 100)}%`; synth.set({ envelope: { sustain } }); }); document.getElementById('release').addEventListener('input', (e) => { const release = parseFloat(e.target.value); document.getElementById('release-value').textContent = `${release.toFixed(2)} s`; synth.set({ envelope: { release } }); }); // Draw ADSR visualization function drawADSR() { const canvas = document.getElementById('adsr-canvas'); const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; // Clear canvas ctx.clearRect(0, 0, width, height); // Get current values const attack = parseFloat(document.getElementById('attack').value); const decay = parseFloat(document.getElementById('decay').value); const sustain = parseFloat(document.getElementById('sustain').value); const release = parseFloat(document.getElementById('release').value); // Normalize times for visualization const totalTime = attack + decay + release + 1; // Add 1 second for sustain // Draw envelope ctx.beginPath(); ctx.moveTo(0, height); // Start at bottom left // Attack const attackWidth = (attack / totalTime) * width; ctx.lineTo(attackWidth, 0); // Decay const decayWidth = (decay / totalTime) * width; ctx.lineTo(attackWidth + decayWidth, height * (1 - sustain)); // Sustain const sustainWidth = (1 / totalTime) * width; ctx.lineTo(attackWidth + decayWidth + sustainWidth, height * (1 - sustain)); // Release const releaseWidth = (release / totalTime) * width; ctx.lineTo(attackWidth + decayWidth + sustainWidth + releaseWidth, height); ctx.strokeStyle = '#107c10'; ctx.lineWidth = 3; ctx.stroke(); } // Update ADSR visualization when values change ['attack', 'decay', 'sustain', 'release'].forEach(id => { document.getElementById(id).addEventListener('input', drawADSR); }); // Initial draw drawADSR(); // X/Y Pad functionality const xyPad = document.getElementById('xy-pad'); const xyHandle = document.getElementById('xy-handle'); let isDragging = false; function updateXYPosition(x, y) { const rect = xyPad.getBoundingClientRect(); let posX = x - rect.left; let posY = y - rect.top; // Constrain to pad boundaries posX = Math.max(0, Math.min(posX, rect.width)); posY = Math.max(0, Math.min(posY, rect.height)); // Update handle position xyHandle.style.left = `${posX}px`; xyHandle.style.top = `${posY}px`; // Update values (0-1 range) const xValue = posX / rect.width; const yValue = posY / rect.height; document.getElementById('x-value').textContent = xValue.toFixed(2); document.getElementById('y-value').textContent = yValue.toFixed(2); // In a real implementation, these values would control modulation destinations } xyPad.addEventListener('mousedown', (e) => { isDragging = true; updateXYPosition(e.clientX, e.clientY); }); document.addEventListener('mousemove', (e) => { if (isDragging) { updateXYPosition(e.clientX, e.clientY); } }); document.addEventListener('mouseup', () => { isDragging = false; }); // Touch support for mobile xyPad.addEventListener('touchstart', (e) => { isDragging = true; const touch = e.touches[0]; updateXYPosition(touch.clientX, touch.clientY); e.preventDefault(); }); document.addEventListener('touchmove', (e) => { if (isDragging) { const touch = e.touches[0]; updateXYPosition(touch.clientX, touch.clientY); e.preventDefault(); } }); document.addEventListener('touchend', () => { isDragging = false; }); // Initialize XY pad handle position xyHandle.style.left = '50%'; xyHandle.style.top = '50%'; // Effect toggles document.getElementById('reverb-toggle').addEventListener('change', (e) => { reverb.wet.value = e.target.checked ? parseFloat(document.getElementById('reverb-wet').value) : 0; }); document.getElementById('delay-toggle').addEventListener('change', (e) => { delay.wet.value = e.target.checked ? parseFloat(document.getElementById('delay-wet').value) : 0; }); document.getElementById('filter-toggle').addEventListener('change', (e) => { filter.frequency.value = e.target.checked ? parseFloat(document.getElementById('filter-frequency').value) : 20000; }); document.getElementById('distortion-toggle').addEventListener('change', (e) => { distortion.wet.value = e.target.checked ? parseFloat(document.getElementById('distortion-wet').value) : 0; }); document.getElementById('chorus-toggle').addEventListener('change', (e) => { chorus.wet.value = e.target.checked ? parseFloat(document.getElementById('chorus-wet').value) : 0; }); document.getElementById('phaser-toggle').addEventListener('change', (e) => { phaser.wet.value = e.target.checked ? parseFloat(document.getElementById('phaser-wet').value) : 0; }); // Effect parameter updates document.getElementById('reverb-decay').addEventListener('input', (e) => { reverb.decay = parseFloat(e.target.value); }); document.getElementById('reverb-wet').addEventListener('input', (e) => { const wet = parseFloat(e.target.value); document.getElementById('reverb-wet').nextElementSibling.textContent = `${Math.round(wet * 100)}%`; if (document.getElementById('reverb-toggle').checked) { reverb.wet.value = wet; } }); document.getElementById('delay-time').addEventListener('input', (e) => { delay.delayTime.value = parseFloat(e.target.value); document.getElementById('delay-time').nextElementSibling.textContent = `${parseFloat(e.target.value).toFixed(2)} s`; }); document.getElementById('delay-feedback').addEventListener('input', (e) => { delay.feedback.value = parseFloat(e.target.value); document.getElementById('delay-feedback').nextElementSibling.textContent = `${Math.round(parseFloat(e.target.value) * 100)}%`; }); document.getElementById('delay-wet').addEventListener('input', (e) => { const wet = parseFloat(e.target.value); document.getElementById('delay-wet').nextElementSibling.textContent = `${Math.round(wet * 100)}%`; if (document.getElementById('delay-toggle').checked) { delay.wet.value = wet; } }); document.getElementById('filter-type').addEventListener('change', (e) => { filter.type = e.target.value; }); document.getElementById('filter-frequency').addEventListener('input', (e) => { const freq = parseFloat(e.target.value); document.getElementById('filter-frequency').nextElementSibling.textContent = `${freq} Hz`; if (document.getElementById('filter-toggle').checked) { filter.frequency.value = freq; } }); document.getElementById('filter-q').addEventListener('input', (e) => { filter.Q.value = parseFloat(e.target.value); document.getElementById('filter-q').nextElementSibling.textContent = parseFloat(e.target.value).toFixed(1); }); document.getElementById('distortion-drive').addEventListener('input', (e) => { distortion.distortion = parseFloat(e.target.value); document.getElementById('distortion-drive').nextElementSibling.textContent = `${Math.round(parseFloat(e.target.value) * 100)}%`; }); document.getElementById('distortion-wet').addEventListener('input', (e) => { const wet = parseFloat(e.target.value); document.getElementById('distortion-wet').nextElementSibling.textContent = `${Math.round(wet * 100)}%`; if (document.getElementById('distortion-toggle').checked) { distortion.wet.value = wet; } }); document.getElementById('chorus-rate').addEventListener('input', (e) => { chorus.frequency.value = parseFloat(e.target.value); document.getElementById('chorus-rate').nextElementSibling.textContent = `${parseFloat(e.target.value).toFixed(1)} Hz`; }); document.getElementById('chorus-depth').addEventListener('input', (e) => { chorus.depth = parseFloat(e.target.value); document.getElementById('chorus-depth').nextElementSibling.textContent = `${Math.round(parseFloat(e.target.value) * 100)}%`; }); document.getElementById('chorus-wet').addEventListener('input', (e) => { const wet = parseFloat(e.target.value); document.getElementById('chorus-wet').nextElementSibling.textContent = `${Math.round(wet * 100)}%`; if (document.getElementById('chorus-toggle').checked) { chorus.wet.value = wet; } }); document.getElementById('phaser-rate').addEventListener('input', (e) => { phaser.frequency.value = parseFloat(e.target.value); document.getElementById('phaser-rate').nextElementSibling.textContent = `${parseFloat(e.target.value).toFixed(1)} Hz`; }); document.getElementById('phaser-depth').addEventListener('input', (e) => { phaser.depth = parseFloat(e.target.value); document.getElementById('phaser-depth').nextElementSibling.textContent = `${Math.round(parseFloat(e.target.value) * 100)}%`; }); document.getElementById('phaser-wet').addEventListener('input', (e) => { const wet = parseFloat(e.target.value); document.getElementById('phaser-wet').nextElementSibling.textContent = `${Math.round(wet * 100)}%`; if (document.getElementById('phaser-toggle').checked) { phaser.wet.value = wet; } }); // Keyboard interaction const keys = document.querySelectorAll('.key'); const notes = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G5', 'A5']; keys.forEach((key, index) => { key.addEventListener('mousedown', () => { synth.triggerAttack(notes[index]); key.classList.add('active'); }); key.addEventListener('mouseup', () => { synth.triggerRelease(notes[index]); key.classList.remove('active'); }); key.addEventListener('mouseleave', () => { if (key.classList.contains('active')) { synth.triggerRelease(notes[index]); key.classList.remove('active'); } }); }); // Touch support for keyboard keys.forEach((key, index) => { key.addEventListener('touchstart', (e) => { synth.triggerAttack(notes[index]); key.classList.add('active'); e.preventDefault(); }); key.addEventListener('touchend', (e) => { synth.triggerRelease(notes[index]); key.classList.remove('active'); e.preventDefault(); }); }); // Preset functionality document.getElementById('save-preset').addEventListener('click', () => { alert('Preset saved! (In a full implementation, this would save to localStorage or a database)'); }); document.getElementById('load-preset').addEventListener('click', () => { alert('Preset loaded! (In a full implementation, this would load from localStorage or a database)'); }); // Initialize all sliders with their value displays document.querySelectorAll('input[type="range"]').forEach(input => { const valueSpan = input.nextElementSibling; if (valueSpan && valueSpan.tagName === 'SPAN') { const updateValue = () => { const val = parseFloat(input.value); if (input.id.includes('freq')) { valueSpan.textContent = `${val} Hz`; } else if (input.id.includes('detune')) { valueSpan.textContent = `${val} cents`; } else if (input.id.includes('volume')) { valueSpan.textContent = `${val} dB`; } else if (input.id.includes('time') || input.id.includes('attack') || input.id.includes('decay') || input.id.includes('release')) { valueSpan.textContent = `${val.toFixed(2)} s`; } else if (input.id.includes('q')) { valueSpan.textContent = val.toFixed(1); } else if (input.id.includes('rate')) { valueSpan.textContent = `${val.toFixed(1)} Hz`; } else { valueSpan.textContent = `${Math.round(val * 100)}%`; } }; input.addEventListener('input', updateValue); updateValue(); // Initial update } });