WaveSynth / index.html
namelessai's picture
Update index.html
9429448 verified
<!DOCTYPE html>
<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>