mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
Refactor ani parser
This commit is contained in:
parent
0bb694d9d2
commit
f6cf3a1897
4 changed files with 65 additions and 82 deletions
Binary file not shown.
BIN
packages/webamp/js/__tests__/fixtures/ani/piano.ani
Normal file
BIN
packages/webamp/js/__tests__/fixtures/ani/piano.ani
Normal file
Binary file not shown.
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue