102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
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",
|
|
});
|
|
});
|