TelegramPWA/src/util/voiceRecording.ts
2025-07-25 19:34:49 +02:00

111 lines
2.9 KiB
TypeScript

import type { IOpusRecorder } from 'opus-recorder';
import { requestMeasure } from '../lib/fasterdom/fasterdom';
export type Result = { blob: Blob; duration: number; waveform: number[] };
const encoderPath = new URL('opus-recorder/dist/encoderWorker.min', import.meta.url).href;
const MIN_RECORDING_TIME = 1000;
const POLYFILL_OPTIONS = { encoderPath, reuseWorker: true };
const BLOB_PARAMS = { type: 'audio/ogg' };
const FFT_SIZE = 64;
const MIN_VOLUME = 0.1;
let opusRecorderPromise: Promise<{ default: IOpusRecorder }>;
let OpusRecorder: IOpusRecorder;
let mediaRecorder: IOpusRecorder;
export async function init() {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!opusRecorderPromise) {
opusRecorderPromise = import('opus-recorder');
OpusRecorder = (await opusRecorderPromise).default;
mediaRecorder = new OpusRecorder(POLYFILL_OPTIONS);
}
return opusRecorderPromise;
}
export async function start(analyzerCallback: (volume: number) => void) {
await startMediaRecorder();
const startedAt = Date.now();
let pausedAt: number;
const chunks: Uint8Array[] = [];
const waveform: number[] = [];
mediaRecorder.ondataavailable = (typedArray) => {
chunks.push(typedArray);
};
const releaseAnalyzer = subscribeToAnalyzer(mediaRecorder, (volume: number) => {
waveform.push(volume * 255);
analyzerCallback(volume);
});
return {
stop: () => new Promise<Result>((resolve, reject) => {
mediaRecorder.onstop = () => {
resolve({
blob: new Blob(chunks, BLOB_PARAMS),
duration: Math.round(((pausedAt || Date.now()) - startedAt) / 1000),
waveform,
});
};
mediaRecorder.onerror = reject;
const delayStop = Math.max(0, startedAt + MIN_RECORDING_TIME - Date.now());
setTimeout(() => {
mediaRecorder.stop();
releaseAnalyzer();
}, delayStop);
}),
pause: () => {
const delayStop = Math.max(0, startedAt + MIN_RECORDING_TIME - Date.now());
setTimeout(() => {
mediaRecorder.pause();
pausedAt = Date.now();
releaseAnalyzer();
}, delayStop);
},
};
}
async function startMediaRecorder() {
await init();
await mediaRecorder.start();
}
function subscribeToAnalyzer(recorder: IOpusRecorder, cb: (volume: number) => void) {
const source = recorder.sourceNode;
const analyser = source.context.createAnalyser();
analyser.fftSize = FFT_SIZE;
source.connect(analyser);
const dataLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(dataLength);
let isDestroyed = false;
function tick() {
if (isDestroyed) {
return;
}
analyser.getByteFrequencyData(dataArray);
const sum = dataArray.reduce((acc, current) => acc + current, 0);
const mean = (sum / dataLength);
const volume = mean / 255;
cb(volume < MIN_VOLUME ? 0 : volume);
requestMeasure(tick);
}
tick();
return () => {
isDestroyed = true;
};
}