← TutSynth results

TutSynth

Qwen 3.6 35b a3b · full

Initial view of TutSynth

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 59,345 bytes · SHA-256 5fbca9eec809
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synth Playground — Learn How Synths Work</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');

  :root {
    --bg: #0a0a0f;
    --panel: #12121a;
    --panel-border: #1e1e2e;
    --accent: #6c5ce7;
    --accent-glow: #a29bfe;
    --accent-dim: #4a3db5;
    --green: #00cec9;
    --orange: #fdcb6e;
    --pink: #fd79a8;
    --red: #ff6b6b;
    --text: #e2e2e8;
    --text-dim: #8888a0;
    --text-bright: #ffffff;
    --knob-size: 64px;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'Inter', sans-serif;
    overflow-x: hidden;
    min-height: 100vh;
  }

  /* ── Header ── */
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 24px;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
  }
  .header h1 {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-bright);
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .header h1 .icon { font-size: 22px; }
  .header-controls { display: flex; gap: 8px; align-items: center; }

  .btn {
    padding: 8px 16px;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    background: var(--panel);
    color: var(--text);
    font-family: 'Inter', sans-serif;
    font-size: 13px;
    cursor: pointer;
    transition: all 0.2s;
  }
  .btn:hover { border-color: var(--accent); color: var(--accent-glow); }
  .btn.active { background: var(--accent); border-color: var(--accent); color: white; }
  .btn-accent {
    background: var(--accent);
    border-color: var(--accent);
    color: white;
    font-weight: 500;
  }
  .btn-accent:hover { background: var(--accent-dim); }

  /* ── Layout ── */
  .app { display: flex; height: calc(100vh - 57px); }

  /* ── Tutorial Panel ── */
  .tutorial-panel {
    width: 320px;
    min-width: 320px;
    background: var(--panel);
    border-right: 1px solid var(--panel-border);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transition: width 0.3s;
  }
  .tutorial-panel.collapsed { width: 0; min-width: 0; border: none; }

  .tutorial-header {
    padding: 16px;
    border-bottom: 1px solid var(--panel-border);
    font-weight: 600;
    font-size: 14px;
    color: var(--text-bright);
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .tutorial-progress {
    padding: 12px 16px;
    border-bottom: 1px solid var(--panel-border);
  }
  .progress-bar {
    height: 4px;
    background: var(--panel-border);
    border-radius: 2px;
    overflow: hidden;
    margin-bottom: 6px;
  }
  .progress-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--green));
    border-radius: 2px;
    transition: width 0.4s;
  }
  .progress-text { font-size: 11px; color: var(--text-dim); }

  .tutorial-content {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
  }
  .tutorial-content::-webkit-scrollbar { width: 4px; }
  .tutorial-content::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 2px; }

  .lesson {
    margin-bottom: 20px;
    padding: 14px;
    border-radius: 10px;
    border: 1px solid var(--panel-border);
    background: rgba(255,255,255,0.02);
    transition: all 0.3s;
  }
  .lesson.locked { opacity: 0.35; pointer-events: none; }
  .lesson.active {
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.08);
    box-shadow: 0 0 20px rgba(108, 92, 231, 0.1);
  }
  .lesson.completed { border-color: var(--green); }

  .lesson-num {
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--accent-glow);
    margin-bottom: 6px;
  }
  .lesson.completed .lesson-num { color: var(--green); }
  .lesson.locked .lesson-num { color: var(--text-dim); }

  .lesson h3 {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-bright);
    margin-bottom: 8px;
  }
  .lesson p {
    font-size: 12px;
    line-height: 1.6;
    color: var(--text-dim);
    margin-bottom: 10px;
  }
  .lesson .tip {
    font-size: 11px;
    padding: 8px 10px;
    background: rgba(0, 206, 201, 0.08);
    border-left: 2px solid var(--green);
    border-radius: 0 6px 6px 0;
    color: var(--green);
    line-height: 1.5;
  }
  .lesson .action-btn {
    display: inline-block;
    margin-top: 8px;
    padding: 6px 14px;
    background: var(--accent);
    border: none;
    border-radius: 6px;
    color: white;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
  }
  .lesson .action-btn:hover { background: var(--accent-dim); }

  /* ── Synth Area ── */
  .synth-area {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
  }
  .synth-area::-webkit-scrollbar { width: 6px; }
  .synth-area::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 3px; }

  /* ── Visualizer ── */
  .visualizer {
    height: 160px;
    background: var(--panel);
    border-bottom: 1px solid var(--panel-border);
    position: relative;
    flex-shrink: 0;
  }
  .visualizer canvas { width: 100%; height: 100%; }
  .viz-label {
    position: absolute;
    top: 8px;
    left: 12px;
    font-size: 10px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-dim);
    background: rgba(0,0,0,0.4);
    padding: 3px 8px;
    border-radius: 4px;
  }

  /* ── Synth Panels ── */
  .synth-body { padding: 20px; }
  .synth-row {
    display: flex;
    gap: 16px;
    margin-bottom: 16px;
    flex-wrap: wrap;
  }

  .synth-panel {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 12px;
    padding: 16px;
    position: relative;
  }
  .synth-panel .panel-label {
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--text-dim);
    margin-bottom: 14px;
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .panel-dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--accent);
  }
  .synth-panel.active .panel-dot { background: var(--green); box-shadow: 0 0 8px var(--green); }

  /* ── Waveform Selector ── */
  .waveform-selector {
    display: flex;
    gap: 6px;
  }
  .waveform-btn {
    width: 56px;
    height: 56px;
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    background: rgba(255,255,255,0.02);
    color: var(--text-dim);
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    transition: all 0.2s;
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
  }
  .waveform-btn:hover { border-color: var(--accent); color: var(--accent-glow); }
  .waveform-btn.selected {
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.15);
    color: var(--accent-glow);
  }
  .waveform-btn svg { width: 36px; height: 24px; }

  /* ── Controls Row ── */
  .controls-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 16px;
  }
  .control {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
  }
  .control-label {
    font-size: 10px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-dim);
    text-align: center;
  }
  .control-value {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    color: var(--accent-glow);
    background: rgba(108, 92, 231, 0.1);
    padding: 2px 8px;
    border-radius: 4px;
  }

  /* ── Knob ── */
  .knob-container {
    position: relative;
    width: var(--knob-size);
    height: var(--knob-size);
  }
  .knob-svg { width: 100%; height: 100%; }
  .knob-track {
    fill: none;
    stroke: var(--panel-border);
    stroke-width: 4;
  }
  .knob-fill {
    fill: none;
    stroke: var(--accent);
    stroke-width: 4;
    stroke-linecap: round;
    transition: stroke-dashoffset 0.1s;
  }
  .knob-fill.green { stroke: var(--green); }
  .knob-fill.orange { stroke: var(--orange); }
  .knob-fill.pink { stroke: var(--pink); }
  .knob-indicator {
    fill: var(--accent-glow);
    transition: cx 0.1s, cy 0.1s;
  }
  .knob-indicator.green { fill: var(--green); }
  .knob-indicator.orange { fill: var(--orange); }
  .knob-indicator.pink { fill: var(--pink); }
  .knob-center {
    fill: var(--panel);
    stroke: var(--panel-border);
    stroke-width: 1;
  }
  .knob-dragging .knob-fill { stroke: var(--orange); }
  .knob-dragging .knob-indicator { fill: var(--orange); }

  /* ── Range Slider ── */
  .range-control {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .range-track {
    position: relative;
    height: 6px;
    background: var(--panel-border);
    border-radius: 3px;
    cursor: pointer;
  }
  .range-fill {
    position: absolute;
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--accent-glow));
    border-radius: 3px;
    pointer-events: none;
  }
  .range-thumb {
    position: absolute;
    width: 16px;
    height: 16px;
    background: white;
    border-radius: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    cursor: grab;
    box-shadow: 0 0 8px rgba(108, 92, 231, 0.4);
    pointer-events: none;
  }
  input[type="range"] {
    -webkit-appearance: none;
    width: 100%;
    height: 6px;
    background: transparent;
    outline: none;
    cursor: pointer;
  }
  input[type="range"]::-webkit-slider-runnable-track {
    height: 6px;
    background: var(--panel-border);
    border-radius: 3px;
  }
  input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    background: white;
    border-radius: 50%;
    margin-top: -5px;
    box-shadow: 0 0 8px rgba(108, 92, 231, 0.4);
    cursor: grab;
  }

  /* ── ADSR Visualization ── */
  .adsr-visual {
    width: 100%;
    height: 80px;
    margin-top: 8px;
    background: rgba(0,0,0,0.2);
    border-radius: 8px;
    overflow: hidden;
  }
  .adsr-visual canvas { width: 100%; height: 100%; }

  /* ── Keyboard ── */
  .keyboard-container {
    background: var(--panel);
    border-top: 1px solid var(--panel-border);
    padding: 16px 20px 20px;
    flex-shrink: 0;
  }
  .keyboard-label {
    font-size: 10px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-dim);
    margin-bottom: 10px;
    text-align: center;
  }
  .keyboard {
    display: flex;
    justify-content: center;
    position: relative;
    height: 140px;
    user-select: none;
  }
  .key {
    position: relative;
    cursor: pointer;
    transition: all 0.05s;
  }
  .key-white {
    width: 44px;
    height: 140px;
    background: linear-gradient(180deg, #f0f0f0, #e0e0e0);
    border: 1px solid #ccc;
    border-radius: 0 0 6px 6px;
    margin: 0 1px;
    z-index: 1;
  }
  .key-white:hover { background: linear-gradient(180deg, #fff, #eee); }
  .key-white.active {
    background: linear-gradient(180deg, var(--accent-glow), var(--accent));
    border-color: var(--accent);
    box-shadow: 0 0 20px rgba(108, 92, 231, 0.5);
  }
  .key-black {
    width: 28px;
    height: 90px;
    background: linear-gradient(180deg, #333, #111);
    border: 1px solid #000;
    border-radius: 0 0 4px 4px;
    position: absolute;
    z-index: 2;
    margin-left: -14px;
  }
  .key-black:hover { background: linear-gradient(180deg, #444, #222); }
  .key-black.active {
    background: linear-gradient(180deg, var(--accent), var(--accent-dim));
    box-shadow: 0 0 15px rgba(108, 92, 231, 0.5);
  }
  .key-label {
    position: absolute;
    bottom: 8px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 9px;
    font-weight: 500;
    color: #999;
    pointer-events: none;
  }
  .key-black .key-label { color: #666; bottom: 6px; font-size: 8px; }
  .key-white.active .key-label { color: white; }
  .key-black.active .key-label { color: var(--accent-glow); }

  /* ── Presets ── */
  .presets-bar {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 16px;
  }
  .preset-btn {
    padding: 6px 12px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: rgba(255,255,255,0.03);
    color: var(--text-dim);
    font-size: 11px;
    cursor: pointer;
    transition: all 0.2s;
    font-family: 'Inter', sans-serif;
  }
  .preset-btn:hover { border-color: var(--accent); color: var(--accent-glow); }
  .preset-btn.active { border-color: var(--accent); background: rgba(108,92,231,0.15); color: var(--accent-glow); }

  /* ── Tooltip ── */
  .tooltip {
    position: fixed;
    background: #1a1a2e;
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    padding: 10px 14px;
    font-size: 12px;
    color: var(--text);
    max-width: 240px;
    z-index: 1000;
    pointer-events: none;
    box-shadow: 0 8px 32px rgba(0,0,0,0.4);
    line-height: 1.5;
    opacity: 0;
    transition: opacity 0.15s;
  }
  .tooltip.visible { opacity: 1; }
  .tooltip .tt-title {
    font-weight: 600;
    color: var(--accent-glow);
    margin-bottom: 4px;
    font-size: 12px;
  }

  /* ── Responsive ── */
  @media (max-width: 900px) {
    .tutorial-panel { display: none; }
    .controls-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
  }

  /* ── Glow animations ── */
  @keyframes pulse {
    0%, 100% { opacity: 0.6; }
    50% { opacity: 1; }
  }
  .pulse { animation: pulse 2s ease-in-out infinite; }

  /* ── Filter visualization ── */
  .filter-viz {
    width: 100%;
    height: 60px;
    margin-top: 8px;
    background: rgba(0,0,0,0.2);
    border-radius: 8px;
    overflow: hidden;
  }
  .filter-viz canvas { width: 100%; height: 100%; }

  /* ── LFO section ── */
  .lfo-waveform {
    display: flex;
    gap: 4px;
    margin-bottom: 10px;
  }
  .lfo-wave-btn {
    width: 36px;
    height: 30px;
    border: 1px solid var(--panel-border);
    border-radius: 6px;
    background: rgba(255,255,200,0.03);
    color: var(--text-dim);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
  }
  .lfo-wave-btn:hover { border-color: var(--orange); }
  .lfo-wave-btn.selected { border-color: var(--orange); background: rgba(253,203,110,0.1); color: var(--orange); }
  .lfo-wave-btn svg { width: 22px; height: 14px; }

  /* ── Effect section ── */
  .effect-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 10px;
  }
  .toggle {
    width: 36px;
    height: 20px;
    background: var(--panel-border);
    border-radius: 10px;
    position: relative;
    cursor: pointer;
    transition: background 0.2s;
  }
  .toggle.on { background: var(--accent); }
  .toggle::after {
    content: '';
    width: 16px;
    height: 16px;
    background: white;
    border-radius: 50%;
    position: absolute;
    top: 2px;
    left: 2px;
    transition: left 0.2s;
  }
  .toggle.on::after { left: 18px; }
  .toggle-label { font-size: 12px; color: var(--text-dim); }

  .note-display {
    position: absolute;
    top: 8px;
    right: 12px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--accent-glow);
    background: rgba(108, 92, 231, 0.1);
    padding: 3px 10px;
    border-radius: 4px;
    min-width: 60px;
    text-align: center;
  }
</style>
</head>
<body>

<div class="header">
  <h1><span class="icon">🎛️</span> Synth Playground</h1>
  <div class="header-controls">
    <button class="btn" id="toggleTutorial">📖 Tutorial</button>
    <button class="btn" id="resetBtn">↺ Reset</button>
  </div>
</div>

<div class="app">
  <!-- Tutorial Panel -->
  <div class="tutorial-panel" id="tutorialPanel">
    <div class="tutorial-header">📚 Learn Synthesis</div>
    <div class="tutorial-progress">
      <div class="progress-bar"><div class="progress-fill" id="progressFill" style="width: 0%"></div></div>
      <div class="progress-text" id="progressText">Lesson 0 of 7</div>
    </div>
    <div class="tutorial-content" id="tutorialContent"></div>
  </div>

  <!-- Synth Area -->
  <div class="synth-area" id="synthArea">
    <!-- Visualizer -->
    <div class="visualizer">
      <div class="viz-label">Waveform</div>
      <div class="note-display" id="noteDisplay">—</div>
      <canvas id="vizCanvas"></canvas>
    </div>

    <div class="synth-body">
      <!-- Presets -->
      <div class="presets-bar">
        <span style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:var(--text-dim);margin-right:4px;">Presets:</span>
        <button class="preset-btn" data-preset="sine-bell">🔔 Sine Bell</button>
        <button class="preset-btn" data-preset="saw-lead">🎸 Saw Lead</button>
        <button class="preset-btn" data-preset="square-bass">🎵 Square Bass</button>
        <button class="preset-btn" data-preset="tri-pad">🎹 Tri Pad</button>
        <button class="preset-btn" data-preset="fm-bell">✨ FM Bell</button>
        <button class="preset-btn" data-preset="filtered-noise">💨 Filtered Noise</button>
        <button class="preset-btn" data-preset="warm-sub">🌊 Warm Sub</button>
      </div>

      <!-- Oscillator -->
      <div class="synth-row">
        <div class="synth-panel" id="oscPanel">
          <div class="panel-label"><span class="panel-dot"></span> Oscillator</div>
          <div class="controls-grid">
            <div style="grid-column: 1 / -1;">
              <div class="control-label" style="margin-bottom:8px;">Waveform</div>
              <div class="waveform-selector">
                <button class="waveform-btn selected" data-wave="sine">
                  <svg viewBox="0 0 36 24"><path d="M2 12 Q11 0 18 12 Q25 24 34 12" fill="none" stroke="currentColor" stroke-width="2"/></svg>
                  Sine
                </button>
                <button class="waveform-btn" data-wave="square">
                  <svg viewBox="0 0 36 24"><path d="M2 18 L2 6 L18 6 L18 18 L34 18 L34 6" fill="none" stroke="currentColor" stroke-width="2"/></svg>
                  Square
                </button>
                <button class="waveform-btn" data-wave="sawtooth">
                  <svg viewBox="0 0 36 24"><path d="M2 18 L18 6 L18 18 L34 6" fill="none" stroke="currentColor" stroke-width="2"/></svg>
                  Saw
                </button>
                <button class="waveform-btn" data-wave="triangle">
                  <svg viewBox="0 0 36 24"><path d="M2 18 L9 6 L27 18 L34 6" fill="none" stroke="currentColor" stroke-width="2"/></svg>
                  Tri
                </button>
              </div>
            </div>
            <div class="control">
              <div class="control-label">Octave</div>
              <input type="range" id="octaveSlider" min="-2" max="2" value="0" step="1">
              <div class="control-value" id="octaveValue">0</div>
            </div>
            <div class="control">
              <div class="control-label">Detune</div>
              <input type="range" id="detuneSlider" min="-100" max="100" value="0" step="1">
              <div class="control-value" id="detuneValue">0¢</div>
            </div>
            <div class="control">
              <div class="control-label">Volume</div>
              <input type="range" id="oscVolume" min="0" max="100" value="70">
              <div class="control-value" id="oscVolumeValue">70%</div>
            </div>
          </div>
        </div>
      </div>

      <!-- Filter -->
      <div class="synth-row">
        <div class="synth-panel" id="filterPanel">
          <div class="panel-label"><span class="panel-dot"></span> Filter</div>
          <div class="controls-grid">
            <div class="control">
              <div class="control-label">Type</div>
              <div class="waveform-selector">
                <button class="waveform-btn selected" data-filter="lowpass" style="width:48px;height:44px;font-size:8px;">LP</button>
                <button class="waveform-btn" data-filter="highpass" style="width:48px;height:44px;font-size:8px;">HP</button>
                <button class="waveform-btn" data-filter="bandpass" style="width:48px;height:44px;font-size:8px;">BP</button>
              </div>
            </div>
            <div class="control">
              <div class="control-label">Cutoff</div>
              <input type="range" id="filterCutoff" min="20" max="20000" value="5000" step="1">
              <div class="control-value" id="filterCutoffValue">5000 Hz</div>
            </div>
            <div class="control">
              <div class="control-label">Resonance</div>
              <input type="range" id="filterRes" min="0" max="30" value="1" step="0.1">
              <div class="control-value" id="filterResValue">1.0</div>
            </div>
            <div class="control" style="grid-column: 1 / -1;">
              <div class="control-label">Filter Response</div>
              <div class="filter-viz"><canvas id="filterVizCanvas"></canvas></div>
            </div>
          </div>
        </div>
      </div>

      <!-- ADSR Envelope -->
      <div class="synth-row">
        <div class="synth-panel" id="adsrPanel">
          <div class="panel-label"><span class="panel-dot"></span> Amplifier (ADSR)</div>
          <div class="controls-grid">
            <div class="control">
              <div class="control-label">Attack</div>
              <input type="range" id="attackSlider" min="0" max="2000" value="10" step="1">
              <div class="control-value" id="attackValue">10ms</div>
            </div>
            <div class="control">
              <div class="control-label">Decay</div>
              <input type="range" id="decaySlider" min="0" max="2000" value="200" step="1">
              <div class="control-value" id="decayValue">200ms</div>
            </div>
            <div class="control">
              <div class="control-label">Sustain</div>
              <input type="range" id="sustainSlider" min="0" max="100" value="70" step="1">
              <div class="control-value" id="sustainValue">70%</div>
            </div>
            <div class="control">
              <div class="control-label">Release</div>
              <input type="range" id="releaseSlider" min="0" max="3000" value="300" step="1">
              <div class="control-value" id="releaseValue">300ms</div>
            </div>
            <div class="control" style="grid-column: 1 / -1;">
              <div class="control-label">Envelope Shape</div>
              <div class="adsr-visual"><canvas id="adsrCanvas"></canvas></div>
            </div>
          </div>
        </div>
      </div>

      <!-- LFO -->
      <div class="synth-row">
        <div class="synth-panel" id="lfoPanel">
          <div class="panel-label"><span class="panel-dot"></span> LFO (Modulation)</div>
          <div class="controls-grid">
            <div class="control">
              <div class="control-label">Rate</div>
              <input type="range" id="lfoRate" min="0.1" max="20" value="4" step="0.1">
              <div class="control-value" id="lfoRateValue">4 Hz</div>
            </div>
            <div class="control">
              <div class="control-label">Depth</div>
              <input type="range" id="lfoDepth" min="0" max="100" value="0" step="1">
              <div class="control-value" id="lfoDepthValue">0</div>
            </div>
            <div class="control">
              <div class="control-label">Target</div>
              <select id="lfoTarget" style="background:var(--panel);border:1px solid var(--panel-border);border-radius:6px;color:var(--text);padding:6px 10px;font-size:12px;font-family:'Inter',sans-serif;">
                <option value="pitch">Pitch</option>
                <option value="filter">Filter</option>
                <option value="volume">Volume</option>
              </select>
            </div>
            <div class="control">
              <div class="control-label">Waveform</div>
              <div class="lfo-waveform">
                <button class="lfo-wave-btn selected" data-lfo="sine"><svg viewBox="0 0 22 14"><path d="M1 7 Q6 0 11 7 Q16 14 21 7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
                <button class="lfo-wave-btn" data-lfo="triangle"><svg viewBox="0 0 22 14"><path d="M1 7 L5 1 L11 13 L17 1 L21 7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
                <button class="lfo-wave-btn" data-lfo="sawtooth"><svg viewBox="0 0 22 14"><path d="M1 13 L11 1 L11 13 L21 1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
                <button class="lfo-wave-btn" data-lfo="square"><svg viewBox="0 0 22 14"><path d="M1 12 L1 2 L11 2 L11 12 L21 12 L21 2" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- Effects -->
      <div class="synth-row">
        <div class="synth-panel" id="fxPanel">
          <div class="panel-label"><span class="panel-dot"></span> Effects</div>
          <div class="controls-grid">
            <div class="control">
              <div class="effect-toggle">
                <div class="toggle" id="delayToggle"></div>
                <span class="toggle-label">Delay</span>
              </div>
              <input type="range" id="delayTime" min="0.01" max="1" value="0.3" step="0.01" style="width:100%;margin-top:6px;">
              <div class="control-value" id="delayTimeValue" style="text-align:center;margin-top:2px;">300ms</div>
              <input type="range" id="delayFeedback" min="0" max="0.9" value="0.4" step="0.01" style="width:100%;margin-top:6px;">
              <div class="control-value" id="delayFeedbackValue" style="text-align:center;margin-top:2px;">40%</div>
            </div>
            <div class="control">
              <div class="effect-toggle">
                <div class="toggle" id="reverbToggle"></div>
                <span class="toggle-label">Reverb</span>
              </div>
              <input type="range" id="reverbMix" min="0" max="100" value="30" step="1" style="width:100%;margin-top:6px;">
              <div class="control-value" id="reverbMixValue" style="text-align:center;margin-top:2px;">30%</div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Keyboard -->
    <div class="keyboard-container">
      <div class="keyboard-label">Use your computer keyboard (A-K = white keys, W-U = black keys) or click below</div>
      <div class="keyboard" id="keyboard"></div>
    </div>
  </div>
</div>

<div class="tooltip" id="tooltip">
  <div class="tt-title"></div>
  <div class="tt-body"></div>
</div>

<script>
// ═══════════════════════════════════════════════════════════
//  SYNTH ENGINE
// ═══════════════════════════════════════════════════════════

const NOTE_NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
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 NOTE_FREQ = {
  'C0':16.35,'C#0':17.32,'D0':18.35,'D#0':19.45,'E0':20.60,'F0':21.83,
  'F#0':23.12,'G0':24.50,'G#0':25.96,'A0':27.50,'A#0':29.14,'B0':30.87,
  'C1':32.70,'C#1':34.65,'D1':36.71,'D#1':38.89,'E1':41.20,'F1':43.65,
  'F#1':46.25,'G1':49.00,'G#1':51.91,'A1':55.00,'A#1':58.27,'B1':61.74,
  'C2':65.41,'C#2':69.30,'D2':73.42,'D#2':77.78,'E2':82.41,'F2':87.31,
  'F#2':92.50,'G2':98.00,'G#2':103.83,'A2':110.00,'A#2':116.54,'B2':123.47,
  '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,'F5':698.46,
  'F#5':739.99,'G5':783.99,'G#5':830.61,'A5':880.00,'A#5':932.33,'B5':987.77,
  'C6':1046.50
};

let audioCtx = null;
let masterGain = null;
let analyser = null;
let filterNode = null;
let delayNode = null;
let delayFeedbackNode = null;
let reverbNode = null;
let reverbGain = null;
let dryGain = null;
let lfoNode = null;
let lfoGain = null;
let lfoConnected = false;

const state = {
  waveform: 'sine',
  filterType: 'lowpass',
  filterCutoff: 5000,
  filterRes: 1,
  attack: 10,
  decay: 200,
  sustain: 70,
  release: 300,
  octave: 0,
  detune: 0,
  oscVolume: 70,
  lfoRate: 4,
  lfoDepth: 0,
  lfoTarget: 'pitch',
  lfoWaveform: 'sine',
  delayTime: 0.3,
  delayFeedback: 0.4,
  delayEnabled: false,
  reverbMix: 30,
  reverbEnabled: false,
  activeNotes: {},
  tutorialStep: 0,
  completedLessons: new Set()
};

const presets = {
  'sine-bell': { waveform:'sine', filterCutoff:8000, filterRes:0.5, attack:5, decay:800, sustain:20, release:1500, lfoDepth:0, filterType:'lowpass' },
  'saw-lead': { waveform:'sawtooth', filterCutoff:3000, filterRes:4, attack:5, decay:200, sustain:80, release:300, lfoDepth:15, lfoTarget:'pitch', lfoRate:5, filterType:'lowpass' },
  'square-bass': { waveform:'square', filterCutoff:600, filterRes:2, attack:10, decay:300, sustain:60, release:100, octave:-1, filterType:'lowpass' },
  'tri-pad': { waveform:'triangle', filterCutoff:4000, filterRes:1, attack:400, decay:300, sustain:70, release:800, filterType:'lowpass' },
  'fm-bell': { waveform:'sine', filterCutoff:12000, filterRes:0, attack:1, decay:500, sustain:0, release:2000, lfoDepth:30, lfoTarget:'pitch', lfoRate:8, filterType:'lowpass' },
  'filtered-noise': { waveform:'sawtooth', filterCutoff:1500, filterRes:8, attack:50, decay:400, sustain:40, release:500, filterType:'bandpass' },
  'warm-sub': { waveform:'sine', filterCutoff:500, filterRes:0, attack:20, decay:100, sustain:80, release:200, octave:-2, filterType:'lowpass' }
};

function initAudio() {
  if (audioCtx) return;
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();

  // Master gain
  masterGain = audioCtx.createGain();
  masterGain.gain.value = 0.8;

  // Analyser
  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 2048;

  // Filter
  filterNode = audioCtx.createBiquadFilter();
  filterNode.type = state.filterType;
  filterNode.frequency.value = state.filterCutoff;
  filterNode.Q.value = state.filterRes;

  // Delay
  delayNode = audioCtx.createDelay(2);
  delayNode.delayTime.value = state.delayTime;
  delayFeedbackNode = audioCtx.createGain();
  delayFeedbackNode.gain.value = state.delayFeedback;

  // Reverb (convolver with generated impulse)
  reverbNode = audioCtx.createConvolver();
  reverbGain = audioCtx.createGain();
  dryGain = audioCtx.createGain();
  dryGain.gain.value = 1;
  reverbGain.gain.value = state.reverbMix / 100;
  generateReverbIR();

  // LFO
  lfoNode = audioCtx.createOscillator();
  lfoGain = audioCtx.createGain();
  lfoNode.type = state.lfoWaveform;
  lfoNode.frequency.value = state.lfoRate;
  lfoGain.gain.value = state.lfoDepth;

  // Routing
  filterNode.connect(masterGain);
  masterGain.connect(dryGain);
  dryGain.connect(analyser);
  analyser.connect(audioCtx.destination);

  // Delay routing
  masterGain.connect(delayNode);
  delayNode.connect(delayFeedbackNode);
  delayFeedbackNode.connect(delayNode);
  delayNode.connect(reverbNode);

  // Reverb routing
  masterGain.connect(reverbNode);
  reverbNode.connect(reverbGain);
  reverbGain.connect(analyser);

  // LFO routing
  lfoNode.connect(lfoGain);
  lfoNode.start();

  // Update LFO connections
  updateLFOConnections();
}

function generateReverbIR() {
  const rate = audioCtx.sampleRate;
  const length = rate * 2;
  const impulse = audioCtx.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, 2.5);
    }
  }
  reverbNode.buffer = impulse;
}

function getNoteFreq(note) {
  const baseFreq = NOTE_FREQ[note];
  if (!baseFreq) return 440;
  return baseFreq * Math.pow(2, state.octave);
}

function noteOn(note) {
  initAudio();
  if (state.activeNotes[note]) return;

  const freq = getNoteFreq(note);
  const osc = audioCtx.createOscillator();
  const envGain = audioCtx.createGain();

  osc.type = state.waveform;
  osc.frequency.value = freq;
  osc.detune.value = state.detune;

  // ADSR
  const now = audioCtx.currentTime;
  const a = state.attack / 1000;
  const d = state.decay / 1000;
  const s = state.sustain / 100;
  const vol = state.oscVolume / 100;

  envGain.gain.setValueAtTime(0, now);
  envGain.gain.linearRampToValueAtTime(vol, now + a);
  envGain.gain.linearRampToValueAtTime(vol * s, now + a + d);

  osc.connect(envGain);
  envGain.connect(filterNode);

  osc.start(now);

  state.activeNotes[note] = { osc, envGain };
  updateNoteDisplay(note);
  updateActiveKeys();
}

function noteOff(note) {
  const entry = state.activeNotes[note];
  if (!entry) return;

  const { osc, envGain } = entry;
  const now = audioCtx.currentTime;
  const r = state.release / 1000;

  envGain.gain.cancelScheduledValues(now);
  envGain.gain.setValueAtTime(envGain.gain.value, now);
  envGain.gain.linearRampToValueAtTime(0, now + r);

  osc.stop(now + r + 0.05);
  delete state.activeNotes[note];
  updateNoteDisplay(null);
  updateActiveKeys();
}

function updateLFOConnections() {
  if (!audioCtx) return;
  // Disconnect previous
  try { lfoGain.disconnect(); } catch(e) {}

  lfoNode.frequency.value = state.lfoRate;
  lfoNode.type = state.lfoWaveform;
  lfoGain.gain.value = state.lfoDepth;

  if (state.lfoDepth > 0) {
    if (state.lfoTarget === 'pitch') {
      for (const note in state.activeNotes) {
        state.activeNotes[note].osc.detune.connect(lfoGain);
      }
    } else if (state.lfoTarget === 'filter') {
      lfoGain.connect(filterNode.frequency);
    } else if (state.lfoTarget === 'volume') {
      for (const note in state.activeNotes) {
        lfoGain.connect(state.activeNotes[note].envGain.gain);
      }
    }
  }
}

function updateFilter() {
  if (!filterNode) return;
  filterNode.type = state.filterType;
  filterNode.frequency.setTargetAtTime(state.filterCutoff, audioCtx.currentTime, 0.01);
  filterNode.Q.setTargetAtTime(state.filterRes, audioCtx.currentTime, 0.01);
  drawFilterViz();
}

// ═══════════════════════════════════════════════════════════
//  KEYBOARD
// ═══════════════════════════════════════════════════════════

const keyboardKeys = [
  { note:'C4', type:'white', key:'A' },
  { note:'C#4', type:'black', key:'W' },
  { note:'D4', type:'white', key:'S' },
  { note:'D#4', type:'black', key:'E' },
  { note:'E4', type:'white', key:'D' },
  { note:'F4', type:'white', key:'F' },
  { note:'F#4', type:'black', key:'T' },
  { note:'G4', type:'white', key:'G' },
  { note:'G#4', type:'black', key:'Y' },
  { note:'A4', type:'white', key:'H' },
  { note:'A#4', type:'black', key:'U' },
  { note:'B4', type:'white', key:'J' },
  { note:'C5', type:'white', key:'K' },
  { note:'C#5', type:'black', key:'O' },
  { note:'D5', type:'white', key:'L' },
  { note:'D#5', type:'black', key:'P' },
  { note:'E5', type:'white', key:';' },
];

function buildKeyboard() {
  const kb = document.getElementById('keyboard');
  kb.innerHTML = '';
  let whiteIndex = 0;

  keyboardKeys.forEach((k) => {
    const keyEl = document.createElement('div');
    keyEl.className = `key key-${k.type}`;
    keyEl.dataset.note = k.note;
    keyEl.dataset.key = k.key;

    const label = document.createElement('span');
    label.className = 'key-label';
    label.textContent = k.key;
    keyEl.appendChild(label);

    if (k.type === 'black') {
      const whiteWidth = 46; // 44 + 2 margin
      const leftPos = whiteIndex * whiteWidth - 14;
      keyEl.style.left = leftPos + 'px';
    } else {
      whiteIndex++;
    }

    keyEl.addEventListener('mousedown', (e) => { e.preventDefault(); noteOn(k.note); });
    keyEl.addEventListener('mouseup', () => noteOff(k.note));
    keyEl.addEventListener('mouseleave', () => noteOff(k.note));
    keyEl.addEventListener('touchstart', (e) => { e.preventDefault(); noteOn(k.note); });
    keyEl.addEventListener('touchend', () => noteOff(k.note));

    kb.appendChild(keyEl);
  });
}

function updateActiveKeys() {
  document.querySelectorAll('.key').forEach(el => {
    const note = el.dataset.note;
    el.classList.toggle('active', !!state.activeNotes[note]);
  });
}

// ═══════════════════════════════════════════════════════════
//  VISUALIZATION
// ═══════════════════════════════════════════════════════════

function drawWaveform() {
  const canvas = document.getElementById('vizCanvas');
  const ctx = canvas.getContext('2d');
  const dpr = window.devicePixelRatio || 1;
  canvas.width = canvas.offsetWidth * dpr;
  canvas.height = canvas.offsetHeight * dpr;
  ctx.scale(dpr, dpr);
  const w = canvas.offsetWidth;
  const h = canvas.offsetHeight;

  ctx.clearRect(0, 0, w, h);

  // Grid
  ctx.strokeStyle = 'rgba(255,255,255,0.04)';
  ctx.lineWidth = 1;
  for (let y = 0; y < h; y += h/8) {
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
  }
  for (let x = 0; x < w; x += w/16) {
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
  }

  // Center line
  ctx.strokeStyle = 'rgba(255,255,255,0.1)';
  ctx.beginPath(); ctx.moveTo(0, h/2); ctx.lineTo(w, h/2); ctx.stroke();

  if (!analyser) return;

  const bufferLength = analyser.frequencyBinCount;
  const dataArray = new Uint8Array(bufferLength);
  analyser.getByteTimeDomainData(dataArray);

  // Glow
  ctx.shadowColor = '#6c5ce7';
  ctx.shadowBlur = 15;
  ctx.lineWidth = 2.5;
  ctx.strokeStyle = '#6c5ce7';
  ctx.beginPath();

  const sliceWidth = w / bufferLength;
  let x = 0;
  for (let i = 0; i < bufferLength; i++) {
    const v = dataArray[i] / 128.0;
    const y = (v * h) / 2;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
    x += sliceWidth;
  }
  ctx.lineTo(w, h/2);
  ctx.stroke();
  ctx.shadowBlur = 0;

  // Fill under curve
  ctx.lineTo(w, h/2);
  ctx.lineTo(0, h/2);
  ctx.closePath();
  const grad = ctx.createLinearGradient(0, 0, w, 0);
  grad.addColorStop(0, 'rgba(108,92,231,0.15)');
  grad.addColorStop(0.5, 'rgba(0,206,201,0.1)');
  grad.addColorStop(1, 'rgba(108,92,231,0.15)');
  ctx.fillStyle = grad;
  ctx.fill();
}

function drawFilterViz() {
  const canvas = document.getElementById('filterVizCanvas');
  const ctx = canvas.getContext('2d');
  const dpr = window.devicePixelRatio || 1;
  canvas.width = canvas.offsetWidth * dpr;
  canvas.height = canvas.offsetHeight * dpr;
  ctx.scale(dpr, dpr);
  const w = canvas.offsetWidth;
  const h = canvas.offsetHeight;

  ctx.clearRect(0, 0, w, h);

  // Draw frequency response curve
  const numPoints = 200;
  ctx.lineWidth = 2;
  ctx.strokeStyle = '#fdcb6e';
  ctx.shadowColor = '#fdcb6e';
  ctx.shadowBlur = 8;
  ctx.beginPath();

  for (let i = 0; i < numPoints; i++) {
    const freq = 20 * Math.pow(1000, i / (numPoints - 1));
    const normalizedFreq = freq / 22050;
    let gain = 0;

    if (state.filterType === 'lowpass') {
      const ratio = freq / state.filterCutoff;
      gain = -20 * Math.log10(Math.max(1, Math.pow(ratio, state.filterRes / 2)));
      gain = Math.max(-60, Math.min(20, gain));
      if (state.filterRes > 5) {
        const peak = Math.abs(Math.log10(ratio)) < 0.1 ? state.filterRes * 3 : 0;
        gain += peak * Math.exp(-Math.pow(Math.log10(ratio) * 4, 2));
      }
    } else if (state.filterType === 'highpass') {
      const ratio = state.filterCutoff / freq;
      gain = -20 * Math.log10(Math.max(1, Math.pow(ratio, state.filterRes / 2)));
      gain = Math.max(-60, Math.min(20, gain));
    } else if (state.filterType === 'bandpass') {
      const center = state.filterCutoff;
      const bw = center * (state.filterRes / 10);
      const dist = Math.abs(Math.log(freq / center));
      const bwLog = Math.log(bw / center);
      gain = -20 * Math.log10(Math.max(1, dist / Math.max(bwLog, 0.01)));
      gain = Math.max(-60, Math.min(20, gain));
      if (state.filterRes > 5) {
        gain += state.filterRes * 2 * Math.exp(-Math.pow(dist / 0.3, 2));
      }
    }

    const x = (i / (numPoints - 1)) * w;
    const y = h - ((gain + 60) / 80) * h;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();
  ctx.shadowBlur = 0;

  // Fill under
  ctx.lineTo(w, h);
  ctx.lineTo(0, h);
  ctx.closePath();
  ctx.fillStyle = 'rgba(253,203,110,0.06)';
  ctx.fill();

  // Cutoff marker
  const cutoffX = (Math.log10(state.filterCutoff / 20) / Math.log10(20000 / 20)) * w;
  ctx.strokeStyle = 'rgba(253,203,110,0.4)';
  ctx.setLineDash([4, 4]);
  ctx.beginPath();
  ctx.moveTo(cutoffX, 0);
  ctx.lineTo(cutoffX, h);
  ctx.stroke();
  ctx.setLineDash([]);
}

function drawADSR() {
  const canvas = document.getElementById('adsrCanvas');
  const ctx = canvas.getContext('2d');
  const dpr = window.devicePixelRatio || 1;
  canvas.width = canvas.offsetWidth * dpr;
  canvas.height = canvas.offsetHeight * dpr;
  ctx.scale(dpr, dpr);
  const w = canvas.offsetWidth;
  const h = canvas.offsetHeight;

  ctx.clearRect(0, 0, w, h);

  const a = Math.min(state.attack / 2000, 1);
  const d = Math.min(state.decay / 2000, 1);
  const s = state.sustain / 100;
  const total = 0.15; // display window
  const r = Math.min(state.release / 3000, 1);

  const ax = a * total;
  const dx = ax + d * total;
  const sx = dx + (1 - a - d) * total * 0.5;
  const rx = sx + r * total;

  const topY = 8;
  const botY = h - 8;
  const susY = botY - s * (botY - topY);

  ctx.lineWidth = 2;
  ctx.strokeStyle = '#00cec9';
  ctx.shadowColor = '#00cec9';
  ctx.shadowBlur = 6;
  ctx.beginPath();
  ctx.moveTo(0, botY);
  ctx.lineTo(ax, topY);
  ctx.lineTo(dx, susY);
  ctx.lineTo(sx, susY);
  ctx.lineTo(rx, botY);
  ctx.stroke();
  ctx.shadowBlur = 0;

  // Fill
  ctx.lineTo(rx, botY);
  ctx.lineTo(0, botY);
  ctx.closePath();
  ctx.fillStyle = 'rgba(0,206,201,0.08)';
  ctx.fill();

  // Labels
  ctx.font = '9px Inter';
  ctx.fillStyle = 'rgba(255,255,255,0.4)';
  ctx.textAlign = 'center';
  ctx.fillText('A', ax, botY + 14);
  ctx.fillText('D', dx, botY + 14);
  ctx.fillText('S', sx, topY - 4);
  ctx.fillText('R', rx, botY + 14);
}

// ═══════════════════════════════════════════════════════════
//  TUTORIAL
// ═══════════════════════════════════════════════════════════

const lessons = [
  {
    num: '01',
    title: 'Oscillators — The Sound Source',
    desc: 'Every synth starts with an oscillator — a wave that vibrates at a specific frequency. The waveform determines the basic timbre (tone color) of the sound.',
    tip: '🎯 <b>Try it:</b> Click each waveform and play a note. Sine is pure and smooth. Square has hollow, hollow tones. Saw is bright and buzzy. Triangle is softer than saw.',
    complete: () => {
      return Object.keys(state.activeNotes).length > 0;
    }
  },
  {
    num: '02',
    title: 'Frequency & Octave',
    desc: 'Frequency (in Hz) determines pitch. Higher frequency = higher pitch. The octave control shifts the entire keyboard up or down by 2 octaves.',
    tip: '🎯 <b>Try it:</b> Play a note, then change the octave. Each step doubles or halves the frequency. Try octave -2 for bass sounds!',
    complete: () => state.octave !== 0
  },
  {
    num: '03',
    title: 'Detune — Thickening the Sound',
    desc: 'Detune slightly offsets the oscillator frequency (in cents). Even tiny detuning creates a "chorus" effect that makes sounds feel wider and richer.',
    tip: '🎯 <b>Try it:</b> Hold a note, then slowly increase detune. At ±10¢ you\'ll hear the beating effect. Go further for dissonance!',
    complete: () => state.detune !== 0
  },
  {
    num: '04',
    title: 'Filter — Shaping the Tone',
    desc: 'A filter removes or emphasizes certain frequencies. A lowpass filter lets low frequencies through and cuts highs (like muffling). Highpass does the opposite.',
    tip: '🎯 <b>Try it:</b> Play a saw wave and sweep the cutoff knob. Low = muffled. High = bright and open. Increase resonance for a "whistling" peak at the cutoff.',
    complete: () => state.filterCutoff < 3000
  },
  {
    num: '05',
    title: 'ADSR Envelope — The Shape of a Note',
    desc: 'ADSR controls how a note evolves over time: <b>A</b>ttack (how fast it reaches max volume), <b>D</b>ecay (time to fall to sustain level), <b>S</b>ustain (volume while held), <b>R</b>elease (fade out when released).',
    tip: '🎯 <b>Try it:</b> Hold a key and watch the envelope visualization. Set attack to 0 for instant. Set release long for a pad-like fade-out. Try attack=0, decay=2000, sustain=0 for a bell sound!',
    complete: () => state.attack === 0 || state.release > 500
  },
  {
    num: '06',
    title: 'LFO — Automatic Modulation',
    desc: 'An LFO (Low Frequency Oscillator) modulates a parameter at a rate too slow to hear as pitch — but you hear its <b>effect</b>. It\'s like an auto-vibrato or auto-wah.',
    tip: '🎯 <b>Try it:</b> Set LFO rate to 4 Hz and depth to 50, target to "Pitch" — you get vibrato! Try "Filter" for auto-wah. Change the waveform to hear different modulation flavors.',
    complete: () => state.lfoDepth > 0
  },
  {
    num: '07',
    title: 'Effects — Space & Depth',
    desc: '<b>Delay</b> repeats your sound (echo). <b>Reverb</b> simulates acoustic space (room, hall, cave). Together they make dry synth sounds feel alive.',
    tip: '🎯 <b>Try it:</b> Enable delay and sweep the time. Short = slapback echo. Long = spacious. Enable reverb and adjust mix. Combine both for massive sounds!',
    complete: () => state.delayEnabled || state.reverbEnabled
  }
];

function buildTutorial() {
  const container = document.getElementById('tutorialContent');
  container.innerHTML = '';
  lessons.forEach((lesson, i) => {
    const div = document.createElement('div');
    div.className = `lesson ${i < state.tutorialStep ? 'completed' : i === state.tutorialStep ? 'active' : 'locked'}`;
    div.id = `lesson-${i}`;
    div.innerHTML = `
      <div class="lesson-num">${lesson.num}${i < state.tutorialStep ? ' ✓' : i === state.tutorialStep ? ' ▶' : ' 🔒'}</div>
      <h3>${lesson.title}</h3>
      <p>${lesson.desc}</p>
      <div class="tip">${lesson.tip}</div>
      ${i === state.tutorialStep ? `<button class="action-btn" onclick="checkLesson(${i})">Check Progress →</button>` : ''}
    `;
    container.appendChild(div);
  });
  updateProgress();
}

function checkLesson(step) {
  if (step !== state.tutorialStep) return;
  if (lessons[step].complete()) {
    state.completedLessons.add(step);
    state.tutorialStep++;
    buildTutorial();
    if (state.tutorialStep >= lessons.length) {
      const container = document.getElementById('tutorialContent');
      const div = document.createElement('div');
      div.className = 'lesson completed';
      div.innerHTML = `
        <div class="lesson-num">🎉</div>
        <h3>You've completed all lessons!</h3>
        <p>Now experiment freely. Try the presets, combine effects, and discover your own sounds.</p>
      `;
      container.appendChild(div);
    }
  }
}

function updateProgress() {
  const pct = (state.tutorialStep / lessons.length) * 100;
  document.getElementById('progressFill').style.width = pct + '%';
  document.getElementById('progressText').textContent = `Lesson ${state.tutorialStep} of ${lessons.length}`;
}

// ═══════════════════════════════════════════════════════════
//  UI BINDINGS
// ═══════════════════════════════════════════════════════════

function bindSlider(id, valueId, suffix, callback) {
  const slider = document.getElementById(id);
  const valueEl = document.getElementById(valueId);
  slider.addEventListener('input', () => {
    const val = parseFloat(slider.value);
    valueEl.textContent = val + suffix;
    if (callback) callback(val);
  });
}

function initBindings() {
  // Waveform selector
  document.querySelectorAll('.waveform-btn[data-wave]').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.waveform-btn[data-wave]').forEach(b => b.classList.remove('selected'));
      btn.classList.add('selected');
      state.waveform = btn.dataset.wave;
    });
  });

  // Filter type
  document.querySelectorAll('.waveform-btn[data-filter]').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.waveform-btn[data-filter]').forEach(b => b.classList.remove('selected'));
      btn.classList.add('selected');
      state.filterType = btn.dataset.filter;
      updateFilter();
    });
  });

  // LFO waveform
  document.querySelectorAll('.lfo-wave-btn[data-lfo]').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.lfo-wave-btn[data-lfo]').forEach(b => b.classList.remove('selected'));
      btn.classList.add('selected');
      state.lfoWaveform = btn.dataset.lfo;
      if (lfoNode) lfoNode.type = state.lfoWaveform;
    });
  });

  // Sliders
  bindSlider('octaveSlider', 'octaveValue', '', v => { state.octave = v; });
  bindSlider('detuneSlider', 'detuneValue', '¢', v => { state.detune = v; });
  bindSlider('oscVolume', 'oscVolumeValue', '%', v => { state.oscVolume = v; });
  bindSlider('filterCutoff', 'filterCutoffValue', ' Hz', v => { state.filterCutoff = v; updateFilter(); });
  bindSlider('filterRes', 'filterResValue', '', v => { state.filterRes = v; updateFilter(); });
  bindSlider('attackSlider', 'attackValue', 'ms', v => { state.attack = v; drawADSR(); });
  bindSlider('decaySlider', 'decayValue', 'ms', v => { state.decay = v; drawADSR(); });
  bindSlider('sustainSlider', 'sustainValue', '%', v => { state.sustain = v; drawADSR(); });
  bindSlider('releaseSlider', 'releaseValue', 'ms', v => { state.release = v; drawADSR(); });
  bindSlider('lfoRate', 'lfoRateValue', ' Hz', v => { state.lfoRate = v; if(lfoNode) lfoNode.frequency.value = v; });
  bindSlider('lfoDepth', 'lfoDepthValue', '', v => {
    state.lfoDepth = v;
    if (lfoGain) lfoGain.gain.value = v;
    updateLFOConnections();
  });
  bindSlider('delayTime', 'delayTimeValue', 'ms', v => {
    state.delayTime = v;
    if (delayNode) delayNode.delayTime.value = v / 1000;
  });
  bindSlider('delayFeedback', 'delayFeedbackValue', '%', v => {
    state.delayFeedback = v;
    if (delayFeedbackNode) delayFeedbackNode.gain.value = v / 100;
  });
  bindSlider('reverbMix', 'reverbMixValue', '%', v => {
    state.reverbMix = v;
    if (reverbGain) reverbGain.gain.value = v / 100;
  });

  // LFO target
  document.getElementById('lfoTarget').addEventListener('change', (e) => {
    state.lfoTarget = e.target.value;
    updateLFOConnections();
  });

  // Delay toggle
  document.getElementById('delayToggle').addEventListener('click', function() {
    this.classList.toggle('on');
    state.delayEnabled = this.classList.contains('on');
    if (delayNode) {
      delayNode.connect(analyser);
    }
  });

  // Reverb toggle
  document.getElementById('reverbToggle').addEventListener('click', function() {
    this.classList.toggle('on');
    state.reverbEnabled = this.classList.contains('on');
  });

  // Presets
  document.querySelectorAll('.preset-btn').forEach(btn => {
    btn.addEventListener('click', () => applyPreset(btn.dataset.preset));
  });

  // Tutorial toggle
  document.getElementById('toggleTutorial').addEventListener('click', () => {
    document.getElementById('tutorialPanel').classList.toggle('collapsed');
  });

  // Reset
  document.getElementById('resetBtn').addEventListener('click', () => {
    Object.assign(state, {
      waveform:'sine', filterType:'lowpass', filterCutoff:5000, filterRes:1,
      attack:10, decay:200, sustain:70, release:300, octave:0, detune:0,
      oscVolume:70, lfoRate:4, lfoDepth:0, lfoTarget:'pitch', lfoWaveform:'sine',
      delayTime:0.3, delayFeedback:0.4, delayEnabled:false, reverbMix:30, reverbEnabled:false
    });
    // Reset UI
    document.querySelectorAll('.waveform-btn[data-wave]').forEach(b => b.classList.remove('selected'));
    document.querySelector('.waveform-btn[data-wave="sine"]').classList.add('selected');
    document.querySelectorAll('.waveform-btn[data-filter]').forEach(b => b.classList.remove('selected'));
    document.querySelector('.waveform-btn[data-filter="lowpass"]').classList.add('selected');
    document.querySelectorAll('.lfo-wave-btn[data-lfo]').forEach(b => b.classList.remove('selected'));
    document.querySelector('.lfo-wave-btn[data-lfo="sine"]').classList.add('selected');
    document.getElementById('delayToggle').classList.remove('on');
    document.getElementById('reverbToggle').classList.remove('on');
    document.getElementById('lfoTarget').value = 'pitch';
    document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));

    // Reset sliders
    ['octaveSlider','detuneSlider','oscVolume','filterCutoff','filterRes','attackSlider','decaySlider','sustainSlider','releaseSlider','lfoRate','lfoDepth','delayTime','delayFeedback','reverbMix'].forEach(id => {
      document.getElementById(id).dispatchEvent(new Event('input'));
    });

    drawFilterViz();
    drawADSR();
    buildTutorial();
  });

  // Keyboard input
  const pressedKeys = new Set();
  window.addEventListener('keydown', (e) => {
    if (e.repeat) return;
    const key = e.key.toLowerCase();
    if (KEY_MAP[key] && !pressedKeys.has(key)) {
      pressedKeys.add(key);
      noteOn(KEY_MAP[key]);
    }
  });
  window.addEventListener('keyup', (e) => {
    const key = e.key.toLowerCase();
    if (KEY_MAP[key]) {
      pressedKeys.delete(key);
      noteOff(KEY_MAP[key]);
    }
  });
}

