Skip to content

Commit 416a08b

Browse files
committed
Complete MIDI Studio V2 sequence UX, section playback sync, instrument editor core controls, and piano audition workflow - PR_26146_072-075-midi-studio-v2-ux-instrument-and-export-lane
1 parent a38677b commit 416a08b

11 files changed

Lines changed: 561 additions & 16 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# PR_26146_072_075 Bundle Validation
2+
3+
## Summary
4+
- PASS: MIDI Studio V2 sequence builder UX now shows populated-section counts, sequence item counts, duplicate sequence action, and distinct Available Sections vs Song Sequence panels.
5+
- PASS: Musical section colors are reused across Song Sequence options, Octave Timeline canvas sections, and section playback buttons.
6+
- PASS: Instruments tab remains the SSoT owner for GM Type, GM Instrument, Volume, Pan/Balance, Octave Range, and Transpose.
7+
- PASS: Piano audition keyboard uses the selected instrument lane settings and produced Preview Synth oscillator playback in Playwright.
8+
- PASS: Canvas hover/select state is observable and redrawn through the canvas renderer.
9+
- PASS: Play/Stop passed in targeted MIDI Studio validation.
10+
- WARN: `npm run test:workspace-v2` completed with unrelated Workspace Manager/Input Mapping failures outside MIDI Studio V2.
11+
12+
## Validation Commands
13+
- PASS: `node --check tools/midi-studio-v2/js/controls/SongSheetControl.js`
14+
- PASS: `node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
15+
- PASS: `node --check tools/midi-studio-v2/js/controls/OctaveTimelineCanvasRenderer.js`
16+
- PASS: `node --check src/engine/audio/PreviewSynthEngine.js`
17+
- PASS: `node --check tools/midi-studio-v2/js/bootstrap.js`
18+
- PASS: `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
19+
- PASS: `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --grep "PR072-075" --reporter=list`
20+
- PASS: `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --grep "PR061|PR069|PR070|PR071|PR072-075" --reporter=list`
21+
- FAIL unrelated: `npm run test:workspace-v2` ran 72 Workspace Manager V2 tests; 46 passed and 26 failed in Workspace Manager/Input Mapping/manifest expectations.
22+
- PASS: `git diff --check` completed with only LF/CRLF normalization warnings.
23+
24+
## Targeted Playwright Evidence
25+
- PASS: Sequence builder add/remove/reorder/duplicate flow verified.
26+
- PASS: Available Sections excludes empty Bridge and shows populated count.
27+
- PASS: Song Sequence item count updates after add, duplicate, move, and remove.
28+
- PASS: Sequence option color indices stay stable for repeated Verse and Chorus items.
29+
- PASS: Parsed canonical song model sequence is `Intro, Verse, Chorus, Verse, Chorus, Outro`.
30+
- PASS: Octave Timeline canvas section labels and colors follow the canonical musical sequence.
31+
- PASS: Section playback button color state matches the canvas section color state.
32+
- PASS: Existing section buttons operate normally; missing Boss remains disabled/unwired.
33+
- PASS: Volume, Pan, Octave Range, and Transpose update `studioArrangement.previewLaneSettings`.
34+
- PASS: Audition keyboard tracks octave range and selected lane settings.
35+
- PASS: Piano audition click started Preview Synth oscillator playback and passed current lane settings into playback.
36+
- PASS: `selectedInstrumentId` sync works from Instruments tab to Octave Timeline and back.
37+
- PASS: Canvas hover and selected-cell dataset states update on pointer/click.
38+
- PASS: Global Play and Stop controls remain functional.
39+
40+
## Workspace V2 Failures
41+
The required workspace lane failed outside this PR's MIDI Studio scope. Representative failures:
42+
- `live-highlights Input Mapping V2 release, combo, and double-click mapping tokens`
43+
- `captures two-input combos and preserves Input Mapping V2 tile scroll during token updates`
44+
- `launches Input Mapping V2 and captures keyboard mappings`
45+
- Multiple Workspace Manager manifest tests expecting 11 tool tiles received 12.
46+
- One Asset Manager V2 session-context test timed out after 120000 ms.
47+
48+
These failures are not caused by the modified MIDI Studio V2 files and were not changed in this PR lane.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# PR_26146_072_075 Control Ownership Matrix
2+
3+
| Control | Owning tab | Editable/read-only | Canonical model field | Wired/unwired |
4+
| --- | --- | --- | --- | --- |
5+
| Song name | Song Setup | Editable | `music.songs[].name` | Wired |
6+
| Classification | Song Setup | Editable | `music.songs[].classification` | Wired |
7+
| Generated ID | Song Setup | Read-only | `music.songs[].id` from `camelCase(Name) + "-" + Classification` | Wired |
8+
| Tempo/BPM | Song Setup | Editable | `music.songs[].studioArrangement.tempo` | Wired |
9+
| Key | Song Setup | Editable | `music.songs[].studioArrangement.key` | Wired |
10+
| Style | Song Setup | Editable | `music.songs[].studioArrangement.style` | Wired |
11+
| Notes | Song Setup | Editable | `music.songs[].director.notes` | Wired |
12+
| Named musical section inputs | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.sections` | Wired |
13+
| Custom musical sections | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.sections` | Wired |
14+
| Available Sections list | Song Setup | Read-only derived | Populated Song Sheet section inputs | Wired |
15+
| Available Sections count | Song Setup | Read-only derived | Populated Song Sheet section inputs | Wired |
16+
| Add to Sequence | Song Setup | Action | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
17+
| Song Sequence list | Song Setup | Editable order/list | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
18+
| Song Sequence count | Song Setup | Read-only derived | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
19+
| Duplicate sequence item | Song Setup | Action | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
20+
| Move sequence item up/down | Song Setup | Action | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
21+
| Remove sequence item | Song Setup | Action | `music.songs[].studioArrangement.songSheet.sequence` | Wired |
22+
| Drag/drop sequence | Song Setup | Future action | Future sequence editing gesture | Unwired red |
23+
| Apply Song Sheet To: Chords/Pad | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.applyTargets.chordsPad` | Wired |
24+
| Apply Song Sheet To: Bass | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.applyTargets.bass` | Wired |
25+
| Apply Song Sheet To: Drums | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.applyTargets.drums` | Wired |
26+
| Apply Song Sheet To: Lead | Song Setup | Editable | `music.songs[].studioArrangement.songSheet.applyTargets.lead` | Wired |
27+
| Parse Guided Song Sheet | Song Setup | Action | `music.songs[].studioArrangement` | Wired |
28+
| Octave Timeline canvas cells | Octave Timeline | Editable notes | `music.songs[].studioArrangement.lanes[selectedInstrumentId]` | Wired |
29+
| Octave Timeline quick instrument select | Octave Timeline | Action/state | `tools.midi-studio-v2.selectedInstrumentId` | Wired |
30+
| Octave Timeline Mute/Solo/Show | Octave Timeline | Quick performance state | `studioArrangement.previewLaneSettings.muted/soloed/visible` | Wired |
31+
| Section select/play buttons | Octave Timeline | Playback workflow state | Canonical grid sections derived from Song Sheet sequence | Wired |
32+
| Missing section buttons | Octave Timeline | Disabled placeholder | No canonical section exists | Unwired red/disabled |
33+
| GM Type dropdown | Instruments | Editable | `studioArrangement.previewLaneSettings.instrumentTypes` | Wired |
34+
| GM Instrument dropdown | Instruments | Editable | `studioArrangement.previewLaneSettings.instruments` | Wired |
35+
| Instrument display name | Instruments | Editable | `studioArrangement.previewLaneSettings.displayNames` | Wired |
36+
| Volume | Instruments | Editable | `studioArrangement.previewLaneSettings.volumes` | Wired |
37+
| Pan/Balance | Instruments | Editable | `studioArrangement.previewLaneSettings.pans` | Wired |
38+
| Octave Range | Instruments | Editable | `studioArrangement.previewLaneSettings.octaveRanges` | Wired |
39+
| Transpose | Instruments | Editable | `studioArrangement.previewLaneSettings.transposes` | Wired |
40+
| Velocity | Instruments | Editable | `studioArrangement.previewLaneSettings.velocities` | Wired |
41+
| Duration | Instruments | Editable | `studioArrangement.previewLaneSettings.durations` | Wired |
42+
| Effects controls | Instruments | Future placeholders | Future effects/settings | Unwired red |
43+
| Advanced MIDI controls | Instruments | Future placeholders | Future advanced MIDI settings | Unwired red |
44+
| Audition piano keyboard | Instruments | Action | Preview Synth from selected instrument settings | Wired |
45+
| MIDI source/import/inspect | MIDI Import | Editable/action | MIDI source normalization into `studioArrangement` | Wired |
46+
| Auto-create helpers | Auto-Create Parts | Action/helper fields | `studioArrangement.lanes` and helper-only generation settings | Wired |
47+
| Export audio save controls | Export | Future action | Future rendered audio workflow | Unwired red |
48+
| Diagnostics JSON/details | Diagnostics | Read-only derived | Current selected canonical song model and diagnostics | Wired |
49+
50+
No duplicate editable controls were added for song metadata, instrument settings, export settings, or sequence ownership.
51+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# PR_26146_072_075 Section Sequence Map
2+
3+
## Source Of Truth
4+
The authoritative musical-section order is `music.songs[].studioArrangement.songSheet.sequence`.
5+
Only populated named sections from `music.songs[].studioArrangement.songSheet.sections` appear in Available Sections.
6+
7+
## Valid Sections
8+
For the targeted validation song:
9+
10+
| Section | Populated | Available Sections | Color owner |
11+
| --- | --- | --- | --- |
12+
| Intro | Yes | Yes | Song Sheet sequence index 0 |
13+
| Verse | Yes | Yes | Song Sheet sequence index 1 |
14+
| Chorus | Yes | Yes | Song Sheet sequence index 2 |
15+
| Bridge | No | No | Not assigned until populated |
16+
| Outro | Yes | Yes | Song Sheet sequence index 3 |
17+
18+
## Sequence Flow
19+
The validated sequence after Parse Guided Song Sheet is:
20+
21+
| Order | Section | Color index |
22+
| --- | --- | --- |
23+
| 1 | Intro | 0 |
24+
| 2 | Verse | 1 |
25+
| 3 | Chorus | 2 |
26+
| 4 | Verse | 1 |
27+
| 5 | Chorus | 2 |
28+
| 6 | Outro | 3 |
29+
30+
Repeated sequence items reuse the same section color as their section definition.
31+
32+
## Color Synchronization
33+
Section colors are shared through the MIDI Studio V2 section color helpers:
34+
35+
| Surface | Color source | Validation |
36+
| --- | --- | --- |
37+
| Song Sequence items | Populated section label color index | PASS |
38+
| Octave Timeline bars | Parsed canonical `studioArrangement.sections` generated from sequence | PASS |
39+
| Playback section buttons | Matching section label in current result sections | PASS |
40+
| Missing section buttons | No canonical section exists | PASS, disabled/unwired |
41+
42+
## Playback Synchronization
43+
Playback order follows the parsed canonical arrangement generated from the Song Sheet sequence. Section playback buttons inherit the same section labels and colors used by the canvas timeline. Play and Stop remained functional in targeted validation.
44+

src/engine/audio/PreviewSynthEngine.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ function chordNotes(chordName) {
5959
return tones ? tones.map((tone) => `${tone}4`) : [];
6060
}
6161

62+
function finiteNumber(value, fallback) {
63+
const number = Number(value);
64+
return Number.isFinite(number) ? number : fallback;
65+
}
66+
67+
function clampNumber(value, min, max, fallback) {
68+
return Math.max(min, Math.min(max, finiteNumber(value, fallback)));
69+
}
70+
6271
export class PreviewSynthEngine {
6372
constructor({ windowRef = globalThis } = {}) {
6473
this.context = null;
@@ -238,8 +247,10 @@ export class PreviewSynthEngine {
238247
warningKeys.add(key);
239248
}
240249
}
241-
if (this.canScheduleEvent(event, instrument)) {
242-
events.push({ ...event, previewInstrument: instrument });
250+
const adjustedEvent = this.eventWithLaneSettings(event, laneSettings);
251+
const adjustedInstrument = this.instrumentWithLaneSettings(instrument, event.lane, laneSettings);
252+
if (this.canScheduleEvent(adjustedEvent, adjustedInstrument)) {
253+
events.push({ ...adjustedEvent, previewInstrument: adjustedInstrument });
243254
}
244255
});
245256
return {
@@ -249,6 +260,29 @@ export class PreviewSynthEngine {
249260
};
250261
}
251262

263+
instrumentWithLaneSettings(instrument, lane, laneSettings = {}) {
264+
const baseVolume = finiteNumber(instrument?.volume, null);
265+
const fallbackVolume = instrument?.synthRole === "percussion" ? 0.16 : instrument?.synthRole === "pad" ? 0.1 : 0.11;
266+
const laneVolume = clampNumber(laneSettings.volumes?.[lane], 0, 1, 1);
267+
const velocityScale = clampNumber(laneSettings.velocities?.[lane], 1, 127, 100) / 100;
268+
const transpose = clampNumber(laneSettings.transposes?.[lane], -24, 24, 0);
269+
const pan = clampNumber(laneSettings.pans?.[lane], -1, 1, 0);
270+
return {
271+
...instrument,
272+
pan,
273+
transposeSemitones: finiteNumber(instrument?.transposeSemitones, 0) + transpose,
274+
volumeOverride: Math.max(0, (baseVolume ?? fallbackVolume) * laneVolume * velocityScale)
275+
};
276+
}
277+
278+
eventWithLaneSettings(event, laneSettings = {}) {
279+
const durationScale = clampNumber(laneSettings.durations?.[event.lane], 0.1, 8, 1);
280+
return {
281+
...event,
282+
durationBeats: Math.max(0.05, finiteNumber(event.durationBeats, 1) * durationScale)
283+
};
284+
}
285+
252286
scheduleEvents({ context, events, secondsPerBeat, secondsPerStep, startStep }) {
253287
const now = context.currentTime;
254288
events.forEach((event) => {
@@ -359,7 +393,10 @@ export class PreviewSynthEngine {
359393

360394
volumeForEvent(event, instrument = null) {
361395
const fallbackVolume = event.kind === "chord" ? 0.1 : event.kind === "drum" ? 0.16 : 0.11;
362-
const volume = Number(instrument?.volume || fallbackVolume);
396+
if (Number.isFinite(Number(instrument?.volumeOverride))) {
397+
return Math.max(0, Math.min(1, Number(instrument.volumeOverride)));
398+
}
399+
const volume = Number(instrument?.volume ?? fallbackVolume);
363400
if (event.kind === "drum") {
364401
return Math.max(volume, 0.16);
365402
}

0 commit comments

Comments
 (0)