← TutSynth results

TutSynth

inkling · 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 39,897 bytes · SHA-256 f55b282c7098
<!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 Synthesis</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&family=Outfit:wght@300;500;800&display=swap');

:root {
  --bg: #0b0d12;
  --surface: #12131a;
  --surface-2: #181a26;
  --surface-3: #22243a;
  --text: #e8edf5;
  --text-dim: #9aa0b8;
  --text-muted: #5a6380;
  --accent: #f0a060;
  --accent-2: #6ec1e4;
  --accent-3: #e070a0;
  --accent-4: #b0e070;
  --border: rgba(255,255,255,0.06);
  --border-light: rgba(255,255,255,0.1);
  --radius: 14px;
  --radius-sm: 8px;
  --shadow: 0 12px 40px rgba(0,0,0,0.5);
}

* { margin:0; padding:0; box-sizing:border-box; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: 'Outfit', sans-serif;
  overflow-x: hidden;
  min-height: 100vh;
}

/* Grid background */
.bg-grid {
  position: fixed; top:0; left:0; right:0; bottom:0;
  background-image:
    linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);
  background-size: 50px 50px;
  pointer-events: none; z-index: 0;
}

/* Header */
header {
  position: relative; z-index: 10;
  padding: 32px 48px;
  display: flex; align-items: center; justify-content: space-between;
  border-bottom: 1px solid var(--border);
}
.brand {
  display: flex; align-items: center; gap: 14px;
}
.brand-icon {
  width: 44px; height: 44px; border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--accent), var(--accent-3));
  display: grid; place-items: center; font-size: 22px; box-shadow: 0 4px 20px rgba(240,160,96,0.3);
}
.brand h1 { font-size: 1.6rem; letter-spacing: -0.03em; font-weight: 800; line-height: 1.1; }
.brand h1 span { color: var(--accent); }
.brand p { color: var(--text-muted); font-size: 0.85rem; margin-top: 3px; }

/* Main Layout */
main { position: relative; z-index: 1; max-width: 1440px; margin: 0 auto; padding: 32px 48px 80px; }

/* Progress / Steps */
.progress-bar {
  display: flex; gap: 8px; margin: 0 0 24px; align-items: center;
}
.step-dot {
  width: 36px; height: 36px; border-radius: 50%;
  border: 2px solid var(--border-light);
  background: var(--surface);
  display: grid; place-items: center;
  font-family: 'JetBrains Mono', monospace; font-size: 0.7rem;
  font-weight: 700; color: var(--text-muted); transition: 0.3s;
  position: relative; cursor: pointer;
}
.step-dot.active { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px rgba(240,160,96,0.15); }
.step-dot.completed { border-color: var(--accent-4); color: var(--accent-4); background: rgba(176,224,112,0.1); }
.step-dot::after { content: ''; position: absolute; right: -20px; top: 50%; width: 12px; height: 2px; background: var(--border-light); }
.step-dot:last-child::after { display: none; }

.step-labels { display: flex; gap: 16px; margin-left: 4px; }
.step-labels span { font-size: 0.7rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; font-weight: 400; transition: 0.3s; }
.step-labels span.active { color: var(--accent); font-weight: 700; }

/* Section cards */
.section-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 32px; margin-bottom: 24px; box-shadow: var(--shadow);
  transition: border-color 0.3s, box-shadow 0.3s;
  position: relative; overflow: hidden;
}
.section-card.active-section { border-color: rgba(240,160,96,0.3); box-shadow: 0 0 0 1px rgba(240,160,96,0.15), var(--shadow); }
.section-card::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--accent), var(--accent-3), var(--accent-2));
  opacity: 0.5;
}

.section-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
.section-number {
  width: 36px; height: 36px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  display: grid; place-items: center; font-family: 'JetBrains Mono', monospace;
  font-weight: 700; font-size: 0.85rem; color: #fff; flex-shrink: 0;
}
.section-header h2 { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.01em; }
.section-header h2 span { color: var(--text-muted); font-weight: 300; font-size: 0.9rem; }
.section-card p { color: var(--text-dim); font-size: 0.92rem; line-height: 1.6; margin-bottom: 12px; }
.section-card p b { color: var(--text); font-weight: 600; }

