feat: build reproducible dataset editor

This commit is contained in:
Codex
2026-08-28 10:46:33 +09:00
parent 6a1197d60e
commit 08fcf578de
16 changed files with 2579 additions and 11 deletions
+194
View File
@@ -0,0 +1,194 @@
import { encodeMonoPcm16Wav } from "/packages/acoustics-core/wav.js";
function assertNear(actual, expected, tolerance, label) {
assert(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected}, received ${actual}`);
}
async function waitFor(predicate, label, timeoutMs = 15000) {
const deadline = performance.now() + timeoutMs;
while (performance.now() < deadline) {
const value = predicate();
if (value) return value;
await new Promise((resolve) => setTimeout(resolve, 40));
}
throw new Error(`${label} 대기 시간이 초과됐습니다.`);
}
function dispatchValue(windowObject, element, value, eventName = "input") {
element.value = String(value);
element.dispatchEvent(new windowObject.Event(eventName, { bubbles: true }));
}
function dispatchChecked(windowObject, element, checked) {
element.checked = checked;
element.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
}
function dispatchFiles(windowObject, input, files) {
const transfer = new windowObject.DataTransfer();
files.forEach((file) => transfer.items.add(file));
input.files = transfer.files;
input.dispatchEvent(new windowObject.Event("change", { bubbles: true }));
}
async function digestHex(blob) {
const digest = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer());
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
}
test("Dataset Builder 전체 저장·재연결 흐름이 동일한 scene과 preview WAV를 재현한다", async () => {
const frame = document.createElement("iframe");
frame.src = "/apps/dataset-builder/?integration-test=1";
frame.style.cssText = "position:fixed;left:-20000px;top:0;width:1400px;height:1000px;border:0";
document.body.append(frame);
await new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
frame.addEventListener("error", () => reject(new Error("Builder iframe load failed")), { once: true });
});
const windowObject = frame.contentWindow;
const documentObject = frame.contentDocument;
const objectUrls = [];
const downloads = [];
const originalRevoke = windowObject.URL.revokeObjectURL.bind(windowObject.URL);
const originalAnchorClick = windowObject.HTMLAnchorElement.prototype.click;
windowObject.URL.revokeObjectURL = (url) => objectUrls.push(url);
windowObject.HTMLAnchorElement.prototype.click = function captureDownload() {
downloads.push({ fileName: this.download, url: this.href });
};
try {
await waitFor(() => documentObject.documentElement.dataset.appReady === "true", "Builder 초기화");
await waitFor(() => documentObject.documentElement.dataset.sceneValid === "true", "기본 scene 검증");
assert(documentObject.querySelector("#scene-id").value !== "single-tone", "new default scene gets a unique id");
const previewSamples = new Float32Array(2000);
for (let index = 0; index < previewSamples.length; index += 1) {
previewSamples[index] = Math.sin((2 * Math.PI * 440 * index) / 8000) * 0.12;
}
const wavBlob = encodeMonoPcm16Wav(previewSamples, 8000);
const audioFile = new windowObject.File([wavBlob], "machine.wav", { type: "audio/wav" });
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480"><rect width="640" height="480" fill="#173128"/><circle cx="420" cy="170" r="70" fill="#65f2bf"/></svg>';
const imageFile = new windowObject.File([svg], "machine.svg", { type: "image/svg+xml" });
const staleSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240"><rect width="320" height="240" fill="#ff0000"/></svg>';
const staleImageFile = new windowObject.File([staleSvg], "stale.svg", { type: "image/svg+xml" });
dispatchFiles(windowObject, documentObject.querySelector("#audio-input"), [audioFile]);
await waitFor(
() => [...documentObject.querySelectorAll(".asset-chip__state")].some((item) => item.textContent === "연결됨"),
"오디오 hash/decode",
);
dispatchFiles(windowObject, documentObject.querySelector("#image-input"), [staleImageFile]);
dispatchFiles(windowObject, documentObject.querySelector("#image-input"), [imageFile]);
await waitFor(() => documentObject.querySelectorAll(".asset-chip__state").length === 2, "이미지 decode");
await waitFor(() => documentObject.documentElement.dataset.assetsReady === "true", "asset 준비");
documentObject.querySelector("#add-source-button").click();
assertEqual(documentObject.querySelectorAll(".source-card").length, 2, "source card count");
dispatchValue(windowObject, documentObject.querySelector("#source-name"), "asset source");
dispatchValue(windowObject, documentObject.querySelector("#source-type"), "asset", "change");
dispatchChecked(windowObject, documentObject.querySelector("#source-loop"), true);
const canvas = documentObject.querySelector("#scene-canvas");
const canvasRect = canvas.getBoundingClientRect();
canvas.dispatchEvent(new windowObject.PointerEvent("pointerdown", {
bubbles: true,
clientX: canvasRect.left + canvasRect.width * 0.75,
clientY: canvasRect.top + canvasRect.height * 0.25,
}));
assertNear(Number(documentObject.querySelector("#source-x").value), 0.3, 0.002, "canvas x");
assertNear(Number(documentObject.querySelector("#source-y").value), 0.225, 0.002, "canvas y");
dispatchValue(windowObject, documentObject.querySelector("#ambient-type"), "asset", "change");
dispatchChecked(windowObject, documentObject.querySelector("#ambient-loop"), true);
await waitFor(() => documentObject.documentElement.dataset.sceneValid === "true", "편집 scene 검증");
documentObject.querySelector("#wav-button").click();
const firstWavDownload = await waitFor(
() => downloads.find((download) => download.fileName.endsWith(".preview-mix.wav")),
"첫 preview WAV",
);
const firstWav = await fetch(firstWavDownload.url).then((response) => response.blob());
const firstWavHash = await digestHex(firstWav);
await waitFor(() => !documentObject.querySelector("#export-button").disabled, "첫 JSON 저장 버튼 복구");
documentObject.querySelector("#export-button").click();
const firstJsonDownload = await waitFor(
() => downloads.find((download) => download.fileName.endsWith(".scene.json")),
"첫 Scene JSON",
);
const firstJsonText = await fetch(firstJsonDownload.url).then((response) => response.text());
const firstScene = JSON.parse(firstJsonText);
assertEqual(firstScene.sources.length, 2, "exported source count");
assertEqual(firstScene.sources[1].signal.loop, true, "source loop exported");
assertEqual(firstScene.ambient[0].signal.loop, true, "ambient loop exported");
assertEqual(firstScene.image.widthPixels, 640, "image width exported");
assert(firstScene.assets.every((asset) => /^[a-f0-9]{64}$/.test(asset.sha256)), "asset hashes exported");
const sceneFile = new windowObject.File([firstJsonText], "roundtrip.scene.json", { type: "application/json" });
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [sceneFile]);
await waitFor(() => documentObject.querySelector("#save-state").textContent.startsWith("Scene 불러옴"), "Scene 재로드");
await waitFor(() => documentObject.documentElement.dataset.assetsReady === "false", "재로드 후 원본 파일 요구");
dispatchFiles(windowObject, documentObject.querySelector("#audio-input"), [audioFile]);
await waitFor(
() => [...documentObject.querySelectorAll(".asset-chip")].some((chip) =>
chip.querySelector(".asset-chip__label").textContent.includes("machine.wav") &&
chip.querySelector(".asset-chip__state").textContent === "연결됨"),
"오디오 SHA 재연결",
);
dispatchFiles(windowObject, documentObject.querySelector("#image-input"), [imageFile]);
await waitFor(() => documentObject.documentElement.dataset.assetsReady === "true", "이미지 SHA 재연결");
const downloadCountBeforeSecondWav = downloads.length;
documentObject.querySelector("#wav-button").click();
const secondWavDownload = await waitFor(
() => downloads.slice(downloadCountBeforeSecondWav).find((download) => download.fileName.endsWith(".preview-mix.wav")),
"두 번째 preview WAV",
);
const secondWav = await fetch(secondWavDownload.url).then((response) => response.blob());
assertEqual(await digestHex(secondWav), firstWavHash, "round-trip preview WAV hash");
await waitFor(() => !documentObject.querySelector("#export-button").disabled, "두 번째 JSON 저장 버튼 복구");
const downloadCountBeforeSecondJson = downloads.length;
documentObject.querySelector("#export-button").click();
const secondJsonDownload = await waitFor(
() => downloads.slice(downloadCountBeforeSecondJson).find((download) => download.fileName.endsWith(".scene.json")),
"두 번째 Scene JSON",
);
const secondScene = await fetch(secondJsonDownload.url).then((response) => response.json());
assertEqual(JSON.stringify(secondScene), JSON.stringify(firstScene), "canonical scene round trip");
documentObject.querySelector("#remove-image-button").click();
await waitFor(() => documentObject.querySelector("#remove-image-button").classList.contains("is-hidden"), "image remove");
await waitFor(() => !documentObject.querySelector("#export-button").disabled, "grid-only JSON button");
const downloadCountBeforeGridJson = downloads.length;
documentObject.querySelector("#export-button").click();
const gridJsonDownload = await waitFor(
() => downloads.slice(downloadCountBeforeGridJson).find((download) => download.fileName.endsWith(".scene.json")),
"grid-only Scene JSON",
);
const gridScene = await fetch(gridJsonDownload.url).then((response) => response.json());
assertEqual(gridScene.image, undefined, "image reference removed");
const stableName = documentObject.querySelector("#scene-name").value;
const invalidFile = new windowObject.File(["{"], "invalid.scene.json", { type: "application/json" });
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [invalidFile]);
await waitFor(() => documentObject.querySelector("#save-state").textContent.startsWith("불러오기 실패"), "invalid import error");
assertEqual(documentObject.querySelector("#scene-name").value, stableName, "invalid import preserves scene");
dispatchValue(windowObject, documentObject.querySelector("#sample-rate"), "96000", "change");
await waitFor(() => !documentObject.querySelector("#wav-button").disabled, "cancel fixture validation");
const downloadCountBeforeCancel = downloads.length;
documentObject.querySelector("#wav-button").click();
const cancelSceneFile = new windowObject.File([firstJsonText], "cancel.scene.json", { type: "application/json" });
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [cancelSceneFile]);
await waitFor(() => documentObject.querySelector("#save-state").textContent.startsWith("Scene 불러옴"), "import during decode");
await new Promise((resolve) => setTimeout(resolve, 250));
assertEqual(downloads.length, downloadCountBeforeCancel, "cancelled decode does not create stale download");
} finally {
windowObject.HTMLAnchorElement.prototype.click = originalAnchorClick;
windowObject.URL.revokeObjectURL = originalRevoke;
for (const url of objectUrls) originalRevoke(url);
frame.remove();
}
});
+2
View File
@@ -11,7 +11,9 @@ globalThis.assertEqual = (actual, expected, message = "values differ") => {
try {
await import("/tests/unit/contracts.test.js");
await import("/tests/unit/acoustics.test.js");
await import("/tests/unit/builder.test.js");
await import("/tests/integration/cpu-baseline.test.js");
await import("/tests/integration/builder-ui.test.js");
} catch (error) {
registeredTests.push({
name: "테스트 모듈을 불러온다",
+67
View File
@@ -6,9 +6,11 @@ import {
prepareDelaySpectrum,
} from "/packages/acoustics-core/fractional-delay.js";
import { dbToLinear, distance3 } from "/packages/acoustics-core/math.js";
import { renderSceneMix } from "/packages/acoustics-core/mixdown.js";
import { whiteNoiseAt } from "/packages/acoustics-core/noise.js";
import { sphericalAttenuation, synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js";
import { getFrameCount } from "/packages/acoustics-core/stft.js";
import { encodeMonoPcm16Wav } from "/packages/acoustics-core/wav.js";
function assertNear(actual, expected, tolerance, label) {
assert(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected} ± ${tolerance}, received ${actual}`);
@@ -302,3 +304,68 @@ test("CPU DAS가 event-loop 취소와 working-set 한도를 지킨다", async ()
assert(stftStarted, "STFT started before abort");
assert(abortError?.name === "AbortError", "async abort");
});
test("scene mix가 peak 정규화와 clipping 통계를 정확히 기록한다", () => {
const assetBuffers = new Map([
["levels", { sampleRate: 4, channels: [Float32Array.of(2, -1.5, 1, -0.5)] }],
]);
const scene = {
sampleRate: 4,
duration: 1,
seed: 1,
mix: { normalization: "peak", targetPeak: 0.5 },
sources: [{
id: "levels",
gainDb: 0,
startTime: 0,
duration: 1,
signal: { type: "asset", assetId: "levels" },
}],
ambient: [],
};
const normalized = renderSceneMix(scene, assetBuffers);
assertNear(normalized.preNormalizationPeak, 2, 1e-12, "pre-normalization peak");
assertNear(normalized.appliedScale, 0.25, 1e-12, "normalization scale");
assertNear(normalized.peak, 0.5, 1e-12, "normalized peak");
assertEqual(normalized.clippedSampleCount, 0, "normalized clipping count");
[0.5, -0.375, 0.25, -0.125].forEach((expected, index) => {
assertNear(normalized.samples[index], expected, 1e-12, `normalized sample ${index}`);
});
const unnormalized = renderSceneMix({ ...scene, mix: { normalization: "none", targetPeak: 0.5 } }, assetBuffers);
assertNear(unnormalized.preNormalizationPeak, 2, 1e-12, "unnormalized input peak");
assertNear(unnormalized.appliedScale, 1, 1e-12, "unnormalized scale");
assertNear(unnormalized.peak, 2, 1e-12, "unnormalized output peak");
assertEqual(unnormalized.clippedSampleCount, 2, "unnormalized clipping count");
});
test("PCM16 WAV가 표준 mono header를 쓰고 범위 밖 sample을 clamp한다", async () => {
const blob = encodeMonoPcm16Wav(Float32Array.of(-2, -1, -0.5, 0, 0.5, 1, 2), 8000);
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
const view = new DataView(buffer);
const ascii = (offset, length) => String.fromCharCode(...bytes.subarray(offset, offset + length));
assertEqual(blob.type, "audio/wav", "WAV MIME type");
assertEqual(buffer.byteLength, 58, "WAV byte length");
assertEqual(ascii(0, 4), "RIFF", "RIFF id");
assertEqual(view.getUint32(4, true), 50, "RIFF chunk size");
assertEqual(ascii(8, 4), "WAVE", "WAVE id");
assertEqual(ascii(12, 4), "fmt ", "fmt id");
assertEqual(view.getUint32(16, true), 16, "PCM fmt chunk size");
assertEqual(view.getUint16(20, true), 1, "PCM format");
assertEqual(view.getUint16(22, true), 1, "mono channel count");
assertEqual(view.getUint32(24, true), 8000, "sample rate");
assertEqual(view.getUint32(28, true), 16000, "byte rate");
assertEqual(view.getUint16(32, true), 2, "block align");
assertEqual(view.getUint16(34, true), 16, "bits per sample");
assertEqual(ascii(36, 4), "data", "data id");
assertEqual(view.getUint32(40, true), 14, "PCM data byte length");
const actualSamples = Array.from({ length: 7 }, (_, index) => view.getInt16(44 + index * 2, true));
const expectedSamples = [-32768, -32768, -16384, 0, 16383, 32767, 32767];
expectedSamples.forEach((expected, index) => {
assertEqual(actualSamples[index], expected, `PCM sample ${index}`);
});
});
+170
View File
@@ -0,0 +1,170 @@
import { coordinateToPixel, pixelToCoordinate } from "/apps/dataset-builder/canvas-view.js";
import {
assetDescriptorForFile,
createSceneId,
createSource,
matchAssetDescriptor,
prepareSceneForEditing,
referencedAssetIds,
} from "/apps/dataset-builder/model.js";
import { decodeAudioFile } from "/packages/audio-io/browser-audio.js";
import {
MAX_MIX_SAMPLE_OPERATIONS,
MAX_MIX_WORKING_BYTES,
renderSceneMix,
} from "/packages/acoustics-core/mixdown.js";
import { encodeMonoPcm16Wav } from "/packages/acoustics-core/wav.js";
function assertNear(actual, expected, tolerance, label) {
assert(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected}, received ${actual}`);
}
test("Builder model이 schema-safe하고 안정적인 source/asset id를 만든다", () => {
const original = {
id: "scene",
sampleRate: 8000,
duration: 1,
sources: [{ id: "source-1", position: [0, 0, 2] }],
};
const scene = prepareSceneForEditing(original);
const added = createSource(scene);
assertEqual(added.id, "source-2", "unique source id");
assertEqual(original.assets, undefined, "input remains immutable");
assertEqual(scene.mix.targetPeak, 0.95, "default target peak");
const descriptor = assetDescriptorForFile(
{ name: "기계음 샘플.WAV", type: "audio/wav" },
"audio",
new Set(["audio"]),
);
assert(/^[a-z0-9][a-z0-9._-]*$/.test(descriptor.id), "schema-safe asset id");
assertEqual(descriptor.id, "audio-2", "collision suffix");
assertEqual(createSceneId(123456, 789), "scene-2n9c-lx", "deterministic scene id");
assert(/^[a-z0-9][a-z0-9._-]*$/.test(createSceneId()), "generated scene id is schema-safe");
});
test("asset 재연결은 fileName보다 SHA-256을 우선하고 충돌을 분리한다", () => {
const assets = [
{ id: "old", kind: "audio", fileName: "audio/machine.wav", sha256: "a".repeat(64) },
{ id: "unhashed", kind: "audio", fileName: "ambient.wav" },
];
const hashMatch = matchAssetDescriptor(assets, new Set(), "renamed.wav", "audio", "a".repeat(64));
assertEqual(hashMatch.descriptor.id, "old", "hash match");
const nameMatch = matchAssetDescriptor(assets, new Set(), "ambient.wav", "audio", "b".repeat(64));
assertEqual(nameMatch.descriptor.id, "unhashed", "unhashed name match");
const conflict = matchAssetDescriptor(assets, new Set(), "machine.wav", "audio", "c".repeat(64));
assertEqual(conflict.descriptor, null, "different hash does not reconnect");
assertEqual(conflict.conflict, true, "same-name conflict reported");
const duplicateHashAssets = [
{ id: "copy-1", kind: "audio", fileName: "copy-1.wav", sha256: "d".repeat(64) },
{ id: "copy-2", kind: "audio", fileName: "copy-2.wav", sha256: "d".repeat(64) },
];
const secondCopy = matchAssetDescriptor(duplicateHashAssets, new Set(["copy-1"]), "same.wav", "audio", "d".repeat(64));
assertEqual(secondCopy.descriptor.id, "copy-2", "unbound duplicate hash descriptor");
});
test("Builder가 image/source/ambient에서 참조하는 asset을 모두 찾는다", () => {
const ids = referencedAssetIds({
image: { assetId: "image" },
sources: [
{ signal: { type: "asset", assetId: "machine" } },
{ signal: { type: "tone", frequency: 1000 } },
],
ambient: [{ signal: { type: "asset", assetId: "ambient" } }],
});
assertEqual([...ids].join(","), "image,machine,ambient", "referenced assets");
});
test("canvas 좌표 변환이 y-up 평면의 중앙과 모서리를 왕복한다", () => {
const plane = { xMin: -2, xMax: 2, yMin: -1, yMax: 3 };
const topLeft = pixelToCoordinate({ x: 0, y: 0 }, plane, 400, 200);
assertEqual(topLeft.x, -2, "left is xMin");
assertEqual(topLeft.y, 3, "top is yMax");
const bottomRight = pixelToCoordinate({ x: 400, y: 200 }, plane, 400, 200);
assertEqual(bottomRight.x, 2, "right is xMax");
assertEqual(bottomRight.y, -1, "bottom is yMin");
const pixel = coordinateToPixel([0.75, 1.25, 2], plane, 400, 200);
const roundTrip = pixelToCoordinate(pixel, plane, 400, 200);
assertNear(roundTrip.x, 0.75, 1e-12, "x round trip");
assertNear(roundTrip.y, 1.25, 1e-12, "y round trip");
});
test("확인용 mix가 과도한 메모리와 sample 연산을 allocation 전에 거부한다", () => {
const signal = { type: "tone", frequency: 1000 };
const largeMemory = {
sampleRate: 192000,
duration: 300,
seed: 1,
mix: { normalization: "none", targetPeak: 0.95 },
sources: [{ id: "source", gainDb: 0, startTime: 0, duration: 300, signal }],
ambient: [],
};
let memoryError;
try { renderSceneMix(largeMemory); } catch (error) { memoryError = error; }
assert(memoryError?.message.includes("작업 메모리"), "memory guard message");
assert(MAX_MIX_WORKING_BYTES < 192000 * 300 * 8, "fixture exceeds memory guard");
const sources = Array.from({ length: 18 }, (_, index) => ({
id: `source-${index}`, gainDb: 0, startTime: 0, duration: 300, signal,
}));
const largeOperations = { ...largeMemory, sampleRate: 48000, sources };
let operationError;
try { renderSceneMix(largeOperations); } catch (error) { operationError = error; }
assert(operationError?.message.includes("연산량"), "operation guard message");
assert(MAX_MIX_SAMPLE_OPERATIONS < 48000 * 300 * sources.length, "fixture exceeds operation guard");
});
test("WAV encoder가 잘못된 sample rate와 비유한 sample을 거부한다", () => {
let rateError;
try { encodeMonoPcm16Wav(Float32Array.of(0), 0); } catch (error) { rateError = error; }
assert(rateError?.message.includes("sample rate"), "sample-rate guard");
let sampleError;
try { encodeMonoPcm16Wav(Float32Array.of(0, Number.NaN), 48000); } catch (error) { sampleError = error; }
assert(sampleError?.message.includes("유한한"), "finite-sample guard");
});
test("브라우저 audio decoder가 caller의 누적 메모리 budget을 지킨다", async () => {
const wav = encodeMonoPcm16Wav(Float32Array.of(0, 0.25, -0.25, 0), 8000);
const file = new File([wav], "tiny.wav", { type: "audio/wav" });
let budgetError;
try { await decodeAudioFile(file, 8000, 8); } catch (error) { budgetError = error; }
assert(budgetError?.message.includes("decode 크기"), "decoded-byte budget error");
const header = new ArrayBuffer(44);
const view = new DataView(header);
const write = (offset, text) => [...text].forEach((character, index) => view.setUint8(offset + index, character.charCodeAt(0)));
const channels = 4;
const sampleRate = 48000;
const dataByteLength = sampleRate * 300 * channels * 2;
write(0, "RIFF"); view.setUint32(4, 36 + dataByteLength, true); write(8, "WAVE"); write(12, "fmt ");
view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, channels, true);
view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * channels * 2, true);
view.setUint16(32, channels * 2, true); view.setUint16(34, 16, true); write(36, "data");
view.setUint32(40, dataByteLength, true);
const multichannelFile = new File([header], "four-channel.wav", { type: "audio/wav" });
let channelError;
try { await decodeAudioFile(multichannelFile, sampleRate); } catch (error) { channelError = error; }
assert(channelError?.message.includes("예상 decode 크기 219.7 MiB"), "WAV channel-count preflight");
});
test("Builder mix Worker가 기본 tone 장면을 별도 thread에서 생성한다", async () => {
const response = await fetch("/assets/sample-scenes/single-tone.scene.json");
const scene = await response.json();
const result = await new Promise((resolve, reject) => {
const worker = new Worker("/apps/dataset-builder/mix-worker.js", { type: "module" });
worker.addEventListener("message", ({ data }) => {
worker.terminate();
if (data.ok) resolve(data.result);
else reject(new Error(data.message));
});
worker.addEventListener("error", (event) => {
worker.terminate();
reject(new Error(event.message));
});
worker.postMessage({ scene, assetBuffers: [] });
});
assertEqual(result.samples.length, 12000, "worker sample count");
assert(result.peak > 0.5 && result.peak < 0.502, "worker tone peak");
assertEqual(result.clippedSampleCount, 0, "worker clipping count");
});