Skip to content

Commit c0e9691

Browse files
committed
Clean MIDI Studio V2 Song Setup tab and rename Studio to Octave Timeline - PR_26146_052-midi-studio-v2-song-setup-tab-cleanup
1 parent 40cad07 commit c0e9691

9 files changed

Lines changed: 322 additions & 76 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PR_26146_052 MIDI Studio V2 Song Setup Tab Cleanup Validation
2+
3+
## Summary
4+
5+
Status: PASS
6+
7+
Playwright impacted: Yes
8+
9+
This PR reorders the MIDI Studio V2 tabs so Song Setup is first/default, renames the visible Studio tab to Octave Timeline, adds canonical Add Song support, removes the visible top selected-song details form from Song Setup, and uses the existing Key/Style dropdowns as canonical song arrangement editors.
10+
11+
## Files Changed
12+
13+
- `tools/midi-studio-v2/index.html`
14+
- `tools/midi-studio-v2/js/MidiStudioV2App.js`
15+
- `tools/midi-studio-v2/js/bootstrap.js`
16+
- `tools/midi-studio-v2/js/controls/SongDetailsControl.js`
17+
- `tools/midi-studio-v2/js/controls/SongSetupControl.js`
18+
- `tools/midi-studio-v2/js/controls/SongSheetControl.js`
19+
- `tools/midi-studio-v2/styles/midiStudioV2.css`
20+
- `tests/playwright/tools/MidiStudioV2.spec.mjs`
21+
- `docs/dev/codex_commands.md`
22+
- `docs/dev/commit_comment.txt`
23+
- `docs/dev/reports/PR_26146_052-midi-studio-v2-song-setup-tab-cleanup_validation.md`
24+
25+
## Validation Commands
26+
27+
- PASS: changed-file JavaScript syntax checks with `node --check`.
28+
- PASS: HTML external-only check for `<script>` usage.
29+
- PASS: HTML no-inline-style/no-inline-handler check for `<style| on[a-z]+=`.
30+
- PASS: `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --grep "cleans up Song Setup tab|canvas octave timeline edits canonical data"`.
31+
- PASS: `git diff --check` with line-ending warnings only.
32+
33+
## Playwright Coverage
34+
35+
The targeted MIDI Studio V2 tests prove:
36+
37+
- Song Setup appears before Octave Timeline.
38+
- The visible Studio tab text is gone.
39+
- Octave Timeline tab exists.
40+
- Add Song creates `new-song-4` in the canonical payload model.
41+
- The top Key/Style detail fields are removed from the visible Song Setup surface.
42+
- Key and Style dropdowns update the selected song canonical arrangement.
43+
- Fields below/around Song Sheet were removed from Song Setup, including Director and Rendered Export Targets panels.
44+
- Song Sheet bottom spacing is reduced.
45+
- Play and Stop still work from the canvas-backed Octave Timeline.
46+
- The PR051 canvas timeline still edits canonical data and updates playback without DOM grid repaint classes.
47+
48+
## Coverage Artifacts
49+
50+
- `docs/dev/reports/playwright_v8_coverage_report.txt` was refreshed by the targeted Playwright run.
51+
- `docs/dev/reports/coverage_changed_js_guardrail.txt` was refreshed by the targeted Playwright run.
52+
- Coverage is advisory only; the guardrail reports low function coverage for `MidiStudioV2App.js` as WARN, not FAIL.
53+
54+
## Samples Decision
55+
56+
Full samples smoke test: SKIP.
57+
58+
Reason: explicitly out of scope for this PR and not requested; this change is limited to MIDI Studio V2 UI/runtime behavior.
59+
60+
## Notes
61+
62+
An initial focused Playwright run exposed that the hidden `#songDetails` element still computed visible because local CSS forced `display: grid`. The hidden-state CSS rule was corrected and the focused Playwright suite passed on rerun.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 87 additions & 52 deletions
Large diffs are not rendered by default.

tools/midi-studio-v2/index.html

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
7070
</nav>
7171

7272
<nav class="midi-studio-v2__tabs" aria-label="MIDI Studio sections" role="tablist">
73-
<button class="is-active" type="button" role="tab" aria-selected="true" data-midi-studio-tab="studio">Studio</button>
74-
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="song-setup">Song Setup</button>
73+
<button class="is-active" type="button" role="tab" aria-selected="true" data-midi-studio-tab="song-setup">Song Setup</button>
74+
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="studio">Octave Timeline</button>
7575
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="instruments">Instruments</button>
7676
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="auto-create-parts">Auto-Create Parts</button>
7777
<button type="button" role="tab" aria-selected="false" data-midi-studio-tab="midi-import">MIDI Import</button>
@@ -144,16 +144,17 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
144144
</div>
145145
</section>
146146

