Add emit proxy fix and HRTF audio mode
This commit is contained in:
@@ -7,7 +7,14 @@ import {
|
||||
type EffectId,
|
||||
type EffectRuntime,
|
||||
} from './effects';
|
||||
import { applySpatialMixToNodes, resolveSpatialMix, SPATIAL_RAMP_SECONDS, SPATIAL_TIME_CONSTANT_SECONDS } from './spatial';
|
||||
import { resolveSpatialMix } from './spatial';
|
||||
import {
|
||||
applySpatialOutput,
|
||||
createSpatialOutputRuntime,
|
||||
disconnectSpatialOutputRuntime,
|
||||
type SpatialOutputRuntime,
|
||||
type SpatialRenderMode,
|
||||
} from './spatialOutput';
|
||||
|
||||
export type SpatialPeerRuntime = {
|
||||
nickname: string;
|
||||
@@ -15,7 +22,7 @@ export type SpatialPeerRuntime = {
|
||||
y: number;
|
||||
listenGain?: number;
|
||||
gain?: GainNode;
|
||||
panner?: StereoPannerNode;
|
||||
spatialOutput?: SpatialOutputRuntime;
|
||||
audioElement?: HTMLAudioElement;
|
||||
};
|
||||
|
||||
@@ -36,7 +43,7 @@ type ActiveSpatialSampleRuntime = {
|
||||
range: number;
|
||||
baseGain: number;
|
||||
gainNode: GainNode;
|
||||
pannerNode: StereoPannerNode | null;
|
||||
spatialOutput: SpatialOutputRuntime;
|
||||
sourceNode: AudioBufferSourceNode;
|
||||
};
|
||||
|
||||
@@ -56,6 +63,7 @@ export class AudioEngine {
|
||||
private loopbackEnabled = false;
|
||||
private loopbackRuntime: EffectRuntime | null = null;
|
||||
private outputMode: OutputMode = 'stereo';
|
||||
private spatialMode: SpatialRenderMode = 'classic';
|
||||
private masterVolume = 50;
|
||||
private voiceLayerEnabled = true;
|
||||
private effectIndex = EFFECT_SEQUENCE.findIndex((effect) => effect.id === 'off');
|
||||
@@ -189,6 +197,19 @@ export class AudioEngine {
|
||||
this.outputMode = mode;
|
||||
}
|
||||
|
||||
setSpatialMode(mode: SpatialRenderMode): void {
|
||||
this.spatialMode = mode;
|
||||
}
|
||||
|
||||
getSpatialMode(): SpatialRenderMode {
|
||||
return this.spatialMode;
|
||||
}
|
||||
|
||||
toggleSpatialMode(): SpatialRenderMode {
|
||||
this.spatialMode = this.spatialMode === 'classic' ? 'hrtf' : 'classic';
|
||||
return this.spatialMode;
|
||||
}
|
||||
|
||||
setMasterVolume(value: number): number {
|
||||
const next = Math.max(0, Math.min(100, Number.isFinite(value) ? Math.round(value) : 50));
|
||||
this.masterVolume = next;
|
||||
@@ -279,21 +300,20 @@ export class AudioEngine {
|
||||
const gainNode = this.audioCtx.createGain();
|
||||
sourceNode.connect(gainNode);
|
||||
|
||||
let pannerNode: StereoPannerNode | undefined;
|
||||
if (this.supportsStereoPanner()) {
|
||||
pannerNode = this.audioCtx.createStereoPanner();
|
||||
if (this.voiceLayerEnabled) {
|
||||
gainNode.connect(pannerNode).connect(this.masterGainNode ?? this.audioCtx.destination);
|
||||
}
|
||||
} else {
|
||||
if (this.voiceLayerEnabled) {
|
||||
gainNode.connect(this.masterGainNode ?? this.audioCtx.destination);
|
||||
}
|
||||
let spatialOutput: SpatialOutputRuntime = { kind: 'none' };
|
||||
if (this.voiceLayerEnabled) {
|
||||
spatialOutput = createSpatialOutputRuntime({
|
||||
audioCtx: this.audioCtx,
|
||||
inputNode: gainNode,
|
||||
destination: this.masterGainNode ?? this.audioCtx.destination,
|
||||
outputMode: this.outputMode,
|
||||
spatialMode: this.spatialMode,
|
||||
});
|
||||
}
|
||||
|
||||
peer.audioElement = audioElement;
|
||||
peer.gain = gainNode;
|
||||
peer.panner = pannerNode;
|
||||
peer.spatialOutput = spatialOutput;
|
||||
}
|
||||
|
||||
updateSpatialAudio(peers: Iterable<SpatialPeerRuntime>, playerPosition: { x: number; y: number }): void {
|
||||
@@ -310,13 +330,15 @@ export class AudioEngine {
|
||||
});
|
||||
const listenGain = Number.isFinite(peer.listenGain) ? Math.max(0, peer.listenGain as number) : 1;
|
||||
const scaledMix = mix ? { ...mix, gain: mix.gain * listenGain } : null;
|
||||
applySpatialMixToNodes({
|
||||
applySpatialOutput({
|
||||
audioCtx: this.audioCtx,
|
||||
runtime: peer.spatialOutput ?? { kind: 'none' },
|
||||
gainNode: peer.gain,
|
||||
pannerNode: peer.panner ?? null,
|
||||
mix: scaledMix,
|
||||
outputMode: this.outputMode,
|
||||
transition: 'target',
|
||||
dx: peer.x - playerPosition.x,
|
||||
dy: peer.y - playerPosition.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -375,20 +397,20 @@ export class AudioEngine {
|
||||
const gainNode = audioCtx.createGain();
|
||||
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
source.connect(gainNode);
|
||||
let pannerNode: StereoPannerNode | null = null;
|
||||
if (this.supportsStereoPanner() && this.outputMode === 'stereo') {
|
||||
pannerNode = audioCtx.createStereoPanner();
|
||||
gainNode.connect(pannerNode).connect(sfxGainNode);
|
||||
} else {
|
||||
gainNode.connect(sfxGainNode);
|
||||
}
|
||||
const spatialOutput = createSpatialOutputRuntime({
|
||||
audioCtx,
|
||||
inputNode: gainNode,
|
||||
destination: sfxGainNode,
|
||||
outputMode: this.outputMode,
|
||||
spatialMode: this.spatialMode,
|
||||
});
|
||||
const runtime: ActiveSpatialSampleRuntime = {
|
||||
sourceX: sourcePosition.x,
|
||||
sourceY: sourcePosition.y,
|
||||
range: Math.max(1, range),
|
||||
baseGain: gain,
|
||||
gainNode,
|
||||
pannerNode,
|
||||
spatialOutput,
|
||||
sourceNode: source,
|
||||
};
|
||||
this.activeSpatialSamples.add(runtime);
|
||||
@@ -401,7 +423,7 @@ export class AudioEngine {
|
||||
// Ignore stale graph disconnects.
|
||||
}
|
||||
gainNode.disconnect();
|
||||
pannerNode?.disconnect();
|
||||
disconnectSpatialOutputRuntime(spatialOutput);
|
||||
};
|
||||
source.start();
|
||||
} catch {
|
||||
@@ -428,20 +450,20 @@ export class AudioEngine {
|
||||
const gainNode = audioCtx.createGain();
|
||||
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
source.connect(gainNode);
|
||||
let pannerNode: StereoPannerNode | null = null;
|
||||
if (this.supportsStereoPanner() && this.outputMode === 'stereo') {
|
||||
pannerNode = audioCtx.createStereoPanner();
|
||||
gainNode.connect(pannerNode).connect(sfxGainNode);
|
||||
} else {
|
||||
gainNode.connect(sfxGainNode);
|
||||
}
|
||||
const spatialOutput = createSpatialOutputRuntime({
|
||||
audioCtx,
|
||||
inputNode: gainNode,
|
||||
destination: sfxGainNode,
|
||||
outputMode: this.outputMode,
|
||||
spatialMode: this.spatialMode,
|
||||
});
|
||||
const runtime: ActiveSpatialSampleRuntime = {
|
||||
sourceX: sourcePosition.x,
|
||||
sourceY: sourcePosition.y,
|
||||
range: Math.max(1, range),
|
||||
baseGain: gain,
|
||||
gainNode,
|
||||
pannerNode,
|
||||
spatialOutput,
|
||||
sourceNode: source,
|
||||
};
|
||||
this.activeSpatialSamples.add(runtime);
|
||||
@@ -455,7 +477,7 @@ export class AudioEngine {
|
||||
// Ignore stale graph disconnects.
|
||||
}
|
||||
gainNode.disconnect();
|
||||
pannerNode?.disconnect();
|
||||
disconnectSpatialOutputRuntime(spatialOutput);
|
||||
resolve();
|
||||
};
|
||||
source.start();
|
||||
@@ -526,10 +548,12 @@ export class AudioEngine {
|
||||
peer.audioElement.remove();
|
||||
}
|
||||
peer.gain?.disconnect();
|
||||
peer.panner?.disconnect();
|
||||
if (peer.spatialOutput) {
|
||||
disconnectSpatialOutputRuntime(peer.spatialOutput);
|
||||
}
|
||||
peer.audioElement = undefined;
|
||||
peer.gain = undefined;
|
||||
peer.panner = undefined;
|
||||
peer.spatialOutput = undefined;
|
||||
}
|
||||
|
||||
private rebuildOutboundEffectGraph(): void {
|
||||
@@ -589,8 +613,6 @@ export class AudioEngine {
|
||||
: { gain: baseGain, pan: 0 };
|
||||
if (!resolved) return;
|
||||
const finalGain = resolved.gain;
|
||||
const panValue = spec.sourcePosition ? resolved.pan : undefined;
|
||||
|
||||
if (finalGain <= 0) return;
|
||||
|
||||
const startTime = audioCtx.currentTime + (spec.delay ?? 0);
|
||||
@@ -603,16 +625,40 @@ export class AudioEngine {
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + spec.duration);
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
if (panValue !== undefined && this.supportsStereoPanner() && this.outputMode === 'stereo') {
|
||||
const panner = audioCtx.createStereoPanner();
|
||||
panner.pan.setValueAtTime(Math.max(-1, Math.min(1, panValue)), startTime);
|
||||
gainNode.connect(panner).connect(sfxGainNode);
|
||||
let spatialOutput: SpatialOutputRuntime | null = null;
|
||||
if (spec.sourcePosition && this.outputMode === 'stereo') {
|
||||
if (this.spatialMode === 'hrtf' && typeof audioCtx.createPanner === 'function') {
|
||||
const panner = audioCtx.createPanner();
|
||||
panner.panningModel = 'HRTF';
|
||||
panner.distanceModel = 'inverse';
|
||||
panner.refDistance = 1;
|
||||
panner.maxDistance = 10000;
|
||||
panner.rolloffFactor = 0;
|
||||
panner.positionX.setValueAtTime(spec.sourcePosition.x, startTime);
|
||||
panner.positionY.setValueAtTime(0, startTime);
|
||||
panner.positionZ.setValueAtTime(-spec.sourcePosition.y, startTime);
|
||||
gainNode.connect(panner).connect(sfxGainNode);
|
||||
spatialOutput = { kind: 'hrtf', node: panner };
|
||||
} else if (this.supportsStereoPanner()) {
|
||||
const panner = audioCtx.createStereoPanner();
|
||||
panner.pan.setValueAtTime(Math.max(-1, Math.min(1, resolved.pan)), startTime);
|
||||
gainNode.connect(panner).connect(sfxGainNode);
|
||||
spatialOutput = { kind: 'classic', node: panner };
|
||||
} else {
|
||||
gainNode.connect(sfxGainNode);
|
||||
}
|
||||
} else {
|
||||
gainNode.connect(sfxGainNode);
|
||||
}
|
||||
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(startTime + spec.duration);
|
||||
oscillator.onended = () => {
|
||||
if (spatialOutput) {
|
||||
disconnectSpatialOutputRuntime(spatialOutput);
|
||||
}
|
||||
gainNode.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
private applySpatialSampleRuntime(
|
||||
@@ -628,22 +674,27 @@ export class AudioEngine {
|
||||
baseGain: sample.baseGain,
|
||||
});
|
||||
if (initial) {
|
||||
const gainValue = mix?.gain ?? 0;
|
||||
sample.gainNode.gain.setTargetAtTime(gainValue, this.audioCtx.currentTime, ONE_SHOT_ATTACK_SECONDS);
|
||||
if (sample.pannerNode) {
|
||||
const panValue = mix?.pan ?? 0;
|
||||
const resolvedPan = this.outputMode === 'mono' ? 0 : Math.max(-1, Math.min(1, panValue));
|
||||
sample.pannerNode.pan.setValueAtTime(resolvedPan, this.audioCtx.currentTime);
|
||||
}
|
||||
applySpatialOutput({
|
||||
audioCtx: this.audioCtx,
|
||||
runtime: sample.spatialOutput,
|
||||
gainNode: sample.gainNode,
|
||||
mix,
|
||||
outputMode: this.outputMode,
|
||||
transition: 'linear',
|
||||
dx: sample.sourceX - playerPosition.x,
|
||||
dy: sample.sourceY - playerPosition.y,
|
||||
});
|
||||
return;
|
||||
}
|
||||
applySpatialMixToNodes({
|
||||
applySpatialOutput({
|
||||
audioCtx: this.audioCtx,
|
||||
runtime: sample.spatialOutput,
|
||||
gainNode: sample.gainNode,
|
||||
pannerNode: sample.pannerNode,
|
||||
mix,
|
||||
outputMode: this.outputMode,
|
||||
transition: 'target',
|
||||
dx: sample.sourceX - playerPosition.x,
|
||||
dy: sample.sourceY - playerPosition.y,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user