{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
);
}