Skip to content

Commit 1b178bf

Browse files
committed
Advance MIDI Studio V2 arrangement visibility section playback workflow instrument range ownership and export readiness - PR_26146_093-096-midi-studio-v2-arrangement-and-export-lane
1 parent 61d63c6 commit 1b178bf

11 files changed

Lines changed: 492 additions & 25 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PR_26146_093-096 Arrangement Visibility Map
2+
3+
## Generated And Manual Indicators
4+
5+
MIDI Studio now exposes generated/manual ownership in three places:
6+
7+
- Instrument list rows show `Generated X / Manual Y` per lane.
8+
- Timeline quick instrument rows show the same per-lane counts.
9+
- Canvas timeline notes use generated/manual source colors and source stripes.
10+
11+
## Data Hooks
12+
13+
- Per-lane count badge: `data-arrangement-source-counts="<lane>"`
14+
- Generated lane count: `data-generated-count`
15+
- Manual lane count: `data-manual-count`
16+
- Total lane event count: `data-total-count`
17+
- Canvas generated count: `data-generated-note-count`
18+
- Canvas manual count: `data-manual-note-count`
19+
- Canvas snapshot count object: `timelineCanvasState().sourceCounts`
20+
21+
## Ownership
22+
23+
- Source of truth remains the normalized canonical song arrangement.
24+
- Counts derive from `currentResult.timeline`.
25+
- Manual source state is still created by editable note cells and timeline edits.
26+
- Generated source state remains created by Song Sheet generation/regeneration and generated lane helpers.
27+
28+
## Preservation
29+
30+
- Regeneration workflow remains in Song Setup.
31+
- Safe regeneration warning workflow remains unchanged.
32+
- Manual lane preservation is unchanged where supported.
33+
- Canvas-backed Octave Timeline rendering is preserved.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# PR_26146_093-096 Bundle Validation
2+
3+
Task: `PR_26146_093-096-midi-studio-v2-arrangement-and-export-lane`
4+
5+
## Result
6+
7+
- Implementation: complete for the requested MIDI Studio V2 lane.
8+
- Changed-file syntax checks: PASS.
9+
- Targeted MIDI Studio Playwright validation: PASS.
10+
- `git diff --check`: PASS.
11+
- `npm run test:workspace-v2`: FAIL in Workspace Manager V2 tests outside the MIDI Studio lane.
12+
13+
## Validation Commands
14+
15+
### Changed-file syntax checks
16+
17+
PASS:
18+
19+
- `node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
20+
- `node --check tools/midi-studio-v2/js/controls/OctaveTimelineCanvasRenderer.js`
21+
- `node --check tools/midi-studio-v2/js/controls/ExportPanelControl.js`
22+
- `node --check tools/midi-studio-v2/js/MidiStudioV2App.js`
23+
- `node --check tools/midi-studio-v2/js/bootstrap.js`
24+
- `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
25+
26+
### Targeted MIDI Studio Playwright
27+
28+
PASS:
29+
30+
`npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs -g "validates PR089-096 production templates arrangement playback instruments and export readiness" --project=playwright --workers=1 --reporter=list`
31+
32+
Result: `1 passed`.
33+
34+
Coverage artifacts were refreshed:
35+
36+
- `docs/dev/reports/playwright_v8_coverage_report.txt`
37+
- `docs/dev/reports/coverage_changed_js_guardrail.txt`
38+
39+
### Workspace V2 Gate
40+
41+
FAIL:
42+
43+
`npm run test:workspace-v2`
44+
45+
Observed result: `48 passed`, `24 failed`.
46+
47+
Failure summary:
48+
49+
- Workspace Manager V2 suite, not MIDI Studio V2.
50+
- Repeated `#workspaceToolTiles [data-workspace-tool-id]` count mismatch: expected `11`, received `12`.
51+
- One Input Mapping V2 assertion mismatch for captured keyboard/gamepad text.
52+
- One Workspace Manager V2 session-context test timed out and then could not stop coverage because the page/browser was already closed.
53+
54+
The failing files are outside this lane's changed MIDI Studio files.
55+
56+
### Diff Whitespace Check
57+
58+
PASS:
59+
60+
`git diff --check`
61+
62+
Git emitted line-ending normalization warnings for existing working-copy settings on a few changed files, but the command exited `0` with no whitespace errors.
63+
64+
## Playwright Coverage Against Requirements
65+
66+
Covered by the targeted MIDI Studio validation:
67+
68+
- Generated/manual note visibility.
69+
- Section playback workflow.
70+
- Sequence playback workflow.
71+
- Active section highlighting.
72+
- Instrument range visibility.
73+
- Audition keyboard range behavior.
74+
- Export readiness summary.
75+
- Canonical model integrity.
76+
- Play/Stop correctness.
77+
78+
## Changed Runtime Files
79+
80+
- `tools/midi-studio-v2/index.html`
81+
- `tools/midi-studio-v2/js/bootstrap.js`
82+
- `tools/midi-studio-v2/js/MidiStudioV2App.js`
83+
- `tools/midi-studio-v2/js/controls/ExportPanelControl.js`
84+
- `tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
85+
- `tools/midi-studio-v2/js/controls/OctaveTimelineCanvasRenderer.js`
86+
- `tools/midi-studio-v2/styles/midiStudioV2.css`
87+
- `tests/playwright/tools/MidiStudioV2.spec.mjs`
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# PR_26146_093-096 Playback Workflow Audit
2+
3+
## Section Preview
4+
5+
- Control: `#playSectionButton`
6+
- Owner: Octave Timeline
7+
- Behavior: plays the selected section range through Preview Synth and timing preview.
8+
- Status text: `Playing section: <label>`
9+
- Active section visibility: canvas `data-playback-section`, `data-playback-section-color`, and the section-highlight rendering.
10+
11+
## Sequence Preview
12+
13+
- Control: `#playSequenceButton`
14+
- Owner: Octave Timeline
15+
- Behavior: plays the full normalized Song Sequence range from step `0` to `totalSteps - 1`.
16+
- Status text: `Playing sequence: Song Sequence`
17+
- Canonical source: populated Song Sheet sections in sequence/build order.
18+
19+
## Loop Preview
20+
21+
- Control: `#playLoopButton`
22+
- Owner: Octave Timeline
23+
- Behavior: preserved existing loop preview behavior.
24+
- Status text: `Playing loop: <start> to <end>`
25+
- Loop state: continues to use selected loop bounds and wraps the timing playhead.
26+
27+
## Stop Workflow
28+
29+
- Control: `#stopTimingPreviewButton`
30+
- Behavior: stops timing preview, clears Preview Synth active lanes, and reports stopped state.
31+
- Status text: `Preview Synth timing preview stopped.`
32+
33+
## Global Play/Stop
34+
35+
- Global Play/Stop remains owned by the global nav playback controls.
36+
- Targeted Playwright verifies Play disables while playing and Stop re-enables Play.
37+
38+
## Instrument Range And Audition
39+
40+
- Selected instrument editor shows `Playable range`.
41+
- Audition keyboard exposes `data-playable-range`.
42+
- Audition range summary exposes `data-audition-range-summary`.
43+
- Changing octave range refreshes the selected instrument editor and keyboard note set.
44+
45+
## Export Workflow
46+
47+
- Export tab readiness now includes selected song, classification, generated ID, sequence length, note count, instrument count, and target output formats.
48+
- SoundFont/render pipeline controls remain visible and red/unwired.
49+
- Save WAV, Save MP3, and Save OGG ownership remains in Export.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@ async function visibleMidiStudioControlOwnership(page, activeTabId) {
761761
playButton: { canonical: "playback from selected canonical song model", kind: "action", owner: "Global NAV", wired: "wired" },
762762
playLoopButton: { canonical: "timing preview playback state", kind: "workflow-state", owner: "Octave Timeline", wired: "wired" },
763763
playSectionButton: { canonical: "timing preview playback state", kind: "workflow-state", owner: "Octave Timeline", wired: "wired" },
764+
playSequenceButton: { canonical: "timing preview playback state from Song Sequence order", kind: "workflow-state", owner: "Octave Timeline", wired: "wired" },
764765
regenerateArrangementButton: { canonical: "music.songs[].studioArrangement generated lanes from Song Sheet sequence", kind: "canonical-action", owner: "Song Setup", wired: "wired" },
765766
renderedExportSaveButton: { canonical: "future rendered audio renderer", kind: "unwired", owner: "Export", wired: "unwired" },
766767
renderedExportTargetTypeSelect: { canonical: "future rendered audio renderer", kind: "unwired", owner: "Export", wired: "unwired" },
@@ -968,6 +969,9 @@ async function visibleMidiStudioControlOwnership(page, activeTabId) {
968969
if (element.dataset.instrumentDerivedField) {
969970
return { canonical: "derived from selected instrument preview mapping", kind: "readonly", owner: "Instruments", wired: "wired" };
970971
}
972+
if (element.dataset.auditionRangeSummary) {
973+
return { canonical: "derived from selected instrument octave range", kind: "readonly", owner: "Instruments", wired: "wired" };
974+
}
971975
if (element.dataset.auditionNote) {
972976
return { canonical: "Preview Synth audition action", kind: "action", owner: "Instruments", wired: "wired" };
973977
}
@@ -5267,7 +5271,7 @@ test.describe("MIDI Studio V2", () => {
52675271
}
52685272
});
52695273

5270-
test("validates PR089-092 production templates safe regeneration instruments and export readiness", async ({ page }) => {
5274+
test("validates PR089-096 production templates arrangement playback instruments and export readiness", async ({ page }) => {
52715275
await page.setViewportSize({ width: 1600, height: 900 });
52725276
const server = await openMidiStudioForImport(page);
52735277
try {
@@ -5306,6 +5310,12 @@ test.describe("MIDI Studio V2", () => {
53065310
const manualLead = "F5 F5 F5 F5 | F5 F5 F5 F5 | F5 F5 F5 F5 | F5 F5 F5 F5 | F5 F5 F5 F5 | F5 F5 F5 F5";
53075311
await page.locator("#instrumentGridLeadInput").fill(manualLead);
53085312
await page.locator("#normalizeInstrumentGridButton").click();
5313+
await selectMidiStudioTab(page, "studio");
5314+
await waitForCanvasRender(page);
5315+
const manualLeadCounts = page.locator("#timelineInstrumentQuickList [data-arrangement-source-counts='lead']");
5316+
await expect(manualLeadCounts).toContainText("Generated 0 / Manual 24");
5317+
await expect(manualLeadCounts).toHaveAttribute("data-generated-count", "0");
5318+
await expect(manualLeadCounts).toHaveAttribute("data-manual-count", "24");
53095319
await selectMidiStudioTab(page, "song-setup");
53105320
await page.locator("#regenerateArrangementButton").click();
53115321
await expect(page.locator("#regenerateArrangementButton")).toHaveAttribute("data-regeneration-pending", "true");
@@ -5342,6 +5352,38 @@ test.describe("MIDI Studio V2", () => {
53425352

53435353
await selectMidiStudioTab(page, "studio");
53445354
await waitForCanvasRender(page);
5355+
const generatedLeadCounts = page.locator("#timelineInstrumentQuickList [data-arrangement-source-counts='lead']");
5356+
await expect(generatedLeadCounts).toContainText("Generated 24 / Manual 0");
5357+
await expect(generatedLeadCounts).toHaveAttribute("data-generated-count", "24");
5358+
await expect(generatedLeadCounts).toHaveAttribute("data-manual-count", "0");
5359+
const canvasSourceState = await canvasTimelineState(page);
5360+
expect(canvasSourceState.sourceCounts.generated).toBeGreaterThan(0);
5361+
expect(canvasSourceState.sourceCounts.generated + canvasSourceState.sourceCounts.manual).toBeGreaterThan(0);
5362+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-generated-note-count", /\d+/);
5363+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-manual-note-count", /\d+/);
5364+
5365+
await page.locator("#instrumentGridSectionSelect").selectOption("Verse");
5366+
await page.locator("#playSectionButton").click();
5367+
await expect(page.locator("#instrumentGridTransportState")).toContainText("Playing section: Verse");
5368+
await expect(page.locator("#instrumentGridOutput")).toHaveAttribute("data-preview-playback-mode", "section");
5369+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-playback-section", "Verse");
5370+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-playback-section-color", /#/);
5371+
await page.locator("#stopTimingPreviewButton").click();
5372+
await expect(page.locator("#instrumentGridTransportState")).toContainText("Preview Synth timing preview stopped.");
5373+
5374+
await page.locator("#playSequenceButton").click();
5375+
await expect(page.locator("#instrumentGridTransportState")).toContainText("Playing sequence: Song Sequence");
5376+
await expect(page.locator("#instrumentGridOutput")).toHaveAttribute("data-preview-playback-mode", "sequence");
5377+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-playback-section", "Verse");
5378+
await page.locator("#stopTimingPreviewButton").click();
5379+
5380+
await page.locator("#instrumentGridLoopStartSelect").selectOption("Verse");
5381+
await page.locator("#instrumentGridLoopEndSelect").selectOption("Solo");
5382+
await page.locator("#playLoopButton").click();
5383+
await expect(page.locator("#instrumentGridTransportState")).toContainText("Playing loop: Verse to Solo");
5384+
await expect(page.locator("#instrumentGridOutput")).toHaveAttribute("data-preview-playback-mode", "loop");
5385+
await page.locator("#stopTimingPreviewButton").click();
5386+
53455387
await page.locator("#duplicateInstrumentRowButton").click();
53465388
await expect(timelineQuickInstrumentRow(page, "lead-1")).toHaveClass(/is-selected/);
53475389
await expect(page.locator("#timelineInstrumentQuickList")).toHaveAttribute("data-selected-instrument-id", "lead-1");
@@ -5352,15 +5394,27 @@ test.describe("MIDI Studio V2", () => {
53525394
await selectMidiStudioTab(page, "instruments");
53535395
await expect(instrumentRow(page, "lead-1")).toHaveClass(/is-selected/);
53545396
await expect(page.locator("#instrumentAuditionKeyboard")).toHaveAttribute("data-selected-instrument-id", "lead-1");
5397+
await expect(page.locator("#selectedInstrumentEditor [data-instrument-playable-range-lane='lead-1']")).toHaveText("C3 to B6");
5398+
await expect(page.locator("#instrumentAuditionKeyboard")).toHaveAttribute("data-playable-range", "C3 to B6");
5399+
await expect(page.locator("#instrumentAuditionKeyboard [data-audition-range-summary='lead-1']")).toHaveText("C3 to B6");
5400+
await setInputValue(page, "#previewOctaveLowLead1Input", "4");
5401+
await expect(page.locator("#selectedInstrumentEditor [data-instrument-playable-range-lane='lead-1']")).toHaveText("C4 to B6");
5402+
await expect(page.locator("#instrumentAuditionKeyboard")).toHaveAttribute("data-playable-range", "C4 to B6");
5403+
await expect(page.locator("#instrumentAuditionKeyboard [data-audition-note='C3']")).toHaveCount(0);
5404+
await expect(page.locator("#instrumentAuditionKeyboard [data-audition-note='C4']")).toHaveCount(1);
53555405
await page.locator("#instrumentAuditionKeyboard [data-audition-note='C5']").click();
53565406
await expect(page.locator("#statusLog")).toHaveValue(/INFO Auditioned C5 for Lead 1/);
53575407

53585408
await selectMidiStudioTab(page, "export");
53595409
await expect(page.locator("#exportRenderSource [data-export-field='selected-song'] dd")).toHaveText("Camptown Races UAT Reel");
53605410
await expect(page.locator("#exportRenderSource [data-export-field='classification'] dd")).toHaveText("Loop");
53615411
await expect(page.locator("#exportRenderSource [data-export-field='generated-id'] dd")).toHaveText("camptownRacesUatReel-Loop");
5412+
await expect(page.locator("#exportRenderSource [data-export-field='sequence-length'] dd")).toHaveText("2");
5413+
await expect.poll(async () => Number(await page.locator("#exportRenderSource [data-export-field='note-count'] dd").textContent())).toBeGreaterThan(0);
5414+
await expect.poll(async () => Number(await page.locator("#exportRenderSource [data-export-field='instrument-count'] dd").textContent())).toBeGreaterThan(4);
53625415
await expect(page.locator("#exportRenderSource [data-export-field='target-output-formats'] dd")).toHaveText("WAV: missing; MP3: missing; OGG: missing");
5363-
await expect(page.locator("#exportStatusDetails [data-export-field='status'] dd")).toContainText("WARN: Export source is ready, but target paths are missing for WAV, MP3, OGG");
5416+
await expect(page.locator("#exportStatusDetails [data-export-field='status'] dd")).toContainText("WARN: Export source is ready with 2 sequence item(s)");
5417+
await expect(page.locator("#exportStatusDetails [data-export-field='status'] dd")).toContainText("target paths are missing for WAV, MP3, OGG");
53645418
await expect(page.locator("#futureSoundFontSelect")).toHaveAttribute("data-midi-studio-unwired", "not-implemented");
53655419
await expect(page.locator("#renderedExportSaveButton")).toHaveText("Save WAV");
53665420
await page.locator("#renderedExportTargetTypeSelect").selectOption("mp3");

tools/midi-studio-v2/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
515515
<div class="midi-studio-v2__transport-actions" aria-label="Grid transport actions">
516516
<button id="jumpToSectionButton" type="button">Jump To Section</button>
517517
<button id="playSectionButton" type="button">Play Section</button>
518+
<button id="playSequenceButton" type="button">Play Sequence</button>
518519
<button id="playLoopButton" type="button">Play Loop</button>
519520
<button id="stopTimingPreviewButton" type="button">Stop</button>
520521
</div>

0 commit comments

Comments
 (0)