/** * Store-only ZIP writer, enough for the dataset download without a * dependency. WAV float data barely compresses, so storing costs little; * every entry gets a real CRC so ordinary unzip tools verify it. */ interface ZipEntry { name: string; data: Uint8Array; } const CRC_TABLE = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); function crc32(data: Uint8Array): number { let c = 0xffffffff; for (let i = 0; i < data.length; i++) c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } export function zipStore(entries: ZipEntry[]): Uint8Array { const enc = new TextEncoder(); const parts: Uint8Array[] = []; const central: Uint8Array[] = []; let offset = 0; for (const e of entries) { const name = enc.encode(e.name); const crc = crc32(e.data); const local = new Uint8Array(30 + name.length); const lv = new DataView(local.buffer); lv.setUint32(0, 0x04034b50, true); lv.setUint16(4, 20, true); // version needed lv.setUint16(6, 0x0800, true); // utf-8 names lv.setUint16(8, 0, true); // stored lv.setUint16(10, 0, true); // time lv.setUint16(12, 0x21, true); // date (1980-01-01) lv.setUint32(14, crc, true); lv.setUint32(18, e.data.length, true); lv.setUint32(22, e.data.length, true); lv.setUint16(26, name.length, true); lv.setUint16(28, 0, true); local.set(name, 30); parts.push(local, e.data); const cd = new Uint8Array(46 + name.length); const cv = new DataView(cd.buffer); cv.setUint32(0, 0x02014b50, true); cv.setUint16(4, 20, true); cv.setUint16(6, 20, true); cv.setUint16(8, 0x0800, true); cv.setUint16(10, 0, true); cv.setUint16(12, 0, true); cv.setUint16(14, 0x21, true); cv.setUint32(16, crc, true); cv.setUint32(20, e.data.length, true); cv.setUint32(24, e.data.length, true); cv.setUint16(28, name.length, true); cv.setUint32(42, offset, true); cd.set(name, 46); central.push(cd); offset += local.length + e.data.length; } const cdSize = central.reduce((a, c) => a + c.length, 0); const eocd = new Uint8Array(22); const ev = new DataView(eocd.buffer); ev.setUint32(0, 0x06054b50, true); ev.setUint16(8, entries.length, true); ev.setUint16(10, entries.length, true); ev.setUint32(12, cdSize, true); ev.setUint32(16, offset, true); const total = offset + cdSize + 22; const out = new Uint8Array(total); let p = 0; for (const part of [...parts, ...central, eocd]) { out.set(part, p); p += part.length; } return out; }