mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-20 14:49:11 +01:00
147 lines
3.3 KiB
TypeScript
147 lines
3.3 KiB
TypeScript
// Copyright 2026 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type {
|
|
WorkletMessageType,
|
|
RendererMessageType,
|
|
} from '../types/AudioRecorder.std.ts';
|
|
import { Encoder } from '@signalapp/lame';
|
|
|
|
declare const sampleRate: number;
|
|
|
|
type AudioWorkletProcessor = Readonly<{
|
|
port: MessagePort;
|
|
}>;
|
|
|
|
declare const AudioWorkletProcessor: {
|
|
prototype: AudioWorkletProcessor;
|
|
new (): AudioWorkletProcessor;
|
|
};
|
|
|
|
type AudioWorkletProcessorImpl = Readonly<{
|
|
process(inputs: Array<Array<Float32Array<ArrayBuffer>>>): boolean;
|
|
}> &
|
|
AudioWorkletProcessor;
|
|
|
|
type AudioWorkletProcessorConstructor = Readonly<{
|
|
new (): AudioWorkletProcessorImpl;
|
|
}>;
|
|
|
|
declare function registerProcessor(
|
|
name: string,
|
|
processorCtor: AudioWorkletProcessorConstructor
|
|
): void;
|
|
|
|
const BIT_RATE = 90;
|
|
const Q = 7;
|
|
|
|
// Produce a peak value every 100 milliseconds
|
|
const PEAK_EVERY_S = 0.1;
|
|
const PEAK_EVERY = Math.round(sampleRate * PEAK_EVERY_S);
|
|
|
|
// Keep maximum peak over last 5 seconds
|
|
const WINDOW_SIZE = Math.round(5 / PEAK_EVERY_S);
|
|
|
|
class Mp3Encoder
|
|
extends AudioWorkletProcessor
|
|
implements AudioWorkletProcessorImpl
|
|
{
|
|
readonly #encoder = new Encoder({
|
|
q: Q,
|
|
sampleRate,
|
|
bitRate: BIT_RATE,
|
|
});
|
|
#isStopped = false;
|
|
#peakSquares = 0;
|
|
#peakSamples = 0;
|
|
#window = new Array<number>();
|
|
#windowOffset = 0;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.port.onmessage = ({ data }: { data: RendererMessageType }) => {
|
|
if (data.type !== 'stop') {
|
|
throw new Error('Unexpected message');
|
|
}
|
|
if (this.#isStopped) {
|
|
throw new Error('Already stopped');
|
|
}
|
|
this.#isStopped = true;
|
|
|
|
const chunk = new Uint8Array(this.#encoder.flush());
|
|
|
|
const lametagFrame = new Uint8Array(this.#encoder.getLametagFrame());
|
|
this.port.postMessage(
|
|
{
|
|
type: 'complete',
|
|
lametagFrame: lametagFrame,
|
|
finalFrame: chunk,
|
|
} satisfies WorkletMessageType,
|
|
[lametagFrame.buffer, chunk.buffer]
|
|
);
|
|
};
|
|
}
|
|
|
|
process(inputs: Array<Array<Float32Array<ArrayBuffer>>>): boolean {
|
|
if (this.#isStopped) {
|
|
return false;
|
|
}
|
|
|
|
const [input] = inputs;
|
|
if (input == null) {
|
|
throw new Error(`Invalid input count: ${inputs.length}`);
|
|
}
|
|
|
|
const [channel] = input;
|
|
if (channel == null) {
|
|
return true;
|
|
}
|
|
|
|
for (const sample of channel) {
|
|
this.#peakSquares += sample ** 2;
|
|
this.#peakSamples += 1;
|
|
if (this.#peakSamples < PEAK_EVERY) {
|
|
continue;
|
|
}
|
|
|
|
const peak = Math.min(
|
|
1,
|
|
Math.max(0, Math.sqrt(this.#peakSquares / this.#peakSamples))
|
|
);
|
|
this.#window[this.#windowOffset] = peak;
|
|
this.#windowOffset = (this.#windowOffset + 1) % WINDOW_SIZE;
|
|
|
|
let max = 1e-23;
|
|
for (const oldPeak of this.#window) {
|
|
max = Math.max(max, oldPeak);
|
|
}
|
|
|
|
this.#peakSquares = 0;
|
|
this.#peakSamples = 0;
|
|
|
|
this.port.postMessage({
|
|
type: 'peak',
|
|
peak: peak / max,
|
|
} satisfies WorkletMessageType);
|
|
}
|
|
|
|
const shared = this.#encoder.encode(channel);
|
|
if (shared.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const copy = new Uint8Array(shared);
|
|
this.port.postMessage(
|
|
{
|
|
type: 'chunk',
|
|
chunk: copy,
|
|
} satisfies WorkletMessageType,
|
|
[copy.buffer]
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor('mp3-encoder', Mp3Encoder);
|