Skip to content

Commit 96e2fda

Browse files
committed
Complete MIDI Studio V2 section editing sequence workflow generation preview and timeline section visibility - PR_26146_081-084-midi-studio-v2-song-builder-and-generation-lane
1 parent 93fc336 commit 96e2fda

11 files changed

Lines changed: 540 additions & 16 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# PR_26146_081_084 Bundle Validation
2+
3+
Task: `PR_26146_081-084-midi-studio-v2-song-builder-and-generation-lane`
4+
5+
Baseline: `PR_26146_072-080` local architecture state.
6+
7+
## Scope Validated
8+
9+
- First-class Intro, Verse, Chorus, Bridge, Outro section editors now show progression, bar count, and estimated duration metadata.
10+
- Custom sections remain supported and only populated sections flow into Available Sections.
11+
- Song Sequence actions Add, Duplicate, Move Up, Move Down, and Remove remain wired; selected sequence state exposes matching section color metadata.
12+
- Parse Guided Song Sheet builds from populated sections, sequence order, and Apply Song Sheet To targets.
13+
- Generated arrangement updates the canonical song model, Octave Timeline, diagnostics/status, JSON details, and Song Sheet generation summary.
14+
- Octave Timeline headers expose section labels, section colors, clicked header selection, and current playback section state.
15+
- Existing MIDI Studio ownership for instruments, export, warnings, unwired controls, Play/Stop, selected instrument sync, multiple songs, launch NAV, and canvas rendering was preserved by the targeted regression group.
16+
17+
## Validation
18+
19+
| Check | Result | Notes |
20+
| --- | --- | --- |
21+
| Changed-file syntax checks | PASS | `node --check` passed for changed JS/MJS files. |
22+
| Targeted MIDI Studio Playwright validation | PASS | `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs -g "PR072-075\|PR076-079\|PR072-080\|PR081-084"` ran 4 tests, 4 passed. |
23+
| `npm run test:workspace-v2` | TIMEOUT | Ran twice. First timed out after 300s; second timed out after 600s. Latest Workspace artifacts show unrelated WorkspaceManagerV2 failures expecting 11 tool tiles and receiving 12. |
24+
| `git diff --check` | PASS | No whitespace errors. Git emitted CRLF normalization warnings only. |
25+
26+
## Coverage Evidence
27+
28+
- `docs/dev/reports/playwright_v8_coverage_report.txt`
29+
- `docs/dev/reports/coverage_changed_js_guardrail.txt`
30+
31+
Latest targeted MIDI Studio run covered changed runtime JS files:
32+
33+
- `tools/midi-studio-v2/js/MidiStudioV2App.js`: 68%
34+
- `tools/midi-studio-v2/js/controls/InstrumentGridControl.js`: 72%
35+
- `tools/midi-studio-v2/js/controls/SongSheetControl.js`: 87%
36+
- `tools/midi-studio-v2/js/controls/OctaveTimelineCanvasRenderer.js`: 93%
37+
- `tools/midi-studio-v2/js/bootstrap.js`: 100%
38+
39+
## Residual Risk
40+
41+
- Workspace Manager V2 lane remains outside this MIDI Studio change and did not complete within the available validation window.
42+
- Timeline duplicate-label selection maps the clicked canvas section to the matching Song Sequence index; the legacy section select still stores labels, so duplicate labels remain label-based in that select.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# PR_26146_081_084 Generation Flow
2+
3+
## Source Inputs
4+
5+
- Section editors: `Intro`, `Verse`, `Chorus`, `Bridge`, `Outro`.
6+
- Custom sections: `Label: chord progression` rows in the custom section editor.
7+
- Available Sections: derived from populated sections only.
8+
- Song Sequence: explicit playback/build order from the sequence list.
9+
- Apply Song Sheet To: `Chords/Pad`, `Bass`, `Drums`, `Lead`.
10+
11+
## Flow
12+
13+
1. `SongSheetControl.availableSections()` reads populated named/custom section editors.
14+
2. `SongSheetControl.refreshSectionBuilder()` updates hidden canonical section text, Available Sections, section metrics, and sequence state.
15+
3. `SongSheetControl.composeGuidedSheet()` emits parser source with tempo/key/style, sequence order, and section bodies.
16+
4. `SongSheetParser.parse()` returns ordered section occurrences, bars, chord count, duration, warnings, and timeline data.
17+
5. `MidiStudioV2App.syncSelectedArrangementFromSongSheetResult()` updates canonical `music.songs[].studioArrangement.songSheet`.
18+
6. `MidiStudioV2App.applySongSheetToGrid()` builds the editable arrangement from the parsed sequence order.
19+
7. `InstrumentGridParser.generateLane()` fills selected generated targets and `normalizeInstrumentGrid()` updates the canonical lanes/sections and JSON Details.
20+
8. `SongSheetControl.render()` receives the generation summary and shows sections used, bars generated, notes generated, and target lanes affected.
21+
22+
## Target Behavior
23+
24+
- `Chords/Pad` enabled updates `chords` and generated `pad`.
25+
- `Bass` enabled generates bass from chords.
26+
- `Drums` enabled generates the basic drum lane when selected by default or user choice.
27+
- `Lead` remains disabled by default and is only generated when explicitly selected.
28+
- Untargeted lanes are preserved when they already match bar count; otherwise they receive rests to keep the arrangement parseable.
29+
30+
## Summary Fields
31+
32+
- `Sections used`: sequence order used for the generated arrangement.
33+
- `Bars generated`: total bars in the generated sequence.
34+
- `Notes generated`: generated/affected event count across selected target lanes.
35+
- `Target lanes affected`: human labels for selected Apply Song Sheet To targets.
36+
37+
## Verification
38+
39+
Playwright `PR081-084` verifies populated-section rules, sequence actions, canonical updates, JSON Details, generation summary, target lanes, and lead-disabled default behavior.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# PR_26146_081_084 Section Visibility Map
2+
3+
## Color Authority
4+
5+
- The normalized Octave Timeline section model remains the authority for musical section colors.
6+
- Repeated section labels share the same `colorIndex`.
7+
- Available Sections and Song Sequence options use the same section color helpers as the Octave Timeline.
8+
9+
## Timeline Header
10+
11+
- Canvas dataset exposes `data-section-header-labels` as the ordered section labels.
12+
- Canvas snapshot exposes `sectionHeaderLabels`, `sections`, and `playbackSection`.
13+
- Section names are drawn directly in the top timeline header row and in the frozen header.
14+
- Current playback/playhead section exposes:
15+
- `data-playback-section`
16+
- `data-playback-section-color`
17+
- `data-playback-section-index`
18+
19+
## Click Mapping
20+
21+
- `OctaveTimelineCanvasRenderer.sectionHeaderFromPoint()` maps header clicks to the section at the clicked step.
22+
- `InstrumentGridControl.selectTimelineHeaderSection()` moves the playhead to the clicked section, highlights that section, and emits a `select-section` transport event.
23+
- `MidiStudioV2App.handleInstrumentGridTransport()` maps timeline header selection back to `SongSheetControl.selectSequenceItem()`.
24+
- Duplicate labels use the clicked timeline section index so the corresponding Song Sequence item is selected.
25+
26+
## Preserved Surfaces
27+
28+
- Frozen Bar/Beat visibility remains canvas-backed.
29+
- Piano audition and note-cell editing remain canvas-backed.
30+
- Section preset buttons retain the same section colors and unavailable red/unwired state.
31+
- Play/Stop controls remain owned by the playback workflow and were covered by the targeted MIDI Studio group.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,33 @@ async function clickCanvasCell(page, rowToken, stepIndex) {
521521
await page.mouse.click(point.x, point.y);
522522
}
523523

