Load piano Enter-demo from external recording JSON
This commit is contained in:
4
client/public/piano_demo.json
Normal file
4
client/public/piano_demo.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// 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.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -112,49 +112,19 @@ const PIANO_SHARP_KEY_MIDI_BY_CODE: Record<string, number> = {
|
|||||||
KeyP: 75,
|
KeyP: 75,
|
||||||
BracketRight: 78,
|
BracketRight: 78,
|
||||||
};
|
};
|
||||||
const PIANO_DEMO_STEPS_F_MAJOR: Array<{ midi: number; durationMs: number; gapMs: number }> = [
|
type PianoDemoEvent = {
|
||||||
// "Yama no Ongakuka" / "Das Lied vom Musikanten" core melody in F major.
|
t: number;
|
||||||
{ midi: 65, durationMs: 240, gapMs: 40 }, // F4
|
keyId: string;
|
||||||
{ midi: 69, durationMs: 240, gapMs: 40 }, // A4
|
midi: number;
|
||||||
{ midi: 72, durationMs: 240, gapMs: 60 }, // C5
|
on: boolean;
|
||||||
{ midi: 72, durationMs: 240, gapMs: 40 }, // C5
|
instrument?: string;
|
||||||
{ midi: 70, durationMs: 240, gapMs: 40 }, // Bb4
|
voiceMode?: 'mono' | 'poly';
|
||||||
{ midi: 69, durationMs: 240, gapMs: 60 }, // A4
|
attack?: number;
|
||||||
{ midi: 67, durationMs: 240, gapMs: 40 }, // G4
|
decay?: number;
|
||||||
{ midi: 69, durationMs: 240, gapMs: 40 }, // A4
|
release?: number;
|
||||||
{ midi: 65, durationMs: 240, gapMs: 60 }, // F4
|
brightness?: number;
|
||||||
{ midi: 65, durationMs: 480, gapMs: 120 }, // F4 (held)
|
emitRange?: number;
|
||||||
{ 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)
|
|
||||||
];
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -320,7 +290,8 @@ let activePianoMonophonicKey: string | null = null;
|
|||||||
let activePianoDemoRunToken = 0;
|
let activePianoDemoRunToken = 0;
|
||||||
let activePianoDemoItemId: string | null = null;
|
let activePianoDemoItemId: string | null = null;
|
||||||
const activePianoDemoTimeoutIds: number[] = [];
|
const activePianoDemoTimeoutIds: number[] = [];
|
||||||
const activePianoDemoNotes = new Map<string, number>();
|
const activePianoDemoNotes = new Map<string, { runtimeKey: string; midi: number }>();
|
||||||
|
let pianoDemoEvents: PianoDemoEvent[] = [];
|
||||||
const activeRemotePianoKeys = new Set<string>();
|
const activeRemotePianoKeys = new Set<string>();
|
||||||
let pianoPreviewTimeoutId: number | null = null;
|
let pianoPreviewTimeoutId: number | null = null;
|
||||||
let activeTeleport:
|
let activeTeleport:
|
||||||
@@ -375,6 +346,7 @@ loadMicInputGain();
|
|||||||
loadMasterVolume();
|
loadMasterVolume();
|
||||||
void loadHelp();
|
void loadHelp();
|
||||||
void loadPianoHelp();
|
void loadPianoHelp();
|
||||||
|
void loadPianoDemo();
|
||||||
void loadChangelog();
|
void loadChangelog();
|
||||||
|
|
||||||
/** Fetches a required DOM element and casts it to the requested element type. */
|
/** Fetches a required DOM element and casts it to the requested element type. */
|
||||||
@@ -491,6 +463,45 @@ async function loadPianoHelp(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Loads piano demo note events from `piano_demo.json` for Enter-key demo playback. */
|
||||||
|
async function loadPianoDemo(): Promise<void> {
|
||||||
|
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<string, unknown>;
|
||||||
|
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. */
|
/** Renders changelog sections into the collapsible updates panel. */
|
||||||
function renderChangelog(changelog: ChangelogData): void {
|
function renderChangelog(changelog: ChangelogData): void {
|
||||||
dom.updatesPanel.innerHTML = '';
|
dom.updatesPanel.innerHTML = '';
|
||||||
@@ -1035,13 +1046,13 @@ function stopPianoDemo(sendNoteOff = true): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const itemId = activePianoDemoItemId;
|
const itemId = activePianoDemoItemId;
|
||||||
for (const [keyId, midi] of Array.from(activePianoDemoNotes.entries())) {
|
for (const [logicalKey, note] of Array.from(activePianoDemoNotes.entries())) {
|
||||||
pianoSynth.noteOff(keyId);
|
pianoSynth.noteOff(note.runtimeKey);
|
||||||
if (sendNoteOff && itemId && Number.isFinite(midi)) {
|
if (sendNoteOff && itemId && Number.isFinite(note.midi)) {
|
||||||
signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: false });
|
signaling.send({ type: 'item_piano_note', itemId, keyId: note.runtimeKey, midi: note.midi, on: false });
|
||||||
}
|
}
|
||||||
|
activePianoDemoNotes.delete(logicalKey);
|
||||||
}
|
}
|
||||||
activePianoDemoNotes.clear();
|
|
||||||
activePianoDemoItemId = null;
|
activePianoDemoItemId = null;
|
||||||
return hadActiveDemo;
|
return hadActiveDemo;
|
||||||
}
|
}
|
||||||
@@ -1049,31 +1060,46 @@ function stopPianoDemo(sendNoteOff = true): boolean {
|
|||||||
/** Starts the built-in piano demo sequence from the beginning. */
|
/** Starts the built-in piano demo sequence from the beginning. */
|
||||||
function startPianoDemo(item: WorldItem, itemId: string): void {
|
function startPianoDemo(item: WorldItem, itemId: string): void {
|
||||||
stopPianoDemo(true);
|
stopPianoDemo(true);
|
||||||
|
if (pianoDemoEvents.length === 0) {
|
||||||
|
updateStatus('demo unavailable');
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const runToken = activePianoDemoRunToken;
|
const runToken = activePianoDemoRunToken;
|
||||||
activePianoDemoItemId = itemId;
|
activePianoDemoItemId = itemId;
|
||||||
let atMs = 0;
|
for (let index = 0; index < pianoDemoEvents.length; index += 1) {
|
||||||
for (let index = 0; index < PIANO_DEMO_STEPS_F_MAJOR.length; index += 1) {
|
const event = pianoDemoEvents[index]!;
|
||||||
const step = PIANO_DEMO_STEPS_F_MAJOR[index]!;
|
const timeoutId = window.setTimeout(() => {
|
||||||
const startTimeoutId = window.setTimeout(() => {
|
|
||||||
if (runToken !== activePianoDemoRunToken) return;
|
if (runToken !== activePianoDemoRunToken) return;
|
||||||
const liveItem = state.items.get(itemId);
|
const liveItem = state.items.get(itemId);
|
||||||
if (!liveItem || liveItem.type !== 'piano') return;
|
if (!liveItem || liveItem.type !== 'piano') return;
|
||||||
const liveConfig = getPianoParams(liveItem);
|
const baseConfig = getPianoParams(liveItem);
|
||||||
const midi = Math.max(0, Math.min(127, step.midi + liveConfig.octave * 12));
|
const config = {
|
||||||
const keyId = `__piano_demo_${runToken}_${index}`;
|
instrument: event.instrument ? normalizePianoInstrument(event.instrument) : baseConfig.instrument,
|
||||||
activePianoDemoNotes.set(keyId, midi);
|
voiceMode: event.voiceMode ?? baseConfig.voiceMode,
|
||||||
playLocalPianoNote(liveItem, itemId, keyId, midi, liveConfig);
|
octave: 0,
|
||||||
const stopTimeoutId = window.setTimeout(() => {
|
attack: event.attack ?? baseConfig.attack,
|
||||||
if (runToken !== activePianoDemoRunToken) return;
|
decay: event.decay ?? baseConfig.decay,
|
||||||
if (!activePianoDemoNotes.has(keyId)) return;
|
release: event.release ?? baseConfig.release,
|
||||||
activePianoDemoNotes.delete(keyId);
|
brightness: event.brightness ?? baseConfig.brightness,
|
||||||
pianoSynth.noteOff(keyId);
|
emitRange: event.emitRange ?? baseConfig.emitRange,
|
||||||
signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: false });
|
} as ReturnType<typeof getPianoParams>;
|
||||||
}, step.durationMs);
|
const logicalKey = event.keyId;
|
||||||
activePianoDemoTimeoutIds.push(stopTimeoutId);
|
const runtimeKey = `__piano_demo_${logicalKey}`;
|
||||||
}, atMs);
|
if (event.on) {
|
||||||
activePianoDemoTimeoutIds.push(startTimeoutId);
|
if (activePianoDemoNotes.has(logicalKey)) return;
|
||||||
atMs += step.durationMs + step.gapMs;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user