Add emit initial delay option for widget audio emit
This commit is contained in:
@@ -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.28 R309";
|
window.CHGRID_WEB_VERSION = "2026.02.28 R310";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type EmitOutput = {
|
|||||||
effectRuntime: EffectRuntime | null;
|
effectRuntime: EffectRuntime | null;
|
||||||
effect: EffectId;
|
effect: EffectId;
|
||||||
effectValue: number;
|
effectValue: number;
|
||||||
|
initialDelaySeconds: number;
|
||||||
loopDelaySeconds: number;
|
loopDelaySeconds: number;
|
||||||
gain: GainNode;
|
gain: GainNode;
|
||||||
panner: StereoPannerNode | null;
|
panner: StereoPannerNode | null;
|
||||||
@@ -83,6 +84,14 @@ function resolveEmitLoopDelaySeconds(item: WorldItem): number {
|
|||||||
return Math.round(clamped * 10) / 10;
|
return Math.round(clamped * 10) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves the optional emit initial delay in seconds from item params. */
|
||||||
|
function resolveEmitInitialDelaySeconds(item: WorldItem): number {
|
||||||
|
const globals = getItemTypeGlobalProperties(item.type);
|
||||||
|
const delaySeconds = Number(item.params.emitInitialDelay ?? globals.emitInitialDelay ?? 0);
|
||||||
|
const clamped = Number.isFinite(delaySeconds) ? Math.max(0, Math.min(300, delaySeconds)) : 0;
|
||||||
|
return Math.round(clamped * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
export class ItemEmitRuntime {
|
export class ItemEmitRuntime {
|
||||||
private readonly outputs = new Map<string, EmitOutput>();
|
private readonly outputs = new Map<string, EmitOutput>();
|
||||||
private readonly resumeStateByItemId = new Map<string, EmitResumeState>();
|
private readonly resumeStateByItemId = new Map<string, EmitResumeState>();
|
||||||
@@ -216,6 +225,7 @@ export class ItemEmitRuntime {
|
|||||||
const initialRates = resolveEmitRates(item);
|
const initialRates = resolveEmitRates(item);
|
||||||
setElementPreservesPitch(element, initialRates.preservePitch);
|
setElementPreservesPitch(element, initialRates.preservePitch);
|
||||||
element.playbackRate = initialRates.playbackRate;
|
element.playbackRate = initialRates.playbackRate;
|
||||||
|
const initialDelaySeconds = resolveEmitInitialDelaySeconds(item);
|
||||||
const loopDelaySeconds = resolveEmitLoopDelaySeconds(item);
|
const loopDelaySeconds = resolveEmitLoopDelaySeconds(item);
|
||||||
const onEnded = () => {
|
const onEnded = () => {
|
||||||
const delaySeconds = this.outputs.get(item.id)?.loopDelaySeconds ?? 0;
|
const delaySeconds = this.outputs.get(item.id)?.loopDelaySeconds ?? 0;
|
||||||
@@ -314,10 +324,14 @@ export class ItemEmitRuntime {
|
|||||||
effectRuntime,
|
effectRuntime,
|
||||||
effect,
|
effect,
|
||||||
effectValue,
|
effectValue,
|
||||||
|
initialDelaySeconds,
|
||||||
loopDelaySeconds,
|
loopDelaySeconds,
|
||||||
gain,
|
gain,
|
||||||
panner,
|
panner,
|
||||||
});
|
});
|
||||||
|
if (!resumeState && !this.nextEmitStartAtMs.has(item.id) && initialDelaySeconds > 0) {
|
||||||
|
this.nextEmitStartAtMs.set(item.id, Date.now() + initialDelaySeconds * 1000);
|
||||||
|
}
|
||||||
this.resumeStateByItemId.delete(item.id);
|
this.resumeStateByItemId.delete(item.id);
|
||||||
this.tryStartEmitPlayback(item.id, element);
|
this.tryStartEmitPlayback(item.id, element);
|
||||||
}
|
}
|
||||||
@@ -361,6 +375,7 @@ export class ItemEmitRuntime {
|
|||||||
output.effectValue = effectValue;
|
output.effectValue = effectValue;
|
||||||
}
|
}
|
||||||
const nextRates = resolveEmitRates(item);
|
const nextRates = resolveEmitRates(item);
|
||||||
|
output.initialDelaySeconds = resolveEmitInitialDelaySeconds(item);
|
||||||
output.loopDelaySeconds = resolveEmitLoopDelaySeconds(item);
|
output.loopDelaySeconds = resolveEmitLoopDelaySeconds(item);
|
||||||
setElementPreservesPitch(output.element, nextRates.preservePitch);
|
setElementPreservesPitch(output.element, nextRates.preservePitch);
|
||||||
const nextPlaybackRate = nextRates.playbackRate;
|
const nextPlaybackRate = nextRates.playbackRate;
|
||||||
|
|||||||
@@ -161,6 +161,7 @@
|
|||||||
"emitVolume": 100,
|
"emitVolume": 100,
|
||||||
"emitSoundSpeed": 50,
|
"emitSoundSpeed": 50,
|
||||||
"emitSoundTempo": 50,
|
"emitSoundTempo": 50,
|
||||||
|
"emitInitialDelay": 0,
|
||||||
"emitLoopDelay": 0,
|
"emitLoopDelay": 0,
|
||||||
"emitEffect": "off",
|
"emitEffect": "off",
|
||||||
"emitEffectValue": 50,
|
"emitEffectValue": 50,
|
||||||
@@ -177,6 +178,7 @@
|
|||||||
- `emitVolume`: integer, range `0-100`, default `100`.
|
- `emitVolume`: integer, range `0-100`, default `100`.
|
||||||
- `emitSoundSpeed`: integer, range `0-100`, default `50`; controls emitted sound speed/pitch (`0=0.5x`, `50=1.0x`, `100=2.0x`).
|
- `emitSoundSpeed`: integer, range `0-100`, default `50`; controls emitted sound speed/pitch (`0=0.5x`, `50=1.0x`, `100=2.0x`).
|
||||||
- `emitSoundTempo`: integer, range `0-100`, default `50`; controls emitted sound tempo (`0=0.5x`, `50=1.0x`, `100=2.0x`).
|
- `emitSoundTempo`: integer, range `0-100`, default `50`; controls emitted sound tempo (`0=0.5x`, `50=1.0x`, `100=2.0x`).
|
||||||
|
- `emitInitialDelay`: number, range `0-300`, precision `0.1`, default `0`; delay in seconds before emitted audio starts after enable.
|
||||||
- `emitLoopDelay`: number, range `0-300`, precision `0.1`, default `0`; delay in seconds between each emitted playback.
|
- `emitLoopDelay`: number, range `0-300`, precision `0.1`, default `0`; delay in seconds between each emitted playback.
|
||||||
- `emitEffect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
|
- `emitEffect`: one of `reverb | echo | flanger | high_pass | low_pass | off`, default `off`.
|
||||||
- `emitEffectValue`: number, range `0-100`, precision `0.1`, default `50`.
|
- `emitEffectValue`: number, range `0-100`, precision `0.1`, default `50`.
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
- `emitVolume=100`
|
- `emitVolume=100`
|
||||||
- `emitSoundSpeed=50`
|
- `emitSoundSpeed=50`
|
||||||
- `emitSoundTempo=50`
|
- `emitSoundTempo=50`
|
||||||
|
- `emitInitialDelay=0`
|
||||||
- `emitLoopDelay=0`
|
- `emitLoopDelay=0`
|
||||||
- `emitEffect="off"`
|
- `emitEffect="off"`
|
||||||
- `emitEffectValue=50`
|
- `emitEffectValue=50`
|
||||||
@@ -154,6 +155,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
- `directional=false`
|
- `directional=false`
|
||||||
- `emitSoundSpeed=50`
|
- `emitSoundSpeed=50`
|
||||||
- `emitSoundTempo=50`
|
- `emitSoundTempo=50`
|
||||||
|
- `emitInitialDelay=0`
|
||||||
- `emitLoopDelay=0`
|
- `emitLoopDelay=0`
|
||||||
|
|
||||||
### Use
|
### Use
|
||||||
@@ -167,6 +169,7 @@ This is behavior-focused documentation for item types and their defaults.
|
|||||||
- `emitVolume`: integer `0..100`
|
- `emitVolume`: integer `0..100`
|
||||||
- `emitSoundSpeed`: integer `0..100` (`0=0.5x`, `50=1.0x`, `100=2.0x`) for speed/pitch
|
- `emitSoundSpeed`: integer `0..100` (`0=0.5x`, `50=1.0x`, `100=2.0x`) for speed/pitch
|
||||||
- `emitSoundTempo`: integer `0..100` (`0=0.5x`, `50=1.0x`, `100=2.0x`) for tempo
|
- `emitSoundTempo`: integer `0..100` (`0=0.5x`, `50=1.0x`, `100=2.0x`) for tempo
|
||||||
|
- `emitInitialDelay`: number `0..300` with `0.1` step/precision; delay in seconds before emitted audio starts after enable
|
||||||
- `emitLoopDelay`: number `0..300` with `0.1` step/precision; delay in seconds between each emitted loop playback
|
- `emitLoopDelay`: number `0..300` with `0.1` step/precision; delay in seconds between each emitted loop playback
|
||||||
- `emitEffect`: `reverb | echo | flanger | high_pass | low_pass | off`
|
- `emitEffect`: `reverb | echo | flanger | high_pass | low_pass | off`
|
||||||
- `emitEffectValue`: number `0..100` with `0.1` precision
|
- `emitEffectValue`: number `0..100` with `0.1` precision
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `itemTypes[].capabilities`: server-declared actions supported by the type
|
- `itemTypes[].capabilities`: server-declared actions supported by the type
|
||||||
- `itemTypes[].editableProperties`: editable property keys by item type
|
- `itemTypes[].editableProperties`: editable property keys by item type
|
||||||
- `itemTypes[].propertyMetadata`: property-level metadata (`valueType`, optional `label`, optional `range`, optional `tooltip`, optional `maxLength`, optional `options`, optional `visibleWhen`)
|
- `itemTypes[].propertyMetadata`: property-level metadata (`valueType`, optional `label`, optional `range`, optional `tooltip`, optional `maxLength`, optional `options`, optional `visibleWhen`)
|
||||||
- `itemTypes[].globalProperties`: non-editable global values (`useSound`, `emitSound`, `useCooldownMs`, `emitRange`, `directional`, `emitSoundSpeed`, `emitSoundTempo`, `emitLoopDelay`)
|
- `itemTypes[].globalProperties`: non-editable global values (`useSound`, `emitSound`, `useCooldownMs`, `emitRange`, `directional`, `emitSoundSpeed`, `emitSoundTempo`, `emitInitialDelay`, `emitLoopDelay`)
|
||||||
- `adminMenu.actions`: server-authored admin root menu labels/ordering for the authenticated user.
|
- `adminMenu.actions`: server-authored admin root menu labels/ordering for the authenticated user.
|
||||||
- Client item UI requires this metadata from the server; there is no fallback item definition map.
|
- Client item UI requires this metadata from the server; there is no fallback item definition map.
|
||||||
- Client property help/type rendering is metadata-driven; it does not infer fallback types/tooltips from hardcoded key heuristics.
|
- Client property help/type rendering is metadata-driven; it does not infer fallback types/tooltips from hardcoded key heuristics.
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ GLOBAL_ITEM_PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
|||||||
"tooltip": "Global emitted sound tempo percent. 50 is normal.",
|
"tooltip": "Global emitted sound tempo percent. 50 is normal.",
|
||||||
"range": {"min": 0, "max": 100, "step": 0.1},
|
"range": {"min": 0, "max": 100, "step": 0.1},
|
||||||
},
|
},
|
||||||
|
"emitInitialDelay": {
|
||||||
|
"valueType": "number",
|
||||||
|
"tooltip": "Delay in seconds before emitted audio starts after this sound is enabled.",
|
||||||
|
"range": {"min": 0, "max": 300, "step": 0.1},
|
||||||
|
},
|
||||||
"emitLoopDelay": {
|
"emitLoopDelay": {
|
||||||
"valueType": "number",
|
"valueType": "number",
|
||||||
"tooltip": "Delay in seconds between each emitted playback.",
|
"tooltip": "Delay in seconds between each emitted playback.",
|
||||||
@@ -159,5 +164,6 @@ def get_item_global_properties(item_type: ItemType) -> dict[str, str | int | boo
|
|||||||
"directional": bool(definition.directional),
|
"directional": bool(definition.directional),
|
||||||
"emitSoundSpeed": 50,
|
"emitSoundSpeed": 50,
|
||||||
"emitSoundTempo": 50,
|
"emitSoundTempo": 50,
|
||||||
|
"emitInitialDelay": 0,
|
||||||
"emitLoopDelay": 0,
|
"emitLoopDelay": 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ EDITABLE_PROPERTIES: tuple[str, ...] = (
|
|||||||
"emitVolume",
|
"emitVolume",
|
||||||
"emitSoundSpeed",
|
"emitSoundSpeed",
|
||||||
"emitSoundTempo",
|
"emitSoundTempo",
|
||||||
|
"emitInitialDelay",
|
||||||
"emitLoopDelay",
|
"emitLoopDelay",
|
||||||
"emitEffect",
|
"emitEffect",
|
||||||
"emitEffectValue",
|
"emitEffectValue",
|
||||||
@@ -34,6 +35,7 @@ DEFAULT_PARAMS: dict = {
|
|||||||
"emitVolume": 100,
|
"emitVolume": 100,
|
||||||
"emitSoundSpeed": 50,
|
"emitSoundSpeed": 50,
|
||||||
"emitSoundTempo": 50,
|
"emitSoundTempo": 50,
|
||||||
|
"emitInitialDelay": 0,
|
||||||
"emitLoopDelay": 0,
|
"emitLoopDelay": 0,
|
||||||
"emitEffect": "off",
|
"emitEffect": "off",
|
||||||
"emitEffectValue": 50,
|
"emitEffectValue": 50,
|
||||||
@@ -48,6 +50,7 @@ PARAM_KEYS: tuple[str, ...] = (
|
|||||||
"emitVolume",
|
"emitVolume",
|
||||||
"emitSoundSpeed",
|
"emitSoundSpeed",
|
||||||
"emitSoundTempo",
|
"emitSoundTempo",
|
||||||
|
"emitInitialDelay",
|
||||||
"emitLoopDelay",
|
"emitLoopDelay",
|
||||||
"emitEffect",
|
"emitEffect",
|
||||||
"emitEffectValue",
|
"emitEffectValue",
|
||||||
@@ -86,6 +89,11 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
|||||||
"tooltip": "Playback tempo percent for emitted sound. 50 is normal, 0 is half, 100 is double. Using speed and tempo together may sound weird.",
|
"tooltip": "Playback tempo percent for emitted sound. 50 is normal, 0 is half, 100 is double. Using speed and tempo together may sound weird.",
|
||||||
"range": {"min": 0, "max": 100, "step": 0.1},
|
"range": {"min": 0, "max": 100, "step": 0.1},
|
||||||
},
|
},
|
||||||
|
"emitInitialDelay": {
|
||||||
|
"valueType": "number",
|
||||||
|
"tooltip": "Delay in seconds before emitted audio starts after this sound is enabled.",
|
||||||
|
"range": {"min": 0, "max": 300, "step": 0.1},
|
||||||
|
},
|
||||||
"emitLoopDelay": {
|
"emitLoopDelay": {
|
||||||
"valueType": "number",
|
"valueType": "number",
|
||||||
"tooltip": "Delay in seconds between each playing of this audio.",
|
"tooltip": "Delay in seconds between each playing of this audio.",
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ def validate_update(item: WorldItem, next_params: dict) -> dict:
|
|||||||
raise ValueError("emitSoundTempo must be between 0 and 100.")
|
raise ValueError("emitSoundTempo must be between 0 and 100.")
|
||||||
next_params["emitSoundTempo"] = round(emit_tempo, 1)
|
next_params["emitSoundTempo"] = round(emit_tempo, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
emit_initial_delay = float(next_params.get("emitInitialDelay", item.params.get("emitInitialDelay", 0)))
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError("emitInitialDelay must be a number between 0 and 300.") from exc
|
||||||
|
if not (0 <= emit_initial_delay <= 300):
|
||||||
|
raise ValueError("emitInitialDelay must be between 0 and 300.")
|
||||||
|
next_params["emitInitialDelay"] = round(emit_initial_delay, 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
emit_loop_delay = float(next_params.get("emitLoopDelay", item.params.get("emitLoopDelay", 0)))
|
emit_loop_delay = float(next_params.get("emitLoopDelay", item.params.get("emitLoopDelay", 0)))
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
|
|||||||
Reference in New Issue
Block a user