From f6cf3a1897efdc7e3d7e668035e0c30f4ddd3eff Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 8 Dec 2020 23:07:52 -0800 Subject: [PATCH] Refactor ani parser --- .../js/__snapshots__/aniParser.test.ts.snap | Bin 4193 -> 7008 bytes .../js/__tests__/fixtures/ani/piano.ani | Bin 0 -> 4062 bytes packages/webamp/js/aniParser.test.ts | 9 ++ packages/webamp/js/aniParser.ts | 138 +++++++----------- 4 files changed, 65 insertions(+), 82 deletions(-) create mode 100644 packages/webamp/js/__tests__/fixtures/ani/piano.ani diff --git a/packages/webamp/js/__snapshots__/aniParser.test.ts.snap b/packages/webamp/js/__snapshots__/aniParser.test.ts.snap index 860e0d13320afc91f823620137a4ceda5131f2bf..09f235c2dc19a35debae303d2c1d079d89757231 100644 GIT binary patch delta 930 zcmdT>OHbQC5N2r;7t3lY!b4S2wY4e=Qfl@QI|--<%L%9llyCu66#@%fV>M(`+ek!e zEhf1%msTn2A?yFaExY=34e1% zdH+E=ynONkerH$UV>a*X4h4*ZxalSOHJWb63vnaa$3&uTSKVJzRqs?v9!Kx@Q+IZ% z7>GkpdHaai&Q?{}KG3N=A}~BvS7yqIUtI#$`uLdu delta 56 zcmV-80LTB}HsK(UHIwfLBC`+%-vN^i6c&@b4hplQ2~Y)-1{hG2pb-?aR}Xu!5o OGcc391v0Y@6rlq&tP%47 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 0000000000000000000000000000000000000000..b293aba22c6ce2692577eef491ed9665e2d84a15 GIT binary patch literal 4062 zcmeH~!A`phaJU8e8Js6EAE-E1*AweYQ3p50ziPBm~kQ03aUWuphY&?17 z30(i#MJQq=hD`*+mYLm|-EL_+U#3$n77EWPqFnyETq@q&mWZfWE?gJOxvM0`m3zBa z_pOR!w_MERs<%h*=3TGnJ9o{FmB}Akr`hb8b>-B2x9v7MR^IhJ*S9;4+d3w@Yp{EJ9|S$H+&GK#H`&HDbb%C9xb-b zfjQ_4*5&}~M|ju+snP$v?cueD{|}yqM@AQJG(&ZAJXVN-Uo``(d8cLN1$(@nr!O`X zj9-NIE=)o==Go7-IU#{$)IWWp;AeZaDUduWHYJsae}uk13Is4%mH>JF$%X>$fj_t5 zCkfwCFeO1zz_~z0{xlav+iO4p=O&pGJdS^@xj+B|8O2NI18b8D=Au@=LvavAfyxL| z3KR!Z3Zm^bp&-B$#TS}>3PyeGy!00l>VORe`hz}V$7l+8CQJcmgfIp3wyXnWMIh^f mm8b(2)g}5V(DX$l>w { 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 }; }