murmuraba
Version:
Real-time audio noise reduction with advanced chunked processing for web applications
200 lines (199 loc) • 6.83 kB
JavaScript
import { vi, beforeEach, afterEach } from 'vitest';
export function createMockGainNode(initialValue = 1) {
return {
gain: {
value: initialValue,
setValueAtTime: vi.fn().mockReturnThis(),
linearRampToValueAtTime: vi.fn().mockReturnThis(),
exponentialRampToValueAtTime: vi.fn().mockReturnThis(),
setTargetAtTime: vi.fn().mockReturnThis(),
setValueCurveAtTime: vi.fn().mockReturnThis(),
cancelScheduledValues: vi.fn().mockReturnThis(),
cancelAndHoldAtTime: vi.fn().mockReturnThis(),
},
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn(),
numberOfInputs: 1,
numberOfOutputs: 1,
channelCount: 2,
channelCountMode: 'max',
channelInterpretation: 'speakers',
};
}
export function createMockAnalyserNode() {
return {
fftSize: 2048,
frequencyBinCount: 1024,
minDecibels: -100,
maxDecibels: -30,
smoothingTimeConstant: 0.8,
getByteFrequencyData: vi.fn((array) => {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 128) + 64;
}
}),
getByteTimeDomainData: vi.fn((array) => {
for (let i = 0; i < array.length; i++) {
array[i] = 128 + Math.floor(Math.sin(i * 0.1) * 64);
}
}),
getFloatFrequencyData: vi.fn(),
getFloatTimeDomainData: vi.fn(),
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn(),
};
}
export function createMockScriptProcessor(bufferSize = 4096, numberOfInputChannels = 1, numberOfOutputChannels = 1) {
return {
bufferSize,
numberOfInputs: 1,
numberOfOutputs: 1,
onaudioprocess: null,
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
};
}
export function createMockBiquadFilter() {
return {
type: 'lowpass',
frequency: { value: 350, setValueAtTime: vi.fn() },
Q: { value: 1, setValueAtTime: vi.fn() },
gain: { value: 0, setValueAtTime: vi.fn() },
detune: { value: 0, setValueAtTime: vi.fn() },
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn(),
getFrequencyResponse: vi.fn(),
};
}
export function createMockMediaStreamSource() {
return {
connect: vi.fn().mockReturnThis(),
disconnect: vi.fn(),
mediaStream: null,
numberOfInputs: 0,
numberOfOutputs: 1,
};
}
export function createMockMediaStreamDestination() {
return {
stream: {
id: 'mock-output-stream',
active: true,
getTracks: vi.fn().mockReturnValue([]),
getAudioTracks: vi.fn().mockReturnValue([{ kind: 'audio' }]),
getVideoTracks: vi.fn().mockReturnValue([]),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addTrack: vi.fn(),
removeTrack: vi.fn(),
clone: vi.fn(),
getTrackById: vi.fn(),
},
numberOfInputs: 1,
numberOfOutputs: 0,
};
}
export function createMockAudioBuffer(numberOfChannels = 2, length = 48000, sampleRate = 48000) {
return {
numberOfChannels,
length,
sampleRate,
duration: length / sampleRate,
getChannelData: vi.fn(() => new Float32Array(length)),
copyFromChannel: vi.fn(),
copyToChannel: vi.fn(),
};
}
export function createMockAudioContext(options = {}) {
const { state = 'running', sampleRate = 48000, currentTime = 0, baseLatency = 0.01, outputLatency = 0.02, includeWorklet = false, includeAnalyser = false, includeBiquadFilter = false, includeMediaStreamDestination = false, } = options;
const context = {
state,
sampleRate,
currentTime,
baseLatency,
outputLatency,
destination: {
maxChannelCount: 2,
numberOfInputs: 1,
numberOfOutputs: 0,
channelCount: 2,
channelCountMode: 'max',
channelInterpretation: 'speakers',
},
listener: {
positionX: { value: 0 },
positionY: { value: 0 },
positionZ: { value: 0 },
forwardX: { value: 0 },
forwardY: { value: 0 },
forwardZ: { value: -1 },
upX: { value: 0 },
upY: { value: 1 },
upZ: { value: 0 },
},
createGain: vi.fn(() => createMockGainNode()),
createScriptProcessor: vi.fn((buffer, input, output) => createMockScriptProcessor(buffer, input, output)),
createMediaStreamSource: vi.fn(() => createMockMediaStreamSource()),
createBuffer: vi.fn((channels, length, rate) => createMockAudioBuffer(channels, length, rate)),
decodeAudioData: vi.fn().mockImplementation(() => Promise.resolve(createMockAudioBuffer())),
close: vi.fn().mockResolvedValue(undefined),
suspend: vi.fn().mockResolvedValue(undefined),
resume: vi.fn().mockResolvedValue(undefined),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
};
if (includeWorklet) {
context.audioWorklet = {
addModule: vi.fn().mockResolvedValue(undefined),
};
}
if (includeAnalyser) {
context.createAnalyser = vi.fn(() => createMockAnalyserNode());
}
if (includeBiquadFilter) {
context.createBiquadFilter = vi.fn(() => createMockBiquadFilter());
}
if (includeMediaStreamDestination) {
context.createMediaStreamDestination = vi.fn(() => createMockMediaStreamDestination());
}
return context;
}
/**
* Setup AudioContext mock globally
* Returns the mock instance for further customization
*/
export function setupAudioContextMock(options) {
const mockContext = createMockAudioContext(options);
// Store original if exists
const original = global.AudioContext;
// Setup mock
global.AudioContext = vi.fn(() => mockContext);
global.webkitAudioContext = global.AudioContext;
// Return cleanup function and mock
return {
context: mockContext,
restore: () => {
if (original) {
global.AudioContext = original;
global.webkitAudioContext = original;
}
},
};
}
/**
* Helper to use AudioContext mock in beforeEach/afterEach
*/
export function useAudioContextMock(options) {
let mock;
beforeEach(() => {
mock = setupAudioContextMock(options);
});
afterEach(() => {
mock?.restore();
vi.clearAllMocks();
});
return () => mock?.context;
}