Do not compute waveform for a long audio
This commit is contained in:
parent
44ecfe4746
commit
b1d49f7b3e
3 changed files with 64 additions and 6 deletions
BIN
fixtures/long-audio.mp3
Normal file
BIN
fixtures/long-audio.mp3
Normal file
Binary file not shown.
|
@ -9,6 +9,7 @@ import { WaveformCache } from '../types/Audio';
|
||||||
|
|
||||||
const MAX_WAVEFORM_COUNT = 1000;
|
const MAX_WAVEFORM_COUNT = 1000;
|
||||||
const MAX_PARALLEL_COMPUTE = 8;
|
const MAX_PARALLEL_COMPUTE = 8;
|
||||||
|
const MAX_AUDIO_DURATION = 15 * 60; // 15 minutes
|
||||||
|
|
||||||
export type ComputePeaksResult = {
|
export type ComputePeaksResult = {
|
||||||
duration: number;
|
duration: number;
|
||||||
|
@ -35,6 +36,36 @@ const computeQueue = new PQueue({
|
||||||
concurrency: MAX_PARALLEL_COMPUTE,
|
concurrency: MAX_PARALLEL_COMPUTE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function getAudioDuration(
|
||||||
|
url: string,
|
||||||
|
buffer: ArrayBuffer
|
||||||
|
): Promise<number> {
|
||||||
|
const blob = new Blob([buffer]);
|
||||||
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const audio = new Audio();
|
||||||
|
audio.muted = true;
|
||||||
|
audio.src = blobURL;
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', event => {
|
||||||
|
const error = new Error(
|
||||||
|
`Failed to load audio from: ${url} due to error: ${event.type}`
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Number.isNaN(audio.duration)) {
|
||||||
|
throw new Error(`Invalid audio duration for: ${url}`);
|
||||||
|
}
|
||||||
|
return audio.duration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load audio from `url`, decode PCM data, and compute RMS peaks for displaying
|
* Load audio from `url`, decode PCM data, and compute RMS peaks for displaying
|
||||||
* the waveform.
|
* the waveform.
|
||||||
|
@ -61,10 +92,21 @@ async function doComputePeaks(
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const raw = await response.arrayBuffer();
|
const raw = await response.arrayBuffer();
|
||||||
|
|
||||||
|
const duration = await getAudioDuration(url, raw);
|
||||||
|
|
||||||
|
const peaks = new Array(barCount).fill(0);
|
||||||
|
if (duration > MAX_AUDIO_DURATION) {
|
||||||
|
window.log.info(
|
||||||
|
`GlobalAudioContext: audio ${url} duration ${duration}s is too long`
|
||||||
|
);
|
||||||
|
const emptyResult = { peaks, duration };
|
||||||
|
waveformCache.set(url, emptyResult);
|
||||||
|
return emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await audioContext.decodeAudioData(raw);
|
const data = await audioContext.decodeAudioData(raw);
|
||||||
|
|
||||||
// Compute RMS peaks
|
// Compute RMS peaks
|
||||||
const peaks = new Array(barCount).fill(0);
|
|
||||||
const norms = new Array(barCount).fill(0);
|
const norms = new Array(barCount).fill(0);
|
||||||
|
|
||||||
const samplesPerPeak = data.length / peaks.length;
|
const samplesPerPeak = data.length / peaks.length;
|
||||||
|
@ -94,7 +136,7 @@ async function doComputePeaks(
|
||||||
peaks[i] /= max;
|
peaks[i] /= max;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = { peaks, duration: data.duration };
|
const result = { peaks, duration };
|
||||||
waveformCache.set(url, result);
|
waveformCache.set(url, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -118,10 +160,11 @@ export async function computePeaks(
|
||||||
const promise = computeQueue.add(() => doComputePeaks(url, barCount));
|
const promise = computeQueue.add(() => doComputePeaks(url, barCount));
|
||||||
|
|
||||||
inProgressMap.set(computeKey, promise);
|
inProgressMap.set(computeKey, promise);
|
||||||
const result = await promise;
|
try {
|
||||||
|
return await promise;
|
||||||
|
} finally {
|
||||||
inProgressMap.delete(computeKey);
|
inProgressMap.delete(computeKey);
|
||||||
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalContents: Contents = {
|
const globalContents: Contents = {
|
||||||
|
|
|
@ -804,6 +804,21 @@ story.add('Audio', () => {
|
||||||
return renderBothDirections(props);
|
return renderBothDirections(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Long Audio', () => {
|
||||||
|
const props = createProps({
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: AUDIO_MP3,
|
||||||
|
fileName: 'long-audio.mp3',
|
||||||
|
url: '/fixtures/long-audio.mp3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 'sent',
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderBothDirections(props);
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Audio with Caption', () => {
|
story.add('Audio with Caption', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
attachments: [
|
attachments: [
|
||||||
|
|
Loading…
Add table
Reference in a new issue