Skip to content

Commit 5138bdf

Browse files
committed
Pivot MIDI Studio V2 to octave timeline editor workflow with GM instrument controls - PR_26146_028-midi-studio-v2-octave-timeline-editor
1 parent 6fed252 commit 5138bdf

11 files changed

Lines changed: 987 additions & 118 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PR_26146_028 MIDI Studio V2 Octave Timeline Editor Validation
2+
3+
## Status
4+
5+
PASS
6+
7+
## Scope Completed
8+
9+
- Replaced the Studio primary editing surface with an octave/note timeline grid.
10+
- Added left-column Songs and Instruments sections.
11+
- Restored a MIDI Import tab and kept MIDI import controls in the Songs area.
12+
- Moved instrument controls into left-column instrument rows with show/hide, GM family type, instrument, mute, solo, add, and delete controls.
13+
- Added GM family grouping for Piano, Chromatic Percussion, Organ, Guitar, Bass, Strings, Ensemble, Brass, Reed, Pipe, Synth Lead, Synth Pad, Synth Effects, Ethnic, Percussive, and Sound Effects.
14+
- Rendered visible octave/note rows and beat/bar columns in the center timeline.
15+
- Implemented click-to-toggle note editing for the selected instrument.
16+
- Highlighted selected instrument notes and dimmed non-selected instrument notes.
17+
- Hid notes for hidden instruments and excluded hidden lanes from Preview Synth playback.
18+
- Preserved beat/bar playhead progression and Play/Stop behavior from visible timeline data.
19+
- Normalized imported local MIDI note data into editable studio arrangement lanes.
20+
- Preserved percussion-style Preview Synth playback for drum/percussion rows.
21+
- Removed the Export tab; export rendering remains out of scope.
22+
23+
## Validation Commands
24+
25+
- PASS: `node --check src/engine/audio/InstrumentGridParser.js`
26+
- PASS: `node --check src/engine/audio/PreviewInstrumentPacks.js`
27+
- PASS: `node --check src/engine/audio/PreviewSynthEngine.js`
28+
- PASS: `node --check tools/midi-studio-v2/js/MidiStudioV2App.js`
29+
- PASS: `node --check tools/midi-studio-v2/js/bootstrap.js`
30+
- PASS: `node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
31+
- PASS: `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
32+
- PASS: `rg --pcre2 -n "<style|on(click|change|input|submit)=|<script(?![^>]*src)" tools/midi-studio-v2/index.html; if ($LASTEXITCODE -eq 1) { exit 0 }`
33+
- PASS: `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs -g "octave timeline editor is the default editable and playable Studio workflow|roadmap exists" --reporter=list --workers=1 --timeout=60000`
34+
- PASS: `git diff --check`
35+
36+
## Playwright Proof Points
37+
38+
- Octave timeline editor is visible by default on Studio.
39+
- Imported manifest songs populate the left Songs column.
40+
- Selecting manifest songs immediately updates timeline notes.
41+
- Left Instruments rows populate with show/hide, GM Type, Instrument, mute, solo, add, and delete controls.
42+
- GM Type and Instrument dropdowns update together.
43+
- Selecting an instrument highlights its notes.
44+
- Non-selected instrument notes dim.
45+
- Show/hide excludes hidden instrument notes from the visible timeline.
46+
- Octave rows render with note labels.
47+
- Clicking note cells toggles editable selected-instrument timeline data.
48+
- Playback uses visible note cell data.
49+
- Playhead advances by beat/bar columns.
50+
- Local `.midi` import normalizes parsed MIDI notes into editable timeline lanes.
51+
- Imported MIDI timeline data can be edited and played.
52+
- Play and Stop update control state and Preview Synth state.
53+
- Drum/percussion playback schedules percussion-style buffer events.
54+
55+
## Coverage
56+
57+
- `docs/dev/reports/playwright_v8_coverage_report.txt` refreshed by the targeted Playwright run.
58+
- `docs/dev/reports/coverage_changed_js_guardrail.txt` refreshed by the targeted Playwright run.
59+
- Coverage is advisory; no changed runtime JS coverage warnings were reported.
60+
61+
## Explicit Non-Runs
62+
63+
- Full samples smoke test was not run per request.
64+
- Export rendering was not implemented or validated.
65+
- SoundFont playback was not implemented or validated.
66+
- MIDI recording/input was not implemented or validated.
67+