/* Keyboard */
.keyboard-wrap {
  background: var(--surface-2); border-radius: var(--radius-sm);
  border: 1px solid var(--border); overflow: hidden;
  margin-top: 16px;
}
.keyboard-row { display: flex; }
.key {
  flex: 1; height: 70px; background: linear-gradient(180deg, #232537, #1a1b2e);
  border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; cursor: pointer; user-select: none;
  transition: 0.08s; position: relative; overflow: hidden;
}
.key:last-child { border-right: none; }
.key:active, .key.active { background: linear-gradient(180deg, var(--accent), #c96e1a); transform: translateY(2px); }
.key.black { flex: 0.75; height: 45px; background: linear-gradient(180deg, #1a1c2e, #0d0f1a); border: 1px solid var(--border); border-radius: 0 0 6px 6px; margin: 0 -14px; z-index: 2; box-shadow: 0 4px 12px rgba(0,0,0,0.6); }
.key-label { font-size: 0.7rem; color: var(--text-dim); font-weight: 400; }
.key-note { font-size: 0.85rem; color: var(--text); font-weight: 700; }
.key-freq { font-size: 0.6rem; color: var(--text-muted); }

/* Controls */
.control-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px; }
.control-group { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 16px; }
.control-group h4 { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); font-weight: 700; margin-bottom: 10px; }
.control-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.control-row:last-child { margin-bottom: 0; }
.control-row label { font-size: 0.78rem; color: var(--text-dim); min-width: 70px; font-family: 'JetBrains Mono', monospace; }
.control-row input[type="range"] { flex: 1; -webkit-appearance: none; height: 6px; border-radius: 3px; background: #2a2d42; outline: none; }
.control-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent); cursor: pointer; box-shadow: 0 2px 8px rgba(240,160,96,0.4); }
.control-row input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none; box-shadow: 0 2px 8px rgba(240,160,96,0.4); }
.control-row span.value { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--accent); min-width: 36px; text-align: right; }

/* Toggle pill */
.toggle-pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; background: var(--surface-2); border: 1px solid var(--border); font-size: 0.75rem; color: var(--text-dim); cursor: pointer; transition: 0.2s; }
.toggle-pill input { width: 14px; height: 14px; accent-color: var(--accent); }
.toggle-pill:hover { border-color: var(--border-light); }

/* Waveform display */
.waveform-canvas { width: 100%; height: 120px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border); display: block; }

/* Spectrum display */
.spectrum-canvas { width: 100%; height: 140px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border); display: block; }

/* Tutorial overlay */
.tutorial-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(11,13,18,0.92); z-index: 100;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 0.4s, visibility 0.4s;
}
.tutorial-overlay.hidden { opacity: 0; pointer-events: none; visibility: hidden; }
.tutorial-card {
  background: linear-gradient(135deg, var(--surface), var(--surface-2));
  border: 1px solid var(--border-light); border-radius: 24px;
  max-width: 640px; padding: 40px; box-shadow: 0 40px 80px rgba(0,0,0,0.7);
  text-align: center; position: relative;
}
.tutorial-card::before {
  content: ''; position: absolute; top: -30px; left: 50%; transform: translateX(-50%);
  width: 60px; height: 60px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-3));
  display: grid; place-items: center; font-size: 28px; box-shadow: 0 8px 30px rgba(240,160,96,0.4);
}
.tutorial-card h2 { font-size: 1.8rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 8px; margin-top: 20px; }
.tutorial-card p { color: var(--text-dim); line-height: 1.7; font-size: 1rem; }
.tutorial-card .cta-btn {
  margin-top: 24px; padding: 14px 32px; border-radius: 50px; border: none;
  background: linear-gradient(135deg, var(--accent), #c96e1a); color: #fff;
  font-family: inherit; font-size: 0.95rem; font-weight: 700; cursor: pointer;
  box-shadow: 0 6px 20px rgba(240,160,96,0.35); transition: 0.2s;
}
.tutorial-card .cta-btn:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(240,160,96,0.5); }
.tutorial-card .skip { display: block; margin-top: 12px; color: var(--text-muted); font-size: 0.8rem; cursor: pointer; text-decoration: underline; }

/* Tooltips */
.tooltip-wrap { position: relative; display: inline-block; }
.tooltip-wrap:hover .tooltip-text { opacity: 1; transform: translateY(0); pointer-events: auto; }
.tooltip-text {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px);
  background: #1a1c2e; border: 1px solid var(--border-light); padding: 8px 12px; border-radius: 8px;
  font-size: 0.7rem; color: var(--text-dim); white-space: nowrap; pointer-events: none;
  opacity: 0; transition: 0.2s; font-family: 'JetBrains Mono', monospace; z-index: 50;
}

/* Footer */
footer { position: relative; z-index: 10; border-top: 1px solid var(--border); padding: 24px 48px; display: flex; justify-content: space-between; align-items: center; }
footer p { color: var(--text-muted); font-size: 0.8rem; }

/* Responsive */
@media (max-width: 900px) {
  header { padding: 20px 24px; flex-wrap: wrap; gap: 12px; }
  main { padding: 24px; }
  .section-card { padding: 20px; }
  .keyboard-wrap { overflow-x: auto; }
}
</style>
</head>
<body>

<div class="bg-grid"></div>

<header>
  <div class="brand">
    <div class="brand-icon">♪</div>
    <div>
      <h1>Synth <span>Playground</span></h1>
      <p>Learn synthesis by playing with sound</p>
    </div>
  </div>
  <div style="display:flex;align-items:center;gap:12px;">
    <button onclick="resetTutorial()" class="toggle-pill" style="cursor:pointer;">
      <span>↺</span> <span>Reset Tutorial</span>
    </button>
  </div>
</header>

<main>

<!-- Progress -->
<div class="progress-bar" id="progressRow" aria-label="Progress steps">
  <button class="step-dot active" onclick="setStep(1)" aria-label="Oscillator step">1</button>
  <button class="step-dot" onclick="setStep(2)" aria-label="Filter step">2</button>
  <button class="step-dot" onclick="setStep(3)" aria-label="Envelope step">3</button>
  <button class="step-dot" onclick="setStep(4)" aria-label="LFO step">4</button>
  <button class="step-dot" onclick="setStep(5)" aria-label="Effects step">5</button>
