diff options
Diffstat (limited to 'apps/web/src/voiceover/__tests__/enhancements.test.ts')
| -rw-r--r-- | apps/web/src/voiceover/__tests__/enhancements.test.ts | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/apps/web/src/voiceover/__tests__/enhancements.test.ts b/apps/web/src/voiceover/__tests__/enhancements.test.ts new file mode 100644 index 00000000..bc5b4b7c --- /dev/null +++ b/apps/web/src/voiceover/__tests__/enhancements.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, test } from "bun:test"; +import { + DEFAULT_VOICE_OVER_ENHANCEMENTS, + encodeAudioBufferToWav, + getAudioBufferPeak, + hasEnabledVoiceOverEnhancement, + type VoiceOverEnhancementSettings, +} from "@/voiceover/enhancements"; + +/* eslint-disable opencut/prefer-object-params -- Browser AudioBuffer methods are positional. */ +class FakeAudioBuffer implements AudioBuffer { + duration: number; + length: number; + numberOfChannels: number; + sampleRate: number; + + constructor( + private readonly channels: Array<Float32Array<ArrayBuffer>>, + sampleRate: number, + ) { + this.length = channels[0]?.length ?? 0; + this.numberOfChannels = channels.length; + this.sampleRate = sampleRate; + this.duration = this.length / sampleRate; + } + + copyFromChannel( + destination: Float32Array<ArrayBuffer>, + channelNumber: number, + bufferOffset = 0, + ): void { + destination.set( + this.getChannelData(channelNumber).slice( + bufferOffset, + bufferOffset + destination.length, + ), + ); + } + + copyToChannel( + source: Float32Array<ArrayBuffer>, + channelNumber: number, + bufferOffset = 0, + ): void { + this.getChannelData(channelNumber).set(source, bufferOffset); + } + + getChannelData(channel: number): Float32Array<ArrayBuffer> { + const data = this.channels[channel]; + if (!data) { + throw new Error(`Missing channel ${channel}`); + } + return data; + } +} +/* eslint-enable opencut/prefer-object-params */ + +describe("hasEnabledVoiceOverEnhancement", () => { + test("detects enabled defaults", () => { + expect( + hasEnabledVoiceOverEnhancement({ + settings: DEFAULT_VOICE_OVER_ENHANCEMENTS, + }), + ).toBe(true); + }); + + test("detects an all-disabled chain", () => { + const settings: VoiceOverEnhancementSettings = { + voiceBoost: false, + noiseGate: false, + highPass: false, + deEss: false, + limiter: false, + normalize: false, + }; + expect(hasEnabledVoiceOverEnhancement({ settings })).toBe(false); + }); +}); + +describe("getAudioBufferPeak", () => { + test("returns the maximum absolute sample", () => { + const buffer = new FakeAudioBuffer( + [ + createSamples([0, -0.25, 0.5]), + createSamples([0.1, -0.8, 0.2]), + ], + 48000, + ); + + expect(getAudioBufferPeak({ buffer })).toBeCloseTo(0.8); + }); +}); + +describe("encodeAudioBufferToWav", () => { + test("writes a 16-bit PCM WAV header", async () => { + const buffer = new FakeAudioBuffer( + [createSamples([0, 0.5, -0.5])], + 44100, + ); + + const blob = encodeAudioBufferToWav({ buffer }); + const view = new DataView(await blob.arrayBuffer()); + + expect(blob.type).toBe("audio/wav"); + expect(blob.size).toBe(50); + expect(readAscii({ view, offset: 0, length: 4 })).toBe("RIFF"); + expect(readAscii({ view, offset: 8, length: 4 })).toBe("WAVE"); + expect(view.getUint16(22, true)).toBe(1); + expect(view.getUint32(24, true)).toBe(44100); + expect(view.getUint16(34, true)).toBe(16); + expect(readAscii({ view, offset: 36, length: 4 })).toBe("data"); + }); +}); + +function readAscii({ + view, + offset, + length, +}: { + view: DataView; + offset: number; + length: number; +}): string { + return Array.from({ length }, (_, index) => + String.fromCharCode(view.getUint8(offset + index)), + ).join(""); +} + +function createSamples(values: number[]): Float32Array<ArrayBuffer> { + const samples = new Float32Array(new ArrayBuffer(values.length * 4)); + samples.set(values); + return samples; +} |
