Skip to content

Instantly share code, notes, and snippets.

@o-az
Created March 19, 2026 20:00
Show Gist options
  • Select an option

  • Save o-az/042e99e5488d377100452872c0be54a0 to your computer and use it in GitHub Desktop.

Select an option

Save o-az/042e99e5488d377100452872c0be54a0 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trance Generator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a1a;
color: #e0e0ff;
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
}
canvas#bg {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
width: 100%;
max-width: 800px;
padding: 30px 20px;
}
h1 {
text-align: center;
font-size: 2.2rem;
background: linear-gradient(135deg, #00d4ff, #a855f7, #ff6ec7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
letter-spacing: 2px;
}
.subtitle {
text-align: center;
color: #7a7aaa;
font-size: 0.9rem;
margin-bottom: 30px;
}
.main-btn {
display: block;
margin: 0 auto 30px;
padding: 16px 60px;
font-size: 1.3rem;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
border: none;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s;
color: #fff;
}
.main-btn.play {
background: linear-gradient(135deg, #a855f7, #6d28d9);
box-shadow: 0 0 30px rgba(168,85,247,0.4);
}
.main-btn.play:hover {
box-shadow: 0 0 50px rgba(168,85,247,0.7);
transform: scale(1.05);
}
.main-btn.stop {
background: linear-gradient(135deg, #ef4444, #b91c1c);
box-shadow: 0 0 30px rgba(239,68,68,0.4);
}
.panel {
background: rgba(20, 20, 45, 0.85);
border: 1px solid rgba(168,85,247,0.2);
border-radius: 16px;
padding: 24px;
margin-bottom: 20px;
backdrop-filter: blur(10px);
}
.panel h2 {
font-size: 1rem;
color: #a855f7;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 2px;
}
.controls-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.control {
display: flex;
flex-direction: column;
}
.control label {
font-size: 0.78rem;
color: #8888bb;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 1px;
}
.control .value {
font-size: 0.75rem;
color: #a855f7;
text-align: right;
margin-bottom: 2px;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: #1a1a3a;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, #a855f7, #00d4ff);
cursor: pointer;
box-shadow: 0 0 8px rgba(168,85,247,0.5);
}
select {
background: #1a1a3a;
color: #e0e0ff;
border: 1px solid rgba(168,85,247,0.3);
border-radius: 8px;
padding: 8px 12px;
font-size: 0.9rem;
cursor: pointer;
outline: none;
}
.toggle-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 4px;
}
.toggle-btn {
padding: 8px 16px;
border: 1px solid rgba(168,85,247,0.3);
border-radius: 20px;
background: transparent;
color: #8888bb;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
}
.toggle-btn.active {
background: rgba(168,85,247,0.25);
border-color: #a855f7;
color: #e0e0ff;
box-shadow: 0 0 12px rgba(168,85,247,0.2);
}
.preset-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
.preset-btn {
padding: 8px 20px;
border: 1px solid rgba(0,212,255,0.3);
border-radius: 20px;
background: rgba(0,212,255,0.08);
color: #00d4ff;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
}
.preset-btn:hover {
background: rgba(0,212,255,0.2);
box-shadow: 0 0 15px rgba(0,212,255,0.2);
}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<div class="container">
<h1>TRANCE GENERATOR</h1>
<p class="subtitle">Procedural trance music synthesis</p>
<div class="preset-row">
<button class="preset-btn" onclick="loadPreset('classic')">Classic Trance</button>
<button class="preset-btn" onclick="loadPreset('uplifting')">Uplifting</button>
<button class="preset-btn" onclick="loadPreset('psy')">Psytrance</button>
<button class="preset-btn" onclick="loadPreset('deep')">Deep Trance</button>
<button class="preset-btn" onclick="loadPreset('acid')">Acid</button>
</div>
<button id="mainBtn" class="main-btn play" onclick="togglePlay()">PLAY</button>
<div class="panel">
<h2>Tempo & Key</h2>
<div class="controls-grid">
<div class="control">
<label>BPM</label>
<div class="value" id="bpmVal">138</div>
<input type="range" id="bpm" min="120" max="160" value="138" oninput="updateParam('bpm')">
</div>
<div class="control">
<label>Key</label>
<select id="key" onchange="updateParam('key')">
<option value="0">C minor</option>
<option value="1">C# minor</option>
<option value="2" selected>D minor</option>
<option value="3">Eb minor</option>
<option value="4">E minor</option>
<option value="5">F minor</option>
<option value="7">G minor</option>
<option value="9">A minor</option>
<option value="10">Bb minor</option>
</select>
</div>
</div>
</div>
<div class="panel">
<h2>Layers</h2>
<div class="toggle-row">
<button class="toggle-btn active" id="tog_kick" onclick="toggleLayer('kick')">Kick</button>
<button class="toggle-btn active" id="tog_bass" onclick="toggleLayer('bass')">Bass</button>
<button class="toggle-btn active" id="tog_arp" onclick="toggleLayer('arp')">Arpeggio</button>
<button class="toggle-btn active" id="tog_pad" onclick="toggleLayer('pad')">Pad</button>
<button class="toggle-btn active" id="tog_hihat" onclick="toggleLayer('hihat')">Hi-Hat</button>
<button class="toggle-btn" id="tog_lead" onclick="toggleLayer('lead')">Lead</button>
<button class="toggle-btn" id="tog_clap" onclick="toggleLayer('clap')">Clap</button>
</div>
</div>
<div class="panel">
<h2>Sound Design</h2>
<div class="controls-grid">
<div class="control">
<label>Arp Speed</label>
<div class="value" id="arpSpeedVal">1/16</div>
<input type="range" id="arpSpeed" min="0" max="2" value="1" step="1" oninput="updateParam('arpSpeed')">
</div>
<div class="control">
<label>Arp Cutoff</label>
<div class="value" id="arpCutoffVal">2200 Hz</div>
<input type="range" id="arpCutoff" min="400" max="6000" value="2200" oninput="updateParam('arpCutoff')">
</div>
<div class="control">
<label>Bass Intensity</label>
<div class="value" id="bassIntVal">70%</div>
<input type="range" id="bassInt" min="0" max="100" value="70" oninput="updateParam('bassInt')">
</div>
<div class="control">
<label>Pad Warmth</label>
<div class="value" id="padWarmthVal">60%</div>
<input type="range" id="padWarmth" min="0" max="100" value="60" oninput="updateParam('padWarmth')">
</div>
<div class="control">
<label>Lead Detune</label>
<div class="value" id="leadDetuneVal">15 ct</div>
<input type="range" id="leadDetune" min="0" max="50" value="15" oninput="updateParam('leadDetune')">
</div>
<div class="control">
<label>Reverb</label>
<div class="value" id="reverbVal">50%</div>
<input type="range" id="reverb" min="0" max="100" value="50" oninput="updateParam('reverb')">
</div>
<div class="control">
<label>Delay Feedback</label>
<div class="value" id="delayVal">35%</div>
<input type="range" id="delay" min="0" max="80" value="35" oninput="updateParam('delay')">
</div>
<div class="control">
<label>Master Volume</label>
<div class="value" id="masterVal">75%</div>
<input type="range" id="master" min="0" max="100" value="75" oninput="updateParam('master')">
</div>
</div>
</div>
</div>
<script>
// ---- Audio Engine ----
let ctx, masterGain, reverbNode, delayNode, delayFeedback, analyser;
let playing = false;
let schedulerTimer = null;
let nextNoteTime = 0;
let currentStep = 0;
const SCHEDULE_AHEAD = 0.1;
const LOOKAHEAD = 25;
const state = {
bpm: 138, key: 2,
layers: { kick: true, bass: true, arp: true, pad: true, hihat: true, lead: false, clap: false },
arpSpeed: 1, arpCutoff: 2200,
bassInt: 70, padWarmth: 60, leadDetune: 15,
reverbMix: 0.5, delayFb: 0.35, master: 0.75
};
// Minor scale intervals
const MINOR = [0, 2, 3, 5, 7, 8, 10];
function noteFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }
function scaleNote(root, degree) {
const oct = Math.floor(degree / 7);
const idx = ((degree % 7) + 7) % 7;
return root + oct * 12 + MINOR[idx];
}
// Chord progressions (in scale degrees) - classic trance: i - VI - III - VII
const PROGRESSIONS = [
[[0,2,4],[5,7,9],[2,4,6],[6,8,10]], // i VI III VII
[[0,2,4],[3,5,7],[4,6,8],[3,5,7]], // i iv v iv
[[0,2,4],[5,7,9],[3,5,7],[4,6,8]], // i VI iv v
];
let currentProg = 0;
function getChord(bar) {
const prog = PROGRESSIONS[currentProg % PROGRESSIONS.length];
return prog[bar % prog.length];
}
// Arp patterns
const ARP_PATTERNS = [
[0, 1, 2, 1, 0, 2, 1, 2],
[0, 2, 1, 2, 0, 1, 2, 0],
[2, 1, 0, 1, 2, 0, 1, 0],
];
let currentArpPattern = 0;
function createReverb() {
const conv = ctx.createConvolver();
const rate = ctx.sampleRate;
const len = rate * 2.5;
const buf = ctx.createBuffer(2, len, rate);
for (let ch = 0; ch < 2; ch++) {
const d = buf.getChannelData(ch);
for (let i = 0; i < len; i++) {
d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2.5);
}
}
conv.buffer = buf;
return conv;
}
function initAudio() {
ctx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = ctx.createGain();
masterGain.gain.value = state.master;
analyser = ctx.createAnalyser();
analyser.fftSize = 256;
reverbNode = createReverb();
const reverbGain = ctx.createGain();
reverbGain.gain.value = state.reverbMix;
reverbGain._id = 'reverbGain';
window._reverbGain = reverbGain;
const dryGain = ctx.createGain();
dryGain.gain.value = 1 - state.reverbMix * 0.5;
window._dryGain = dryGain;
delayNode = ctx.createDelay(1);
delayNode.delayTime.value = 60 / state.bpm * 0.75;
delayFeedback = ctx.createGain();
delayFeedback.gain.value = state.delayFb;
// Routing
masterGain.connect(dryGain);
masterGain.connect(reverbGain);
reverbGain.connect(reverbNode);
reverbNode.connect(analyser);
dryGain.connect(analyser);
masterGain.connect(delayNode);
delayNode.connect(delayFeedback);
delayFeedback.connect(delayNode);
delayNode.connect(analyser);
analyser.connect(ctx.destination);
}
// ---- Synth Voices ----
function playKick(time) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(150, time);
osc.frequency.exponentialRampToValueAtTime(30, time + 0.12);
gain.gain.setValueAtTime(0.9, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.4);
osc.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc.stop(time + 0.4);
// Click layer
const osc2 = ctx.createOscillator();
const g2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(1000, time);
osc2.frequency.exponentialRampToValueAtTime(60, time + 0.02);
g2.gain.setValueAtTime(0.6, time);
g2.gain.exponentialRampToValueAtTime(0.001, time + 0.04);
osc2.connect(g2);
g2.connect(masterGain);
osc2.start(time);
osc2.stop(time + 0.05);
}
function playHiHat(time, open) {
const bufSize = ctx.sampleRate * 0.05;
const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource();
src.buffer = buf;
const hp = ctx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 7000;
const gain = ctx.createGain();
const dur = open ? 0.15 : 0.05;
gain.gain.setValueAtTime(open ? 0.18 : 0.15, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + dur);
src.connect(hp);
hp.connect(gain);
gain.connect(masterGain);
src.start(time);
src.stop(time + dur + 0.01);
}
function playClap(time) {
const bufSize = ctx.sampleRate * 0.1;
const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufSize; i++) {
const env = Math.exp(-i / (ctx.sampleRate * 0.03));
data[i] = (Math.random() * 2 - 1) * env;
}
const src = ctx.createBufferSource();
src.buffer = buf;
const bp = ctx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = 1200;
bp.Q.value = 1.5;
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.35, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);
src.connect(bp);
bp.connect(gain);
gain.connect(masterGain);
src.start(time);
src.stop(time + 0.2);
}
function playBass(time, freq, dur) {
const osc = ctx.createOscillator();
const osc2 = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sawtooth';
osc2.type = 'square';
osc.frequency.setValueAtTime(freq, time);
osc2.frequency.setValueAtTime(freq * 1.002, time);
filter.type = 'lowpass';
filter.frequency.setValueAtTime(200 + state.bassInt * 15, time);
filter.frequency.exponentialRampToValueAtTime(80, time + dur * 0.8);
filter.Q.value = 5;
const vol = state.bassInt / 100 * 0.4;
gain.gain.setValueAtTime(vol, time);
gain.gain.setValueAtTime(vol, time + dur * 0.7);
gain.gain.exponentialRampToValueAtTime(0.001, time + dur);
osc.connect(filter);
osc2.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc2.start(time);
osc.stop(time + dur + 0.01);
osc2.stop(time + dur + 0.01);
}
let padOscs = [];
function updatePad(time, chord, root) {
stopPad(time);
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 600 + state.padWarmth * 30;
filter.Q.value = 1;
gain.gain.setValueAtTime(0, time);
gain.gain.linearRampToValueAtTime(0.12, time + 0.8);
const oscs = [];
chord.forEach(deg => {
const midi = scaleNote(root + 48, deg);
['sine', 'triangle'].forEach((type, i) => {
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = noteFreq(midi) * (i === 1 ? 1.003 : 1);
osc.connect(filter);
osc.start(time);
oscs.push(osc);
});
});
filter.connect(gain);
gain.connect(masterGain);
padOscs = { oscs, gain, filter };
}
function stopPad(time) {
if (padOscs.oscs) {
padOscs.gain.gain.setValueAtTime(padOscs.gain.gain.value, time);
padOscs.gain.gain.linearRampToValueAtTime(0, time + 0.3);
const o = padOscs.oscs;
setTimeout(() => o.forEach(x => { try { x.stop(); } catch(e){} }), 500);
padOscs = [];
}
}
function playArp(time, chord, root) {
const pattern = ARP_PATTERNS[currentArpPattern % ARP_PATTERNS.length];
const stepInPattern = currentStep % pattern.length;
const deg = chord[pattern[stepInPattern] % chord.length];
const midi = scaleNote(root + 60, deg);
const freq = noteFreq(midi);
const osc = ctx.createOscillator();
const osc2 = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sawtooth';
osc2.type = 'square';
osc.frequency.value = freq;
osc2.frequency.value = freq * 0.999;
filter.type = 'lowpass';
const stepDur = getStepDuration();
filter.frequency.setValueAtTime(state.arpCutoff, time);
filter.frequency.exponentialRampToValueAtTime(Math.max(state.arpCutoff * 0.3, 100), time + stepDur * 0.8);
filter.Q.value = 6;
gain.gain.setValueAtTime(0.22, time);
gain.gain.setValueAtTime(0.22, time + stepDur * 0.6);
gain.gain.exponentialRampToValueAtTime(0.001, time + stepDur * 0.95);
osc.connect(filter);
osc2.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc2.start(time);
osc.stop(time + stepDur);
osc2.stop(time + stepDur);
}
function playLead(time, chord, root) {
if (currentStep % 4 !== 0) return;
const deg = chord[0] + 7;
const midi = scaleNote(root + 60, deg);
const freq = noteFreq(midi);
const dur = getStepDuration() * 3;
const osc = ctx.createOscillator();
const osc2 = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sawtooth';
osc2.type = 'sawtooth';
osc.frequency.value = freq;
osc2.frequency.value = freq + state.leadDetune * 0.1;
filter.type = 'lowpass';
filter.frequency.setValueAtTime(3000, time);
filter.frequency.exponentialRampToValueAtTime(800, time + dur);
filter.Q.value = 3;
gain.gain.setValueAtTime(0, time);
gain.gain.linearRampToValueAtTime(0.15, time + 0.05);
gain.gain.setValueAtTime(0.15, time + dur * 0.6);
gain.gain.exponentialRampToValueAtTime(0.001, time + dur);
osc.connect(filter);
osc2.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
osc.start(time);
osc2.start(time);
osc.stop(time + dur + 0.01);
osc2.stop(time + dur + 0.01);
}
function getStepDuration() {
const sixteenth = 60 / state.bpm / 4;
if (state.arpSpeed === 0) return sixteenth * 2; // 1/8
if (state.arpSpeed === 1) return sixteenth; // 1/16
return sixteenth / 2; // 1/32
}
function scheduleStep(time) {
const stepsPerBeat = state.arpSpeed === 0 ? 2 : state.arpSpeed === 1 ? 4 : 8;
const stepsPerBar = stepsPerBeat * 4;
const bar = Math.floor(currentStep / stepsPerBar);
const beatStep = currentStep % stepsPerBeat;
const barStep = currentStep % stepsPerBar;
const chord = getChord(bar);
const root = state.key;
// Kick: every beat
if (state.layers.kick && barStep % stepsPerBeat === 0) {
playKick(time);
}
// Hi-hat: offbeat 16ths
if (state.layers.hihat && stepsPerBeat >= 4) {
if (barStep % (stepsPerBeat / 2) === Math.floor(stepsPerBeat / 4)) {
playHiHat(time, false);
} else if (barStep % stepsPerBeat === Math.floor(stepsPerBeat / 2)) {
playHiHat(time, true);
}
} else if (state.layers.hihat) {
if (beatStep === 1) playHiHat(time, barStep % (stepsPerBeat * 2) === stepsPerBeat + 1);
}
// Clap: beats 2 and 4
if (state.layers.clap && barStep % stepsPerBeat === 0) {
const beat = Math.floor(barStep / stepsPerBeat);
if (beat === 1 || beat === 3) playClap(time);
}
// Bass: every beat
if (state.layers.bass && barStep % stepsPerBeat === 0) {
const bassMidi = scaleNote(root + 36, chord[0]);
playBass(time, noteFreq(bassMidi), 60 / state.bpm * 0.8);
}
// Pad: update on bar change
if (state.layers.pad && barStep === 0) {
updatePad(time, chord, root);
}
// Arp
if (state.layers.arp) {
playArp(time, chord, root);
}
// Lead
if (state.layers.lead) {
playLead(time, chord, root);
}
// Change progression every 8 bars
if (currentStep > 0 && currentStep % (stepsPerBar * 8) === 0) {
currentProg++;
currentArpPattern++;
}
}
function scheduler() {
while (nextNoteTime < ctx.currentTime + SCHEDULE_AHEAD) {
scheduleStep(nextNoteTime);
currentStep++;
nextNoteTime += getStepDuration();
}
}
function togglePlay() {
const btn = document.getElementById('mainBtn');
if (!playing) {
if (!ctx) initAudio();
if (ctx.state === 'suspended') ctx.resume();
playing = true;
currentStep = 0;
nextNoteTime = ctx.currentTime;
schedulerTimer = setInterval(scheduler, LOOKAHEAD);
btn.textContent = 'STOP';
btn.className = 'main-btn stop';
startVisualizer();
} else {
playing = false;
clearInterval(schedulerTimer);
stopPad(ctx.currentTime);
btn.textContent = 'PLAY';
btn.className = 'main-btn play';
}
}
function toggleLayer(name) {
state.layers[name] = !state.layers[name];
const btn = document.getElementById('tog_' + name);
btn.classList.toggle('active');
if (name === 'pad' && !state.layers.pad) stopPad(ctx ? ctx.currentTime : 0);
}
function updateParam(id) {
const el = document.getElementById(id);
const v = parseFloat(el.value);
switch(id) {
case 'bpm':
state.bpm = v;
document.getElementById('bpmVal').textContent = v;
if (delayNode) delayNode.delayTime.value = 60 / v * 0.75;
break;
case 'key':
state.key = parseInt(v);
break;
case 'arpSpeed':
state.arpSpeed = v;
document.getElementById('arpSpeedVal').textContent = v === 0 ? '1/8' : v === 1 ? '1/16' : '1/32';
break;
case 'arpCutoff':
state.arpCutoff = v;
document.getElementById('arpCutoffVal').textContent = Math.round(v) + ' Hz';
break;
case 'bassInt':
state.bassInt = v;
document.getElementById('bassIntVal').textContent = v + '%';
break;
case 'padWarmth':
state.padWarmth = v;
document.getElementById('padWarmthVal').textContent = v + '%';
if (padOscs.filter) padOscs.filter.frequency.value = 600 + v * 30;
break;
case 'leadDetune':
state.leadDetune = v;
document.getElementById('leadDetuneVal').textContent = v + ' ct';
break;
case 'reverb':
state.reverbMix = v / 100;
document.getElementById('reverbVal').textContent = v + '%';
if (window._reverbGain) window._reverbGain.gain.value = state.reverbMix;
if (window._dryGain) window._dryGain.gain.value = 1 - state.reverbMix * 0.5;
break;
case 'delay':
state.delayFb = v / 100;
document.getElementById('delayVal').textContent = v + '%';
if (delayFeedback) delayFeedback.gain.value = state.delayFb;
break;
case 'master':
state.master = v / 100;
document.getElementById('masterVal').textContent = v + '%';
if (masterGain) masterGain.gain.value = state.master;
break;
}
}
function loadPreset(name) {
const presets = {
classic: { bpm: 138, key: 2, arpSpeed: 1, arpCutoff: 2200, bassInt: 70, padWarmth: 60, leadDetune: 15, reverb: 50, delay: 35, master: 75,
layers: { kick:true, bass:true, arp:true, pad:true, hihat:true, lead:false, clap:false }},
uplifting: { bpm: 140, key: 4, arpSpeed: 1, arpCutoff: 3500, bassInt: 60, padWarmth: 80, leadDetune: 20, reverb: 65, delay: 45, master: 75,
layers: { kick:true, bass:true, arp:true, pad:true, hihat:true, lead:true, clap:true }},
psy: { bpm: 148, key: 0, arpSpeed: 2, arpCutoff: 4500, bassInt: 90, padWarmth: 30, leadDetune: 5, reverb: 30, delay: 25, master: 70,
layers: { kick:true, bass:true, arp:true, pad:false, hihat:true, lead:false, clap:false }},
deep: { bpm: 128, key: 9, arpSpeed: 0, arpCutoff: 1200, bassInt: 50, padWarmth: 90, leadDetune: 25, reverb: 70, delay: 50, master: 70,
layers: { kick:true, bass:true, arp:true, pad:true, hihat:false, lead:false, clap:false }},
acid: { bpm: 142, key: 5, arpSpeed: 1, arpCutoff: 5000, bassInt: 85, padWarmth: 40, leadDetune: 10, reverb: 35, delay: 55, master: 75,
layers: { kick:true, bass:true, arp:true, pad:false, hihat:true, lead:true, clap:true }},
};
const p = presets[name];
if (!p) return;
// Set slider values
const setSlider = (id, val) => { document.getElementById(id).value = val; updateParam(id); };
setSlider('bpm', p.bpm);
document.getElementById('key').value = p.key; updateParam('key');
setSlider('arpSpeed', p.arpSpeed);
setSlider('arpCutoff', p.arpCutoff);
setSlider('bassInt', p.bassInt);
setSlider('padWarmth', p.padWarmth);
setSlider('leadDetune', p.leadDetune);
setSlider('reverb', p.reverb);
setSlider('delay', p.delay);
setSlider('master', p.master);
Object.keys(p.layers).forEach(l => {
if (state.layers[l] !== p.layers[l]) toggleLayer(l);
});
}
// ---- Visualizer ----
const canvas = document.getElementById('bg');
const canvasCtx = canvas.getContext('2d');
let animFrame;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function startVisualizer() {
if (animFrame) return;
drawVisualizer();
}
function drawVisualizer() {
animFrame = requestAnimationFrame(drawVisualizer);
const W = canvas.width, H = canvas.height;
canvasCtx.fillStyle = 'rgba(10, 10, 26, 0.15)';
canvasCtx.fillRect(0, 0, W, H);
if (!analyser || !playing) {
canvasCtx.fillStyle = 'rgba(10, 10, 26, 1)';
canvasCtx.fillRect(0, 0, W, H);
if (!playing) { animFrame = null; return; }
}
const bufLen = analyser.frequencyBinCount;
const data = new Uint8Array(bufLen);
analyser.getByteFrequencyData(data);
// Bars
const barW = W / bufLen * 2.5;
for (let i = 0; i < bufLen; i++) {
const v = data[i] / 255;
const h = v * H * 0.6;
const hue = 260 + v * 60;
canvasCtx.fillStyle = `hsla(${hue}, 80%, ${40 + v * 30}%, ${0.4 + v * 0.4})`;
canvasCtx.fillRect(i * barW, H - h, barW - 1, h);
}
// Waveform
const timeData = new Uint8Array(bufLen);
analyser.getByteTimeDomainData(timeData);
canvasCtx.strokeStyle = 'rgba(168, 85, 247, 0.5)';
canvasCtx.lineWidth = 2;
canvasCtx.beginPath();
const sliceW = W / bufLen;
for (let i = 0; i < bufLen; i++) {
const y = (timeData[i] / 128) * H / 2;
if (i === 0) canvasCtx.moveTo(0, y);
else canvasCtx.lineTo(i * sliceW, y);
}
canvasCtx.stroke();
}
// Start with static background
canvasCtx.fillStyle = '#0a0a1a';
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment