Skip to content

Commit 4ac9269

Browse files
committed
Reshape MIDI Studio V2 editor into spreadsheet-style instrument note grid - PR_26146_019-midi-studio-v2-spreadsheet-note-grid
1 parent 90003f3 commit 4ac9269

4 files changed

Lines changed: 340 additions & 77 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# PR_26146_019 MIDI Studio V2 Spreadsheet Note Grid Validation
2+
3+
## Scope
4+
- Reshaped the MIDI Studio V2 advanced instrument grid into a spreadsheet-style note grid.
5+
- Kept `Instrument/Lane` as the first column and timing-aligned editable note cells after it.
6+
- Preserved Load Example And Play, Stop All Audio, Preview Synth playback, snapping, playhead movement, lane helpers, mute/solo, instrument selection, MIDI inspection, and honest export status behavior.
7+
- No SoundFont playback, export rendering, MIDI input, recording, inline script/style/event handlers, or hidden fallback songs were added.
8+
9+
## Changed Files
10+
- `tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
11+
- `tools/midi-studio-v2/styles/midiStudioV2.css`
12+
- `tests/playwright/tools/MidiStudioV2.spec.mjs`
13+
- `docs/dev/reports/PR_26146_019-midi-studio-v2-spreadsheet-note-grid_validation.md`
14+
- `docs/dev/reports/codex_review.diff`
15+
- `docs/dev/reports/codex_changed_files.txt`
16+
- `docs/dev/reports/playwright_v8_coverage_report.txt`
17+
- `docs/dev/reports/coverage_changed_js_guardrail.txt`
18+
- `docs/dev/codex_commands.md`
19+
- `docs/dev/commit_comment.txt`
20+
21+
## Validation
22+
- PASS: `node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
23+
- PASS: `node --check tools/midi-studio-v2/js/MidiStudioV2App.js`
24+
- PASS: `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
25+
- PASS: `npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --grep "applies Preview Synth instruments" --timeout=30000`
26+
- PASS: `npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs`
27+
- Result: 37 passed.
28+
- PASS: `git diff --check`
29+
- Note: Git reported CRLF normalization warnings for existing working-copy line endings in `tests/playwright/tools/MidiStudioV2.spec.mjs` and `tools/midi-studio-v2/styles/midiStudioV2.css`; no whitespace errors were reported.
30+
31+
## Coverage Reports
32+
- Updated `docs/dev/reports/playwright_v8_coverage_report.txt`.
33+
- Updated `docs/dev/reports/coverage_changed_js_guardrail.txt`.
34+
- Guardrail status: advisory PASS, no changed runtime JS coverage warnings.
35+
36+
## Environment Notes
37+
- Chromium was missing for Playwright validation, so Chromium was installed under `node_modules/.cache/ms-playwright` with `PLAYWRIGHT_BROWSERS_PATH` scoped to the repo-local cache for validation.
38+
- The repo-local Playwright browser cache was removed after validation and packaging preparation.
39+
40+
## Skips
41+
- Full samples smoke test: SKIPPED per request.
42+
- Workspace Manager V2 registration/handoff: SKIPPED because this PR did not touch Workspace V2 registration or handoff.
43+
44+
## ZIP
45+
- Repo-structured delta ZIP: `tmp/PR_26146_019-midi-studio-v2-spreadsheet-note-grid_delta.zip`

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,27 @@ async function audioDiagnosticsRows(page) {
287287
])));
288288
}
289289

290+
function spreadsheetCell(page, lane, stepIndex) {
291+
return page.locator(`.midi-studio-v2__spreadsheet-note-cell[data-lane="${lane}"][data-step-index="${stepIndex}"]`);
292+
}
293+
294+
async function setSpreadsheetRowToggle(page, laneLabel, kind, checked) {
295+
const toggle = page.locator(`.midi-studio-v2__lane-header-cell [aria-label="${kind} ${laneLabel}"]`);
296+
await expect(toggle).toHaveCount(1);
297+
await toggle.evaluate(
298+
(input, nextChecked) => {
299+
input.checked = nextChecked;
300+
input.dispatchEvent(new Event("change", { bubbles: true }));
301+
},
302+
checked
303+
);
304+
if (checked) {
305+
await expect(toggle).toBeChecked();
306+
} else {
307+
await expect(toggle).not.toBeChecked();
308+
}
309+
}
310+
290311
test.describe("MIDI Studio V2", () => {
291312
test.afterAll(async () => {
292313
await workspaceV2CoverageReporter.writeReport();
@@ -372,6 +393,13 @@ test.describe("MIDI Studio V2", () => {
372393
await expect(page.locator("#previewInstrumentPadSelect")).toHaveValue("ambient-pad");
373394
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded explicit demo test song data\. Demo paths are declared for UAT only; they are not hidden fallback assets\./);
374395
await expect(page.locator("#instrumentGridSectionSelect")).toContainText("intro");
396+
await expect(page.locator(".midi-studio-v2__grid-cell").first()).toHaveText("Instrument/Lane");
397+
await expect(page.locator(".midi-studio-v2__spreadsheet-note-cell[data-lane]")).not.toHaveCount(0);
398+
await expect(spreadsheetCell(page, "chords", 0)).toHaveText("Am");
399+
await expect(spreadsheetCell(page, "bass", 0)).toHaveText("A2");
400+
await expect(spreadsheetCell(page, "pad", 0)).toHaveText("Am");
401+
await expect(spreadsheetCell(page, "lead", 0)).toHaveText("A4");
402+
await expect(spreadsheetCell(page, "drums", 0)).toHaveText("kick");
375403
await page.locator("#playSectionButton").click();
376404
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview Synth started for section intro with \d+ playable events\./);
377405
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(true);
@@ -422,7 +450,8 @@ test.describe("MIDI Studio V2", () => {
422450
await expect(page.locator("#instrumentGridTransportState")).toContainText("Playing section Preview Synth timing preview: intro");
423451
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "resume"))).toBe(true);
424452
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(true);
425-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).not.toHaveAttribute("data-step-index", "0");
453+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).not.toHaveAttribute("data-step-index", "0");
454+
await expect(spreadsheetCell(page, "bass", 0)).toHaveText("A2");
426455
const diagnostics = await audioDiagnosticsRows(page);
427456
expect(Number(diagnostics["Playable note count"])).toBeGreaterThan(0);
428457
expect(diagnostics).toMatchObject({
@@ -739,6 +768,33 @@ Am F`);
739768
}
740769
});
741770

