Add emit sound tempo and global emit speed/tempo defaults
This commit is contained in:
@@ -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.22 R130";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.22 R131";
|
||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HEARING_RADIUS, type WorldItem } from '../state/gameState';
|
||||
import { getItemTypeGlobalProperties } from '../items/itemRegistry';
|
||||
import { AudioEngine } from './audioEngine';
|
||||
import { connectEffectChain, disconnectEffectRuntime, type EffectId, type EffectRuntime } from './effects';
|
||||
import { normalizeRadioEffect, normalizeRadioEffectValue } from './radioStationRuntime';
|
||||
@@ -33,6 +34,26 @@ function resolveEmitPlaybackRate(raw: unknown): number {
|
||||
return 1 + ((clamped - 50) / 50) * 1;
|
||||
}
|
||||
|
||||
function setElementPreservesPitch(element: HTMLAudioElement, enabled: boolean): void {
|
||||
const target = element as HTMLAudioElement & {
|
||||
preservesPitch?: boolean;
|
||||
mozPreservesPitch?: boolean;
|
||||
webkitPreservesPitch?: boolean;
|
||||
};
|
||||
if ('preservesPitch' in target) target.preservesPitch = enabled;
|
||||
if ('mozPreservesPitch' in target) target.mozPreservesPitch = enabled;
|
||||
if ('webkitPreservesPitch' in target) target.webkitPreservesPitch = enabled;
|
||||
}
|
||||
|
||||
function resolveEmitRates(item: WorldItem): { playbackRate: number; preservePitch: boolean } {
|
||||
const globals = getItemTypeGlobalProperties(item.type);
|
||||
const speed = resolveEmitPlaybackRate(item.params.emitSoundSpeed ?? globals.emitSoundSpeed ?? 50);
|
||||
const tempo = resolveEmitPlaybackRate(item.params.emitSoundTempo ?? globals.emitSoundTempo ?? 50);
|
||||
const playbackRate = Math.max(0.25, Math.min(4, speed * tempo));
|
||||
const preservePitch = Math.abs(speed - 1) < 0.001;
|
||||
return { playbackRate, preservePitch };
|
||||
}
|
||||
|
||||
export class ItemEmitRuntime {
|
||||
private readonly outputs = new Map<string, EmitOutput>();
|
||||
private layerEnabled = true;
|
||||
@@ -110,7 +131,9 @@ export class ItemEmitRuntime {
|
||||
const effect = normalizeRadioEffect(item.params.emitEffect);
|
||||
const effectValue = normalizeRadioEffectValue(item.params.emitEffectValue);
|
||||
const effectRuntime = connectEffectChain(audioCtx, effectInput, gain, effect, effectValue);
|
||||
element.playbackRate = resolveEmitPlaybackRate(item.params.emitSoundSpeed);
|
||||
const initialRates = resolveEmitRates(item);
|
||||
setElementPreservesPitch(element, initialRates.preservePitch);
|
||||
element.playbackRate = initialRates.playbackRate;
|
||||
if (this.audio.supportsStereoPanner()) {
|
||||
panner = audioCtx.createStereoPanner();
|
||||
gain.connect(panner).connect(audioCtx.destination);
|
||||
@@ -148,7 +171,9 @@ export class ItemEmitRuntime {
|
||||
output.effect = effect;
|
||||
output.effectValue = effectValue;
|
||||
}
|
||||
const nextPlaybackRate = resolveEmitPlaybackRate(item.params.emitSoundSpeed);
|
||||
const nextRates = resolveEmitRates(item);
|
||||
setElementPreservesPitch(output.element, nextRates.preservePitch);
|
||||
const nextPlaybackRate = nextRates.playbackRate;
|
||||
if (Math.abs(output.element.playbackRate - nextPlaybackRate) > 0.001) {
|
||||
output.element.playbackRate = nextPlaybackRate;
|
||||
}
|
||||
|
||||
@@ -52,15 +52,15 @@ const DEFAULT_ITEM_TYPE_EDITABLE_PROPERTIES: Record<ItemType, string[]> = {
|
||||
dice: ['title', 'sides', 'number'],
|
||||
wheel: ['title', 'spaces'],
|
||||
clock: ['title', 'timeZone', 'use24Hour'],
|
||||
widget: ['title', 'enabled', 'directional', 'facing', 'emitRange', 'emitVolume', 'emitSoundSpeed', 'emitEffect', 'emitEffectValue', 'useSound', 'emitSound'],
|
||||
widget: ['title', 'enabled', 'directional', 'facing', 'emitRange', 'emitVolume', 'emitSoundSpeed', 'emitSoundTempo', 'emitEffect', 'emitEffectValue', 'useSound', 'emitSound'],
|
||||
};
|
||||
|
||||
const DEFAULT_ITEM_TYPE_GLOBAL_PROPERTIES: Record<ItemType, Record<string, string | number | boolean>> = {
|
||||
radio_station: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000, emitRange: 20, directional: true },
|
||||
dice: { useSound: 'sounds/roll.ogg', emitSound: 'none', useCooldownMs: 1000, emitRange: 15, directional: false },
|
||||
wheel: { useSound: 'sounds/spin.ogg', emitSound: 'none', useCooldownMs: 4000, emitRange: 15, directional: false },
|
||||
clock: { useSound: 'none', emitSound: 'sounds/clock.ogg', useCooldownMs: 1000, emitRange: 10, directional: false },
|
||||
widget: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000, emitRange: 15, directional: false },
|
||||
radio_station: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000, emitRange: 20, directional: true, emitSoundSpeed: 50, emitSoundTempo: 50 },
|
||||
dice: { useSound: 'sounds/roll.ogg', emitSound: 'none', useCooldownMs: 1000, emitRange: 15, directional: false, emitSoundSpeed: 50, emitSoundTempo: 50 },
|
||||
wheel: { useSound: 'sounds/spin.ogg', emitSound: 'none', useCooldownMs: 4000, emitRange: 15, directional: false, emitSoundSpeed: 50, emitSoundTempo: 50 },
|
||||
clock: { useSound: 'none', emitSound: 'sounds/clock.ogg', useCooldownMs: 1000, emitRange: 10, directional: false, emitSoundSpeed: 50, emitSoundTempo: 50 },
|
||||
widget: { useSound: 'none', emitSound: 'none', useCooldownMs: 1000, emitRange: 15, directional: false, emitSoundSpeed: 50, emitSoundTempo: 50 },
|
||||
};
|
||||
|
||||
export type ItemPropertyValueType = 'boolean' | 'text' | 'number' | 'list' | 'sound';
|
||||
@@ -197,6 +197,7 @@ export function itemPropertyLabel(key: string): string {
|
||||
if (key === 'mediaVolume') return 'media volume';
|
||||
if (key === 'emitVolume') return 'emit volume';
|
||||
if (key === 'emitSoundSpeed') return 'emit sound speed';
|
||||
if (key === 'emitSoundTempo') return 'emit sound tempo';
|
||||
if (key === 'mediaChannel') return 'media channel';
|
||||
if (key === 'mediaEffect') return 'media effect';
|
||||
if (key === 'mediaEffectValue') return 'media effect value';
|
||||
|
||||
@@ -754,6 +754,7 @@ function inferItemPropertyValueType(item: WorldItem, key: string): string | unde
|
||||
key === 'mediaVolume' ||
|
||||
key === 'emitVolume' ||
|
||||
key === 'emitSoundSpeed' ||
|
||||
key === 'emitSoundTempo' ||
|
||||
key === 'mediaEffectValue' ||
|
||||
key === 'emitEffectValue' ||
|
||||
key === 'facing' ||
|
||||
@@ -2167,6 +2168,14 @@ function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boo
|
||||
return;
|
||||
}
|
||||
signaling.send({ type: 'item_update', itemId, params: { emitSoundSpeed: parsed.value } });
|
||||
} else if (propertyKey === 'emitSoundTempo') {
|
||||
const parsed = validateNumericItemPropertyInput(item, propertyKey, value, true);
|
||||
if (!parsed.ok) {
|
||||
updateStatus(parsed.message);
|
||||
audio.sfxUiCancel();
|
||||
return;
|
||||
}
|
||||
signaling.send({ type: 'item_update', itemId, params: { emitSoundTempo: parsed.value } });
|
||||
} else if (propertyKey === 'mediaEffect' || propertyKey === 'emitEffect') {
|
||||
const normalized = value.trim().toLowerCase() as EffectId;
|
||||
if (!EFFECT_IDS.has(normalized)) {
|
||||
|
||||
Reference in New Issue
Block a user