Files
Haven/test/micMeterLevel.test.js

91 lines
3.3 KiB
JavaScript

'use strict';
// The mic level meter in voice settings reads voice.currentMicLevel, which
// the noise gate loop sets from an analyser on the raw mic. It has to keep
// moving with noise reduction off, on the gate, and on AI suppression, and
// switching between them must never unhook the analyser. Before 4.22.0 the
// level was only worked out in gate mode, so the meter sat flat with noise
// reduction off (reported again from Haven Desktop).
const assert = require('node:assert/strict');
const test = require('node:test');
function fakeNode(name) {
const node = {
name,
targets: new Set(),
connect(target) { node.targets.add(target); return target; },
disconnect(target) { if (target) node.targets.delete(target); else node.targets.clear(); },
};
return node;
}
function loadVoice() {
const saved = {};
global.localStorage = { getItem: () => null, setItem: (k, v) => { saved[k] = v; }, removeItem() {} };
global.AudioWorkletNode = class {
constructor() {
Object.assign(this, fakeNode('rnnoise'));
this.port = { postMessage() {}, onmessage: null, onmessageerror: null };
}
};
delete require.cache[require.resolve('../public/js/voice.js')];
const { VoiceManager } = require('../public/js/voice.js');
const source = fakeNode('source');
const analyser = Object.assign(fakeNode('analyser'), {
frequencyBinCount: 8,
level: 25,
getByteFrequencyData(arr) { arr.fill(this.level); },
});
const gain = Object.assign(fakeNode('gain'), { gain: { value: 1, setTargetAtTime(v) { this.value = v; } } });
source.connect(analyser);
source.connect(gain);
const voice = Object.assign(Object.create(VoiceManager.prototype), {
audioCtx: { currentTime: 0, sampleRate: 48000 },
inVoice: true,
noiseMode: 'gate',
noiseSensitivity: 10,
currentMicLevel: 0,
_noiseGateInterval: null,
_noiseGateAnalyser: analyser,
_noiseGateGain: gain,
_rnnoiseSource: source,
_rnnoiseNode: null,
_rnnoiseReady: false,
_rnnoiseWasmBytes: new ArrayBuffer(4096),
});
return { voice, source, analyser, gain };
}
test('the mic level follows the raw mic in every noise mode, and while switching', (t) => {
const ticks = [];
const realSetInterval = global.setInterval;
const realClearInterval = global.clearInterval;
global.setInterval = (fn) => { ticks.push(fn); return ticks.length; };
global.clearInterval = () => {};
t.after(() => {
global.setInterval = realSetInterval;
global.clearInterval = realClearInterval;
delete global.localStorage;
delete global.AudioWorkletNode;
});
const { voice, source, analyser } = loadVoice();
voice._startNoiseGate();
assert.equal(ticks.length, 1);
const tick = ticks[0];
for (const mode of ['off', 'suppress', 'off', 'gate', 'suppress', 'gate', 'off']) {
voice.setNoiseMode(mode);
voice.currentMicLevel = 0;
analyser.level = 25;
tick();
assert.equal(voice.currentMicLevel, 50, `${mode}: the meter moves`);
analyser.level = 0;
tick();
assert.equal(voice.currentMicLevel, 0, `${mode}: and falls back with silence`);
assert.ok(source.targets.has(analyser), `${mode}: the analyser still hears the raw mic`);
assert.equal(!!voice._rnnoiseNode, mode === 'suppress', `${mode}: AI suppression only in its own mode`);
}
});