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> |