Add piano demo playback on Enter with stop on C
This commit is contained in:
@@ -87,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keys": "Piano mode",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,9 +27,13 @@
|
|||||||
"keys": "X",
|
"keys": "X",
|
||||||
"description": "Play the saved recording."
|
"description": "Play the saved recording."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"keys": "Enter",
|
||||||
|
"description": "Play demo melody. Press Enter again to restart it."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"keys": "C",
|
"keys": "C",
|
||||||
"description": "Stop recording playback."
|
"description": "Stop demo and recording playback."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"keys": "Escape",
|
"keys": "Escape",
|
||||||
|
|||||||
@@ -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 R211";
|
window.CHGRID_WEB_VERSION = "2026.02.23 R212";
|
||||||
// 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,6 +112,26 @@ 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 }> = [
|
||||||
|
{ 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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -274,6 +294,10 @@ const activePianoKeys = new Set<string>();
|
|||||||
const activePianoKeyMidi = new Map<string, number>();
|
const activePianoKeyMidi = new Map<string, number>();
|
||||||
const activePianoHeldOrder: string[] = [];
|
const activePianoHeldOrder: string[] = [];
|
||||||
let activePianoMonophonicKey: string | null = null;
|
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>();
|
const activeRemotePianoKeys = new Set<string>();
|
||||||
let pianoPreviewTimeoutId: number | null = null;
|
let pianoPreviewTimeoutId: number | null = null;
|
||||||
let activeTeleport:
|
let activeTeleport:
|
||||||
@@ -928,6 +952,7 @@ async function startPianoUseMode(itemId: string): Promise<void> {
|
|||||||
/** Exits local piano key mode and releases any held notes. */
|
/** Exits local piano key mode and releases any held notes. */
|
||||||
function stopPianoUseMode(announce = true): void {
|
function stopPianoUseMode(announce = true): void {
|
||||||
if (!activePianoItemId) return;
|
if (!activePianoItemId) return;
|
||||||
|
stopPianoDemo(true);
|
||||||
const itemId = activePianoItemId;
|
const itemId = activePianoItemId;
|
||||||
for (const code of Array.from(activePianoKeys)) {
|
for (const code of Array.from(activePianoKeys)) {
|
||||||
const midi = activePianoKeyMidi.get(code);
|
const midi = activePianoKeyMidi.get(code);
|
||||||
@@ -976,6 +1001,59 @@ function playLocalPianoNote(
|
|||||||
signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: true });
|
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. */
|
/** Handles key release while in piano mode, including mono fallback retrigger behavior. */
|
||||||
function handlePianoUseModeKeyUp(code: string): void {
|
function handlePianoUseModeKeyUp(code: string): void {
|
||||||
if (!activePianoKeys.delete(code)) return;
|
if (!activePianoKeys.delete(code)) return;
|
||||||
@@ -2456,6 +2534,12 @@ function handlePianoUseModeInput(code: string): void {
|
|||||||
stopPianoUseMode(false);
|
stopPianoUseMode(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (code === 'Enter') {
|
||||||
|
startPianoDemo(item, itemId);
|
||||||
|
updateStatus('demo play');
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (code === 'KeyZ') {
|
if (code === 'KeyZ') {
|
||||||
signaling.send({ type: 'item_piano_recording', itemId, action: 'toggle_record' });
|
signaling.send({ type: 'item_piano_recording', itemId, action: 'toggle_record' });
|
||||||
return;
|
return;
|
||||||
@@ -2465,6 +2549,7 @@ function handlePianoUseModeInput(code: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (code === 'KeyC') {
|
if (code === 'KeyC') {
|
||||||
|
stopPianoDemo(true);
|
||||||
signaling.send({ type: 'item_piano_recording', itemId, action: 'stop_playback' });
|
signaling.send({ type: 'item_piano_recording', itemId, action: 'stop_playback' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ Applies to effect select, user/item list modes, item selection, item property li
|
|||||||
- `-` / `=`: Shift octave down/up
|
- `-` / `=`: Shift octave down/up
|
||||||
- `Z`: Start/stop recording on this piano (max 30s)
|
- `Z`: Start/stop recording on this piano (max 30s)
|
||||||
- `X`: Play back saved recording on this piano
|
- `X`: Play back saved recording on this piano
|
||||||
- `C`: Stop playback on this piano
|
- `Enter`: Play demo melody (press again to restart)
|
||||||
|
- `C`: Stop demo/playback on this piano
|
||||||
- `Escape`: Exit piano mode
|
- `Escape`: Exit piano mode
|
||||||
|
|
||||||
## Help Viewer Mode
|
## Help Viewer Mode
|
||||||
|
|||||||
Reference in New Issue
Block a user