Skip to content

Commit 45dca2a

Browse files
committed
Refine MIDI Studio V2 song fields and synchronize instrument selection with audition keyboard - PR_26146_060-midi-studio-v2-song-fields-and-instrument-sync
1 parent ec7c1d3 commit 45dca2a

9 files changed

Lines changed: 578 additions & 49 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# PR_26146_060 MIDI Studio V2 Song Fields And Instrument Sync Validation
2+
3+
Status: PASS
4+
5+
## Scope Validated
6+
7+
- Removed Tags and Usage from the editable Song Setup details UI.
8+
- Made Song Id read-only by default and derived from Name as camelCase on Name edits.
9+
- Kept Song Sheet fields editable and tightened the Song Sheet grid so fields use available horizontal space.
10+
- Restored Octave Timeline quick Instruments controls for select, Mute, Solo, and Hide/Show without moving GM Type or Instrument dropdowns back into the timeline.
11+
- Kept GM Type and Instrument/Patch dropdowns owned by the Instruments tab.
12+
- Synced timeline quick selection and Instruments tab selection through the same `selectedInstrumentId` state.
13+
- Replaced Play Middle C with a horizontal audition keyboard that follows the selected instrument octave range.
14+
- Preserved canvas-backed timeline editing, Export wording, Editing History placeholders, Play/Stop, manifest import, multiple songs, GM controls, launch NAV, and unwired-control behavior.
15+
16+
## Validation Commands
17+
18+
```powershell
19+
node --check tools/midi-studio-v2/js/controls/SongDetailsControl.js
20+
node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js
21+
node --check tools/midi-studio-v2/js/MidiStudioV2App.js
22+
node --check tools/midi-studio-v2/js/bootstrap.js
23+
node --check tests/playwright/tools/MidiStudioV2.spec.mjs
24+
rg --pcre2 -n "<style\b|<script\b(?![^>]*\bsrc=)|\son[a-z]+\s=|style\s=" tools/midi-studio-v2/index.html; if ($LASTEXITCODE -eq 1) { exit 0 } else { exit $LASTEXITCODE }
25+
npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright -g "syncs PR060 song fields instrument selection and audition keyboard"
26+
npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright -g "canvas note editing flow supports hover click drag paint erase and playback|keeps JSON wording and Song Setup editing history placeholders honest|enforces SSoT export ownership and future control honesty|syncs PR060 song fields instrument selection and audition keyboard"
27+
git diff --check
28+
```
29+
30+
## Results
31+
32+
- JS syntax checks: PASS.
33+
- Inline script/style/event handler guard: PASS, no matches.
34+
- Targeted PR060 Playwright test: PASS, 1 passed.
35+
- Targeted MIDI Studio V2 regression Playwright set: PASS, 4 passed.
36+
- `git diff --check`: PASS. Git reported line-ending normalization warnings only.
37+
- Full samples smoke test: not run, per PR instruction.
38+
39+
## Playwright Proof Points
40+
41+
- Tags and Usage fields are absent from Song Setup.
42+
- Name edits update Id to `camptownRacesUatReel` and `newSong4`.
43+
- Song Sheet intro/loop fields are editable and rendered in a multi-column grid.
44+
- Octave Timeline shows quick instrument select/mute/solo/visibility controls.
45+
- Timeline quick controls contain no GM Type or Instrument dropdowns.
46+
- Instruments tab owns GM Type and Instrument/Patch dropdowns.
47+
- Selecting Bass in Octave Timeline selects Bass in Instruments.
48+
- Selecting Lead in Instruments selects Lead in Octave Timeline.
49+
- `InstrumentGridControl.selectedInstrumentId` is the canonical selected-instrument state; `selectedLane` is an accessor, not a second own state slot.
50+
- Instruments tab renders the audition keyboard with Bass octave range 1-3 and 36 keys.
51+
- Clicking C2 on the audition keyboard produces a Preview Synth oscillator event and status log.
52+
- Play and Stop still work after the PR060 changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# PR_26146_060 MIDI Studio V2 SSoT Selection Notes
2+
3+
## Selected Instrument Ownership
4+
5+
- Canonical selected instrument state lives in `InstrumentGridControl.selectedInstrumentId`.
6+
- Existing `selectedLane` reads/writes are now an accessor over `selectedInstrumentId` so older internal call sites do not create a second selected-instrument value.
7+
- The Instruments tab rows and Octave Timeline quick rows both render from the same `previewLaneState` and selected instrument value.
8+
- Selecting an instrument from either surface calls the same `handleLaneSelection()` / `selectLane()` flow.
9+
10+
## Owning Surfaces
11+
12+
- Song Setup owns editable song metadata: Name, derived/read-only Id, Tempo/BPM, Key, Style, Notes, Song Sheet fields, Sections/Loop fields, and Editing History placeholders.
13+
- Song Setup no longer owns or displays editable Tags or Usage fields.
14+
- Instruments owns GM Type, GM Instrument/Patch, Volume, Pan, add/delete, and future instrument placeholders.
15+
- Octave Timeline owns note editing and quick performance/editing controls only: select, Mute, Solo, Hide/Show.
16+
- Octave Timeline does not render Type or Instrument/Patch dropdowns.
17+
- Export ownership and PR059 Save WAV/MP3/OGG wording remain unchanged.
18+
19+
## Audition Keyboard
20+
21+
- The old Play Middle C button was removed.
22+
- Instruments now renders a generated horizontal keyboard from the selected instrument's octave range.
23+
- Key clicks emit `audition-note` with the selected lane, selected instrument, note name, and Preview Synth mapping details.
24+
- Wired Preview Synth audition remains normal styling; no unwired state is shown for the keyboard.
25+
26+
## Song Id Derivation
27+
28+
- Name edits derive Id through camelCase tokenization.
29+
- Examples covered by Playwright:
30+
- `Camptown Races UAT Reel` -> `camptownRacesUatReel`
31+
- `New Song 4` -> `newSong4`
32+
- Duplicate derived IDs receive a numeric suffix to preserve manifest uniqueness.
33+
- Manual Id edits are ignored by default because Id is read-only in the Song Setup UI.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ async function selectInstrumentRow(page, lane) {
515515
await instrumentRow(page, lane).locator(`[data-lane-label="${lane}"]`).click();
516516
}
517517

