- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer - Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support - Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline - Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots) - Migrate core application to Svelte 5 runes (, , ) with reactive stores - Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|
import {
|
|
floatTo16BitPCM,
|
|
encodeBase64,
|
|
decodeBase64,
|
|
decodeAudioData
|
|
} from '../services/audioUtils';
|
|
|
|
describe('Audio Utilities & PCM DSP', () => {
|
|
test('floatTo16BitPCM converts Float32Array to 16-bit PCM buffer with proper clamping', () => {
|
|
// Input with values within range, positive clamp (>1), negative clamp (<-1), and zero
|
|
const input = new Float32Array([0.0, 0.5, 1.0, 1.5, -0.5, -1.0, -1.5]);
|
|
const buffer = floatTo16BitPCM(input);
|
|
|
|
expect(buffer.byteLength).toBe(input.length * 2);
|
|
|
|
const view = new DataView(buffer);
|
|
// 0.0 -> 0
|
|
expect(view.getInt16(0, true)).toBe(0);
|
|
// 0.5 -> 0.5 * 32767 = 16383.5 -> 16383
|
|
expect(Math.abs(view.getInt16(2, true) - 16383)).toBeLessThanOrEqual(1);
|
|
// 1.0 -> 32767
|
|
expect(view.getInt16(4, true)).toBe(0x7fff);
|
|
// 1.5 clamped to 1.0 -> 32767
|
|
expect(view.getInt16(6, true)).toBe(0x7fff);
|
|
// -0.5 -> -0.5 * 32768 = -16384
|
|
expect(Math.abs(view.getInt16(8, true) - -16384)).toBeLessThanOrEqual(1);
|
|
// -1.0 -> -32768
|
|
expect(view.getInt16(10, true)).toBe(-0x8000);
|
|
// -1.5 clamped to -1.0 -> -32768
|
|
expect(view.getInt16(12, true)).toBe(-0x8000);
|
|
});
|
|
|
|
test('encodeBase64 and decodeBase64 perform lossless roundtrip for arbitrary byte arrays', () => {
|
|
const rawBytes = new Uint8Array([0, 1, 255, 128, 42, 64, 18, 99, 127]);
|
|
const encoded = encodeBase64(rawBytes);
|
|
expect(typeof encoded).toBe('string');
|
|
expect(encoded.length).toBeGreaterThan(0);
|
|
|
|
const decoded = decodeBase64(encoded);
|
|
expect(decoded.length).toBe(rawBytes.length);
|
|
expect(Array.from(decoded)).toEqual(Array.from(rawBytes));
|
|
});
|
|
|
|
test('decodeAudioData converts raw PCM Int16 bytes into AudioBuffer channels', async () => {
|
|
// Construct mock AudioContext with createBuffer
|
|
const sampleRate = 24000;
|
|
const numChannels = 1;
|
|
|
|
const mockChannelData = new Float32Array(4);
|
|
const mockAudioBuffer = {
|
|
length: 4,
|
|
numberOfChannels: 1,
|
|
sampleRate,
|
|
getChannelData: () => mockChannelData
|
|
};
|
|
|
|
const mockCtx = {
|
|
createBuffer: (channels: number, length: number, rate: number) => {
|
|
expect(channels).toBe(1);
|
|
expect(length).toBe(4);
|
|
expect(rate).toBe(sampleRate);
|
|
return mockAudioBuffer;
|
|
}
|
|
} as unknown as AudioContext;
|
|
|
|
// 4 16-bit samples: 0, 32767, -32768, 16384
|
|
const int16Samples = new Int16Array([0, 32767, -32768, 16384]);
|
|
const byteData = new Uint8Array(int16Samples.buffer);
|
|
|
|
const result = await decodeAudioData(byteData, mockCtx, sampleRate, numChannels);
|
|
expect(result).toBe(mockAudioBuffer);
|
|
|
|
// Verify channel data normalized between -1.0 and 1.0
|
|
expect(mockChannelData[0]).toBe(0);
|
|
expect(mockChannelData[1]).toBeCloseTo(1.0, 2);
|
|
expect(mockChannelData[2]).toBe(-1.0);
|
|
expect(mockChannelData[3]).toBeCloseTo(0.5, 2);
|
|
});
|
|
|
|
test('decodeAudioData handles uneven byte lengths gracefully', async () => {
|
|
const mockChannelData = new Float32Array(2);
|
|
const mockAudioBuffer = {
|
|
length: 2,
|
|
numberOfChannels: 1,
|
|
sampleRate: 24000,
|
|
getChannelData: () => mockChannelData
|
|
};
|
|
|
|
const mockCtx = {
|
|
createBuffer: () => mockAudioBuffer
|
|
} as unknown as AudioContext;
|
|
|
|
// 5 bytes (uneven) -> trimmed to 4 bytes (2 samples)
|
|
const unevenBytes = new Uint8Array([0, 0, 255, 127, 42]);
|
|
const result = await decodeAudioData(unevenBytes, mockCtx);
|
|
expect(result).toBe(mockAudioBuffer);
|
|
});
|
|
});
|