Music Easy
Vaporwave Synth
Generative vaporwave synthesizer with 10 tracks — Web Audio API step sequencer with supersaw pads, bass, lead, and drums. Click play and browse.
Open in Lab
MCP
web-audio-api
Targets: JS HTML
Code
:root {
--bg: #0f1020;
--panel: #17192e;
--panel-2: #1f2340;
--text: #f2f4ff;
--muted: #aab0d6;
--accent: #ff7ad9;
--accent-2: #7afcff;
--ok: #8dffb1;
--warn: #ffd27a;
--border: rgba(255, 255, 255, 0.08);
--shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
body {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at top left, rgba(255, 122, 217, 0.18), transparent 30%),
radial-gradient(circle at top right, rgba(122, 252, 255, 0.16), transparent 28%),
linear-gradient(180deg, #111326, #0b0c18);
color: var(--text);
min-height: 100vh;
padding: 20px;
}
.hidden {
display: none !important;
}
/* App shell */
.app {
max-width: 1140px;
margin: 0 auto;
display: grid;
gap: 16px;
}
.header {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid var(--border);
border-radius: 20px;
padding: 24px 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
}
h1 {
font-size: clamp(24px, 4vw, 40px);
letter-spacing: -0.03em;
line-height: 1.1;
}
.header p {
color: var(--muted);
font-size: 14px;
margin-top: 6px;
}
/* Layout */
.layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 16px;
}
/* Track list */
.tracklist {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.005));
border: 1px solid var(--border);
border-radius: 20px;
padding: 12px;
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
gap: 4px;
max-height: 580px;
overflow-y: auto;
}
.track-item {
display: grid;
grid-template-columns: 28px 1fr auto;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 14px;
cursor: pointer;
border: 1px solid transparent;
transition: all 120ms ease;
font-size: 13px;
color: var(--muted);
}
.track-item:hover {
background: rgba(255, 255, 255, 0.04);
color: var(--text);
}
.track-item.active {
background: linear-gradient(135deg, rgba(255, 122, 217, 0.15), rgba(122, 252, 255, 0.08));
border-color: rgba(255, 122, 217, 0.2);
color: var(--text);
}
.track-num {
font-weight: 700;
font-size: 12px;
text-align: center;
opacity: 0.5;
}
.track-item.active .track-num {
color: var(--accent);
opacity: 1;
}
.track-name {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-info {
font-size: 11px;
opacity: 0.5;
white-space: nowrap;
}
/* Eq animation on active playing track */
.track-item.active.playing .track-num {
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* Player */
.player {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border: 1px solid var(--border);
border-radius: 20px;
padding: 20px;
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
display: grid;
gap: 16px;
}
/* Now playing */
.now-playing {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.now-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent-2);
font-weight: 700;
}
.now-title {
font-size: 18px;
font-weight: 800;
letter-spacing: -0.02em;
}
.now-meta {
font-size: 12px;
color: var(--muted);
margin-left: auto;
}
/* Transport */
.transport {
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
}
.t-btn {
appearance: none;
border: 0;
border-radius: 14px;
width: 46px;
height: 46px;
display: grid;
place-items: center;
cursor: pointer;
color: var(--text);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
border: 1px solid var(--border);
transition: transform 100ms, background 100ms;
}
.t-btn:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.08);
}
.t-btn:active {
transform: translateY(0);
}
.t-play {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 122, 217, 0.3), rgba(122, 252, 255, 0.18));
border-color: rgba(255, 122, 217, 0.25);
}
.t-play:hover {
background: linear-gradient(135deg, rgba(255, 122, 217, 0.4), rgba(122, 252, 255, 0.25));
}
.t-stop {
color: #ff8080;
}
/* Sliders */
.sliders {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.slider-row {
display: grid;
grid-template-columns: 56px 1fr 36px;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
}
.slider-val {
font-weight: 700;
font-size: 12px;
text-align: right;
color: var(--text);
}
input[type="range"] {
width: 100%;
accent-color: var(--accent);
}
/* Stats */
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.stat {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 14px;
padding: 10px 14px;
}
.sk {
font-size: 11px;
color: var(--muted);
display: block;
margin-bottom: 2px;
}
.sv {
font-size: 18px;
font-weight: 800;
letter-spacing: -0.03em;
}
/* Visualizer */
.visualizer {
height: 140px;
border-radius: 18px;
border: 1px solid var(--border);
background: linear-gradient(180deg, rgba(122, 252, 255, 0.04), rgba(255, 122, 217, 0.06));
position: relative;
overflow: hidden;
}
.bars {
position: absolute;
inset: 10px;
display: grid;
grid-template-columns: repeat(32, 1fr);
gap: 4px;
align-items: end;
}
.bar {
border-radius: 999px 999px 0 0;
min-height: 6px;
background: linear-gradient(180deg, rgba(122, 252, 255, 0.9), rgba(255, 122, 217, 0.8));
opacity: 0.8;
transition: height 60ms;
}
/* Steps */
.steps {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
}
.step {
height: 52px;
border-radius: 14px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
display: grid;
place-items: center;
color: var(--muted);
font-size: 12px;
font-weight: 600;
transition: transform 80ms, background 80ms, box-shadow 80ms;
}
.step.active {
background: linear-gradient(180deg, rgba(255, 122, 217, 0.22), rgba(122, 252, 255, 0.12));
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 122, 217, 0.15);
}
/* Editor */
.editor-section {
margin-top: 4px;
}
.editor-toggle {
appearance: none;
border: 0;
border-radius: 12px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
color: var(--muted);
background: var(--panel-2);
border: 1px solid var(--border);
transition: color 100ms, background 100ms;
}
.editor-toggle:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.06);
}
.editor-panel {
margin-top: 10px;
display: grid;
gap: 10px;
}
.editor-panel textarea {
background: var(--panel-2);
color: var(--text);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
min-height: 260px;
resize: vertical;
width: 100%;
}
.apply-btn {
appearance: none;
border: 0;
border-radius: 12px;
padding: 10px 20px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
color: var(--text);
background: linear-gradient(135deg, rgba(122, 252, 255, 0.2), rgba(255, 122, 217, 0.15));
border: 1px solid rgba(122, 252, 255, 0.2);
transition: transform 100ms;
justify-self: start;
}
.apply-btn:hover {
transform: translateY(-1px);
}
.editor-status {
font-size: 12px;
min-height: 16px;
}
.editor-status.ok {
color: var(--ok);
}
.editor-status.err {
color: var(--warn);
}
/* Responsive */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.tracklist {
max-height: 200px;
flex-direction: row;
flex-wrap: wrap;
}
.track-item {
flex: 0 0 auto;
}
.sliders {
grid-template-columns: 1fr;
}
.stats-row {
grid-template-columns: 1fr 1fr;
}
}/* ── Scales & Roots ─────────────────────────────────── */
const SCALES = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
};
const ROOTS = {
C: 60,
"C#": 61,
Db: 61,
D: 62,
"D#": 63,
Eb: 63,
E: 64,
F: 65,
"F#": 66,
Gb: 66,
G: 67,
"G#": 68,
Ab: 68,
A: 69,
"A#": 70,
Bb: 70,
B: 71,
};
/* ── 10 Presets (Tracks) ───────────────────────────── */
/* Each track has a distinct personality:
* 1 Classic vaporwave — steady, familiar
* 2 Ethereal mallsoft — no drums, glacial, ultra-reverb
* 3 Dark bass-driven — dry, punchy, active bass line
* 4 Cascading arpeggios — fast steps, bright, constant hi-hats
* 5 Slow tape — minimal, heavy swing, tape-degraded
* 6 Ambient meditation — no drums, slowest, massive space
* 7 Bright cruise — fastest, major key, full drums, dry
* 8 Lo-fi boom bap — warm, muffled, sparse fragments
* 9 Cinematic neon — no drums, atmospheric, wide delay
* 10 Funky groove — complex drums, syncopated, danceable
*/
const PRESETS = [
// 1 — Classic supersaw vaporwave
{
name: "Vaporwave Basico",
root: "C",
mode: "minor",
bpm: 78,
swing: 0.03,
drums: { kick: "x...x...", snare: "....x...", hat: ".x.x.x.x" },
pad: [0, 2, 4, 7, 6, 4, 2, 0],
bass: [0, null, -2, null, -4, null, -2, null],
lead: [12, 10, 7, null, 10, 7, 5, null],
fx: { room: 0.72, delay: 0.34, tone: 900, master: 0.72, slow: 2.0 },
synth: {},
},
// 2 — Pure sine tones, no drums, ethereal mall muzak
{
name: "Mallsoft Suave",
root: "Ab",
mode: "major",
bpm: 58,
swing: 0,
drums: { kick: "........", snare: "........", hat: "........" },
pad: [0, 4, 7, 11, 14, 11, 7, 4],
bass: [0, null, null, null, 0, null, null, null],
lead: [14, 12, 11, 9, 7, 9, 11, 12],
fx: { room: 0.94, delay: 0.55, tone: 520, master: 0.58, slow: 4.5 },
synth: {
padType: "sine",
padVoices: 1,
padDetune: 0,
padAttack: 0.15,
padRelease: 1.2,
bassType: "sine",
bassSub: false,
leadType: "sine",
leadQ: 0.3,
},
},
// 3 — Dark square-wave bass, aggressive, dry
{
name: "Bassline Nocturno",
root: "D",
mode: "minor",
bpm: 84,
swing: 0.02,
drums: { kick: "x.x..x.x", snare: "....x...", hat: "x.x.x.x." },
pad: [7, null, 5, null, 3, null, 5, null],
bass: [0, 3, -2, 0, -4, -2, 0, 3],
lead: [14, null, 12, 14, 10, null, 12, null],
fx: { room: 0.42, delay: 0.18, tone: 1050, master: 0.76, slow: 1.6 },
synth: {
padType: "square",
padVoices: 2,
padDetune: 5,
padQ: 1.5,
bassType: "square",
bassQ: 2.0,
leadType: "sawtooth",
kickPitch: 120,
kickDecay: 0.28,
snareTone: 180,
},
},
// 4 — Ultra-wide 5-voice supersaw, 8-bit lead, cascading arps
{
name: "Arpegio VHS",
root: "A",
mode: "minor",
bpm: 80,
swing: 0,
drums: { kick: "x...x...", snare: "........", hat: "xxxxxxxx" },
pad: [0, 2, 4, 7, 9, 11, 14, 16],
bass: [0, null, -4, null, 0, null, -4, null],
lead: [7, 9, 11, 14, 16, 14, 11, 9],
fx: { room: 0.65, delay: 0.42, tone: 1300, master: 0.7, slow: 1.4 },
synth: {
padVoices: 5,
padDetune: 15,
bassType: "triangle",
leadType: "square",
leadQ: 1.0,
hatFreq: 8000,
hatDecay: 0.03,
},
},
// 5 — Detuned triangle, slow tape, heavy swing
{
name: "Beat Lento",
root: "G",
mode: "minor",
bpm: 66,
swing: 0.06,
drums: { kick: "x.......", snare: "....x...", hat: "........" },
pad: [0, 2, 3, null, -1, null, 0, null],
bass: [0, null, null, null, -3, null, null, null],
lead: [12, null, null, 10, null, null, 7, null],
fx: { room: 0.82, delay: 0.38, tone: 580, master: 0.68, slow: 3.2 },
synth: {
padType: "triangle",
padDetune: 20,
padAttack: 0.08,
padRelease: 0.9,
bassType: "sine",
leadType: "triangle",
kickPitch: 100,
kickDecay: 0.35,
snareDecay: 0.25,
},
},
// 6 — Sine chorus ambient, no drums, glacial meditation
{
name: "Acordes Sonados",
root: "Db",
mode: "major",
bpm: 56,
swing: 0,
drums: { kick: "........", snare: "........", hat: "........" },
pad: [0, 4, 7, 11, 14, 11, 7, 4],
bass: [0, null, null, null, -3, null, null, null],
lead: [21, 19, 16, 14, 12, 14, 16, 19],
fx: { room: 0.95, delay: 0.58, tone: 480, master: 0.55, slow: 5.0 },
synth: {
padType: "sine",
padVoices: 4,
padDetune: 3,
padAttack: 0.2,
padRelease: 1.5,
padQ: 0.3,
bassType: "sine",
bassSub: false,
bassQ: 0.5,
leadType: "sine",
leadQ: 0.3,
},
},
// 7 — Bright square pad, punchy drums, fastest, energetic
{
name: "Sunset Cruise",
root: "E",
mode: "major",
bpm: 94,
swing: 0.04,
drums: { kick: "x.x.x.x.", snare: "..x...x.", hat: ".x.x.x.x" },
pad: [0, 2, 4, 7, 4, 2, 0, -1],
bass: [0, -1, -3, -1, 0, 2, 0, -1],
lead: [7, 9, 11, 14, 16, 14, 11, 9],
fx: { room: 0.38, delay: 0.16, tone: 1400, master: 0.78, slow: 1.5 },
synth: {
padType: "square",
padVoices: 3,
padDetune: 7,
leadType: "sawtooth",
leadQ: 1.0,
kickPitch: 160,
kickDecay: 0.15,
snareTone: 260,
hatDecay: 0.04,
},
},
// 8 — Heavily detuned triangle, warm lo-fi, muffled drums
{
name: "Lo-fi Vapor",
root: "Bb",
mode: "minor",
bpm: 72,
swing: 0.05,
drums: { kick: "x..x..x.", snare: "....x...", hat: "..x...x." },
pad: [0, 3, 5, 3, 0, -2, 0, null],
bass: [0, null, -2, null, -3, null, 0, null],
lead: [12, null, 10, null, null, 7, null, null],
fx: { room: 0.74, delay: 0.32, tone: 540, master: 0.66, slow: 2.6 },
synth: {
padType: "triangle",
padVoices: 2,
padDetune: 25,
padAttack: 0.06,
bassType: "triangle",
leadType: "sine",
kickPitch: 110,
kickDecay: 0.3,
hatFreq: 4500,
hatDecay: 0.08,
},
},
// 9 — Clean sine, cinematic, no drums, huge delay
{
name: "Intro de Neon",
root: "D#",
mode: "minor",
bpm: 60,
swing: 0,
drums: { kick: "........", snare: "........", hat: "........" },
pad: [0, null, 4, null, 7, null, 4, null],
bass: [0, null, null, null, 0, null, null, null],
lead: [14, 12, null, 10, null, 7, null, 12],
fx: { room: 0.92, delay: 0.62, tone: 680, master: 0.6, slow: 4.0 },
synth: {
padType: "sine",
padVoices: 2,
padDetune: 2,
padAttack: 0.25,
padRelease: 1.8,
padQ: 0.4,
bassType: "sine",
bassSub: false,
bassQ: 0.5,
leadType: "triangle",
leadQ: 0.4,
},
},
// 10 — Square-wave funk, complex drums, punchy stabs, max swing
{
name: "Vaporwave Groove",
root: "F",
mode: "minor",
bpm: 90,
swing: 0.07,
drums: { kick: "x..x.x..", snare: "..x...x.", hat: ".xxx.xxx" },
pad: [0, null, 3, null, 7, null, 10, null],
bass: [0, -2, 0, 3, -4, -2, 0, 3],
lead: [12, 14, null, 10, 12, null, 7, 10],
fx: { room: 0.48, delay: 0.22, tone: 1100, master: 0.78, slow: 1.7 },
synth: {
padType: "square",
padVoices: 2,
padDetune: 4,
padAttack: 0.01,
padRelease: 0.3,
bassType: "square",
bassQ: 2.5,
leadType: "square",
leadQ: 1.2,
kickPitch: 150,
kickDecay: 0.18,
snareTone: 240,
},
},
];
/* ── Per-track synth defaults ──────────────────────── */
const DEFAULT_SYNTH = {
padType: "sawtooth",
padVoices: 3,
padDetune: 9,
padAttack: 0.03,
padRelease: 0.6,
padQ: 0.8,
bassType: "sawtooth",
bassSub: true,
bassQ: 1.2,
leadType: "triangle",
leadQ: 0.6,
kickPitch: 140,
kickDecay: 0.22,
snareTone: 220,
snareDecay: 0.18,
hatFreq: 6500,
hatDecay: 0.06,
};
function getSynth() {
return { ...DEFAULT_SYNTH, ...(currentPreset.synth || {}) };
}
/* ── State ──────────────────────────────────────────── */
let audioCtx, masterGain, dryGain, delayNode, feedbackGain, wetGain, analyser, compressor;
let schedulerTimer = null,
nextNoteTime = 0,
stepIndex = 0,
isPlaying = false;
let currentTrack = 0;
let currentPreset = structuredClone(PRESETS[0]);
/* ── DOM refs ───────────────────────────────────────── */
const $ = (id) => document.getElementById(id);
const trackListEl = $("trackList");
const playBtn = $("playBtn"),
stopBtn = $("stopBtn"),
prevBtn = $("prevBtn"),
nextBtn = $("nextBtn");
const iconPlay = $("iconPlay"),
iconPause = $("iconPause");
const tempoSlider = $("tempoSlider"),
volumeSlider = $("volumeSlider");
const tempoValue = $("tempoValue"),
volumeValue = $("volumeValue");
const scaleValue = $("scaleValue"),
bpmValue = $("bpmValue"),
stateValue = $("stateValue");
const nowTitle = $("nowTitle"),
nowMeta = $("nowMeta");
const barsEl = $("bars"),
stepsEl = $("steps");
const editorToggle = $("editorToggle"),
editorPanel = $("editorPanel");
const presetEditor = $("presetEditor"),
applyBtn = $("applyBtn"),
editorStatus = $("editorStatus");
const barNodes = [],
stepNodes = [];
/* ── UI ─────────────────────────────────────────────── */
function buildTrackList() {
trackListEl.innerHTML = "";
PRESETS.forEach((p, i) => {
const el = document.createElement("div");
el.className = "track-item" + (i === currentTrack ? " active" : "");
el.dataset.index = i;
el.innerHTML = `<span class="track-num">${i + 1}</span><span class="track-name">${p.name}</span><span class="track-info">${p.root} ${p.mode} ${p.bpm}</span>`;
el.addEventListener("click", () => selectTrack(i, true));
trackListEl.appendChild(el);
});
}
function highlightTrack() {
trackListEl.querySelectorAll(".track-item").forEach((el, i) => {
el.classList.toggle("active", i === currentTrack);
el.classList.toggle("playing", i === currentTrack && isPlaying);
});
}
function updateNowPlaying() {
const p = currentPreset;
nowTitle.textContent = p.name;
nowMeta.textContent = `${p.root} ${p.mode} \u2022 ${p.bpm} BPM`;
syncEditor();
}
function syncEditor() {
presetEditor.value = JSON.stringify(currentPreset, null, 2);
editorStatus.textContent = "";
editorStatus.className = "editor-status";
}
function validatePreset(p) {
if (!p || typeof p !== "object") throw new Error("Must be an object");
if (!ROOTS[p.root]) throw new Error("Invalid root");
if (!SCALES[p.mode]) throw new Error("Mode must be major or minor");
["pad", "bass", "lead"].forEach((k) => {
if (!Array.isArray(p[k]) || p[k].length !== 8) throw new Error(`${k} must be 8-step array`);
});
if (!p.drums) throw new Error("Missing drums");
["kick", "snare", "hat"].forEach((k) => {
if (typeof p.drums[k] !== "string" || p.drums[k].length !== 8)
throw new Error(`drums.${k} must be 8 chars`);
});
}
function updateStats() {
tempoValue.textContent = Math.round(Number(tempoSlider.value));
volumeValue.textContent = Math.round(Number(volumeSlider.value) * 100) + "%";
bpmValue.textContent = Math.round(Number(tempoSlider.value));
scaleValue.textContent = `${currentPreset.root} ${currentPreset.mode}`;
stateValue.textContent = isPlaying ? "Playing" : "Ready";
}
function updatePlayIcon() {
iconPlay.classList.toggle("hidden", isPlaying);
iconPause.classList.toggle("hidden", !isPlaying);
}
function highlightStep(idx) {
stepNodes.forEach((n, i) => n.classList.toggle("active", i === idx));
}
/* ── Track selection ────────────────────────────────── */
function selectTrack(i, autoPlay) {
const wasPlaying = isPlaying;
if (wasPlaying) stopPlayback();
currentTrack = i;
currentPreset = structuredClone(PRESETS[i]);
tempoSlider.value = currentPreset.bpm;
volumeSlider.value = currentPreset.fx.master;
updateNowPlaying();
updateStats();
highlightTrack();
highlightStep(-1);
if (audioCtx) updateEffects();
if (autoPlay || wasPlaying) startPlayback();
}
function nextTrack() {
selectTrack((currentTrack + 1) % PRESETS.length, true);
}
function prevTrack() {
selectTrack((currentTrack - 1 + PRESETS.length) % PRESETS.length, true);
}
/* ── Audio engine ───────────────────────────────────── */
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
dryGain = audioCtx.createGain();
wetGain = audioCtx.createGain();
delayNode = audioCtx.createDelay(1.5);
feedbackGain = audioCtx.createGain();
analyser = audioCtx.createAnalyser();
compressor = audioCtx.createDynamicsCompressor();
analyser.fftSize = 128;
analyser.smoothingTimeConstant = 0.85;
dryGain.gain.value = 0.9;
wetGain.gain.value = 0.22;
delayNode.delayTime.value = 0.3;
feedbackGain.gain.value = 0.3;
masterGain.gain.value = Number(volumeSlider.value);
dryGain.connect(compressor);
delayNode.connect(feedbackGain);
feedbackGain.connect(delayNode);
wetGain.connect(compressor);
compressor.connect(masterGain);
masterGain.connect(analyser);
analyser.connect(audioCtx.destination);
updateEffects();
renderViz();
}
function updateEffects() {
if (!audioCtx) return;
const fx = currentPreset.fx,
t = audioCtx.currentTime;
delayNode.delayTime.setValueAtTime(Math.max(0.05, Math.min(0.8, fx.delay || 0.3)), t);
feedbackGain.gain.setValueAtTime(Math.max(0, Math.min(0.75, (fx.room || 0.3) * 0.62)), t);
wetGain.gain.setValueAtTime(Math.max(0, Math.min(0.45, (fx.room || 0.3) * 0.32)), t);
masterGain.gain.setValueAtTime(Number(volumeSlider.value), t);
}
function midiToFreq(m) {
return 440 * Math.pow(2, (m - 69) / 12);
}
function degreeToMidi(deg, oct = 0) {
const root = ROOTS[currentPreset.root],
sc = SCALES[currentPreset.mode];
const dir = deg < 0 ? -1 : 1,
abs = Math.abs(deg);
const o = Math.floor(abs / sc.length),
idx = abs % sc.length;
return root + dir * (sc[idx] + o * 12) + oct * 12;
}
function connectFX(src, amt = 0.3) {
const s = audioCtx.createGain();
s.gain.value = amt;
src.connect(dryGain);
src.connect(s);
s.connect(delayNode);
delayNode.connect(wetGain);
}
function env(g, t, a, d, sus, rel, pk = 1) {
g.gain.cancelScheduledValues(t);
g.gain.setValueAtTime(0.0001, t);
g.gain.linearRampToValueAtTime(pk, t + a);
g.gain.linearRampToValueAtTime(Math.max(0.0001, sus), t + a + d);
g.gain.setTargetAtTime(0.0001, t + a + d, Math.max(0.02, rel));
}
/* ── Instruments (timbre driven by preset.synth) ──── */
function playKick(t) {
const sy = getSynth();
const o = audioCtx.createOscillator(),
g = audioCtx.createGain(),
f = audioCtx.createBiquadFilter();
o.type = "sine";
o.frequency.setValueAtTime(sy.kickPitch, t);
o.frequency.exponentialRampToValueAtTime(40, t + 0.18);
f.type = "lowpass";
f.frequency.value = 180;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(1, t + 0.005);
g.gain.exponentialRampToValueAtTime(0.0001, t + sy.kickDecay);
o.connect(f);
f.connect(g);
connectFX(g, 0.04);
o.start(t);
o.stop(t + sy.kickDecay + 0.03);
}
function playSnare(t) {
const sy = getSynth();
const bs = audioCtx.sampleRate * sy.snareDecay,
buf = audioCtx.createBuffer(1, bs, audioCtx.sampleRate),
d = buf.getChannelData(0);
for (let i = 0; i < bs; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / bs);
const n = audioCtx.createBufferSource(),
nf = audioCtx.createBiquadFilter(),
ng = audioCtx.createGain();
const to = audioCtx.createOscillator(),
tg = audioCtx.createGain();
n.buffer = buf;
nf.type = "highpass";
nf.frequency.value = 1800;
ng.gain.setValueAtTime(0.0001, t);
ng.gain.exponentialRampToValueAtTime(0.55, t + 0.005);
ng.gain.exponentialRampToValueAtTime(0.0001, t + sy.snareDecay);
to.type = "triangle";
to.frequency.setValueAtTime(sy.snareTone, t);
tg.gain.setValueAtTime(0.0001, t);
tg.gain.exponentialRampToValueAtTime(0.25, t + 0.003);
tg.gain.exponentialRampToValueAtTime(0.0001, t + 0.12);
n.connect(nf);
nf.connect(ng);
connectFX(ng, 0.12);
to.connect(tg);
connectFX(tg, 0.05);
n.start(t);
n.stop(t + sy.snareDecay + 0.02);
to.start(t);
to.stop(t + 0.13);
}
function playHat(t) {
const sy = getSynth();
const bs = audioCtx.sampleRate * sy.hatDecay,
buf = audioCtx.createBuffer(1, bs, audioCtx.sampleRate),
d = buf.getChannelData(0);
for (let i = 0; i < bs; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / bs);
const src = audioCtx.createBufferSource(),
f = audioCtx.createBiquadFilter(),
g = audioCtx.createGain();
src.buffer = buf;
f.type = "highpass";
f.frequency.value = sy.hatFreq;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.18, t + 0.001);
g.gain.exponentialRampToValueAtTime(0.0001, t + sy.hatDecay * 0.8);
src.connect(f);
f.connect(g);
connectFX(g, 0.04);
src.start(t);
src.stop(t + sy.hatDecay);
}
function playPad(t, freq, dur, cut = 1200, gv = 0.14) {
const sy = getSynth();
const mix = audioCtx.createGain(),
f = audioCtx.createBiquadFilter(),
a = audioCtx.createGain();
f.type = "lowpass";
f.frequency.setValueAtTime(cut, t);
f.Q.value = sy.padQ;
for (let i = 0; i < sy.padVoices; i++) {
const det = sy.padVoices === 1 ? 0 : ((i / (sy.padVoices - 1)) * 2 - 1) * sy.padDetune;
const o = audioCtx.createOscillator();
o.type = sy.padType;
o.frequency.setValueAtTime(freq, t);
o.detune.value = det;
o.connect(mix);
o.start(t);
o.stop(t + dur + 0.3);
}
env(a, t, sy.padAttack, 0.18, gv, dur * sy.padRelease, gv);
mix.connect(f);
f.connect(a);
connectFX(a, 0.28);
}
function playBass(t, freq, dur, cut = 420, gv = 0.18) {
const sy = getSynth();
const o = audioCtx.createOscillator(),
f = audioCtx.createBiquadFilter(),
a = audioCtx.createGain();
o.type = sy.bassType;
o.frequency.setValueAtTime(freq, t);
f.type = "lowpass";
f.frequency.setValueAtTime(cut, t);
f.Q.value = sy.bassQ;
if (sy.bassSub) {
const sub = audioCtx.createOscillator();
sub.type = "sine";
sub.frequency.setValueAtTime(freq / 2, t);
sub.connect(f);
sub.start(t);
sub.stop(t + dur + 0.08);
}
env(a, t, 0.008, 0.1, gv, Math.max(0.06, dur * 0.3), gv);
o.connect(f);
f.connect(a);
connectFX(a, 0.08);
o.start(t);
o.stop(t + dur + 0.08);
}
function playLead(t, freq, dur, cut = 1400, gv = 0.08) {
const sy = getSynth();
const o = audioCtx.createOscillator(),
a = audioCtx.createGain(),
f = audioCtx.createBiquadFilter();
o.type = sy.leadType;
o.frequency.setValueAtTime(freq, t);
f.type = "lowpass";
f.frequency.setValueAtTime(cut, t);
f.Q.value = sy.leadQ;
env(a, t, 0.01, 0.08, gv, Math.max(0.08, dur * 0.3), gv);
o.connect(f);
f.connect(a);
connectFX(a, 0.22);
o.start(t);
o.stop(t + dur + 0.1);
}
/* ── Sequencer ──────────────────────────────────────── */
function stepDur() {
return (60 / Number(tempoSlider.value) / 2) * Number(currentPreset.fx.slow || 2);
}
function triggerStep(step, t) {
const sd = stepDur(),
sh = sd * 0.85,
lo = sd * 1.6;
if (currentPreset.drums.kick[step] === "x") playKick(t);
if (currentPreset.drums.snare[step] === "x") playSnare(t);
if (currentPreset.drums.hat[step] === "x") playHat(t);
const bd = currentPreset.bass[step];
if (bd != null)
playBass(
t,
midiToFreq(degreeToMidi(bd, -1)),
sh,
Math.max(240, currentPreset.fx.tone * 0.5),
0.18
);
const pd = currentPreset.pad[step];
if (pd != null) playPad(t, midiToFreq(degreeToMidi(pd, 0)), lo, currentPreset.fx.tone, 0.12);
const ld = currentPreset.lead[step];
if (ld != null)
playLead(
t + 0.01,
midiToFreq(degreeToMidi(ld, 0)),
sh * 0.9,
currentPreset.fx.tone * 1.25,
0.07
);
highlightStep(step);
}
function scheduler() {
while (nextNoteTime < audioCtx.currentTime + 0.12) {
const sw = (stepIndex % 2 === 1 ? currentPreset.swing || 0 : 0) * stepDur();
triggerStep(stepIndex, nextNoteTime + sw);
nextNoteTime += stepDur();
stepIndex = (stepIndex + 1) % 8;
}
}
/* ── Playback ───────────────────────────────────────── */
function startPlayback() {
initAudio();
if (audioCtx.state === "suspended") audioCtx.resume();
if (isPlaying) {
stopPlayback();
return;
}
updateEffects();
isPlaying = true;
nextNoteTime = audioCtx.currentTime + 0.05;
stepIndex = 0;
schedulerTimer = setInterval(scheduler, 25);
updateStats();
updatePlayIcon();
highlightTrack();
}
function stopPlayback() {
isPlaying = false;
if (schedulerTimer) {
clearInterval(schedulerTimer);
schedulerTimer = null;
}
highlightStep(-1);
updateStats();
updatePlayIcon();
highlightTrack();
}
/* ── Visualizer ─────────────────────────────────────── */
function renderViz() {
if (!analyser) {
requestAnimationFrame(renderViz);
return;
}
const d = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(d);
for (let i = 0; i < barNodes.length; i++) {
const v = d[i % d.length] / 255;
barNodes[i].style.height = `${8 + v * 120}px`;
barNodes[i].style.opacity = 0.35 + v * 0.8;
}
requestAnimationFrame(renderViz);
}
/* ── Init ───────────────────────────────────────────── */
function init() {
buildTrackList();
for (let i = 0; i < 32; i++) {
const b = document.createElement("div");
b.className = "bar";
barsEl.appendChild(b);
barNodes.push(b);
}
for (let i = 0; i < 8; i++) {
const s = document.createElement("div");
s.className = "step";
s.textContent = i + 1;
stepsEl.appendChild(s);
stepNodes.push(s);
}
selectTrack(0, false);
}
/* ── Events ─────────────────────────────────────────── */
playBtn.addEventListener("click", startPlayback);
stopBtn.addEventListener("click", stopPlayback);
prevBtn.addEventListener("click", prevTrack);
nextBtn.addEventListener("click", nextTrack);
tempoSlider.addEventListener("input", updateStats);
volumeSlider.addEventListener("input", () => {
volumeValue.textContent = Math.round(Number(volumeSlider.value) * 100) + "%";
if (masterGain && audioCtx)
masterGain.gain.setValueAtTime(Number(volumeSlider.value), audioCtx.currentTime);
});
editorToggle.addEventListener("click", () => {
editorPanel.classList.toggle("hidden");
editorToggle.textContent = editorPanel.classList.contains("hidden")
? "Edit Preset JSON"
: "Hide Editor";
});
applyBtn.addEventListener("click", () => {
try {
const p = JSON.parse(presetEditor.value);
validatePreset(p);
currentPreset = p;
if (audioCtx) updateEffects();
updateNowPlaying();
updateStats();
editorStatus.textContent = "Preset applied.";
editorStatus.className = "editor-status ok";
} catch (e) {
editorStatus.textContent = `Invalid: ${e.message}`;
editorStatus.className = "editor-status err";
}
});
window.addEventListener("beforeunload", () => {
stopPlayback();
if (audioCtx) audioCtx.close();
});
init();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vaporwave Synth — stealthisdesign</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="header">
<h1>Vaporwave Synth</h1>
<p>10 generative tracks — Web Audio API</p>
</header>
<div class="layout">
<aside class="tracklist" id="trackList"></aside>
<main class="player">
<div class="now-playing">
<span class="now-label">Now Playing</span>
<span class="now-title" id="nowTitle">--</span>
<span class="now-meta" id="nowMeta"></span>
</div>
<div class="transport">
<button id="prevBtn" class="t-btn" aria-label="Previous track">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
<button id="playBtn" class="t-btn t-play" aria-label="Play">
<svg id="iconPlay" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg id="iconPause" width="22" height="22" viewBox="0 0 24 24" fill="currentColor" class="hidden"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg>
</button>
<button id="nextBtn" class="t-btn" aria-label="Next track">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<button id="stopBtn" class="t-btn t-stop" aria-label="Stop">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
</div>
<div class="sliders">
<label class="slider-row">
<span>Tempo</span>
<input id="tempoSlider" type="range" min="60" max="120" step="1" value="78" />
<span class="slider-val" id="tempoValue">78</span>
</label>
<label class="slider-row">
<span>Volume</span>
<input id="volumeSlider" type="range" min="0" max="1" step="0.01" value="0.72" />
<span class="slider-val" id="volumeValue">72%</span>
</label>
</div>
<div class="stats-row">
<div class="stat"><span class="sk">Scale</span><span class="sv" id="scaleValue">C minor</span></div>
<div class="stat"><span class="sk">BPM</span><span class="sv" id="bpmValue">78</span></div>
<div class="stat"><span class="sk">State</span><span class="sv" id="stateValue">Ready</span></div>
</div>
<div class="visualizer"><div class="bars" id="bars"></div></div>
<div class="steps" id="steps"></div>
<div class="editor-section">
<button id="editorToggle" class="editor-toggle">Edit Preset JSON</button>
<div id="editorPanel" class="editor-panel hidden">
<textarea id="presetEditor"></textarea>
<button id="applyBtn" class="apply-btn">Apply Changes</button>
<div class="editor-status" id="editorStatus"></div>
</div>
</div>
</main>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Vaporwave Synth
A generative vaporwave synthesizer built entirely with the Web Audio API. Features 10 preset tracks spanning lo-fi, mallsoft, synthwave, and ambient styles. Each track defines an 8-step sequence for supersaw pads, sawtooth bass, triangle lead, and percussive drums — all synthesized in real-time with delay, filtering, and compression effects.