1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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;
}
|