diff --git a/client/public/version.js b/client/public/version.js index a648b9b..909432c 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -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 R190"; +window.CHGRID_WEB_VERSION = "2026.02.22 R191"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/audio/audioEngine.ts b/client/src/audio/audioEngine.ts index 69b38b4..df6c7ab 100644 --- a/client/src/audio/audioEngine.ts +++ b/client/src/audio/audioEngine.ts @@ -29,6 +29,7 @@ type SoundSpec = { }; type OutputMode = 'stereo' | 'mono'; +const SPATIAL_RAMP_SECONDS = 0.2; export class AudioEngine { private audioCtx: AudioContext | null = null; @@ -300,7 +301,7 @@ export class AudioEngine { const gainValue = mix?.gain ?? 0; const listenGain = Number.isFinite(peer.listenGain) ? Math.max(0, peer.listenGain as number) : 1; const panValue = mix?.pan ?? 0; - peer.gain.gain.linearRampToValueAtTime(gainValue * listenGain, this.audioCtx.currentTime + 0.1); + peer.gain.gain.linearRampToValueAtTime(gainValue * listenGain, this.audioCtx.currentTime + SPATIAL_RAMP_SECONDS); if (peer.panner) { const resolvedPan = this.outputMode === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue)); peer.panner.pan.setValueAtTime(resolvedPan, this.audioCtx.currentTime); diff --git a/client/src/audio/itemEmitRuntime.ts b/client/src/audio/itemEmitRuntime.ts index 89510b1..410eab5 100644 --- a/client/src/audio/itemEmitRuntime.ts +++ b/client/src/audio/itemEmitRuntime.ts @@ -27,6 +27,7 @@ type EmitSpatialConfig = { const ITEM_EMIT_BASE_GAIN = 1; const SUBSCRIBE_PRELOAD_SQUARES = 5; const UNSUBSCRIBE_HYSTERESIS_SQUARES = 8; +const SPATIAL_RAMP_SECONDS = 0.2; /** Maps a 0-100 speed control to playback-rate range used by emitted audio. */ function resolveEmitPlaybackRate(raw: unknown): number { @@ -223,10 +224,10 @@ export class ItemEmitRuntime { const gainValue = mix?.gain ?? 0; const panValue = mix?.pan ?? 0; const emitVolume = volumePercentToGain(item.params.emitVolume, 100); - output.gain.gain.linearRampToValueAtTime(gainValue * emitVolume, audioCtx.currentTime + 0.1); + output.gain.gain.linearRampToValueAtTime(gainValue * emitVolume, audioCtx.currentTime + SPATIAL_RAMP_SECONDS); if (output.panner) { const resolvedPan = this.audio.getOutputMode() === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue)); - output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1); + output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + SPATIAL_RAMP_SECONDS); } } } diff --git a/client/src/audio/radioStationRuntime.ts b/client/src/audio/radioStationRuntime.ts index 25d2ca6..0834102 100644 --- a/client/src/audio/radioStationRuntime.ts +++ b/client/src/audio/radioStationRuntime.ts @@ -165,6 +165,7 @@ type RadioSpatialConfig = { const SUBSCRIBE_PRELOAD_SQUARES = 5; const UNSUBSCRIBE_HYSTERESIS_SQUARES = 8; +const SPATIAL_RAMP_SECONDS = 0.2; export class RadioStationRuntime { private readonly sharedRadioSources = new Map(); @@ -295,10 +296,10 @@ export class RadioStationRuntime { }); const gainValue = mix?.gain ?? 0; const panValue = mix?.pan ?? 0; - output.gain.gain.linearRampToValueAtTime(gainValue, audioCtx.currentTime + 0.1); + output.gain.gain.linearRampToValueAtTime(gainValue, audioCtx.currentTime + SPATIAL_RAMP_SECONDS); if (output.panner) { const resolvedPan = this.audio.getOutputMode() === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue)); - output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + 0.1); + output.panner.pan.linearRampToValueAtTime(resolvedPan, audioCtx.currentTime + SPATIAL_RAMP_SECONDS); } } } diff --git a/client/src/audio/spatial.ts b/client/src/audio/spatial.ts index a6aee29..b24ff7b 100644 --- a/client/src/audio/spatial.ts +++ b/client/src/audio/spatial.ts @@ -51,7 +51,8 @@ export function resolveSpatialMix(options: SpatialMixOptions): SpatialMixResult } const volumeRatio = Math.max(0, 1 - distance / effectiveRange); - let gain = baseGain * Math.pow(volumeRatio, 2); + const shapedVolume = volumeRatio * volumeRatio * (3 - 2 * volumeRatio); + let gain = baseGain * shapedVolume; const clampedX = Math.max(-range, Math.min(range, dx)); let pan = Math.sin((clampedX / range) * (Math.PI / 2));