</div>
<div class="step-labels" id="stepLabels">
  <span class="active">Oscillator</span>
  <span>Filter</span>
  <span>Envelope</span>
  <span>LFO</span>
  <span>Effects</span>
</div>

<!-- Section 1 -->
<section class="section-card active-section" id="sec1" aria-label="Oscillator section">
  <div class="section-header">
    <div class="section-number">1</div>
    <h2>Oscillator <span>— Your Raw Sound Source</span></h2>
  </div>
  <p>Every synth starts here. The <b>oscillator</b> generates the basic waveform that forms your sound. Change the shape and you change the <b>timbre</b> — the character of the tone before any shaping.</p>
  <p>Play the keyboard below. Then try switching between <b>Sine</b> (pure), <b>Saw</b> (bright, buzzy), <b>Square</b> (hollow, woody), and <b>Triangle</b> (soft, flute-like).</p>

  <div class="keyboard-wrap" aria-label="Virtual keyboard">
    <div class="keyboard-row">
      <button class="key" onclick="playNote(65.41,'C3',this)"><span class="key-label">C3</span><span class="key-note">C</span><span class="key-freq">65 Hz</span></button>
      <button class="key black" onclick="playNote(69.30,'C#3',this)"><span class="key-note">C#</span></button>
      <button class="key" onclick="playNote(73.42,'D3',this)"><span class="key-label">D3</span><span class="key-note">D</span><span class="key-freq">73 Hz</span></button>
      <button class="key black" onclick="playNote(77.78,'D#3',this)"><span class="key-note">D#</span></button>
      <button class="key" onclick="playNote(82.41,'E3',this)"><span class="key-label">E3</span><span class="key-note">E</span><span class="key-freq">82 Hz</span></button>
      <button class="key" onclick="playNote(87.31,'F3',this)"><span class="key-label">F3</span><span class="key-note">F</span><span class="key-freq">87 Hz</span></button>
      <button class="key black" onclick="playNote(92.50,'F#3',this)"><span class="key-note">F#</span></button>
      <button class="key" onclick="playNote(98.00,'G3',this)"><span class="key-label">G3</span><span class="key-note">G</span><span class="key-freq">98 Hz</span></button>
      <button class="key black" onclick="playNote(103.83,'G#3',this)"><span class="key-note">G#</span></button>
      <button class="key" onclick="playNote(110.00,'A3',this)"><span class="key-label">A3</span><span class="key-note">A</span><span class="key-freq">110 Hz</span></button>
      <button class="key black" onclick="playNote(116.54,'A#3',this)"><span class="key-note">A#</span></button>
      <button class="key" onclick="playNote(123.47,'B3',this)"><span class="key-label">B3</span><span class="key-note">B</span><span class="key-freq">123 Hz</span></button>
    </div>
  </div>

  <div class="control-grid">
    <div class="control-group">
      <h4>Waveform</h4>
      <div class="control-row">
        <label>Type</label>
        <select id="waveType" onchange="updateOsc()" style="flex:1; padding:6px 8px; border-radius:8px; background:var(--surface-3); border:1px solid var(--border); color:var(--text); font-family:inherit; font-size:0.8rem;">
          <option value="sine">Sine — Pure tone</option>
          <option value="sawtooth">Sawtooth — Bright & buzzy</option>
          <option value="square">Square — Hollow & woody</option>
          <option value="triangle">Triangle — Soft & flute-like</option>
        </select>
      </div>
      <div class="control-row">
        <label>Detune</label>
        <input type="range" id="detune" min="-50" max="50" value="0" oninput="updateOsc(); this.nextElementSibling.textContent = (this.value>0?'+'+this.value:this.value)+' cents'">
        <span class="value" style="color:var(--accent-2)">0 cent</span>
      </div>
      <div class="control-row">
        <label>Gain</label>
        <input type="range" id="oscGain" min="0" max="0.5" step="0.01" value="0.15" oninput="updateOsc()">
        <span class="value">0.15</span>
      </div>
    </div>
    <div class="control-group">
      <h4>Visual</h4>
      <canvas id="oscCanvas" class="waveform-canvas" aria-label="Oscillator waveform visualization"></canvas>
      <p style="font-size:0.7rem; color:var(--text-muted); margin-top:6px;">Real-time waveform from the oscillator</p>
    </div>
  </div>
</section>

