Refactor ani parser

This commit is contained in:
Jordan Eldredge 2020-12-08 23:07:52 -08:00
parent 0bb694d9d2
commit f6cf3a1897
4 changed files with 65 additions and 82 deletions

Binary file not shown.

View file

@ -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();
});
});

View file

@ -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<T>(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 };
}