← TutSynth results

TutSynth

thinkingcap qwen3.6 27b · basic

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 47,262 bytes · SHA-256 8d113e6908c3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synth Explorer — Learn Synthesis Interactively</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');

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

  :root {
    --bg: #0a0a0f;
    --panel: #12121a;
    --panel-border: #1e1e2e;
    --accent: #6c5ce7;
    --accent-glow: rgba(108, 92, 231, 0.4);
    --text: #e0e0e8;
    --text-dim: #6b6b80;
    --success: #00cec9;
    --warning: #fdcb6e;
    --danger: #ff7675;
    --knob-size: 52px;
  }

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

  .app-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  header {
    text-align: center;
    padding: 30px 0 20px;
  }

  header h1 {
    font-size: 2.2em;
    font-weight: 700;
    background: linear-gradient(135deg, #6c5ce7, #a29bfe, #fd79a8);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    margin-bottom: 6px;
  }

  header p {
    color: var(--text-dim);
    font-size: 0.95em;
  }

  .progress-bar-container {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 20px auto;
    max-width: 600px;
  }

  .progress-track {
    flex: 1;
    height: 6px;
    background: var(--panel-border);
    border-radius: 3px;
    overflow: hidden;
  }

  .progress-fill {
    height: 100%;
    background: linear-gradient(90deg, #6c5ce7, #a29bfe);
    border-radius: 3px;
    transition: width 0.6s ease;
  }

  .progress-label {
    font-size: 0.8em;
    color: var(--text-dim);
    white-space: nowrap;
    font-family: 'JetBrains Mono', monospace;
  }

  .main-layout {
    display: grid;
    grid-template-columns: 320px 1fr;
    gap: 20px;
    margin-top: 16px;
  }

  @media (max-width: 900px) {
    .main-layout {
      grid-template-columns: 1fr;
    }
  }

  /* Tutorial Panel */
  .tutorial-panel {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 16px;
    padding: 24px;
    position: sticky;
    top: 20px;
    max-height: calc(100vh - 40px);
    overflow-y: auto;
  }

  .tutorial-panel::-webkit-scrollbar { width: 4px; }
  .tutorial-panel::-webkit-scrollbar-track { background: transparent; }
  .tutorial-panel::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 2px; }

  .step-indicator {
    display: flex;
    gap: 6px;
    margin-bottom: 20px;
  }

  .step-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--panel-border);
    transition: all 0.3s;
  }

  .step-dot.active {
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent-glow);
  }

  .step-dot.completed {
    background: var(--success);
  }

  .tutorial-title {
    font-size: 1.3em;
    font-weight: 600;
    margin-bottom: 12px;
    line-height: 1.3;
  }

  .tutorial-body {
    color: var(--text-dim);
    font-size: 0.9em;
    line-height: 1.7;
    margin-bottom: 20px;
  }

  .tutorial-body strong {
    color: var(--text);
  }

  .tutorial-body code {
    background: rgba(108, 92, 231, 0.15);
    padding: 2px 6px;
    border-radius: 4px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85em;
    color: #a29bfe;
  }

  .challenge-box {
    background: rgba(108, 92, 231, 0.08);
    border: 1px solid rgba(108, 92, 231, 0.2);
    border-radius: 10px;
    padding: 14px;
    margin-bottom: 16px;
  }

  .challenge-box h4 {
    font-size: 0.85em;
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 8px;
  }

  .challenge-box p {
    font-size: 0.85em;
    line-height: 1.6;
  }

  .nav-buttons {
    display: flex;
    gap: 10px;
  }

  .btn {
    padding: 10px 20px;
    border: none;
    border-radius: 8px;
    font-family: 'Inter', sans-serif;
    font-size: 0.9em;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
  }

  .btn-primary {
    background: var(--accent);
    color: white;
    flex: 1;
  }

  .btn-primary:hover {
    background: #7c6cf7;
    box-shadow: 0 4px 15px var(--accent-glow);
  }

  .btn-secondary {
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--panel-border);
  }

  .btn-secondary:hover {
    border-color: var(--text-dim);
    color: var(--text);
  }

  /* Synth Area */
  .synth-area {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  /* Visualizer */
  .visualizer-container {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 16px;
    padding: 20px;
    position: relative;
  }

  .visualizer-label {
    font-size: 0.75em;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--text-dim);
    margin-bottom: 12px;
  }

  #waveformCanvas {
    width: 100%;
    height: 140px;
    border-radius: 8px;
    background: rgba(0,0,0,0.3);
  }

  /* Control Modules */
  .modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
  }

  .module {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    padding: 18px;
    transition: all 0.3s;
    position: relative;
    overflow: hidden;
  }

  .module.locked {
    opacity: 0.35;
    pointer-events: none;
    filter: grayscale(0.8);
  }

  .module.unlocked-anim {
    animation: unlockPulse 0.6s ease;
  }

  @keyframes unlockPulse {
    0% { box-shadow: 0 0 0 0 var(--accent-glow); }
    50% { box-shadow: 0 0 30px 10px var(--accent-glow); }
    100% { box-shadow: none; }
  }

  .module-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
  }

  .module-name {
    font-size: 0.8em;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--text-dim);
    font-weight: 600;
  }

  .lock-icon {
    font-size: 0.9em;
    opacity: 0.5;
  }

  .module.unlocked-anim .lock-icon,
  .module:not(.locked) .lock-icon {
    display: none;
  }

  /* Waveform Selector */
  .wave-selector {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
  }

  .wave-btn {
    flex: 1;
    padding: 10px 6px;
    background: rgba(0,0,0,0.3);
    border: 2px solid transparent;
    border-radius: 8px;
    color: var(--text-dim);
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    font-family: 'Inter', sans-serif;
    font-size: 0.7em;
  }

  .wave-btn:hover { border-color: var(--panel-border); color: var(--text); }
  .wave-btn.active {
    border-color: var(--accent);
    color: var(--accent);
    background: rgba(108, 92, 231, 0.1);
  }

  .wave-icon {
    width: 40px;
    height: 20px;
  }

  /* Sliders */
  .param-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
  }

  .param-label {
    font-size: 0.75em;
    color: var(--text-dim);
    min-width: 28px;
    font-family: 'JetBrains Mono', monospace;
    text-align: right;
  }

  .param-slider {
    flex: 1;
    -webkit-appearance: none;
    appearance: none;
    height: 4px;
    background: var(--panel-border);
    border-radius: 2px;
    outline: none;
  }

  .param-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
    box-shadow: 0 2px 8px var(--accent-glow);
    transition: transform 0.15s;
  }

  .param-slider::-webkit-slider-thumb:hover {
    transform: scale(1.2);
  }

  .param-value {
    font-size: 0.75em;
    color: var(--text-dim);
    min-width: 40px;
    text-align: right;
    font-family: 'JetBrains Mono', monospace;
  }

  /* Keyboard */
  .keyboard-container {
    background: var(--panel);
    border: 1px solid var(--panel-border);
    border-radius: 16px;
    padding: 20px;
  }

  .keyboard-hint {
    font-size: 0.75em;
    color: var(--text-dim);
    margin-bottom: 14px;
    text-align: center;
  }

  .keyboard-hint kbd {
    background: rgba(255,255,255,0.08);
    padding: 2px 7px;
    border-radius: 4px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9em;
    border: 1px solid var(--panel-border);
  }

  .keyboard {
    display: flex;
    justify-content: center;
    gap: 3px;
    position: relative;
    height: 140px;
    user-select: none;
  }

  .key-white {
    width: 48px;
    height: 140px;
    background: linear-gradient(180deg, #f0f0f5, #d8d8e0);
    border: 1px solid #c0c0c8;
    border-radius: 0 0 6px 6px;
    cursor: pointer;
    transition: all 0.08s;
    position: relative;
    z-index: 1;
  }

  .key-white:hover { background: linear-gradient(180deg, #e8e8f0, #d0d0d8); }
  .key-white.active {
    background: linear-gradient(180deg, #a29bfe, #6c5ce7);
    border-color: #6c5ce7;
    transform: scaleY(0.98);
    transform-origin: top;
  }

  .key-black {
    width: 32px;
    height: 90px;
    background: linear-gradient(180deg, #2a2a35, #1a1a25);
    border: 1px solid #111;
    border-radius: 0 0 4px 4px;
    cursor: pointer;
    position: absolute;
    z-index: 2;
    transition: all 0.08s;
  }

  .key-black:hover { background: linear-gradient(180deg, #3a3a45, #2a2a35); }
  .key-black.active {
    background: linear-gradient(180deg, #6c5ce7, #4a3db5);
    transform: scaleY(0.97);
    transform-origin: top;
  }

  .key-label {
    position: absolute;
    bottom: 8px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.6em;
    color: #999;
    font-family: 'JetBrains Mono', monospace;
    pointer-events: none;
  }

  .key-black .key-label { color: #666; bottom: 6px; }

  /* ADSR Visual */
  .adsr-visual {
    margin-top: 10px;
    height: 50px;
    position: relative;
  }

  #adsrCanvas {
    width: 100%;
    height: 50px;
    border-radius: 6px;
    background: rgba(0,0,0,0.2);
  }

  /* Filter visual */
  .filter-visual {
    margin-top: 10px;
    height: 50px;
  }

  #filterCanvas {
    width: 100%;
    height: 50px;
    border-radius: 6px;
    background: rgba(0,0,0,0.2);
  }

  /* Notification */
  .notification {
    position: fixed;
    top: 20px;
    right: 20px;
    background: var(--panel);
    border: 1px solid var(--accent);
    border-radius: 12px;
    padding: 16px 20px;
    box-shadow: 0 8px 30px rgba(0,0,0,0.5);
    z-index: 1000;
    transform: translateX(120%);
    transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    max-width: 300px;
  }

  .notification.show { transform: translateX(0); }

  .notification h4 {
    color: var(--accent);
    font-size: 0.85em;
    margin-bottom: 4px;
  }

  .notification p {
    color: var(--text-dim);
    font-size: 0.8em;
    line-height: 1.5;
  }

  /* Toggle switch */
  .toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }

  .toggle-label {
    font-size: 0.75em;
    color: var(--text-dim);
  }

  .toggle-switch {
    width: 40px;
    height: 22px;
    background: var(--panel-border);
    border-radius: 11px;
    position: relative;
    cursor: pointer;
    transition: background 0.3s;
  }

  .toggle-switch.on { background: var(--accent); }

  .toggle-switch::after {
    content: '';
    width: 18px;
    height: 18px;
    background: white;
    border-radius: 50%;
    position: absolute;
    top: 2px;
    left: 2px;
    transition: transform 0.3s;
  }

  .toggle-switch.on::after { transform: translateX(18px); }

  /* Octave selector */
  .octave-control {
    display: flex;
    align-items: center;
    gap: 8px;
    justify-content: center;
    margin-top: 12px;
  }

  .octave-btn {
    width: 32px;
    height: 32px;
    border-radius: 6px;
    background: rgba(0,0,0,0.3);
    border: 1px solid var(--panel-border);
    color: var(--text-dim);
    cursor: pointer;
    font-size: 1em;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
  }

  .octave-btn:hover { border-color: var(--accent); color: var(--text); }

  .octave-display {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85em;
    color: var(--text);
    min-width: 60px;
    text-align: center;
  }

  /* Completed badge */
  .completed-badge {
    display: none;
    position: absolute;
    top: 12px;
    right: 12px;
    background: var(--success);
    color: #000;
    font-size: 0.65em;
    padding: 3px 8px;
    border-radius: 4px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .module.completed .completed-badge { display: block; }

  /* Scrollbar for body */
  ::-webkit-scrollbar { width: 8px; }
  ::-webkit-scrollbar-track { background: var(--bg); }
  ::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 4px; }
</style>
</head>
<body>

<div class="app-container">
  <header>
    <h1>🎹 Synth Explorer</h1>
    <p>An interactive guide to understanding synthesizers</p>
    <div class="progress-bar-container">
      <span class="progress-label" id="progressLabel">Step 1 / 6</span>
      <div class="progress-track">
        <div class="progress-fill" id="progressFill" style="width: 16.6%"></div>
      </div>
    </div>
  </header>

  <div class="main-layout">
    <!-- Tutorial Panel -->
    <div class="tutorial-panel" id="tutorialPanel">
      <div class="step-indicator" id="stepIndicator"></div>
      <h2 class="tutorial-title" id="tutorialTitle"></h2>
      <div class="tutorial-body" id="tutorialBody"></div>
      <div class="challenge-box" id="challengeBox">
        <h4>🎯 Try This</h4>
        <p id="challengeText"></p>
      </div>
      <div class="nav-buttons">
        <button class="btn btn-secondary" id="prevBtn" onclick="prevStep()">← Back</button>
        <button class="btn btn-primary" id="nextBtn" onclick="nextStep()">Next →</button>
      </div>
    </div>

    <!-- Synth Area -->
    <div class="synth-area">
      <!-- Visualizer -->
      <div class="visualizer-container">
        <div class="visualizer-label">Waveform Output</div>
        <canvas id="waveformCanvas"></canvas>
      </div>

      <!-- Modules -->
      <div class="modules-grid" id="modulesGrid">
        <!-- Oscillator Module -->
        <div class="module" id="mod-oscillator">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">Oscillator</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="wave-selector" id="waveSelector">
            <button class="wave-btn active" data-wave="sine" onclick="setWaveform('sine')">
              <svg class="wave-icon" viewBox="0 0 40 20"><path d="M0,10 Q10,0 20,10 Q30,20 40,10" fill="none" stroke="currentColor" stroke-width="2"/></svg>
              Sine
            </button>
            <button class="wave-btn" data-wave="triangle" onclick="setWaveform('triangle')">
              <svg class="wave-icon" viewBox="0 0 40 20"><path d="M0,10 L10,2 L20,18 L30,2 L40,10" fill="none" stroke="currentColor" stroke-width="2"/></svg>
              Triangle
            </button>
            <button class="wave-btn" data-wave="sawtooth" onclick="setWaveform('sawtooth')">
              <svg class="wave-icon" viewBox="0 0 40 20"><path d="M0,18 L13,2 L13,18 L26,2 L26,18 L40,2" fill="none" stroke="currentColor" stroke-width="2"/></svg>
              Sawtooth
            </button>
            <button class="wave-btn" data-wave="square" onclick="setWaveform('square')">
              <svg class="wave-icon" viewBox="0 0 40 20"><path d="M0,18 L0,2 L13,2 L13,18 L26,18 L26,2 L40,2 L40,18" fill="none" stroke="currentColor" stroke-width="2"/></svg>
              Square
            </button>
          </div>
        </div>

        <!-- Envelope Module -->
        <div class="module locked" id="mod-envelope">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">Envelope (ADSR)</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="param-row">
            <span class="param-label">ATK</span>
            <input type="range" class="param-slider" id="attackSlider" min="0.01" max="2" step="0.01" value="0.05" oninput="updateEnvelope()">
            <span class="param-value" id="attackVal">0.05</span>
          </div>
          <div class="param-row">
            <span class="param-label">DEC</span>
            <input type="range" class="param-slider" id="decaySlider" min="0.01" max="2" step="0.01" value="0.3" oninput="updateEnvelope()">
            <span class="param-value" id="decayVal">0.30</span>
          </div>
          <div class="param-row">
            <span class="param-label">SUS</span>
            <input type="range" class="param-slider" id="sustainSlider" min="0" max="1" step="0.01" value="0.7" oninput="updateEnvelope()">
            <span class="param-value" id="sustainVal">0.70</span>
          </div>
          <div class="param-row">
            <span class="param-label">REL</span>
            <input type="range" class="param-slider" id="releaseSlider" min="0.01" max="3" step="0.01" value="0.5" oninput="updateEnvelope()">
            <span class="param-value" id="releaseVal">0.50</span>
          </div>
          <div class="adsr-visual">
            <canvas id="adsrCanvas"></canvas>
          </div>
        </div>

        <!-- Filter Module -->
        <div class="module locked" id="mod-filter">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">Filter</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="param-row">
            <span class="param-label">CUT</span>
            <input type="range" class="param-slider" id="cutoffSlider" min="100" max="15000" step="10" value="8000" oninput="updateFilter()">
            <span class="param-value" id="cutoffVal">8000</span>
          </div>
          <div class="param-row">
            <span class="param-label">RES</span>
            <input type="range" class="param-slider" id="resonanceSlider" min="0" max="25" step="0.1" value="1" oninput="updateFilter()">
            <span class="param-value" id="resonanceVal">1.0</span>
          </div>
          <div class="filter-visual">
            <canvas id="filterCanvas"></canvas>
          </div>
        </div>

        <!-- LFO Module -->
        <div class="module locked" id="mod-lfo">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">LFO</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="toggle-row">
            <span class="toggle-label">Enable LFO</span>
            <div class="toggle-switch" id="lfoToggle" onclick="toggleLFO()"></div>
          </div>
          <div class="param-row">
            <span class="param-label">RATE</span>
            <input type="range" class="param-slider" id="lfoRateSlider" min="0.1" max="20" step="0.1" value="5" oninput="updateLFO()">
            <span class="param-value" id="lfoRateVal">5.0</span>
          </div>
          <div class="param-row">
            <span class="param-label">DEPTH</span>
            <input type="range" class="param-slider" id="lfoDepthSlider" min="0" max="100" step="1" value="30" oninput="updateLFO()">
            <span class="param-value" id="lfoDepthVal">30</span>
          </div>
        </div>

        <!-- Effects Module -->
        <div class="module locked" id="mod-effects">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">Effects</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="toggle-row">
            <span class="toggle-label">Delay</span>
            <div class="toggle-switch" id="delayToggle" onclick="toggleEffect('delay')"></div>
          </div>
          <div class="param-row">
            <span class="param-label">TIME</span>
            <input type="range" class="param-slider" id="delayTimeSlider" min="0.01" max="1" step="0.01" value="0.3" oninput="updateEffect('delay')">
            <span class="param-value" id="delayTimeVal">0.30</span>
          </div>
          <div class="param-row">
            <span class="param-label">FEED</span>
            <input type="range" class="param-slider" id="delayFeedbackSlider" min="0" max="0.9" step="0.01" value="0.4" oninput="updateEffect('delay')">
            <span class="param-value" id="delayFeedVal">0.40</span>
          </div>
          <div class="toggle-row" style="margin-top: 8px;">
            <span class="toggle-label">Reverb</span>
            <div class="toggle-switch" id="reverbToggle" onclick="toggleEffect('reverb')"></div>
          </div>
        </div>

        <!-- Master Module -->
        <div class="module locked" id="mod-master">
          <span class="completed-badge">✓ Done</span>
          <div class="module-header">
            <span class="module-name">Master</span>
            <span class="lock-icon">🔒</span>
          </div>
          <div class="param-row">
            <span class="param-label">VOL</span>
            <input type="range" class="param-slider" id="volumeSlider" min="0" max="1" step="0.01" value="0.5" oninput="updateMaster()">
            <span class="param-value" id="volumeVal">0.50</span>
          </div>
        </div>
      </div>

      <!-- Keyboard -->
      <div class="keyboard-container">
        <div class="keyboard-hint">
          Play with your mouse or keyboard: <kbd>A</kbd>–<kbd>L</kbd> for white keys, <kbd>W</kbd><kbd>E</kbd><kbd>T</kbd><kbd>Y</kbd><kbd>U</kbd> for black keys &nbsp;|&nbsp; Octave: <kbd>Z</kbd>/<kbd>X</kbd>
        </div>
        <div class="keyboard" id="keyboard"></div>
        <div class="octave-control">
          <button class="octave-btn" onclick="changeOctave(-1)">−</button>
          <span class="octave-display" id="octaveDisplay">Oct 4</span>
          <button class="octave-btn" onclick="changeOctave(1)">+</button>
        </div>
      </div>
    </div>
  </div>
</div>

<div class="notification" id="notification">
  <h4 id="notifTitle"></h4>
  <p id="notifText"></p>
</div>

<script>
// ==================== AUDIO ENGINE ====================
let audioCtx = null;
let masterGain = null;
let filterNode = null;
let delayNode = null;
let feedbackNode = null;
let reverbNode = null;
let dryGain = null;
let wetGain = null;
let lfoNode = null;
let lfoGain = null;
let analyser = null;

const activeNotes = {};
let currentWaveform = 'sine';
let octave = 4;
let envelope = { attack: 0.05, decay: 0.3, sustain: 0.7, release: 0.5 };
let filterSettings = { cutoff: 8000, resonance: 1 };
let lfoSettings = { enabled: false, rate: 5, depth: 30 };
let effectSettings = { delay: { enabled: false, time: 0.3, feedback: 0.4 }, reverb: { enabled: false } };

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

  masterGain = audioCtx.createGain();
  masterGain.gain.value = 0.5;

  filterNode = audioCtx.createBiquadFilter();
  filterNode.type = 'lowpass';
  filterNode.frequency.value = 8000;
  filterNode.Q.value = 1;

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

  // Delay setup
  delayNode = audioCtx.createDelay(2);
  delayNode.delayTime.value = 0.3;
  feedbackNode = audioCtx.createGain();
  feedbackNode.gain.value = 0.4;
  dryGain = audioCtx.createGain();
  dryGain.gain.value = 1;
  wetGain = audioCtx.createGain();
  wetGain.gain.value = 0;

  // Reverb setup (convolver with generated impulse)
  reverbNode = audioCtx.createConvolver();
  reverbNode.buffer = generateReverbImpulse(2, 3);

  // Routing: source -> filter -> [dry] -> masterGain -> analyser -> destination
  //                    -> [delay -> feedback loop] -> wet -> masterGain
  filterNode.connect(dryGain);
  dryGain.connect(masterGain);

  filterNode.connect(delayNode);
  delayNode.connect(feedbackNode);
  feedbackNode.connect(delayNode);
  delayNode.connect(wetGain);
  wetGain.connect(masterGain);

  // Reverb routing
  const reverbDry = audioCtx.createGain();
  const reverbWet = audioCtx.createGain();
  reverbWet.gain.value = 0;
  masterGain.disconnect();
  masterGain.connect(reverbDry);
  masterGain.connect(reverbNode);
  reverbNode.connect(reverbWet);
  reverbDry.connect(analyser);
  reverbWet.connect(analyser);
  analyser.connect(audioCtx.destination);

  // LFO setup (modulates filter frequency)
  lfoNode = audioCtx.createOscillator();
  lfoGain = audioCtx.createGain();
  lfoGain.gain.value = 0;
  lfoNode.connect(lfoGain);
  lfoGain.connect(filterNode.frequency);
  lfoNode.start();

  window._reverbDry = reverbDry;
  window._reverbWet = reverbWet;
}

function generateReverbImpulse(length, decay) {
  const rate = audioCtx.sampleRate;
  const lengthInSamples = Math.floor(rate * length);
  const impulse = audioCtx.createBuffer(2, lengthInSamples, rate);
  for (let ch = 0; ch < 2; ch++) {
    const data = impulse.getChannelData(ch);
    for (let i = 0; i < lengthInSamples; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / lengthInSamples, decay);
    }
  }
  return impulse;
}

function noteOn(note, freq) {
  initAudio();
  if (activeNotes[note]) return;

  const osc = audioCtx.createOscillator();
  osc.type = currentWaveform;
  osc.frequency.value = freq;

  const envGain = audioCtx.createGain();
  envGain.gain.setValueAtTime(0, audioCtx.currentTime);
  envGain.gain.linearRampToValueAtTime(1, audioCtx.currentTime + envelope.attack);
  envGain.gain.linearRampToValueAtTime(envelope.sustain, audioCtx.currentTime + envelope.attack + envelope.decay);

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

  activeNotes[note] = { osc, envGain };
}

function noteOff(note) {
  const n = activeNotes[note];
  if (!n) return;

  n.envGain.gain.cancelScheduledValues(audioCtx.currentTime);
  n.envGain.gain.setValueAtTime(n.envGain.gain.value, audioCtx.currentTime);
  n.envGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + envelope.release);
  n.osc.stop(audioCtx.currentTime + envelope.release + 0.1);

  setTimeout(() => {
    try { n.envGain.disconnect(); } catch(e) {}
  }, (envelope.release + 0.2) * 1000);

  delete activeNotes[note];
}

// ==================== NOTE FREQUENCIES ====================
const noteNames = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];

function getFreq(note, oct) {
  const idx = noteNames.indexOf(note);
  if (idx === -1) return 440;
  const midi = (oct + 1) * 12 + idx;
  return 440 * Math.pow(2, (midi - 69) / 12);
}

// ==================== KEYBOARD SETUP ====================
const whiteKeys = ['C','D','E','F','G','A','B'];
const blackKeyMap = { 'C#': 0, 'D#': 1, 'F#': 3, 'G#': 4, 'A#': 5 };

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

  whiteKeys.forEach((note, i) => {
    const key = document.createElement('div');
    key.className = 'key-white';
    key.dataset.note = note;
    key.dataset.index = i;
    key.addEventListener('mousedown', () => playKey(note));
    key.addEventListener('mouseup', () => stopKey(note));
    key.addEventListener('mouseleave', () => stopKey(note));
    key.addEventListener('touchstart', (e) => { e.preventDefault(); playKey(note); });
    key.addEventListener('touchend', () => stopKey(note));

    const label = document.createElement('span');
    label.className = 'key-label';
    label.textContent = note;
    key.appendChild(label);
    kb.appendChild(key);
  });

  // Black keys positioned absolutely
  const blackNotes = ['C#','D#','F#','G#','A#'];
  const whiteKeyWidth = 48;
  const gap = 3;

  blackNotes.forEach((note) => {
    const idx = blackKeyMap[note];
    if (idx === undefined) return;
    const key = document.createElement('div');
    key.className = 'key-black';
    key.dataset.note = note;
    const leftPos = (idx + 1) * (whiteKeyWidth + gap) - 18;
    key.style.left = leftPos + 'px';
    key.addEventListener('mousedown', () => playKey(note));
    key.addEventListener('mouseup', () => stopKey(note));
    key.addEventListener('mouseleave', () => stopKey(note));
    key.addEventListener('touchstart', (e) => { e.preventDefault(); playKey(note); });
    key.addEventListener('touchend', () => stopKey(note));

    const label = document.createElement('span');
    label.className = 'key-label';
    label.textContent = note;
    key.appendChild(label);
    kb.appendChild(key);
  });
}

function playKey(note) {
  initAudio();
  const freq = getFreq(note, octave);
  noteOn(note, freq);
  highlightKey(note, true);
}

function stopKey(note) {
  noteOff(note);
  highlightKey(note, false);
}

function highlightKey(note, on) {
  document.querySelectorAll(`[data-note="${note}"]`).forEach(el => {
    el.classList.toggle('active', on);
  });
}

// Computer keyboard mapping
const keyMap = {
  'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E',
  'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A',
  'u': 'A#', 'j': 'B'
};

const pressedKeys = new Set();

document.addEventListener('keydown', (e) => {
  if (e.repeat) return;
  const k = e.key.toLowerCase();
  if (k === 'z') { changeOctave(-1); return; }
  if (k === 'x') { changeOctave(1); return; }
  if (keyMap[k] && !pressedKeys.has(k)) {
    pressedKeys.add(k);
    playKey(keyMap[k]);
  }
});

document.addEventListener('keyup', (e) => {
  const k = e.key.toLowerCase();
  if (keyMap[k]) {
    pressedKeys.delete(k);
    stopKey(keyMap[k]);
  }
});

function changeOctave(dir) {
  octave = Math.max(1, Math.min(7, octave + dir));
  document.getElementById('octaveDisplay').textContent = `Oct ${octave}`;
}

// ==================== PARAMETER UPDATES ====================
function setWaveform(wave) {
  currentWaveform = wave;
  document.querySelectorAll('.wave-btn').forEach(b => b.classList.toggle('active', b.dataset.wave === wave));
}

function updateEnvelope() {
  envelope.attack = parseFloat(document.getElementById('attackSlider').value);
  envelope.decay = parseFloat(document.getElementById('decaySlider').value);
  envelope.sustain = parseFloat(document.getElementById('sustainSlider').value);
  envelope.release = parseFloat(document.getElementById('releaseSlider').value);

  document.getElementById('attackVal').textContent = envelope.attack.toFixed(2);
  document.getElementById('decayVal').textContent = envelope.decay.toFixed(2);
  document.getElementById('sustainVal').textContent = envelope.sustain.toFixed(2);
  document.getElementById('releaseVal').textContent = envelope.release.toFixed(2);

  drawADSR();
}

function updateFilter() {
  filterSettings.cutoff = parseFloat(document.getElementById('cutoffSlider').value);
  filterSettings.resonance = parseFloat(document.getElementById('resonanceSlider').value);

  document.getElementById('cutoffVal').textContent = Math.round(filterSettings.cutoff);
  document.getElementById('resonanceVal').textContent = filterSettings.resonance.toFixed(1);

  if (filterNode) {
    filterNode.frequency.setTargetAtTime(filterSettings.cutoff, audioCtx.currentTime, 0.02);
    filterNode.Q.setTargetAtTime(filterSettings.resonance, audioCtx.currentTime, 0.02);
  }
  drawFilterResponse();
}

function updateLFO() {
  lfoSettings.rate = parseFloat(document.getElementById('lfoRateSlider').value);
  lfoSettings.depth = parseFloat(document.getElementById('lfoDepthSlider').value);

  document.getElementById('lfoRateVal').textContent = lfoSettings.rate.toFixed(1);
  document.getElementById('lfoDepthVal').textContent = Math.round(lfoSettings.depth);

  if (lfoNode) {
    lfoNode.frequency.setTargetAtTime(lfoSettings.rate, audioCtx.currentTime, 0.02);
    const depthHz = (filterSettings.cutoff * lfoSettings.depth / 100) * 0.5;
    lfoGain.gain.setTargetAtTime(depthHz, audioCtx.currentTime, 0.02);
  }
}

function toggleLFO() {
  lfoSettings.enabled = !lfoSettings.enabled;
  document.getElementById('lfoToggle').classList.toggle('on', lfoSettings.enabled);
  if (lfoGain) {
    const depthHz = lfoSettings.enabled ? (filterSettings.cutoff * lfoSettings.depth / 100) * 0.5 : 0;
    lfoGain.gain.setTargetAtTime(depthHz, audioCtx.currentTime, 0.02);
  }
}

function toggleEffect(type) {
  effectSettings[type].enabled = !effectSettings[type].enabled;
  document.getElementById(type + 'Toggle').classList.toggle('on', effectSettings[type].enabled);

  if (type === 'delay' && wetGain) {
    wetGain.gain.setTargetAtTime(effectSettings.delay.enabled ? 0.5 : 0, audioCtx.currentTime, 0.02);
  }
  if (type === 'reverb' && window._reverbWet) {
    window._reverbWet.gain.setTargetAtTime(effectSettings.reverb.enabled ? 0.6 : 0, audioCtx.currentTime, 0.02);
  }
}

function updateEffect(type) {
  if (type === 'delay') {
    effectSettings.delay.time = parseFloat(document.getElementById('delayTimeSlider').value);
    effectSettings.delay.feedback = parseFloat(document.getElementById('delayFeedbackSlider').value);
    document.getElementById('delayTimeVal').textContent = effectSettings.delay.time.toFixed(2);
    document.getElementById('delayFeedVal').textContent = effectSettings.delay.feedback.toFixed(2);

    if (delayNode) {
      delayNode.delayTime.setTargetAtTime(effectSettings.delay.time, audioCtx.currentTime, 0.02);
      feedbackNode.gain.setTargetAtTime(effectSettings.delay.feedback, audioCtx.currentTime, 0.02);
    }
  }
}

function updateMaster() {
  const vol = parseFloat(document.getElementById('volumeSlider').value);
  document.getElementById('volumeVal').textContent = vol.toFixed(2);
  if (masterGain) masterGain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.02);
}