docs/dev/roadmaps/MIDI_STUDIO_V2_ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
- [x] Playable upbeat public-domain/traditional-style test song arrangement includes Lead, Bass, Chords/Pad, and Drums.
99
- [x] Studio tab shows a visible track/timeline editor without opening accordions.
1010
- [x] Tabs organize Studio, Song Setup, Instruments, Auto-Create Parts, and Diagnostics; MIDI Import lives under Selected Song Details and export actions live in the action bar.
11-
- [.] Editable note/timeline grid remains the primary music studio editing surface.
11+
- [x] Editable note/timeline grid remains the primary music studio editing surface.
1212
- [x] Track volume, pan, mute, and solo controls live on instrument timeline rows.
1313
- [.] Song setup fields for tempo, key, style, intro, and loop.
14-
- [ ] MIDI import conversion to editable tracks.
14+
- [x] MIDI import conversion to editable tracks.
1515
- [ ] Rendered WAV/MP3/OGG export.
1616
- [ ] SoundFont and real instrument playback.
1717
- [ ] Auto-Create Parts helpers for generated bass, pad, arpeggio, and drums.

src/engine/audio/InstrumentGridParser.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,14 @@ export class InstrumentGridParser {
173173
}
174174

175175
laneNamesFor(lanes = {}) {
176-
const names = DEFAULT_LANE_NAMES.slice();
176+
const names = [];
177177
Object.keys(lanes || {}).forEach((lane) => {
178178
const normalized = String(lane || "").trim();
179179
if (normalized && !names.includes(normalized)) {
180180
names.push(normalized);
181181
}
182182
});
183-
return names;
183+
return names.length ? names : DEFAULT_LANE_NAMES.slice();
184184
}
185185

