mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-26 00:29:14 +00:00
91 lines
3.3 KiB
JavaScript
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`);
|
|
}
|
|
});
|