524+
async function clickCanvasSectionHeader(page, label, occurrenceIndex = 0) {
525+
await page.locator("[data-octave-timeline-canvas='true']").evaluate((canvas) => {
526+
const top = canvas.getBoundingClientRect().top + window.scrollY;
527+
window.scrollTo(0, Math.max(0, top - 160));
528+
});
529+
await page.locator("#instrumentGridOutput").evaluate((output, target) => {
530+
const point = window.__midiStudioV2App.instrumentGrid.timelineCanvasSectionHeaderCenter(target.label, target.occurrenceIndex);
531+
if (!point) {
532+
return;
533+
}
534+
const rect = output.getBoundingClientRect();
535+
const desiredX = rect.left + Math.min(rect.width - 24, Math.max(24, rect.width * 0.65));
536+
if (point.x < rect.left + 24 || point.x > rect.right - 24) {
537+
output.scrollLeft = Math.max(0, output.scrollLeft + point.x - desiredX);
538+
}
539+
}, { label, occurrenceIndex });
540+
await page.waitForFunction((target) => {
541+
const output = document.querySelector("#instrumentGridOutput");
542+
const point = window.__midiStudioV2App.instrumentGrid.timelineCanvasSectionHeaderCenter(target.label, target.occurrenceIndex);
543+
const rect = output?.getBoundingClientRect();
544+
return Boolean(point && rect && point.x >= rect.left + 8 && point.x <= rect.right - 8);
545+
}, { label, occurrenceIndex });
546+
const point = await page.evaluate((target) => window.__midiStudioV2App.instrumentGrid.timelineCanvasSectionHeaderCenter(target.label, target.occurrenceIndex), { label, occurrenceIndex });
547+
expect(point).toBeTruthy();
548+
await page.mouse.click(point.x, point.y);
549+
}
550+
524551
async function clickCanvasKeyboardKey(page, rowToken) {
525552
await page.locator("#instrumentGridOutput").evaluate((output, target) => {
526553
const state = window.__midiStudioV2App.instrumentGrid.timelineCanvasState();
@@ -5010,6 +5037,122 @@ test.describe("MIDI Studio V2", () => {
50105037
}
50115038
});
50125039

5040+
test("validates PR081-084 song builder generation and timeline section visibility", async ({ page }) => {
5041+
await page.setViewportSize({ width: 1600, height: 900 });
5042+
const server = await openMidiStudioForImport(page);
5043+
try {
5044+
await page.locator("#toolImportManifestInput").setInputFiles(uatManifestPath);
5045+
await selectMidiStudioTab(page, "song-setup");
5046+
5047+
await fillSongSheetSectionBuilder(page, "Intro: G C\nVerse: G Em C D\nChorus: C G\nBridge:\nOutro: D\nBreak: F G");
5048+
await expect(page.locator("[data-song-sheet-section-metrics='Intro']")).toContainText("G C");
5049+
await expect(page.locator("[data-song-sheet-section-metrics='Intro']")).toContainText("2 bars");
5050+
await expect(page.locator("[data-song-sheet-section-metrics='Intro']")).toHaveAttribute("data-song-sheet-section-duration-seconds", /\d/);
5051+
await expect(page.locator("[data-song-sheet-section-metrics='Bridge']")).toHaveText("Empty");
5052+
await expect(page.locator("[data-song-sheet-section-metrics='Bridge']")).toHaveAttribute("data-song-sheet-section-populated", "false");
5053+
await expect(page.locator("[data-song-sheet-custom-section-metrics]")).toContainText("Break: F G");
5054+
await expect(page.locator("#songSheetAvailableSectionsList option")).toHaveText([
5055+
"Intro - 2 bars / 2 chords",
5056+
"Verse - 4 bars / 4 chords",
5057+
"Chorus - 2 bars / 2 chords",
5058+
"Outro - 1 bar / 1 chord",
5059+
"Break - 2 bars / 2 chords"
5060+
]);
5061+
await expect(page.locator("#songSheetAvailableSectionsList option", { hasText: "Bridge" })).toHaveCount(0);
5062+
5063+
await clearSongSheetSequence(page);
5064+
await addSongSheetSequenceLabels(page, ["Intro", "Verse", "Chorus"]);
5065+
await page.locator("#songSheetSequenceList").selectOption({ index: 1 });
5066+
await page.locator("#songSheetDuplicateSequenceButton").click();
5067+
await expect(page.locator("#songSheetSequenceList option")).toHaveText(["Intro", "Verse", "Verse", "Chorus"]);
5068+
await page.locator("#songSheetSequenceMoveDownButton").click();
5069+
await expect(page.locator("#songSheetSequenceList option")).toHaveText(["Intro", "Verse", "Chorus", "Verse"]);
5070+
await page.locator("#songSheetSequenceMoveUpButton").click();
5071+
await page.locator("#songSheetSequenceRemoveButton").click();
5072+
await addSongSheetSequenceLabels(page, ["Intro", "Verse", "Chorus", "Verse", "Outro"]);
5073+
await page.locator("#songSheetSequenceList").selectOption({ index: 3 });
5074+
await expect(page.locator("#songSheetSequenceList")).toHaveAttribute("data-song-sheet-selected-section", "Verse");
5075+
await expect(page.locator("#songSheetSequenceList option").nth(3)).toHaveAttribute("data-song-sheet-sequence-selected", "true");
5076+
const selectedSequenceColor = await page.locator("#songSheetSequenceList option").nth(3).getAttribute("data-song-sheet-section-color-index");
5077+
expect(selectedSequenceColor).toBe(await page.locator("#songSheetSequenceList").getAttribute("data-song-sheet-selected-section-color-index"));
5078+
5079+
const sequence = "Intro, Verse, Chorus, Verse, Outro";
5080+
await expect(page.locator("#songSheetApplyChordsPadInput")).toBeChecked();
5081+
await expect(page.locator("#songSheetApplyBassInput")).toBeChecked();
5082+
await expect(page.locator("#songSheetApplyDrumsInput")).toBeChecked();
5083+
await expect(page.locator("#songSheetApplyLeadInput")).not.toBeChecked();
5084+
await page.locator("#parseSongSheetButton").click();
5085+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='sequence'] dd")).toHaveText(sequence);
5086+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='sections-used'] dd")).toHaveText(sequence);
5087+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='bars-generated'] dd")).toHaveText("13");
5088+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='notes-generated'] dd")).toHaveText(/\d+/);
5089+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='target-lanes-affected'] dd")).toHaveText("Chords/Pad, Bass, Drums");
5090+
5091+
const generated = await page.evaluate(() => {
5092+
const app = window.__midiStudioV2App;
5093+
const song = app.selectedSong();
5094+
const gridResult = app.currentInstrumentGridResult();
5095+
return {
5096+
bars: gridResult.barCount,
5097+
chordEvents: gridResult.timeline.filter((event) => event.lane === "chords").length,
5098+
drumEvents: gridResult.timeline.filter((event) => event.lane === "drums").length,
5099+
json: document.querySelector("#inspectorOutput").textContent,
5100+
leadEvents: gridResult.timeline.filter((event) => event.lane === "lead").length,
5101+
noteSummary: document.querySelector("[data-song-sheet-summary-field='notes-generated'] dd")?.textContent,
5102+
sectionLabels: gridResult.sections.map((section) => section.label),
5103+
sections: song.studioArrangement.sections,
5104+
sequence: song.studioArrangement.songSheet.sequence,
5105+
songSheetSections: song.studioArrangement.songSheet.sections
5106+
};
5107+
});
5108+
expect(generated).toEqual(expect.objectContaining({
5109+
bars: 13,
5110+
sectionLabels: ["Intro", "Verse", "Chorus", "Verse", "Outro"],
5111+
sections: "Intro:2, Verse:4, Chorus:2, Verse:4, Outro:1",
5112+
sequence,
5113+
songSheetSections: "Intro: G C\nVerse: G Em C D\nChorus: C G\nOutro: D\nBreak: F G"
5114+
}));
5115+
expect(Number(generated.noteSummary)).toBeGreaterThan(0);
5116+
expect(generated.chordEvents).toBeGreaterThan(0);
5117+
expect(generated.drumEvents).toBeGreaterThan(0);
5118+
expect(generated.leadEvents).toBe(0);
5119+
expect(generated.json).toContain('"sequence": "Intro, Verse, Chorus, Verse, Outro"');
5120+
5121+
await selectMidiStudioTab(page, "studio");
5122+
await waitForCanvasRender(page);
5123+
const canvasState = await canvasTimelineState(page);
5124+
expect(canvasState.sectionHeaderLabels).toEqual(["Intro", "Verse", "Chorus", "Verse", "Outro"]);
5125+
expect(canvasState.sections.map((section) => section.label)).toEqual(generated.sectionLabels);
5126+
expect(canvasState.sections[1].color).toBe(canvasState.sections[3].color);
5127+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-section-header-labels", "Intro|Verse|Chorus|Verse|Outro");
5128+
expect(await page.evaluate(() => {
5129+
const list = document.querySelector("#songSheetSequenceList");
5130+
list.selectedIndex = 0;
5131+
list.dispatchEvent(new Event("change", { bubbles: true }));
5132+
return list.selectedIndex;
5133+
})).toBe(0);
5134+
await clickCanvasSectionHeader(page, "Verse", 3);
5135+
await expect.poll(async () => page.evaluate(() => document.querySelector("#songSheetSequenceList").selectedIndex)).toBe(3);
5136+
await expect(page.locator("#songSheetSequenceList")).toHaveAttribute("data-song-sheet-selected-section", "Verse");
5137+
await expect(octaveTimelineCanvas(page)).toHaveAttribute("data-playback-section", "Verse");
5138+
const selectedTimelineSection = await canvasTimelineState(page);
5139+
expect(selectedTimelineSection.playbackSection).toEqual(expect.objectContaining({
5140+
index: 3,
5141+
label: "Verse"
5142+
}));
5143+
5144+
await page.locator("#playButton").click();
5145+
await expect(page.locator("#playButton")).toBeDisabled();
5146+
await expect(page.locator("#stopButton")).toBeEnabled();
5147+
await page.locator("#stopButton").click();
5148+
await expect(page.locator("#stopButton")).toBeDisabled();
5149+
await expect(page.locator("#playButton")).toBeEnabled();
5150+
} finally {
5151+
await workspaceV2CoverageReporter.stop(page);
5152+
await server.close();
5153+
}
5154+
});
5155+
50135156
test("derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song", async ({ page }) => {
50145157
const server = await openMidiStudioForImport(page);
50155158
try {

tools/midi-studio-v2/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,26 +274,32 @@ <h3 id="exportStatusHeading">Export Status</h3>
274274
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionIntroInput">
275275
<span>Intro</span>
276276
<textarea id="songSheetSectionIntroInput" rows="2" data-song-sheet-section-name="Intro" placeholder="C F"></textarea>
277+
<output id="songSheetSectionIntroMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-section-metrics="Intro">Empty</output>
277278
</label>
278279
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionVerseInput">
279280
<span>Verse</span>
280281
<textarea id="songSheetSectionVerseInput" rows="2" data-song-sheet-section-name="Verse" placeholder="Am F"></textarea>
282+
<output id="songSheetSectionVerseMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-section-metrics="Verse">Empty</output>
281283
</label>
282284
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionChorusInput">
283285
<span>Chorus</span>
284286
<textarea id="songSheetSectionChorusInput" rows="2" data-song-sheet-section-name="Chorus" placeholder="G C"></textarea>
287+
<output id="songSheetSectionChorusMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-section-metrics="Chorus">Empty</output>
285288
</label>
286289
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionBridgeInput">
287290
<span>Bridge</span>
288291
<textarea id="songSheetSectionBridgeInput" rows="2" data-song-sheet-section-name="Bridge" placeholder="Dm G"></textarea>
292+
<output id="songSheetSectionBridgeMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-section-metrics="Bridge">Empty</output>
289293
</label>
290294
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionOutroInput">
291295
<span>Outro</span>
292296
<textarea id="songSheetSectionOutroInput" rows="2" data-song-sheet-section-name="Outro" placeholder="C"></textarea>
297+
<output id="songSheetSectionOutroMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-section-metrics="Outro">Empty</output>
293298
</label>
294299
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetCustomSectionsInput">
295300
<span>Custom sections</span>
296301
<textarea id="songSheetCustomSectionsInput" rows="2" placeholder="Solo: Dm G&#10;Break: F G"></textarea>
302+
<output id="songSheetCustomSectionMetrics" class="midi-studio-v2__section-metrics" data-song-sheet-custom-section-metrics>Empty</output>
297303
</label>
298304
<button id="songSheetAddCustomSectionButton" class="midi-studio-v2__song-sheet-add-custom" type="button">Add Custom Section</button>
299305
</div>

0 commit comments

Comments
 (0)