fix: RNNoise mono-left-only audio via explicit ChannelMerger stereo upmix
RNNoise worklet outputs mono (1ch) which played only in the left ear. Replace unreliable automatic up-mixing (channelCount/channelCountMode on inputGain) with a ChannelMergerNode that duplicates the mono signal to both L and R channels — guaranteed stereo by the Web Audio spec.
This commit is contained in:
@@ -25,6 +25,7 @@ export class AudioManager {
|
|||||||
streamGeneration = 0;
|
streamGeneration = 0;
|
||||||
inputSwitchChain = Promise.resolve(null);
|
inputSwitchChain = Promise.resolve(null);
|
||||||
rnnoiseNode = null;
|
rnnoiseNode = null;
|
||||||
|
stereoMerger = null;
|
||||||
rnnoiseEnabled = false;
|
rnnoiseEnabled = false;
|
||||||
rnnoiseReady = false;
|
rnnoiseReady = false;
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -40,11 +41,6 @@ export class AudioManager {
|
|||||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
this.ctx = new AudioContextClass({ sampleRate: 48000 });
|
this.ctx = new AudioContextClass({ sampleRate: 48000 });
|
||||||
this.inputGain = this.ctx.createGain();
|
this.inputGain = this.ctx.createGain();
|
||||||
// Force stereo up-mix so mono sources (e.g. RNNoise worklet output)
|
|
||||||
// are duplicated to both L+R channels instead of left-only.
|
|
||||||
this.inputGain.channelCount = 2;
|
|
||||||
this.inputGain.channelCountMode = 'explicit';
|
|
||||||
this.inputGain.channelInterpretation = 'speakers';
|
|
||||||
this.inputDestination = this.ctx.createMediaStreamDestination();
|
this.inputDestination = this.ctx.createMediaStreamDestination();
|
||||||
this.analyser = this.ctx.createAnalyser();
|
this.analyser = this.ctx.createAnalyser();
|
||||||
this.analyser.fftSize = 256;
|
this.analyser.fftSize = 256;
|
||||||
@@ -210,9 +206,18 @@ export class AudioManager {
|
|||||||
wasmBinary,
|
wasmBinary,
|
||||||
maxChannels: 1,
|
maxChannels: 1,
|
||||||
});
|
});
|
||||||
this.rnnoiseNode.connect(this.inputGain);
|
|
||||||
|
// Explicit mono→stereo: RNNoise outputs 1 channel, so duplicate it
|
||||||
|
// to both L and R via a ChannelMergerNode. This is spec-guaranteed
|
||||||
|
// stereo, unlike relying on automatic up-mixing which fails in some
|
||||||
|
// browsers when the source is an AudioWorkletNode.
|
||||||
|
this.stereoMerger = this.ctx.createChannelMerger(2);
|
||||||
|
this.rnnoiseNode.connect(this.stereoMerger, 0, 0); // mono → left
|
||||||
|
this.rnnoiseNode.connect(this.stereoMerger, 0, 1); // mono → right
|
||||||
|
this.stereoMerger.connect(this.inputGain);
|
||||||
|
|
||||||
this.rnnoiseReady = true;
|
this.rnnoiseReady = true;
|
||||||
console.log('[AudioManager] RNNoise worklet loaded and connected');
|
console.log('[AudioManager] RNNoise worklet loaded and connected (mono→stereo via ChannelMerger)');
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('[AudioManager] Failed to load RNNoise worklet:', err);
|
console.error('[AudioManager] Failed to load RNNoise worklet:', err);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class AudioManager {
|
|||||||
private streamGeneration = 0;
|
private streamGeneration = 0;
|
||||||
private inputSwitchChain: Promise<MediaStream | null> = Promise.resolve(null);
|
private inputSwitchChain: Promise<MediaStream | null> = Promise.resolve(null);
|
||||||
private rnnoiseNode: AudioWorkletNode | null = null;
|
private rnnoiseNode: AudioWorkletNode | null = null;
|
||||||
|
private stereoMerger: ChannelMergerNode | null = null;
|
||||||
private rnnoiseEnabled = false;
|
private rnnoiseEnabled = false;
|
||||||
private rnnoiseReady = false;
|
private rnnoiseReady = false;
|
||||||
|
|
||||||
@@ -45,11 +46,6 @@ export class AudioManager {
|
|||||||
this.ctx = new AudioContextClass({ sampleRate: 48000 });
|
this.ctx = new AudioContextClass({ sampleRate: 48000 });
|
||||||
|
|
||||||
this.inputGain = this.ctx.createGain();
|
this.inputGain = this.ctx.createGain();
|
||||||
// Force stereo up-mix so mono sources (e.g. RNNoise worklet output)
|
|
||||||
// are duplicated to both L+R channels instead of left-only.
|
|
||||||
this.inputGain.channelCount = 2;
|
|
||||||
this.inputGain.channelCountMode = 'explicit';
|
|
||||||
this.inputGain.channelInterpretation = 'speakers';
|
|
||||||
this.inputDestination = this.ctx.createMediaStreamDestination();
|
this.inputDestination = this.ctx.createMediaStreamDestination();
|
||||||
this.analyser = this.ctx.createAnalyser();
|
this.analyser = this.ctx.createAnalyser();
|
||||||
this.analyser.fftSize = 256;
|
this.analyser.fftSize = 256;
|
||||||
@@ -258,9 +254,18 @@ export class AudioManager {
|
|||||||
wasmBinary,
|
wasmBinary,
|
||||||
maxChannels: 1,
|
maxChannels: 1,
|
||||||
});
|
});
|
||||||
this.rnnoiseNode.connect(this.inputGain!);
|
|
||||||
|
// Explicit mono→stereo: RNNoise outputs 1 channel, so duplicate it
|
||||||
|
// to both L and R via a ChannelMergerNode. This is spec-guaranteed
|
||||||
|
// stereo, unlike relying on automatic up-mixing which fails in some
|
||||||
|
// browsers when the source is an AudioWorkletNode.
|
||||||
|
this.stereoMerger = this.ctx!.createChannelMerger(2);
|
||||||
|
this.rnnoiseNode.connect(this.stereoMerger, 0, 0); // mono → left
|
||||||
|
this.rnnoiseNode.connect(this.stereoMerger, 0, 1); // mono → right
|
||||||
|
this.stereoMerger.connect(this.inputGain!);
|
||||||
|
|
||||||
this.rnnoiseReady = true;
|
this.rnnoiseReady = true;
|
||||||
console.log('[AudioManager] RNNoise worklet loaded and connected');
|
console.log('[AudioManager] RNNoise worklet loaded and connected (mono→stereo via ChannelMerger)');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AudioManager] Failed to load RNNoise worklet:', err);
|
console.error('[AudioManager] Failed to load RNNoise worklet:', err);
|
||||||
this.rnnoiseReady = false;
|
this.rnnoiseReady = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user