/ proof-of-concept / midiboot
Sign in
proof-of-concept / midiboot
midiboot / src / components / TrackList.tsx
124 lines · 5.2 KBBlameHistoryRaw
1import { store, useApp } from '../state/store.ts';
2import { getInstrument, listInstruments } from '../instruments/registry.ts';
3import type { KnobSpec } from '../instruments/types.ts';
4import { trackColor } from './colors.ts';
6function fmtKnob(v: number): string {
7 const a = Math.abs(v);
8 if (a >= 1e5 || (a > 0 && a < 0.01)) return v.toExponential(1);
9 return String(Number(v.toPrecision(3)));
12function KnobSlider({ knob, value, onChange }: { knob: KnobSpec; value: number | undefined; onChange: (v: number) => void }) {
13 const cur = value ?? knob.default;
14 const log = knob.scale === 'log';
15 const pos = log ? (1000 * Math.log(cur / knob.min)) / Math.log(knob.max / knob.min) : cur;
16 return (
17 <label className="knob" title={`${knob.label} (default ${fmtKnob(knob.default)})`}>
18 <span className="knoblabel">{knob.label}</span>
19 <input
20 type="range"
21 min={log ? 0 : knob.min}
22 max={log ? 1000 : knob.max}
23 step={log ? 1 : (knob.step ?? (knob.max - knob.min) / 200)}
24 value={pos}
25 onChange={(e) => {
26 const raw = Number(e.target.value);
27 onChange(log ? knob.min * Math.pow(knob.max / knob.min, raw / 1000) : raw);
28 }}
29 onClick={(e) => e.stopPropagation()}
30 />
31 <span className="knobvalue">{fmtKnob(cur)}</span>
32 </label>
33 );
36export function TrackList() {
37 const app = useApp();
38 const song = app.rec?.song ?? null;
39 if (!song) return <div className="tracklist" />;
40 const instruments = listInstruments();
41 return (
42 <div className="tracklist">
43 {song.tracks.map((t, i) => {
44 const active = t.id === app.activeTrackId;
45 const armed = t.id === app.armedTrackId;
46 return (
47 <div key={t.id} className={`trackrow ${active ? 'active' : ''}`} onClick={() => store.setActiveTrack(t.id)}>
48 <div className="trackrow-top">
49 <span className="chip" style={{ background: trackColor(i) }} />
50 <input
51 className="trackname"
52 value={t.name}
53 onChange={(e) => store.updateTrack(t.id, { name: e.target.value }, false)}
54 onBlur={() => store.endEdit()}
55 onClick={(e) => e.stopPropagation()}
56 />
57 <button
58 className="mini danger"
59 title="Delete track"
60 onClick={(e) => { e.stopPropagation(); store.removeTrack(t.id); }}
61 >✕</button>
62 </div>
63 <div className="trackrow-bottom">
64 <select
65 value={t.instrumentId}
66 onChange={(e) => store.updateTrack(t.id, { instrumentId: e.target.value })}
67 onClick={(e) => e.stopPropagation()}
68 title="Instrument"
69 >
70 {instruments.map((ins) => (
71 <option key={ins.id} value={ins.id}>{ins.name}</option>
72 ))}
73 </select>
74 <button
75 className={`mini ${t.mute ? 'toggled' : ''}`}
76 title="Mute"
77 onClick={(e) => { e.stopPropagation(); store.updateTrack(t.id, { mute: !t.mute }, false); store.endEdit(); }}
78 >M</button>
79 <button
80 className={`mini ${t.solo ? 'toggled' : ''}`}
81 title="Solo"
82 onClick={(e) => { e.stopPropagation(); store.updateTrack(t.id, { solo: !t.solo }, false); store.endEdit(); }}
83 >S</button>
84 <button
85 className={`mini rec ${armed ? 'toggled' : ''}`}
86 title="Arm for recording"
87 onClick={(e) => { e.stopPropagation(); store.setArmedTrack(armed ? null : t.id); }}
88 >R</button>
89 <button className="mini" title="Move track up" onClick={(e) => { e.stopPropagation(); store.moveTrack(t.id, -1); }}>↑</button>
90 <button className="mini" title="Move track down" onClick={(e) => { e.stopPropagation(); store.moveTrack(t.id, 1); }}>↓</button>
91 <button className="mini" title="Duplicate track" onClick={(e) => { e.stopPropagation(); store.duplicateTrack(t.id); }}>⧉</button>
92 </div>
93 {active && (getInstrument(t.instrumentId).knobs ?? []).length > 0 && (
94 <div className="trackrow-knobs">
95 {(getInstrument(t.instrumentId).knobs ?? []).map((k) => (
96 <KnobSlider
97 key={k.key}
98 knob={k}
99 value={t.settings?.[k.key]}
100 onChange={(v) => store.setTrackSetting(t.id, k.key, v)}
101 />
102 ))}
103 </div>
104 )}
105 <div className="trackrow-vol">
106 <input
107 type="range"
108 min={0}
109 max={1.5}
110 step={0.01}
111 value={t.volume}
112 title={`Volume ${(t.volume * 100).toFixed(0)}%`}
113 onChange={(e) => store.updateTrack(t.id, { volume: Number(e.target.value) }, false)}
114 onClick={(e) => e.stopPropagation()}
115 />
116 <span className="notecount">{t.notes.length} notes</span>
117 </div>
118 </div>
119 );
120 })}
121 <button className="addtrack" onClick={() => store.addTrack()}>+ Add track</button>
122 </div>
123 );
↑↓ move↵openescclose