Skip to content

Commit 3193311

Browse files
committed
Simplify MIDI Studio V2 Song Sheet fields and move export actions into header - PR_26146_010-midi-studio-v2-guided-song-sheet-fields
1 parent e68a3ca commit 3193311

8 files changed

Lines changed: 282 additions & 42 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# PR_26146_010-midi-studio-v2-guided-song-sheet-fields Validation
2+
3+
## Summary
4+
- Added guided MIDI Studio V2 Song Sheet fields for tempo/BPM, key, style, intro progression, and loop progression.
5+
- Guided fields compose into the existing shared SongSheetParser source text contract before parsing.
6+
- Kept advanced raw Song Sheet parsing available behind the existing raw textarea path.
7+
- Moved Rendered Export Targets actions into the section header and added explicit WAV/MP3/OGG export planning buttons.
8+
- Export buttons report WARN for unimplemented rendering, FAIL for missing song/target, and do not claim files are written.
9+
10+
## Lanes Executed
11+
- Runtime/tool: MIDI Studio V2 UI controls, Song Sheet parsing flow, rendered preview, MIDI inspection, and export action status.
12+
- Contract/static: external-only HTML/JS/CSS guard and changed-file syntax checks.
13+
14+
## Lanes Skipped
15+
- Workspace Manager V2 registration/handoff: SKIP because this PR did not touch Workspace Manager registration or handoff files.
16+
- Samples: SKIP because sample JSON alignment is out of scope.
17+
- Full samples smoke: SKIP per BUILD instruction.
18+
19+
## Validation Commands
20+
- PASS: `node --check tools/midi-studio-v2/js/MidiStudioV2App.js`
21+
- PASS: `node --check tools/midi-studio-v2/js/bootstrap.js`
22+
- PASS: `node --check tools/midi-studio-v2/js/controls/SongSheetControl.js`
23+
- PASS: `node --check tools/midi-studio-v2/js/controls/RenderedExportActionsControl.js`
24+
- PASS: `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
25+
- PASS: `Select-String -Path tools/midi-studio-v2/index.html -Pattern '<script(?![^>]*src=)|<style|\son[a-z]+\s*='`
26+
- PASS: `npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright --workers=1 --reporter=line`
27+
- PASS: `git diff --check`
28+
29+
## Playwright Coverage
30+
- PASS: 17 targeted MIDI Studio V2 tests.
31+
- Covered guided tempo/key/style/intro/loop entry.
32+
- Covered guided fields normalizing into the existing Song Sheet summary.
33+
- Covered invalid tempo and missing key failures before parser render.
34+
- Covered invalid chord warnings and empty intro/loop warnings.
35+
- Covered raw advanced malformed syntax rejection without partial section render.
36+
- Covered Rendered Export Targets header placement and WAV/MP3/OGG action status.
37+
- Covered missing rendered target and missing selected song export failures.
38+
- Covered existing MIDI source inspection, rendered preview behavior, header actions, accordion behavior, and invalid payload rejection.
39+
40+
## Notes
41+
- Two early Playwright attempts timed out while debugging new test paths. The causes were a wrapped header flex-basis issue that let rendered target content intercept export button clicks, and a raw Song Sheet test attempting to interact with a closed advanced `<details>` section. Both were fixed before the final passing run.
42+
- Browser availability was already satisfied through `PLAYWRIGHT_BROWSERS_PATH=$env:TEMP\ms-playwright`; no Chromium install was required in this run.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ async function openMidiStudio(page, routePayload = validManifest, midiRoutes = {
184184
return server;
185185
}
186186

187+
async function fillGuidedSongSheet(page, { intro = "Am F", key = "A minor", loop = "Am F C G", style = "retro-arcade", tempo = "132" } = {}) {
188+
await page.locator("#songSheetTempoInput").fill(tempo);
189+
await page.locator("#songSheetKeyInput").fill(key);
190+
await page.locator("#songSheetStyleInput").fill(style);
191+
await page.locator("#songSheetIntroInput").fill(intro);
192+
await page.locator("#songSheetLoopInput").fill(loop);
193+
}
194+
187195
test.describe("MIDI Studio V2", () => {
188196
test.afterAll(async () => {
189197
await workspaceV2CoverageReporter.writeReport();
@@ -202,6 +210,11 @@ test.describe("MIDI Studio V2", () => {
202210
await expect(page.locator("#directorPanel")).toContainText("heroic");
203211
await expect(page.locator("#playButton")).toHaveText("Play Rendered Preview");
204212
await expect(page.locator("#inspectMidiSourceButton")).toBeEnabled();
213+
const renderedHeader = page.locator('.accordion-v2__header[aria-controls="renderedTargetsContent"]');
214+
await expect(renderedHeader).toContainText("Rendered Export Targets");
215+
await expect(renderedHeader.locator("#exportWavButton")).toBeVisible();
216+
await expect(renderedHeader.locator("#exportMp3Button")).toBeVisible();
217+
await expect(renderedHeader.locator("#exportOggButton")).toBeVisible();
205218
await expect(page.locator("#midiSourceDetails")).toContainText("No MIDI source inspected.");
206219
await expect(page.locator("#playbackState")).toContainText("Live MIDI synthesis: NOT IMPLEMENTED");
207220
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded 3 MIDI songs/);
@@ -229,6 +242,24 @@ test.describe("MIDI Studio V2", () => {
229242
}
230243
});
231244

245+
test("reports rendered export header action status without claiming files were written", async ({ page }) => {
246+
const server = await openMidiStudio(page);
247+
try {
248+
await page.locator("#exportWavButton").click();
249+
await page.locator("#exportMp3Button").click();
250+
await page.locator("#exportOggButton").click();
251+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for WAV\. Planned target: assets\/music\/rendered\/theme-main\.wav\./);
252+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for MP3\. Planned target: assets\/music\/rendered\/theme-main\.mp3\./);
253+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for OGG\. Planned target: assets\/music\/rendered\/theme-main\.ogg\./);
254+
await page.locator('[data-song-id="source-only"]').click();
255+
await page.locator("#exportWavButton").click();
256+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Missing rendered WAV export target for Source Only\. Add music\.songs\[\]\.rendered\.wav before exporting\./);
257+
} finally {
258+
await workspaceV2CoverageReporter.stop(page);
259+
await server.close();
260+
}
261+
});
262+
232263
test("loads selected MIDI source metadata on request", async ({ page }) => {
233264
const server = await openMidiStudio(page, validManifest, {
234265
"assets/music/midi/theme-main.mid": validMidiBytes
@@ -335,18 +366,10 @@ test.describe("MIDI Studio V2", () => {
335366
}
336367
});
337368

338-
test("parses a valid Song Sheet into section summary metadata", async ({ page }) => {
369+
test("parses guided Song Sheet fields into section summary metadata", async ({ page }) => {
339370
const server = await openMidiStudio(page);
340371
try {
341-
await page.locator("#songSheetInput").fill(`tempo=132
342-
key=A minor
343-
style=retro-arcade
344-
345-
[intro]
346-
Am F
347-
348-
[loop]
349-
Am F C G`);
372+
await fillGuidedSongSheet(page);
350373
await page.locator("#parseSongSheetButton").click();
351374
await expect(page.locator("#songSheetSummary")).toContainText("intro: 2 bars, 2 chords");
352375
await expect(page.locator("#songSheetSummary")).toContainText("loop: 4 bars, 4 chords, loop");
@@ -370,38 +393,62 @@ Am F C G`);
370393
}
371394
});
372395