147-
<section class="accordion-v2 tool-starter__accordion tool-starter__accordion--fill is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="song-setup diagnostics">
147+
<section class="accordion-v2 tool-starter__accordion tool-starter__accordion--fill is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="song-setup">
148148
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="songDetailsContent">
149-
<span>Selected Song Details</span>
149+
<span>Song Setup</span>
150150
<span class="accordion-v2__icon" aria-hidden="true">+</span>
151151
</button>
152152
<div id="songDetailsContent" class="accordion-v2__content midi-studio-v2__selected-song-details">
153-
<dl id="songDetails" class="midi-studio-v2__details"></dl>
154-
<hr class="midi-studio-v2__details-divider">
155-
<section id="songSheetContent" class="midi-studio-v2__stack midi-studio-v2__song-sheet-panel" aria-label="Song Sheet">
156-
<h3>Song Sheet</h3>
153+
<div class="midi-studio-v2__song-setup-header">
154+
<h3 id="songSheetHeading">Song Sheet</h3>
155+
<button id="addSongButton" type="button">Add Song</button>
156+
</div>
157+
<section id="songSheetContent" class="midi-studio-v2__stack midi-studio-v2__song-sheet-panel" aria-labelledby="songSheetHeading">
157158
<div class="midi-studio-v2__song-sheet-grid">
158159
<label class="tool-starter__field" for="songSheetTempoInput">
159160
<span>Tempo/BPM</span>
@@ -195,6 +196,7 @@ <h3>Song Sheet</h3>
195196
<button id="parseSongSheetButton" type="button">Parse Guided Song Sheet</button>
196197
</div>
197198
</section>
199+
<dl id="songDetails" class="midi-studio-v2__details" hidden></dl>
198200
</div>
199201
</section>
200202

@@ -318,7 +320,7 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
318320
</div>
319321
</section>
320322

321-
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="song-setup diagnostics">
323+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="diagnostics">
322324
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="directorContent">
323325
<span>Game Music Director</span>
324326
<span class="accordion-v2__icon" aria-hidden="true">+</span>
@@ -328,7 +330,7 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
328330
</div>
329331
</section>
330332

331-
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="song-setup diagnostics">
333+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="diagnostics">
332334
<button class="accordion-v2__header midi-studio-v2__rendered-targets-header" type="button" aria-expanded="true" aria-controls="renderedTargetsContent">
333335
<span>Rendered Export Targets</span>
334336
<span class="accordion-v2__icon" aria-hidden="true">+</span>
@@ -371,7 +373,7 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
371373
</div>
372374
</section>
373375

374-
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="studio diagnostics">
376+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="studio song-setup diagnostics">
375377
<div class="accordion-v2__header tool-starter__status-accordion-header" aria-expanded="true" aria-controls="statusLogContent">
376378
<span>Status</span>
377379
<div class="tool-starter__status-header-actions">

