synthx-xbox-edition / script.js
SREAL's picture
I want you to make a tone.JS based synthesizer with 3 oscillators, ADSR, an X/Y modulation matrix and a large variety of effects with all relevant parameters. give it a graphic interface that is reminiscent of the original XBOX GUI.
d4e9dbb verified
// 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
}
});