373-
test("shows Song Sheet warnings for invalid chords and empty sections", async ({ page }) => {
396+
test("shows guided Song Sheet warnings for invalid chords and empty intro loop fields", async ({ page }) => {
374397
const server = await openMidiStudio(page);
375398
try {
376-
await page.locator("#songSheetInput").fill(`tempo=120
377-
key=C major
378-
style=chip
379-
380-
[loop]
381-
C Hm G
382-
383-
[break]`);
399+
await fillGuidedSongSheet(page, {
400+
intro: "",
401+
key: "C major",
402+
loop: "C Hm G",
403+
style: "chip",
404+
tempo: "120"
405+
});
384406
await page.locator("#parseSongSheetButton").click();
385407
await expect(page.locator("#songSheetSummary")).toContainText('Invalid chord "Hm"');
386-
await expect(page.locator("#songSheetSummary")).toContainText("Section break is empty.");
408+
await expect(page.locator("#songSheetSummary")).toContainText("Section intro is empty.");
387409
await expect(page.locator("#songSheetSummary")).toContainText("loop: 2 bars, 2 chords, loop");
388410
await expect(page.locator("#statusLog")).toHaveValue(/WARN Song Sheet parsed with warnings: Invalid chord "Hm" in section loop/);
389411
await expect(page.locator("#statusLog")).toHaveValue(/OK Song Sheet parsed: 2 sections, 2 bars, 2 chords\./);
412+
await page.locator("#songSheetLoopInput").fill("");
413+
await page.locator("#parseSongSheetButton").click();
414+
await expect(page.locator("#songSheetSummary")).toContainText("Section loop is empty.");
415+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Song Sheet parsed with warnings: Section intro is empty\.; Section loop is empty\./);
390416
} finally {
391417
await workspaceV2CoverageReporter.stop(page);
392418
await server.close();
393419
}
394420
});
395421

396-
test("rejects malformed Song Sheet syntax without partial section summary", async ({ page }) => {
422+
test("rejects invalid guided Song Sheet tempo and missing key before parser render", async ({ page }) => {
397423
const server = await openMidiStudio(page);
398424
try {
425+
await fillGuidedSongSheet(page, { tempo: "-1" });
426+
await page.locator("#parseSongSheetButton").click();
427+
await expect(page.locator("#songSheetSummary")).toContainText("Invalid tempo/BPM. Enter a positive number before parsing the guided Song Sheet.");
428+
await expect(page.locator("#songSheetSummary")).not.toContainText("intro:");
429+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Song Sheet rejected: Invalid tempo\/BPM\. Enter a positive number before parsing the guided Song Sheet\./);
430+
await page.locator("#songSheetTempoInput").fill("132");
431+
await page.locator("#songSheetKeyInput").fill("");
432+
await page.locator("#parseSongSheetButton").click();
433+
await expect(page.locator("#songSheetSummary")).toContainText("Missing key. Enter a key before parsing the guided Song Sheet.");
434+
await expect(page.locator("#songSheetSummary")).not.toContainText("loop:");
435+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Song Sheet rejected: Missing key\. Enter a key before parsing the guided Song Sheet\./);
436+
} finally {
437+
await workspaceV2CoverageReporter.stop(page);
438+
await server.close();
439+
}
440+
});
441+
442+
test("rejects malformed raw Song Sheet syntax without partial section summary", async ({ page }) => {
443+
const server = await openMidiStudio(page);
444+
try {
445+
await page.locator(".midi-studio-v2__advanced-song-sheet summary").click();
399446
await page.locator("#songSheetInput").fill(`tempo:132
400447
key=A minor
401448
402449
[loop]
403450
Am F`);
404-
await page.locator("#parseSongSheetButton").click();
451+
await page.locator("#parseRawSongSheetButton").click();
405452
await expect(page.locator("#songSheetSummary")).toContainText("Unsupported Song Sheet syntax on line 1: tempo:132");
406453
await expect(page.locator("#songSheetSummary")).not.toContainText("loop:");
407454
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Song Sheet rejected: Unsupported Song Sheet syntax on line 1: tempo:132/);
@@ -416,12 +463,7 @@ Am F`);
416463
"assets/music/midi/theme-main.mid": validMidiBytes
417464
});
418465
try {
419-
await page.locator("#songSheetInput").fill(`tempo=132
420-
key=A minor
421-
style=retro-arcade
422-
423-
[loop]
424-
Am F C G`);
466+
await fillGuidedSongSheet(page, { intro: "", loop: "Am F C G" });
425467
await page.locator("#parseSongSheetButton").click();
426468
await expect(page.locator("#songSheetSummary")).toContainText("loop: 4 bars, 4 chords, loop");
427469
await page.locator("#inspectMidiSourceButton").click();
@@ -535,6 +577,8 @@ Am F C G`);
535577
await expect(page.locator("#songList")).toContainText("No MIDI songs loaded.");
536578
await expect(page.locator("#playButton")).toBeDisabled();
537579
await expect(page.locator("#statusLog")).toHaveValue(/FAIL MIDI Studio V2 payload rejected before render .* music\.songs\[0\]\.id is required\./);
580+
await page.locator("#exportWavButton").click();
581+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Missing MIDI song for WAV export\. Load or select a song before exporting\./);
538582
} finally {
539583
await workspaceV2CoverageReporter.stop(page);
540584
await server.close();

tools/midi-studio-v2/index.html

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,35 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
8787
<span class="accordion-v2__icon" aria-hidden="true">+</span>
8888
</button>
8989
<div id="songSheetContent" class="accordion-v2__content midi-studio-v2__stack">
90-
<label class="tool-starter__field" for="songSheetInput">
91-
<span>Structured song sheet</span>
92-
<textarea id="songSheetInput" class="midi-studio-v2__song-sheet-input" rows="9" placeholder="tempo=132&#10;key=A minor&#10;style=retro-arcade&#10;&#10;[intro]&#10;Am F&#10;&#10;[loop]&#10;Am F C G"></textarea>
90+
<label class="tool-starter__field" for="songSheetTempoInput">
91+
<span>Tempo/BPM</span>
92+
<input id="songSheetTempoInput" type="number" min="1" step="1" inputmode="numeric" placeholder="132">
9393
</label>
94-
<button id="parseSongSheetButton" type="button">Parse Song Sheet</button>
94+
<label class="tool-starter__field" for="songSheetKeyInput">
95+
<span>Key</span>
96+
<input id="songSheetKeyInput" type="text" placeholder="A minor">
97+
</label>
98+
<label class="tool-starter__field" for="songSheetStyleInput">
99+
<span>Style</span>
100+
<input id="songSheetStyleInput" type="text" placeholder="retro-arcade">
101+
</label>
102+
<label class="tool-starter__field" for="songSheetIntroInput">
103+
<span>Intro chord progression</span>
104+
<input id="songSheetIntroInput" type="text" placeholder="Am F">
105+
</label>
106+
<label class="tool-starter__field" for="songSheetLoopInput">
107+
<span>Loop chord progression</span>
108+
<input id="songSheetLoopInput" type="text" placeholder="Am F C G">
109+
</label>
110+
<button id="parseSongSheetButton" type="button">Parse Guided Song Sheet</button>
111+
<details class="midi-studio-v2__advanced-song-sheet">
112+
<summary>Advanced raw song sheet</summary>
113+
<label class="tool-starter__field" for="songSheetInput">
114+
<span>Structured song sheet</span>
115+
<textarea id="songSheetInput" class="midi-studio-v2__song-sheet-input" rows="9" placeholder="tempo=132&#10;key=A minor&#10;style=retro-arcade&#10;&#10;[intro]&#10;Am F&#10;&#10;[loop]&#10;Am F C G"></textarea>
116+
</label>
117+
<button id="parseRawSongSheetButton" type="button">Parse Raw Song Sheet</button>
118+
</details>
95119
</div>
96120
</section>
97121
</aside>
@@ -158,10 +182,15 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
158182
</section>
159183

160184
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true">
161-
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="renderedTargetsContent">
185+
<div class="accordion-v2__header midi-studio-v2__rendered-targets-header" aria-expanded="true" aria-controls="renderedTargetsContent">
162186
<span>Rendered Export Targets</span>
163-
<span class="accordion-v2__icon" aria-hidden="true">+</span>
164-
</button>
187+
<div class="midi-studio-v2__header-actions" aria-label="Rendered export actions">
188+
<button id="exportWavButton" type="button">Export WAV</button>
189+
<button id="exportMp3Button" type="button">Export MP3</button>
190+
<button id="exportOggButton" type="button">Export OGG</button>
191+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
192+
</div>
193+
</div>
165194
<div id="renderedTargetsContent" class="accordion-v2__content">
166195
<dl id="renderedTargets" class="midi-studio-v2__details"></dl>
167196
</div>

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class MidiStudioV2App {
1313
midiSourceInspection,
1414
playback,
1515
playbackControl,
16+
renderedExportActions,
1617
serializer,
1718
shell,
1819
songList,
@@ -31,6 +32,7 @@ export class MidiStudioV2App {
3132
this.payload = null;
3233
this.playback = playback;
3334
this.playbackControl = playbackControl;
35+
this.renderedExportActions = renderedExportActions;
3436
this.selectedSongId = "";
3537
this.serializer = serializer;
3638
this.shell = shell;
@@ -52,6 +54,7 @@ export class MidiStudioV2App {
5254
onPlay: () => this.playSelectedSong(),
5355
onStop: () => this.stopPlayback()
5456
});
57+
this.renderedExportActions.mount({ onExport: (format) => this.exportRenderedTarget(format) });
5558
this.actionNav.mount({
5659
onToolCopyJson: () => this.copyJson(),
5760
onToolExportToolState: () => this.exportToolState(),
@@ -154,8 +157,8 @@ export class MidiStudioV2App {
154157
this.statusLog.ok(`MIDI source inspected for ${song.name}: format ${result.format}, ${result.trackCount} track${result.trackCount === 1 ? "" : "s"}, ${result.ticksPerQuarterNote} TPQN.`);
155158
}
156159

157-
parseSongSheet(sourceText) {
158-
const result = this.songSheetParser.parse(sourceText);
160+
parseSongSheet(request) {
161+
const result = request?.ok === false ? request : this.songSheetParser.parse(request?.sourceText || request);
159162
this.songSheet.render(result);
160163
if (!result.ok) {
161164
this.statusLog.fail(`Song Sheet rejected: ${result.message}`);
@@ -167,6 +170,21 @@ export class MidiStudioV2App {
167170
this.statusLog.ok(`Song Sheet parsed: ${result.sections.length} section${result.sections.length === 1 ? "" : "s"}, ${result.bars} bars, ${result.chordCount} chords.`);
168171
}
169172

173+
exportRenderedTarget(format) {
174+
const song = this.selectedSong();
175+
const label = String(format || "").toUpperCase();
176+
if (!song) {
177+
this.statusLog.fail(`Missing MIDI song for ${label} export. Load or select a song before exporting.`);
178+
return;
179+
}
180+
const target = String(song.rendered?.[format] || "").trim();
181+
if (!target) {
182+
this.statusLog.fail(`Missing rendered ${label} export target for ${song.name}. Add music.songs[].rendered.${format} before exporting.`);
183+
return;
184+
}
185+
this.statusLog.warn(`Export rendering not implemented for ${label}. Planned target: ${target}.`);
186+
}
187+
170188
stopPlayback() {
171189
this.playback.stop();
172190
this.playbackControl.setStopped(this.selectedSong());

0 commit comments

Comments
 (0)