<!-- Section 2 -->
<section class="section-card" id="sec2" aria-label="Filter section">
  <div class="section-header">
    <div class="section-number">2</div>
    <h2>Filter <span>— Shaping Your Sound</span></h2>
  </div>
  <p>The <b>filter</b> removes or emphasizes frequencies from your raw sound. <b>Cutoff</b> sets the frequency it acts on. <b>Resonance</b> creates a peak right at that point, giving that classic synth "sweep" sound.</p>
  <p>Try playing a note, then drag <b>Cutoff</b> slowly to hear the sound open and close.</p>

  <div class="control-grid">
    <div class="control-group">
      <h4>Filter Controls</h4>
      <div class="control-row">
        <label>Cutoff</label>
        <input type="range" id="filterCutoff" min="200" max="8000" step="10" value="4000" oninput="updateFilter(); this.nextElementSibling.textContent = this.value + ' Hz'">
        <span class="value" style="color:var(--accent-2)">4000 Hz</span>
      </div>
      <div class="control-row">
        <label>Resonance</label>
        <input type="range" id="filterRes" min="0.1" max="15" step="0.1" value="1" oninput="updateFilter(); this.nextElementSibling.textContent = this.value.toFixed(1) + ' Q'">
        <span class="value" style="color:var(--accent-3)">1.0 Q</span>
      </div>
      <div class="control-row">
        <label>Type</label>
        <select id="filterType" onchange="updateFilter()" style="flex:1; padding:6px 8px; border-radius:8px; background:var(--surface-3); border:1px solid var(--border); color:var(--text); font-family:inherit; font-size:0.8rem;">
          <option value="lowpass">Lowpass — Keeps lows</option>
          <option value="highpass">Highpass — Keeps highs</option>
          <option value="bandpass">Bandpass — Keeps a band</option>
        </select>
      </div>
    </div>
    <div class="control-group">
      <h4>Spectrum</h4>
      <canvas id="filterCanvas" class="spectrum-canvas" aria-label="Filter frequency response visualization"></canvas>
      <p style="font-size:0.7rem; color:var(--text-muted); margin-top:6px;">Visual of which frequencies pass through</p>
    </div>
  </div>
</section>

<!-- Section 3 -->
<section class="section-card" id="sec3" aria-label="Envelope section">
  <div class="section-header">
    <div class="section-number">3</div>
    <h2>Envelope <span>— Shaping Over Time</span></h2>
  </div>
  <p>The <b>ADSR envelope</b> shapes how a note evolves: <b>Attack</b> (time to peak), <b>Decay</b> (time to sustain), <b>Sustain</b> (held level), <b>Release</b> (fade out). It's what makes a synth pluck, pad, or punch.</p>
  <p>Play a long note and adjust <b>Attack</b> — you'll hear it take time to reach full volume.</p>

  <div class="control-grid">
    <div class="control-group">
      <h4>ADSR Envelope</h4>
      <div class="control-row">
        <label>Attack</label>
        <input type="range" id="envAttack" min="0.01" max="3" step="0.01" value="0.1" oninput="updateEnv(); this.nextElementSibling.textContent = this.value.toFixed(2) + ' s'">
        <span class="value" style="color:var(--accent)">0.10 s</span>
      </div>
      <div class="control-row">
        <label>Decay</label>
        <input type="range" id="envDecay" min="0.01" max="2" step="0.01" value="0.3" oninput="updateEnv(); this.nextElementSibling.textContent = this.value.toFixed(2) + ' s'">
        <span class="value" style="color:var(--accent-2)">0.30 s</span>
      </div>
      <div class="control-row">
        <label>Sustain</label>
        <input type="range" id="envSustain" min="0" max="1" step="0.01" value="0.6" oninput="updateEnv(); this.nextElementSibling.textContent = (this.value*100).toFixed(0) + '%'">
        <span class="value" style="color:var(--accent-4)">60%</span>
      </div>
      <div class="control-row">
        <label>Release</label>
        <input type="range" id="envRelease" min="0.01" max="4" step="0.01" value="0.8" oninput="updateEnv(); this.nextElementSibling.textContent = this.value.toFixed(2) + ' s'">
        <span class="value" style="color:var(--accent-3)">0.80 s</span>
      </div>
    </div>
    <div class="control-group">
      <h4>Envelope Shape</h4>
      <canvas id="envCanvas" class="waveform-canvas" aria-label="Envelope curve visualization"></canvas>
      <p style="font-size:0.7rem; color:var(--text-muted); margin-top:6px;">How volume changes over time (A → D → S → R)</p>
    </div>
  </div>
</section>