186186
parseLane({ barCount, beatsPerBar, generatedSource, lane, sections, source, stepsPerBar, subdivision }) {

src/engine/audio/PreviewInstrumentPacks.js

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
export const PREVIEW_INSTRUMENT_PACKS = [
22
{
33
id: "retro-square-lead",
4-
label: "Retro Square Lead",
4+
label: "Lead 1 (Square)",
55
synthRole: "lead",
6-
typeGroup: "Synth",
6+
typeGroup: "Synth Lead",
77
volume: 0.075,
88
waveform: "square"
99
},
1010
{
1111
id: "retro-pulse-lead",
12-
label: "Retro Pulse Lead",
12+
label: "Lead 2 (Sawtooth)",
1313
synthRole: "lead",
14-
typeGroup: "Synth",
14+
typeGroup: "Synth Lead",
1515
volume: 0.07,
16-
waveform: "square"
16+
waveform: "sawtooth"
1717
},
1818
{
1919
id: "synth-bass",
20-
label: "Synth Bass",
20+
label: "Synth Bass 1",
2121
synthRole: "bass",
2222
typeGroup: "Bass",
2323
transposeSemitones: -12,
@@ -27,53 +27,89 @@ export const PREVIEW_INSTRUMENT_PACKS = [
2727
{
2828
approximationWarning: "Keyboard preview is an approximate electric-piano style synth patch.",
2929
id: "preview-electric-piano",
30-
label: "Preview Electric Piano",
30+
label: "Electric Piano 1",
31+
synthRole: "lead",
32+
typeGroup: "Piano",
33+
volume: 0.065,
34+
waveform: "sine"
35+
},
36+
{
37+
approximationWarning: "Piano preview is an approximate sine-based synth patch.",
38+
id: "preview-acoustic-grand-piano",
39+
label: "Acoustic Grand Piano",
3140
synthRole: "lead",
32-
typeGroup: "Keyboard",
41+
typeGroup: "Piano",
3342
volume: 0.065,
3443
waveform: "sine"
3544
},
45+
{
46+
approximationWarning: "Chromatic percussion preview is an approximate mallet synth patch.",
47+
id: "preview-celesta",
48+
label: "Celesta",
49+
synthRole: "lead",
50+
typeGroup: "Chromatic Percussion",
51+
volume: 0.058,
52+
waveform: "sine"
53+
},
54+
{
55+
approximationWarning: "Organ preview is an approximate drawbar synth patch.",
56+
id: "preview-drawbar-organ",
57+
label: "Drawbar Organ",
58+
synthRole: "pad",
59+
typeGroup: "Organ",
60+
volume: 0.052,
61+
waveform: "sine"
62+
},
3663
{
3764
id: "warm-pad",
3865
label: "Warm Pad",
3966
durationScale: 1.3,
4067
synthRole: "pad",
41-
typeGroup: "Keyboard",
68+
typeGroup: "Synth Pad",
4269
volume: 0.045,
4370
waveform: "sine"
4471
},
4572
{
4673
id: "basic-drums",
47-
label: "Basic Drums",
74+
label: "Standard Drum Kit",
4875
synthRole: "percussion",
49-
typeGroup: "Percussion",
76+
typeGroup: "Percussive",
5077
volume: 0.16,
5178
waveform: "noise"
5279
},
5380
{
5481
id: "ambient-pad",
55-
label: "Ambient Pad",
82+
label: "Pad 2 (Warm)",
5683
durationScale: 1.55,
5784
synthRole: "pad",
5885
transposeSemitones: 12,
59-
typeGroup: "FX",
86+
typeGroup: "Synth Pad",
6087
volume: 0.035,
6188
waveform: "sine"
6289
},
90+
{
91+
approximationWarning: "Violin preview is an approximate bowed synth patch.",
92+
id: "preview-violin",
93+
label: "Violin",
94+
synthRole: "lead",
95+
typeGroup: "Strings",
96+
volume: 0.055,
97+
waveform: "sine"
98+
},
6399
{
64100
approximationWarning: "String preview is an approximate synth pad, not a sampled string section.",
65101
durationScale: 1.45,
66102
id: "preview-string-ensemble",
67-
label: "Preview String Ensemble",
103+
label: "String Ensemble 1",
68104
synthRole: "pad",
69-
typeGroup: "Strings",
105+
typeGroup: "Ensemble",
70106
volume: 0.04,
71107
waveform: "sine"
72108
},
73109
{
74110
approximationWarning: "Brass preview is an approximate synth brass patch.",
75111
id: "preview-brass-stab",
76-
label: "Preview Brass Stab",
112+
label: "Brass Section",
77113
synthRole: "lead",
78114
typeGroup: "Brass",
79115
volume: 0.07,
@@ -82,20 +118,56 @@ export const PREVIEW_INSTRUMENT_PACKS = [
82118
{
83119
approximationWarning: "Woodwind preview is an approximate breathy synth lead.",
84120
id: "preview-woodwind",
85-
label: "Preview Woodwind",
121+
label: "Clarinet",
86122
synthRole: "lead",
87-
typeGroup: "Woodwind",
123+
typeGroup: "Reed",
88124
volume: 0.06,
89125
waveform: "triangle"
90126
},
127+
{
128+
approximationWarning: "Pipe preview is an approximate flute-like synth lead.",
129+
id: "preview-flute",
130+
label: "Flute",
131+
synthRole: "lead",
132+
typeGroup: "Pipe",
133+
volume: 0.055,
134+
waveform: "sine"
135+
},
91136
{
92137
approximationWarning: "Guitar preview is an approximate plucked synth tone.",
93138
id: "preview-clean-guitar",
94-
label: "Preview Clean Guitar",
139+
label: "Electric Guitar (Clean)",
95140
synthRole: "lead",
96141
typeGroup: "Guitar",
97142
volume: 0.06,
98143
waveform: "triangle"
144+
},
145+
{
146+
approximationWarning: "Synth effects preview is an approximate effects patch.",
147+
id: "preview-synth-fx",
148+
label: "FX 1 (Rain)",
149+
synthRole: "pad",
150+
typeGroup: "Synth Effects",
151+
volume: 0.04,
152+
waveform: "sawtooth"
153+
},
154+
{
155+
approximationWarning: "Ethnic instrument preview is an approximate plucked synth patch.",
156+
id: "preview-shamisen",
157+
label: "Shamisen",
158+
synthRole: "lead",
159+
typeGroup: "Ethnic",
160+
volume: 0.055,
161+
waveform: "triangle"
162+
},
163+
{
164+
approximationWarning: "Sound effect preview is an approximate synth effect patch.",
165+
id: "preview-sci-fi",
166+
label: "FX 8 (Sci-Fi)",
167+
synthRole: "lead",
168+
typeGroup: "Sound Effects",
169+
volume: 0.055,
170+
waveform: "square"
99171
}
100172
];
101173

src/engine/audio/PreviewSynthEngine.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export class PreviewSynthEngine {
144144
const instruments = laneSettings.instruments || {};
145145
const muted = laneSettings.muted || {};
146146
const soloed = laneSettings.soloed || {};
147+
const visible = laneSettings.visible || {};
147148
const soloedLanes = Object.entries(soloed).filter((entry) => entry[1]).map(([lane]) => lane);
148149
const warnings = [];
149150
const warningKeys = new Set();
@@ -153,6 +154,9 @@ export class PreviewSynthEngine {
153154
if (!Number.isFinite(stepIndex) || stepIndex < startStep || stepIndex > endStep) {
154155
return;
155156
}
157+
if (visible[event.lane] === false) {
158+
return;
159+
}
156160
if (muted[event.lane] || (soloedLanes.length && !soloedLanes.includes(event.lane))) {
157161
return;
158162
}

0 commit comments

Comments
 (0)