771+
test("edits spreadsheet note cells into playable timeline data", async ({ page }) => {
772+
const server = await openMidiStudio(page);
773+
try {
774+
await fillInstrumentGrid(page);
775+
await page.locator("#normalizeInstrumentGridButton").click();
776+
const beforeDiagnostics = await audioDiagnosticsRows(page);
777+
const beforeCount = Number(beforeDiagnostics["Playable note count"]);
778+
await spreadsheetCell(page, "lead", 3).fill("B4");
779+
await expect(page.locator("#instrumentGridLeadInput")).toHaveValue("E4 G4 A4 B4 | C5 B4 G4 -");
780+
await page.locator("#normalizeInstrumentGridButton").click();
781+
await expect(spreadsheetCell(page, "lead", 3)).toHaveText("B4");
782+
const editedModel = await page.evaluate(() => window.__midiStudioV2App.lastInstrumentGridResult);
783+
expect(editedModel.timeline.some((event) => event.lane === "lead" && event.value === "B4" && event.bar === 1)).toBe(true);
784+
expect(Number((await audioDiagnosticsRows(page))["Playable note count"])).toBeGreaterThan(beforeCount);
785+
await page.locator("#playSectionButton").click();
786+
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview Synth started for section intro with \d+ playable events\./);
787+
788+
await spreadsheetCell(page, "lead", 1).fill("BAD");
789+
await page.locator("#normalizeInstrumentGridButton").click();
790+
await expect(page.locator("#instrumentGridSummary")).toContainText('Invalid note token "BAD"');
791+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Instrument grid rejected: Invalid note token "BAD" in lead at bar 1, beat 2\./);
792+
} finally {
793+
await workspaceV2CoverageReporter.stop(page);
794+
await server.close();
795+
}
796+
});
797+
742798
test("keeps beat bar alignment consistent across grid lanes", async ({ page }) => {
743799
const server = await openMidiStudio(page);
744800
try {
@@ -748,9 +804,19 @@ Am F`);
748804
});
749805
await page.locator("#normalizeInstrumentGridButton").click();
750806
expect(await page.locator(".midi-studio-v2__instrument-grid").evaluate((grid) => getComputedStyle(grid).gridTemplateColumns.split(" ").length)).toBe(9);
751-
expect(await page.locator(".midi-studio-v2__grid-cell--section").evaluateAll((cells) => cells.map((cell) => cell.style.gridColumn))).toEqual(["span 4", "span 4"]);
752-
expect(await page.locator(".midi-studio-v2__grid-cell--beat").evaluateAll((cells) => cells.map((cell) => cell.textContent))).toEqual(["B1.1", "B1.2", "B1.3", "B1.4", "B2.1", "B2.2", "B2.3", "B2.4"]);
753-
expect(await page.locator(".midi-studio-v2__instrument-grid").evaluate((grid) => Array.from(grid.children).filter((cell) => cell.textContent === "Am").length)).toBe(3);
807+
await expect(page.locator(".midi-studio-v2__grid-cell").first()).toHaveText("Instrument/Lane");
808+
expect(await page.locator(".midi-studio-v2__grid-cell--timing-header").evaluateAll((cells) => cells.map((cell) => cell.textContent))).toEqual([
809+
"Section introBar 1Beat 1",
810+
"Section introBar 1Beat 2",
811+
"Section introBar 1Beat 3",
812+
"Section introBar 1Beat 4",
813+
"Section loopBar 2Beat 1",
814+
"Section loopBar 2Beat 2",
815+
"Section loopBar 2Beat 3",
816+
"Section loopBar 2Beat 4"
817+
]);
818+
await expect(spreadsheetCell(page, "chords", 0)).toHaveText("Am");
819+
await expect(spreadsheetCell(page, "bass", 0)).toHaveText("A2");
754820
} finally {
755821
await workspaceV2CoverageReporter.stop(page);
756822
await server.close();
@@ -813,11 +879,11 @@ Am F`);
813879
await expect(page.locator("#instrumentGridSnapIndicator")).toContainText("Snap: 1 bar / 2 beats / 1/2");
814880
await page.locator("#normalizeInstrumentGridButton").click();
815881
expect(await page.locator(".midi-studio-v2__instrument-grid").evaluate((grid) => getComputedStyle(grid).gridTemplateColumns.split(" ").length)).toBe(5);
816-
expect(await page.locator(".midi-studio-v2__grid-cell--beat").evaluateAll((cells) => cells.map((cell) => cell.textContent))).toEqual([
817-
"B1.1.1",
818-
"B1.1.2",
819-
"B1.2.1",
820-
"B1.2.2"
882+
expect(await page.locator(".midi-studio-v2__grid-cell--timing-header").evaluateAll((cells) => cells.map((cell) => cell.textContent))).toEqual([
883+
"Section introBar 1Beat 1Subdivision 1",
884+
"Section introBar 1Beat 1Subdivision 2",
885+
"Section introBar 1Beat 2Subdivision 1",
886+
"Section introBar 1Beat 2Subdivision 2"
821887
]);
822888
await page.locator("#instrumentGridSubdivisionInput").selectOption("16");
823889
await expect(page.locator("#instrumentGridSnapIndicator")).toContainText("Snap: 1 bar / 2 beats / 1/16");
@@ -843,15 +909,15 @@ Am F`);
843909
await expect(page.locator("#instrumentGridOutput")).toContainText("Bar 5");
844910
await expect(page.locator(".midi-studio-v2__grid-cell--bar")).toHaveCount(5);
845911
await expect(page.locator(".midi-studio-v2__grid-cell--ruler").first()).toContainText("1");
846-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-section", "intro");
912+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-section", "intro");
847913
await expect(page.locator("#instrumentGridSectionSelect")).toContainText("bridge");
848914
await page.locator("#instrumentGridLoopStartSelect").selectOption("loop");
849915
await page.locator("#instrumentGridLoopEndSelect").selectOption("boss");
850916
expect(await page.locator(".midi-studio-v2__grid-cell--loop-region").count()).toBeGreaterThan(0);
851917
await page.locator('[data-section-preset="boss"]').click();
852918
await expect(page.locator("#instrumentGridSectionSelect")).toHaveValue("boss");
853919
await page.locator("#jumpToSectionButton").click();
854-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-section", "boss");
920+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-section", "boss");
855921
await expect(page.locator("#statusLog")).toHaveValue(/OK Timing playhead jumped to section boss\./);
856922
} finally {
857923
await workspaceV2CoverageReporter.stop(page);
@@ -877,12 +943,12 @@ Am F`);
877943
await expect(page.locator('[data-lane="bass"][data-source="generated"]')).toHaveCount(4);
878944
await expect(page.locator('[data-lane="bass"][data-source="manual"]')).toHaveCount(1);
879945
await page.locator("#instrumentGridSectionSelect").selectOption("intro");
880-
const beforeStep = await page.locator(".midi-studio-v2__grid-cell--playhead-active").getAttribute("data-step-index");
946+
const beforeStep = await page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active").getAttribute("data-step-index");
881947
await page.locator("#playSectionButton").click();
882948
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview Synth started for section intro with \d+ playable events\./);
883-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).not.toHaveAttribute("data-step-index", beforeStep || "");
884-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-beat", /1|2/);
885-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-subdivision-step", /1|2/);
949+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).not.toHaveAttribute("data-step-index", beforeStep || "");
950+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-beat", /1|2/);
951+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-subdivision-step", /1|2/);
886952
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(true);
887953
await page.locator("#stopTimingPreviewButton").click();
888954
await expect(page.locator("#instrumentGridTransportState")).toContainText("Preview Synth timing preview stopped.");
@@ -905,11 +971,11 @@ Am F`);
905971
pad: ""
906972
});
907973
await page.locator("#normalizeInstrumentGridButton").click();
908-
const beforeStep = await page.locator(".midi-studio-v2__grid-cell--playhead-active").getAttribute("data-step-index");
974+
const beforeStep = await page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active").getAttribute("data-step-index");
909975
await page.locator("#playSectionButton").click();
910976
await expect(page.locator("#statusLog")).toHaveValue(/FAIL No playable Preview Synth notes found for section intro\. Generate or enter chords, bass, pad, lead, or drum cells before playing\./);
911977
await expect(page.locator("#audioDiagnostics")).toContainText("No playable Preview Synth notes found for section intro.");
912-
await expect(page.locator(".midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-step-index", beforeStep || "0");
978+
await expect(page.locator(".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active")).toHaveAttribute("data-step-index", beforeStep || "0");
913979
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(false);
914980
} finally {
915981
await workspaceV2CoverageReporter.stop(page);
@@ -946,16 +1012,16 @@ Am F`);
9461012
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview instrument selected for Lead: Retro Pulse Lead\./);
9471013
await page.locator("#normalizeInstrumentGridButton").click();
9481014