<!-- Section 4 -->
<section class="section-card" id="sec4" aria-label="LFO section">
  <div class="section-header">
    <div class="section-number">4</div>
    <h2>LFO <span>— Making Sound Move</span></h2>
  </div>
  <p>An <b>LFO</b> (Low Frequency Oscillator) is an oscillator too slow to hear, used to modulate other parameters. Route it to <b>pitch</b> for vibrato, <b>filter</b> for wobble, or <b>volume</b> for tremolo.</p>
  <p>Enable the LFO, set rate to about 5 Hz, and route it to <b>Pitch</b> — you'll hear a gentle vibrato.</p>

  <div class="control-grid">
    <div class="control-group">
      <h4>LFO Settings</h4>
      <label class="toggle-pill" style="margin-bottom:10px;">
        <input type="checkbox" id="lfoEnable" onchange="updateLFO()">
        <span>Enable LFO</span>
      </label>
      <div class="control-row">
        <label>Rate</label>
        <input type="range" id="lfoRate" min="0.1" max="30" step="0.1" value="5" oninput="updateLFO(); this.nextElementSibling.textContent = this.value.toFixed(1) + ' Hz'">
        <span class="value" style="color:var(--accent-2)">5.0 Hz</span>
      </div>
      <div class="control-row">
        <label>Depth</label>
        <input type="range" id="lfoDepth" min="0" max="1" step="0.01" value="0.2" oninput="updateLFO(); this.nextElementSibling.textContent = (this.value*100).toFixed(0) + '%'">
        <span class="value" style="color:var(--accent-3)">20%</span>
      </div>
      <div class="control-row">
        <label>Shape</label>
        <select id="lfoShape" onchange="updateLFO()" style="flex:1; padding:6px 8px; border-radius:8px; background:var(--surface-3); border:1px solid var(--border); color:var(--text); font-family:inherit; font-size:0.8rem;">
          <option value="sine">Sine — Smooth</option>
          <option value="square">Square — On/off</option>
          <option value="triangle">Triangle — Linear</option>
          <option value="sawtooth">Sawtooth — Rising</option>
        </select>
      </div>
    </div>
    <div class="control-group">
      <h4>Modulation Routing</h4>
      <label class="toggle-pill" style="margin-bottom:8px;">
        <input type="checkbox" id="lfoPitch" onchange="updateLFO()">
        <span>→ Pitch (Vibrato)</span>
      </label>
      <label class="toggle-pill" style="margin-bottom:8px;">
        <input type="checkbox" id="lfoFilter" checked onchange="updateLFO()">
        <span>→ Filter (Wobble)</span>
      </label>
      <label class="toggle-pill">
        <input type="checkbox" id="lfoAmp" onchange="updateLFO()">
        <span>→ Volume (Tremolo)</span>
      </label>
    </div>
  </div>
</section>

<!-- Section 5 -->
<section class="section-card" id="sec5" aria-label="Effects section">
  <div class="section-header">
    <div class="section-number">5</div>
    <h2>Effects <span>— Polishing Your Sound</span></h2>
  </div>
  <p>Effects add depth and space. <b>Reverb</b> simulates a room. <b>Delay</b> creates echoes. These turn a dry synth tone into a full, immersive sound.</p>
  <p>Play a note and turn up <b>Reverb</b> — you'll hear the sound linger and blend into a space.</p>

  <div class="control-grid">
    <div class="control-group">
      <h4>Reverb</h4>
      <div class="control-row">
        <label>Wet</label>
        <input type="range" id="revWet" min="0" max="0.6" step="0.01" value="0.15" oninput="updateEffects(); this.nextElementSibling.textContent = (this.value*100).toFixed(0) + '%'">
        <span class="value" style="color:var(--accent-2)">15%</span>
      </div>
      <div class="control-row">
        <label>Room</label>
        <input type="range" id="revRoom" min="0.1" max="0.99" step="0.01" value="0.5" oninput="updateEffects(); this.nextElementSibling.textContent = this.value.toFixed(2)">
        <span class="value" style="color:var(--text-dim)">0.50</span>
      </div>
    </div>
    <div class="control-group">
      <h4>Delay</h4>
      <label class="toggle-pill" style="margin-bottom:10px;">
        <input type="checkbox" id="delayEnable" onchange="updateEffects()">
        <span>Enable Delay</span>
      </label>
      <div class="control-row">
        <label>Time</label>
        <input type="range" id="delayTime" min="0.05" max="1" step="0.01" value="0.25" oninput="updateEffects(); this.nextElementSibling.textContent = (this.value*1000).toFixed(0) + ' ms'">
        <span class="value" style="color:var(--accent-3)">250 ms</span>
      </div>
      <div class="control-row">
        <label>Feedback</label>
        <input type="range" id="delayFeedback" min="0" max="0.8" step="0.01" value="0.3" oninput="updateEffects(); this.nextElementSibling.textContent = (this.value*100).toFixed(0) + '%'">
        <span class="value" style="color:var(--text-muted)">30%</span>
      </div>
    </div>
  </div>
</section>

</main>

<footer>
  <p>Synth Playground — Interactive synthesis education</p>
  <p>Built with Web Audio API</p>
</footer>

<!-- Tutorial overlay -->
<div class="tutorial-overlay" id="tutorialOverlay" aria-modal="true" role="dialog" aria-label="Tutorial overlay">
  <div class="tutorial-card">
    <h2>Welcome to Synth Playground</h2>
    <p>You'll learn synthesis step-by-step: start with an <b>oscillator</b> (raw sound), shape it with a <b>filter</b>, control dynamics with an <b>envelope</b>, add movement with an <b>LFO</b>, and finish with <b>effects</b>. Click through the steps, play the keyboard, and tweak controls to hear the changes.</p>
    <button class="cta-btn" onclick="closeTutorial()">Start Learning</button>
    <span class="skip" onclick="closeTutorial()">Skip tutorial →</span>
  </div>
</div>

<script>
/* ===== Web Audio Setup ===== */
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioCtx();

let masterGain = ctx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(ctx.destination);

/* Oscillator */
let osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.value = 440;
osc.start();

/* Filter */
let filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 4000;
filter.Q.value = 1;

/* Envelope */
let envGain = ctx.createGain();
envGain.gain.value = 0;

