synthwave-studio / script.js
SREAL's picture
there is no audio playing when midi notes are pressed, also, get rid of the extra files like the sequencer etc. clean it up and make it work.
1b11257 verified
// 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 = '<div class="text-cyan-700">NO ACTIVE NOTES</div>';
return;
}
let html = '';
activeNotes.forEach((note, key) => {
html += `<div class="py-1 px-2 rounded mb-1 bg-gray-900 border border-cyan-900 flex justify-between">
<span>${note.name}</span>
<span class="text-cyan-600">VEL: ${note.velocity}</span>
</div>`;
});
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);
});