Add piano mono/poly, octave, and expanded drum voice set

This commit is contained in:
Jage9
2026-02-23 00:22:36 -05:00
parent 019e49802d
commit 29eb6a63e3
17 changed files with 338 additions and 72 deletions

View File

@@ -255,6 +255,7 @@ let activeTeleportLoopStop: (() => void) | null = null;
let activeTeleportLoopToken = 0;
let activePianoItemId: string | null = null;
const activePianoKeys = new Set<string>();
const activePianoKeyMidi = new Map<string, number>();
const activeRemotePianoKeys = new Set<string>();
let pianoPreviewTimeoutId: number | null = null;
let activeTeleport:
@@ -796,6 +797,8 @@ function getItemSpatialConfig(item: WorldItem): { range: number; directional: bo
/** Resolves piano params with safe defaults for local play mode. */
function getPianoParams(item: WorldItem): {
instrument: PianoInstrumentId;
voiceMode: 'mono' | 'poly';
octave: number;
attack: number;
decay: number;
release: number;
@@ -816,12 +819,16 @@ function getPianoParams(item: WorldItem): {
: 'piano';
const rawAttack = Number(item.params.attack);
const rawDecay = Number(item.params.decay);
const rawOctave = Number(item.params.octave);
const rawVoiceMode = String(item.params.voiceMode ?? defaultsVoiceModeForInstrument(instrument)).trim().toLowerCase();
const rawRelease = Number(item.params.release);
const rawBrightness = Number(item.params.brightness);
const rawEmitRange = Number(item.params.emitRange ?? getItemTypeGlobalProperties(item.type).emitRange ?? 15);
const defaults = DEFAULT_PIANO_SETTINGS_BY_INSTRUMENT[instrument];
return {
instrument,
voiceMode: rawVoiceMode === 'mono' ? 'mono' : 'poly',
octave: Math.max(-2, Math.min(2, Number.isFinite(rawOctave) ? Math.round(rawOctave) : defaultsOctaveForInstrument(instrument))),
attack: Math.max(0, Math.min(100, Number.isFinite(rawAttack) ? Math.round(rawAttack) : defaults.attack)),
decay: Math.max(0, Math.min(100, Number.isFinite(rawDecay) ? Math.round(rawDecay) : defaults.decay)),
release: Math.max(0, Math.min(100, Number.isFinite(rawRelease) ? Math.round(rawRelease) : defaults.release)),
@@ -830,6 +837,17 @@ function getPianoParams(item: WorldItem): {
};
}
/** Returns default voice mode for a given piano instrument. */
function defaultsVoiceModeForInstrument(instrument: PianoInstrumentId): 'mono' | 'poly' {
if (instrument === 'bass' || instrument === 'violin' || instrument === 'brass') return 'mono';
return 'poly';
}
/** Returns default octave offset for a given piano instrument. */
function defaultsOctaveForInstrument(instrument: PianoInstrumentId): number {
return instrument === 'bass' ? -1 : 0;
}
/** Normalizes arbitrary instrument strings into supported piano synth ids. */
function normalizePianoInstrument(value: unknown): PianoInstrumentId {
const raw = String(value ?? 'piano').trim().toLowerCase();
@@ -839,6 +857,7 @@ function normalizePianoInstrument(value: unknown): PianoInstrumentId {
if (raw === 'bass') return 'bass';
if (raw === 'violin') return 'violin';
if (raw === 'synth_lead') return 'synth_lead';
if (raw === 'brass') return 'brass';
if (raw === 'nintendo') return 'nintendo';
if (raw === 'drum_kit') return 'drum_kit';
return 'piano';
@@ -872,13 +891,14 @@ function stopPianoUseMode(announce = true): void {
if (!activePianoItemId) return;
const itemId = activePianoItemId;
for (const code of Array.from(activePianoKeys)) {
const midi = getPianoMidiForCode(code);
if (midi === null) continue;
const midi = activePianoKeyMidi.get(code);
if (!Number.isFinite(midi)) continue;
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi, on: false });
pianoSynth.noteOff(code);
}
activePianoItemId = null;
activePianoKeys.clear();
activePianoKeyMidi.clear();
state.mode = 'normal';
if (announce) {
updateStatus('Stopped piano.');
@@ -908,8 +928,10 @@ async function previewPianoSettingChange(
pianoSynth.noteOff(previewKeyId);
pianoSynth.noteOn(
previewKeyId,
'preview',
60,
instrument,
'poly',
attack,
decay,
release,
@@ -933,6 +955,8 @@ function playRemotePianoNote(note: {
keyId: string;
midi: number;
instrument: string;
voiceMode: 'mono' | 'poly';
octave: number;
attack: number;
decay: number;
release: number;
@@ -944,13 +968,18 @@ function playRemotePianoNote(note: {
const ctx = audio.context;
const destination = audio.getOutputDestinationNode();
if (!ctx || !destination) return;
const runtimeKey = `${note.senderId}:${note.keyId}`;
const runtimeKey = `${note.senderId}:${note.itemId}:${note.keyId}`;
if (activeRemotePianoKeys.has(runtimeKey)) return;
if (note.voiceMode === 'mono') {
stopRemotePianoNotesForSource(note.senderId, note.itemId);
}
activeRemotePianoKeys.add(runtimeKey);
pianoSynth.noteOn(
runtimeKey,
`remote:${note.senderId}:${note.itemId}`,
Math.max(0, Math.min(127, Math.round(note.midi))),
normalizePianoInstrument(note.instrument),
note.voiceMode,
Math.max(0, Math.min(100, Math.round(note.attack))),
Math.max(0, Math.min(100, Math.round(note.decay))),
Math.max(0, Math.min(100, Math.round(note.release))),
@@ -966,9 +995,13 @@ function playRemotePianoNote(note: {
/** Stops one inbound piano note previously started for another user. */
function stopRemotePianoNote(senderId: string, keyId: string): void {
const runtimeKey = `${senderId}:${keyId}`;
if (!activeRemotePianoKeys.delete(runtimeKey)) return;
pianoSynth.noteOff(runtimeKey);
const suffix = `:${keyId}`;
for (const runtimeKey of Array.from(activeRemotePianoKeys)) {
if (!runtimeKey.startsWith(`${senderId}:`)) continue;
if (!runtimeKey.endsWith(suffix)) continue;
activeRemotePianoKeys.delete(runtimeKey);
pianoSynth.noteOff(runtimeKey);
}
}
/** Stops all currently active remote piano notes for a sender id. */
@@ -981,6 +1014,16 @@ function stopAllRemotePianoNotesForSender(senderId: string): void {
}
}
/** Stops all remote piano notes for one sender+item source group. */
function stopRemotePianoNotesForSource(senderId: string, itemId: string): void {
const prefix = `${senderId}:${itemId}:`;
for (const runtimeKey of Array.from(activeRemotePianoKeys)) {
if (!runtimeKey.startsWith(prefix)) continue;
activeRemotePianoKeys.delete(runtimeKey);
pianoSynth.noteOff(runtimeKey);
}
}
/** Enters help-view mode and announces the first help line. */
function openHelpViewer(): void {
if (helpViewerLines.length === 0) {
@@ -1195,7 +1238,7 @@ function getItemPropertyValue(item: WorldItem, key: string): string {
function inferItemPropertyValueType(item: WorldItem, key: string): string | undefined {
if (key === 'useSound' || key === 'emitSound') return 'sound';
if (key === 'enabled' || key === 'use24Hour' || key === 'directional') return 'boolean';
if (key === 'mediaChannel' || key === 'mediaEffect' || key === 'emitEffect' || key === 'timeZone' || key === 'instrument') return 'list';
if (key === 'mediaChannel' || key === 'mediaEffect' || key === 'emitEffect' || key === 'timeZone' || key === 'instrument' || key === 'voiceMode') return 'list';
if (
key === 'x' ||
key === 'y' ||
@@ -1208,6 +1251,7 @@ function inferItemPropertyValueType(item: WorldItem, key: string): string | unde
key === 'emitEffectValue' ||
key === 'facing' ||
key === 'emitRange' ||
key === 'octave' ||
key === 'attack' ||
key === 'decay' ||
key === 'release' ||
@@ -2285,11 +2329,16 @@ function handlePianoUseModeInput(code: string): void {
}
if (code.startsWith('Digit')) {
const digit = Number(code.slice(5));
if (Number.isInteger(digit) && digit >= 1 && digit <= 9) {
const instrument = PIANO_INSTRUMENT_OPTIONS[digit - 1];
const instrumentIndex = digit === 0 ? 9 : digit - 1;
if (Number.isInteger(instrumentIndex) && instrumentIndex >= 0 && instrumentIndex < PIANO_INSTRUMENT_OPTIONS.length) {
const instrument = PIANO_INSTRUMENT_OPTIONS[instrumentIndex];
if (instrument) {
const defaults = DEFAULT_PIANO_SETTINGS_BY_INSTRUMENT[instrument];
const voiceMode = defaultsVoiceModeForInstrument(instrument);
const octave = defaultsOctaveForInstrument(instrument);
item.params.instrument = instrument;
item.params.voiceMode = voiceMode;
item.params.octave = octave;
item.params.attack = defaults.attack;
item.params.decay = defaults.decay;
item.params.release = defaults.release;
@@ -2317,17 +2366,21 @@ function handlePianoUseModeInput(code: string): void {
const midi = getPianoMidiForCode(code);
if (midi === null) return;
if (activePianoKeys.has(code)) return;
const config = getPianoParams(item);
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 config = getPianoParams(item);
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,
midi,
`local:${itemId}`,
playedMidi,
config.instrument,
config.voiceMode,
config.attack,
config.decay,
config.release,
@@ -2335,7 +2388,7 @@ function handlePianoUseModeInput(code: string): void {
{ 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, on: true });
signaling.send({ type: 'item_piano_note', itemId, keyId: code, midi: playedMidi, on: true });
}
/** Handles effect menu list navigation and selection. */
@@ -2623,6 +2676,10 @@ const itemPropertyEditor = createItemPropertyEditor({
const brightness = Number(value);
if (!Number.isFinite(brightness)) return;
void previewPianoSettingChange(item, { brightness });
return;
}
if (key === 'octave') {
void previewPianoSettingChange(item, {});
}
},
updateStatus,
@@ -2802,8 +2859,9 @@ function setupInputHandlers(): void {
if (activePianoKeys.delete(code)) {
pianoSynth.noteOff(code);
const itemId = activePianoItemId;
const midi = getPianoMidiForCode(code);
if (itemId && midi !== null) {
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 });
}
}