/* global React */
const { useState, useRef, useEffect } = React;
// ═══════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════
const KE_NOTE_NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
const ke_midiToHz = m => 440 * Math.pow(2, (m - 69) / 12);
const ke_midiToName = m => KE_NOTE_NAMES[m % 12] + (Math.floor(m / 12) - 1);
const KE_BASS_MIDI = [28,31,33,35,36,38,40,41,43,45,47,48,50,52];
const KE_TRACKS = ['kick','clap','hat','ohat','bass'];
// Brand-mapped palette
const KE = {
kick: '#E5179C', // pink
clap: '#FF2DAA', // pink-hot
hat: '#F4ECDD', // paper
ohat: '#C8FF00', // acid
bass: '#D9CFBE', // paper-dim
pad: '#B8137E', // pink-deep
bg: '#1A1614', // ink-soft (slightly lifted vs page ink)
surface: '#221C1A',
border: 'rgba(244,236,221,0.12)',
borderS: 'rgba(244,236,221,0.06)',
text: '#F4ECDD',
muted: '#9CA3AE',
dim: '#5C636E',
label: '#F4ECDD',
unit: '#9CA3AE',
};
const KE_TRACK_INFO = {
kick: { label:'KICK', color: KE.kick },
clap: { label:'CLAP', color: KE.clap },
hat: { label:'H.HAT', color: KE.hat },
ohat: { label:'O.HAT', color: KE.ohat },
bass: { label:'BASS', color: KE.bass },
};
const KE_FONT_LABEL = "var(--font-mono)";
const KE_FONT_TITLE = "var(--font-display)";
// ═══════════════════════════════════════════════════════
// SYNTHESIS
// ═══════════════════════════════════════════════════════
function ke_mkReverb(ctx) {
const conv = ctx.createConvolver();
const len = ctx.sampleRate * 2.5;
const buf = ctx.createBuffer(2, len, ctx.sampleRate);
for (let c = 0; c < 2; c++) {
const d = buf.getChannelData(c);
for (let i = 0; i < len; i++)
d[i] = (Math.random()*2-1) * Math.pow(1-i/len, 2.2);
}
conv.buffer = buf;
return conv;
}
function ke_synthKick(ctx, t, master, p) {
const vol = (p.trackVol.kick/100) * (p.masterVol/100);
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(p.kickFreqStart, t);
osc.frequency.exponentialRampToValueAtTime(Math.max(p.kickFreqStart*0.17,20), t+p.kickDecay*0.55);
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(1.5*vol, t+0.003);
g.gain.exponentialRampToValueAtTime(0.001, t+p.kickDecay);
osc.connect(g); g.connect(master);
osc.start(t); osc.stop(t+p.kickDecay+0.1);
}
function ke_synthClap(ctx, t, reverb, master, p, clapBufs) {
const vol = (p.trackVol.clap/100) * (p.masterVol/100);
for (let i = 0; i < 3; i++) {
const src = ctx.createBufferSource(); src.buffer = clapBufs[i];
const bpf = ctx.createBiquadFilter();
bpf.type='bandpass'; bpf.frequency.value=1200+i*350; bpf.Q.value=0.7;
const g = ctx.createGain();
const delay = i*0.013;
g.gain.setValueAtTime((0.38-i*0.08)*vol, t+delay);
g.gain.exponentialRampToValueAtTime(0.001, t+delay+0.2);
src.connect(bpf); bpf.connect(g);
g.connect(reverb); g.connect(master);
src.start(t+delay);
}
}
function ke_synthHat(ctx, t, master, p, open, noiseBuf) {
const key = open ? 'ohat' : 'hat';
const vol = (p.trackVol[key]/100) * (p.masterVol/100);
const dur = open ? 0.22 : 0.04;
const src = ctx.createBufferSource(); src.buffer = noiseBuf;
const hpf = ctx.createBiquadFilter();
hpf.type='highpass'; hpf.frequency.value=open?6500:7200;
const g = ctx.createGain();
g.gain.setValueAtTime(vol*(open?0.22:0.28), t);
g.gain.exponentialRampToValueAtTime(0.001, t+dur);
src.connect(hpf); hpf.connect(g); g.connect(master);
src.start(t);
}
function ke_synthBass(ctx, t, step, master, p) {
const vol = (p.trackVol.bass/100) * (p.masterVol/100);
const midi = KE_BASS_MIDI[p.bassNotes[step] ?? 4];
const hz = ke_midiToHz(midi);
const osc1 = ctx.createOscillator();
const osc2 = ctx.createOscillator();
const filt = ctx.createBiquadFilter();
const g = ctx.createGain();
const m1 = ctx.createGain(); m1.gain.value = 0.7;
const m2 = ctx.createGain(); m2.gain.value = 0.3;
osc1.type='sawtooth'; osc1.frequency.value=hz;
osc2.type='square'; osc2.frequency.value=hz; osc2.detune.value=5;
const cutoff = Math.max(p.bassFilterFreq, 80);
const envTop = Math.min(cutoff+p.bassEnvAmt, 18000);
filt.type='lowpass';
filt.frequency.setValueAtTime(cutoff*0.3, t);
filt.frequency.exponentialRampToValueAtTime(envTop, t+0.04);
filt.frequency.exponentialRampToValueAtTime(Math.max(cutoff*0.25,60), t+0.28);
filt.Q.value = p.bassResonance;
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.65*vol, t+0.008);
g.gain.exponentialRampToValueAtTime(0.001, t+0.32);
osc1.connect(m1); osc2.connect(m2);
m1.connect(filt); m2.connect(filt);
filt.connect(g); g.connect(master);
osc1.start(t); osc1.stop(t+0.38);
osc2.start(t); osc2.stop(t+0.38);
}
function ke_synthPad(ctx, t, reverb, p) {
if (!p.padEnabled) return null;
const vol = p.padVol / 100;
return [36,43,48,55].map((note, i) => {
const osc = ctx.createOscillator();
const filt = ctx.createBiquadFilter();
const g = ctx.createGain();
osc.type = i%2===0?'sawtooth':'triangle';
osc.frequency.value = ke_midiToHz(note);
osc.detune.value = (Math.random()-0.5)*10;
filt.type='lowpass'; filt.frequency.value=500+i*60;
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(vol*(0.06-i*0.01), t+3.5);
osc.connect(filt); filt.connect(g); g.connect(reverb);
osc.start(t);
return {osc, g};
});
}
// ═══════════════════════════════════════════════════════
// UI PRIMITIVES
// ═══════════════════════════════════════════════════════
function KeKnob({ label, value, min, max, inc=1, unit='', onChange, color }) {
const display = inc >= 1
? Math.round(value)
: inc < 0.05 ? parseFloat(value).toFixed(2)
: parseFloat(value).toFixed(1);
return (
{label}
onChange(parseFloat(e.target.value))}
style={{ width:'72px', cursor:'pointer', accentColor: color }} />
{display}{unit}
);
}
function KePill({ active, accent, onClick, children }) {
return (
);
}
function KeSectionLabel({ children }) {
return (
{children}
);
}
// ═══════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════
function KevinEngine() {
const [bpm, setBpm] = useState(124);
const [swing, setSwing] = useState(6);
const [masterVol, setMasterVol] = useState(78);
const [reverbWet, setReverbWet] = useState(26);
const [duration, setDuration] = useState(45);
const [steps, setSteps] = useState({
kick: [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
clap: [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
hat: [0,1,0,1, 0,1,0,1, 0,1,0,1, 0,1,0,1],
ohat: [0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1],
bass: [1,0,0,1, 0,0,0,0, 1,0,0,0, 0,1,0,0],
});
const [trackVol, setTrackVol] = useState({ kick:90, clap:72, hat:54, ohat:58, bass:82 });
const [muted, setMuted] = useState({ kick:false, clap:false, hat:false, ohat:false, bass:false });
const [soloed, setSoloed] = useState({ kick:false, clap:false, hat:false, ohat:false, bass:false });
const [bassNotes, setBassNotes] = useState([4,4,4,4, 4,4,3,4, 4,4,4,3, 4,4,6,4]);
const [kickFreqStart, setKickFreqStart] = useState(160);
const [kickDecay, setKickDecay] = useState(0.46);
const [bassFilterFreq, setBassFilterFreq] = useState(820);
const [bassResonance, setBassResonance] = useState(6);
const [bassEnvAmt, setBassEnvAmt] = useState(1100);
const [padVol, setPadVol] = useState(6);
const [padEnabled, setPadEnabled] = useState(true);
const [playing, setPlaying] = useState(false);
const [currentStep, setCurrentStep] = useState(-1);
const [elapsed, setElapsed] = useState(0);
// Mobile: show 8 of 16 steps at a time, toggle Bar A/B
const [isNarrow, setIsNarrow] = useState(() =>
typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches);
const [barPage, setBarPage] = useState(0); // 0 = steps 1–8, 1 = steps 9–16
const ctxRef = useRef(null);
const masterGRef = useRef(null);
const revGRef = useRef(null);
const timerRef = useRef(null);
const animRef = useRef(null);
const nextNoteRef = useRef(0);
const stepRef = useRef(0);
const startRef = useRef(0);
const padRef = useRef(null);
const cleanupRef = useRef(null);
const Eref = useRef({});
const displayStepRef = useRef(-1);
const prevDisplayRef = useRef(-1);
const stepPadRefs = useRef({});
useEffect(() => {
Eref.current = {
bpm, swing, masterVol, reverbWet, duration,
steps, trackVol, muted, solo: soloed, bassNotes,
kickFreqStart, kickDecay,
bassFilterFreq, bassResonance, bassEnvAmt,
padVol, padEnabled,
};
});
useEffect(() => {
if (!masterGRef.current || !ctxRef.current) return;
masterGRef.current.gain.linearRampToValueAtTime(masterVol/100, ctxRef.current.currentTime+0.05);
}, [masterVol]);
useEffect(() => {
if (!revGRef.current || !ctxRef.current) return;
revGRef.current.gain.linearRampToValueAtTime(reverbWet/100, ctxRef.current.currentTime+0.05);
}, [reverbWet]);
useEffect(() => {
const mq = window.matchMedia('(max-width: 640px)');
const h = e => setIsNarrow(e.matches);
mq.addEventListener('change', h);
return () => mq.removeEventListener('change', h);
}, []);
const toggleStep = (t, i) => setSteps(p => ({ ...p, [t]: p[t].map((v,j) => j===i ? 1-v : v) }));
const cycleBassNote = (i, d) => setBassNotes(p => { const n=[...p]; n[i]=(n[i]+d+KE_BASS_MIDI.length)%KE_BASS_MIDI.length; return n; });
const clearTrack = t => setSteps(p => ({ ...p, [t]: Array(16).fill(0) }));
const fillTrack = (t, pat) => setSteps(p => ({ ...p, [t]: pat }));
const handlePlay = () => {
if (ctxRef.current) ctxRef.current.close();
const ctx = new (window.AudioContext || window.webkitAudioContext)();
ctxRef.current = ctx;
const master = ctx.createGain();
master.gain.value = Eref.current.masterVol / 100;
masterGRef.current = master;
const reverb = ke_mkReverb(ctx);
const revGain = ctx.createGain();
revGain.gain.value = Eref.current.reverbWet / 100;
revGRef.current = revGain;
reverb.connect(revGain); revGain.connect(master); master.connect(ctx.destination);
// Pre-build noise buffers once — reused every step to avoid GC pressure
const mkNoise = secs => {
const len = Math.floor(ctx.sampleRate * secs);
const b = ctx.createBuffer(1, len, ctx.sampleRate);
const d = b.getChannelData(0);
for (let j = 0; j < len; j++) d[j] = Math.random()*2-1;
return b;
};
const prebuilt = {
clap: [mkNoise(0.12), mkNoise(0.12), mkNoise(0.12)],
hat: mkNoise(0.08),
ohat: mkNoise(0.3),
};
padRef.current = ke_synthPad(ctx, ctx.currentTime, reverb, Eref.current);
stepRef.current = 0; startRef.current = ctx.currentTime;
nextNoteRef.current = ctx.currentTime + 0.08;
setPlaying(true); setElapsed(0);
const cleanup = () => {
if (timerRef.current) clearTimeout(timerRef.current);
if (animRef.current) cancelAnimationFrame(animRef.current);
if (padRef.current) {
padRef.current.forEach(({osc, g}) => {
try { g.gain.linearRampToValueAtTime(0, ctx.currentTime+0.5); osc.stop(ctx.currentTime+0.6); } catch(_){}
});
padRef.current = null;
}
KE_TRACKS.forEach(t => {
for (let i = 0; i < 16; i++) {
const el = stepPadRefs.current[`${t}-${i}`];
if (el) el.removeAttribute('data-cur');
}
});
displayStepRef.current = -1;
prevDisplayRef.current = -1;
setPlaying(false); setCurrentStep(-1); setElapsed(0);
};
cleanupRef.current = cleanup;
let lastReactTime = 0;
const tick = () => {
const now = ctx.currentTime;
const e = Math.min(now - startRef.current, Eref.current.duration);
const cs = displayStepRef.current;
// Direct DOM step highlight — no React re-render, runs at full 60fps
if (cs !== prevDisplayRef.current) {
KE_TRACKS.forEach(t => {
const prev = stepPadRefs.current[`${t}-${prevDisplayRef.current}`];
if (prev) prev.removeAttribute('data-cur');
const next = stepPadRefs.current[`${t}-${cs}`];
if (next) next.setAttribute('data-cur', '1');
});
prevDisplayRef.current = cs;
}
// React state throttled to ~10fps — only for elapsed + transport text
if (now - lastReactTime >= 0.1) {
lastReactTime = now;
setElapsed(e);
setCurrentStep(cs);
}
if (e < Eref.current.duration) animRef.current = requestAnimationFrame(tick);
};
animRef.current = requestAnimationFrame(tick);
const doSchedule = () => {
if (!ctx || ctx.state==='closed') return;
const p = Eref.current;
const stepDur = (60/p.bpm)/4;
const swingAmt = (p.swing/100)*stepDur;
const soloAny = Object.values(p.solo).some(Boolean);
const now = ctx.currentTime;
while (nextNoteRef.current < now + 0.2) {
const t = nextNoteRef.current;
if (t - startRef.current >= p.duration) { cleanup(); return; }
const s = stepRef.current;
const st = t + (s%2===1 ? swingAmt : 0);
const ok = k => !p.muted[k] && !(soloAny && !p.solo[k]);
if (p.steps.kick[s] && ok('kick')) ke_synthKick(ctx, st, master, p);
if (p.steps.clap[s] && ok('clap')) ke_synthClap(ctx, st, reverb, master, p, prebuilt.clap);
if (p.steps.hat[s] && ok('hat')) ke_synthHat(ctx, st, master, p, false, prebuilt.hat);
if (p.steps.ohat[s] && ok('ohat')) ke_synthHat(ctx, st, master, p, true, prebuilt.ohat);
if (p.steps.bass[s] && ok('bass')) ke_synthBass(ctx, st, s, master, p);
displayStepRef.current = s;
stepRef.current = (s+1) % 16;
nextNoteRef.current += stepDur;
}
timerRef.current = setTimeout(doSchedule, 40);
};
doSchedule();
};
const handleStop = () => { if (cleanupRef.current) cleanupRef.current(); };
useEffect(() => () => { if (cleanupRef.current) cleanupRef.current(); }, []);
const progress = Math.min(elapsed / duration, 1);
const soloAny = Object.values(soloed).some(Boolean);
const PRESETS = {
kick: {
'Four on floor': [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
'Synco': [1,0,0,0, 0,0,1,0, 1,0,0,0, 0,1,0,0],
'Half time': [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0],
},
clap: {
'2 & 4': [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
'Ghost roll': [0,0,1,0, 0,0,1,0, 1,0,0,0, 0,0,1,0],
'Off beat': [0,0,1,0, 0,0,0,0, 0,0,1,0, 0,0,1,0],
},
hat: {
'16ths': [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1],
'8ths swing': [0,1,0,1, 0,1,0,1, 0,1,0,1, 0,1,0,1],
'Forest gv': [1,0,1,0, 1,1,0,1, 1,0,1,0, 0,1,1,0],
},
ohat: {
'Off beat': [0,0,0,0, 0,0,0,1, 0,0,0,0, 0,0,0,1],
'Every 4': [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
'Sparse': [0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0],
},
bass: {
'Groove': [1,0,0,1, 0,0,0,0, 1,0,0,0, 0,1,0,0],
'Minimal': [1,0,0,0, 0,0,1,0, 0,0,0,0, 1,0,0,0],
'Driving': [1,0,1,0, 0,0,1,0, 1,0,0,0, 0,0,1,0],
},
};
const stepStart = isNarrow ? barPage * 8 : 0;
const stepEnd = isNarrow ? barPage * 8 + 8 : 16;
// ─── RENDER ───
return (
{/* HEADER */}
Groove Sequencer · Try the sound
Kevin Engine
16 Steps · Live Edit
{/* GLOBAL */}
{/* SEQUENCER */}
Sequencer
{/* Bar A/B toggle — mobile only */}
{isNarrow && (
Bars
{[0,1].map(p => {
const active = barPage === p;
const isPlayingBar = playing && Math.floor(currentStep/8) === p;
return (
);
})}
16 STEPS
)}
{/* Beat numbers */}
{Array.from({length:16}).map((_,i) => {
if (i < stepStart || i >= stepEnd) return null;
const vis = i - stepStart;
return (
0&&vis%4===0 ? '6px' : '0',
}}>{String(i+1).padStart(2,'0')}
);
})}
{!isNarrow &&
}
{KE_TRACKS.map(track => {
const info = KE_TRACK_INFO[track];
const isMuted = muted[track];
const isSolo = soloed[track];
const dimmed = isMuted || (soloAny && !isSolo);
const presets = PRESETS[track];
return (
{/* Track label */}
{info.label}
setMuted(p=>({...p,[track]:!p[track]}))}>M
setSoloed(p=>({...p,[track]:!p[track]}))}>S
{/* Step pads */}
{steps[track].map((on, i) => {
if (i < stepStart || i >= stepEnd) return null;
const vis = i - stepStart;
const col = info.color;
return (
{ stepPadRefs.current[`${track}-${i}`] = el; }}
onClick={() => toggleStep(track, i)}
style={{
flex:1, height: isNarrow ? '34px' : '28px', borderRadius:'4px', cursor:'pointer',
marginLeft: vis>0&&vis%4===0 ? '6px' : '0',
background: on ? `${col}55` : KE.surface,
border:`1px solid ${on ? `${col}80` : KE.border}`,
boxShadow: on ? `0 0 4px ${col}33` : 'none',
opacity: dimmed ? 0.2 : 1,
transition:'background 0.04s, box-shadow 0.04s, opacity 0.12s',
willChange: 'background, box-shadow',
'--cc': col,
'--cc-glow': `${col}99`,
}}
/>
);
})}
{/* Right: vol + preset — wraps below pads on narrow */}
{/* Bass note row */}
{track==='bass' && (
{steps.bass.map((on, i) => {
if (i < stepStart || i >= stepEnd) return null;
const vis = i - stepStart;
return (
0&&vis%4===0 ? '6px' : '0',
minWidth:0,
}}>
{on ? (
cycleBassNote(i,1)} style={{
fontSize:'9px', color: KE.bass, cursor:'pointer',
userSelect:'none', lineHeight:'1.2', opacity:0.8,
}}>▲
{ke_midiToName(KE_BASS_MIDI[bassNotes[i]])}
cycleBassNote(i,-1)} style={{
fontSize:'9px', color: KE.bass, cursor:'pointer',
userSelect:'none', lineHeight:'1.2', opacity:0.8,
}}>▼
) : (
·
)}
);
})}
{!isNarrow &&
}
)}
);
})}
{/* SYNTH PARAMS */}
Synth Parameters
Atmosphere Pad
{/* TRANSPORT */}
{elapsed.toFixed(1)}s
{playing
? `Step ${String(currentStep+1).padStart(2,'0')} / 16 · ${bpm} BPM`
: '— Stopped —'
}
{duration}s
{/* Footer hint */}
Click pads to toggle · M = Mute · S = Solo · ▲▼ Bass notes · Presets per track
);
}
Object.assign(window, { KevinEngine });