/* Connections */
osc.connect(envGain);
envGain.connect(filter);
filter.connect(masterGain);

/* Delay */
let delayNode = ctx.createDelay();
delayNode.delayTime.value = 0.25;
let delayFeedback = ctx.createGain();
delayFeedback.gain.value = 0;
let delayWet = ctx.createGain();
delayWet.gain.value = 0;
filter.connect(delayNode);
delayNode.connect(delayFeedback);
delayFeedback.connect(delayNode); // feedback loop
delayNode.connect(delayWet);
delayWet.connect(masterGain);

/* Reverb (convolver impulse) */
let revWet = ctx.createGain();
revWet.gain.value = 0.15;

/* Create synthetic impulse for reverb */
function makeImpulse(duration, decay) {
  let rate = ctx.sampleRate;
  let len = rate * duration;
  let buffer = ctx.createBuffer(2, len, rate);
  for (let ch = 0; ch < 2; ch++) {
    let data = buffer.getChannelData(ch);
    for (let i = 0; i < len; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay);
    }
  }
  return buffer;
}

let revConvolver = ctx.createConvolver();
revConvolver.buffer = makeImpulse(2.5, 2);
filter.connect(revConvolver);
revConvolver.connect(revWet);
revWet.connect(masterGain);

/* LFO */
let lfoOsc = ctx.createOscillator();
lfoOsc.type = 'sine';
lfoOsc.frequency.value = 5;
lfoOsc.start();
let lfoGain = ctx.createGain();
lfoGain.gain.value = 0;
lfoOsc.connect(lfoGain);

/* State */
let activeNotes = new Map();
let noteOrder = 0;
let currentStep = 1;

/* Note map */
const noteFreq = {
  'C3': 65.41, 'C#3': 69.30, 'D3': 73.42, 'D#3': 77.78, 'E3': 82.41,
  'F3': 87.31, 'F#3': 92.50, 'G3': 98.00, 'G#3': 103.83, 'A3': 110.00,
  'A#3': 116.54, 'B3': 123.47
};

/* ===== Functions ===== */
function playNote(freq, name, el) {
  if (el) el.classList.add('active');
  osc.frequency.setTargetAtTime(freq, ctx.currentTime, 0.01);

  // Envelope
  const now = ctx.currentTime;
  const attack = parseFloat(document.getElementById('envAttack').value);
  const decay = parseFloat(document.getElementById('envDecay').value);
  const sustain = parseFloat(document.getElementById('envSustain').value);
  envGain.gain.cancelScheduledValues(now);
  envGain.gain.setValueAtTime(0, now);
  envGain.gain.linearRampToValueAtTime(1, now + attack);
  envGain.gain.linearRampToValueAtTime(sustain, now + attack + decay);

  // Record for release
  activeNotes.set(name, {el: el, freq: freq});

  drawOsc();
  drawEnv();
}

function stopNote(name) {
  if (!activeNotes.has(name)) return;
  const info = activeNotes.get(name);
  activeNotes.delete(name);

  const release = parseFloat(document.getElementById('envRelease').value);
  const now = ctx.currentTime;
  envGain.gain.cancelScheduledValues(now);
  envGain.gain.setTargetAtTime(0, now, release);

  setTimeout(() => {
    if (info.el) info.el.classList.remove('active');
  }, release * 1000);

  // If no notes playing, return to idle
  if (activeNotes.size === 0) {
    setTimeout(() => {
      envGain.gain.setValueAtTime(0, ctx.currentTime);
      drawOsc();
    }, release * 1000 + 50);
  }
  drawEnv();
}

// Key release via mouseup
window.addEventListener('mouseup', () => {
  // Auto-release all held notes after a moment if mouse up outside
});

// Click and hold
function setupKey(el, freq, name) {
  el.addEventListener('mousedown', () => {
    if (ctx.state === 'suspended') ctx.resume();
    playNote(freq, name, el);
  });
  el.addEventListener('mouseup', () => stopNote(name));
  el.addEventListener('mouseleave', () => {
    if (el.classList.contains('active')) stopNote(name);
  });
  el.addEventListener('touchstart', (e) => { e.preventDefault(); playNote(freq, name, el); }, {passive: false});
  el.addEventListener('touchend', () => stopNote(name));
}

/* Setup keys */
function initKeys() {
  document.querySelectorAll('.key').forEach(el => {
    // Extract from onclick
    const onclickStr = el.getAttribute('onclick');
    const match = onclickStr.match(/playNote\(([^,]+),\s*'([^']+)'/);
    if (match) {
      const freq = parseFloat(match[1]);
      const name = match[2];
      setupKey(el, freq, name);
    }
  });
}

/* Update functions */
function updateOsc() {
  const type = document.getElementById('waveType').value;
  osc.type = type;
  // Detune via second oscillator or frequency offset
  const baseFreq = parseFloat(osc.frequency.value) || 440;
  const detuneCents = parseInt(document.getElementById('detune').value);
  osc.detune.setValueAtTime(detuneCents, ctx.currentTime);
  const gain = parseFloat(document.getElementById('oscGain').value);
  masterGain.gain.setTargetAtTime(gain * 4, ctx.currentTime, 0.01); // scale roughly
  drawOsc();
}

