summaryrefslogtreecommitdiff
path: root/apps/web/src/media/audio-mastering.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/media/audio-mastering.ts')
-rw-r--r--apps/web/src/media/audio-mastering.ts103
1 files changed, 103 insertions, 0 deletions
diff --git a/apps/web/src/media/audio-mastering.ts b/apps/web/src/media/audio-mastering.ts
new file mode 100644
index 00000000..f901c3bf
--- /dev/null
+++ b/apps/web/src/media/audio-mastering.ts
@@ -0,0 +1,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]),
+ );
+ }
+ }
+}