import { useState, useEffect, useRef, useCallback } from "react"; const NUM_STEPS = 16; const DRUM_TRACKS = [ { id:"kick", label:"KICK", color:"#ff2200" }, { id:"snare", label:"SNARE", color:"#ffcc00" }, { id:"hihat", label:"HI-HAT", color:"#00ff88" }, { id:"clap", label:"CLAP", color:"#0088ff" }, { id:"cowbell", label:"COWBELL", color:"#ff00cc" }, { id:"sample", label:"SAMPLE", color:"#ff8800" }, ]; const INST = { piano: { notes:["C5","B4","A4","G4","F4","E4","D4","C4"], freqs:{C5:523.25,B4:493.88,A4:440,G4:392,F4:349.23,E4:329.63,D4:293.66,C4:261.63}, color:"#ffcc00", wave:"sine" }, bass: { notes:["C3","B2","A2","G2","F2","E2","D2","C2"], freqs:{C3:130.81,B2:123.47,A2:110,G2:98,F2:87.31,E2:82.41,D2:73.42,C2:65.41}, color:"#00ff88", wave:"sawtooth" }, lead: { notes:["C6","B5","A5","G5","F5","E5","D5","C5"], freqs:{C6:1046.5,B5:987.77,A5:880,G5:783.99,F5:698.46,E5:659.25,D5:587.33,C5:523.25}, color:"#0088ff", wave:"sawtooth" }, trumpet:{ notes:["C5","B4","A4","G4","F4","E4","D4","C4"], freqs:{C5:523.25,B4:493.88,A4:440,G4:392,F4:349.23,E4:329.63,D4:293.66,C4:261.63}, color:"#ffaa00", wave:"sawtooth" }, sax: { notes:["C5","B4","A4","G4","F4","E4","D4","C4"], freqs:{C5:523.25,B4:493.88,A4:440,G4:392,F4:349.23,E4:329.63,D4:293.66,C4:261.63}, color:"#ff5500", wave:"sawtooth" }, }; const WAVEFORMS = ["sine","square","sawtooth","triangle"]; const TABS = ["drums","piano","bass","lead","trumpet","sax","mixer"]; function createDrumSound(ctx, type, sampleBuf) { const now = ctx.currentTime; const g = ctx.createGain(); g.connect(ctx.destination); if (type==="kick") { const o=ctx.createOscillator(); o.connect(g); o.frequency.setValueAtTime(150,now); o.frequency.exponentialRampToValueAtTime(0.001,now+0.5); g.gain.setValueAtTime(1,now); g.gain.exponentialRampToValueAtTime(0.001,now+0.5); o.start(now); o.stop(now+0.5); } else if (type==="snare") { const buf=ctx.createBuffer(1,ctx.sampleRate*0.2,ctx.sampleRate); const d=buf.getChannelData(0); for(let i=0;i { const o = ctx.createOscillator(); o.type = "sawtooth"; o.frequency.value = freq * n; const og = ctx.createGain(); og.gain.value = amp * vol * 0.3; o.connect(og); og.connect(g); o.start(now); o.stop(now+0.5); }); g.gain.setValueAtTime(0,now); g.gain.linearRampToValueAtTime(1,now+0.03); g.gain.setValueAtTime(1,now+0.1); g.gain.exponentialRampToValueAtTime(0.001,now+0.5); } function playSax(ctx, freq, vol) { const now = ctx.currentTime; const g = ctx.createGain(); g.connect(ctx.destination); const o = ctx.createOscillator(); o.type = "sawtooth"; o.frequency.value = freq; const bp = ctx.createBiquadFilter(); bp.type="bandpass"; bp.frequency.value=freq*2; bp.Q.value=1.5; const lp = ctx.createBiquadFilter(); lp.type="lowpass"; lp.frequency.value=freq*6; const og = ctx.createGain(); og.gain.value = vol * 0.5; o.connect(bp); bp.connect(lp); lp.connect(og); og.connect(g); // vibrato const vib = ctx.createOscillator(); vib.frequency.value = 5.5; const vibG = ctx.createGain(); vibG.gain.value = freq * 0.012; vib.connect(vibG); vibG.connect(o.frequency); g.gain.setValueAtTime(0,now); g.gain.linearRampToValueAtTime(1,now+0.06); g.gain.setValueAtTime(0.85,now+0.15); g.gain.exponentialRampToValueAtTime(0.001,now+0.55); o.start(now); vib.start(now); o.stop(now+0.55); vib.stop(now+0.55); } function playTone(ctx, freq, wave, vol, attack, decay) { const now = ctx.currentTime; const o=ctx.createOscillator(); const g=ctx.createGain(); o.type=wave; o.frequency.value=freq; o.connect(g); g.connect(ctx.destination); g.gain.setValueAtTime(0,now); g.gain.linearRampToValueAtTime(vol,now+attack); g.gain.exponentialRampToValueAtTime(0.001,now+attack+decay); o.start(now); o.stop(now+attack+decay); } function PitchSequencer({ notes, grid, setGrid, step, color }) { const toggle=(ni,si)=>setGrid(g=>{const n=g.map(r=>[...r]);n[ni][si]=!n[ni][si];return n;}); return (
{Array(NUM_STEPS).fill(0).map((_,i)=>(
{i+1}
))}
{notes.map((note,ni)=>(
{note}
{Array(NUM_STEPS).fill(0).map((_,si)=>{ const on=grid[ni][si]; const active=step===si&&on; return
toggle(ni,si)} style={{width:"30px",height:"24px",background:active?color:on?color+"55":"#111",border:`1px solid ${active?color:on?color+"44":"#1e1e1e"}`,cursor:"pointer",transition:"background 0.04s",borderRadius:"2px"}} />; })}
))}
); } function EnvVol({env,setEnv,volKey,volumes,setVolumes,color="#ff2200"}) { return (
{[["ATK","attack",0.01,1,0.01,v=>v.toFixed(2)+"s"],["DEC","decay",0.05,2,0.05,v=>v.toFixed(2)+"s"]].map(([lbl,key,mn,mx,st,fmt])=>(
{lbl} setEnv(v=>({...v,[key]:+e.target.value}))} style={{accentColor:color,width:"80px"}} /> {fmt(env[key])}
))}
VOL setVolumes(v=>({...v,[volKey]:+e.target.value}))} style={{accentColor:color,width:"80px"}} /> {Math.round(volumes[volKey]*100)}
); } // Animated step indicator function StepLight({step,color}) { return (
{Array(NUM_STEPS).fill(0).map((_,i)=>(
))}
); } export default function RetroStudio() { const [bpm,setBpm]=useState(120); const [playing,setPlaying]=useState(false); const [step,setStep]=useState(-1); const [activeTab,setActiveTab]=useState("drums"); const [drumGrid,setDrumGrid]=useState(()=>Object.fromEntries(DRUM_TRACKS.map(t=>[t.id,Array(NUM_STEPS).fill(false)]))); const [grids,setGrids]=useState(()=>Object.fromEntries(Object.keys(INST).map(k=>[k,Array(8).fill(null).map(()=>Array(NUM_STEPS).fill(false))]))); const [waves,setWaves]=useState({bass:"sine",lead:"sawtooth"}); const [volumes,setVolumes]=useState({kick:0.8,snare:0.7,hihat:0.5,clap:0.6,cowbell:0.7,sample:0.8,piano:0.6,bass:0.6,lead:0.5,trumpet:0.65,sax:0.6}); const [envs,setEnvs]=useState({piano:{attack:0.02,decay:0.35},bass:{attack:0.03,decay:0.5},lead:{attack:0.05,decay:0.3}}); const [sampleBuf,setSampleBuf]=useState(null); const [sampleName,setSampleName]=useState(null); const [pulse,setPulse]=useState(false); const ctxRef=useRef(null); const intervalRef=useRef(null); const stepRef=useRef(0); const stateRef=useRef({}); stateRef.current={drumGrid,grids,volumes,waves,envs,sampleBuf}; const getCtx=()=>{ if(!ctxRef.current) ctxRef.current=new(window.AudioContext||window.webkitAudioContext)(); if(ctxRef.current.state==="suspended") ctxRef.current.resume(); return ctxRef.current; }; const tick=useCallback(()=>{ const ctx=getCtx(); const s=stepRef.current; const {drumGrid,grids,volumes,waves,envs,sampleBuf}=stateRef.current; DRUM_TRACKS.forEach(t=>{ if(drumGrid[t.id][s]) createDrumSound(ctx,t.id,sampleBuf); }); INST.piano.notes.forEach((n,ni)=>{ if(grids.piano[ni][s]) playTone(ctx,INST.piano.freqs[n],"sine",volumes.piano,envs.piano.attack,envs.piano.decay); }); INST.bass.notes.forEach((n,ni)=>{ if(grids.bass[ni][s]) playTone(ctx,INST.bass.freqs[n],waves.bass||"sine",volumes.bass,envs.bass.attack,envs.bass.decay); }); INST.lead.notes.forEach((n,ni)=>{ if(grids.lead[ni][s]) playTone(ctx,INST.lead.freqs[n],waves.lead||"sawtooth",volumes.lead,envs.lead.attack,envs.lead.decay); }); INST.trumpet.notes.forEach((n,ni)=>{ if(grids.trumpet[ni][s]) playTrumpet(ctx,INST.trumpet.freqs[n],volumes.trumpet); }); INST.sax.notes.forEach((n,ni)=>{ if(grids.sax[ni][s]) playSax(ctx,INST.sax.freqs[n],volumes.sax); }); setStep(s); setPulse(p=>!p); stepRef.current=(s+1)%NUM_STEPS; },[]); useEffect(()=>{ if(playing){ stepRef.current=0; tick(); intervalRef.current=setInterval(tick,(60/bpm/4)*1000); } else { clearInterval(intervalRef.current); setStep(-1); } return ()=>clearInterval(intervalRef.current); },[playing,bpm,tick]); useEffect(()=>{ if(playing){ clearInterval(intervalRef.current); intervalRef.current=setInterval(tick,(60/bpm/4)*1000); } },[bpm]); const clearAll=()=>{ setDrumGrid(Object.fromEntries(DRUM_TRACKS.map(t=>[t.id,Array(NUM_STEPS).fill(false)]))); setGrids(Object.fromEntries(Object.keys(INST).map(k=>[k,Array(8).fill(null).map(()=>Array(NUM_STEPS).fill(false))]))); }; const setGrid=(inst,g)=>setGrids(gs=>({...gs,[inst]:g instanceof Function?g(gs[inst]):g})); const handleUpload=async(e)=>{ const file=e.target.files[0]; if(!file) return; const ctx=getCtx(); const ab=await file.arrayBuffer(); ctx.decodeAudioData(ab,buf=>{setSampleBuf(buf);setSampleName(file.name);}); }; const tabColor=(t)=>({drums:"#ff2200",piano:INST.piano.color,bass:INST.bass.color,lead:INST.lead.color,trumpet:INST.trumpet.color,sax:INST.sax.color,mixer:"#aaaaaa"}[t]||"#fff"); const isActive=(t)=>activeTab===t; return (
{/* HEADER */}
{/* animated logo */}
RETROSTUDIO
FULL BAND SEQUENCER
{/* beat pulse dots */}
{DRUM_TRACKS.map((t,i)=>(
))}
BPM setBpm(Math.min(240,Math.max(40,parseInt(e.target.value)||120)))} style={{background:"#111",color:"#ffcc00",border:"1px solid #2a2a2a",padding:"5px 8px",width:"50px",textAlign:"center",fontFamily:"'Courier New',monospace",fontSize:"13px"}} /> setBpm(+e.target.value)} style={{accentColor:"#ffcc00",width:"80px"}} />
{/* STEP BAR */} {playing && (
{Array(NUM_STEPS).fill(0).map((_,i)=>(
))}
)} {/* TABS */}
{TABS.map(t=>( ))}
{/* DRUMS */} {activeTab==="drums" && <>
DRUM SEQUENCER
{DRUM_TRACKS.map(track=>(
{track.label}
{drumGrid[track.id].map((on,i)=>{ const active=step===i&&on; return
setDrumGrid(g=>({...g,[track.id]:g[track.id].map((v,j)=>j===i?!v:v)}))} style={{width:"30px",height:"26px",background:active?track.color:on?track.color+"55":"#111",border:`1px solid ${active?track.color:on?track.color+"55":"#1e1e1e"}`,cursor:"pointer",borderRadius:"2px",transition:"background 0.04s"}} />; })}
))}
SAMPLE
{sampleName||"NO FILE LOADED"}
{sampleBuf&&
{sampleBuf.duration.toFixed(2)}s
}
{DRUM_TRACKS.map(t=>(
{t.label} setVolumes(v=>({...v,[t.id]:+e.target.value}))} style={{accentColor:t.color,flex:1}} /> {Math.round(volumes[t.id]*100)}
))}
} {/* PIANO */} {activeTab==="piano" && <>
PIANO SEQUENCER
setEnvs(ev=>({...ev,piano:e instanceof Function?e(ev.piano):e}))} volKey="piano" volumes={volumes} setVolumes={setVolumes} color={INST.piano.color} /> setGrid("piano",g)} step={step} color={INST.piano.color} /> } {/* BASS */} {activeTab==="bass" && <>
BASS SEQUENCER
{WAVEFORMS.map(w=>)}
setEnvs(ev=>({...ev,bass:e instanceof Function?e(ev.bass):e}))} volKey="bass" volumes={volumes} setVolumes={setVolumes} color={INST.bass.color} /> setGrid("bass",g)} step={step} color={INST.bass.color} /> } {/* LEAD */} {activeTab==="lead" && <>
LEAD SEQUENCER
{WAVEFORMS.map(w=>)}
setEnvs(ev=>({...ev,lead:e instanceof Function?e(ev.lead):e}))} volKey="lead" volumes={volumes} setVolumes={setVolumes} color={INST.lead.color} /> setGrid("lead",g)} step={step} color={INST.lead.color} /> } {/* TRUMPET */} {activeTab==="trumpet" && <>
TRUMPET SEQUENCER
Bright brass tone · harmonic-series synthesis · natural attack transient
VOL setVolumes(v=>({...v,trumpet:+e.target.value}))} style={{accentColor:INST.trumpet.color,width:"120px"}} /> {Math.round(volumes.trumpet*100)}
setGrid("trumpet",g)} step={step} color={INST.trumpet.color} /> } {/* SAX */} {activeTab==="sax" && <>
SAXOPHONE SEQUENCER
Warm reedy tone · bandpass filtered sawtooth · vibrato LFO
VOL setVolumes(v=>({...v,sax:+e.target.value}))} style={{accentColor:INST.sax.color,width:"120px"}} /> {Math.round(volumes.sax*100)}
setGrid("sax",g)} step={step} color={INST.sax.color} /> } {/* MIXER */} {activeTab==="mixer" && <>
CHANNEL MIXER
{[...DRUM_TRACKS.map(t=>({id:t.id,label:t.label,color:t.color})), {id:"piano",label:"PIANO",color:INST.piano.color}, {id:"bass",label:"BASS",color:INST.bass.color}, {id:"lead",label:"LEAD",color:INST.lead.color}, {id:"trumpet",label:"TRMPT",color:INST.trumpet.color}, {id:"sax",label:"SAX",color:INST.sax.color}, ].map(ch=>(
{ch.label}
setVolumes(v=>({...v,[ch.id]:+e.target.value}))} style={{writingMode:"vertical-lr",direction:"rtl",height:"70px",accentColor:ch.color}} />
{Math.round((volumes[ch.id]||0.5)*100)}
))}
BPM {bpm} STATUS {playing?"PLAYING":"STOPPED"} STEP {step<0?"--":step+1}/{NUM_STEPS} SAMPLE {sampleName||"NONE"}
}
RETROSTUDIO v3.0 DRUMS · PIANO · BASS · LEAD · TRUMPET · SAX · MIXER
); }