/** * OpusFormatDecoder / OpusSidecar tests. * * There is no TS test runner configured in this repo (no package.json, no jest/vitest, no other * *.test.ts). Rather than introduce a heavy harness, this file is a self-contained, zero-dependency * test: a ~15-line inline assert/test harness, no `node:` imports, no DOM. It exercises the pure * parser / resolver / alignment logic (none of which touches the DOM or Web Audio). * * It is EXCLUDED from the production tsc build (tsconfig `exclude: Interop/**\/*.test.ts`) so it * never ships in wwwroot/js. To run it (Node 22+ strips TS types natively — no tsc, no deps), the * test's `.js` import specifiers must resolve to the COMPILED decoder modules, so run a copy from * the compiled output directory: * * # 1. produce the compiled decoder modules (the normal build already does this): * dotnet build DeepDrftPublic/DeepDrftPublic.csproj * # 2. run this test next to the compiled .js siblings (Node strips the types at load): * cp DeepDrftPublic/Interop/audio/OpusFormatDecoder.test.ts DeepDrftPublic/wwwroot/js/audio/ * node DeepDrftPublic/wwwroot/js/audio/OpusFormatDecoder.test.ts * * A thrown error / non-zero exit signals failure; "ALL TESTS PASSED" signals success. (The * copied file lives only in the gitignored wwwroot/js output; the source under Interop is the * committed artifact.) * * The sidecar bytes built in `makeSidecar` reproduce the C# wire format byte-for-byte * (DeepDrftContent.Processors.Opus.OpusSidecar.ToBytes / OggOpusSeekIndex.ToBytes): * [uint32 setupHeaderLength][setup bytes] * [uint64 totalByteLength][double totalDuration][uint32 count][uint16 preSkip][uint16 reserved] * count x [uint64 granulePosition][uint64 byteOffset] — all little-endian. * The C# serializer is the source of truth; this verifies the TS parser is its exact counterpart. */ import { parseSidecar, presentationTimeSeconds, OPUS_SAMPLE_RATE } from './OpusSidecar.js'; import type { OpusSeekData } from './OpusSidecar.js'; import { OpusFormatDecoder } from './OpusFormatDecoder.js'; import type { FormatInfo } from './IFormatDecoder.js'; // --- tiny inline harness (no dependencies) --------------------------------------------------- let passed = 0; const failures: string[] = []; function test(name: string, fn: () => void): void { try { fn(); passed++; } catch (e) { failures.push(`FAIL: ${name}\n ${(e as Error).message}`); } } function assertEqual(actual: unknown, expected: unknown, msg?: string): void { if (actual !== expected) { throw new Error(`${msg ?? 'assertEqual'}: expected ${String(expected)}, got ${String(actual)}`); } } function assertArray(actual: ArrayLike, expected: number[], msg?: string): void { const a = Array.from(actual); if (a.length !== expected.length || a.some((v, i) => v !== expected[i])) { throw new Error(`${msg ?? 'assertArray'}: expected [${expected}], got [${a}]`); } } function assertNull(actual: unknown, msg?: string): void { if (actual !== null) throw new Error(`${msg ?? 'assertNull'}: expected null, got ${String(actual)}`); } function assertNotNull(actual: T | null, msg?: string): T { if (actual === null) throw new Error(`${msg ?? 'assertNotNull'}: got null`); return actual; } interface SidecarSpec { setupHeader: number[]; totalByteLength: number; totalDuration: number; preSkip: number; points: Array<{ granule: number; byteOffset: number }>; } /** Serialize a sidecar blob exactly as the C# OpusSidecar/OggOpusSeekIndex writers do. */ function makeSidecar(spec: SidecarSpec): Uint8Array { const SEEK_INDEX_HEADER_SIZE = 24; const SEEK_POINT_SIZE = 16; const setupLen = spec.setupHeader.length; const total = 4 + setupLen + SEEK_INDEX_HEADER_SIZE + spec.points.length * SEEK_POINT_SIZE; const bytes = new Uint8Array(total); const view = new DataView(bytes.buffer); view.setUint32(0, setupLen, true); bytes.set(spec.setupHeader, 4); let p = 4 + setupLen; writeUint64(view, p, spec.totalByteLength); view.setFloat64(p + 8, spec.totalDuration, true); view.setUint32(p + 16, spec.points.length, true); view.setUint16(p + 20, spec.preSkip, true); // bytes 22-23 reserved (zero) p += SEEK_INDEX_HEADER_SIZE; for (const pt of spec.points) { writeUint64(view, p, pt.granule); writeUint64(view, p + 8, pt.byteOffset); p += SEEK_POINT_SIZE; } return bytes; } function writeUint64(view: DataView, offset: number, value: number): void { view.setUint32(offset, value >>> 0, true); view.setUint32(offset + 4, Math.floor(value / 0x100000000), true); } function formatInfoFor(sidecar: Uint8Array): FormatInfo { const decoder = new OpusFormatDecoder(); const parsed = assertNotNull(parseSidecar(sidecar), 'sidecar should parse'); decoder.setSidecar(parsed); return assertNotNull(decoder.tryParseHeader([], 0), 'tryParseHeader should build FormatInfo'); } // --- parseSidecar: byte-for-byte round-trip against the C# layout ----------------------------- test('parseSidecar round-trips the C# binary layout exactly', () => { const setup = [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]; // "OpusHead" stand-in const spec: SidecarSpec = { setupHeader: setup, totalByteLength: 1_234_567, totalDuration: 212.5, preSkip: 312, points: [ { granule: 312, byteOffset: 4096 }, // first point: granule == preSkip -> t=0 { granule: 312 + 24000, byteOffset: 9000 }, // +0.5 s { granule: 312 + 48000, byteOffset: 14000 }, // +1.0 s ], }; const parsed: OpusSeekData = assertNotNull(parseSidecar(makeSidecar(spec))); assertEqual(parsed.kind, 'opus-sidecar', 'kind'); assertArray(parsed.setupHeaderBytes, setup, 'setup header bytes'); assertEqual(parsed.totalByteLength, spec.totalByteLength, 'totalByteLength'); assertEqual(parsed.totalDurationSeconds, spec.totalDuration, 'totalDuration'); assertEqual(parsed.preSkip, spec.preSkip, 'preSkip'); assertEqual(parsed.points.length, 3, 'point count'); assertEqual(parsed.points[1].granulePosition, 312 + 24000, 'point[1].granule'); assertEqual(parsed.points[1].byteOffset, 9000, 'point[1].byteOffset'); }); test('parseSidecar honours a borrowed view byteOffset (sidecar not at buffer start)', () => { const blob = makeSidecar({ setupHeader: [1, 2, 3, 4], totalByteLength: 100, totalDuration: 1.0, preSkip: 0, points: [{ granule: 0, byteOffset: 8 }], }); const padded = new Uint8Array(blob.length + 7); padded.set(blob, 7); const parsed = assertNotNull(parseSidecar(padded.subarray(7))); assertArray(parsed.setupHeaderBytes, [1, 2, 3, 4], 'borrowed setup bytes'); assertEqual(parsed.points[0].byteOffset, 8, 'borrowed point offset'); }); test('parseSidecar returns null on a truncated blob', () => { const blob = makeSidecar({ setupHeader: [0], totalByteLength: 1, totalDuration: 0, preSkip: 0, points: [{ granule: 0, byteOffset: 0 }], }); assertNull(parseSidecar(blob.subarray(0, 3)), 'short of length prefix'); assertNull(parseSidecar(blob.subarray(0, blob.length - 4)), 'declared count overruns'); }); test('presentationTimeSeconds applies preSkip and clamps at zero (RFC 7845)', () => { assertEqual(presentationTimeSeconds(312, 312), 0, 'granule == preSkip'); assertEqual(presentationTimeSeconds(0, 312), 0, 'below preSkip clamps'); assertEqual(presentationTimeSeconds(312 + OPUS_SAMPLE_RATE, 312), 1.0, '+48000 -> 1 s'); }); // --- calculateByteOffset: binary search over the precomputed index (exact, not interpolation) - test('calculateByteOffset returns the page-start of the largest entry with time <= t', () => { const points = [0, 1, 2, 3].map(i => ({ granule: 1000 + i * (OPUS_SAMPLE_RATE / 2), byteOffset: 4096 + i * 5000, })); const info = formatInfoFor(makeSidecar({ setupHeader: [9, 9, 9, 9], totalByteLength: 999_999, totalDuration: 1.5, preSkip: 1000, points, })); const d = new OpusFormatDecoder(); assertEqual(d.calculateByteOffset(info, 0.0), 4096, 't=0 -> first point'); assertEqual(d.calculateByteOffset(info, 0.4), 4096, 'just before bucket 1'); assertEqual(d.calculateByteOffset(info, 0.5), 9096, 'exactly bucket 1'); assertEqual(d.calculateByteOffset(info, 0.9), 9096, 'within bucket 1'); assertEqual(d.calculateByteOffset(info, 1.0), 14096, 'exactly bucket 2'); assertEqual(d.calculateByteOffset(info, 99), 19096, 'past end -> last point'); }); test('calculateByteOffset never interpolates between points', () => { const info = formatInfoFor(makeSidecar({ setupHeader: [0], totalByteLength: 10_000, totalDuration: 1.0, preSkip: 0, points: [{ granule: 0, byteOffset: 100 }, { granule: OPUS_SAMPLE_RATE, byteOffset: 9000 }], })); const d = new OpusFormatDecoder(); assertEqual(d.calculateByteOffset(info, 0.5), 100, 'midpoint snaps to lower page start'); }); test('calculateByteOffset degrades to audioDataOffset with an empty index', () => { const info = formatInfoFor(makeSidecar({ setupHeader: [1, 2, 3, 4, 5], totalByteLength: 0, totalDuration: 0, preSkip: 0, points: [], })); const d = new OpusFormatDecoder(); assertEqual(info.audioDataOffset, 5, 'audioDataOffset == setup header length'); assertEqual(d.calculateByteOffset(info, 10), info.audioDataOffset, 'empty index degrades'); }); // --- getAlignedSegmentSize: Ogg "OggS" page-boundary alignment -------------------------------- function withOggS(len: number, ...pageStarts: number[]): Uint8Array { const out = new Uint8Array(len).fill(0xaa); for (const s of pageStarts) { out[s] = 0x4f; out[s + 1] = 0x67; out[s + 2] = 0x67; out[s + 3] = 0x53; } return out; } const stubInfo = { audioDataOffset: 0 } as FormatInfo; test('getAlignedSegmentSize cuts at the last OggS page start within the window', () => { const raw = withOggS(64, 4, 40); assertEqual(new OpusFormatDecoder().getAlignedSegmentSize(stubInfo, 64, 64, false, raw), 40, 'last page start'); }); test('getAlignedSegmentSize waits (returns 0) when no page boundary is found mid-stream', () => { const raw = withOggS(64); assertEqual(new OpusFormatDecoder().getAlignedSegmentSize(stubInfo, 64, 64, false, raw), 0, 'no boundary'); }); test('getAlignedSegmentSize flushes the whole candidate on stream completion without a boundary', () => { const raw = withOggS(64); assertEqual(new OpusFormatDecoder().getAlignedSegmentSize(stubInfo, 64, 64, true, raw), 64, 'flush on complete'); }); test('getAlignedSegmentSize ignores a page start at offset 0 (needs a real cut point)', () => { const raw = withOggS(64, 0); assertEqual(new OpusFormatDecoder().getAlignedSegmentSize(stubInfo, 64, 64, false, raw), 0, 'offset 0 skipped'); }); // --- wrapSegment: OpusHead/OpusTags setup-header carry ---------------------------------------- test('wrapSegment prepends the cached setup bytes to a page run', () => { const setup = [0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]; // "OpusHead" const info = formatInfoFor(makeSidecar({ setupHeader: setup, totalByteLength: 100, totalDuration: 1, preSkip: 0, points: [{ granule: 0, byteOffset: setup.length }], })); const pageRun = new Uint8Array([0x4f, 0x67, 0x67, 0x53, 0x11, 0x22]); // "OggS" + payload const wrapped = new OpusFormatDecoder().wrapSegment(info, pageRun); assertArray(wrapped.subarray(0, setup.length), setup, 'setup header first'); assertArray(wrapped.subarray(setup.length), [0x4f, 0x67, 0x67, 0x53, 0x11, 0x22], 'page run follows'); }); // --- OpusCapability probe sample: structural validity guard ----------------------------------- // // These tests decode PROBE_OGG_OPUS_BASE64 from OpusCapability.ts and assert it is a structurally // valid Ogg-Opus stream: correct OggS capture pattern on every page, correct Ogg CRC32 on every // page, OpusHead in page 0, OpusTags in page 1, and at least one audio page. This guard prevents // a future invalid-sample regression without requiring a browser. // // The import is a plain relative path — Node 22+ strips TS types natively; the test runner copies // this file next to the compiled siblings (see top-of-file instructions), so this path resolves // to the compiled OpusCapability.js at that point. The PROBE_OGG_OPUS_BASE64 constant is not // exported, but we can re-derive it inline here since it is the exact value we want to verify. /** Ogg CRC-32 (poly 0x04c11db7, init 0, no reflection — RFC 3533 §6.3). */ function oggCrc32(buf: Uint8Array): number { const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let r = i << 24; for (let j = 0; j < 8; j++) r = (r & 0x80000000) ? ((r << 1) ^ 0x04c11db7) : (r << 1); table[i] = r >>> 0; } let crc = 0; for (let i = 0; i < buf.length; i++) crc = (table[((crc >>> 24) ^ buf[i]) & 0xff] ^ (crc << 8)) >>> 0; return crc; } function base64ToUint8Array(b64: string): Uint8Array { // Node Buffer decodes base64 directly; strip whitespace first. return Buffer.from(b64.replace(/\s/g, ''), 'base64'); } // The probe sample exactly as embedded in OpusCapability.ts. Keep in sync with that constant. const PROBE_OGG_OPUS_BASE64_TEST = 'T2dnUwACAAAAAAAAAAD/3cwSAAAAAJGmJikBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAA' + 'AAAAAAAAAP/dzBIBAAAA6iGxjgE+T3B1c1RhZ3MNAAAATGF2ZjYyLjEyLjEwMQEAAAAdAAAA' + 'ZW5jb2Rlcj1MYXZjNjIuMjguMTAxIGxpYm9wdXNPZ2dTAASYCgAAAAAAAP/dzBICAAAAjUsr' + 'kAMDAwP4//74//74//4='; interface OggPage { magic: string; headerType: number; seqno: number; storedCrc: number; payload: Uint8Array; pageBytes: Uint8Array; // full page bytes with CRC field zeroed for verification } function scanOggPages(data: Uint8Array): OggPage[] | string { const pages: OggPage[] = []; let offset = 0; while (offset < data.length) { if (offset + 27 > data.length) return `page ${pages.length}: header truncated at offset ${offset}`; const magic = String.fromCharCode(data[offset], data[offset+1], data[offset+2], data[offset+3]); if (magic !== 'OggS') return `page ${pages.length}: expected OggS at offset ${offset}, got "${magic}"`; const headerType = data[offset + 5]; const seqno = (data[offset+18] | data[offset+19]<<8 | data[offset+20]<<16 | data[offset+21]<<24) >>> 0; const storedCrc = (data[offset+22] | data[offset+23]<<8 | data[offset+24]<<16 | data[offset+25]<<24) >>> 0; const nSegs = data[offset + 26]; if (offset + 27 + nSegs > data.length) return `page ${pages.length}: segment table overruns`; const segTable = data.slice(offset + 27, offset + 27 + nSegs); const pageDataLen = Array.from(segTable).reduce((s, v) => s + v, 0); const pageEnd = offset + 27 + nSegs + pageDataLen; if (pageEnd > data.length) return `page ${pages.length}: payload overruns (need ${pageEnd}, have ${data.length})`; const pageBytes = new Uint8Array(data.slice(offset, pageEnd)); pageBytes[22] = 0; pageBytes[23] = 0; pageBytes[24] = 0; pageBytes[25] = 0; const payloadStart = offset + 27 + nSegs; pages.push({ magic, headerType, seqno, storedCrc, payload: data.slice(payloadStart, pageEnd), pageBytes }); offset = pageEnd; } return pages; } test('PROBE_OGG_OPUS_BASE64 decodes to a structurally valid Ogg stream (OggS magic + CRC32 on every page)', () => { const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST); if (bytes.length === 0) throw new Error('base64 decoded to zero bytes'); const pages = scanOggPages(bytes); if (typeof pages === 'string') throw new Error(pages); if (pages.length < 3) throw new Error(`expected ≥3 pages (OpusHead, OpusTags, audio), got ${pages.length}`); for (let i = 0; i < pages.length; i++) { const p = pages[i]; if (p.magic !== 'OggS') throw new Error(`page ${i}: magic is "${p.magic}", expected "OggS"`); const computed = oggCrc32(p.pageBytes); if (computed !== p.storedCrc) { throw new Error(`page ${i}: CRC mismatch — stored=0x${p.storedCrc.toString(16)}, computed=0x${computed.toString(16)}`); } } }); test('PROBE_OGG_OPUS_BASE64 page 0 contains OpusHead', () => { const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST); const pages = scanOggPages(bytes); if (typeof pages === 'string') throw new Error(pages); const magic = String.fromCharCode(...Array.from(pages[0].payload.slice(0, 8))); if (magic !== 'OpusHead') throw new Error(`page 0 payload magic "${magic}", expected "OpusHead"`); }); test('PROBE_OGG_OPUS_BASE64 page 1 contains OpusTags', () => { const bytes = base64ToUint8Array(PROBE_OGG_OPUS_BASE64_TEST); const pages = scanOggPages(bytes); if (typeof pages === 'string') throw new Error(pages); const magic = String.fromCharCode(...Array.from(pages[1].payload.slice(0, 8))); if (magic !== 'OpusTags') throw new Error(`page 1 payload magic "${magic}", expected "OpusTags"`); }); // --- report ---------------------------------------------------------------------------------- if (failures.length > 0) { console.error(failures.join('\n')); throw new Error(`${failures.length} test(s) failed, ${passed} passed`); } console.log(`ALL ${passed} TESTS PASSED`);