Skip to content

Commit 29a1c0d

Browse files
committed
Build first-class MIDI Studio V2 tool with multi-song manifest playback surface - PR_26146_003-build-midi-tools-v2
1 parent e4cbaf8 commit 29a1c0d

24 files changed

Lines changed: 1537 additions & 12 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# PR_26146_003-build-midi-tools-v2 Validation
2+
3+
## Changed Files
4+
5+
- `tools/midi-studio-v2/index.html`
6+
- `tools/midi-studio-v2/README.md`
7+
- `tools/midi-studio-v2/styles/midiStudioV2.css`
8+
- `tools/midi-studio-v2/js/MidiStudioV2App.js`
9+
- `tools/midi-studio-v2/js/bootstrap.js`
10+
- `tools/midi-studio-v2/js/controls/*.js`
11+
- `tools/midi-studio-v2/js/services/*.js`
12+
- `tools/schemas/tools/midi-studio-v2.schema.json`
13+
- `tools/schemas/game.manifest.schema.json`
14+
- `tools/toolRegistry.js`
15+
- `tools/renderToolsIndex.js`
16+
- `tools/index.html`
17+
- `tools/workspace-manager-v2/js/services/WorkspaceManagerV2ContextService.js`
18+
- `tests/playwright/tools/MidiStudioV2.spec.mjs`
19+
- `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
20+
- `docs/dev/codex_commands.md`
21+
- `docs/dev/commit_comment.txt`
22+
- `docs/dev/reports/codex_review.diff`
23+
- `docs/dev/reports/codex_changed_files.txt`
24+
25+
## Scope
26+
27+
Built MIDI Studio V2 as a first-class Workspace V2 tool under `tools/midi-studio-v2/`.
28+
29+
The tool preserves the Tool Template V2 header, NAV, left/center/right panels, accordions, status/logging, external CSS, external JavaScript, and HTML restrictions. It does not add MIDI input, recording, a DAW workflow, piano-roll editing, hidden fallback songs, hidden default manifests, or silent sample data.
30+
31+
## Validation Lanes
32+
33+
- contract - executed through schema parsing, manifest/toolState serializer checks, HTML restriction scan, and Workspace Manager V2 registration updates.
34+
- runtime - partially executed through changed-file syntax checks and targeted serializer validation.
35+
- integration - attempted through targeted MIDI Studio V2 Playwright and `npm run test:workspace-v2`; blocked by missing local Playwright Chromium browser.
36+
- engine - skipped for code changes; existing `src/engine/audio/MediaTrackService.js` was reused and not modified.
37+
- samples - SKIP because sample JSON alignment is out of scope.
38+
- recovery/UAT - not directly changed beyond Workspace V2 tool registration/handoff.
39+
40+
## Commands
41+
42+
- `node --check` on changed JavaScript and Playwright files.
43+
- JSON parse check for `tools/schemas/tools/midi-studio-v2.schema.json` and `tools/schemas/game.manifest.schema.json`.
44+
- HTML inline restriction scan for `tools/midi-studio-v2/index.html`.
45+
- Targeted serializer validation with Node ESM import.
46+
- `git diff --check`
47+
- `npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright --workers=1 --reporter=line`
48+
- `npm.cmd run test:workspace-v2`
49+
50+
## Results
51+
52+
- Changed-file JavaScript syntax checks: PASS.
53+
- JSON schema parse checks: PASS.
54+
- HTML inline script/style/event handler scan: PASS.
55+
- Serializer valid/invalid payload check: PASS.
56+
- `git diff --check`: PASS with line-ending warnings only for existing tracked files.
57+
- Targeted MIDI Studio V2 Playwright: BLOCKED. Playwright could not launch because local Chromium is missing at `C:\Users\DavidQ\AppData\Local\ms-playwright\chromium-1217\chrome-win64\chrome.exe`.
58+
- `npm run test:workspace-v2`: BLOCKED/TIMEOUT. The lane reached Playwright execution and then hit the same missing local Chromium browser across Workspace Manager V2 tests.
59+
- Playwright V8 coverage: generated advisory report at `docs/dev/reports/playwright_v8_coverage_report.txt`; changed runtime JS files are reported as WARN/uncovered because Chromium could not launch.
60+
61+
## Playwright Coverage Added
62+
63+
Targeted MIDI Studio V2 tests cover:
64+
65+
- tool launch;
66+
- valid multi-song manifest render;
67+
- selecting multiple songs;
68+
- missing MIDI source failure status;
69+
- play/stop control state with mocked `Audio`;
70+
- invalid payload rejection before render.
71+
72+
Expected pass behavior:
73+
74+
- Valid manifest-owned `music.songs` metadata renders without hidden fallback data, song selection updates details, missing source fails visibly, and mocked play/stop transitions button state.
75+
76+
Expected fail behavior:
77+
78+
- Invalid song payloads reject before render and leave the song list empty.
79+
80+
## Manual Validation
81+
82+
1. Open `tools/midi-studio-v2/index.html`.
83+
2. Import a game manifest with root `music.songs`.
84+
3. Confirm the Songs list shows every song.
85+
4. Select each song and confirm source, instrument set, rendered WAV/MP3/OGG targets, details, and Game Music Director rows update.
86+
5. Select a song with an empty `sourceMidi` and click Play.
87+
6. Confirm the status log shows a FAIL message telling the user to add `music.songs[].sourceMidi`.
88+
7. Select a song with a `.mid` source and use Play, Stop, and Loop.
89+
90+
Expected outcome:
91+
92+
- Valid payloads render, invalid payloads reject before render, and playback failures are visible and actionable.
93+
94+
## Samples Decision
95+
96+
SKIP. Sample JSON alignment is out of scope, and this PR does not claim sample launch compatibility.
97+
98+
## Src Follow-Up
99+
100+
Existing `src/engine/audio/MediaTrackService.js` supports media-track preview attempts and is reused for the first MIDI Studio V2 play/stop surface. A broader shared `src/` MIDI parsing/synthesis/rendered export capability does not exist yet. Future PRs should add shared `src/` MIDI capability before implementing live MIDI synthesis, MIDI parsing, or rendered WAV/MP3/OGG export pipelines.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { expect, test } from "@playwright/test";
2+
import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs";
3+
import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs";
4+
5+
const validManifest = {
6+
schema: "html-js-gaming.game-manifest",
7+
version: 1,
8+
game: { id: "MidiFixture", name: "MIDI Fixture", folder: "MidiFixture" },
9+
launch: { directPath: "games/MidiFixture/index.html" },
10+
tools: {
11+
"asset-manager-v2": { version: 1, assets: {} },
12+
"midi-studio-v2": {
13+
activeSongId: "theme-main",
14+
directorMode: { enabled: true, defaultIntensity: "medium" }
15+
}
16+
},
17+
music: {
18+
version: 1,
19+
runtimePreference: "rendered",
20+
songs: [
21+
{
22+
id: "theme-main",
23+
name: "Main Theme",
24+
sourceMidi: "assets/music/midi/theme-main.mid",
25+
instrumentSet: "General MIDI",
26+
rendered: {
27+
wav: "assets/music/rendered/theme-main.wav",
28+
mp3: "assets/music/rendered/theme-main.mp3",
29+
ogg: "assets/music/rendered/theme-main.ogg"
30+
},
31+
defaultRuntimeFormat: "ogg",
32+
loop: { enabled: true, startSeconds: 1, endSeconds: 64 },
33+
director: {
34+
mood: "heroic",
35+
intensity: "medium",
36+
usage: ["title", "menu"],
37+
notes: "Opening cue."
38+
},
39+
tags: ["theme", "menu"]
40+
},
41+
{
42+
id: "combat-light",
43+
name: "Light Combat",
44+
sourceMidi: "",
45+
instrumentSet: { id: "gm-combat", name: "Combat GM" },
46+
rendered: {
47+
wav: "assets/music/rendered/combat-light.wav",
48+
mp3: "assets/music/rendered/combat-light.mp3",
49+
ogg: "assets/music/rendered/combat-light.ogg"
50+
},
51+
defaultRuntimeFormat: "ogg",
52+
loop: { enabled: true },
53+
director: {
54+
mood: "tense",
55+
intensity: "high",
56+
usage: ["combat"],
57+
notes: "Short encounter loop."
58+
},
59+
tags: ["combat"]
60+
}
61+
]
62+
}
63+
};
64+
65+
function installMockAudio(page) {
66+
return page.addInitScript(() => {
67+
window.__midiStudioAudioEvents = [];
68+
window.Audio = class MockAudio {
69+
constructor(src) {
70+
this.currentTime = 0;
71+
this.duration = 12;
72+
this.loop = false;
73+
this.paused = true;
74+
this.preload = "";
75+
this.src = src;
76+
this.volume = 1;
77+
}
78+
79+
play() {
80+
this.paused = false;
81+
window.__midiStudioAudioEvents.push({ action: "play", loop: this.loop, src: this.src });
82+
return Promise.resolve();
83+
}
84+
85+
pause() {
86+
this.paused = true;
87+
window.__midiStudioAudioEvents.push({ action: "pause", src: this.src });
88+
}
89+
};
90+
});
91+
}
92+
93+
async function openMidiStudio(page, routePayload = validManifest) {
94+
const server = await startRepoServer();
95+
await installMockAudio(page);
96+
await page.route("**/midi-fixture.game.manifest.json", async (route) => {
97+
await route.fulfill({
98+
contentType: "application/json",
99+
body: JSON.stringify(routePayload)
100+
});
101+
});
102+
await workspaceV2CoverageReporter.start(page);
103+
await page.goto(`${server.baseUrl}/tools/midi-studio-v2/index.html?manifestPath=/midi-fixture.game.manifest.json`, { waitUntil: "networkidle" });
104+
return server;
105+
}
106+
107+
test.describe("MIDI Studio V2", () => {
108+
test.afterAll(async () => {
109+
await workspaceV2CoverageReporter.writeReport();
110+
});
111+
112+
test("launches and renders a valid multi-song manifest payload", async ({ page }) => {
113+
const server = await openMidiStudio(page);
114+
try {
115+
await expect(page.locator("body[data-tool-id='midi-studio-v2']")).toBeVisible();
116+
await expect(page.locator("[data-midi-studio-header]")).toContainText("MIDI Studio V2");
117+
await expect(page.locator("#songList [data-song-id]")).toHaveCount(2);
118+
await expect(page.locator('[data-song-id="theme-main"]')).toHaveAttribute("aria-pressed", "true");
119+
await expect(page.locator("#songSourceField")).toHaveValue("assets/music/midi/theme-main.mid");
120+
await expect(page.locator("#instrumentSetField")).toHaveValue("General MIDI");
121+
await expect(page.locator("#renderedTargets")).toContainText("theme-main.ogg");
122+
await expect(page.locator("#directorPanel")).toContainText("heroic");
123+
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded 2 MIDI songs/);
124+
} finally {
125+
await workspaceV2CoverageReporter.stop(page);
126+
await server.close();
127+
}
128+
});
129+
130+
test("selects multiple songs and updates source, director, and rendered targets", async ({ page }) => {
131+
const server = await openMidiStudio(page);
132+
try {
133+
await page.locator('[data-song-id="combat-light"]').click();
134+
await expect(page.locator('[data-song-id="combat-light"]')).toHaveAttribute("aria-pressed", "true");
135+
await expect(page.locator("#songSourceField")).toHaveValue("missing sourceMidi");
136+
await expect(page.locator("#instrumentSetField")).toHaveValue("Combat GM");
137+
await expect(page.locator("#renderedTargets")).toContainText("combat-light.mp3");
138+
await expect(page.locator("#directorPanel")).toContainText("Short encounter loop.");
139+
await page.locator('[data-song-id="theme-main"]').click();
140+
await expect(page.locator("#songSourceField")).toHaveValue("assets/music/midi/theme-main.mid");
141+
} finally {
142+
await workspaceV2CoverageReporter.stop(page);
143+
await server.close();
144+
}
145+
});
146+
147+
test("shows actionable failure for a selected song with missing MIDI source", async ({ page }) => {
148+
const server = await openMidiStudio(page);
149+
try {
150+
await page.locator('[data-song-id="combat-light"]').click();
151+
await page.locator("#playButton").click();
152+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Missing MIDI source for Light Combat\. Add music\.songs\[\]\.sourceMidi in game\.manifest\.json\./);
153+
await expect(page.locator("#stopButton")).toBeDisabled();
154+
await expect(page.locator("#playButton")).toBeEnabled();
155+
} finally {
156+
await workspaceV2CoverageReporter.stop(page);
157+
await server.close();
158+
}
159+
});
160+
161+
test("updates play and stop control state without requiring real audio output", async ({ page }) => {
162+
const server = await openMidiStudio(page);
163+
try {
164+
await page.locator("#loopToggle").check();
165+
await page.locator("#playButton").click();
166+
await expect(page.locator("#playButton")).toBeDisabled();
167+
await expect(page.locator("#stopButton")).toBeEnabled();
168+
await expect(page.locator("#playbackState")).toContainText("Playing preview: Main Theme");
169+
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview play started for Main Theme/);
170+
expect(await page.evaluate(() => window.__midiStudioAudioEvents)).toEqual([
171+
{ action: "play", loop: true, src: "assets/music/midi/theme-main.mid" }
172+
]);
173+
await page.locator("#stopButton").click();
174+
await expect(page.locator("#stopButton")).toBeDisabled();
175+
await expect(page.locator("#playButton")).toBeEnabled();
176+
await expect(page.locator("#playbackState")).toContainText("Stopped: Main Theme");
177+
} finally {
178+
await workspaceV2CoverageReporter.stop(page);
179+
await server.close();
180+
}
181+
});
182+
183+
test("rejects invalid payloads before render", async ({ page }) => {
184+
const server = await openMidiStudio(page, {
185+
music: {
186+
version: 1,
187+
songs: [{ name: "Broken Song" }]
188+
}
189+
});
190+
try {
191+
await expect(page.locator("#songList")).toContainText("No MIDI songs loaded.");
192+
await expect(page.locator("#playButton")).toBeDisabled();
193+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL MIDI Studio V2 payload rejected before render .* music\.songs\[0\]\.id is required\./);
194+
} finally {
195+
await workspaceV2CoverageReporter.stop(page);
196+
await server.close();
197+
}
198+
});
199+
});

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,10 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13061306
await expect(audioSfxCard).toBeVisible();
13071307
await expect(audioSfxCard.locator("a", { hasText: "Audio / SFX Playground V2" })).toHaveAttribute("href", "/tools/audio-sfx-playground-v2/index.html");
13081308
await expect(audioSfxCard).toContainText("oscillator and noise-based game sound effects");
1309+
const midiStudioCard = page.locator(".tools-platform-card", { has: page.locator("h3", { hasText: "MIDI Studio V2" }) });
1310+
await expect(midiStudioCard).toBeVisible();
1311+
await expect(midiStudioCard.locator("a", { hasText: "MIDI Studio V2" })).toHaveAttribute("href", "/tools/midi-studio-v2/index.html");
1312+
await expect(midiStudioCard).toContainText("multi-song MIDI metadata");
13091313
const toolsIndexState = await page.evaluate(async () => {
13101314
const registryModule = await import("/tools/toolRegistry.js");
13111315
const launchModule = await import("/tools/shared/toolLaunchSSoTData.js");
@@ -1338,7 +1342,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13381342
world: launchModule.getSampleToolLaunchDefinition("world-vector-studio-v2"),
13391343
object: launchModule.getSampleToolLaunchDefinition("object-vector-studio-v2"),
13401344
inputMapping: launchModule.getSampleToolLaunchDefinition("input-mapping-v2"),
1341-
audioSfx: launchModule.getSampleToolLaunchDefinition("audio-sfx-playground-v2")
1345+
audioSfx: launchModule.getSampleToolLaunchDefinition("audio-sfx-playground-v2"),
1346+
midiStudio: launchModule.getSampleToolLaunchDefinition("midi-studio-v2")
13421347
},
13431348
removedLaunchDefinitions: {
13441349
assetBrowser: launchModule.getSampleToolLaunchDefinition("asset-browser"),
@@ -1382,14 +1387,16 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13821387
expect(toolsIndexState.viewersCards).toContain("Collision Inspector V2");
13831388
expect(toolsIndexState.utilitiesCards).toContain("Audio / SFX Playground V2");
13841389
expect(toolsIndexState.utilitiesCards).toContain("Input Mapping V2");
1390+
expect(toolsIndexState.utilitiesCards).toContain("MIDI Studio V2");
13851391
expect(toolsIndexState.utilitiesCards).toContain("Text to Speech V2");
1386-
expect(toolsIndexState.allCards).toEqual(expect.arrayContaining(["World Vector Studio V2", "Object Vector Studio V2", "Collision Inspector V2", "Input Mapping V2", "Audio / SFX Playground V2"]));
1387-
expect(toolsIndexState.registryIds).toEqual(expect.arrayContaining(["world-vector-studio-v2", "object-vector-studio-v2", "collision-inspector-v2", "input-mapping-v2", "audio-sfx-playground-v2"]));
1388-
expect(toolsIndexState.launchIds).toEqual(expect.arrayContaining(["tool.world-vector-studio-v2", "tool.object-vector-studio-v2", "tool.collision-inspector-v2", "tool.input-mapping-v2", "tool.audio-sfx-playground-v2"]));
1392+
expect(toolsIndexState.allCards).toEqual(expect.arrayContaining(["World Vector Studio V2", "Object Vector Studio V2", "Collision Inspector V2", "Input Mapping V2", "Audio / SFX Playground V2", "MIDI Studio V2"]));
1393+
expect(toolsIndexState.registryIds).toEqual(expect.arrayContaining(["world-vector-studio-v2", "object-vector-studio-v2", "collision-inspector-v2", "input-mapping-v2", "audio-sfx-playground-v2", "midi-studio-v2"]));
1394+
expect(toolsIndexState.launchIds).toEqual(expect.arrayContaining(["tool.world-vector-studio-v2", "tool.object-vector-studio-v2", "tool.collision-inspector-v2", "tool.input-mapping-v2", "tool.audio-sfx-playground-v2", "tool.midi-studio-v2"]));
13891395
expect(toolsIndexState.studioLaunchDefinitions.world.launchDefinition.targetPath).toBe("/tools/world-vector-studio-v2/index.html");
13901396
expect(toolsIndexState.studioLaunchDefinitions.object.launchDefinition.targetPath).toBe("/tools/object-vector-studio-v2/index.html");
13911397
expect(toolsIndexState.studioLaunchDefinitions.inputMapping.launchDefinition.targetPath).toBe("/tools/input-mapping-v2/index.html");
13921398
expect(toolsIndexState.studioLaunchDefinitions.audioSfx.launchDefinition.targetPath).toBe("/tools/audio-sfx-playground-v2/index.html");
1399+
expect(toolsIndexState.studioLaunchDefinitions.midiStudio.launchDefinition.targetPath).toBe("/tools/midi-studio-v2/index.html");
13931400
expect(toolsIndexState.allCards).not.toContain("Asset Browser / Import Hub");
13941401
expect(toolsIndexState.allCards).not.toContain("Tile Model Converter");
13951402
expect(toolsIndexState.registryIds).not.toContain("asset-browser");
@@ -1418,6 +1425,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14181425
expect(toolsIndexState.plannedCards).not.toContain("Character Voice Presets");
14191426
expect(toolsIndexState.plannedCards).not.toContain("Game Character Voice / Event Integration");
14201427
expect(toolsIndexState.plannedCards).not.toContain("Input Mapper / Controller Tester");
1428+
expect(toolsIndexState.plannedCards).not.toContain("MIDI / Music Playground V2");
14211429
expect(toolsIndexState.plannedCards).not.toEqual(expect.arrayContaining([
14221430
"Raspberry Pi Speech Deployment",
14231431
"Queue-Based Speech Playback",

tools/index.html

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,6 @@ <h3>Replay Inspector</h3>
7373
</div>
7474
</div>
7575

76-
<div class="card">
77-
<h3>MIDI / Music Playground V2</h3>
78-
<p>Create and experiment with retro melodies, MIDI playback, sequencing, public-domain music adaptation, and lightweight chiptune composition.</p>
79-
<div class="meta">
80-
<span class="pill planned">Planned</span>
81-
</div>
82-
</div>
83-
8476
<div class="card">
8577
<h3>Piper WASM Backend</h3>
8678
<p>Add a local Piper-powered speech backend for higher-quality offline voice generation in supported browsers.</p>

tools/midi-studio-v2/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# MIDI Studio V2
2+
3+
First-Class Tool V2 surface for manifest-owned MIDI song metadata.
4+
5+
Current scope:
6+
7+
- renders multiple songs from `game.manifest.json` root `music.songs`;
8+
- honors `tools.midi-studio-v2.activeSongId` and `directorMode` preferences;
9+
- displays `.mid` source paths, instrument sets, rendered WAV/MP3/OGG targets, loop metadata, tags, and Game Music Director notes;
10+
- attempts browser media preview for selected `.mid` sources;
11+
- reports missing source paths and unsupported preview playback visibly;
12+
- rejects invalid payloads before render.
13+
14+
Out of scope:
15+
16+
- MIDI input;
17+
- recording;
18+
- piano-roll or DAW editing;
19+
- hidden fallback songs or sample data.
20+
21+
Future `src/` follow-up: add shared MIDI parsing/synthesis/rendering capability when live MIDI preview and rendered WAV/MP3/OGG export move beyond this first usable metadata surface.

0 commit comments

Comments
 (0)