// ==================== VISUALIZERS ====================
function drawWaveform() {
  const canvas = document.getElementById('waveformCanvas');
  const ctx = canvas.getContext('2d');
  canvas.width = canvas.offsetWidth * 2;
  canvas.height = canvas.offsetHeight * 2;
  ctx.scale(1, 1);

  function render() {
    requestAnimationFrame(render);
    if (!analyser) return;

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

    ctx.fillStyle = 'rgba(10, 10, 15, 0.3)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.lineWidth = 2;
    ctx.strokeStyle = '#6c5ce7';
    ctx.shadowColor = '#6c5ce7';
    ctx.shadowBlur = 8;
    ctx.beginPath();

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

function drawADSR() {
  const canvas = document.getElementById('adsrCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  canvas.width = canvas.offsetWidth * 2;
  canvas.height = canvas.offsetHeight * 2;

  const w = canvas.width;
  const h = canvas.height;
  const pad = 10;

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

  // Calculate envelope shape
  const totalDuration = envelope.attack + envelope.decay + 1.5 + envelope.release;
  const scaleX = (w - pad * 2) / totalDuration;

  let x = pad;
  const points = [];

  // Attack
  points.push({ x, y: h - pad });
  x += envelope.attack * scaleX;
  points.push({ x, y: pad });

  // Decay
  x += envelope.decay * scaleX;
  points.push({ x, y: pad + (h - pad * 2) * (1 - envelope.sustain) });

  // Sustain
  x += 1.5 * scaleX;
  points.push({ x, y: pad + (h - pad * 2) * (1 - envelope.sustain) });

  // Release
  x += envelope.release * scaleX;
  points.push({ x, y: h - pad });

  // Draw fill
  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
  ctx.closePath();
  ctx.fillStyle = 'rgba(108, 92, 231, 0.15)';
  ctx.fill();

  // Draw line
  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
  ctx.strokeStyle = '#6c5ce7';
  ctx.lineWidth = 2;
  ctx.stroke();

  // Labels
  ctx.fillStyle = 'rgba(255,255,255,0.3)';
  ctx.font = `${Math.floor(h * 0.18)}px JetBrains Mono`;
  const labels = ['A', 'D', 'S', 'R'];
  let lx = pad + envelope.attack * scaleX;
  ctx.fillText(labels[0], lx - 4, h - 2);
  lx += envelope.decay * scaleX / 2;
  ctx.fillText(labels[1], lx - 3, h - 2);
  lx += (1.5 * scaleX) / 2;
  ctx.fillText(labels[2], lx - 3, h - 2);
  lx += (envelope.release * scaleX) / 2;
  ctx.fillText(labels[3], lx - 3, h - 2);
}

function drawFilterResponse() {
  const canvas = document.getElementById('filterCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  canvas.width = canvas.offsetWidth * 2;
  canvas.height = canvas.offsetHeight * 2;

  const w = canvas.width;
  const h = canvas.height;
  const pad = 10;

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

  // Draw lowpass filter response curve
  const cutoffFreq = filterSettings.cutoff;
  const resonance = filterSettings.resonance;

  ctx.beginPath();
  for (let px = 0; px < w - pad * 2; px++) {
    const freq = Math.pow(10, 2 + (px / (w - pad * 2)) * 4); // 100Hz to 10kHz
    let gain;

    if (freq === cutoffFreq) {
      gain = resonance > 0 ? Math.min(resonance / 5, 1) : 0.707;
    } else {
      const ratio = freq / cutoffFreq;
      gain = 1 / Math.sqrt(1 + Math.pow(ratio, 2));
      if (resonance > 1 && Math.abs(Math.log2(ratio)) < 0.5) {
        gain *= (1 + resonance * 0.3);
      }
    }

    const y = h - pad - gain * (h - pad * 2);
    if (px === 0) ctx.moveTo(pad + px, y);
    else ctx.lineTo(pad + px, y);
  }

  ctx.strokeStyle = '#6c5ce7';
  ctx.lineWidth = 2;
  ctx.stroke();

  // Fill under curve
  ctx.lineTo(w - pad, h - pad);
  ctx.lineTo(pad, h - pad);
  ctx.closePath();
  ctx.fillStyle = 'rgba(108, 92, 231, 0.1)';
  ctx.fill();

  // Cutoff marker
  const cutoffPx = ((Math.log10(cutoffFreq) - 2) / 4) * (w - pad * 2);
  if (cutoffPx > 0 && cutoffPx < w - pad * 2) {
    ctx.beginPath();
    ctx.moveTo(pad + cutoffPx, pad);
    ctx.lineTo(pad + cutoffPx, h - pad);
    ctx.strokeStyle = 'rgba(108, 92, 231, 0.4)';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.stroke();
    ctx.setLineDash([]);

    ctx.fillStyle = '#a29bfe';
    ctx.font = `${Math.floor(h * 0.18)}px JetBrains Mono`;
    ctx.fillText(Math.round(cutoffFreq) + 'Hz', pad + cutoffPx + 4, h - 2);
  }
}

// ==================== TUTORIAL SYSTEM ====================
const steps = [
  {
    title: "🎵 Welcome to Synthesis!",
    body: `A <strong>synthesizer</strong> generates sound electronically. At its heart is an <strong>oscillator</strong> — a circuit that creates a repeating waveform. The shape of this wave determines the <strong>timbre</strong> (tone color) you hear.<br><br>Every synth sound starts here. Let's explore!`,
    challenge: "Click on the keyboard keys below to play notes. Try each waveform type — notice how dramatically they change the sound!",
    unlockModule: 'oscillator',
    checkComplete: () => true // Always completable for step 1
  },
  {
    title: "📈 The Envelope (ADSR)",
    body: `An <strong>envelope</strong> shapes how a sound evolves over time. <code>ADSR</code> stands for:<br><br>• <strong>A</strong>tack — how fast the sound reaches full volume<br>• <strong>D</strong>ecay — how quickly it drops to sustain level<br>• <strong>S</strong>ustain — the held volume while you keep the key pressed<br>• <strong>R</strong>elease — how long it fades after you release<br><br>This is why a piano sounds different from an organ!`,
    challenge: "Try setting Attack to 0.5s and Release to 2s for a soft, pad-like sound. Then try Attack=0.01, Decay=0.1, Sustain=0.1 for a percussive pluck!",
    unlockModule: 'envelope',
    checkComplete: () => {
      const atk = parseFloat(document.getElementById('attackSlider').value);
      const rel = parseFloat(document.getElementById('releaseSlider').value);
      return atk > 0.2 || rel > 1; // User has experimented with envelope
    }
  },
  {
    title: "🔊 The Filter",
    body: `A <strong>filter</strong> removes certain frequencies from the sound. A <strong>low-pass filter</strong> lets low frequencies through and blocks high ones.<br><br><code>Cutoff</code> sets where the filter starts cutting. <code>Resonance</code> boosts frequencies near the cutoff, creating that classic "squelchy" synth sound.<br><br>Filters are what make a synth go from bright to dark — essential for expressive sounds!`,
    challenge: "Start with Cutoff at 2000Hz and slowly raise it while holding a note. Then crank Resonance up to 15+ and sweep the cutoff again!",
    unlockModule: 'filter',
    checkComplete: () => {
      const cut = parseFloat(document.getElementById('cutoffSlider').value);
      return cut < 4000 || cut > 12000; // User has moved cutoff significantly
    }
  },
  {
    title: "🔄 LFO — Low Frequency Oscillator",
    body: `An <strong>LFO</strong> is a slow oscillator that doesn't produce sound itself. Instead, it <strong>modulates</strong> (changes) other parameters over time.<br><br>When an LFO modulates pitch → you get <strong>vibrato</strong>.<br>When it modulates volume → you get <strong>tremolo</strong>.<br><br>In this synth, the LFO modulates the filter cutoff for a pulsing effect!`,
    challenge: "Enable the LFO and try different rates. Slow (~2Hz) gives a gentle pulse. Fast (~10Hz+) creates a throbbing effect. Adjust depth to control intensity!",
    unlockModule: 'lfo',
    checkComplete: () => lfoSettings.enabled && (lfoSettings.rate < 3 || lfoSettings.rate > 8)
  },
  {
    title: "✨ Effects — Delay & Reverb",
    body: `<strong>Effects</strong> add space and depth to your sound.<br><br><code>Delay</code> repeats the signal after a time interval. <em>Feedback</em> controls how many repeats you get.<br><br><code>Reverb</code> simulates the sound of being in a physical space — a small room or a huge hall.<br><br>Effects transform a dry, thin sound into something lush and atmospheric!`,
    challenge: "Enable both Delay and Reverb. Set delay time to 0.4s with feedback around 0.5 for a spacious echo effect!",
    unlockModule: 'effects',
    checkComplete: () => (effectSettings.delay.enabled || effectSettings.reverb.enabled)
  },
  {
    title: "🎛️ Master & You're a Synth Wizard!",
    body: `The <strong>Master Volume</strong> controls the overall output level. Always keep this reasonable to protect your ears!<br><br>You've now explored all the core building blocks of synthesis:<br><br>✅ Oscillator — generates the raw sound<br>✅ Envelope — shapes it over time<br>✅ Filter — sculpts its frequency content<br>✅ LFO — adds movement and modulation<br>✅ Effects — adds space and atmosphere<br><br>Mix these together creatively to design your own sounds!`,
    challenge: "Design a unique patch! Combine everything you've learned — pick a waveform, shape the envelope, set a filter, add LFO modulation, and layer on effects. Make something that sounds like YOU!",
    unlockModule: 'master',
    checkComplete: () => true
  }
];

let currentStep = 0;
let completedSteps = new Set();

function initTutorial() {
  const indicator = document.getElementById('stepIndicator');
  indicator.innerHTML = '';
  steps.forEach((_, i) => {
    const dot = document.createElement('div');
    dot.className = 'step-dot' + (i === 0 ? ' active' : '');
    indicator.appendChild(dot);
  });

  loadStep(0);
}

function loadStep(index) {
  currentStep = index;
  const step = steps[index];

  document.getElementById('tutorialTitle').textContent = step.title;
  document.getElementById('tutorialBody').innerHTML = step.body;
  document.getElementById('challengeText').textContent = step.challenge;

  // Update progress
  document.getElementById('progressLabel').textContent = `Step ${index + 1} / ${steps.length}`;
  document.getElementById('progressFill').style.width = ((index + 1) / steps.length * 100) + '%';

  // Update step dots
  document.querySelectorAll('.step-dot').forEach((dot, i) => {
    dot.className = 'step-dot';
    if (i < index) dot.classList.add('completed');
    if (i === index) dot.classList.add('active');
  });

  // Unlock module for this step
  if (step.unlockModule && !document.getElementById('mod-' + step.unlockModule).classList.contains('locked')) {
    // Already unlocked
  } else if (step.unlockModule) {
    unlockModule(step.unlockModule);
  }

  // Button states
  document.getElementById('prevBtn').style.visibility = index === 0 ? 'hidden' : 'visible';
  document.getElementById('nextBtn').textContent = index === steps.length - 1 ? '🎉 Finish!' : 'Next →';
}

function unlockModule(name) {
  const mod = document.getElementById('mod-' + name);
  if (!mod) return;
  mod.classList.remove('locked');
  mod.classList.add('unlocked-anim');
  setTimeout(() => mod.classList.remove('unlocked-anim'), 700);

  showNotification(`🔓 ${name.charAt(0).toUpperCase() + name.slice(1)} Unlocked!`, 'Try adjusting the controls in this module.');
}

function nextStep() {
  // Check if current step is "complete" enough to advance
  const step = steps[currentStep];
  if (step.checkComplete && !step.checkComplete()) {
    showNotification('🎯 Keep Exploring!', 'Try the challenge before moving on — experiment with the controls!');
    return;
  }

  completedSteps.add(currentStep);
  const mod = document.getElementById('mod-' + steps[currentStep].unlockModule);
  if (mod) mod.classList.add('completed');

  if (currentStep < steps.length - 1) {
    loadStep(currentStep + 1);
  } else {
    showNotification('🎉 Congratulations!', 'You\'ve explored all the fundamentals of synthesis! Keep experimenting and designing your own sounds.');
  }
}

function prevStep() {
  if (currentStep > 0) {
    loadStep(currentStep - 1);
  }
}

// ==================== NOTIFICATIONS ====================
let notifTimeout = null;

function showNotification(title, text) {
  const el = document.getElementById('notification');
  document.getElementById('notifTitle').textContent = title;
  document.getElementById('notifText').textContent = text;
  el.classList.add('show');
  clearTimeout(notifTimeout);
  notifTimeout = setTimeout(() => el.classList.remove('show'), 4000);
}

// ==================== INIT ====================
buildKeyboard();
initTutorial();
drawWaveform();
drawADSR();
drawFilterResponse();

window.addEventListener('resize', () => {
  drawADSR();
  drawFilterResponse();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"89a88119-78b0-4dc2-9187-c04c8cb91b1e","tokensIn":217,"tokensOut":15400,"tokensTotal":15617,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:30:34.489Z"} -->