import { createUser, get, insertDump, test, TEST_DIR } from "../helpers.ts"; import { assert, assertEquals } from "@std/assert"; import { WAVEFORM_PEAKS } from "../../../shared/waveform.ts"; async function hasFfmpeg(): Promise { try { return (await new Deno.Command("ffmpeg", { args: ["-version"], stdout: "null", stderr: "null", }).output()).success; } catch { return false; } } /** * Returns a mono 16-bit WAV of a 440 Hz tone, quiet for its first `seconds` * and four times louder for as long again. */ function wav(seconds: number, rate = 8000): Uint8Array { const n = seconds * rate * 2; const bytes = new Uint8Array(44 + n * 2); const view = new DataView(bytes.buffer); const ascii = (at: number, s: string) => [...s].forEach((c, i) => view.setUint8(at + i, c.charCodeAt(0))); ascii(0, "RIFF"); view.setUint32(4, 36 + n * 2, true); ascii(8, "WAVEfmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); // PCM view.setUint16(22, 1, true); // mono view.setUint32(24, rate, true); view.setUint32(28, rate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); ascii(36, "data"); view.setUint32(40, n * 2, true); for (let i = 0; i < n; i++) { const gain = i < n / 2 ? 4000 : 16000; view.setInt16( 44 + i * 2, gain * Math.sin((2 * Math.PI * 440 * i) / rate), true, ); } return bytes; } test("GET /api/dumps/:id/waveform: an audio file's peaks, made once, with privacy", async () => { if (!await hasFfmpeg()) return; const owner = await createUser(); const song = insertDump({ userId: owner.id, fileName: "s.wav", fileMime: "audio/wav", fileData: wav(2), }); const res = await get<{ peaks: number[] }>(`/api/dumps/${song.id}/waveform`); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "application/json"); assertEquals(res.headers.get("cache-control"), "public, no-cache"); const { peaks } = res.data; assertEquals(peaks.length, WAVEFORM_PEAKS); // The loud half reaches the top; the quiet half a quarter of it. assertEquals(Math.max(...peaks), 1); assert(Math.abs(peaks[10] - 0.25) < 0.02, `quiet peak ${peaks[10]}`); assert(peaks[140] > 0.98, `loud peak ${peaks[140]}`); assert( [...Deno.readDirSync(`${TEST_DIR}/uploads/thumbnails`)].some((e) => e.name === `${song.id}.waveform.json` ), ); // The kept file is served again, with its ETag. const etag = res.headers.get("etag")!; const again = await get(`/api/dumps/${song.id}/waveform`, { headers: { "If-None-Match": etag }, }); assertEquals(again.status, 304); const secret = insertDump({ userId: owner.id, fileName: "p.wav", fileMime: "audio/wav", fileData: wav(1), isPrivate: true, }); assertEquals((await get(`/api/dumps/${secret.id}/waveform`)).status, 404); assertEquals( (await get(`/api/dumps/${secret.id}/waveform?token=${owner.mediaToken}`)) .status, 200, ); }); test("GET /api/dumps/:id/waveform: 404 for anything but readable audio", async () => { const owner = await createUser(); const text = insertDump({ userId: owner.id, fileName: "t.txt" }); assertEquals((await get(`/api/dumps/${text.id}/waveform`)).status, 404); const noise = insertDump({ userId: owner.id, fileName: "n.mp3", fileMime: "audio/mpeg", fileData: new TextEncoder().encode("not audio at all"), }); assertEquals((await get(`/api/dumps/${noise.id}/waveform`)).status, 404); assertEquals((await get(`/api/dumps/nope/waveform`)).status, 400); });