function applyPreset(name) {
  const p = presets[name];
  if (!p) return;
  Object.assign(state, p);

  // Update UI
  document.querySelectorAll('.waveform-btn[data-wave]').forEach(b => {
    b.classList.toggle('selected', b.dataset.wave === p.waveform);
  });
  document.querySelectorAll('.waveform-btn[data-filter]').forEach(b => {
    b.classList.toggle('selected', b.dataset.filter === p.filterType);
  });
  document.getElementById('octaveSlider').value = p.octave || 0;
  document.getElementById('octaveSlider').dispatchEvent(new Event('input'));
  document.getElementById('detuneSlider').value = p.detune || 0;
  document.getElementById('detuneSlider').dispatchEvent(new Event('input'));
  document.getElementById('oscVolume').value = p.oscVolume || 70;
  document.getElementById('oscVolume').dispatchEvent(new Event('input'));
  document.getElementById('filterCutoff').value = p.filterCutoff;
  document.getElementById('filterCutoff').dispatchEvent(new Event('input'));
  document.getElementById('filterRes').value = p.filterRes;
  document.getElementById('filterRes').dispatchEvent(new Event('input'));
  document.getElementById('attackSlider').value = p.attack;
  document.getElementById('attackSlider').dispatchEvent(new Event('input'));
  document.getElementById('decaySlider').value = p.decay;
  document.getElementById('decaySlider').dispatchEvent(new Event('input'));
  document.getElementById('sustainSlider').value = p.sustain;
  document.getElementById('sustainSlider').dispatchEvent(new Event('input'));
  document.getElementById('releaseSlider').value = p.release;
  document.getElementById('releaseSlider').dispatchEvent(new Event('input'));
  document.getElementById('lfoRate').value = p.lfoRate || 4;
  document.getElementById('lfoRate').dispatchEvent(new Event('input'));
  document.getElementById('lfoDepth').value = p.lfoDepth || 0;
  document.getElementById('lfoDepth').dispatchEvent(new Event('input'));
  document.getElementById('lfoTarget').value = p.lfoTarget || 'pitch';
  document.querySelectorAll('.lfo-wave-btn[data-lfo]').forEach(b => {
    b.classList.toggle('selected', b.dataset.lfo === (p.lfoWaveform || 'sine'));
  });
  document.getElementById('delayTime').value = p.delayTime || 0.3;
  document.getElementById('delayTime').dispatchEvent(new Event('input'));
  document.getElementById('delayFeedback').value = p.delayFeedback || 0.4;
  document.getElementById('delayFeedback').dispatchEvent(new Event('input'));
  document.getElementById('reverbMix').value = p.reverbMix || 30;
  document.getElementById('reverbMix').dispatchEvent(new Event('input'));

  updateFilter();
  drawADSR();

  document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
  document.querySelector(`.preset-btn[data-preset="${name}"]`).classList.add('active');
}

function updateNoteDisplay(note) {
  const el = document.getElementById('noteDisplay');
  el.textContent = note || '—';
}

// ═══════════════════════════════════════════════════════════
//  INIT
// ═══════════════════════════════════════════════════════════

buildKeyboard();
buildTutorial();
initBindings();
drawFilterViz();
drawADSR();

// Animation loop
let vizFrame = 0;
function animate() {
  vizFrame++;
  if (vizFrame % 3 === 0) drawWaveform();
  requestAnimationFrame(animate);
}
animate();

// Resize handler
window.addEventListener('resize', () => {
  drawFilterViz();
  drawADSR();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"7fdc9ec3-f832-4b94-9046-b3a7ba3d2d02","tokensIn":53075,"tokensOut":20290,"tokensTotal":73365,"turns":3,"toolCalls":2,"failedToolCalls":0,"timestamp":"2026-07-21T03:22:14.826Z"} -->