// Tone.js setup let synth; let filter; let reverb; let distortion; let delay; let analyser; let activeNotes = new Map(); let filterEnvelope; let isPlaying = false; // Initialize audio context on first interaction let audioContextStarted = false; function initAudioContext() { if (!audioContextStarted) { Tone.start(); audioContextStarted = true; console.log('Audio context started'); return true; } return false; } // Setup Tone.js components function setupSynth() { if (!audioContextStarted) { console.warn('Audio context not started yet'); return false; } // Clean up existing components if they exist if (synth) { synth.dispose(); } if (filter) { filter.dispose(); } if (reverb) { reverb.dispose(); } if (distortion) { distortion.dispose(); } if (delay) { delay.dispose(); } if (filterEnvelope) { filterEnvelope.dispose(); } // Create effects reverb = new Tone.Reverb({ decay: 2, wet: 0.3 }).toDestination(); distortion = new Tone.Distortion({ distortion: 0.2, wet: 0.2 }).connect(reverb); delay = new Tone.FeedbackDelay({ delayTime: 0.25, feedback: 0.5, wet: 0.25 }).connect(distortion); // Create filter filter = new Tone.Filter({ type: "lowpass", frequency: 1000, Q: 1 }).connect(delay); // Create filter envelope filterEnvelope = new Tone.FrequencyEnvelope({ attack: 0.1, decay: 0.3, sustain: 0.5, release: 1.0, baseFrequency: 200, octaves: 4 }); // Create main synth synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: "sawtooth" }, envelope: { attack: 0.1, decay: 0.3, sustain: 0.5, release: 1.0 } }).connect(filter); // Modulate filter frequency with envelope filterEnvelope.connect(filter.frequency); // Create analyzer for visualizer analyser = new Tone.Analyser("fft", 64); synth.connect(analyser); console.log('Synth initialized'); return true; } // MIDI handling let midiAccess = null; let midiInputs = []; function onMIDISuccess(midi) { midiAccess = midi; updateMIDIStatus('CONNECTED'); document.getElementById('midi-connect-btn').textContent = 'DEVICE CONNECTED'; document.getElementById('midi-connect-btn').disabled = true; // Get inputs const inputs = midi.inputs.values(); for (let input = inputs.next(); input && !input.done; input = inputs.next()) { setupMIDIInput(input.value); } // Listen for new devices midi.addEventListener('statechange', onMIDIStateChange); } function onMIDIFailure(msg) { console.error('Failed to get MIDI access - ' + msg); updateMIDIStatus('CONNECTION FAILED: ' + msg, 'error'); } function onMIDIStateChange(event) { const port = event.port; if (port.state === "connected") { if (port.type === "input") { setupMIDIInput(port); } } else if (port.state === "disconnected") { if (port.type === "input") { removeMIDIInput(port); } } } function setupMIDIInput(input) { midiInputs.push(input); input.addEventListener('midimessage', onMIDIMessage); updateMIDIStatus(`CONNECTED TO: ${input.name}`, 'success'); } function removeMIDIInput(input) { midiInputs = midiInputs.filter(i => i.id !== input.id); updateMIDIStatus(`DISCONNECTED: ${input.name}`, 'warning'); } function onMIDIMessage(message) { const command = message.data[0]; const note = message.data[1]; const velocity = message.data[2]; switch (command) { case 144: // Note on if (velocity > 0) { noteOn(note, velocity); } else { noteOff(note); } break; case 128: // Note off noteOff(note); break; } updateActiveNotesDisplay(); } function noteOn(note, velocity) { if (!synth) { // Initialize audio context and synth on first note initAudioContext(); setupSynth(); } const noteName = Tone.Frequency(note, "midi").toNote(); const velocityNorm = velocity / 127; synth.triggerAttack(noteName, Tone.now(), velocityNorm); activeNotes.set(note, { name: noteName, velocity: velocity }); console.log(`Note On: ${noteName}, Velocity: ${velocity}`); } function noteOff(note) { if (!synth) return; const noteName = Tone.Frequency(note, "midi").toNote(); synth.triggerRelease(noteName, Tone.now()); activeNotes.delete(note); console.log(`Note Off: ${noteName}`); } function updateActiveNotesDisplay() { const container = document.getElementById('active-notes'); if (activeNotes.size === 0) { container.innerHTML = '
NO ACTIVE NOTES
'; return; } let html = ''; activeNotes.forEach((note, key) => { html += `
${note.name} VEL: ${note.velocity}
`; }); container.innerHTML = html; } function updateMIDIStatus(message, type = 'info') { const statusEl = document.getElementById('midi-status'); statusEl.textContent = message; statusEl.className = ''; switch(type) { case 'success': statusEl.classList.add('text-green-400'); break; case 'warning': statusEl.classList.add('text-yellow-400'); break; case 'error': statusEl.classList.add('text-red-400'); break; default: statusEl.classList.add('text-cyan-400'); } } // UI Control Updates function setupUIControls() { // Oscillator Type document.getElementById('oscillator-type').addEventListener('change', (e) => { if (synth) { synth.set({ oscillator: { type: e.target.value } }); } }); // Oscillator Detune document.getElementById('oscillator-detune').addEventListener('input', (e) => { const value = parseInt(e.target.value); document.getElementById('oscillator-detune-value').textContent = `${value} CENTS`; if (synth) { synth.set({ oscillator: { detune: value } }); } }); // Filter Frequency document.getElementById('filter-frequency').addEventListener('input', (e) => { const value = parseInt(e.target.value); document.getElementById('filter-frequency-value').textContent = `${value} HZ`; if (filter) { filter.frequency.value = value; } }); // Filter Resonance (Q) document.getElementById('filter-q').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('filter-q-value').textContent = value.toFixed(1); if (filter) { filter.Q.value = value; } }); // Filter Type document.getElementById('filter-type').addEventListener('change', (e) => { if (filter) { filter.type = e.target.value; } }); // Envelope Attack document.getElementById('envelope-attack').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('envelope-attack-value').textContent = `${value.toFixed(2)} S`; if (synth) { synth.set({ envelope: { attack: value } }); } drawADSR(); // Update ADSR visualization }); // Envelope Decay document.getElementById('envelope-decay').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('envelope-decay-value').textContent = `${value.toFixed(2)} S`; if (synth) { synth.set({ envelope: { decay: value } }); } drawADSR(); // Update ADSR visualization }); // Envelope Sustain document.getElementById('envelope-sustain').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('envelope-sustain-value').textContent = `${Math.round(value * 100)}%`; if (synth) { synth.set({ envelope: { sustain: value } }); } drawADSR(); // Update ADSR visualization }); // Envelope Release document.getElementById('envelope-release').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('envelope-release-value').textContent = `${value.toFixed(1)} S`; if (synth) { synth.set({ envelope: { release: value } }); } drawADSREnvelope(); // Update ADSR visualization }); // Filter Envelope Controls document.getElementById('filter-attack').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('filter-attack-value').textContent = `${value.toFixed(2)} S`; if (filterEnvelope) { filterEnvelope.attack = value; } drawFilterEnvelope(); // Update filter ADSR visualization }); document.getElementById('filter-decay').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('filter-decay-value').textContent = `${value.toFixed(2)} S`; if (filterEnvelope) { filterEnvelope.decay = value; } drawFilterEnvelope(); // Update filter ADSR visualization }); document.getElementById('filter-sustain').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('filter-sustain-value').textContent = `${Math.round(value * 100)}%`; if (filterEnvelope) { filterEnvelope.sustain = value; } drawFilterEnvelope(); // Update filter ADSR visualization }); document.getElementById('filter-release').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('filter-release-value').textContent = `${value.toFixed(1)} S`; if (filterEnvelope) { filterEnvelope.release = value; } drawFilterEnvelope(); // Update filter ADSR visualization }); // Reverb Wet document.getElementById('reverb-wet').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('reverb-wet-value').textContent = `${Math.round(value * 100)}%`; if (reverb) { reverb.wet.value = value; } }); // Distortion Wet document.getElementById('distortion-wet').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('distortion-wet-value').textContent = `${Math.round(value * 100)}%`; if (distortion) { distortion.wet.value = value; } }); // Delay Wet document.getElementById('delay-wet').addEventListener('input', (e) => { const value = parseFloat(e.target.value); document.getElementById('delay-wet-value').textContent = `${Math.round(value * 100)}%`; if (delay) { delay.wet.value = value; } }); } // Visualizer function setupVisualizer() { const canvas = document.getElementById('visualizer'); const ctx = canvas.getContext('2d'); function resizeCanvas() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); function draw() { if (!analyser) { requestAnimationFrame(draw); return; } const values = analyser.getValue(); ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grid ctx.strokeStyle = 'rgba(6, 182, 212, 0.1)'; ctx.lineWidth = 1; // Vertical lines for (let x = 0; x < canvas.width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } // Horizontal lines for (let y = 0; y < canvas.height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // Draw waveform ctx.beginPath(); ctx.lineWidth = 2; ctx.strokeStyle = '#06b6d4'; // cyan-500 const sliceWidth = canvas.width / values.length; let x = 0; for (let i = 0; i < values.length; i++) { const v = (values[i] + 1) / 2; const y = v * canvas.height; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } x += sliceWidth; } ctx.stroke(); requestAnimationFrame(draw); } draw(); } // ADSR Visualizer function setupADSRVisualizer() { const canvas = document.getElementById('adsr-visualizer'); const ctx = canvas.getContext('2d'); function resizeCanvas() { canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // Initial draw drawADSR(); function drawADSR() { if (!canvas.width || !canvas.height) return; ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grid ctx.strokeStyle = 'rgba(6, 182, 212, 0.1)'; ctx.lineWidth = 1; // Vertical lines for (let x = 0; x < canvas.width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } // Horizontal lines for (let y = 0; y < canvas.height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // Draw ADSR envelope const attack = parseFloat(document.getElementById('envelope-attack').value); const decay = parseFloat(document.getElementById('envelope-decay').value); const sustain = parseFloat(document.getElementById('envelope-sustain').value); const release = parseFloat(document.getElementById('envelope-release').value); // Normalize values for visualization const totalTime = attack + decay + 2 + release; // Add some time for sustain and release visualization const attackWidth = (attack / totalTime) * canvas.width; const decayWidth = (decay / totalTime) * canvas.width; const sustainWidth = (2 / totalTime) * canvas.width; const releaseWidth = (release / totalTime) * canvas.width; ctx.beginPath(); ctx.lineWidth = 3; ctx.strokeStyle = '#06b6d4'; // cyan-500 // Start at 0 ctx.moveTo(0, canvas.height); // Attack ctx.lineTo(attackWidth, 0); // Decay const sustainHeight = canvas.height - (sustain * canvas.height); ctx.lineTo(attackWidth + decayWidth, sustainHeight); // Sustain ctx.lineTo(attackWidth + decayWidth + sustainWidth, sustainHeight); // Release ctx.lineTo(attackWidth + decayWidth + sustainWidth + releaseWidth, canvas.height); ctx.stroke(); // Draw labels ctx.fillStyle = '#06b6d4'; ctx.font = '10px monospace'; ctx.fillText('A', 5, canvas.height - 5); ctx.fillText('D', attackWidth - 10, canvas.height - 5); ctx.fillText('S', attackWidth + decayWidth - 10, sustainHeight - 5); ctx.fillText('R', attackWidth + decayWidth + sustainWidth + releaseWidth - 15, canvas.height - 5); } // Make drawADSR accessible globally for updates window.drawADSR = drawADSR; } // Initialize everything when page loads document.addEventListener('DOMContentLoaded', () => { // Setup UI controls setupUIControls(); // Setup visualizers setupVisualizer(); // setupADSRVisualizer(); // This function doesn't exist, using draw functions directly // Initial draw of ADSR envelopes drawADSR(); drawFilterEnvelope(); // MIDI connection button document.getElementById('midi-connect-btn').addEventListener('click', () => { const wasStarted = initAudioContext(); if (wasStarted) { setupSynth(); } if (navigator.requestMIDIAccess) { navigator.requestMIDIAccess() .then(onMIDISuccess, onMIDIFailure); } else { updateMIDIStatus('WEBMIDI NOT SUPPORTED', 'error'); } }); }); // Add missing ADSR drawing functions function drawADSR() { const canvas = document.getElementById('amp-envelope-editor'); if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const dpr = window.devicePixelRatio || 1; canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; ctx.scale(dpr, dpr); // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grid ctx.strokeStyle = 'rgba(6, 182, 212, 0.1)'; ctx.lineWidth = 1; const width = canvas.clientWidth; const height = canvas.clientHeight; // Vertical lines for (let x = 0; x < width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } // Horizontal lines for (let y = 0; y < height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // Draw ADSR envelope const attack = parseFloat(document.getElementById('envelope-attack').value); const decay = parseFloat(document.getElementById('envelope-decay').value); const sustain = parseFloat(document.getElementById('envelope-sustain').value); const release = parseFloat(document.getElementById('envelope-release').value); // Normalize values for visualization (max 5 seconds for attack/decay, 10 for release) const attackX = (attack / 5) * width * 0.2; const decayX = (decay / 5) * width * 0.2; const sustainX = width * 0.4; // Fixed sustain time const releaseX = (release / 10) * width * 0.2; const sustainY = height - (sustain * height); ctx.beginPath(); ctx.lineWidth = 3; ctx.strokeStyle = '#06b6d4'; // cyan-500 // Start at 0 ctx.moveTo(0, height); // Attack ctx.lineTo(attackX, 0); // Decay ctx.lineTo(attackX + decayX, sustainY); // Sustain ctx.lineTo(attackX + decayX + sustainX, sustainY); // Release ctx.lineTo(attackX + decayX + sustainX + releaseX, height); ctx.stroke(); } function drawFilterEnvelope() { const canvas = document.getElementById('filter-envelope-editor'); if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const dpr = window.devicePixelRatio || 1; canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; ctx.scale(dpr, dpr); // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw grid ctx.strokeStyle = 'rgba(6, 182, 212, 0.1)'; ctx.lineWidth = 1; const width = canvas.clientWidth; const height = canvas.clientHeight; // Vertical lines for (let x = 0; x < width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } // Horizontal lines for (let y = 0; y < height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } // Draw ADSR envelope const attack = parseFloat(document.getElementById('filter-attack').value); const decay = parseFloat(document.getElementById('filter-decay').value); const sustain = parseFloat(document.getElementById('filter-sustain').value); const release = parseFloat(document.getElementById('filter-release').value); // Normalize values for visualization (max 5 seconds for attack/decay, 10 for release) const attackX = (attack / 5) * width * 0.2; const decayX = (decay / 5) * width * 0.2; const sustainX = width * 0.4; // Fixed sustain time const releaseX = (release / 10) * width * 0.2; const sustainY = height - (sustain * height); ctx.beginPath(); ctx.lineWidth = 3; ctx.strokeStyle = '#06b6d4'; // cyan-500 // Start at 0 ctx.moveTo(0, height); // Attack ctx.lineTo(attackX, 0); // Decay ctx.lineTo(attackX + decayX, sustainY); // Sustain ctx.lineTo(attackX + decayX + sustainX, sustainY); // Release ctx.lineTo(attackX + decayX + sustainX + releaseX, height); ctx.stroke(); } // Update envelope drawing when controls change document.addEventListener('DOMContentLoaded', () => { // Amplitude envelope controls document.getElementById('envelope-attack').addEventListener('input', drawADSR); document.getElementById('envelope-decay').addEventListener('input', drawADSR); document.getElementById('envelope-sustain').addEventListener('input', drawADSR); document.getElementById('envelope-release').addEventListener('input', drawADSR); // Filter envelope controls document.getElementById('filter-attack').addEventListener('input', drawFilterEnvelope); document.getElementById('filter-decay').addEventListener('input', drawFilterEnvelope); document.getElementById('filter-sustain').addEventListener('input', drawFilterEnvelope); document.getElementById('filter-release').addEventListener('input', drawFilterEnvelope); });