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