518+
function timelineQuickInstrumentRow(page, lane) {
519+
return page.locator(`[data-quick-instrument-lane="${lane}"]`);
520+
}
521+
518522
function instrumentTypeSelect(page, lane) {
519523
return instrumentRow(page, lane).locator("[data-lane-instrument-type-select]");
520524
}
@@ -2136,8 +2140,8 @@ test.describe("MIDI Studio V2", () => {
21362140
await expect(page.locator("#songSheetKeyInput")).toHaveValue("G major");
21372141
await expect(page.locator("#songSheetStyleInput")).toHaveJSProperty("tagName", "SELECT");
21382142
await expect(page.locator("#songSheetStyleInput")).toHaveValue("public-domain-reel");
2139-
await expect(page.locator("#songDetails [data-song-detail-field='tags']")).toHaveValue("uat, upbeat, traditional, public-domain");
2140-
await expect(page.locator("#songDetails [data-song-detail-field='usage']")).toHaveValue("uat, public-domain, preview-synth");
2143+
await expect(page.locator("#songDetails [data-song-detail-field='tags']")).toHaveCount(0);
2144+
await expect(page.locator("#songDetails [data-song-detail-field='usage']")).toHaveCount(0);
21412145
await expect(page.locator("#songDetails [data-song-detail-field='notes']")).toHaveValue("Traditional Camptown Races style public-domain test arrangement with lead, bass, chords/pad, and drums.");
21422146
await expect(page.locator("#songSectionsLoopDetails [data-song-detail-field='sections']")).toHaveValue("verse:2, chorus:2");
21432147
await expect(page.locator("#songSectionsLoopDetails [data-song-detail-field='loopEnabled']")).toBeChecked();
@@ -2154,12 +2158,11 @@ test.describe("MIDI Studio V2", () => {
21542158
await expect(page.locator("#songDetails [data-song-detail-field='instrumentSet']")).toHaveCount(0);
21552159

21562160
await page.locator("#songDetails [data-song-detail-field='name']").fill("Edited UAT Reel");
2157-
await page.locator("#songDetails [data-song-detail-field='id']").fill("edited-uat-reel");
2161+
await expect(page.locator("#songDetails [data-song-detail-field='id']")).toHaveJSProperty("readOnly", true);
2162+
await expect(page.locator("#songDetails [data-song-detail-field='id']")).toHaveValue("editedUatReel");
21582163
await page.locator("#songSheetTempoInput").fill("156");
21592164
await page.locator("#songSheetKeyInput").selectOption("C major");
21602165
await page.locator("#songSheetStyleInput").selectOption("chip");
2161-
await page.locator("#songDetails [data-song-detail-field='tags']").fill("uat, edited");
2162-
await page.locator("#songDetails [data-song-detail-field='usage']").fill("title, test");
21632166
await page.locator("#songDetails [data-song-detail-field='notes']").fill("Edited notes from Song Setup.");
21642167
await page.locator("#songSectionsLoopDetails [data-song-detail-field='loopEnabled']").setChecked(false);
21652168
expect(await page.evaluate(() => {
@@ -2172,21 +2175,17 @@ test.describe("MIDI Studio V2", () => {
21722175
name: song.name,
21732176
notes: song.director.notes,
21742177
style: song.studioArrangement.style,
2175-
tags: song.tags,
2176-
tempo: song.studioArrangement.tempo,
2177-
usage: song.director.usage
2178+
tempo: song.studioArrangement.tempo
21782179
};
21792180
})).toEqual({
2180-
activeSongId: "edited-uat-reel",
2181-
id: "edited-uat-reel",
2181+
activeSongId: "editedUatReel",
2182+
id: "editedUatReel",
21822183
key: "C major",
21832184
loopEnabled: false,
21842185
name: "Edited UAT Reel",
21852186
notes: "Edited notes from Song Setup.",
21862187
style: "chip",
2187-
tags: ["uat", "edited"],
2188-
tempo: "156",
2189-
usage: ["title", "test"]
2188+
tempo: "156"
21902189
});
21912190

21922191
await page.locator('[data-song-id="frog-hop-nursery-rhyme"]').click();
@@ -2195,7 +2194,7 @@ test.describe("MIDI Studio V2", () => {
21952194
await expect(page.locator("#songSheetTempoInput")).toHaveValue("132");
21962195
await expect(page.locator("#songSheetKeyInput")).toHaveValue("C major");
21972196
await expect(page.locator("#songSheetStyleInput")).toHaveValue("chip");
2198-
await expect(page.locator("#songDetails [data-song-detail-field='usage']")).toHaveValue("uat, frog-hop, nursery-rhyme");
2197+
await expect(page.locator("#songDetails [data-song-detail-field='usage']")).toHaveCount(0);
21992198

22002199
await selectMidiStudioTab(page, "instruments");
22012200
await expect(page.locator("#instrumentListContent")).toBeVisible();
@@ -2417,8 +2416,8 @@ test.describe("MIDI Studio V2", () => {
24172416

24182417
await selectMidiStudioTab(page, "instruments");
24192418
await expect(page.locator("#instrumentSettingsContent")).toBeVisible();
2420-
await expect(page.locator("#playMiddleCButton")).toBeVisible();
2421-
await expect(page.locator("#playMiddleCButton")).not.toHaveAttribute("data-midi-studio-unwired");
2419+
await expect(page.locator("#instrumentAuditionKeyboard")).toBeVisible();
2420+
await expect(page.locator("#instrumentAuditionKeyboard")).not.toHaveAttribute("data-midi-studio-unwired");
24222421
await expect(page.locator("#previewVolumeLeadInput")).toBeVisible();
24232422
await expect(page.locator("#previewPanLeadInput")).toBeVisible();
24242423
const instrumentFutureControls = await page.locator('[data-midi-studio-tab-panel="instruments"] [data-midi-studio-future-control]').evaluateAll((controls) => controls.map((control) => ({
@@ -2446,14 +2445,16 @@ test.describe("MIDI Studio V2", () => {
24462445
await page.evaluate(() => {
24472446
window.__midiStudioPreviewSynthEvents = [];
24482447
});
2449-
await page.locator("#playMiddleCButton").click();
2450-
await expect(page.locator("#statusLog")).toHaveValue(/Auditioned middle C for Lead/);
2448+
await page.locator("#instrumentAuditionKeyboard [data-audition-note='C4']").click();
2449+
await expect(page.locator("#statusLog")).toHaveValue(/Auditioned C4 for Lead/);
24512450
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(true);
24522451

24532452
await selectMidiStudioTab(page, "studio");
24542453
await waitForCanvasRender(page);
2455-
await clickCanvasCell(page, "C6", 2);
2456-
expect(await hasCanvasNote(page, "lead", "C6", 2)).toBe(true);
2454+
const editableTarget = await emptyCanvasRun(page, { lane: "lead", length: 1 });
2455+
expect(editableTarget).toBeTruthy();
2456+
await clickCanvasCell(page, editableTarget.rowToken, editableTarget.stepIndex);
2457+
expect(await hasCanvasNote(page, "lead", editableTarget.rowToken, editableTarget.stepIndex)).toBe(true);
24572458
await page.locator("#playButton").click();
24582459
await expect(page.locator("#playButton")).toBeDisabled();
24592460
await expect(page.locator("#stopButton")).toBeEnabled();
@@ -2620,6 +2621,96 @@ test.describe("MIDI Studio V2", () => {
26202621
}
26212622
});
26222623

2624+
test("syncs PR060 song fields instrument selection and audition keyboard", async ({ page }) => {
2625+
const server = await openMidiStudioForImport(page);
2626+
try {
2627+
await page.locator("#toolImportManifestInput").setInputFiles(uatManifestPath);
2628+
2629+
await selectMidiStudioTab(page, "song-setup");
2630+
await expect(page.locator("#songDetails [data-song-detail-field='tags']")).toHaveCount(0);
2631+
await expect(page.locator("#songDetails [data-song-detail-field='usage']")).toHaveCount(0);
2632+
await expect(page.locator("#songDetailsContent")).not.toContainText(/\bTags\b|\bUsage\b/);
2633+
await expect(page.locator("#songDetails [data-song-detail-field='id']")).toHaveJSProperty("readOnly", true);
2634+
await page.locator("#songDetails [data-song-detail-field='name']").fill("Camptown Races UAT Reel");
2635+
await expect(page.locator("#songDetails [data-song-detail-field='id']")).toHaveValue("camptownRacesUatReel");
2636+
await page.locator("#songDetails [data-song-detail-field='name']").fill("New Song 4");
2637+
await expect(page.locator("#songDetails [data-song-detail-field='id']")).toHaveValue("newSong4");
2638+
expect(await page.evaluate(() => ({
2639+
activeSongId: window.__midiStudioV2App.payload.activeSongId,
2640+
id: window.__midiStudioV2App.selectedSong().id,
2641+
name: window.__midiStudioV2App.selectedSong().name
2642+
}))).toEqual({
2643+
activeSongId: "newSong4",
2644+
id: "newSong4",
2645+
name: "New Song 4"
2646+
});
2647+
2648+
await page.locator("#songSheetIntroInput").fill("C Am F G");
2649+
await page.locator("#songSheetLoopInput").fill("F G C C");
2650+
await expect(page.locator("#songSheetIntroInput")).toHaveValue("C Am F G");
2651+
await expect(page.locator("#songSheetLoopInput")).toHaveValue("F G C C");
2652+
const songSheetLayout = await page.locator("#songSheetContent .midi-studio-v2__song-sheet-grid").evaluate((grid) => ({
2653+
columns: getComputedStyle(grid).gridTemplateColumns.split(" ").filter(Boolean).length,
2654+
disabledFields: Array.from(grid.querySelectorAll("input, textarea, select")).filter((field) => field.disabled || field.readOnly).length
2655+
}));
2656+
expect(songSheetLayout.columns).toBeGreaterThan(1);
2657+
expect(songSheetLayout.disabledFields).toBe(0);
2658+
2659+
await selectMidiStudioTab(page, "studio");
2660+
await waitForCanvasRender(page);
2661+
await expect(page.locator("#timelineInstrumentQuickList")).toBeVisible();
2662+
await expect(timelineQuickInstrumentRow(page, "lead")).toBeVisible();
2663+
await expect(timelineQuickInstrumentRow(page, "lead").locator("[data-timeline-quick-mute='lead']")).toBeVisible();
2664+
await expect(timelineQuickInstrumentRow(page, "lead").locator("[data-timeline-quick-solo='lead']")).toBeVisible();
2665+
await expect(timelineQuickInstrumentRow(page, "lead").locator("[data-toggle-instrument-visibility='lead']")).toBeVisible();
2666+
await expect(page.locator("#timelineInstrumentQuickList select")).toHaveCount(0);
2667+
2668+
await timelineQuickInstrumentRow(page, "bass").click();
2669+
expect(await page.evaluate(() => ({
2670+
hasOwnSelectedLane: Object.hasOwn(window.__midiStudioV2App.instrumentGrid, "selectedLane"),
2671+
selectedInstrumentId: window.__midiStudioV2App.instrumentGrid.selectedInstrumentId
2672+
}))).toEqual({
2673+
hasOwnSelectedLane: false,
2674+
selectedInstrumentId: "bass"
2675+
});
2676+
await selectMidiStudioTab(page, "instruments");
2677+
await expect(instrumentRow(page, "bass")).toHaveClass(/is-selected/);
2678+
await expect(instrumentTypeSelect(page, "bass")).toBeVisible();
2679+
await expect(instrumentSelect(page, "bass")).toBeVisible();
2680+
expect(await instrumentTypeSelect(page, "bass").evaluate((select) => select.closest("[data-midi-studio-tab-panel]")?.dataset.midiStudioTabPanel)).toBe("instruments");
2681+
expect(await instrumentSelect(page, "bass").evaluate((select) => select.closest("[data-midi-studio-tab-panel]")?.dataset.midiStudioTabPanel)).toBe("instruments");
2682+
2683+
const keyboard = page.locator("#instrumentAuditionKeyboard");
2684+
await expect(keyboard).toBeVisible();
2685+
await expect(keyboard).not.toHaveAttribute("data-midi-studio-unwired");
2686+
await expect(keyboard).toHaveAttribute("data-selected-lane", "bass");
2687+
await expect(keyboard).toHaveAttribute("data-octave-min", "1");
2688+
await expect(keyboard).toHaveAttribute("data-octave-max", "3");
2689+
expect(await keyboard.locator("[data-audition-note]").count()).toBe(36);
2690+
await page.evaluate(() => {
2691+
window.__midiStudioPreviewSynthEvents = [];
2692+
});
2693+
await keyboard.locator("[data-audition-note='C2']").click();
2694+
await expect(page.locator("#statusLog")).toHaveValue(/Auditioned C2 for Bass/);
2695+
expect(await page.evaluate(() => window.__midiStudioPreviewSynthEvents.some((event) => event.action === "oscillator-start"))).toBe(true);
2696+
2697+
await selectInstrumentRow(page, "lead");
2698+
expect(await page.evaluate(() => window.__midiStudioV2App.instrumentGrid.selectedInstrumentId)).toBe("lead");
2699+
await selectMidiStudioTab(page, "studio");
2700+
await expect(timelineQuickInstrumentRow(page, "lead")).toHaveClass(/is-selected/);
2701+
2702+
await page.locator("#playButton").click();
2703+
await expect(page.locator("#playButton")).toBeDisabled();
2704+
await expect(page.locator("#stopButton")).toBeEnabled();
2705+
await page.locator("#stopButton").click();
2706+
await expect(page.locator("#stopButton")).toBeDisabled();
2707+
await expect(page.locator("#playButton")).toBeEnabled();
2708+
} finally {
2709+
await workspaceV2CoverageReporter.stop(page);
2710+
await server.close();
2711+
}
2712+
});
2713+
26232714
test("derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song", async ({ page }) => {
26242715
const server = await openMidiStudioForImport(page);
26252716
try {

tools/midi-studio-v2/index.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ <h3 id="songDetailsHeading">Song Details</h3>
189189
</button>
190190
<div id="instrumentSettingsContent" class="accordion-v2__content midi-studio-v2__stack">
191191
<div class="midi-studio-v2__instrument-audition">
192-
<button id="playMiddleCButton" type="button">Play C</button>
193-
<p class="tool-starter__hint">Selected instrument audition.</p>
192+
<div id="instrumentAuditionKeyboard" class="midi-studio-v2__audition-keyboard" aria-label="Selected instrument audition keyboard"></div>
194193
</div>
195194
<div class="midi-studio-v2__future-panel" aria-label="Future instrument settings">
196195
<h3>Future Instrument Settings</h3>
@@ -322,6 +321,10 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
322321
<button id="instrumentGridZoomInButton" class="midi-studio-v2__zoom-button" type="button" aria-label="Zoom in octave grid" title="Zoom in octave grid">+</button>
323322
</div>
324323
</div>
324+
<section class="midi-studio-v2__timeline-instruments" data-midi-studio-tab-panel="studio" aria-labelledby="timelineQuickInstrumentsHeading">
325+
<h3 id="timelineQuickInstrumentsHeading">Instruments</h3>
326+
<div id="timelineInstrumentQuickList" class="midi-studio-v2__timeline-instrument-list" aria-label="Octave Timeline quick instrument controls"></div>
327+
</section>
325328
<div class="midi-studio-v2__grid-settings" data-midi-studio-tab-panel="auto-create-parts">
326329
<label class="tool-starter__field" for="instrumentGridSectionsInput">
327330
<span>Sections</span>

0 commit comments

Comments
 (0)