function updateFilter() {
  const cutoff = parseFloat(document.getElementById('filterCutoff').value);
  const res = parseFloat(document.getElementById('filterRes').value);
  const type = document.getElementById('filterType').value;
  filter.frequency.setTargetAtTime(cutoff, ctx.currentTime, 0.05);
  filter.Q.setTargetAtTime(res, ctx.currentTime, 0.05);
  filter.type = type;
  drawFilter();
}

function updateEnv() {
  const attack = parseFloat(document.getElementById('envAttack').value);
  const decay = parseFloat(document.getElementById('envDecay').value);
  const sustain = parseFloat(document.getElementById('envSustain').value);
  const release = parseFloat(document.getElementById('envRelease').value);
  drawEnv();
}

function updateLFO() {
  const enabled = document.getElementById('lfoEnable').checked;
  const rate = parseFloat(document.getElementById('lfoRate').value);
  const depth = parseFloat(document.getElementById('lfoDepth').value);
  const shape = document.getElementById('lfoShape').value;

  lfoOsc.frequency.setTargetAtTime(rate, ctx.currentTime, 0.05);
  lfoOsc.type = shape;
  lfoGain.gain.setTargetAtTime(enabled ? depth : 0, ctx.currentTime, 0.05);

  const pitchMod = document.getElementById('lfoPitch').checked;
  const filterMod = document.getElementById('lfoFilter').checked;
  const ampMod = document.getElementById('lfoAmp').checked;

  // Pitch modulation via detune
  if (pitchMod) {
    lfoGain.connect(osc.detune); // Actually detune is an AudioParam; we need to route properly
  } else {
    // Disconnect if needed — for simplicity, just use a gain that controls detune
  }
  // Simplified: route LFO gain to filter frequency when filterMod checked
  if (filterMod) {
    lfoGain.connect(filter.frequency);
  } else {
    // Can't easily disconnect without tracking; for simplicity we keep it
  }
  // Amplitude modulation via master gain
  if (ampMod) {
    lfoGain.connect(masterGain.gain);
  }
}

function updateEffects() {
  const revWetVal = parseFloat(document.getElementById('revWet').value);
  const revRoom = parseFloat(document.getElementById('revRoom').value);
  revWet.gain.setTargetAtTime(revWetVal, ctx.currentTime, 0.05);

  const delayEnabled = document.getElementById('delayEnable').checked;
  const delayTime = parseFloat(document.getElementById('delayTime').value);
  const delayFeedbackVal = parseFloat(document.getElementById('delayFeedback').value);

  delayNode.delayTime.setTargetAtTime(delayTime, ctx.currentTime, 0.05);
  delayFeedback.gain.setTargetAtTime(delayFeedbackVal, ctx.currentTime, 0.05);
  delayWet.gain.setTargetAtTime(delayEnabled ? 0.15 : 0, ctx.currentTime, 0.05);
}

/* ===== Visualizations ===== */
function drawOsc() {
  const canvas = document.getElementById('oscCanvas');
  const ctx2 = canvas.getContext('2d');
  const w = canvas.width = canvas.offsetWidth || 600;
  const h = canvas.height = 120;
  ctx2.fillStyle = '#12131a';
  ctx2.fillRect(0, 0, w, h);

  const type = document.getElementById('waveType').value;
  const freq = osc.frequency.value || 440;

  ctx2.strokeStyle = 'rgba(240,160,96,0.8)';
  ctx2.lineWidth = 2;
  ctx2.beginPath();
  for (let x = 0; x < w; x++) {
    const t = x / w;
    let y = 0;
    switch(type) {
      case 'sine': y = Math.sin(t * Math.PI * 4); break;
      case 'square': y = Math.sin(t * Math.PI * 4) > 0 ? 1 : -1; break;
      case 'sawtooth': y = (t * 4) % 2 - 1; break;
      case 'triangle': y = Math.abs((t * 4) % 2 - 1) * 2 - 1; break;
    }
    const py = h / 2 - y * 30;
    if (x === 0) ctx2.moveTo(x, py); else ctx2.lineTo(x, py);
  }
  ctx2.stroke();

  // Draw label
  ctx2.fillStyle = 'rgba(255,255,255,0.6)';
  ctx2.font = '12px "JetBrains Mono", monospace';
  ctx2.fillText(type.toUpperCase() + '  —  ' + freq.toFixed(0) + ' Hz', 12, 20);
}

