/* 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 */}
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 */}
setTrackVol(p=>({...p,[track]:+e.target.value}))} style={{ flex:1, minWidth:0, cursor:'pointer', accentColor:info.color }} />
{trackVol[track]}%
{/* 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
Kick Drum
Bass Filter
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 });