TutSynth
GPT 6 Luna · full

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 33,901 bytes · SHA-256 780d47dac890
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WAVEFORM — Learn your synth</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
:root{--bg:#101112;--panel:#17191a;--panel2:#1d2021;--line:#2b2f30;--muted:#858b8a;--text:#f1f2eb;--lime:#d6fa62;--cyan:#70e2d0;--orange:#ff9c6a;--purple:#b4a0ff}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;min-height:100vh;background-image:radial-gradient(ellipse at 52% -20%,#232928 0,transparent 48%)}button,input{font:inherit}button{color:inherit;cursor:pointer}.app{max-width:1440px;margin:auto;padding:24px 36px 40px}.topbar{height:42px;display:flex;justify-content:space-between;align-items:center;margin-bottom:30px}.brand{display:flex;align-items:center;gap:11px;font:700 15px 'Space Grotesk';letter-spacing:.15em}.brand-mark{width:26px;height:26px;border:1px solid #616961;border-radius:50%;display:grid;place-items:center;color:var(--lime);font-size:14px}.brand small{font:400 10px 'DM Mono';letter-spacing:.1em;color:var(--muted);margin-left:5px}.top-right{display:flex;gap:10px;align-items:center}.status{font:10px 'DM Mono';letter-spacing:.12em;color:#a4aaa5;display:flex;align-items:center;gap:8px}.dot{width:7px;height:7px;border-radius:50%;background:#626a64}.dot.on{background:var(--lime);box-shadow:0 0 11px #d6fa6277}.reset{background:none;border:1px solid var(--line);border-radius:6px;padding:9px 13px;color:#aeb2ae;font-size:11px}.hero{display:flex;justify-content:space-between;align-items:end;margin-bottom:26px}.eyebrow{font:10px 'DM Mono';text-transform:uppercase;letter-spacing:.19em;color:var(--lime);margin-bottom:10px}.hero h1{font:500 clamp(32px,4vw,53px)/1 'Space Grotesk';letter-spacing:-.055em;margin:0}.hero h1 span{color:#777d79}.hero-copy{font-size:13px;line-height:1.6;color:#9a9f9b;max-width:300px;margin:0 0 3px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:16px;align-items:start}.main-column{min-width:0}.panel{background:linear-gradient(145deg,#1b1e1f,#17191a);border:1px solid var(--line);border-radius:12px}.scope-panel{padding:14px 18px 12px;margin-bottom:14px;position:relative}.panel-top{display:flex;justify-content:space-between;align-items:center}.section-tag{font:10px 'DM Mono';letter-spacing:.14em;color:#adb2ae;text-transform:uppercase}.scope-label{font:9px 'DM Mono';color:#767d79;letter-spacing:.1em}.scope{height:132px;width:100%;display:block;margin-top:7px}.scope-foot{display:flex;justify-content:space-between;color:#737a76;font:9px 'DM Mono';letter-spacing:.08em;margin-top:-1px}.control-panel{padding:17px 18px 20px;margin-bottom:14px}.control-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:17px}.module-title{display:flex;gap:10px;align-items:center;font:600 13px 'Space Grotesk';letter-spacing:.01em}.module-icon{height:24px;width:24px;border-radius:6px;background:#292d2b;color:var(--lime);display:grid;place-items:center;font:12px 'DM Mono'}.module-meta{font:9px 'DM Mono';color:#747b77;letter-spacing:.12em}.control-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px}.control{min-width:0;position:relative}.control label{display:flex;justify-content:space-between;align-items:center;font:10px 'DM Mono';text-transform:uppercase;letter-spacing:.07em;color:#a3a9a4;margin-bottom:11px}.readout{color:var(--lime);font-size:10px}.control input[type=range]{width:100%;height:3px;appearance:none;background:linear-gradient(90deg,var(--lime) 0%,var(--lime) var(--pct,40%),#3a3e3b var(--pct,40%),#3a3e3b 100%);outline:0;border-radius:9px;display:block;margin:0 0 8px}.control input[type=range]::-webkit-slider-thumb{appearance:none;width:11px;height:11px;border:2px solid var(--lime);background:#17191a;border-radius:50%;box-shadow:0 0 0 3px #d6fa6212}.control input[type=range]::-moz-range-thumb{width:8px;height:8px;border:2px solid var(--lime);background:#17191a;border-radius:50%}.range-ends{font:8px 'DM Mono';color:#646b67;display:flex;justify-content:space-between}.select-row{display:flex;gap:5px}.choice{flex:1;border:1px solid #353a37;background:#202322;border-radius:5px;padding:7px 4px;color:#8d948f;font:9px 'DM Mono';text-transform:uppercase}.choice.active{border-color:#a9c65a;color:var(--lime);background:#292f22}.lock-overlay{position:absolute;inset:-4px -5px -4px;z-index:2;background:#17191add;backdrop-filter:blur(2px);border-radius:7px;display:flex;align-items:center;justify-content:center;gap:7px;color:#979d98;font:10px 'DM Mono';letter-spacing:.07em}.lock-overlay span{font-size:13px}.keyboard-panel{padding:15px 18px 14px}.keyboard-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:13px}.key-hint{font:9px 'DM Mono';color:#737a76}.keys{height:108px;display:flex;position:relative;user-select:none}.key{position:relative;flex:1;border:1px solid #565a56;border-radius:0 0 5px 5px;background:linear-gradient(#f3f3e9,#c9cbc1);height:100%;color:#626660;display:flex;justify-content:center;align-items:end;padding-bottom:8px;font:9px 'DM Mono';transition:background .08s,transform .08s}.key:first-child{border-radius:4px 0 0 5px}.key:last-child{border-radius:0 4px 5px 0}.key.black{position:absolute;z-index:2;height:64%;width:6.2%;flex:none;top:0;background:linear-gradient(#3c4140,#171a1a);border:1px solid #070808;border-radius:0 0 4px 4px;color:#939a94;align-items:end;padding-bottom:5px;box-shadow:0 3px 3px #0008}.key.active{background:var(--lime);transform:translateY(2px)}.key.black.active{background:#a9ce3f}.keyboard-foot{display:flex;justify-content:space-between;margin-top:10px;color:#737a76;font:9px 'DM Mono'}.side{display:flex;flex-direction:column;gap:14px}.lesson-card{padding:19px 18px 17px;border-color:#3a4230;background:linear-gradient(150deg,#20251b,#191c19);min-height:280px}.lesson-top{display:flex;justify-content:space-between;align-items:center}.lesson-kicker{color:var(--lime);font:9px 'DM Mono';letter-spacing:.14em;text-transform:uppercase}.step-count{font:9px 'DM Mono';color:#81877f}.progress{height:2px;background:#393e36;margin:13px 0 20px;display:flex;gap:3px}.progress i{height:100%;flex:1;background:#393e36}.progress i.done{background:var(--lime)}.lesson-card h2{font:500 23px/1.1 'Space Grotesk';letter-spacing:-.04em;margin:0 0 11px}.lesson-card p{font-size:12px;line-height:1.65;color:#a9afa7;margin:0 0 15px}.try-box{border-left:2px solid var(--lime);background:#d6fa620b;padding:10px 11px;margin:0 0 15px;color:#cbd0c3;font-size:11px;line-height:1.5}.try-box b{font:9px 'DM Mono';letter-spacing:.1em;color:var(--lime);display:block;margin-bottom:4px}.lesson-actions{display:flex;gap:7px}.primary{border:0;border-radius:5px;background:var(--lime);color:#1c2114;padding:10px 12px;font:600 10px 'DM Mono';letter-spacing:.04em;flex:1}.primary:disabled{opacity:.5;cursor:default}.secondary{border:1px solid #3a4037;border-radius:5px;background:transparent;color:#a7aea3;padding:9px 11px;font:10px 'DM Mono'}.unlock-note{font:9px 'DM Mono';color:#848b80;margin-top:10px;min-height:12px}.concept-card{padding:16px 17px}.concept-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.concept-head strong{font:600 11px 'Space Grotesk'}.concept-head span{font:9px 'DM Mono';color:#6f7772}.concept-card p{font-size:11px;line-height:1.65;color:#969d98;margin:0}.concept-card em{font-style:normal;color:#d3dbce}.signal{padding:14px 16px;display:flex;align-items:center;gap:12px}.signal-icon{width:29px;height:29px;border-radius:50%;border:1px solid #393e3a;display:grid;place-items:center;color:var(--cyan);font-size:12px}.signal strong{display:block;font:10px 'DM Mono';letter-spacing:.09em;margin-bottom:4px}.signal small{font-size:10px;color:#7f8782}.toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%,20px);opacity:0;pointer-events:none;background:#d6fa62;color:#1a2110;padding:10px 15px;border-radius:6px;font:11px 'DM Mono';z-index:5;transition:.25s}.toast.show{opacity:1;transform:translate(-50%,0)}.disabled-module{opacity:.72}
@media(max-width:900px){.app{padding:20px}.layout{grid-template-columns:minmax(0,1fr) 275px;gap:12px}.control-grid{gap:14px 12px}.hero-copy{max-width:240px}}@media(max-width:700px){.app{padding:16px 13px 28px}.topbar{margin-bottom:22px}.hero{display:block;margin-bottom:20px}.hero-copy{margin-top:11px;max-width:450px}.layout{display:flex;flex-direction:column}.main-column,.side{width:100%}.side{display:grid;grid-template-columns:1fr}.lesson-card{min-height:0}.scope{height:112px}.control-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:17px 18px}.keys{height:92px}.key-hint{font-size:8px}.module-meta{font-size:8px}}@media(max-width:430px){.brand small{display:none}.topbar{gap:8px}.status{white-space:nowrap;font-size:9px}.top-right{gap:7px}.reset{padding:8px 10px}}@media(max-width:380px){.app{padding-left:10px;padding-right:10px}.control-panel{padding:14px}.scope-panel{padding:13px}.hero h1{font-size:34px}.key{font-size:8px}}
</style>
</head>
<body>
<div class="app">
<header class="topbar"><div class="brand"><div class="brand-mark">∿</div>WAVEFORM <small> / SYNTH LAB</small></div><div class="top-right"><div class="status"><i class="dot" id="audioDot"></i><span id="audioStatus">AUDIO SLEEPING</span></div><button class="reset" id="resetBtn">↺ Reset</button></div></header>
<section class="hero"><div><div class="eyebrow">A hands-on field guide</div><h1>Make a sound.<br><span>Then shape it.</span></h1></div><p class="hero-copy">A synthesizer is a set of simple ideas connected together. Explore them one at a time—and hear what each control really does.</p></section>
<div class="layout"><main class="main-column">
<section class="panel scope-panel"><div class="panel-top"><span class="section-tag">Signal in motion</span><span class="scope-label" id="scopeMode">WAITING FOR A NOTE</span></div><canvas class="scope" id="scope" aria-label="Live audio waveform visualization"></canvas><div class="scope-foot"><span>−1</span><span>TIME →</span><span>+1</span></div></section>
<section class="panel control-panel" id="oscModule"><div class="control-head"><div class="module-title"><span class="module-icon">01</span>Oscillator <span class="module-meta">— THE RAW MATERIAL</span></div><span class="module-meta">SOURCE</span></div><div class="control-grid">
<div class="control" id="waveControl"><label>Wave shape</label><div class="select-row"><button class="choice active" data-wave="sine">Sine</button><button class="choice" data-wave="triangle">Tri</button><button class="choice" data-wave="sawtooth">Saw</button><button class="choice" data-wave="square">Square</button></div></div>
<div class="control"><label>Octave <span class="readout" id="octaveVal">0</span></label><input data-param="octave" type="range" min="-2" max="2" value="0" step="1"><div class="range-ends"><span>−2</span><span>+2</span></div></div>
<div class="control"><label>Fine tune <span class="readout" id="tuneVal">0 ct</span></label><input data-param="tune" type="range" min="-50" max="50" value="0"><div class="range-ends"><span>−50</span><span>+50 cents</span></div></div>
<div class="control"><label>Level <span class="readout" id="levelVal">72%</span></label><input data-param="level" type="range" min="0" max="100" value="72"><div class="range-ends"><span>quiet</span><span>loud</span></div></div>
</div></section>
<section class="panel control-panel" id="filterModule"><div class="control-head"><div class="module-title"><span class="module-icon" style="color:var(--cyan)">02</span>Filter <span class="module-meta">— SCULPT THE TONE</span></div><span class="module-meta">TONE</span></div><div class="control-grid">
<div class="control"><label>Cutoff <span class="readout" id="cutoffVal">4.2 kHz</span></label><input data-param="cutoff" type="range" min="120" max="10000" value="4200"><div class="range-ends"><span>dark</span><span>bright</span></div></div>
<div class="control"><label>Resonance <span class="readout" id="resVal">18%</span></label><input data-param="resonance" type="range" min="0" max="95" value="18"><div class="range-ends"><span>smooth</span><span>peak</span></div></div>
<div class="control"><label>Filter type</label><div class="select-row"><button class="choice active" data-filter="lowpass">Low-pass</button><button class="choice" data-filter="highpass">High-pass</button><button class="choice" data-filter="bandpass">Band</button></div></div>
<div class="control"><label>Key tracking <span class="readout" id="trackVal">35%</span></label><input data-param="tracking" type="range" min="0" max="100" value="35"><div class="range-ends"><span>fixed</span><span>follows note</span></div></div>
</div></section>
<section class="panel control-panel" id="envModule"><div class="control-head"><div class="module-title"><span class="module-icon" style="color:var(--orange)">03</span>Amplitude envelope <span class="module-meta">— SHAPE IN TIME</span></div><span class="module-meta">MOVEMENT</span></div><div class="control-grid">
<div class="control"><label>Attack <span class="readout" id="attackVal">35 ms</span></label><input data-param="attack" type="range" min="5" max="2000" value="35"><div class="range-ends"><span>instant</span><span>slow swell</span></div></div>
<div class="control"><label>Decay <span class="readout" id="decayVal">240 ms</span></label><input data-param="decay" type="range" min="20" max="2000" value="240"><div class="range-ends"><span>short</span><span>long</span></div></div>
<div class="control"><label>Sustain <span class="readout" id="sustainVal">62%</span></label><input data-param="sustain" type="range" min="0" max="100" value="62"><div class="range-ends"><span>none</span><span>full</span></div></div>
<div class="control"><label>Release <span class="readout" id="releaseVal">320 ms</span></label><input data-param="release" type="range" min="20" max="2500" value="320"><div class="range-ends"><span>cut</span><span>fade</span></div></div>
</div></section>
<section class="panel control-panel" id="lfoModule"><div class="control-head"><div class="module-title"><span class="module-icon" style="color:var(--purple)">04</span>LFO <span class="module-meta">— AUTOMATE A PARAMETER</span></div><span class="module-meta">MODULATION</span></div><div class="control-grid">
<div class="control"><label>Rate <span class="readout" id="rateVal">4.0 Hz</span></label><input data-param="rate" type="range" min="0.2" max="12" value="4" step="0.1"><div class="range-ends"><span>slow</span><span>fast</span></div></div>
<div class="control"><label>Depth <span class="readout" id="depthVal">0%</span></label><input data-param="depth" type="range" min="0" max="100" value="0"><div class="range-ends"><span>still</span><span>wide sweep</span></div></div>
<div class="control"><label>Modulates</label><div class="select-row"><button class="choice active" data-dest="cutoff">Cutoff</button><button class="choice" data-dest="pitch">Pitch</button></div></div>
<div class="control"><label>Wave shape</label><div class="select-row"><button class="choice active" data-lfoshape="sine">Smooth</button><button class="choice" data-lfoshape="square">Stepped</button></div></div>
</div></section>
<section class="panel control-panel" id="fxModule"><div class="control-head"><div class="module-title"><span class="module-icon" style="color:var(--cyan)">05</span>Space <span class="module-meta">— ADD DEPTH</span></div><span class="module-meta">EFFECTS</span></div><div class="control-grid">
<div class="control"><label>Delay mix <span class="readout" id="delayVal">0%</span></label><input data-param="delay" type="range" min="0" max="70" value="0"><div class="range-ends"><span>dry</span><span>echo</span></div></div>
<div class="control"><label>Delay time <span class="readout" id="timeVal">280 ms</span></label><input data-param="time" type="range" min="80" max="700" value="280"><div class="range-ends"><span>short</span><span>long</span></div></div>
<div class="control"><label>Feedback <span class="readout" id="feedbackVal">32%</span></label><input data-param="feedback" type="range" min="0" max="75" value="32"><div class="range-ends"><span>one echo</span><span>many</span></div></div>
<div class="control"><label>Master <span class="readout" id="masterVal">70%</span></label><input data-param="master" type="range" min="0" max="100" value="70"><div class="range-ends"><span>quiet</span><span>loud</span></div></div>
</div></section>
<section class="panel keyboard-panel"><div class="keyboard-top"><span class="section-tag">Play the instrument</span><span class="key-hint">CLICK KEYS OR USE YOUR KEYBOARD</span></div><div class="keys" id="keys"></div><div class="keyboard-foot"><span>LOWER NOTES</span><span>A W S E D F T G Y H U J K L</span><span>HIGHER NOTES</span></div></section>
</main><aside class="side"><section class="panel lesson-card"><div class="lesson-top"><span class="lesson-kicker">Your learning path</span><span class="step-count" id="stepCount">01 / 06</span></div><div class="progress" id="progress"></div><h2 id="lessonTitle">Start with a wave</h2><p id="lessonText"></p><div class="try-box"><b>YOUR EXPERIMENT</b><span id="lessonTry"></span></div><div class="lesson-actions"><button class="primary" id="nextBtn">I hear the difference →</button><button class="secondary" id="prevBtn">Back</button></div><div class="unlock-note" id="unlockNote">Try changing a control to unlock the next idea.</div></section>
<section class="panel concept-card"><div class="concept-head"><strong>Signal chain</strong><span>THE BIG PICTURE</span></div><p id="chainText"></p></section>
<section class="panel signal"><div class="signal-icon">↗</div><div><strong id="signalTitle">START WITH A NOTE</strong><small id="signalSub">The waveform is your raw sound.</small></div></section></aside></div></div><div class="toast" id="toast"></div>
<script>
(()=>{'use strict';
const lessons=[
{title:'Start with a wave',text:'An oscillator repeats a tiny motion to create sound. The wave shape decides its character: a <em>sine</em> is pure and soft; a <em>saw</em> is bright and buzzy; a square sounds hollow. Every synth starts here.',try:'Play a note. Switch between Sine and Saw—listen for how much brighter the saw feels.',chain:'<em>Oscillator</em> → everything else. The waveform is the raw ingredient, not the finished sound.',signal:'START WITH A NOTE','sub':'The waveform is your raw sound.',modules:['oscModule']},
{title:'Find the pitch',text:'Pitch is simply how fast the wave repeats. Octave moves the frequency by a factor of two; fine tune nudges it by cents. Slightly detuning two sounds makes a thicker, wider tone.',try:'Change the octave, then bring it back. Try a tiny fine-tune shift and hear the color change.',chain:'<em>Oscillator</em> sets pitch and tone. Octaves double or halve frequency; cents are tiny tuning steps.',signal:'PITCH IS FREQUENCY','sub':'Faster vibration = higher note.',modules:['oscModule']},
{title:'Carve with a filter',text:'A filter removes parts of the sound. Low-pass keeps the lows and trims the highs—turn cutoff down for a darker tone. Resonance boosts a narrow band around the cutoff, adding a whistling edge.',try:'Play a sustained note. Sweep Cutoff from bright to dark, then add Resonance near the edge.',chain:'<em>Oscillator → Filter</em>. Start with a rich wave, then remove frequencies until it fits.',signal:'BRIGHTNESS IS A CHOICE','sub':'Cutoff decides which frequencies pass.',modules:['oscModule','filterModule']},
{title:'Give it a shape',text:'The amplitude envelope controls loudness over time. Attack is how fast a note swells; Decay falls to Sustain while you hold; Release is the fade after letting go. Together: ADSR.',try:'Set Attack high and play a key. Then lower it. Hold a note and listen to the Sustain; release to hear the tail.',chain:'<em>Oscillator → Filter → Envelope</em>. ADSR turns a steady tone into a pluck, pad, or organ-like note.',signal:'NOTES HAVE A LIFETIME','sub':'ADSR shapes every key press.',modules:['oscModule','filterModule','envModule']},
{title:'Make it move',text:'An LFO is a slow oscillator used as a motion source—not usually something you hear directly. It can sweep the filter for a wobble, or shift pitch for vibrato. Rate sets speed; Depth sets how much.',try:'Choose Cutoff and slowly raise Depth. Change Rate to hear the wobble speed up. Try Pitch for vibrato.',chain:'<em>LFO → Filter</em> adds motion over time. A tiny pitch depth gives vibrato; bigger filter depth makes a pronounced wobble.',signal:'MOTION, AUTOMATICALLY','sub':'LFO means low-frequency oscillator.',modules:['oscModule','filterModule','envModule','lfoModule']},
{title:'Put it in a space',text:'Delay records a little slice of sound and plays it back later. Mix blends echoes with the dry signal; Time spaces them out; Feedback feeds echoes back for repeating trails. A little goes a long way.',try:'Play a few notes, then raise Delay mix. Increase Feedback to build a repeating trail; pull Master down if it gets loud.',chain:'<em>Oscillator → Filter → Envelope → Effects</em>. Delay adds dimension after the sound is shaped.',signal:'THE FINISHING TOUCH','sub':'Effects place a sound in a scene.',modules:['oscModule','filterModule','envModule','lfoModule','fxModule']}
];
let step=0, interacted=new Set(), audio=null, voices=new Map(), wave='sine', filterType='lowpass', modDest='cutoff', lfoShape='sine', audioStarted=false;const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
function val(name){return +$(`[data-param="${name}"]`).value}
function setRead(id,text){$('#'+id).textContent=text}
const readouts={octave:v=>v>0?'+'+v:v,tune:v=>v+' ct',level:v=>v+'%',cutoff:v=>v>=1000?(v/1000).toFixed(1)+' kHz':v+' Hz',resonance:v=>v+'%',tracking:v=>v+'%',attack:v=>v+' ms',decay:v=>v+' ms',sustain:v=>v+'%',release:v=>v+' ms',rate:v=>Number(v).toFixed(1)+' Hz',depth:v=>v+'%',delay:v=>v+'%',time:v=>v+' ms',feedback:v=>v+'%',master:v=>v+'%'};
function updateRange(input){let min=+input.min,max=+input.max,p=(+input.value-min)/(max-min)*100;input.style.setProperty('--pct',p+'%');let n=input.dataset.param;let ids={octave:'octaveVal',tune:'tuneVal',level:'levelVal',cutoff:'cutoffVal',resonance:'resVal',tracking:'trackVal',attack:'attackVal',decay:'decayVal',sustain:'sustainVal',release:'releaseVal',rate:'rateVal',depth:'depthVal',delay:'delayVal',time:'timeVal',feedback:'feedbackVal',master:'masterVal'};setRead(ids[n],readouts[n](input.value));}
function markAction(){interacted.add(step);refreshLesson()}
function refreshLesson(){const l=lessons[step];$('#stepCount').textContent=String(step+1).padStart(2,'0')+' / 06';$('#lessonTitle').textContent=l.title;$('#lessonText').innerHTML=l.text;$('#lessonTry').textContent=l.try;$('#chainText').innerHTML=l.chain;$('#signalTitle').textContent=l.signal;$('#signalSub').textContent=l.sub;$('#progress').innerHTML=lessons.map((_,i)=>`<i class="${i<=step?'done':''}"></i>`).join('');$('#prevBtn').style.visibility=step===0?'hidden':'visible';$('#nextBtn').textContent=step===5?'Restart the lab ↺':'I hear the difference →';$('#unlockNote').textContent=interacted.has(step)?'Nice. Your next module is ready.':'Try changing a control to unlock the next idea.';$('#nextBtn').disabled=step<5&&!interacted.has(step);for(let i=0;i<5;i++){let el=$('#'+['oscModule','filterModule','envModule','lfoModule','fxModule'][i]);el.classList.toggle('disabled-module',i>step);let old=el.querySelector('.lock-overlay');if(old)old.remove();if(i>step){let cover=document.createElement('div');cover.className='lock-overlay';cover.innerHTML='<span>⌑</span> UNLOCK IN LESSON '+String(i+1).padStart(2,'0');el.style.position='relative';el.appendChild(cover)}}}
function go(to){if(to===step)return;if(to<0)return;if(to>=lessons.length){step=0;interacted.clear()}else step=to;refreshLesson();window.scrollTo({top:0,behavior:'smooth'})}
function toast(msg){let t=$('#toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1400)}
function initAudio(){if(audio)return;const C=window.AudioContext||window.webkitAudioContext;if(!C){toast('Web Audio is not supported in this browser');return}audio=new C();let analyser=audio.createAnalyser();analyser.fftSize=2048;audio.analyser=analyser;audio.master=audio.createGain();audio.master.gain.value=val('master')/100*.24;audio.delay=audio.createDelay(1);audio.delay.delayTime.value=val('time')/1000;audio.feedback=audio.createGain();audio.feedback.gain.value=val('feedback')/100;audio.send=audio.createGain();audio.wet=audio.createGain();audio.send.gain.value=val('delay')/100;audio.wet.gain.value=.72;audio.master.connect(analyser);analyser.connect(audio.destination);audio.send.connect(audio.delay);audio.delay.connect(audio.wet);audio.wet.connect(audio.master);audio.delay.connect(audio.feedback);audio.feedback.connect(audio.delay);audioStarted=true;$('#audioDot').classList.add('on');$('#audioStatus').textContent='AUDIO READY';$('#scopeMode').textContent='LIVE WAVEFORM';drawScope()}
function drawScope(){let canvas=$('#scope'),ctx=canvas.getContext('2d');let dpr=window.devicePixelRatio||1,w=canvas.clientWidth,h=canvas.clientHeight;if(canvas.width!==Math.round(w*dpr)||canvas.height!==Math.round(h*dpr)){canvas.width=w*dpr;canvas.height=h*dpr}ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);ctx.strokeStyle='#292e2b';ctx.lineWidth=1;for(let i=1;i<4;i++){ctx.beginPath();ctx.moveTo(0,h*i/4);ctx.lineTo(w,h*i/4);ctx.stroke()}ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(w,h/2);ctx.strokeStyle='#343a36';ctx.stroke();if(audio&&audio.analyser&&voices.size){let data=new Uint8Array(audio.analyser.fftSize);audio.analyser.getByteTimeDomainData(data);ctx.beginPath();for(let i=0;i<data.length;i++){let x=i/(data.length-1)*w,y=(data[i]/128-1)*h*.42+h/2;if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y)}ctx.strokeStyle='#d6fa62';ctx.lineWidth=2;ctx.shadowColor='#d6fa6270';ctx.shadowBlur=10;ctx.stroke();ctx.shadowBlur=0}else{let t=performance.now()/1000;ctx.beginPath();for(let x=0;x<w;x++){let y=h/2+Math.sin(x*.055+t*1.4)*(10+Math.sin(x*.008+t*.35)*5)+Math.sin(x*.11+t*1.4)*2;if(x===0)ctx.moveTo(x,y);else ctx.lineTo(x,y)}ctx.strokeStyle='#606962';ctx.lineWidth=1.5;ctx.stroke()}requestAnimationFrame(drawScope)}
function makeVoice(midi,key){initAudio();if(!audio)return;audio.resume();if(voices.has(key))return;const now=audio.currentTime,osc=audio.createOscillator(),filter=audio.createBiquadFilter(),amp=audio.createGain(),lfo=audio.createOscillator(),lfoGain=audio.createGain();osc.type=wave;osc.frequency.value=440*Math.pow(2,(midi-69+val('octave')*12)/12);osc.detune.value=val('tune');filter.type=filterType;filter.frequency.value=voiceCutoff(midi);filter.Q.value=val('resonance')/100*16;let peak=val('level')/100,sus=val('sustain')/100,att=val('attack')/1000,dec=val('decay')/1000;amp.gain.setValueAtTime(.0001,now);amp.gain.linearRampToValueAtTime(Math.max(.001,peak),now+att);amp.gain.linearRampToValueAtTime(Math.max(.001,peak*sus),now+att+dec);osc.connect(filter);filter.connect(amp);amp.connect(audio.master);amp.connect(audio.send);lfo.type=lfoShape;lfo.frequency.value=val('rate');lfoGain.gain.value=0;lfo.connect(lfoGain);if(modDest==='cutoff'){lfoGain.gain.value=val('depth')/100*2600;lfoGain.connect(filter.frequency)}else{lfoGain.gain.value=val('depth')/100*35;lfoGain.connect(osc.detune)}osc.start();lfo.start();voices.set(key,{osc,filter,amp,lfo,lfoGain,midi});$('#scopeMode').textContent='LIVE WAVEFORM';markAction()}
function voiceCutoff(midi){let base=val('cutoff'),tracking=val('tracking')/100;return Math.max(80,Math.min(16000,base*Math.pow(2,((midi-60)/12)*tracking)))}
function releaseVoice(key){if(!audio||!voices.has(key))return;let v=voices.get(key),now=audio.currentTime;v.amp.gain.cancelScheduledValues(now);v.amp.gain.setValueAtTime(Math.max(.0001,v.amp.gain.value),now);v.amp.gain.linearRampToValueAtTime(.0001,now+val('release')/1000);setTimeout(()=>{try{v.osc.stop();v.lfo.stop()}catch(e){}voices.delete(key);if(!voices.size)$('#scopeMode').textContent='LIVE WAVEFORM'},val('release')+80);markAction()}
const notes=[['C4',60,'a'],['C#4',61,'w'],['D4',62,'s'],['D#4',63,'e'],['E4',64,'d'],['F4',65,'f'],['F#4',66,'t'],['G4',67,'g'],['G#4',68,'y'],['A4',69,'h'],['A#4',70,'u'],['B4',71,'j'],['C5',72,'k'],['D5',74,'l']];let held=new Set();function buildKeys(){let box=$('#keys');box.innerHTML='';let whiteIndexes=[];notes.forEach((n,i)=>{if(!n[0].includes('#')){let b=document.createElement('button');b.className='key';b.dataset.note=n[1];b.dataset.key=n[2];b.innerHTML=`<span>${n[0]}<br><small>${n[2].toUpperCase()}</small></span>`;box.appendChild(b);whiteIndexes.push({note:n[1],el:b,index:whiteIndexes.length});b.addEventListener('pointerdown',e=>{e.preventDefault();b.setPointerCapture(e.pointerId);b.classList.add('active');makeVoice(n[1],'pointer'+n[1])});b.addEventListener('pointerup',()=>{b.classList.remove('active');releaseVoice('pointer'+n[1])});b.addEventListener('pointercancel',()=>releaseVoice('pointer'+n[1]))}});notes.filter(n=>n[0].includes('#')).forEach(n=>{let idx=notes.filter(x=>!x[0].includes('#')&&x[1]<n[1]).length;let b=document.createElement('button');b.className='key black';b.dataset.note=n[1];b.dataset.key=n[2];b.innerHTML=`<span>${n[2].toUpperCase()}</span>`;b.style.left=`calc(${idx*100/whiteIndexes.length}% - ${100/whiteIndexes.length*0.31}%)`;box.appendChild(b);b.addEventListener('pointerdown',e=>{e.preventDefault();b.setPointerCapture(e.pointerId);b.classList.add('active');makeVoice(n[1],'pointer'+n[1])});b.addEventListener('pointerup',()=>{b.classList.remove('active');releaseVoice('pointer'+n[1])});b.addEventListener('pointercancel',()=>releaseVoice('pointer'+n[1]))})}
function syncVoices(){if(!audio)return;for(let v of voices.values()){v.osc.type=wave;v.osc.frequency.setTargetAtTime(440*Math.pow(2,(v.midi-69+val('octave')*12)/12),audio.currentTime,.025);v.osc.detune.setTargetAtTime(val('tune'),audio.currentTime,.02);v.filter.type=filterType;v.filter.frequency.setTargetAtTime(voiceCutoff(v.midi),audio.currentTime,.03);v.filter.Q.setTargetAtTime(val('resonance')/100*16,audio.currentTime,.03);v.lfo.frequency.setTargetAtTime(val('rate'),audio.currentTime,.03);v.lfo.type=lfoShape;if(modDest==='cutoff'){v.lfoGain.disconnect();v.lfoGain.gain.value=val('depth')/100*2600;v.lfoGain.connect(v.filter.frequency)}else{v.lfoGain.disconnect();v.lfoGain.gain.value=val('depth')/100*35;v.lfoGain.connect(v.osc.detune)}}if(audio){audio.master.gain.setTargetAtTime(val('master')/100*.24,audio.currentTime,.04);audio.delay.delayTime.setTargetAtTime(val('time')/1000,audio.currentTime,.04);audio.feedback.gain.setTargetAtTime(val('feedback')/100,audio.currentTime,.04);audio.send.gain.setTargetAtTime(val('delay')/100,audio.currentTime,.04)}}
$$('input[type=range]').forEach(input=>{updateRange(input);input.addEventListener('input',()=>{updateRange(input);syncVoices();markAction()})});$$('[data-wave]').forEach(b=>b.addEventListener('click',()=>{wave=b.dataset.wave;$$('[data-wave]').forEach(x=>x.classList.toggle('active',x===b));syncVoices();markAction()}));$$('[data-filter]').forEach(b=>b.addEventListener('click',()=>{filterType=b.dataset.filter;$$('[data-filter]').forEach(x=>x.classList.toggle('active',x===b));syncVoices();markAction()}));$$('[data-dest]').forEach(b=>b.addEventListener('click',()=>{modDest=b.dataset.dest;$$('[data-dest]').forEach(x=>x.classList.toggle('active',x===b));syncVoices();markAction()}));$$('[data-lfoshape]').forEach(b=>b.addEventListener('click',()=>{lfoShape=b.dataset.lfoshape;$$('[data-lfoshape]').forEach(x=>x.classList.toggle('active',x===b));syncVoices();markAction()}));
$('#nextBtn').addEventListener('click',()=>go(step===5?6:step+1));$('#prevBtn').addEventListener('click',()=>go(step-1));$('#resetBtn').addEventListener('click',()=>{for(const k in {octave:0,tune:0,level:72,cutoff:4200,resonance:18,tracking:35,attack:35,decay:240,sustain:62,release:320,rate:4,depth:0,delay:0,time:280,feedback:32,master:70}){$(`[data-param="${k}"]`).value=({octave:0,tune:0,level:72,cutoff:4200,resonance:18,tracking:35,attack:35,decay:240,sustain:62,release:320,rate:4,depth:0,delay:0,time:280,feedback:32,master:70})[k];updateRange($(`[data-param="${k}"]`))}wave='sine';filterType='lowpass';modDest='cutoff';lfoShape='sine';$$('[data-wave]').forEach(b=>b.classList.toggle('active',b.dataset.wave==='sine'));$$('[data-filter]').forEach(b=>b.classList.toggle('active',b.dataset.filter==='lowpass'));$$('[data-dest]').forEach(b=>b.classList.toggle('active',b.dataset.dest==='cutoff'));$$('[data-lfoshape]').forEach(b=>b.classList.toggle('active',b.dataset.lfoshape==='sine'));syncVoices();step=0;interacted.clear();refreshLesson();toast('Back to the beginning.')});
buildKeys();refreshLesson();window.addEventListener('keydown',e=>{if(e.repeat||e.target.matches('input,button'))return;let n=notes.find(x=>x[2]===e.key.toLowerCase());if(n&&!held.has(n[2])){held.add(n[2]);let b=$(`[data-key="${n[2]}"]`);if(b)b.classList.add('active');makeVoice(n[1],'key'+n[2])}});window.addEventListener('keyup',e=>{let k=e.key.toLowerCase();if(held.has(k)){held.delete(k);let n=notes.find(x=>x[2]===k),b=$(`[data-key="${k}"]`);if(b)b.classList.remove('active');releaseVoice('key'+k)}});window.addEventListener('blur',()=>{for(let k of held){releaseVoice('key'+k);let b=$(`[data-key="${k}"]`);if(b)b.classList.remove('active')}held.clear()});
})();
</script>
</body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"b075b6fa-40b9-43a6-99db-340d92b4c393","tokensIn":177192,"tokensOut":78824,"tokensTotal":256016,"cost":0.04326976999999999,"turns":11,"toolCalls":10,"failedToolCalls":1,"timestamp":"2026-09-23T02:45:36.590Z"} -->