diff --git a/packages/webamp/js/__snapshots__/aniParser.test.ts.snap b/packages/webamp/js/__snapshots__/aniParser.test.ts.snap index 860e0d13..09f235c2 100644 Binary files a/packages/webamp/js/__snapshots__/aniParser.test.ts.snap and b/packages/webamp/js/__snapshots__/aniParser.test.ts.snap differ diff --git a/packages/webamp/js/__tests__/fixtures/ani/piano.ani b/packages/webamp/js/__tests__/fixtures/ani/piano.ani new file mode 100644 index 00000000..b293aba2 Binary files /dev/null and b/packages/webamp/js/__tests__/fixtures/ani/piano.ani differ diff --git a/packages/webamp/js/aniParser.test.ts b/packages/webamp/js/aniParser.test.ts index 12ac3a65..5046a3a5 100644 --- a/packages/webamp/js/aniParser.test.ts +++ b/packages/webamp/js/aniParser.test.ts @@ -34,6 +34,7 @@ function readPathCss(filePath: string) { describe("Super_Mario_Amp_2.wsz", () => { test("eqslid.cur", async () => { expect(parsePath("Super_Mario_Amp_2/eqslid.cur")).toMatchSnapshot(); + expect(readPathCss("Super_Mario_Amp_2/eqslid.cur")).toMatchSnapshot(); }); test("close.cur", async () => { expect(parsePath("Super_Mario_Amp_2/close.cur")).toMatchSnapshot(); @@ -45,5 +46,13 @@ describe("Super_Mario_Amp_2.wsz", () => { describe("Green Dimension v2.wsz", () => { test("eqslid.cur", async () => { expect(parsePath("Green Dimension v2/eqslid.cur")).toMatchSnapshot(); + expect(readPathCss("Green Dimension v2/eqslid.cur")).toMatchSnapshot(); + }); +}); + +describe("Edge cases", () => { + test("piano.ani", async () => { + expect(parsePath("piano.ani")).toMatchSnapshot(); + expect(readPathCss("piano.ani")).toMatchSnapshot(); }); }); diff --git a/packages/webamp/js/aniParser.ts b/packages/webamp/js/aniParser.ts index 0a5bda2e..42eeda73 100644 --- a/packages/webamp/js/aniParser.ts +++ b/packages/webamp/js/aniParser.ts @@ -24,8 +24,6 @@ export type AniMetadata = { bfAttributes: number; // ANI attribute bit flags }; -type AniInfo = { title: string | null; artist: string | null }; - export type ParsedAni = { rate: number[] | null; seq: number[] | null; @@ -44,99 +42,75 @@ export function parseAni(arr: Uint8Array): ParsedAni { const signature = riff.signature as Chunk; if (signature.format !== "ACON") { - throw new Error(`Expected fromat "ACON", got "${signature.format}"`); + throw new Error( + `Expected format. Expected "ACON", got "${signature.format}"` + ); } - let metadata: null | AniMetadata = null; - let rate: number[] | null = null; - let seq: number[] | null = null; - let images: Uint8Array[] | null = null; - let info: AniInfo = { artist: null, title: null }; + // Helper function to get a chunk by chunkId and transform it if it's non-null. + function mapChunk(chunkId: string, mapper: (chunk: Chunk) => T): T | null { + const chunk = riff.findChunk(chunkId) as Chunk | null; + return chunk == null ? null : mapper(chunk); + } - signature.subChunks.forEach(({ chunkId, chunkData, subChunks, format }) => { - switch (trimNullTerminated(chunkId)) { - case "anih": - metadata = parseMetadata(arr, chunkData.start, chunkData.end); - break; - case "rate": - rate = unpackArray(arr, DWORD, chunkData.start, chunkData.end); - break; - case "seq": - seq = unpackArray(arr, DWORD, chunkData.start, chunkData.end); - break; - case "LIST": // TODO: assert(i === subChunks.length) - switch (format) { - case "INFO": - info = parseInfo(arr, subChunks); - break; - case "fram": - images = subChunks.map((c) => { - if (c.chunkId !== "icon") { - throw new Error(`Unexpected chunk type in fram: ${chunkId}`); - } - return arr.slice(c.chunkData.start, c.chunkData.end); - }); - } - break; - default: - // TODO: We could assert that this never happens - } + const metadata = mapChunk("anih", (c) => { + const words = unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + return { + cbSize: words[0], + nFrames: words[1], + nSteps: words[2], + iWidth: words[3], + iHeight: words[4], + iBitCount: words[5], + nPlanes: words[6], + iDispRate: words[7], + bfAttributes: words[8], + }; }); if (metadata == null) { throw new Error("Did not find anih"); } - if (images == null) { - throw new Error("Did not find LIST"); + const rate = mapChunk("rate", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + // chunkIds are always four chars, hence the trailing space. + const seq = mapChunk("seq ", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + + const lists = riff.findChunk("LIST", true) as Chunk[] | null; + const imageChunk = lists?.find((c) => c.format === "fram"); + if (imageChunk == null) { + throw new Error("Did not find fram LIST"); } - return { ...info, images, rate, seq, metadata }; -} - -function parseInfo(arr: Uint8Array, chunks: Chunk[]): AniInfo { - const info: AniInfo = { title: null, artist: null }; - chunks.forEach((chunk) => { - switch (chunk.chunkId) { - case "INAM": - info.title = trimNullTerminated( - unpackString(arr, chunk.chunkData.start, chunk.chunkData.end) - ); - break; - case "IART": - info.artist = trimNullTerminated( - unpackString(arr, chunk.chunkData.start, chunk.chunkData.end) - ); - break; - default: - // Unexpected subchunk + const images = imageChunk.subChunks.slice(0, metadata.nFrames).map((c) => { + if (c.chunkId !== "icon") { + throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`); } + return arr.slice(c.chunkData.start, c.chunkData.end); }); - return info; -} -function parseMetadata( - arr: Uint8Array, - start: number, - end: number -): AniMetadata { - // TODO: We could assert that we have 9 items here. - const words = unpackArray(arr, DWORD, start, end); - return { - cbSize: words[0], - nFrames: words[1], - nSteps: words[2], - iWidth: words[3], - iHeight: words[4], - iBitCount: words[5], - nPlanes: words[6], - iDispRate: words[7], - bfAttributes: words[8], - }; -} + let title = null; + let artist = null; -// I suspect that RIFF points to byte ranges, but that includes the byte(s?) -// used for null termination. -function trimNullTerminated(str: string): string { - return str.trim(); + const infoChunk = lists?.find((c) => c.format === "INFO"); + if (infoChunk != null) { + infoChunk.subChunks.forEach((c) => { + switch (c.chunkId) { + case "INAM": + title = unpackString(arr, c.chunkData.start, c.chunkData.end); + break; + case "IART": + artist = unpackString(arr, c.chunkData.start, c.chunkData.end); + break; + default: + // Unexpected subchunk + } + }); + } + + return { images, rate, seq, metadata, artist, title }; }