Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>WaveSynth - Serum-inspired Wavetable Synthesizer</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&display=swap'); | |
:root { | |
--primary: #6e45e2; | |
--secondary: #88d3ce; | |
--dark: #1a1a2e; | |
--darker: #0f0f1a; | |
--light: #f8f9fa; | |
--accent: #ff7e5f; | |
} | |
body { | |
font-family: 'Roboto Mono', monospace; | |
background-color: var(--darker); | |
color: var(--light); | |
overflow-x: hidden; | |
} | |
.knob { | |
width: 60px; | |
height: 60px; | |
border-radius: 50%; | |
background: linear-gradient(145deg, #23233a, #1c1c30); | |
box-shadow: 5px 5px 10px #0a0a10, -5px -5px 10px #2a2a4c; | |
position: relative; | |
cursor: pointer; | |
} | |
.knob::after { | |
content: ''; | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
background-color: var(--accent); | |
border-radius: 50%; | |
top: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
.knob-label { | |
font-size: 0.7rem; | |
text-align: center; | |
margin-top: 5px; | |
color: var(--secondary); | |
} | |
.button { | |
padding: 8px 12px; | |
border-radius: 4px; | |
background: linear-gradient(145deg, #23233a, #1c1c30); | |
box-shadow: 3px 3px 6px #0a0a10, -3px -3px 6px #2a2a4c; | |
cursor: pointer; | |
text-align: center; | |
font-size: 0.8rem; | |
transition: all 0.2s; | |
} | |
.button:hover { | |
background: linear-gradient(145deg, #2a2a44, #232338); | |
} | |
.button.active { | |
background: var(--primary); | |
box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.5); | |
} | |
.tab { | |
padding: 8px 16px; | |
border-radius: 4px 4px 0 0; | |
background: var(--dark); | |
cursor: pointer; | |
font-size: 0.8rem; | |
} | |
.tab.active { | |
background: var(--primary); | |
} | |
.panel { | |
background: var(--dark); | |
border-radius: 8px; | |
padding: 15px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
.wave-display { | |
width: 100%; | |
height: 120px; | |
background: var(--darker); | |
border-radius: 4px; | |
position: relative; | |
overflow: hidden; | |
} | |
.slider { | |
-webkit-appearance: none; | |
width: 100%; | |
height: 6px; | |
border-radius: 3px; | |
background: var(--darker); | |
outline: none; | |
} | |
.slider::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 16px; | |
height: 16px; | |
border-radius: 50%; | |
background: var(--accent); | |
cursor: pointer; | |
} | |
.mod-matrix-cell { | |
width: 20px; | |
height: 20px; | |
border-radius: 2px; | |
background: var(--darker); | |
cursor: pointer; | |
transition: all 0.2s; | |
} | |
.mod-matrix-cell.active { | |
background: var(--primary); | |
} | |
.dropdown { | |
position: relative; | |
display: inline-block; | |
} | |
.dropdown-content { | |
display: none; | |
position: absolute; | |
background-color: var(--dark); | |
min-width: 160px; | |
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); | |
z-index: 1; | |
border-radius: 4px; | |
padding: 5px 0; | |
} | |
.dropdown:hover .dropdown-content { | |
display: block; | |
} | |
.dropdown-item { | |
padding: 8px 16px; | |
cursor: pointer; | |
} | |
.dropdown-item:hover { | |
background-color: var(--primary); | |
} | |
.envelope-display { | |
width: 100%; | |
height: 100px; | |
background: var(--darker); | |
border-radius: 4px; | |
position: relative; | |
} | |
.lfo-display { | |
width: 100%; | |
height: 60px; | |
background: var(--darker); | |
border-radius: 4px; | |
position: relative; | |
} | |
.drag-handle { | |
width: 20px; | |
height: 20px; | |
background-color: var(--primary); | |
border-radius: 50%; | |
position: absolute; | |
cursor: move; | |
} | |
.wavetable-position { | |
width: 100%; | |
height: 8px; | |
background: var(--darker); | |
border-radius: 4px; | |
margin-top: 10px; | |
position: relative; | |
} | |
.wavetable-position-handle { | |
width: 16px; | |
height: 16px; | |
background-color: var(--accent); | |
border-radius: 50%; | |
position: absolute; | |
top: -4px; | |
cursor: pointer; | |
} | |
.keyboard { | |
display: flex; | |
height: 120px; | |
margin-top: 20px; | |
} | |
.white-key { | |
flex: 1; | |
background: white; | |
border: 1px solid #ccc; | |
border-radius: 0 0 4px 4px; | |
cursor: pointer; | |
position: relative; | |
z-index: 1; | |
} | |
.black-key { | |
width: 60%; | |
height: 70px; | |
background: black; | |
border-radius: 0 0 3px 3px; | |
position: absolute; | |
z-index: 2; | |
cursor: pointer; | |
} | |
.preset-selector { | |
width: 100%; | |
padding: 8px; | |
border-radius: 4px; | |
background: var(--darker); | |
color: var(--light); | |
border: 1px solid var(--primary); | |
} | |
.preset-selector:focus { | |
outline: none; | |
border-color: var(--accent); | |
} | |
</style> | |
</head> | |
<body class="p-4"> | |
<div class="container mx-auto"> | |
<header class="flex justify-between items-center mb-6"> | |
<h1 class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-300">WaveSynth</h1> | |
<div class="flex items-center space-x-4"> | |
<div class="dropdown"> | |
<button class="button">File</button> | |
<div class="dropdown-content"> | |
<div class="dropdown-item">New</div> | |
<div class="dropdown-item">Open</div> | |
<div class="dropdown-item">Save</div> | |
<div class="dropdown-item">Export</div> | |
</div> | |
</div> | |
<div class="dropdown"> | |
<button class="button">Presets</button> | |
<div class="dropdown-content"> | |
<select class="preset-selector"> | |
<option>Init</option> | |
<option>Bass 1</option> | |
<option>Lead 1</option> | |
<option>Pad 1</option> | |
<option>Pluck 1</option> | |
</select> | |
</div> | |
</div> | |
<button id="play-button" class="button bg-green-600 hover:bg-green-700">Play</button> | |
</div> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4"> | |
<!-- OSCILLATORS --> | |
<div class="panel col-span-1"> | |
<div class="flex space-x-2 mb-4"> | |
<div class="tab active">OSC A</div> | |
<div class="tab">OSC B</div> | |
<div class="tab">SUB</div> | |
<div class="tab">NOISE</div> | |
</div> | |
<div class="wave-display mb-4"> | |
<canvas id="oscA-waveform"></canvas> | |
</div> | |
<div class="wavetable-position"> | |
<div class="wavetable-position-handle" style="left: 30%;"></div> | |
</div> | |
<div class="grid grid-cols-4 gap-4 mt-4"> | |
<div> | |
<div class="knob" id="oscA-tune"></div> | |
<div class="knob-label">Tune</div> | |
</div> | |
<div> | |
<div class="knob" id="oscA-fine"></div> | |
<div class="knob-label">Fine</div> | |
</div> | |
<div> | |
<div class="knob" id="oscA-uni"></div> | |
<div class="knob-label">Unison</div> | |
</div> | |
<div> | |
<div class="knob" id="oscA-uni-det"></div> | |
<div class="knob-label">Detune</div> | |
</div> | |
</div> | |
<div class="grid grid-cols-3 gap-4 mt-4"> | |
<div> | |
<div class="knob" id="oscA-phase"></div> | |
<div class="knob-label">Phase</div> | |
</div> | |
<div> | |
<div class="knob" id="oscA-rand"></div> | |
<div class="knob-label">Rand</div> | |
</div> | |
<div> | |
<div class="knob" id="oscA-level"></div> | |
<div class="knob-label">Level</div> | |
</div> | |
</div> | |
<div class="flex space-x-2 mt-4"> | |
<div class="button active">Sine</div> | |
<div class="button">Saw</div> | |
<div class="button">Square</div> | |
<div class="button">Tri</div> | |
</div> | |
<div class="flex space-x-2 mt-2"> | |
<div class="button">WT 1</div> | |
<div class="button">WT 2</div> | |
<div class="button">WT 3</div> | |
<div class="button">WT 4</div> | |
</div> | |
</div> | |
<!-- FILTERS AND EFFECTS --> | |
<div class="panel col-span-1"> | |
<div class="flex space-x-2 mb-4"> | |
<div class="tab active">FILTER</div> | |
<div class="tab">FX</div> | |
</div> | |
<div class="wave-display mb-4"> | |
<canvas id="filter-response"></canvas> | |
</div> | |
<div class="grid grid-cols-4 gap-4"> | |
<div> | |
<div class="knob" id="filter-cutoff"></div> | |
<div class="knob-label">Cutoff</div> | |
</div> | |
<div> | |
<div class="knob" id="filter-res"></div> | |
<div class="knob-label">Res</div> | |
</div> | |
<div> | |
<div class="knob" id="filter-drive"></div> | |
<div class="knob-label">Drive</div> | |
</div> | |
<div> | |
<div class="knob" id="filter-key"></div> | |
<div class="knob-label">Key</div> | |
</div> | |
</div> | |
<div class="flex space-x-2 mt-4"> | |
<div class="button active">LP</div> | |
<div class="button">HP</div> | |
<div class="button">BP</div> | |
<div class="button">Notch</div> | |
</div> | |
<div class="flex space-x-2 mt-2"> | |
<div class="button">12dB</div> | |
<div class="button active">24dB</div> | |
<div class="button">SVF</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="text-sm font-bold mb-2 text-cyan-300">Effects</h3> | |
<div class="grid grid-cols-3 gap-4"> | |
<div> | |
<div class="knob" id="fx-dist"></div> | |
<div class="knob-label">Dist</div> | |
</div> | |
<div> | |
<div class="knob" id="fx-delay"></div> | |
<div class="knob-label">Delay</div> | |
</div> | |
<div> | |
<div class="knob" id="fx-reverb"></div> | |
<div class="knob-label">Reverb</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- ENVELOPES AND LFOs --> | |
<div class="panel col-span-1"> | |
<div class="flex space-x-2 mb-4"> | |
<div class="tab active">ENV</div> | |
<div class="tab">LFO 1</div> | |
<div class="tab">LFO 2</div> | |
</div> | |
<div class="envelope-display mb-4"> | |
<canvas id="envelope-display"></canvas> | |
</div> | |
<div class="grid grid-cols-4 gap-4"> | |
<div> | |
<div class="knob" id="env-attack"></div> | |
<div class="knob-label">Attack</div> | |
</div> | |
<div> | |
<div class="knob" id="env-decay"></div> | |
<div class="knob-label">Decay</div> | |
</div> | |
<div> | |
<div class="knob" id="env-sustain"></div> | |
<div class="knob-label">Sustain</div> | |
</div> | |
<div> | |
<div class="knob" id="env-release"></div> | |
<div class="knob-label">Release</div> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="text-sm font-bold mb-2 text-cyan-300">LFO 1</h3> | |
<div class="lfo-display mb-2"> | |
<canvas id="lfo1-display"></canvas> | |
</div> | |
<div class="grid grid-cols-4 gap-4"> | |
<div> | |
<div class="knob" id="lfo1-rate"></div> | |
<div class="knob-label">Rate</div> | |
</div> | |
<div> | |
<div class="knob" id="lfo1-amt"></div> | |
<div class="knob-label">Amount</div> | |
</div> | |
<div> | |
<div class="knob" id="lfo1-attack"></div> | |
<div class="knob-label">Attack</div> | |
</div> | |
<div> | |
<div class="knob" id="lfo1-offset"></div> | |
<div class="knob-label">Offset</div> | |
</div> | |
</div> | |
<div class="flex space-x-2 mt-2"> | |
<div class="button active">Sine</div> | |
<div class="button">Square</div> | |
<div class="button">Tri</div> | |
<div class="button">Random</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- MOD MATRIX --> | |
<div class="panel mb-4"> | |
<h2 class="text-lg font-bold mb-4 text-purple-300">Modulation Matrix</h2> | |
<div class="overflow-x-auto"> | |
<table class="w-full"> | |
<thead> | |
<tr> | |
<th class="text-left py-2 px-4">Source</th> | |
<th class="text-center py-2 px-2">Osc A Tune</th> | |
<th class="text-center py-2 px-2">Osc A WT Pos</th> | |
<th class="text-center py-2 px-2">Filter Cutoff</th> | |
<th class="text-center py-2 px-2">Filter Res</th> | |
<th class="text-center py-2 px-2">Amp</th> | |
<th class="text-center py-2 px-2">Pan</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="py-2 px-4">LFO 1</td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell active"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
</tr> | |
<tr> | |
<td class="py-2 px-4">LFO 2</td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell active"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
</tr> | |
<tr> | |
<td class="py-2 px-4">Env 1</td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell active"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
</tr> | |
<tr> | |
<td class="py-2 px-4">Env 2</td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell active"></div></td> | |
</tr> | |
<tr> | |
<td class="py-2 px-4">Velocity</td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell active"></div></td> | |
<td class="text-center"><div class="mod-matrix-cell"></div></td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="grid grid-cols-4 gap-4 mt-4"> | |
<div> | |
<div class="knob" id="mod-amount-1"></div> | |
<div class="knob-label">Amount 1</div> | |
</div> | |
<div> | |
<div class="knob" id="mod-amount-2"></div> | |
<div class="knob-label">Amount 2</div> | |
</div> | |
<div> | |
<div class="knob" id="mod-amount-3"></div> | |
<div class="knob-label">Amount 3</div> | |
</div> | |
<div> | |
<div class="knob" id="mod-amount-4"></div> | |
<div class="knob-label">Amount 4</div> | |
</div> | |
</div> | |
</div> | |
<!-- KEYBOARD --> | |
<div class="panel"> | |
<div class="keyboard" id="keyboard"> | |
<!-- White keys --> | |
<div class="white-key" data-note="C4"></div> | |
<div class="white-key" data-note="D4"></div> | |
<div class="white-key" data-note="E4"></div> | |
<div class="white-key" data-note="F4"></div> | |
<div class="white-key" data-note="G4"></div> | |
<div class="white-key" data-note="A4"></div> | |
<div class="white-key" data-note="B4"></div> | |
<div class="white-key" data-note="C5"></div> | |
<!-- Black keys --> | |
<div class="black-key" style="left: 7%;" data-note="C#4"></div> | |
<div class="black-key" style="left: 21%;" data-note="D#4"></div> | |
<div class="black-key" style="left: 50%;" data-note="F#4"></div> | |
<div class="black-key" style="left: 64%;" data-note="G#4"></div> | |
<div class="black-key" style="left: 78%;" data-note="A#4"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Initialize Tone.js | |
document.addEventListener('DOMContentLoaded', function() { | |
// Create synth | |
const synth = new Tone.PolySynth(Tone.Synth, { | |
oscillator: { | |
type: "sine" | |
}, | |
envelope: { | |
attack: 0.1, | |
decay: 0.2, | |
sustain: 0.5, | |
release: 0.8 | |
} | |
}).toDestination(); | |
// Create effects | |
const filter = new Tone.Filter(800, "lowpass").toDestination(); | |
const reverb = new Tone.Reverb(1.5).toDestination(); | |
const delay = new Tone.PingPongDelay("4n", 0.2).toDestination(); | |
const distortion = new Tone.Distortion(0.4).toDestination(); | |
// Connect effects | |
synth.chain(filter, distortion, delay, reverb, Tone.Destination); | |
// Play button | |
const playButton = document.getElementById('play-button'); | |
let isPlaying = false; | |
playButton.addEventListener('click', function() { | |
if (!isPlaying) { | |
Tone.start(); | |
synth.triggerAttackRelease("C4", "8n"); | |
playButton.textContent = "Stop"; | |
isPlaying = true; | |
} else { | |
synth.releaseAll(); | |
playButton.textContent = "Play"; | |
isPlaying = false; | |
} | |
}); | |
// Keyboard | |
const keys = document.querySelectorAll('.white-key, .black-key'); | |
keys.forEach(key => { | |
key.addEventListener('mousedown', function() { | |
const note = this.getAttribute('data-note'); | |
synth.triggerAttack(note); | |
this.style.backgroundColor = this.classList.contains('white-key') ? '#ccc' : '#555'; | |
}); | |
key.addEventListener('mouseup', function() { | |
const note = this.getAttribute('data-note'); | |
synth.triggerRelease(note); | |
this.style.backgroundColor = this.classList.contains('white-key') ? 'white' : 'black'; | |
}); | |
key.addEventListener('mouseleave', function() { | |
if (this.style.backgroundColor !== (this.classList.contains('white-key') ? 'white' : 'black')) { | |
const note = this.getAttribute('data-note'); | |
synth.triggerRelease(note); | |
this.style.backgroundColor = this.classList.contains('white-key') ? 'white' : 'black'; | |
} | |
}); | |
}); | |
// Waveform displays | |
const oscACtx = document.getElementById('oscA-waveform').getContext('2d'); | |
const oscAChart = new Chart(oscACtx, { | |
type: 'line', | |
data: { | |
labels: Array.from({length: 100}, (_, i) => i), | |
datasets: [{ | |
data: Array.from({length: 100}, (_, i) => Math.sin(i/10)), | |
borderColor: '#6e45e2', | |
borderWidth: 2, | |
pointRadius: 0, | |
tension: 0.1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
x: { display: false }, | |
y: { | |
display: false, | |
min: -1, | |
max: 1 | |
} | |
}, | |
plugins: { | |
legend: { display: false } | |
} | |
} | |
}); | |
const filterCtx = document.getElementById('filter-response').getContext('2d'); | |
const filterChart = new Chart(filterCtx, { | |
type: 'line', | |
data: { | |
labels: Array.from({length: 100}, (_, i) => i), | |
datasets: [{ | |
data: Array.from({length: 100}, (_, i) => { | |
const x = i/100; | |
return Math.exp(-x * 5) * Math.sin(x * 20); | |
}), | |
borderColor: '#88d3ce', | |
borderWidth: 2, | |
pointRadius: 0, | |
tension: 0.1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
x: { display: false }, | |
y: { | |
display: false, | |
min: -1, | |
max: 1 | |
} | |
}, | |
plugins: { | |
legend: { display: false } | |
} | |
} | |
}); | |
const envCtx = document.getElementById('envelope-display').getContext('2d'); | |
const envChart = new Chart(envCtx, { | |
type: 'line', | |
data: { | |
labels: Array.from({length: 100}, (_, i) => i), | |
datasets: [{ | |
data: Array.from({length: 100}, (_, i) => { | |
if (i < 10) return i/10 * 0.8; // Attack | |
else if (i < 30) return 0.8 - (i-10)/20 * 0.3; // Decay | |
else if (i < 70) return 0.5; // Sustain | |
else return 0.5 * (1 - (i-70)/30); // Release | |
}), | |
borderColor: '#ff7e5f', | |
borderWidth: 2, | |
pointRadius: 0, | |
tension: 0.1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
x: { display: false }, | |
y: { | |
display: false, | |
min: 0, | |
max: 1 | |
} | |
}, | |
plugins: { | |
legend: { display: false } | |
} | |
} | |
}); | |
const lfo1Ctx = document.getElementById('lfo1-display').getContext('2d'); | |
const lfo1Chart = new Chart(lfo1Ctx, { | |
type: 'line', | |
data: { | |
labels: Array.from({length: 100}, (_, i) => i), | |
datasets: [{ | |
data: Array.from({length: 100}, (_, i) => Math.sin(i/5)), | |
borderColor: '#6e45e2', | |
borderWidth: 2, | |
pointRadius: 0, | |
tension: 0.1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
x: { display: false }, | |
y: { | |
display: false, | |
min: -1, | |
max: 1 | |
} | |
}, | |
plugins: { | |
legend: { display: false } | |
} | |
} | |
}); | |
// Knob functionality | |
function setupKnob(knobId, param, min, max, defaultValue) { | |
const knob = document.getElementById(knobId); | |
let isDragging = false; | |
let startY = 0; | |
let startValue = defaultValue; | |
let currentValue = defaultValue; | |
knob.addEventListener('mousedown', function(e) { | |
isDragging = true; | |
startY = e.clientY; | |
startValue = currentValue; | |
document.addEventListener('mousemove', onMouseMove); | |
document.addEventListener('mouseup', onMouseUp); | |
}); | |
function onMouseMove(e) { | |
if (!isDragging) return; | |
const deltaY = startY - e.clientY; | |
const range = max - min; | |
const newValue = Math.min(max, Math.max(min, startValue + (deltaY / 100) * range)); | |
currentValue = newValue; | |
updateKnobPosition(); | |
// Update synth parameter | |
if (param === 'filterCutoff') { | |
filter.frequency.value = currentValue; | |
} else if (param === 'filterResonance') { | |
filter.Q.value = currentValue; | |
} else if (param === 'distortion') { | |
distortion.wet.value = currentValue; | |
} else if (param === 'delay') { | |
delay.wet.value = currentValue; | |
} else if (param === 'reverb') { | |
reverb.wet.value = currentValue; | |
} else if (param === 'attack') { | |
synth.set({ envelope: { attack: currentValue } }); | |
} else if (param === 'decay') { | |
synth.set({ envelope: { decay: currentValue } }); | |
} else if (param === 'sustain') { | |
synth.set({ envelope: { sustain: currentValue } }); | |
} else if (param === 'release') { | |
synth.set({ envelope: { release: currentValue } }); | |
} | |
} | |
function onMouseUp() { | |
isDragging = false; | |
document.removeEventListener('mousemove', onMouseMove); | |
document.removeEventListener('mouseup', onMouseUp); | |
} | |
function updateKnobPosition() { | |
const rotation = ((currentValue - min) / (max - min)) * 270 - 135; | |
knob.style.transform = `rotate(${rotation}deg)`; | |
} | |
// Initialize | |
currentValue = defaultValue; | |
updateKnobPosition(); | |
} | |
// Setup knobs | |
setupKnob('oscA-tune', 'tune', -24, 24, 0); | |
setupKnob('oscA-fine', 'fine', -50, 50, 0); | |
setupKnob('oscA-uni', 'unison', 0, 8, 0); | |
setupKnob('oscA-uni-det', 'detune', 0, 50, 0); | |
setupKnob('oscA-phase', 'phase', 0, 360, 0); | |
setupKnob('oscA-rand', 'random', 0, 100, 0); | |
setupKnob('oscA-level', 'level', 0, 1, 0.8); | |
setupKnob('filter-cutoff', 'filterCutoff', 20, 20000, 800); | |
setupKnob('filter-res', 'filterResonance', 0.1, 20, 1); | |
setupKnob('filter-drive', 'drive', 0, 1, 0); | |
setupKnob('filter-key', 'keytrack', 0, 1, 0); | |
setupKnob('fx-dist', 'distortion', 0, 1, 0); | |
setupKnob('fx-delay', 'delay', 0, 1, 0); | |
setupKnob('fx-reverb', 'reverb', 0, 1, 0); | |
setupKnob('env-attack', 'attack', 0.001, 5, 0.1); | |
setupKnob('env-decay', 'decay', 0.001, 5, 0.2); | |
setupKnob('env-sustain', 'sustain', 0, 1, 0.5); | |
setupKnob('env-release', 'release', 0.001, 5, 0.8); | |
setupKnob('lfo1-rate', 'lfoRate', 0.1, 20, 1); | |
setupKnob('lfo1-amt', 'lfoAmount', 0, 1, 0.5); | |
setupKnob('lfo1-attack', 'lfoAttack', 0, 5, 0); | |
setupKnob('lfo1-offset', 'lfoOffset', -1, 1, 0); | |
setupKnob('mod-amount-1', 'modAmount1', 0, 1, 0.5); | |
setupKnob('mod-amount-2', 'modAmount2', 0, 1, 0.5); | |
setupKnob('mod-amount-3', 'modAmount3', 0, 1, 0.5); | |
setupKnob('mod-amount-4', 'modAmount4', 0, 1, 0.5); | |
// Modulation matrix | |
const modCells = document.querySelectorAll('.mod-matrix-cell'); | |
modCells.forEach(cell => { | |
cell.addEventListener('click', function() { | |
this.classList.toggle('active'); | |
}); | |
}); | |
// Waveform buttons | |
const oscWaveButtons = document.querySelectorAll('.panel:nth-child(1) .button:not(.tab)'); | |
oscWaveButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
oscWaveButtons.forEach(btn => btn.classList.remove('active')); | |
this.classList.add('active'); | |
let waveType = 'sine'; | |
if (this.textContent === 'Saw') waveType = 'sawtooth'; | |
else if (this.textContent === 'Square') waveType = 'square'; | |
else if (this.textContent === 'Tri') waveType = 'triangle'; | |
synth.set({ oscillator: { type: waveType } }); | |
// Update waveform display | |
const newData = Array.from({length: 100}, (_, i) => { | |
const x = i/10; | |
if (waveType === 'sine') return Math.sin(x); | |
else if (waveType === 'sawtooth') return (x % (2*Math.PI)) / Math.PI - 1; | |
else if (waveType === 'square') return Math.sin(x) > 0 ? 0.8 : -0.8; | |
else if (waveType === 'triangle') return Math.asin(Math.sin(x)) * (2/Math.PI); | |
return 0; | |
}); | |
oscAChart.data.datasets[0].data = newData; | |
oscAChart.update(); | |
}); | |
}); | |
// Filter type buttons | |
const filterTypeButtons = document.querySelectorAll('.panel:nth-child(2) .button:not(.tab)'); | |
filterTypeButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
if (this.textContent === 'LP' || this.textContent === 'HP' || | |
this.textContent === 'BP' || this.textContent === 'Notch') { | |
filterTypeButtons.forEach(btn => { | |
if (btn.textContent === 'LP' || btn.textContent === 'HP' || | |
btn.textContent === 'BP' || btn.textContent === 'Notch') { | |
btn.classList.remove('active'); | |
} | |
}); | |
this.classList.add('active'); | |
let filterType = 'lowpass'; | |
if (this.textContent === 'HP') filterType = 'highpass'; | |
else if (this.textContent === 'BP') filterType = 'bandpass'; | |
else if (this.textContent === 'Notch') filterType = 'notch'; | |
filter.type = filterType; | |
// Update filter display | |
const newData = Array.from({length: 100}, (_, i) => { | |
const x = i/100; | |
if (filterType === 'lowpass') return Math.exp(-x * 5) * Math.sin(x * 20); | |
else if (filterType === 'highpass') return (1 - Math.exp(-x * 5)) * Math.sin(x * 20); | |
else if (filterType === 'bandpass') return Math.exp(-Math.abs(x-0.5) * 10) * Math.sin(x * 20); | |
else if (filterType === 'notch') return (0.5 + 0.5 * Math.cos(x * 4 * Math.PI)) * Math.sin(x * 20); | |
return 0; | |
}); | |
filterChart.data.datasets[0].data = newData; | |
filterChart.update(); | |
} else if (this.textContent === '12dB' || this.textContent === '24dB' || this.textContent === 'SVF') { | |
filterTypeButtons.forEach(btn => { | |
if (btn.textContent === '12dB' || btn.textContent === '24dB' || btn.textContent === 'SVF') { | |
btn.classList.remove('active'); | |
} | |
}); | |
this.classList.add('active'); | |
} | |
}); | |
}); | |
// LFO waveform buttons | |
const lfoWaveButtons = document.querySelectorAll('.panel:nth-child(3) .button:not(.tab)'); | |
lfoWaveButtons.forEach(button => { | |
button.addEventListener('click', function() { | |
lfoWaveButtons.forEach(btn => btn.classList.remove('active')); | |
this.classList.add('active'); | |
// Update LFO display | |
const newData = Array.from({length: 100}, (_, i) => { | |
const x = i/10; | |
if (this.textContent === 'Sine') return Math.sin(x); | |
else if (this.textContent === 'Square') return Math.sin(x) > 0 ? 0.8 : -0.8; | |
else if (this.textContent === 'Tri') return Math.asin(Math.sin(x)) * (2/Math.PI); | |
else if (this.textContent === 'Random') return Math.random() * 2 - 1; | |
return 0; | |
}); | |
lfo1Chart.data.datasets[0].data = newData; | |
lfo1Chart.update(); | |
}); | |
}); | |
// Tab switching | |
const tabs = document.querySelectorAll('.tab'); | |
tabs.forEach(tab => { | |
tab.addEventListener('click', function() { | |
const panel = this.closest('.panel'); | |
const tabGroup = panel.querySelectorAll('.tab'); | |
const contentGroups = panel.querySelectorAll('.wave-display, .envelope-display, .lfo-display, .wavetable-position, .grid, .flex'); | |
tabGroup.forEach(t => t.classList.remove('active')); | |
this.classList.add('active'); | |
// Hide all content first | |
contentGroups.forEach(group => { | |
group.style.display = 'none'; | |
}); | |
// Show relevant content based on tab | |
if (this.textContent === 'OSC A' || this.textContent === 'OSC B') { | |
panel.querySelector('.wave-display').style.display = 'block'; | |
panel.querySelector('.wavetable-position').style.display = 'block'; | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
panel.querySelectorAll('.flex').forEach(flex => flex.style.display = 'flex'); | |
} else if (this.textContent === 'SUB' || this.textContent === 'NOISE') { | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
panel.querySelectorAll('.flex').forEach(flex => flex.style.display = 'flex'); | |
} else if (this.textContent === 'FILTER') { | |
panel.querySelector('.wave-display').style.display = 'block'; | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
panel.querySelectorAll('.flex').forEach(flex => flex.style.display = 'flex'); | |
} else if (this.textContent === 'FX') { | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
} else if (this.textContent === 'ENV') { | |
panel.querySelector('.envelope-display').style.display = 'block'; | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
} else if (this.textContent === 'LFO 1' || this.textContent === 'LFO 2') { | |
panel.querySelector('.lfo-display').style.display = 'block'; | |
panel.querySelectorAll('.grid').forEach(grid => grid.style.display = 'grid'); | |
panel.querySelectorAll('.flex').forEach(flex => flex.style.display = 'flex'); | |
} | |
}); | |
}); | |
// Initialize first tab as active | |
document.querySelectorAll('.panel').forEach(panel => { | |
const firstTab = panel.querySelector('.tab'); | |
if (firstTab) firstTab.click(); | |
}); | |
// Wavetable position handle | |
const wtHandle = document.querySelector('.wavetable-position-handle'); | |
let isWtDragging = false; | |
wtHandle.addEventListener('mousedown', function(e) { | |
isWtDragging = true; | |
document.addEventListener('mousemove', onWtMouseMove); | |
document.addEventListener('mouseup', onWtMouseUp); | |
e.stopPropagation(); | |
}); | |
function onWtMouseMove(e) { | |
if (!isWtDragging) return; | |
const wtTrack = document.querySelector('.wavetable-position'); | |
const rect = wtTrack.getBoundingClientRect(); | |
let x = e.clientX - rect.left; | |
x = Math.max(0, Math.min(rect.width, x)); | |
const percent = (x / rect.width) * 100; | |
wtHandle.style.left = `${percent}%`; | |
// Update wavetable position in synth | |
// This would control wavetable scanning in a real implementation | |
} | |
function onWtMouseUp() { | |
isWtDragging = false; | |
document.removeEventListener('mousemove', onWtMouseMove); | |
document.removeEventListener('mouseup', onWtMouseUp); | |
} | |
}); | |
</script> | |
</body> | |
</html> |