TutSynth
Qwen 3.6 27b MTP · composer

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 36,486 bytes · SHA-256 eb1f4cd8e875
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Synth Playground — Learn Synthesis Interactively</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a12;
color: #e0e0e8;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
overflow-x: hidden;
}
#synth-app {
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
header {
text-align: center;
padding: 8px 0 4px;
}
header h1 {
font-size: 1.5em;
color: #fff;
letter-spacing: -0.5px;
}
.subtitle {
color: #6a6a8e;
font-size: 0.82em;
margin-top: 2px;
}
#progress-bar {
width: 280px;
height: 4px;
background: #1a1a2e;
border-radius: 2px;
margin: 8px auto;
overflow: hidden;
}
#progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #6c5ce7, #a855f7);
border-radius: 2px;
transition: width 0.5s ease;
}
#next-btn {
background: linear-gradient(135deg, #6c5ce7, #a855f7);
color: white;
border: none;
padding: 7px 22px;
border-radius: 20px;
font-size: 0.82em;
cursor: pointer;
transition: all 0.3s;
margin-top: 4px;
}
#next-btn:hover { transform: scale(1.05); box-shadow: 0 4px 20px rgba(108,92,231,0.4); }
#next-btn:disabled { opacity: 0.4; cursor: default; transform: none; box-shadow: none; }
#synth-body {
display: grid;
grid-template-columns: 250px 1fr;
gap: 10px;
margin-top: 6px;
}
@media (max-width: 800px) {
#synth-body { grid-template-columns: 1fr; }
}
/* Lesson Panel */
#lesson-panel {
background: linear-gradient(135deg, #1a1a2e, #16213e);
border-radius: 10px;
padding: 14px;
border: 1px solid #2a2a4a;
min-height: 180px;
}
#lesson-title {
font-size: 1em;
color: #c084fc;
margin-bottom: 6px;
line-height: 1.3;
}
#lesson-text {
font-size: 0.78em;
line-height: 1.55;
color: #9a9ab8;
}
#lesson-text b, #lesson-text strong {
color: #d4b5ff;
font-weight: 600;
}
/* Controls Panel */
#controls-panel {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
gap: 8px;
}
.control-section {
background: #12121f;
border-radius: 8px;
padding: 10px 12px;
border: 1px solid #2a2a3e;
position: relative;
overflow: hidden;
transition: all 0.5s ease;
}
.control-section.unlocked {
border-color: rgba(108,92,231,0.4);
box-shadow: 0 0 12px rgba(108,92,231,0.06);
}
.control-section.locked {
opacity: 0.35;
filter: grayscale(0.7);
}
.control-section h3 {
font-size: 0.7em;
text-transform: uppercase;
letter-spacing: 1.2px;
color: #6c5ce7;
margin-bottom: 8px;
}
.lock-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10,10,18,0.6);
z-index: 2;
pointer-events: none;
}
.lock-overlay span {
font-size: 0.7em;
color: #555;
}
.control-section.unlocked .lock-overlay { display: none; }
.section-inner { position: relative; z-index: 1; }
/* Controls */
.control-group {
margin-bottom: 8px;
}
.control-group:last-child { margin-bottom: 0; }
.control-group label {
font-size: 0.7em;
color: #6a6a8e;
display: block;
margin-bottom: 3px;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 5px;
background: #1a1a2e;
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: linear-gradient(135deg, #6c5ce7, #a855f7);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(108,92,231,0.4);
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: linear-gradient(135deg, #6c5ce7, #a855f7);
border-radius: 50%;
cursor: pointer;
border: none;
}
.btn-group {
display: flex;
gap: 3px;
flex-wrap: wrap;
}
.btn-group button {
background: #1a1a2e;
color: #6a6a8e;
border: 1px solid #2a2a3e;
padding: 3px 8px;
border-radius: 5px;
font-size: 0.7em;
cursor: pointer;
transition: all 0.2s;
}
.btn-group button.active {
background: #6c5ce7;
color: white;
border-color: #6c5ce7;
}
.btn-group button:hover:not(.active) {
border-color: #6c5ce7;
color: #bbb;
}
/* Visualizer */
#visualizer-section {
grid-column: 1 / -1;
display: flex;
gap: 8px;
}
canvas {
background: #0d0d1a;
border-radius: 6px;
border: 1px solid #2a2a3e;
}
#waveform-canvas { flex: 2; height: 90px; }
#spectrum-canvas { flex: 1; height: 90px; }
/* Keyboard */
#keyboard-section {
grid-column: 1 / -1;
text-align: center;
}
.hint {
font-size: 0.7em;
color: #555;
margin-top: 6px;
}
#piano-keyboard {
display: inline-flex;
position: relative;
height: 110px;
}
.piano-key {
width: 42px;
height: 110px;
background: linear-gradient(180deg, #f5f5fa, #e8e8f0);
border: 1px solid #bbb;
border-radius: 0 0 5px 5px;
cursor: pointer;
position: relative;
z-index: 1;
transition: background 0.05s, box-shadow 0.05s;
}
.piano-key.black {
width: 30px;
height: 68px;
background: linear-gradient(180deg, #2a2a3e, #1a1a2e);
border-color: #111;
margin-left: -15px;
margin-right: -15px;
z-index: 2;
}
.piano-key.active {
background: linear-gradient(180deg, #c084fc, #a855f7) !important;
box-shadow: 0 0 15px rgba(168,85,247,0.5);
}
.piano-key.black.active {
background: linear-gradient(180deg, #d8b4fe, #c084fc) !important;
}
.key-label {
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
font-size: 0.6em;
color: #999;
pointer-events: none;
user-select: none;
}
.piano-key.black .key-label {
color: #777;
bottom: 4px;
}
/* Start Overlay */
#start-overlay {
position: fixed;
inset: 0;
background: rgba(10,10,18,0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.overlay-content {
text-align: center;
max-width: 460px;
padding: 20px;
}
.overlay-content h1 { font-size: 2em; margin-bottom: 12px; }
.overlay-content p { color: #8888aa; line-height: 1.6; margin-bottom: 24px; font-size: 0.95em; }
.big-btn {
background: linear-gradient(135deg, #6c5ce7, #a855f7);
color: white;
border: none;
padding: 14px 40px;
border-radius: 30px;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s;
}
.big-btn:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(108,92,231,0.5); }
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="synth-app">
<header>
<h1>🎹 Synth Playground</h1>
<p class="subtitle">Learn synthesis by building sounds — one concept at a time</p>
<div id="progress-bar"><div id="progress-fill"></div></div>
<button id="next-btn" class="action-btn">Next Concept →</button>
</header>
<main id="synth-body">
<section id="lesson-panel">
<div id="lesson-content">
<h2 id="lesson-title"></h2>
<p id="lesson-text"></p>
<div id="lesson-demo" class="hidden"></div>
</div>
</section>
<section id="controls-panel">
<div id="osc-section" class="control-section locked">
<h3>Oscillators</h3>
<div class="lock-overlay"><span>🔒 Unlock this concept first</span></div>
<div class="section-inner">
<div class="control-group">
<label>Waveform</label>
<div id="wave-select" class="btn-group">
<button data-wave="sine" class="active">Sine</button>
<button data-wave="square">Square</button>
<button data-wave="sawtooth">Saw</button>
<button data-wave="triangle">Triangle</button>
</div>
</div>
<div class="control-group">
<label for="osc-octave">Octave: <span id="octave-val">4</span></label>
<input type="range" id="osc-octave" min="-2" max="5" value="4" step="1">
</div>
</div>
</div>
<div id="filter-section" class="control-section locked">
<h3>Filter</h3>
<div class="lock-overlay"><span>🔒 Unlock this concept first</span></div>
<div class="section-inner">
<div class="control-group">
<label for="filter-cutoff">Cutoff: <span id="cutoff-val">2000</span> Hz</label>
<input type="range" id="filter-cutoff" min="100" max="15000" value="2000" step="10">
</div>
<div class="control-group">
<label for="filter-res">Resonance: <span id="res-val">1</span></label>
<input type="range" id="filter-res" min="0.1" max="25" value="1" step="0.1">
</div>
<div class="control-group">
<label>Type</label>
<div id="filter-type-select" class="btn-group">
<button data-ftype="lowpass" class="active">Low Pass</button>
<button data-ftype="highpass">High Pass</button>
<button data-ftype="bandpass">Band Pass</button>
</div>
</div>
</div>
</div>
<div id="env-section" class="control-section locked">
<h3>Envelope (ADSR)</h3>
<div class="lock-overlay"><span>🔒 Unlock this concept first</span></div>
<div class="section-inner">
<div class="control-group">
<label for="env-attack">Attack: <span id="atk-val">0.01</span>s</label>
<input type="range" id="env-attack" min="0.001" max="2" value="0.01" step="0.001">
</div>
<div class="control-group">
<label for="env-decay">Decay: <span id="dec-val">0.3</span>s</label>
<input type="range" id="env-decay" min="0.01" max="2" value="0.3" step="0.01">
</div>
<div class="control-group">
<label for="env-sustain">Sustain: <span id="sus-val">0.7</span></label>
<input type="range" id="env-sustain" min="0" max="1" value="0.7" step="0.01">
</div>
<div class="control-group">
<label for="env-release">Release: <span id="rel-val">0.5</span>s</label>
<input type="range" id="env-release" min="0.01" max="3" value="0.5" step="0.01">
</div>
</div>
</div>
<div id="lfo-section" class="control-section locked">
<h3>LFO (Vibrato)</h3>
<div class="lock-overlay"><span>🔒 Unlock this concept first</span></div>
<div class="section-inner">
<div class="control-group">
<label for="lfo-rate">Rate: <span id="lfo-rate-val">5</span> Hz</label>
<input type="range" id="lfo-rate" min="0.1" max="20" value="5" step="0.1">
</div>
<div class="control-group">
<label for="lfo-depth">Depth: <span id="lfo-depth-val">0</span></label>
<input type="range" id="lfo-depth" min="0" max="50" value="0" step="1">
</div>
</div>
</div>
<div id="fx-section" class="control-section locked">
<h3>Effects</h3>
<div class="lock-overlay"><span>🔒 Unlock this concept first</span></div>
<div class="section-inner">
<div class="control-group">
<label for="delay-time">Delay Time: <span id="delay-val">0.3</span>s</label>
<input type="range" id="delay-time" min="0" max="1" value="0.3" step="0.01">
</div>
<div class="control-group">
<label for="delay-feedback">Feedback: <span id="feedback-val">0.4</span></label>
<input type="range" id="delay-feedback" min="0" max="0.9" value="0.4" step="0.01">
</div>
<div class="control-group">
<label for="reverb-mix">Reverb Mix: <span id="reverb-val">0</span></label>
<input type="range" id="reverb-mix" min="0" max="1" value="0" step="0.01">
</div>
</div>
</div>
<div id="master-section" class="control-section unlocked">
<h3>Master</h3>
<div class="section-inner">
<div class="control-group">
<label for="master-vol">Volume: <span id="vol-val">0.5</span></label>
<input type="range" id="master-vol" min="0" max="1" value="0.5" step="0.01">
</div>
</div>
</div>
</section>
<section id="visualizer-section">
<canvas id="waveform-canvas"></canvas>
<canvas id="spectrum-canvas"></canvas>
</section>
<section id="keyboard-section">
<div id="piano-keyboard"></div>
<p class="hint">Keyboard: A S D F G H J K L ; (white) | W E T Y U O P (black)</p>
</section>
</main>
<div id="start-overlay">
<div class="overlay-content">
<h1>🎹 Synth Playground</h1>
<p>An interactive guide to understanding synthesizers. Each concept unlocks progressively — learn by doing.</p>
<button id="start-btn" class="big-btn">Start Learning →</button>
</div>
</div>
</div>
<script>
'use strict';
// ── const: NOTE_FREQS ──
const NOTE_FREQS = {
'C3': 130.81, 'C#3': 138.59, 'D3': 146.83, 'D#3': 155.56, 'E3': 164.81,
'F3': 174.61, 'F#3': 185.00, 'G3': 196.00, 'G#3': 207.65, 'A3': 220.00,
'A#3': 233.08, 'B3': 246.94,
'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'E4': 329.63,
'F4': 349.23, 'F#4': 369.99, 'G4': 392.00, 'G#4': 415.30, 'A4': 440.00,
'A#4': 466.16, 'B4': 493.88,
'C5': 523.25, 'C#5': 554.37, 'D5': 587.33, 'D#5': 622.25, 'E5': 659.25,
};
// ── const: KEY_MAP ──
const KEY_MAP = {
'a': 'C4', 'w': 'C#4', 's': 'D4', 'e': 'D#4', 'd': 'E4',
'f': 'F4', 't': 'F#4', 'g': 'G4', 'y': 'G#4', 'h': 'A4',
'u': 'A#4', 'j': 'B4', 'k': 'C5', 'o': 'C#5', 'l': 'D5',
'p': 'D#5', ';': 'E5'
};
// ── const: WHITE_KEYS ──
const WHITE_KEYS = ['C4','D4','E4','F4','G4','A4','B4','C5','D5','E5'];
// ── const: BLACK_KEY_MAP ──
const BLACK_KEY_MAP = { 'C4':'C#4','D4':'D#4','F4':'F#4','G4':'G#4','A4':'A#4' };
// ── class: AudioEngine ──
class AudioEngine {
constructor() {
this.ctx = null;
this.masterGain = null;
this.filterNode = null;
this.delayNode = null;
this.feedbackGain = null;
this.dryGain = null;
this.wetGain = null;
this.convolver = null;
this.reverbDry = null;
this.reverbWet = null;
this.lfoOsc = null;
this.lfoGain = null;
this.analyserWave = null;
this.analyserSpec = null;
this.activeVoices = {};
this.settings = {
waveform: 'sine',
octave: 4,
filterCutoff: 2000,
filterRes: 1,
filterType: 'lowpass',
attack: 0.01,
decay: 0.3,
sustain: 0.7,
release: 0.5,
lfoRate: 5,
lfoDepth: 0,
delayTime: 0.3,
feedback: 0.4,
reverbMix: 0,
volume: 0.5,
};
}
init() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
// Master gain
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = this.settings.volume;
// Analyser nodes for visualization
this.analyserWave = this.ctx.createAnalyser();
this.analyserWave.fftSize = 2048;
this.analyserSpec = this.ctx.createAnalyser();
this.analyserSpec.fftSize = 2048;
// Filter
this.filterNode = this.ctx.createBiquadFilter();
this.filterNode.type = 'lowpass';
this.filterNode.frequency.value = this.settings.filterCutoff;
this.filterNode.Q.value = this.settings.filterRes;
// Delay effect chain
this.delayNode = this.ctx.createDelay(2.0);
this.delayNode.delayTime.value = this.settings.delayTime;
this.feedbackGain = this.ctx.createGain();
this.feedbackGain.gain.value = this.settings.feedback;
this.dryGain = this.ctx.createGain();
this.dryGain.gain.value = 1;
this.wetGain = this.ctx.createGain();
this.wetGain.gain.value = 0.5;
// Reverb (convolver with generated impulse)
this.convolver = this.ctx.createConvolver();
this.reverbDry = this.ctx.createGain();
this.reverbDry.gain.value = 1;
this.reverbWet = this.ctx.createGain();
this.reverbWet.gain.value = this.settings.reverbMix;
this._generateReverbImpulse(2.0, 3);
// LFO for vibrato
this.lfoOsc = this.ctx.createOscillator();
this.lfoOsc.type = 'sine';
this.lfoOsc.frequency.value = this.settings.lfoRate;
this.lfoGain = this.ctx.createGain();
this.lfoGain.gain.value = 0; // depth controlled per voice
this.lfoOsc.start();
// Routing: voices -> filter -> [delay chain] -> [reverb chain] -> master -> analysers -> destination
this.filterNode.connect(this.dryGain);
this.delayNode.connect(this.feedbackGain);
this.feedbackGain.connect(this.delayNode);
this.delayNode.connect(this.wetGain);
// Dry + wet delay mix into reverb chain
const delayMix = this.ctx.createGain();
this.dryGain.connect(delayMix);
this.wetGain.connect(delayMix);
// Reverb routing
delayMix.connect(this.reverbDry);
delayMix.connect(this.convolver);
this.convolver.connect(this.reverbWet);
const reverbMix = this.ctx.createGain();
this.reverbDry.connect(reverbMix);
this.reverbWet.connect(reverbMix);
// Final chain
reverbMix.connect(this.masterGain);
this.masterGain.connect(this.analyserWave);
this.analyserWave.connect(this.analyserSpec);
this.analyserSpec.connect(this.ctx.destination);
}
_generateReverbImpulse(duration, decay) {
const rate = this.ctx.sampleRate;
const length = rate * duration;
const impulse = this.ctx.createBuffer(2, length, rate);
for (let ch = 0; ch < 2; ch++) {
const data = impulse.getChannelData(ch);
for (let i = 0; i < length; i++) {
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
}
}
this.convolver.buffer = impulse;
}
noteOn(noteName) {
if (!this.ctx) return;
if (this.activeVoices[noteName]) return; // already playing
const freq = NOTE_FREQS[noteName];
if (!freq) return;
const now = this.ctx.currentTime;
// Create voice: oscillator -> gain (envelope) -> filter
const osc = this.ctx.createOscillator();
osc.type = this.settings.waveform;
osc.frequency.value = freq;
// Apply octave shift
const octShift = Math.pow(2, this.settings.octave - 4);
osc.frequency.value *= octShift;
// Envelope gain
const envGain = this.ctx.createGain();
envGain.gain.setValueAtTime(0, now);
envGain.gain.linearRampToValueAtTime(1, now + this.settings.attack);
envGain.gain.linearRampToValueAtTime(this.settings.sustain, now + this.settings.attack + this.settings.decay);
// LFO modulation (vibrato)
if (this.settings.lfoDepth > 0) {
const lfoMod = this.ctx.createGain();
lfoMod.gain.value = this.settings.lfoDepth;
this.lfoOsc.connect(lfoMod);
lfoMod.connect(osc.frequency);
}
osc.connect(envGain);
envGain.connect(this.filterNode);
osc.start(now);
this.activeVoices[noteName] = { osc, envGain };
}
noteOff(noteName) {
const voice = this.activeVoices[noteName];
if (!voice) return;
const now = this.ctx.currentTime;
const { osc, envGain } = voice;
// Release phase
envGain.gain.cancelScheduledValues(now);
envGain.gain.setValueAtTime(envGain.gain.value, now);
envGain.gain.linearRampToValueAtTime(0, now + this.settings.release);
osc.stop(now + this.settings.release + 0.1);
// Disconnect LFO mod to avoid memory leak
if (this.lfoOsc) {
setTimeout(() => {
try { osc.disconnect(); envGain.disconnect(); } catch(e) {}
}, (this.settings.release + 0.2) * 1000);
}
delete this.activeVoices[noteName];
}
setSetting(key, value) {
this.settings[key] = value;
switch(key) {
case 'volume':
if (this.masterGain) this.masterGain.gain.value = value;
break;
case 'filterCutoff':
if (this.filterNode) this.filterNode.frequency.value = value;
break;
case 'filterRes':
if (this.filterNode) this.filterNode.Q.value = value;
break;
case 'filterType':
if (this.filterNode) this.filterNode.type = value;
break;
case 'delayTime':
if (this.delayNode) this.delayNode.delayTime.value = value;
break;
case 'feedback':
if (this.feedbackGain) this.feedbackGain.gain.value = value;
break;
case 'reverbMix':
if (this.reverbWet) this.reverbWet.gain.value = value;
break;
case 'lfoRate':
if (this.lfoOsc) this.lfoOsc.frequency.value = value;
break;
}
}
getWaveformData() {
if (!this.analyserWave) return null;
const data = new Uint8Array(this.analyserWave.fftSize);
this.analyserWave.getByteTimeDomainData(data);
return data;
}
getSpectrumData() {
if (!this.analyserSpec) return null;
const data = new Uint8Array(this.analyserSpec.frequencyBinCount);
this.analyserSpec.getByteFrequencyData(data);
return data;
}
}
// ── class: Visualizer ──
class Visualizer {
constructor(waveCanvas, specCanvas) {
this.waveCanvas = waveCanvas;
this.specCanvas = specCanvas;
this.waveCtx = waveCanvas.getContext('2d');
this.specCtx = specCanvas.getContext('2d');
this.resize();
}
resize() {
const dpr = window.devicePixelRatio || 1;
for (const canvas of [this.waveCanvas, this.specCanvas]) {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
}
this.waveW = this.waveCanvas.getBoundingClientRect().width;
this.waveH = this.waveCanvas.getBoundingClientRect().height;
this.specW = this.specCanvas.getBoundingClientRect().width;
this.specH = this.specCanvas.getBoundingClientRect().height;
}
drawWaveform(data) {
const ctx = this.waveCtx;
const w = this.waveW;
const h = this.waveH;
ctx.clearRect(0, 0, w, h);
// Background grid
ctx.strokeStyle = '#1a1a2e';
ctx.lineWidth = 1;
for (let y = 0; y < h; y += h/4) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// Center line
ctx.strokeStyle = '#2a2a4e';
ctx.beginPath();
ctx.moveTo(0, h/2);
ctx.lineTo(w, h/2);
ctx.stroke();
if (!data) return;
// Waveform
const gradient = ctx.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, '#6c5ce7');
gradient.addColorStop(0.5, '#a855f7');
gradient.addColorStop(1, '#ec4899');
ctx.strokeStyle = gradient;
ctx.lineWidth = 2;
ctx.beginPath();
const sliceWidth = w / data.length;
let x = 0;
for (let i = 0; i < data.length; i++) {
const v = data[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
x += sliceWidth;
}
ctx.stroke();
// Glow effect
ctx.shadowColor = '#a855f7';
ctx.shadowBlur = 10;
ctx.strokeStyle = 'rgba(168, 85, 247, 0.3)';
ctx.lineWidth = 4;
ctx.beginPath();
x = 0;
for (let i = 0; i < data.length; i++) {
const v = data[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
x += sliceWidth;
}
ctx.stroke();
ctx.shadowBlur = 0;
}
drawSpectrum(data) {
const ctx = this.specCtx;
const w = this.specW;
const h = this.specH;
ctx.clearRect(0, 0, w, h);
if (!data) return;
const barCount = Math.min(64, data.length / 4);
const barWidth = (w / barCount) - 1;
for (let i = 0; i < barCount; i++) {
const idx = Math.floor(i * data.length / barCount);
const val = data[idx] / 255.0;
const barHeight = val * h * 0.9;
const hue = 260 + (i / barCount) * 80;
ctx.fillStyle = `hsla(${hue}, 70%, ${40 + val * 30}%, ${0.5 + val * 0.5})`;
const x = i * (barWidth + 1);
ctx.fillRect(x, h - barHeight, barWidth, barHeight);
}
}
animate(engine) {
const waveData = engine.getWaveformData();
const specData = engine.getSpectrumData();
this.drawWaveform(waveData);
this.drawSpectrum(specData);
requestAnimationFrame(() => this.animate(engine));
}
}
// ── const: LESSONS ──
const LESSONS = [
{
id: 'welcome',
title: '🎵 Welcome to Synth Playground!',
text: `A synthesizer creates sound electronically. At its heart is an <b>oscillator</b> — a circuit that generates a repeating waveform at a specific frequency (pitch).
Click "Next Concept" to start exploring, or play the keyboard below with your mouse or computer keys!`,
unlocks: null,
},
{
id: 'oscillators',
title: '🎶 Oscillators — The Sound Source',
text: `Every synth sound starts with a waveform. Different waveforms have different <b>harmonic content</b>:
• <b>Sine</b> — Pure, smooth tone (no harmonics)
• <b>Square</b> — Hollow, flute-like (odd harmonics)
• <b>Sawtooth</b> — Bright, buzzy (all harmonics)
• <b>Triangle</b> — Soft, mellow (weak odd harmonics)
<b>Try it:</b> Switch between waveforms and play the same note. Notice how each sounds different even at the same pitch!`,
unlocks: 'osc-section',
},
{
id: 'filter',
title: '🔊 Filter — Shaping the Tone',
text: `A <b>filter</b> removes certain frequencies from your sound, like a sieve for audio.
• <b>Cutoff</b> — The frequency boundary. Frequencies above (lowpass) or below (highpass) this point are reduced.
• <b>Resonance</b> — Boosts the cutoff frequency, creating an emphasized "peak" at that point. High resonance = more character!
<b>Try it:</b> Use a sawtooth wave, then sweep the cutoff from low to high. You'll hear the sound go from dark/muffled to bright/full.`,
unlocks: 'filter-section',
},
{
id: 'envelope',
title: '📈 Envelope (ADSR) — Shaping Over Time',
text: `An <b>envelope</b> controls how the volume changes over time when you press and release a key. ADSR stands for:
• <b>A</b>ttack — How long to reach full volume after pressing
• <b>D</b>ecay — How long to drop from peak to sustain level
• <b>S</b>ustain — The volume held while the key is pressed
• <b>R</b>elease — How long to fade out after releasing
<b>Try it:</b> Set attack high for a "swelling" sound, or set release high for a lingering tail. A piano has fast attack and medium release; a pad has slow attack and long release.`,
unlocks: 'env-section',
},
{
id: 'lfo',
title: '〰️ LFO — Low-Frequency Oscillation',
text: `An <b>LFO</b> is an oscillator that runs too slowly to hear as a pitch. Instead, it <b>modulates</b> (wiggles) other parameters.
Here we use the LFO to modulate pitch, creating <b>vibrato</b> — a subtle periodic pitch variation that makes sounds feel alive and expressive.
• <b>Rate</b> — How fast the vibrato oscillates
• <b>Depth</b> — How much the pitch wiggles
<b>Try it:</b> Hold a note and slowly increase the depth. You'll hear the pitch start to "wobble" like a singer's vibrato!`,
unlocks: 'lfo-section',
},
{
id: 'effects',
title: '✨ Effects — Space and Depth',
text: `Effects add dimension to your sound by simulating physical spaces or creating rhythmic echoes.
• <b>Delay</b> — Repeats the signal after a time interval. <b>Feedback</b> controls how many repeats you get.
• <b>Reverb</b> — Simulates the natural reflections of a room, hall, or space. Adds depth and "air."
<b>Try it:</b> Hold a note with a short envelope (fast attack/decay) and add delay. You'll hear rhythmic echoes! Add reverb for a sense of space.`,
unlocks: 'fx-section',
},
{
id: 'complete',
title: '🎉 You\'ve Unlocked Everything!',
text: `You now understand the core building blocks of synthesis:
1. <b>Oscillators</b> — Generate the raw sound
2. <b>Filters</b> — Shape which frequencies pass through
3. <b>Envelopes</b> — Control how sound evolves over time
4. <b>LFOs</b> — Add movement and modulation
5. <b>Effects</b> — Create space and depth
<b>Now experiment!</b> Combine all these controls to create your own sounds. Try making a bass, a pad, a lead, or something entirely new!`,
unlocks: null,
},
];
// ── class: SynthUI ──
class SynthUI {
constructor(engine) {
this.engine = engine;
this.currentLesson = 0;
this.wireControls();
this.showLesson(0);
}
wireControls() {
// Waveform buttons
document.querySelectorAll('#wave-select button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#wave-select button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.engine.setSetting('waveform', btn.dataset.wave);
});
});
// Filter type buttons
document.querySelectorAll('#filter-type-select button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#filter-type-select button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.engine.setSetting('filterType', btn.dataset.ftype);
});
});
// Range sliders with value display
const sliderMap = [
['osc-octave', 'octave-val', 'octave'],
['filter-cutoff', 'cutoff-val', 'filterCutoff'],
['filter-res', 'res-val', 'filterRes'],
['env-attack', 'atk-val', 'attack'],
['env-decay', 'dec-val', 'decay'],
['env-sustain', 'sus-val', 'sustain'],
['env-release', 'rel-val', 'release'],
['lfo-rate', 'lfo-rate-val', 'lfoRate'],
['lfo-depth', 'lfo-depth-val', 'lfoDepth'],
['delay-time', 'delay-val', 'delayTime'],
['delay-feedback', 'feedback-val', 'feedback'],
['reverb-mix', 'reverb-val', 'reverbMix'],
['master-vol', 'vol-val', 'volume'],
];
sliderMap.forEach(([sliderId, displayId, settingKey]) => {
const slider = document.getElementById(sliderId);
const display = document.getElementById(displayId);
if (slider && display) {
slider.addEventListener('input', () => {
const val = parseFloat(slider.value);
display.textContent = Number.isInteger(val) ? val : val.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
this.engine.setSetting(settingKey, val);
});
}
});
// Next button
document.getElementById('next-btn').addEventListener('click', () => {
if (this.currentLesson < LESSONS.length - 1) {
this.currentLesson++;
this.showLesson(this.currentLesson);
}
});
}
showLesson(index) {
const lesson = LESSONS[index];
document.getElementById('lesson-title').innerHTML = lesson.title;
document.getElementById('lesson-text').innerHTML = lesson.text;
// Update progress bar
const pct = (index / (LESSONS.length - 1)) * 100;
document.getElementById('progress-fill').style.width = pct + '%';
// Unlock section if applicable
if (lesson.unlocks) {
const section = document.getElementById(lesson.unlocks);
if (section) {
section.classList.remove('locked');
section.classList.add('unlocked');
// Animate unlock
section.style.transform = 'scale(1.03)';
setTimeout(() => { section.style.transform = ''; }, 300);
}
}
// Update next button state
const nextBtn = document.getElementById('next-btn');
if (index >= LESSONS.length - 1) {
nextBtn.textContent = '🎉 All Concepts Unlocked!';
nextBtn.disabled = true;
} else {
nextBtn.textContent = 'Next Concept →';
nextBtn.disabled = false;
}
}
advanceLesson() {
if (this.currentLesson < LESSONS.length - 1) {
this.currentLesson++;
this.showLesson(this.currentLesson);
}
}
}
// ── const: REVERSE_KEY_MAP ──
const REVERSE_KEY_MAP = {
'C4': 'a', 'C#4': 'w', 'D4': 's', 'D#4': 'e', 'E4': 'd',
'F4': 'f', 'F#4': 't', 'G4': 'g', 'G#4': 'y', 'A4': 'h',
'A#4': 'u', 'B4': 'j', 'C5': 'k', 'C#5': 'o', 'D5': 'l',
'D#5': 'p', 'E5': ';'
};
// ── const: NOTE_ORDER ──
const NOTE_ORDER = [
'C4', 'C#4', 'D4', 'D#4', 'E4',
'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4',
'C5', 'C#5', 'D5', 'D#5', 'E5'
];
// ── class: PianoKeyboard ──
class PianoKeyboard {
constructor(container) {
this.container = container;
this.keyElements = {};
this.activeKeys = new Set();
this.onNoteOn = null;
this.onNoteOff = null;
this.build();
}
build() {
this.container.innerHTML = '';
NOTE_ORDER.forEach(note => {
const key = document.createElement('div');
const isBlack = note.includes('#');
key.className = `piano-key${isBlack ? ' black' : ''}`;
key.dataset.note = note;
// Add keyboard shortcut label
const shortcutKey = REVERSE_KEY_MAP[note];
if (shortcutKey) {
const label = document.createElement('span');
label.className = 'key-label';
label.textContent = shortcutKey.toUpperCase();
key.appendChild(label);
}
const handleDown = (e) => { e.preventDefault(); this.pressKey(note); };
const handleUp = (e) => { e.preventDefault(); this.releaseKey(note); };
key.addEventListener('mousedown', handleDown);
key.addEventListener('mouseup', handleUp);
key.addEventListener('mouseleave', handleUp);
key.addEventListener('touchstart', handleDown, { passive: false });
key.addEventListener('touchend', handleUp, { passive: false });
this.container.appendChild(key);
this.keyElements[note] = key;
});
}
pressKey(note) {
if (this.activeKeys.has(note)) return;
this.activeKeys.add(note);
const el = this.keyElements[note];
if (el) el.classList.add('active');
if (this.onNoteOn) this.onNoteOn(note);
}
releaseKey(note) {
this.activeKeys.delete(note);
const el = this.keyElements[note];
if (el) el.classList.remove('active');
if (this.onNoteOff) this.onNoteOff(note);
}
clearAll() {
[...this.activeKeys].forEach(n => this.releaseKey(n));
}
}
// ── main ──
(function() {
const engine = new AudioEngine();
let ui, keyboard, visualizer;
let started = false;
// Build piano keyboard
const kbContainer = document.getElementById('piano-keyboard');
keyboard = new PianoKeyboard(kbContainer);
keyboard.onNoteOn = (note) => engine.noteOn(note);
keyboard.onNoteOff = (note) => engine.noteOff(note);
// Computer keyboard handling
const pressedKeys = new Set();
document.addEventListener('keydown', (e) => {
if (!started) return;
const key = e.key.toLowerCase();
if (pressedKeys.has(key)) return;
pressedKeys.add(key);
const note = KEY_MAP[key];
if (note) {
e.preventDefault();
engine.noteOn(note);
keyboard.pressKey(note);
}
});
document.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
pressedKeys.delete(key);
const note = KEY_MAP[key];
if (note) {
engine.noteOff(note);
keyboard.releaseKey(note);
}
});
// Start button
document.getElementById('start-btn').addEventListener('click', () => {
started = true;
engine.init();
ui = new SynthUI(engine);
visualizer = new Visualizer(
document.getElementById('waveform-canvas'),
document.getElementById('spectrum-canvas')
);
visualizer.animate(engine);
// Hide overlay with animation
const overlay = document.getElementById('start-overlay');
overlay.style.transition = 'opacity 0.5s';
overlay.style.opacity = '0';
setTimeout(() => { overlay.style.display = 'none'; }, 500);
// Unlock first lesson section (welcome doesn't unlock anything, next does)
});
// Handle resize for visualizer
window.addEventListener('resize', () => {
if (visualizer) visualizer.resize();
});
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"14fcee56-7ac2-49c6-9873-92fa8796a678","tokensIn":542535,"tokensOut":24288,"tokensTotal":566823,"turns":27,"toolCalls":26,"failedToolCalls":3,"timestamp":"2026-07-16T01:22:29.875Z"} -->