949-
await page.locator("#previewMuteBassToggle").check();
1015+
await setSpreadsheetRowToggle(page, "Bass", "mute", true);
9501016
await expect(page.locator("#statusLog")).toHaveValue(/WARN Lane muted: Bass\./);
9511017
await page.locator("#playSectionButton").click();
9521018
await expect(page.locator("#statusLog")).toHaveValue(/OK Preview Synth started for section intro with \d+ playable events\./);
9531019
await expect(page.locator('.midi-studio-v2__grid-cell--lane-active[data-lane="bass"]')).toHaveCount(0);
9541020
expect((await audioDiagnosticsRows(page))["Muted lanes"]).toBe("bass");
9551021
await page.locator("#stopTimingPreviewButton").click();
9561022

957-
await page.locator("#previewMuteBassToggle").uncheck();
958-
await page.locator("#previewSoloLeadToggle").check();
1023+
await setSpreadsheetRowToggle(page, "Bass", "mute", false);
1024+
await setSpreadsheetRowToggle(page, "Lead", "solo", true);
9591025
await expect(page.locator("#statusLog")).toHaveValue(/OK Lane soloed: Lead\./);
9601026
await page.locator("#playSectionButton").click();
9611027
await expect(page.locator(".midi-studio-v2__grid-cell--lane-active")).not.toHaveCount(0);
@@ -967,7 +1033,7 @@ Am F`);
9671033
expect(soloDiagnostics["Soloed lanes"]).toBe("lead");
9681034
await page.locator("#stopTimingPreviewButton").click();
9691035

970-
await page.locator("#previewSoloLeadToggle").uncheck();
1036+
await setSpreadsheetRowToggle(page, "Lead", "solo", false);
9711037
await page.locator("#previewInstrumentLeadSelect").selectOption("");
9721038
await expect(page.locator("#statusLog")).toHaveValue(/WARN Missing preview instrument selection for Lead\. Choose a Preview Synth instrument before playback\./);
9731039
await page.locator("#playSectionButton").click();

0 commit comments

Comments
 (0)