Files

102 lines
3.2 KiB
TypeScript
Raw Permalink Normal View History

2026-10-14 12:12:13 +00:00
import { readFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
import { PEAK_BOUNDS } from "../../src/audio/dsp";
// 장치 rate 변경에 따른 intersample peak 증가를 실제 Chrome 디코더로 전수 확인한다.
// gain 안전성의 입력 상한을 검증하며 Web Audio나 오디오 데이터는 mock하지 않는다.
test("109 WAV의 24·44.1·48·96kHz 디코드는 고정 peak 상한 안에 있다", async ({
page,
browser,
}, info) => {
const entries = ["audio", "ambience"].flatMap((directory) =>
JSON.parse(
readFileSync(`public/${directory}/manifest.json`, "utf8"),
).files.map((entry: { file: string }) => ({
url: `/${directory}/${entry.file}?v=0.2.5`,
kind:
directory === "ambience"
? "background"
: entry.file === "intro.wav"
? "intro"
: "voice",
})),
);
const errors: string[] = [],
failed: string[] = [],
external: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
page.on("console", (m) => {
if (m.type() === "error") errors.push(m.text());
});
page.on("requestfailed", (r) => failed.push(r.url()));
page.on("request", (r) => {
if (new URL(r.url()).origin !== "http://127.0.0.1:4173")
external.push(r.url());
});
await page.goto("/");
const rows = await page.evaluate(
async ({ entries, bounds }) => {
const contexts = [24000, 44100, 48000, 96000].map(
(rate) => new OfflineAudioContext(1, 1, rate),
);
const rows = [];
for (const entry of entries) {
const response = await fetch(entry.url);
if (!response.ok) throw new Error("WAV fetch failed");
const bytes = await response.arrayBuffer();
for (const context of contexts) {
const buffer = await context.decodeAudioData(bytes.slice(0));
const samples = buffer.getChannelData(0);
const rms = Math.sqrt(
samples.reduce((sum, x) => sum + x * x, 0) / samples.length,
);
const peak = samples.reduce(
(max, x) => Math.max(max, Math.abs(x)),
0,
);
// 배경은 저장 진폭 그대로, voice/intro만 엔진의 RMS .03 정규화를 반영한다.
const normalizedPeak =
entry.kind === "background" ? peak : (peak * 0.03) / rms;
if (
!Number.isFinite(normalizedPeak) ||
normalizedPeak > bounds[entry.kind as keyof typeof bounds]
)
throw new Error(
`${entry.url}: ${context.sampleRate} Hz peak out of bounds`,
);
rows.push({
...entry,
rate: context.sampleRate,
rms,
peak,
normalizedPeak,
});
}
}
return rows;
},
{ entries, bounds: PEAK_BOUNDS },
);
expect(rows).toHaveLength(109 * 4);
expect({ errors, failed, external }).toEqual({
errors: [],
failed: [],
external: [],
});
await info.attach("all-voices-resampled-bounds", {
body: JSON.stringify(
{
chrome: browser.version(),
bounds: PEAK_BOUNDS,
rows,
errors,
failed,
external,
},
null,
2,
),
contentType: "application/json",
});
});