Add directional emit model and per-type emit range defaults
This commit is contained in:
@@ -10,6 +10,12 @@ type EmitOutput = {
|
||||
panner: StereoPannerNode | null;
|
||||
};
|
||||
|
||||
type EmitSpatialConfig = {
|
||||
range: number;
|
||||
directional: boolean;
|
||||
facingDeg: number;
|
||||
};
|
||||
|
||||
const ITEM_EMIT_BASE_GAIN = 0.3;
|
||||
|
||||
export class ItemEmitRuntime {
|
||||
@@ -19,6 +25,7 @@ export class ItemEmitRuntime {
|
||||
constructor(
|
||||
private readonly audio: AudioEngine,
|
||||
private readonly resolveSoundUrl: (soundPath: string) => string,
|
||||
private readonly getSpatialConfig: (item: WorldItem) => EmitSpatialConfig,
|
||||
) {}
|
||||
|
||||
cleanup(itemId: string): void {
|
||||
@@ -108,14 +115,21 @@ export class ItemEmitRuntime {
|
||||
output.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.05);
|
||||
continue;
|
||||
}
|
||||
const spatialConfig = this.getSpatialConfig(item);
|
||||
const mix = resolveSpatialMix({
|
||||
dx: item.x - playerPosition.x,
|
||||
dy: item.y - playerPosition.y,
|
||||
range: HEARING_RADIUS,
|
||||
range: Math.max(1, spatialConfig.range || HEARING_RADIUS),
|
||||
baseGain: ITEM_EMIT_BASE_GAIN,
|
||||
nearFieldDistance: 1,
|
||||
nearFieldGain: 1,
|
||||
nearFieldCenterPan: true,
|
||||
directional: {
|
||||
enabled: spatialConfig.directional,
|
||||
facingDeg: spatialConfig.facingDeg,
|
||||
coneDeg: 120,
|
||||
rearGain: 0.5,
|
||||
},
|
||||
});
|
||||
const gainValue = mix?.gain ?? 0;
|
||||
const panValue = mix?.pan ?? 0;
|
||||
|
||||
@@ -113,12 +113,21 @@ function freshStreamUrl(streamUrl: string): string {
|
||||
return `${streamUrl}${separator}chgrid_start=${Date.now()}`;
|
||||
}
|
||||
|
||||
type RadioSpatialConfig = {
|
||||
range: number;
|
||||
directional: boolean;
|
||||
facingDeg: number;
|
||||
};
|
||||
|
||||
export class RadioStationRuntime {
|
||||
private readonly sharedRadioSources = new Map<string, SharedRadioSource>();
|
||||
private readonly itemRadioOutputs = new Map<string, ItemRadioOutput>();
|
||||
private layerEnabled = true;
|
||||
|
||||
constructor(private readonly audio: AudioEngine) {}
|
||||
constructor(
|
||||
private readonly audio: AudioEngine,
|
||||
private readonly getSpatialConfig: (item: WorldItem) => RadioSpatialConfig,
|
||||
) {}
|
||||
|
||||
cleanup(itemId: string): void {
|
||||
const output = this.itemRadioOutputs.get(itemId);
|
||||
@@ -203,14 +212,21 @@ export class RadioStationRuntime {
|
||||
output.gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.05);
|
||||
continue;
|
||||
}
|
||||
const spatialConfig = this.getSpatialConfig(item);
|
||||
const mix = resolveSpatialMix({
|
||||
dx: item.x - playerPosition.x,
|
||||
dy: item.y - playerPosition.y,
|
||||
range: HEARING_RADIUS,
|
||||
range: Math.max(1, spatialConfig.range || HEARING_RADIUS),
|
||||
baseGain: normalizedVolume,
|
||||
nearFieldDistance: 1,
|
||||
nearFieldGain: 1,
|
||||
nearFieldCenterPan: true,
|
||||
directional: {
|
||||
enabled: spatialConfig.directional,
|
||||
facingDeg: spatialConfig.facingDeg,
|
||||
coneDeg: 120,
|
||||
rearGain: 0.5,
|
||||
},
|
||||
});
|
||||
const gainValue = mix?.gain ?? 0;
|
||||
const panValue = mix?.pan ?? 0;
|
||||
|
||||
@@ -6,6 +6,12 @@ export type SpatialMixOptions = {
|
||||
nearFieldDistance?: number;
|
||||
nearFieldGain?: number;
|
||||
nearFieldCenterPan?: boolean;
|
||||
directional?: {
|
||||
enabled: boolean;
|
||||
facingDeg: number;
|
||||
coneDeg?: number;
|
||||
rearGain?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SpatialMixResult = {
|
||||
@@ -45,5 +51,37 @@ export function resolveSpatialMix(options: SpatialMixOptions): SpatialMixResult
|
||||
}
|
||||
}
|
||||
|
||||
if (options.directional?.enabled) {
|
||||
const coneDeg = Math.max(1, Math.min(359, options.directional.coneDeg ?? 120));
|
||||
const rearGain = Math.max(0, Math.min(1, options.directional.rearGain ?? 0.5));
|
||||
const facingDeg = normalizeDegrees(options.directional.facingDeg);
|
||||
const bearingDeg = bearingFromSourceToListener(dx, dy);
|
||||
const diff = angularDifferenceDeg(facingDeg, bearingDeg);
|
||||
const halfCone = coneDeg / 2;
|
||||
if (diff > halfCone) {
|
||||
const span = Math.max(1, 180 - halfCone);
|
||||
const t = Math.max(0, Math.min(1, (diff - halfCone) / span));
|
||||
const directionalGain = 1 - t * (1 - rearGain);
|
||||
gain *= directionalGain;
|
||||
}
|
||||
}
|
||||
|
||||
return { distance, gain, pan };
|
||||
}
|
||||
|
||||
export function normalizeDegrees(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
const wrapped = value % 360;
|
||||
return wrapped < 0 ? wrapped + 360 : wrapped;
|
||||
}
|
||||
|
||||
function bearingFromSourceToListener(dx: number, dy: number): number {
|
||||
// 0 degrees is north (+y), 90 is east (+x), matching screen-reader compass wording.
|
||||
const degrees = Math.atan2(dx, dy) * (180 / Math.PI);
|
||||
return normalizeDegrees(degrees);
|
||||
}
|
||||
|
||||
function angularDifferenceDeg(a: number, b: number): number {
|
||||
const raw = Math.abs(normalizeDegrees(a) - normalizeDegrees(b));
|
||||
return raw > 180 ? 360 - raw : raw;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user