diff --git a/.oxlint/rules/enforceFileSuffix.mjs b/.oxlint/rules/enforceFileSuffix.mjs index cccc17ddf0..a604febae8 100644 --- a/.oxlint/rules/enforceFileSuffix.mjs +++ b/.oxlint/rules/enforceFileSuffix.mjs @@ -209,6 +209,7 @@ const STD_PACKAGES = new Set([ '@react-types/shared', '@signalapp/minimask', '@signalapp/quill-cjs', + '@signalapp/lame', '@typescript-eslint/eslint-plugin', '@typescript-eslint/parser', 'axe-core', diff --git a/packages/lame/README.md b/packages/lame/README.md index 1eba2e89c2..04ad9b00c0 100644 --- a/packages/lame/README.md +++ b/packages/lame/README.md @@ -22,8 +22,9 @@ cd - emcc -I lame-3.100/include -Oz -DNDEBUG -flto wrapper.c \ ./lame-3.100/libmp3lame/.libs/libmp3lame.a -o wrapper.mjs \ -sEXPORTED_FUNCTIONS=_wrapper_init,_wrapper_get_num_samples,_wrapper_get_in,_wrapper_get_out,_wrapper_encode,_wrapper_flush,_wrapper_get_lametag_frame,_wrapper_close \ - -sEXPORTED_RUNTIME_METHODS=HEAPU8 \ + -sEXPORTED_RUNTIME_METHODS=HEAPU8 -sDYNAMIC_EXECUTION=0 \ -sENVIRONMENT=worklet -sWASM=0 -sWASM_ASYNC_COMPILATION=0 +sed -I '' 's/^async //' wrapper.mjs ``` ## License diff --git a/packages/lame/index.mjs b/packages/lame/index.mjs index 5d383eff92..c573dc975e 100644 --- a/packages/lame/index.mjs +++ b/packages/lame/index.mjs @@ -12,7 +12,7 @@ const { _wrapper_encode, _wrapper_get_lametag_frame, _wrapper_flush, -} = await initWrapper(); +} = initWrapper(); const input = new Float32Array( HEAPU8.buffer, diff --git a/packages/lame/wrapper.mjs b/packages/lame/wrapper.mjs index 65cd8c6748..957546183f 100644 --- a/packages/lame/wrapper.mjs +++ b/packages/lame/wrapper.mjs @@ -1,4 +1,4 @@ -async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_AUDIO_WORKLET=!!globalThis.AudioWorkletGlobalScope;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var WebAssembly={Memory:function(opts){this.buffer=new ArrayBuffer(opts["initial"]*65536)},Module:function(binary){},Instance:function(module,info){this.exports=( +function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_AUDIO_WORKLET=!!globalThis.AudioWorkletGlobalScope;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var WebAssembly={Memory:function(opts){this.buffer=new ArrayBuffer(opts["initial"]*65536)},Module:function(binary){},Instance:function(module,info){this.exports=( // EMSCRIPTEN_START_ASM function instantiate(ma){var a;var b=new Uint8Array(123);for(var c=25;c>=0;--c){b[48+c]=52+c;b[65+c]=c;b[97+c]=26+c}b[43]=62;b[47]=63;function i(j,k,l){var d,e,c=0,f=k,g=l.length,h=k+(g*3>>2)-(l[g-2]=="=")-(l[g-1]=="=");for(;c>4;if(f>2;if(f>>0;C=C>>>0;if(A+C>a.length)throw"trap: invalid memory.fill";a.fill(v,A,A+C)}function la(n){var G=new ArrayBuffer(16973824);var H=new Int8Array(G);var I=new Int16Array(G);var J=new Int32Array(G);var K=new Uint8Array(G);var L=new Uint16Array(G);var M=new Uint32Array(G);var N=new Float32Array(G);var O=new Float64Array(G);var P=Math.imul;var Q=Math.fround;var R=Math.abs;var S=Math.clz32;var T=Math.min;var U=Math.max;var V=Math.floor;var W=Math.ceil;var X=Math.trunc;var Y=Math.sqrt;var Z=n.a;var _=Z.a;var $=Z.b;var aa=Z.c;var ba=Z.d;var ca=Z.e;var da=Z.f;var ea=Z.g;var fa=Z.h;var ga=Z.i;var ha=173200;var ia=0; // EMSCRIPTEN_START_FUNCS diff --git a/ts/services/audioRecorder.dom.ts b/ts/services/audioRecorder.dom.ts index cf5301ce2a..e38b9e6d27 100644 --- a/ts/services/audioRecorder.dom.ts +++ b/ts/services/audioRecorder.dom.ts @@ -77,10 +77,15 @@ export class AudioRecorder { } if (data.type === 'complete') { this.#state = { type: 'idle' }; + chunks.push(data.finalFrame); + + const result = Bytes.concatenate(chunks); + // Replace the original placeholder header with the one that has // full audio duration (necessary for VBR encoding). - chunks[0] = data.lametagFrame; - resolve(Bytes.concatenate(chunks)); + result.set(data.lametagFrame); + + resolve(result); return; } }; @@ -88,7 +93,6 @@ export class AudioRecorder { const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: { ideal: 1 }, - autoGainControl: { ideal: false }, }, }); diff --git a/ts/types/AudioRecorder.std.ts b/ts/types/AudioRecorder.std.ts index ab1f6a59a9..6cd32e8bf8 100644 --- a/ts/types/AudioRecorder.std.ts +++ b/ts/types/AudioRecorder.std.ts @@ -21,6 +21,7 @@ export type WorkletMessageType = Readonly< | { type: 'complete'; lametagFrame: Uint8Array; + finalFrame: Uint8Array; } >; diff --git a/ts/workers/mp3Encoder.std.ts b/ts/workers/mp3Encoder.std.ts index 6232b5b4f4..ff91c87000 100644 --- a/ts/workers/mp3Encoder.std.ts +++ b/ts/workers/mp3Encoder.std.ts @@ -5,6 +5,7 @@ import type { WorkletMessageType, RendererMessageType, } from '../types/AudioRecorder.std.ts'; +import { init, encode, flush, getLametagFrame } from '@signalapp/lame'; declare const sampleRate: number; @@ -33,16 +34,7 @@ declare function registerProcessor( const BIT_RATE = 128; -// Unfortunately `context.audioWorklet.addModule` doesn't wait for top-level -// awaits to be resolved so we have to call `registerProcessor` immediately -// and let the import resolve later on. -const lame = (async () => { - const result = await import('@signalapp/lame'); - - result.init(sampleRate, BIT_RATE); - - return result; -})(); +init(sampleRate, BIT_RATE); class Mp3Encoder extends AudioWorkletProcessor @@ -53,21 +45,22 @@ class Mp3Encoder constructor() { super(); - this.port.onmessage = async ({ data }: { data: RendererMessageType }) => { + this.port.onmessage = ({ data }: { data: RendererMessageType }) => { if (data.type !== 'stop') { throw new Error('Unexpected message'); } this.#isStopped = true; - const { flush, getLametagFrame } = await lame; - this.#sendChunk(flush()); + + const chunk = new Uint8Array(flush()); const lametagFrame = new Uint8Array(getLametagFrame()); this.port.postMessage( { type: 'complete', lametagFrame: lametagFrame, + finalFrame: chunk, } satisfies WorkletMessageType, - [lametagFrame.buffer] + [lametagFrame.buffer, chunk.buffer] ); }; } @@ -83,19 +76,16 @@ class Mp3Encoder } const [channel] = input; - if (channel != null) { - void this.#encode(channel); + if (channel == null) { + return true; } - return true; - } - async #encode(channel: Float32Array): Promise { - const { encode } = await lame; - this.#sendChunk(encode(channel)); - } + const shared = encode(channel); + if (shared.length === 0) { + return true; + } - #sendChunk(chunk: Uint8Array): void { - const copy = new Uint8Array(chunk); + const copy = new Uint8Array(shared); this.port.postMessage( { type: 'chunk', @@ -103,6 +93,7 @@ class Mp3Encoder } satisfies WorkletMessageType, [copy.buffer] ); + return true; } }