Add shared piano recording/playback and mono key fallback

This commit is contained in:
Jage9
2026-02-23 00:36:36 -05:00
parent b4cf85ac44
commit 93b9d19455
13 changed files with 484 additions and 61 deletions

View File

@@ -256,6 +256,8 @@ let activeTeleportLoopToken = 0;
let activePianoItemId: string | null = null;
const activePianoKeys = new Set<string>();
const activePianoKeyMidi = new Map<string, number>();
const activePianoHeldOrder: string[] = [];
let activePianoMonophonicKey: string | null = null;
const activeRemotePianoKeys = new Set<string>();
let pianoPreviewTimeoutId: number | null = null;
let activeTeleport:
@@ -881,6 +883,8 @@ async function startPianoUseMode(itemId: string): Promise<void> {
activePianoItemId = itemId;
activePianoKeys.clear();
activePianoKeyMidi.clear();
activePianoHeldOrder.length = 0;
activePianoMonophonicKey = null;
state.mode = 'pianoUse';
await audio.ensureContext();
updateStatus(`using ${item.title}, press escape to stop.`);
@@ -900,6 +904,8 @@ function stopPianoUseMode(announce = true): void {
activePianoItemId = null;
activePianoKeys.clear();
activePianoKeyMidi.clear();
activePianoHeldOrder.length = 0;
activePianoMonophonicKey = null;
state.mode = 'normal';
if (announce) {
updateStatus('Stopped piano.');
@@ -907,6 +913,85 @@ function stopPianoUseMode(announce = true): void {
}
}
/** Starts one local piano note and sends the matching network note-on packet. */
function playLocalPianoNote(
item: WorldItem,
itemId: string,
keyId: string,
midi: number,
config: ReturnType<typeof getPianoParams>,
): void {
const ctx = audio.context;
const destination = audio.getOutputDestinationNode();
if (!ctx || !destination) return;
const sourceX = item.carrierId === state.player.id ? state.player.x : item.x;
const sourceY = item.carrierId === state.player.id ? state.player.y : item.y;
pianoSynth.noteOn(
keyId,
`local:${itemId}`,
midi,
config.instrument,
config.voiceMode,
config.attack,
config.decay,
config.release,
config.brightness,
{ audioCtx: ctx, destination },
{ x: sourceX - state.player.x, y: sourceY - state.player.y, range: config.emitRange },
);
signaling.send({ type: 'item_piano_note', itemId, keyId, midi, on: true });
}
/** Handles key release while in piano mode, including mono fallback retrigger behavior. */
function handlePianoUseModeKeyUp(code: string): void {
if (!activePianoKeys.delete(code)) return;
const orderIndex = activePianoHeldOrder.lastIndexOf(code);
if (orderIndex >= 0) {
activePianoHeldOrder.splice(orderIndex, 1);
}
const itemId = activePianoItemId;
const midi = activePianoKeyMidi.get(code);
activePianoKeyMidi.delete(code);
if (!itemId || !Number.isFinite(midi)) {
pianoSynth.noteOff(code);
if (activePianoMonophonicKey === code) {
activePianoMonophonicKey = null;
}
return;
}
const item = state.items.get(itemId);
if (!item || item.type !== 'piano') {
pianoSynth.noteOff(code);
if (activePianoMonophonicKey === code) {
activePianoMonophonicKey = null;
}
return;
}
const config = getPianoParams(item);
if (config.voiceMode !== 'mono') {
pianoSynth.noteOff(code);
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi, on: false });
return;
}
if (activePianoMonophonicKey !== code) {
return;
}
pianoSynth.noteOff(code);
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi, on: false });
const fallbackCode = activePianoHeldOrder[activePianoHeldOrder.length - 1] ?? null;
if (!fallbackCode) {
activePianoMonophonicKey = null;
return;
}
const fallbackMidi = activePianoKeyMidi.get(fallbackCode);
if (!Number.isFinite(fallbackMidi)) {
activePianoMonophonicKey = null;
return;
}
activePianoMonophonicKey = fallbackCode;
playLocalPianoNote(item, itemId, fallbackCode, fallbackMidi, config);
}
/** Plays one short C4 preview using the piano item's current/overridden envelope+instrument. */
async function previewPianoSettingChange(
item: WorldItem,
@@ -2328,6 +2413,16 @@ function handlePianoUseModeInput(code: string): void {
stopPianoUseMode(false);
return;
}
if (code === 'Comma') {
signaling.send({ type: 'item_piano_recording', itemId, action: 'toggle_record' });
audio.sfxUiBlip();
return;
}
if (code === 'Period') {
signaling.send({ type: 'item_piano_recording', itemId, action: 'playback' });
audio.sfxUiBlip();
return;
}
if (code === 'Equal' || code === 'Minus') {
const current = getPianoParams(item).octave;
const next = Math.max(-2, Math.min(2, current + (code === 'Equal' ? 1 : -1)));
@@ -2385,25 +2480,19 @@ function handlePianoUseModeInput(code: string): void {
const playedMidi = Math.max(0, Math.min(127, midi + config.octave * 12));
activePianoKeys.add(code);
activePianoKeyMidi.set(code, playedMidi);
const ctx = audio.context;
const destination = audio.getOutputDestinationNode();
if (!ctx || !destination) return;
const sourceX = item.carrierId === state.player.id ? state.player.x : item.x;
const sourceY = item.carrierId === state.player.id ? state.player.y : item.y;
pianoSynth.noteOn(
code,
`local:${itemId}`,
playedMidi,
config.instrument,
config.voiceMode,
config.attack,
config.decay,
config.release,
config.brightness,
{ audioCtx: ctx, destination },
{ x: sourceX - state.player.x, y: sourceY - state.player.y, range: config.emitRange },
);
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi: playedMidi, on: true });
activePianoHeldOrder.push(code);
if (config.voiceMode === 'mono') {
const previousCode = activePianoMonophonicKey;
if (previousCode && previousCode !== code) {
const previousMidi = activePianoKeyMidi.get(previousCode);
pianoSynth.noteOff(previousCode);
if (Number.isFinite(previousMidi)) {
signaling.send({ type: 'item_piano_note', itemId, keyId: previousCode, midi: previousMidi, on: false });
}
}
activePianoMonophonicKey = code;
}
playLocalPianoNote(item, itemId, code, playedMidi, config);
}
/** Handles effect menu list navigation and selection. */
@@ -2871,15 +2960,7 @@ function setupInputHandlers(): void {
document.addEventListener('keyup', (event) => {
const code = normalizeInputCode(event);
if (state.mode === 'pianoUse' && code) {
if (activePianoKeys.delete(code)) {
pianoSynth.noteOff(code);
const itemId = activePianoItemId;
const midi = activePianoKeyMidi.get(code);
activePianoKeyMidi.delete(code);
if (itemId && Number.isFinite(midi)) {
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi, on: false });
}
}
handlePianoUseModeKeyUp(code);
}
if (code) {
state.keysPressed[code] = false;