Add piano demo playback on Enter with stop on C

This commit is contained in:
Jage9
2026-02-23 01:18:17 -05:00
parent 95a1721b5b
commit 4aa7f0bf4d
5 changed files with 94 additions and 4 deletions

View File

@@ -87,7 +87,7 @@
},
{
"keys": "Piano mode",
"description": "When using a piano: press question mark for piano help. 1-9 (and 0 for the 10th slot) changes instrument, -/= changes octave, ASDFGHJKL;' plays C major notes, WETYUOP] plays sharps, Z starts/stops recording, X plays recording, C stops playback, Escape exits"
"description": "When using a piano: press question mark for piano help. 1-9 (and 0 for the 10th slot) changes instrument, -/= changes octave, ASDFGHJKL;' plays C major notes, WETYUOP] plays sharps, Z starts/stops recording, X plays recording, Enter plays demo, C stops demo/playback, Escape exits"
}
]
},

View File

@@ -27,9 +27,13 @@
"keys": "X",
"description": "Play the saved recording."
},
{
"keys": "Enter",
"description": "Play demo melody. Press Enter again to restart it."
},
{
"keys": "C",
"description": "Stop recording playback."
"description": "Stop demo and recording playback."
},
{
"keys": "Escape",

View File

@@ -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 R211";
window.CHGRID_WEB_VERSION = "2026.02.23 R212";
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -112,6 +112,26 @@ const PIANO_SHARP_KEY_MIDI_BY_CODE: Record<string, number> = {
KeyP: 75,
BracketRight: 78,
};
const PIANO_DEMO_STEPS_F_MAJOR: Array<{ midi: number; durationMs: number; gapMs: number }> = [
{ midi: 65, durationMs: 220, gapMs: 40 }, // F4
{ midi: 69, durationMs: 220, gapMs: 40 }, // A4
{ midi: 72, durationMs: 280, gapMs: 60 }, // C5
{ midi: 74, durationMs: 220, gapMs: 40 }, // D5
{ midi: 72, durationMs: 220, gapMs: 40 }, // C5
{ midi: 69, durationMs: 260, gapMs: 60 }, // A4
{ midi: 70, durationMs: 220, gapMs: 40 }, // Bb4
{ midi: 69, durationMs: 220, gapMs: 40 }, // A4
{ midi: 67, durationMs: 260, gapMs: 60 }, // G4
{ midi: 65, durationMs: 360, gapMs: 90 }, // F4
{ midi: 67, durationMs: 220, gapMs: 40 }, // G4
{ midi: 69, durationMs: 220, gapMs: 40 }, // A4
{ midi: 70, durationMs: 220, gapMs: 40 }, // Bb4
{ midi: 72, durationMs: 280, gapMs: 60 }, // C5
{ midi: 70, durationMs: 220, gapMs: 40 }, // Bb4
{ midi: 69, durationMs: 220, gapMs: 40 }, // A4
{ midi: 67, durationMs: 260, gapMs: 60 }, // G4
{ midi: 65, durationMs: 420, gapMs: 120 }, // F4
];
declare global {
interface Window {
@@ -274,6 +294,10 @@ const activePianoKeys = new Set<string>();
const activePianoKeyMidi = new Map<string, number>();
const activePianoHeldOrder: string[] = [];
let activePianoMonophonicKey: string | null = null;
let activePianoDemoRunToken = 0;
let activePianoDemoItemId: string | null = null;
const activePianoDemoTimeoutIds: number[] = [];
const activePianoDemoNotes = new Map<string, number>();
const activeRemotePianoKeys = new Set<string>();
let pianoPreviewTimeoutId: number | null = null;
let activeTeleport:
@@ -928,6 +952,7 @@ async function startPianoUseMode(itemId: string): Promise<void> {
/** Exits local piano key mode and releases any held notes. */
function stopPianoUseMode(announce = true): void {
if (!activePianoItemId) return;
stopPianoDemo(true);
const itemId = activePianoItemId;
for (const code of Array.from(activePianoKeys)) {
const midi = activePianoKeyMidi.get(code);
@@ -976,6 +1001,59 @@ function playLocalPianoNote(
signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: true });
}
/** Stops active piano demo notes/timeouts and optionally emits note-off packets. */
function stopPianoDemo(sendNoteOff = true): boolean {
const hadActiveDemo = activePianoDemoNotes.size > 0 || activePianoDemoTimeoutIds.length > 0;
activePianoDemoRunToken += 1;
while (activePianoDemoTimeoutIds.length > 0) {
const timeoutId = activePianoDemoTimeoutIds.pop();
if (typeof timeoutId === 'number') {
window.clearTimeout(timeoutId);
}
}
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 });
}
}
activePianoDemoNotes.clear();
activePianoDemoItemId = null;
return hadActiveDemo;
}
/** Starts the built-in piano demo sequence from the beginning. */
function startPianoDemo(item: WorldItem, itemId: string): void {
stopPianoDemo(true);
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(() => {
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;
}
}
/** Handles key release while in piano mode, including mono fallback retrigger behavior. */
function handlePianoUseModeKeyUp(code: string): void {
if (!activePianoKeys.delete(code)) return;
@@ -2456,6 +2534,12 @@ function handlePianoUseModeInput(code: string): void {
stopPianoUseMode(false);
return;
}
if (code === 'Enter') {
startPianoDemo(item, itemId);
updateStatus('demo play');
audio.sfxUiBlip();
return;
}
if (code === 'KeyZ') {
signaling.send({ type: 'item_piano_recording', itemId, action: 'toggle_record' });
return;
@@ -2465,6 +2549,7 @@ function handlePianoUseModeInput(code: string): void {
return;
}
if (code === 'KeyC') {
stopPianoDemo(true);
signaling.send({ type: 'item_piano_recording', itemId, action: 'stop_playback' });
return;
}