Spaces:
Running
Running
| // 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 | |
| } | |
| }); |