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); }); });