45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
export const NUM_BARS = 150;
|
|
export const BAR_W = 3;
|
|
export const BAR_GAP = 1;
|
|
export const WAVEFORM_H = 48;
|
|
export const VIEWBOX_W = NUM_BARS * (BAR_W + BAR_GAP); // 600
|
|
|
|
// Module-level cache: survives StrictMode double-effect and re-renders
|
|
const peaksCache = new Map<string, Float32Array>();
|
|
|
|
export async function extractPeaks(
|
|
url: string,
|
|
n: number,
|
|
): Promise<Float32Array> {
|
|
if (peaksCache.has(url)) return peaksCache.get(url)!;
|
|
|
|
// /api/files is public (no auth middleware). Plain fetch avoids the CORS
|
|
// preflight that credentials/Authorization would trigger — the server's
|
|
// oakCors config doesn't set Allow-Credentials so credentialed fetches
|
|
// are blocked, while <audio> bypasses CORS entirely.
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`fetch failed: ${res.status}`);
|
|
const buf = await res.arrayBuffer();
|
|
|
|
const actx = new AudioContext();
|
|
try {
|
|
const decoded = await actx.decodeAudioData(buf);
|
|
const data = decoded.getChannelData(0);
|
|
const blockSize = Math.floor(data.length / n);
|
|
const peaks = new Float32Array(n);
|
|
for (let i = 0; i < n; i++) {
|
|
let peak = 0;
|
|
const offset = i * blockSize;
|
|
for (let j = 0; j < blockSize; j++) {
|
|
const abs = Math.abs(data[offset + j]);
|
|
if (abs > peak) peak = abs;
|
|
}
|
|
peaks[i] = peak;
|
|
}
|
|
peaksCache.set(url, peaks);
|
|
return peaks;
|
|
} finally {
|
|
actx.close();
|
|
}
|
|
}
|