mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(voice): apply noise suppression in the mic test (#2714)
This commit is contained in:
1 parent
3affd295e8
commit
d0c84b3d9b
39 files changed
+454
-1276
No files matched your search
@@ -46,7 +46,7 @@ import {
|
||||
setNoiseSuppressionChoice,
|
||||
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionChoices';
|
||||
import {
|
||||
getNoiseSuppressionChoiceLabelDescriptor,
|
||||
getNoiseSuppressionChoiceLabel,
|
||||
STEREO_MICROPHONE_DESCRIPTION_DESCRIPTOR,
|
||||
STEREO_MICROPHONE_DESCRIPTOR,
|
||||
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionLabels';
|
||||
@@ -284,7 +284,7 @@ export const VoiceTab: React.FC<VoiceTabProps> = observer(({voiceSettings, autoR
|
||||
const noiseSuppressionOptions: Array<ComboboxOption<VoiceNoiseSuppressionBackend>> =
|
||||
getNoiseSuppressionChoiceValues().map((backend) => ({
|
||||
value: backend,
|
||||
label: i18n._(getNoiseSuppressionChoiceLabelDescriptor(backend)),
|
||||
label: getNoiseSuppressionChoiceLabel(i18n, backend),
|
||||
}));
|
||||
const stereoMicrophoneAvailable = isStereoMicrophoneChoiceAvailable();
|
||||
const setPushToTalkEnabled = (enabled: boolean) => {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {createVoiceSoftClipNode} from '@app/features/voice/engine/VoiceSharedAudioContext';
|
||||
import {buildDeepFilterAudioChain, type DeepFilterAudioChain} from '@app/features/voice/utils/DeepFilterNoiseProcessor';
|
||||
import {
|
||||
buildNoiseSuppressionWorkletChain,
|
||||
type NoiseSuppressionWorkletChain,
|
||||
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionChain';
|
||||
import type {NoiseSuppressionWorkletBackend} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionWorkletTypes';
|
||||
|
||||
const logger = new Logger('MicTestAudioGraph');
|
||||
|
||||
@@ -27,6 +32,8 @@ interface CreateMicTestAudioGraphOptions {
|
||||
playbackDelaySeconds: number;
|
||||
deepFilter: boolean;
|
||||
deepFilterNoiseReductionLevel: number;
|
||||
workletBackend: NoiseSuppressionWorkletBackend | null;
|
||||
suppressionStrength: number;
|
||||
}
|
||||
|
||||
export async function createMicTestAudioGraph({
|
||||
@@ -38,6 +45,8 @@ export async function createMicTestAudioGraph({
|
||||
playbackDelaySeconds,
|
||||
deepFilter,
|
||||
deepFilterNoiseReductionLevel,
|
||||
workletBackend,
|
||||
suppressionStrength,
|
||||
}: CreateMicTestAudioGraphOptions): Promise<MicTestAudioGraph> {
|
||||
const source = audioContext.createMediaStreamSource(new MediaStream([sourceTrack]));
|
||||
const inputGainNode = audioContext.createGain();
|
||||
@@ -45,8 +54,9 @@ export async function createMicTestAudioGraph({
|
||||
source.connect(inputGainNode);
|
||||
|
||||
let deepFilterChain: DeepFilterAudioChain | null = null;
|
||||
let passthroughDestination: MediaStreamAudioDestinationNode | null = null;
|
||||
let processedTrack: MediaStreamTrack | null = null;
|
||||
let workletChain: NoiseSuppressionWorkletChain | null = null;
|
||||
let suppressedSource: MediaStreamAudioSourceNode | null = null;
|
||||
let monitorHead: AudioNode = inputGainNode;
|
||||
try {
|
||||
if (deepFilter) {
|
||||
deepFilterChain = await buildDeepFilterAudioChain({
|
||||
@@ -54,25 +64,32 @@ export async function createMicTestAudioGraph({
|
||||
noiseReductionLevel: deepFilterNoiseReductionLevel,
|
||||
});
|
||||
inputGainNode.connect(deepFilterChain.inputDestination);
|
||||
processedTrack = deepFilterChain.processedTrack;
|
||||
} else {
|
||||
passthroughDestination = audioContext.createMediaStreamDestination();
|
||||
inputGainNode.connect(passthroughDestination);
|
||||
processedTrack = passthroughDestination.stream.getAudioTracks()[0] ?? null;
|
||||
}
|
||||
if (!processedTrack) {
|
||||
throw new Error('Mic test graph produced no processed audio track');
|
||||
suppressedSource = audioContext.createMediaStreamSource(new MediaStream([deepFilterChain.processedTrack]));
|
||||
monitorHead = suppressedSource;
|
||||
} else if (workletBackend) {
|
||||
workletChain = await buildNoiseSuppressionWorkletChain({
|
||||
audioContext,
|
||||
backend: workletBackend,
|
||||
suppressionStrength,
|
||||
});
|
||||
inputGainNode.connect(workletChain.inputDestination);
|
||||
suppressedSource = audioContext.createMediaStreamSource(new MediaStream([workletChain.processedTrack]));
|
||||
monitorHead = suppressedSource;
|
||||
}
|
||||
} catch (error) {
|
||||
inputGainNode.disconnect();
|
||||
source.disconnect();
|
||||
passthroughDestination?.disconnect();
|
||||
passthroughDestination?.stream.getTracks().forEach((track) => track.stop());
|
||||
suppressedSource?.disconnect();
|
||||
if (deepFilterChain) {
|
||||
await deepFilterChain.dispose().catch((disposeError) => {
|
||||
logger.debug('Failed to dispose DeepFilter chain after mic test graph init failure', disposeError);
|
||||
});
|
||||
}
|
||||
if (workletChain) {
|
||||
await workletChain.dispose().catch((disposeError) => {
|
||||
logger.debug('Failed to dispose noise suppression chain after mic test graph init failure', disposeError);
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -86,7 +103,7 @@ export async function createMicTestAudioGraph({
|
||||
const softClip = createVoiceSoftClipNode(audioContext);
|
||||
const softClipInput = softClip?.input ?? outputGainNode;
|
||||
const softClipOutput: AudioNode = softClip?.output ?? outputGainNode;
|
||||
inputGainNode.connect(analyser);
|
||||
monitorHead.connect(analyser);
|
||||
analyser.connect(delay);
|
||||
delay.connect(outputGainNode);
|
||||
if (softClip) outputGainNode.connect(softClip.input);
|
||||
@@ -100,13 +117,17 @@ export async function createMicTestAudioGraph({
|
||||
outputGainNode.disconnect();
|
||||
softClipInput.disconnect();
|
||||
softClipOutput.disconnect();
|
||||
passthroughDestination?.disconnect();
|
||||
passthroughDestination?.stream.getTracks().forEach((track) => track.stop());
|
||||
suppressedSource?.disconnect();
|
||||
if (deepFilterChain) {
|
||||
await deepFilterChain.dispose().catch((error) => {
|
||||
logger.warn('Failed to dispose DeepFilter chain for mic test graph', error);
|
||||
});
|
||||
}
|
||||
if (workletChain) {
|
||||
await workletChain.dispose().catch((error) => {
|
||||
logger.warn('Failed to dispose noise suppression chain for mic test graph', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,13 @@ import {
|
||||
createMicTestAudioGraph,
|
||||
type MicTestAudioGraph,
|
||||
} from '@app/features/user/components/modals/tabs/hooks/MicTestAudioGraph';
|
||||
import {
|
||||
getNoiseSuppressionBackendDescriptor,
|
||||
type VoiceNoiseSuppressionBackend,
|
||||
} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionBackends';
|
||||
import {readEffectiveNoiseSuppression} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionRuntime';
|
||||
import {applyNoiseSuppressionOverride} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionSelection';
|
||||
import type {NoiseSuppressionWorkletBackend} from '@app/features/voice/utils/noise_suppression/NoiseSuppressionWorkletTypes';
|
||||
import {
|
||||
applyContentHintToTrack,
|
||||
resolveVoiceProcessing,
|
||||
@@ -41,6 +48,14 @@ export interface MicTestSettings {
|
||||
voiceProcessingMode: VoiceProcessingMode;
|
||||
}
|
||||
|
||||
const MIC_TEST_PROBE_SAMPLE_RATE = 48000;
|
||||
|
||||
function resolveMicTestWorkletBackend(backend: VoiceNoiseSuppressionBackend): NoiseSuppressionWorkletBackend | null {
|
||||
return getNoiseSuppressionBackendDescriptor(backend).engine === 'worklet'
|
||||
? (backend as NoiseSuppressionWorkletBackend)
|
||||
: null;
|
||||
}
|
||||
|
||||
function normalizeOutputDeviceId(deviceId: string): string {
|
||||
return deviceId === 'default' ? '' : deviceId;
|
||||
}
|
||||
@@ -62,6 +77,7 @@ export const useMicTest = (settings: MicTestSettings) => {
|
||||
const restartPendingRef = useRef(false);
|
||||
const activeCaptureSignatureRef = useRef<string | null>(null);
|
||||
const micExplicitlyDenied = MediaPermission.microphoneExplicitlyDenied;
|
||||
const activeNoiseSuppression = readEffectiveNoiseSuppression(MIC_TEST_PROBE_SAMPLE_RATE);
|
||||
const captureSignature = useMemo(
|
||||
() =>
|
||||
JSON.stringify({
|
||||
@@ -73,6 +89,8 @@ export const useMicTest = (settings: MicTestSettings) => {
|
||||
deepFilterNoiseSuppression: settings.deepFilterNoiseSuppression,
|
||||
deepFilterNoiseSuppressionLevel: settings.deepFilterNoiseSuppressionLevel,
|
||||
voiceProcessingMode: settings.voiceProcessingMode,
|
||||
noiseSuppressionBackend: activeNoiseSuppression.backend,
|
||||
noiseSuppressionStrength: activeNoiseSuppression.suppressionStrength,
|
||||
}),
|
||||
[
|
||||
settings.autoGainControl,
|
||||
@@ -83,6 +101,8 @@ export const useMicTest = (settings: MicTestSettings) => {
|
||||
settings.noiseSuppression,
|
||||
settings.outputDeviceId,
|
||||
settings.voiceProcessingMode,
|
||||
activeNoiseSuppression.backend,
|
||||
activeNoiseSuppression.suppressionStrength,
|
||||
],
|
||||
);
|
||||
const updateLevel = useCallback(() => {
|
||||
@@ -171,7 +191,9 @@ export const useMicTest = (settings: MicTestSettings) => {
|
||||
return exhaustive;
|
||||
}
|
||||
}
|
||||
const profile = resolveVoiceProcessing(settings);
|
||||
const effectiveNoiseSuppression = readEffectiveNoiseSuppression(MIC_TEST_PROBE_SAMPLE_RATE);
|
||||
const profile = applyNoiseSuppressionOverride(resolveVoiceProcessing(settings), effectiveNoiseSuppression);
|
||||
const workletBackend = resolveMicTestWorkletBackend(profile.noiseSuppressionBackend);
|
||||
const baseAudioConstraints: MediaTrackConstraints & {voiceIsolation?: boolean} = {
|
||||
echoCancellation: profile.echoCancellation,
|
||||
noiseSuppression: profile.browserNoiseSuppression,
|
||||
@@ -219,6 +241,8 @@ export const useMicTest = (settings: MicTestSettings) => {
|
||||
playbackDelaySeconds: MIC_TEST_MONITOR_DELAY_SECONDS,
|
||||
deepFilter: profile.deepFilter,
|
||||
deepFilterNoiseReductionLevel: profile.deepFilterNoiseReductionLevel,
|
||||
workletBackend,
|
||||
suppressionStrength: effectiveNoiseSuppression.suppressionStrength,
|
||||
});
|
||||
timeDomainDataRef.current = new Float32Array(graphRef.current.analyser.fftSize);
|
||||
const audioElement = new Audio();
|
||||
|
||||
Reference in new issue
Block a user