Files
Desktop/ts/workers/mp3Encoder.std.ts
T
2026-04-30 16:30:26 -07:00

101 lines
2.2 KiB
TypeScript

// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type {
WorkletMessageType,
RendererMessageType,
} from '../types/AudioRecorder.std.ts';
import { init, encode, flush, getLametagFrame } 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 = 128;
init(sampleRate, BIT_RATE);
class Mp3Encoder
extends AudioWorkletProcessor
implements AudioWorkletProcessorImpl
{
#isStopped = false;
constructor() {
super();
this.port.onmessage = ({ data }: { data: RendererMessageType }) => {
if (data.type !== 'stop') {
throw new Error('Unexpected message');
}
this.#isStopped = true;
const chunk = new Uint8Array(flush());
const lametagFrame = new Uint8Array(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;
}
const shared = 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);