TutSynth
Gemma 4 31b · composer

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 20,294 bytes · SHA-256 d859e2b93198
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SynthLab: Learn Synthesis</title>
<style>
/* ── css: layout ── */
:root {
--bg-color: #1a1a2e;
--panel-color: #16213e;
--accent-color: #4ecca3;
--text-color: #e94560;
--light-text: #eeeeee;
}
body {
background-color: var(--bg-color);
color: var(--light-text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
#app {
width: 95vw;
max-width: 1100px;
background: var(--panel-color);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
border: 1px solid #333;
}
header {
text-align: center;
margin-bottom: 2rem;
}
#main-container {
display: grid;
grid-template-columns: 1fr 350px;
gap: 2rem;
}
#synth-interface {
background: #0f3460;
padding: 2rem;
border-radius: 15px;
border: 3px solid #000;
box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
}
#controls-area {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.control-group {
background: rgba(0,0,0,0.3);
padding: 1rem;
border-radius: 10px;
border-left: 4px solid var(--accent-color);
}
.control-group h4 {
margin: 0 0 10px 0;
font-size: 0.9rem;
color: var(--accent-color);
text-transform: uppercase;
}
.slider-row {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.slider-row label {
font-size: 0.8rem;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
}
input[type=range] {
width: 100%;
cursor: pointer;
}
#keyboard {
display: flex;
justify-content: center;
padding: 20px 0;
background: #000;
border-radius: 10px;
overflow-x: auto;
}
.key {
width: 40px;
height: 150px;
border: 1px solid #000;
background: white;
color: black;
cursor: pointer;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 10px;
border-radius: 0 0 5px 5px;
user-select: none;
transition: background 0.1s;
}
.key.black {
background: #333;
color: white;
width: 30px;
height: 100px;
margin-left: -15px;
margin-right: -15px;
z-index: 2;
}
.key:active, .key.active {
background: var(--accent-color);
}
#lesson-panel {
background: rgba(255,255,255,0.05);
padding: 1.5rem;
border-radius: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#lesson-content h2 {
margin-top: 0;
color: var(--accent-color);
}
#lesson-nav {
display: flex;
justify-content: space-between;
margin-top: 2rem;
}
button {
padding: 10px 20px;
border-radius: 5px;
border: none;
background: var(--accent-color);
color: #1a1a2e;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
}
button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
#viz-container {
height: 100px;
background: #000;
margin-bottom: 1rem;
border-radius: 10px;
border: 1px solid #333;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="unlock-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; text-align: center; padding: 20px;">
<h2 style="color: var(--accent-color); font-size: 2.5rem; margin-bottom: 1rem;">Ready to make some noise?</h2>
<p style="font-size: 1.2rem; color: #ccc;">Click anywhere to activate the synthesizer</p>
<div style="margin-top: 2rem; font-size: 3rem;">🔊</div>
</div>
<div id="app">
<header>
<h1 style="margin: 0; display: inline-block; vertical-align: middle;">🎹 SynthLab</h1>
<p id="tutorial-text" style="display: inline-block; margin-left: 20px; font-style: italic; color: #666;">Interactive Synthesis Learning</p>
</header>
<div id="main-container">
<div id="synth-interface">
<div id="controls-area">
<!-- Controls will be injected here by JS based on current lesson -->
</div>
<div id="keyboard">
<!-- Keys will be injected here -->
</div>
</div>
<div id="lesson-panel">
<div id="lesson-content">
<h2 id="lesson-title">Lesson 1: The Oscillator</h2>
<p id="lesson-description">The heart of any synth is the Oscillator. It generates a repeating waveform. Different shapes create different "timbres" (tones).</p>
</div>
<div id="lesson-nav">
<button id="prev-btn" disabled="">Previous</button>
<button id="next-btn">Next Lesson</button>
</div>
</div>
</div>
</div>
<script>
'use strict';
// ── const: SYNTH_STATE ──
const SYNTH_STATE = {
currentStep: 0,
waveType: 'sine',
adsr: {
attack: 0.1,
decay: 0.2,
sustain: 0.5,
release: 0.8
},
filter: {
cutoff: 2000,
resonance: 1
}
};
// ── const: STEPS ──
const STEPS = [
{
title: "Lesson 1: The Oscillator",
description: "The Oscillator is where sound begins. It produces a raw waveform. Try switching between Sine (smooth), Square (hollow), Sawtooth (bright), and Triangle (muted).",
controls: ['waveType'],
highlight: 'Oscillator'
},
{
title: "Lesson 2: The Filter",
description: "A Low-Pass Filter removes high frequencies, making a sound 'darker'. Increasing resonance (Q) adds a peak at the cutoff frequency, creating a 'nasal' or 'squelchy' sound.",
controls: ['waveType', 'filterCutoff', 'filterRes'],
highlight: 'Filter'
},
{
title: "Lesson 3: The Envelope (ADSR)",
description: "ADSR (Attack, Decay, Sustain, Release) shapes how a sound evolves over time. Attack is the fade-in, Decay is the drop to Sustain level, and Release is the fade-out after you let go.",
controls: ['waveType', 'filterCutoff', 'filterRes', 'attack', 'decay', 'sustain', 'release'],
highlight: 'Envelope'
}
];
// ── const: CONTROL_DEFS ──
const CONTROL_DEFS = {
waveType: { label: 'Waveform', type: 'select', options: ['sine', 'square', 'sawtooth', 'triangle'], group: 'Oscillator' },
filterCutoff: { label: 'Cutoff', type: 'range', min: 20, max: 10000, step: 1, unit: 'Hz', group: 'Filter', stateKey: 'filter', stateProp: 'cutoff' },
filterRes: { label: 'Resonance', type: 'range', min: 0.1, max: 20, step: 0.1, unit: '', group: 'Filter', stateKey: 'filter', stateProp: 'resonance' },
attack: { label: 'Attack', type: 'range', min: 0.01, max: 2, step: 0.01, unit: 's', group: 'Envelope', stateKey: 'adsr', stateProp: 'attack' },
decay: { label: 'Decay', type: 'range', min: 0.01, max: 2, step: 0.01, unit: 's', group: 'Envelope', stateKey: 'adsr', stateProp: 'decay' },
sustain: { label: 'Sustain', type: 'range', min: 0, max: 1, step: 0.01, unit: '', group: 'Envelope', stateKey: 'adsr', stateProp: 'sustain' },
release: { label: 'Release', type: 'range', min: 0.01, max: 5, step: 0.01, unit: 's', group: 'Envelope', stateKey: 'adsr', stateProp: 'release' },
};
// ── function: initAudio ──
function initAudio(synth) {
// This handles the "Resume AudioContext" requirement for modern browsers
const unlock = () => {
if (synth.ctx.state === 'suspended') {
synth.ctx.resume().then(() => {
console.log('AudioContext resumed');
document.getElementById('unlock-overlay').style.display = 'none';
});
}
};
const overlay = document.getElementById('unlock-overlay');
if (overlay) {
overlay.onclick = unlock;
}
// Also try to resume on first key press
window.addEventListener('mousedown', unlock, { once: true });
}
// ── function: playNote ──
function playNote(freq) {
initAudio();
const ctx = SYNTH_STATE.audioCtx;
// Stop previous osc if any
if (SYNTH_STATE.oscillator) {
try { SYNTH_STATE.oscillator.stop(); } catch(e){}
}
const osc = ctx.createOscillator();
osc.type = SYNTH_STATE.settings.waveform;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
// Envelope application
const now = ctx.currentTime;
const { attack, decay, sustain } = SYNTH_STATE.settings;
// Initial gain is 0
SYNTH_STATE.gainNode.gain.cancelScheduledValues(now);
SYNTH_STATE.gainNode.gain.setValueAtTime(0, now);
// Attack
SYNTH_STATE.gainNode.gain.linearRampToValueAtTime(0.5, now + attack);
// Decay to sustain
SYNTH_STATE.gainNode.gain.linearRampToValueAtTime(sustain * 0.5, now + attack + decay);
// Filter
SYNTH_STATE.filterNode.frequency.setValueAtTime(SYNTH_STATE.settings.cutoff, now);
SYNTH_STATE.filterNode.Q.setValueAtTime(SYNTH_STATE.settings.resonance, now);
osc.connect(SYNTH_STATE.filterNode);
osc.start();
SYNTH_STATE.oscillator = osc;
}
// ── function: stopNote ──
function stopNote() {
if (!SYNTH_STATE.gainNode) return;
const now = SYNTH_STATE.audioCtx.currentTime;
const { release } = SYNTH_STATE.settings;
SYNTH_STATE.gainNode.gain.cancelScheduledValues(now);
SYNTH_STATE.gainNode.gain.setValueAtTime(SYNTH_STATE.gainNode.gain.value, now);
SYNTH_STATE.gainNode.gain.linearRampToValueAtTime(0, now + release);
// Stop oscillator after release
const oldOsc = SYNTH_STATE.oscillator;
setTimeout(() => {
if (oldOsc) try { oldOsc.stop(); } catch(e){}
}, release * 1000 + 100);
}
// ── function: createKeyboard ──
function createKeyboard() {
const kb = document.getElementById('keyboard');
const notes = [
{ note: 'C', freq: 261.63, black: false },
{ note: 'C#', freq: 277.18, black: true },
{ note: 'D', freq: 293.66, black: false },
{ note: 'D#', freq: 311.13, black: true },
{ note: 'E', freq: 329.63, black: false },
{ note: 'F', freq: 349.23, black: false },
{ note: 'F#', freq: 369.99, black: true },
{ note: 'G', freq: 392.00, black: false },
{ note: 'G#', freq: 415.30, black: true },
{ note: 'A', freq: 440.00, black: false },
{ note: 'A#', freq: 466.16, black: true },
{ note: 'B', freq: 493.88, black: false },
{ note: 'C2', freq: 523.25, black: false },
];
notes.forEach(n => {
const el = document.createElement('div');
el.className = `key ${n.black ? 'black' : 'white'}`;
el.textContent = n.note;
el.dataset.freq = n.freq;
kb.appendChild(el);
});
}
// ── function: updateTutorial ──
function updateTutorial(stepIdx, synth) {
const step = STEPS[stepIdx];
document.getElementById('lesson-title').textContent = step.title;
document.getElementById('lesson-description').textContent = step.description;
document.getElementById('prev-btn').disabled = stepIdx === 0;
document.getElementById('next-btn').disabled = stepIdx === STEPS.length - 1;
const controlsArea = document.getElementById('controls-area');
controlsArea.innerHTML = '';
const groups = {};
step.controls.forEach(ctrlId => {
const def = CONTROL_DEFS[ctrlId];
if (!groups[def.group]) {
groups[def.group] = document.createElement('div');
groups[def.group].className = 'control-group';
groups[def.group].innerHTML = `<h4>${def.group}</h4>`;
controlsArea.appendChild(groups[def.group]);
}
const { container, input } = createControlElement(ctrlId, def);
// Set initial value
let val;
if (ctrlId === 'waveType') val = SYNTH_STATE.waveType;
else if (def.stateKey) val = SYNTH_STATE[def.stateKey][def.stateProp];
input.value = val;
const valDisplay = document.getElementById(`${ctrlId}-val`);
if (valDisplay) valDisplay.textContent = `${val}${def.unit || ''}`;
input.oninput = (e) => {
const newValue = e.target.value;
if (ctrlId === 'waveType') {
SYNTH_STATE.waveType = newValue;
window.currentWaveType = newValue;
} else if (def.stateKey) {
SYNTH_STATE[def.stateKey][def.stateProp] = parseFloat(newValue);
if (def.stateKey === 'filter') {
synth.updateFilter(SYNTH_STATE.filter.cutoff, SYNTH_STATE.filter.resonance);
}
}
if (valDisplay) valDisplay.textContent = `${newValue}${def.unit || ''}`;
};
groups[def.group].appendChild(container);
});
}
// ── function: createControlElement ──
function createControlElement(id, def) {
const container = document.createElement('div');
container.className = 'slider-row';
const labelDiv = document.createElement('div');
labelDiv.className = 'slider-label';
labelDiv.innerHTML = `<span>${def.label}</span><span id="${id}-val"></span>`;
let input;
if (def.type === 'select') {
input = document.createElement('select');
def.options.forEach(opt => {
const o = document.createElement('option');
o.value = opt;
o.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
input.appendChild(o);
});
} else {
input = document.createElement('input');
input.type = 'range';
input.min = def.min;
input.max = def.max;
input.step = def.step;
}
input.id = id + '-slider';
container.appendChild(labelDiv);
container.appendChild(input);
return { container, input, labelDiv };
}
// ── class: SynthEngine ──
class SynthEngine {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.2;
this.masterGain.connect(this.ctx.destination);
this.oscillator = null;
this.filter = null;
this.env = null;
this.setupFilter();
}
setupFilter() {
this.filter = this.ctx.createBiquadFilter();
this.filter.type = 'lowpass';
this.filter.frequency.value = 20000;
this.filter.Q.value = 1;
this.filter.connect(this.masterGain);
}
playNote(freq) {
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
const osc = this.ctx.createOscillator();
const noteGain = this.ctx.createGain();
osc.type = window.currentWaveType || 'sine';
osc.frequency.value = freq;
// ADSR implementation via gainNode schedules
const now = this.ctx.currentTime;
const { attack, decay, sustain, release } = window.currentADSR;
noteGain.gain.setValueAtTime(0, now);
noteGain.gain.linearRampToValueAtTime(1, now + attack);
noteGain.gain.linearRampToValueAtTime(sustain, now + attack + decay);
osc.connect(noteGain);
noteGain.connect(this.filter);
osc.start();
return { osc, noteGain, startTime: now };
}
stopNote(noteObj) {
const now = this.ctx.currentTime;
const { release } = window.currentADSR;
noteObj.noteGain.gain.cancelScheduledValues(now);
noteObj.noteGain.gain.setValueAtTime(noteObj.noteGain.gain.value, now);
noteObj.noteGain.gain.linearRampToValueAtTime(0, now + release);
noteObj.osc.stop(now + release + 0.1);
}
updateFilter(cutoff, res) {
this.filter.frequency.setTargetAtTime(cutoff, this.ctx.currentTime, 0.05);
this.filter.Q.setTargetAtTime(res, this.ctx.currentTime, 0.05);
}
}
// ── function: createVisualizer ──
function createVisualizer(synth) {
const vizContainer = document.createElement('div');
vizContainer.id = 'viz-container';
const canvas = document.createElement('canvas');
canvas.id = 'viz-canvas';
vizContainer.appendChild(canvas);
const controlsArea = document.getElementById('controls-area');
controlsArea.prepend(vizContainer);
const ctx = canvas.getContext('2d');
const analyzer = synth.ctx.createAnalyser();
analyzer.fftSize = 2048;
// Connect master gain to analyzer
synth.masterGain.connect(analyzer);
const bufferLength = analyzer.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
requestAnimationFrame(draw);
// Resize canvas to match display size
if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
analyzer.getByteTimeDomainData(dataArray);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 2;
ctx.strokeStyle = '#4ecca3';
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();
}
draw();
}
// ── main ──
const synth = new SynthEngine();
window.currentWaveType = SYNTH_STATE.waveType;
window.currentADSR = SYNTH_STATE.adsr;
initAudio(synth);
createKeyboard();
createVisualizer(synth);
updateTutorial(SYNTH_STATE.currentStep, synth);
const activeNotes = new Map();
// Keyboard interactions
document.getElementById('keyboard').addEventListener('mousedown', (e) => {
if (e.target.classList.contains('key')) {
const freq = parseFloat(e.target.dataset.freq);
const noteObj = synth.playNote(freq);
activeNotes.set(e.target, noteObj);
e.target.classList.add('active');
}
});
document.getElementById('keyboard').addEventListener('mouseup', (e) => {
if (e.target.classList.contains('key')) {
const noteObj = activeNotes.get(e.target);
if (noteObj) {
synth.stopNote(noteObj);
activeNotes.delete(e.target);
}
e.target.classList.remove('active');
}
});
document.getElementById('keyboard').addEventListener('mouseleave', (e) => {
if (e.target.classList.contains('key')) {
const noteObj = activeNotes.get(e.target);
if (noteObj) {
synth.stopNote(noteObj);
activeNotes.delete(e.target);
}
e.target.classList.remove('active');
}
});
// Navigation
document.getElementById('next-btn').onclick = () => {
if (SYNTH_STATE.currentStep < STEPS.length - 1) {
SYNTH_STATE.currentStep++;
updateTutorial(SYNTH_STATE.currentStep, synth);
}
};
document.getElementById('prev-btn').onclick = () => {
if (SYNTH_STATE.currentStep > 0) {
SYNTH_STATE.currentStep--;
updateTutorial(SYNTH_STATE.currentStep, synth);
}
};
// Keep ADSR synced
setInterval(() => {
window.currentADSR = SYNTH_STATE.adsr;
window.currentWaveType = SYNTH_STATE.waveType;
}, 100);
// Initial Filter Setup
synth.updateFilter(SYNTH_STATE.filter.cutoff, SYNTH_STATE.filter.resonance);
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"composer","sessionId":"1111a602-1188-40fe-a153-72eaa3c68ef3","tokensIn":176667,"tokensOut":7695,"tokensTotal":184362,"turns":19,"toolCalls":18,"failedToolCalls":2,"timestamp":"2026-07-15T01:45:42.551Z"} -->