summaryrefslogtreecommitdiff
path: root/apps/web/src/voiceover/__tests__/enhancements.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/voiceover/__tests__/enhancements.test.ts')
-rw-r--r--apps/web/src/voiceover/__tests__/enhancements.test.ts133
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;
+}