From 0f616a3fe848071b7eeb4b96ec29680034dfd048 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Mon, 23 Feb 2026 01:35:46 -0500 Subject: [PATCH] Load piano Enter-demo from external recording JSON --- client/public/piano_demo.json | 4 + client/public/version.js | 2 +- client/src/main.ts | 164 ++++++++++++++++++++-------------- 3 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 client/public/piano_demo.json diff --git a/client/public/piano_demo.json b/client/public/piano_demo.json new file mode 100644 index 0000000..53d7f1b --- /dev/null +++ b/client/public/piano_demo.json @@ -0,0 +1,4 @@ +{ + "recordingLengthMs": 17394, + "recording": [{"t":118,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":226,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":946,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":1096,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":1459,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":1573,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2018,"keyId":"KeyG","midi":67,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2125,"keyId":"KeyG","midi":67,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2291,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2419,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2590,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2716,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2856,"keyId":"Semicolon","midi":76,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":2958,"keyId":"Semicolon","midi":76,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":3158,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":3259,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":3941,"keyId":"KeyG","midi":67,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4068,"keyId":"KeyG","midi":67,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4183,"keyId":"KeyG","midi":67,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4302,"keyId":"KeyG","midi":67,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4483,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4597,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4738,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":4900,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":5001,"keyId":"Semicolon","midi":76,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":5122,"keyId":"Semicolon","midi":76,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":5504,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":5600,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6347,"keyId":"KeyH","midi":69,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6473,"keyId":"KeyH","midi":69,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6601,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6769,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6853,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":6990,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":7133,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":7228,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":7428,"keyId":"KeyH","midi":69,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":7550,"keyId":"KeyH","midi":69,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":8659,"keyId":"Semicolon","midi":76,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":8660,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":8768,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":8792,"keyId":"Semicolon","midi":76,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":9492,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":9492,"keyId":"KeyF","midi":65,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":9600,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":9600,"keyId":"KeyF","midi":65,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":10429,"keyId":"KeyH","midi":69,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":10537,"keyId":"KeyH","midi":69,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":10749,"keyId":"Semicolon","midi":76,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":10896,"keyId":"Semicolon","midi":76,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11008,"keyId":"Quote","midi":77,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11149,"keyId":"Quote","midi":77,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11253,"keyId":"Semicolon","midi":76,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11384,"keyId":"Semicolon","midi":76,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11569,"keyId":"KeyL","midi":74,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":11677,"keyId":"KeyL","midi":74,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":12423,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":12557,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":12695,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":12798,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":12936,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":13045,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":13297,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":13440,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":13619,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":13750,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":14139,"keyId":"KeyK","midi":72,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":14272,"keyId":"KeyK","midi":72,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":14658,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":14784,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":15208,"keyId":"KeyG","midi":67,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":15317,"keyId":"KeyG","midi":67,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":15781,"keyId":"KeyJ","midi":71,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":15882,"keyId":"KeyJ","midi":71,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":16334,"keyId":"KeyG","midi":67,"on":true,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15},{"t":16428,"keyId":"KeyG","midi":67,"on":false,"instrument":"piano","voiceMode":"poly","attack":15,"decay":45,"release":35,"brightness":55,"emitRange":15}] +} diff --git a/client/public/version.js b/client/public/version.js index bbbf0ce..d455d36 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.23 R215"; +window.CHGRID_WEB_VERSION = "2026.02.23 R216"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 5524e10..b02a81d 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -112,49 +112,19 @@ const PIANO_SHARP_KEY_MIDI_BY_CODE: Record = { KeyP: 75, BracketRight: 78, }; -const PIANO_DEMO_STEPS_F_MAJOR: Array<{ midi: number; durationMs: number; gapMs: number }> = [ - // "Yama no Ongakuka" / "Das Lied vom Musikanten" core melody in F major. - { midi: 65, durationMs: 240, gapMs: 40 }, // F4 - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 72, durationMs: 240, gapMs: 60 }, // C5 - { midi: 72, durationMs: 240, gapMs: 40 }, // C5 - { midi: 70, durationMs: 240, gapMs: 40 }, // Bb4 - { midi: 69, durationMs: 240, gapMs: 60 }, // A4 - { midi: 67, durationMs: 240, gapMs: 40 }, // G4 - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 65, durationMs: 240, gapMs: 60 }, // F4 - { midi: 65, durationMs: 480, gapMs: 120 }, // F4 (held) - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 70, durationMs: 240, gapMs: 40 }, // Bb4 - { midi: 72, durationMs: 240, gapMs: 60 }, // C5 - { midi: 72, durationMs: 240, gapMs: 40 }, // C5 - { midi: 74, durationMs: 240, gapMs: 40 }, // D5 - { midi: 72, durationMs: 240, gapMs: 60 }, // C5 - { midi: 70, durationMs: 240, gapMs: 40 }, // Bb4 - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 67, durationMs: 240, gapMs: 60 }, // G4 - { midi: 65, durationMs: 480, gapMs: 120 }, // F4 (held) - { midi: 72, durationMs: 240, gapMs: 40 }, // C5 - { midi: 72, durationMs: 240, gapMs: 40 }, // C5 - { midi: 74, durationMs: 240, gapMs: 60 }, // D5 - { midi: 65, durationMs: 240, gapMs: 40 }, // F4 - { midi: 65, durationMs: 240, gapMs: 40 }, // F4 - { midi: 65, durationMs: 240, gapMs: 60 }, // F4 - { midi: 67, durationMs: 240, gapMs: 40 }, // G4 - { midi: 65, durationMs: 240, gapMs: 40 }, // F4 - { midi: 64, durationMs: 240, gapMs: 60 }, // E4 - { midi: 65, durationMs: 480, gapMs: 120 }, // F4 (held) - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 70, durationMs: 240, gapMs: 40 }, // Bb4 - { midi: 72, durationMs: 240, gapMs: 60 }, // C5 - { midi: 72, durationMs: 240, gapMs: 40 }, // C5 - { midi: 74, durationMs: 240, gapMs: 40 }, // D5 - { midi: 72, durationMs: 240, gapMs: 60 }, // C5 - { midi: 70, durationMs: 240, gapMs: 40 }, // Bb4 - { midi: 69, durationMs: 240, gapMs: 40 }, // A4 - { midi: 67, durationMs: 240, gapMs: 60 }, // G4 - { midi: 65, durationMs: 520, gapMs: 140 }, // F4 (held, phrase ending) -]; +type PianoDemoEvent = { + t: number; + keyId: string; + midi: number; + on: boolean; + instrument?: string; + voiceMode?: 'mono' | 'poly'; + attack?: number; + decay?: number; + release?: number; + brightness?: number; + emitRange?: number; +}; declare global { interface Window { @@ -320,7 +290,8 @@ let activePianoMonophonicKey: string | null = null; let activePianoDemoRunToken = 0; let activePianoDemoItemId: string | null = null; const activePianoDemoTimeoutIds: number[] = []; -const activePianoDemoNotes = new Map(); +const activePianoDemoNotes = new Map(); +let pianoDemoEvents: PianoDemoEvent[] = []; const activeRemotePianoKeys = new Set(); let pianoPreviewTimeoutId: number | null = null; let activeTeleport: @@ -375,6 +346,7 @@ loadMicInputGain(); loadMasterVolume(); void loadHelp(); void loadPianoHelp(); +void loadPianoDemo(); void loadChangelog(); /** Fetches a required DOM element and casts it to the requested element type. */ @@ -491,6 +463,45 @@ async function loadPianoHelp(): Promise { } } +/** Loads piano demo note events from `piano_demo.json` for Enter-key demo playback. */ +async function loadPianoDemo(): Promise { + try { + const response = await fetch(withBase('piano_demo.json'), { cache: 'no-store' }); + if (!response.ok) { + return; + } + const data = (await response.json()) as { recording?: unknown }; + const rawEvents = Array.isArray(data.recording) ? data.recording : []; + const parsed: PianoDemoEvent[] = []; + for (const entry of rawEvents) { + if (!entry || typeof entry !== 'object') continue; + const record = entry as Record; + const t = Number(record.t); + const midi = Number(record.midi); + const keyId = String(record.keyId ?? '').trim(); + const on = record.on === true; + if (!Number.isFinite(t) || !Number.isFinite(midi) || !keyId) continue; + parsed.push({ + t: Math.max(0, Math.round(t)), + keyId: keyId.slice(0, 32), + midi: Math.max(0, Math.min(127, Math.round(midi))), + on, + instrument: typeof record.instrument === 'string' ? record.instrument : undefined, + voiceMode: record.voiceMode === 'mono' ? 'mono' : record.voiceMode === 'poly' ? 'poly' : undefined, + attack: Number.isFinite(Number(record.attack)) ? Math.max(0, Math.min(100, Math.round(Number(record.attack)))) : undefined, + decay: Number.isFinite(Number(record.decay)) ? Math.max(0, Math.min(100, Math.round(Number(record.decay)))) : undefined, + release: Number.isFinite(Number(record.release)) ? Math.max(0, Math.min(100, Math.round(Number(record.release)))) : undefined, + brightness: Number.isFinite(Number(record.brightness)) ? Math.max(0, Math.min(100, Math.round(Number(record.brightness)))) : undefined, + emitRange: Number.isFinite(Number(record.emitRange)) ? Math.max(5, Math.min(20, Math.round(Number(record.emitRange)))) : undefined, + }); + } + parsed.sort((a, b) => a.t - b.t); + pianoDemoEvents = parsed; + } catch { + // Demo remains unavailable if loading/parsing fails. + } +} + /** Renders changelog sections into the collapsible updates panel. */ function renderChangelog(changelog: ChangelogData): void { dom.updatesPanel.innerHTML = ''; @@ -1035,13 +1046,13 @@ function stopPianoDemo(sendNoteOff = true): boolean { } } const itemId = activePianoDemoItemId; - for (const [keyId, midi] of Array.from(activePianoDemoNotes.entries())) { - pianoSynth.noteOff(keyId); - if (sendNoteOff && itemId && Number.isFinite(midi)) { - signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: false }); + for (const [logicalKey, note] of Array.from(activePianoDemoNotes.entries())) { + pianoSynth.noteOff(note.runtimeKey); + if (sendNoteOff && itemId && Number.isFinite(note.midi)) { + signaling.send({ type: 'item_piano_note', itemId, keyId: note.runtimeKey, midi: note.midi, on: false }); } + activePianoDemoNotes.delete(logicalKey); } - activePianoDemoNotes.clear(); activePianoDemoItemId = null; return hadActiveDemo; } @@ -1049,31 +1060,46 @@ function stopPianoDemo(sendNoteOff = true): boolean { /** Starts the built-in piano demo sequence from the beginning. */ function startPianoDemo(item: WorldItem, itemId: string): void { stopPianoDemo(true); + if (pianoDemoEvents.length === 0) { + updateStatus('demo unavailable'); + audio.sfxUiCancel(); + return; + } const runToken = activePianoDemoRunToken; activePianoDemoItemId = itemId; - let atMs = 0; - for (let index = 0; index < PIANO_DEMO_STEPS_F_MAJOR.length; index += 1) { - const step = PIANO_DEMO_STEPS_F_MAJOR[index]!; - const startTimeoutId = window.setTimeout(() => { + for (let index = 0; index < pianoDemoEvents.length; index += 1) { + const event = pianoDemoEvents[index]!; + const timeoutId = window.setTimeout(() => { if (runToken !== activePianoDemoRunToken) return; const liveItem = state.items.get(itemId); if (!liveItem || liveItem.type !== 'piano') return; - const liveConfig = getPianoParams(liveItem); - const midi = Math.max(0, Math.min(127, step.midi + liveConfig.octave * 12)); - const keyId = `__piano_demo_${runToken}_${index}`; - activePianoDemoNotes.set(keyId, midi); - playLocalPianoNote(liveItem, itemId, keyId, midi, liveConfig); - const stopTimeoutId = window.setTimeout(() => { - if (runToken !== activePianoDemoRunToken) return; - if (!activePianoDemoNotes.has(keyId)) return; - activePianoDemoNotes.delete(keyId); - pianoSynth.noteOff(keyId); - signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: false }); - }, step.durationMs); - activePianoDemoTimeoutIds.push(stopTimeoutId); - }, atMs); - activePianoDemoTimeoutIds.push(startTimeoutId); - atMs += step.durationMs + step.gapMs; + const baseConfig = getPianoParams(liveItem); + const config = { + instrument: event.instrument ? normalizePianoInstrument(event.instrument) : baseConfig.instrument, + voiceMode: event.voiceMode ?? baseConfig.voiceMode, + octave: 0, + attack: event.attack ?? baseConfig.attack, + decay: event.decay ?? baseConfig.decay, + release: event.release ?? baseConfig.release, + brightness: event.brightness ?? baseConfig.brightness, + emitRange: event.emitRange ?? baseConfig.emitRange, + } as ReturnType; + const logicalKey = event.keyId; + const runtimeKey = `__piano_demo_${logicalKey}`; + if (event.on) { + if (activePianoDemoNotes.has(logicalKey)) return; + activePianoDemoNotes.set(logicalKey, { runtimeKey, midi: event.midi }); + playLocalPianoNote(liveItem, itemId, runtimeKey, event.midi, config); + } else { + const active = activePianoDemoNotes.get(logicalKey); + if (active) { + activePianoDemoNotes.delete(logicalKey); + pianoSynth.noteOff(active.runtimeKey); + signaling.send({ type: 'item_piano_note', itemId, keyId: active.runtimeKey, midi: active.midi, on: false }); + } + } + }, event.t); + activePianoDemoTimeoutIds.push(timeoutId); } }