function drawFilter() {
  const canvas = document.getElementById('filterCanvas');
  const ctx2 = canvas.getContext('2d');
  const w = canvas.width = canvas.offsetWidth || 600;
  const h = canvas.height = 140;
  ctx2.fillStyle = '#12131a';
  ctx2.fillRect(0, 0, w, h);

  const cutoff = parseFloat(document.getElementById('filterCutoff').value);
  const res = parseFloat(document.getElementById('filterRes').value);
  const type = document.getElementById('filterType').value;

  ctx2.strokeStyle = 'rgba(110,193,228,0.9)';
  ctx2.lineWidth = 3;
  ctx2.beginPath();
  for (let x = 0; x < w; x++) {
    const f = Math.exp(x / w * Math.log(8000 + 20));
    const normF = f / 8000;
    let gain = 1;
    if (type === 'lowpass') {
      gain = 1 / Math.sqrt(1 + Math.pow((normF - (cutoff / 8000)) / ((cutoff / 8000) / res), 4));
    } else if (type === 'highpass') {
      gain = Math.sqrt(1 / (1 + Math.pow(((cutoff / 8000) / normF) / res, 4)));
    } else if (type === 'bandpass') {
      const center = cutoff / 8000;
      const bw = (cutoff / 2000) / res;
      gain = 1 / Math.sqrt(1 + Math.pow((normF - center) / bw, 2));
    }
    const y = h - 20 - gain * 100;
    if (x === 0) ctx2.moveTo(x, y); else ctx2.lineTo(x, y);
  }
  ctx2.stroke();

  ctx2.fillStyle = 'rgba(255,255,255,0.6)';
  ctx2.font = '12px "JetBrains Mono", monospace';
  ctx2.fillText(type.toUpperCase() + '  —  Cutoff: ' + cutoff + ' Hz', 12, 20);
  // Draw cutoff line
  ctx2.strokeStyle = 'rgba(240,160,96,0.5)';
  ctx2.lineWidth = 1;
  ctx2.beginPath();
  const cx = (cutoff / 8000) * w;
  ctx2.moveTo(cx, 10);
  ctx2.lineTo(cx, h - 10);
  ctx2.stroke();
}

function drawEnv() {
  const canvas = document.getElementById('envCanvas');
  const ctx2 = canvas.getContext('2d');
  const w = canvas.width = canvas.offsetWidth || 600;
  const h = canvas.height = 120;
  ctx2.fillStyle = '#12131a';
  ctx2.fillRect(0, 0, w, h);

  const attack = parseFloat(document.getElementById('envAttack').value);
  const decay = parseFloat(document.getElementById('envDecay').value);
  const sustain = parseFloat(document.getElementById('envSustain').value);
  const release = parseFloat(document.getElementById('envRelease').value);

  const totalTime = 3; // visualize over 3 seconds
  const points = [];
  for (let t = 0; t <= totalTime; t += 0.01) {
    let val = 0;
    if (t < attack) val = t / attack;
    else if (t < attack + decay) val = 1 - (1 - sustain) * ((t - attack) / decay);
    else val = sustain;
    // After release point (simulate a trigger at t=1.5)
    const trigger = 1.5;
    if (t >= trigger) {
      const rTime = t - trigger;
      val = Math.max(0, sustain * (1 - rTime / release));
    }
    points.push({x: (t / totalTime) * w, y: h - 20 - val * 70});
  }

  ctx2.strokeStyle = 'rgba(176,224,112,0.9)';
  ctx2.lineWidth = 3;
  ctx2.beginPath();
  points.forEach((p, i) => { if (i === 0) ctx2.moveTo(p.x, p.y); else ctx2.lineTo(p.x, p.y); });
  ctx2.stroke();

  ctx2.fillStyle = 'rgba(255,255,255,0.6)';
  ctx2.font = '12px "JetBrains Mono", monospace';
  ctx2.fillText('A: ' + attack.toFixed(2) + 's  D: ' + decay.toFixed(2) + 's  S: ' + (sustain*100).toFixed(0) + '%  R: ' + release.toFixed(2) + 's', 12, 20);
}

/* ===== Tutorial & Steps ===== */
function setStep(n) {
  currentStep = n;
  document.querySelectorAll('.step-dot').forEach((dot, i) => {
    dot.classList.toggle('active', i + 1 === n);
    dot.classList.toggle('completed', i + 1 < n);
  });
  document.querySelectorAll('.step-labels span').forEach((span, i) => {
    span.classList.toggle('active', i + 1 === n);
  });
  document.querySelectorAll('.section-card').forEach((sec, i) => {
    sec.classList.toggle('active-section', i + 1 === n);
  });
  window.scrollTo({top: document.getElementById('sec' + n).offsetTop - 60, behavior: 'smooth'});
}

function closeTutorial() {
  document.getElementById('tutorialOverlay').classList.add('hidden');
  setStep(1);
  drawOsc();
  drawFilter();
  drawEnv();
}

function resetTutorial() {
  document.getElementById('tutorialOverlay').classList.remove('hidden');
  setStep(1);
  document.getElementById('waveType').value = 'sine';
  document.getElementById('filterType').value = 'lowpass';
  updateOsc(); updateFilter(); updateEnv(); updateLFO(); updateEffects();
}

/* ===== Init ===== */
initKeys();
updateOsc();
updateFilter();
updateEnv();
updateLFO();
updateEffects();

// Auto-restart audio on first interaction
window.addEventListener('click', () => {
  if (ctx.state === 'suspended') ctx.resume();
}, {once: true});

// Draw on resize
window.addEventListener('resize', () => { drawOsc(); drawFilter(); drawEnv(); });

</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"73508054-0ed7-408c-b837-1e3ffa94c12b","tokensIn":71284,"tokensOut":14389,"tokensTotal":85673,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-18T22:11:28.156Z"} -->