tools/midi-studio-v2/js/MidiStudioV2App.js

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class MidiStudioV2App {
2424
serializer,
2525
shell,
2626
songList,
27+
songSetup,
2728
songSheet,
2829
songSheetParser,
2930
statusLog,
@@ -54,6 +55,7 @@ export class MidiStudioV2App {
5455
this.serializer = serializer;
5556
this.shell = shell;
5657
this.songList = songList;
58+
this.songSetup = songSetup;
5759
this.songSheet = songSheet;
5860
this.songSheetParser = songSheetParser;
5961
this.statusLog = statusLog;
@@ -67,8 +69,12 @@ export class MidiStudioV2App {
6769
this.accordions.forEach((accordion) => accordion.mount());
6870
this.statusLog.mount();
6971
this.songList.mount({ onSelect: (songId) => this.selectSong(songId) });
72+
this.songSetup.mount({ onAddSong: () => this.addSong() });
7073
this.details.mount({ onChange: (field, value) => this.handleSongDetailsChange(field, value) });
71-
this.songSheet.mount({ onParse: (sourceText) => this.parseSongSheet(sourceText) });
74+
this.songSheet.mount({
75+
onFieldChange: (field, value) => this.handleSongSheetFieldChange(field, value),
76+
onParse: (sourceText) => this.parseSongSheet(sourceText)
77+
});
7278
this.instrumentGrid.mount({
7379
onGenerate: (lane, input) => this.generateInstrumentLane(lane, input),
7480
onLaneSettingChange: (kind, detail) => this.handlePreviewLaneSettingChange(kind, detail),
@@ -187,7 +193,7 @@ export class MidiStudioV2App {
187193
this.render();
188194
this.applySelectedSongArrangement("active manifest song");
189195
this.statusLog.ok(`Loaded ${this.payload.songs.length} MIDI song${this.payload.songs.length === 1 ? "" : "s"} from ${sourceLabel} via ${normalized.sourceKind}.`);
190-
this.statusLog.info("Next: select a MIDI Studio song, review the Studio tab timeline, then press Play to audition the imported arrangement.");
196+
this.statusLog.info("Next: select a MIDI Studio song, review the Octave Timeline tab, then press Play to audition the imported arrangement.");
191197
return true;
192198
}
193199

@@ -247,6 +253,23 @@ export class MidiStudioV2App {
247253
this.updateAudioDiagnostics();
248254
}
249255

256+
handleSongSheetFieldChange(field, value) {
257+
if (field !== "key" && field !== "style") {
258+
return;
259+
}
260+
const song = this.selectedSong();
261+
const arrangement = song?.studioArrangement || null;
262+
if (!arrangement) {
263+
this.statusLog.warn(`Song Sheet ${field} was not applied because no editable arrangement is selected.`);
264+
return;
265+
}
266+
arrangement[field] = String(value || "").trim();
267+
this.details.showJson(song);
268+
this.markDirty({ changedKeys: ["data.songs.studioArrangement"], reason: "midi-studio-song-setup-edited" });
269+
this.statusLog.info(`Updated Song Sheet ${field} for ${song.name}.`);
270+
this.updateAudioDiagnostics();
271+
}
272+
250273
syncSongSheetFields(arrangement) {
251274
this.songSheet.applyGuidedDefaults({
252275
intro: this.songSheet.introInput.value,
@@ -355,6 +378,88 @@ export class MidiStudioV2App {
355378
this.updateAudioDiagnostics();
356379
}
357380

381+
addSong() {
382+
this.stopPlayback({ log: false });
383+
if (!this.payload) {
384+
this.payload = {
385+
activeSongId: "",
386+
directorMode: { enabled: false },
387+
runtimePreference: "live-midi",
388+
songs: [],
389+
version: 1
390+
};
391+
}
392+
const song = this.createAddedSong();
393+
this.payload.songs.push(song);
394+
this.payload.activeSongId = song.id;
395+
this.importedSongBaselines.set(song.id, deepClone(song));
396+
this.render();
397+
this.applySelectedSongArrangement("Add Song");
398+
this.markDirty({ changedKeys: ["data.songs"], reason: "midi-studio-song-added" });
399+
this.statusLog.ok(`Added MIDI song: ${song.name}. Canonical model now has ${this.payload.songs.length} song${this.payload.songs.length === 1 ? "" : "s"}.`);
400+
this.updateAudioDiagnostics();
401+
}
402+
403+
createAddedSong() {
404+
const source = this.selectedSong();
405+
const draftNumber = this.nextSongDraftNumber();
406+
return {
407+
defaultRuntimeFormat: source?.defaultRuntimeFormat || "ogg",
408+
director: {
409+
mood: "draft",
410+
intensity: "medium",
411+
usage: ["song-setup"],
412+
notes: "Created in MIDI Studio V2 Song Setup."
413+
},
414+
id: `new-song-${draftNumber}`,
415+
instrumentSet: source?.instrumentSet || "General MIDI",
416+
loop: source?.loop ? deepClone(source.loop) : { enabled: false, endSeconds: "", startSeconds: "" },
417+
name: `New Song ${draftNumber}`,
418+
rendered: { mp3: "", ogg: "", wav: "" },
419+
sourceMidi: "",
420+
studioArrangement: source?.studioArrangement ? deepClone(source.studioArrangement) : this.defaultAddedSongArrangement(),
421+
tags: ["draft"]
422+
};
423+
}
424+
425+
nextSongDraftNumber() {
426+
const existingIds = new Set((this.payload?.songs || []).map((song) => song.id));
427+
let draftNumber = (this.payload?.songs?.length || 0) + 1;
428+
while (existingIds.has(`new-song-${draftNumber}`)) {
429+
draftNumber += 1;
430+
}
431+
return draftNumber;
432+
}
433+
434+
defaultAddedSongArrangement() {
435+
return {
436+
beatsPerBar: "4",
437+
key: "C major",
438+
lanes: {
439+
bass: "C2 - G2 - | C2 - G2 -",
440+
chords: "C - G - | C - G -",
441+
drums: "kick hat snare hat | kick hat snare hat",
442+
lead: "E4 - G4 - | E4 - G4 -",
443+
pad: "C - G - | C - G -"
444+
},
445+
previewInstruments: {
446+
bass: "synth-bass",
447+
chords: "warm-pad",
448+
drums: "basic-drums",
449+
lead: "retro-pulse-lead",
450+
pad: "ambient-pad"
451+
},
452+
sections: "draft:2",
453+
songSheet: {
454+
intro: "C G",
455+
loop: "C G"
456+
},
457+
style: "retro-arcade",
458+
subdivision: "1",
459+
tempo: "120"
460+
};
461+
}
462+
358463
async playSelectedSong() {
359464
const song = this.selectedSong();
360465
if (song?.studioArrangement) {

tools/midi-studio-v2/js/bootstrap.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PlaybackControl } from "./controls/PlaybackControl.js";
1010
import { RenderedExportActionsControl } from "./controls/RenderedExportActionsControl.js";
1111
import { SongDetailsControl } from "./controls/SongDetailsControl.js";
1212
import { SongListControl } from "./controls/SongListControl.js";
13+
import { SongSetupControl } from "./controls/SongSetupControl.js";
1314
import { SongSheetControl } from "./controls/SongSheetControl.js";
1415
import { StatusLogControl } from "./controls/StatusLogControl.js";
1516
import { StudioTabsControl } from "./controls/StudioTabsControl.js";
@@ -126,6 +127,7 @@ window.addEventListener("DOMContentLoaded", () => {
126127
serializer: new MidiStudioStateSerializer(),
127128
shell: new ToolShellControl(),
128129
songList: new SongListControl({ list: requireElement("#songList") }),
130+
songSetup: new SongSetupControl({ addSongButton: requireElement("#addSongButton") }),
129131
songSheet: new SongSheetControl({
130132
introInput: requireElement("#songSheetIntroInput"),
131133
keyInput: requireElement("#songSheetKeyInput"),
@@ -139,6 +141,7 @@ window.addEventListener("DOMContentLoaded", () => {
139141
statusLog,
140142
studioTabs: new StudioTabsControl({
141143
buttons: Array.from(document.querySelectorAll("[data-midi-studio-tab]")),
144+
defaultTab: "song-setup",
142145
panels: Array.from(document.querySelectorAll("[data-midi-studio-tab-panel]"))
143146
}),
144147
windowRef: window

tools/midi-studio-v2/js/controls/SongDetailsControl.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class SongDetailsControl {
6363
return;
6464
}
6565
this.syncSourceFields(song);
66-
this.renderEditableDetails(song);
66+
this.renderCurrentDetails(song);
6767
this.renderDefinitionList(this.renderedTargets, [
6868
["WAV", song.rendered.wav || "No rendered WAV target declared."],
6969
["MP3", song.rendered.mp3 || "No rendered MP3 target declared."],
@@ -74,6 +74,12 @@ export class SongDetailsControl {
7474

7575
renderEmptyDetails(payload) {
7676
this.details.replaceChildren();
77+
this.details.classList.remove("midi-studio-v2__editable-details");
78+
if (this.details.hidden) {
79+
this.renderDefinitionList(this.renderedTargets, [["WAV", "No rendered WAV target selected."], ["MP3", "No rendered MP3 target selected."], ["OGG", "No rendered OGG target selected."]]);
80+
this.inspector.textContent = JSON.stringify(payload || {}, null, 2);
81+
return;
82+
}
7783
const empty = document.createElement("p");
7884
empty.className = "tool-starter__hint";
7985
empty.textContent = "No song selected.";
@@ -82,6 +88,15 @@ export class SongDetailsControl {
8288
this.inspector.textContent = JSON.stringify(payload || {}, null, 2);
8389
}
8490

91+
renderCurrentDetails(song) {
92+
if (this.details.hidden) {
93+
this.details.replaceChildren();
94+
this.details.classList.remove("midi-studio-v2__editable-details");
95+
return;
96+
}
97+
this.renderEditableDetails(song);
98+
}
99+
85100
renderEditableDetails(song) {
86101
this.details.replaceChildren();
87102
this.details.classList.add("midi-studio-v2__editable-details");
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class SongSetupControl {
2+
constructor({ addSongButton }) {
3+
this.addSongButton = addSongButton;
4+
}
5+
6+
mount({ onAddSong = () => {} } = {}) {
7+
this.addSongButton.addEventListener("click", () => onAddSong());
8+
}
9+
}

tools/midi-studio-v2/js/controls/SongSheetControl.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ export class SongSheetControl {
2828
this.tempoInput = tempoInput;
2929
}
3030

31-
mount({ onParse }) {
31+
mount({ onFieldChange = () => {}, onParse }) {
3232
this.parseButton.addEventListener("click", () => onParse(this.composeGuidedSheet()));
33+
this.keyInput.addEventListener("change", () => onFieldChange("key", this.keyInput.value));
34+
this.styleInput.addEventListener("change", () => onFieldChange("style", this.styleInput.value));
3335
}
3436

3537
applyGuidedDefaults({ intro, key, loop, style, tempo }) {

0 commit comments

Comments
 (0)