/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / components / TrackList.tsx
139 lines · 6.0 KBCodeBlameHistory
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 1import { store, useApp, usePlayerVersion } from '../state/store.ts';
2import { player } from '../audio/player.ts';
78851f6Replace instrawave with simulated instruments driven by manifestsJeremy Magland 3import { getInstrument, listInstruments } from '../instruments/registry.ts';
4import type { KnobSpec } from '../instruments/types.ts';
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 5import { trackColor } from './colors.ts';
78851f6Replace instrawave with simulated instruments driven by manifestsJeremy Magland 7function fmtKnob(v: number): string {
8 const a = Math.abs(v);
9 if (a >= 1e5 || (a > 0 && a < 0.01)) return v.toExponential(1);
10 return String(Number(v.toPrecision(3)));
13function KnobSlider({ knob, value, onChange }: { knob: KnobSpec; value: number | undefined; onChange: (v: number) => void }) {
14 const cur = value ?? knob.default;
15 const log = knob.scale === 'log';
16 const pos = log ? (1000 * Math.log(cur / knob.min)) / Math.log(knob.max / knob.min) : cur;
17 return (
18 <label className="knob" title={`${knob.label} (default ${fmtKnob(knob.default)})`}>
19 <span className="knoblabel">{knob.label}</span>
20 <input
21 type="range"
22 min={log ? 0 : knob.min}
23 max={log ? 1000 : knob.max}
24 step={log ? 1 : (knob.step ?? (knob.max - knob.min) / 200)}
25 value={pos}
26 onChange={(e) => {
27 const raw = Number(e.target.value);
28 onChange(log ? knob.min * Math.pow(knob.max / knob.min, raw / 1000) : raw);
29 }}
30 onClick={(e) => e.stopPropagation()}
31 />
32 <span className="knobvalue">{fmtKnob(cur)}</span>
33 </label>
34 );
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 37export function TrackList() {
38 const app = useApp();
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 39 usePlayerVersion();
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 40 const song = app.rec?.song ?? null;
41 if (!song) return <div className="tracklist" />;
42 const instruments = listInstruments();
43 return (
44 <div className="tracklist">
45 {song.tracks.map((t, i) => {
46 const active = t.id === app.activeTrackId;
47 const armed = t.id === app.armedTrackId;
48 return (
49 <div key={t.id} className={`trackrow ${active ? 'active' : ''}`} onClick={() => store.setActiveTrack(t.id)}>
50 <div className="trackrow-top">
51 <span className="chip" style={{ background: trackColor(i) }} />
52 <input
53 className="trackname"
54 value={t.name}
55 onChange={(e) => store.updateTrack(t.id, { name: e.target.value }, false)}
56 onBlur={() => store.endEdit()}
57 onClick={(e) => e.stopPropagation()}
58 />
59 <button
60 className="mini danger"
61 title="Delete track"
62 onClick={(e) => { e.stopPropagation(); store.removeTrack(t.id); }}
63 >✕</button>
64 </div>
65 <div className="trackrow-bottom">
66 <select
67 value={t.instrumentId}
68 onChange={(e) => store.updateTrack(t.id, { instrumentId: e.target.value })}
69 onClick={(e) => e.stopPropagation()}
70 title="Instrument"
71 >
72 {instruments.map((ins) => (
73 <option key={ins.id} value={ins.id}>{ins.name}</option>
74 ))}
75 </select>
76 <button
77 className={`mini ${t.mute ? 'toggled' : ''}`}
78 title="Mute"
79 onClick={(e) => { e.stopPropagation(); store.updateTrack(t.id, { mute: !t.mute }, false); store.endEdit(); }}
80 >M</button>
81 <button
82 className={`mini ${t.solo ? 'toggled' : ''}`}
83 title="Solo"
84 onClick={(e) => { e.stopPropagation(); store.updateTrack(t.id, { solo: !t.solo }, false); store.endEdit(); }}
85 >S</button>
86 <button
87 className={`mini rec ${armed ? 'toggled' : ''}`}
88 title="Arm for recording"
89 onClick={(e) => { e.stopPropagation(); store.setArmedTrack(armed ? null : t.id); }}
90 >R</button>
f67f407Time selections, loop playback, and further DAW-standard editingJeremy Magland 91 <button className="mini" title="Move track up" onClick={(e) => { e.stopPropagation(); store.moveTrack(t.id, -1); }}>↑</button>
92 <button className="mini" title="Move track down" onClick={(e) => { e.stopPropagation(); store.moveTrack(t.id, 1); }}>↓</button>
93 <button className="mini" title="Duplicate track" onClick={(e) => { e.stopPropagation(); store.duplicateTrack(t.id); }}>⧉</button>
78851f6Replace instrawave with simulated instruments driven by manifestsJeremy Magland 95 {active && (getInstrument(t.instrumentId).knobs ?? []).length > 0 && (
96 <div className="trackrow-knobs">
97 {(getInstrument(t.instrumentId).knobs ?? []).map((k) => (
98 <KnobSlider
99 key={k.key}
100 knob={k}
101 value={t.settings?.[k.key]}
102 onChange={(v) => store.setTrackSetting(t.id, k.key, v)}
103 />
104 ))}
105 </div>
106 )}
cdfbf5bRender whole tracks through direct-import instrument modulesJeremy Magland 107 {player.renderProgress.has(t.id) && (
108 <div className="trackrow-render" title="This track is rendering">
109 <span className="renderbar">
110 <span className="renderbar-fill" style={{ width: `${Math.round((player.renderProgress.get(t.id)?.frac ?? 0) * 100)}%` }} />
111 </span>
112 <span className="renderstage">{player.renderProgress.get(t.id)?.stage}</span>
113 <button
114 className="mini danger"
115 title="Cancel rendering"
116 onClick={(e) => { e.stopPropagation(); player.cancelRender(); }}
117 >✕</button>
118 </div>
119 )}
0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 120 <div className="trackrow-vol">
121 <input
122 type="range"
123 min={0}
124 max={1.5}
125 step={0.01}
126 value={t.volume}
127 title={`Volume ${(t.volume * 100).toFixed(0)}%`}
128 onChange={(e) => store.updateTrack(t.id, { volume: Number(e.target.value) }, false)}
129 onClick={(e) => e.stopPropagation()}
130 />
131 <span className="notecount">{t.notes.length} notes</span>
132 </div>
133 </div>
134 );
135 })}
136 <button className="addtrack" onClick={() => store.addTrack()}>+ Add track</button>
137 </div>
138 );
↑↓ move↵openescclose