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
|
const MASTER_LIMITER_THRESHOLD_DB = -1;
const MASTER_LIMITER_KNEE_DB = 0;
const MASTER_LIMITER_RATIO = 20;
const MASTER_LIMITER_ATTACK_SECONDS = 0.001;
const MASTER_LIMITER_RELEASE_SECONDS = 0.12;
const MASTER_OUTPUT_HEADROOM = 0.98;
export function getAudioBufferPeak({
audioBuffer,
}: {
audioBuffer: AudioBuffer;
}): number {
let peak = 0;
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
const channelData = audioBuffer.getChannelData(channel);
for (let index = 0; index < channelData.length; index++) {
const magnitude = Math.abs(channelData[index]);
if (magnitude > peak) {
peak = magnitude;
}
}
}
return peak;
}
export function createAudioMasteringChain({
audioContext,
destination,
}: {
audioContext: AudioContext | OfflineAudioContext;
destination: AudioNode;
}): {
input: GainNode;
} {
const input = audioContext.createGain();
const limiter = audioContext.createDynamicsCompressor();
const outputGain = audioContext.createGain();
limiter.threshold.value = MASTER_LIMITER_THRESHOLD_DB;
limiter.knee.value = MASTER_LIMITER_KNEE_DB;
limiter.ratio.value = MASTER_LIMITER_RATIO;
limiter.attack.value = MASTER_LIMITER_ATTACK_SECONDS;
limiter.release.value = MASTER_LIMITER_RELEASE_SECONDS;
outputGain.gain.value = MASTER_OUTPUT_HEADROOM;
input.connect(limiter);
limiter.connect(outputGain);
outputGain.connect(destination);
return { input };
}
export async function applyAudioMasteringToBuffer({
audioBuffer,
}: {
audioBuffer: AudioBuffer;
}): Promise<AudioBuffer> {
if (getAudioBufferPeak({ audioBuffer }) <= MASTER_OUTPUT_HEADROOM) {
return audioBuffer;
}
const offlineContext = new OfflineAudioContext(
audioBuffer.numberOfChannels,
Math.max(1, audioBuffer.length),
audioBuffer.sampleRate,
);
const source = offlineContext.createBufferSource();
source.buffer = audioBuffer;
const { input } = createAudioMasteringChain({
audioContext: offlineContext,
destination: offlineContext.destination,
});
source.connect(input);
source.start(0);
const renderedBuffer = await offlineContext.startRendering();
clampAudioBufferPeak({
audioBuffer: renderedBuffer,
maxPeak: MASTER_OUTPUT_HEADROOM,
});
return renderedBuffer;
}
function clampAudioBufferPeak({
audioBuffer,
maxPeak,
}: {
audioBuffer: AudioBuffer;
maxPeak: number;
}): void {
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
const channelData = audioBuffer.getChannelData(channel);
for (let index = 0; index < channelData.length; index++) {
channelData[index] = Math.max(
-maxPeak,
Math.min(maxPeak, channelData[index]),
);
}
}
}
|