fix: normalize ambience loudness for v0.2.5
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user