"""pop_idx.py — ДИСКОВЫЕ ИНДЕКСЫ аудио-наборов SprPoP. Зачем. Раскладка звукового набора (какой эффект в какой странице, по какому смещению и какой длины) сегодня печатается упаковщиком в gen/pop_sound_tbl.h и становится частью EXE. Из-за этого замена набора (SDLPoP <-> MSDOS) требует перекомпиляции игры. Индекс переносит эту раскладку НА ДИСК: пара `SND/snd.idx` + `SND/snd.arc` меняется целиком, а бинарник остаётся прежним. Формат читают ДВЕ стороны — этот упаковщик и загрузчик на Z80, — поэтому писатель и разборщик живут в одном месте и покрыты тестом (tools/tests/test_pop_idx.py). Числа little-endian: у Z80 это родной порядок, и разбор на нём — просто чтение байтов подряд. PSI1 — индекс ЭФФЕКТОВ, ровно один сектор DSS (512 Б): 0..3 'PSI1' магия и версия 4 57 число плотных записей, id 0..56 5 5 размер записи 6 7 log2 размера блока: 1 << 7 = 128 7 1 формат PCM: 8 бит без знака, моно, CBL_FREQ_10K9 8 pages сколько PCM-страниц в снд-архиве (1..16) 9..15 0 резерв 16..300 57 записей по 5 байт: page (u8), off (u16), len (u16) 301..511 0 добивка до сектора Размер записи 5 — это НЕ произвольное число, а sizeof(pop_snd_ent_t) в ABI SDCC z80 (uint8 + uint16 + uint16 без выравнивания). Загрузчик читает записи ПРЯМО в резидентную таблицу, поэтому расхождение здесь молча разъедет всю раскладку; на стороне C это проверяется статически. Нулевая `len` означает, что у этого id нет оцифровки (это музыка). """ import struct SECTOR = 512 # --- PSI1: эффекты --------------------------------------------------------- SND_MAGIC = b"PSI1" SND_COUNT = 57 # пространство id оригинала: 0..56 SND_REC = 5 # sizeof(pop_snd_ent_t) в ABI SDCC z80 SND_HDR = 16 SND_MAX_PAGES = 16 # compile-time вместимость загрузчика BLOCK = 128 # блок запроса CBL при 8 бит BLOCK_LOG2 = 7 PCM_U8_MONO_10K9 = 1 # единственный формат, который умеет наш насос PAGE = 16384 class IdxError(Exception): """Индекс не разобрался: битый, чужой версии или противоречивый.""" def snd_pack(entries, pages): """Собрать PSI1 из 57 записей (page, off, len) и числа страниц.""" if len(entries) != SND_COUNT: raise IdxError("записей должно быть %d, а не %d" % (SND_COUNT, len(entries))) if not 1 <= pages <= SND_MAX_PAGES: raise IdxError("страниц %d — вне 1..%d" % (pages, SND_MAX_PAGES)) out = bytearray(SECTOR) out[0:4] = SND_MAGIC out[4] = SND_COUNT out[5] = SND_REC out[6] = BLOCK_LOG2 out[7] = PCM_U8_MONO_10K9 out[8] = pages for i, (page, off, ln) in enumerate(entries): _check_entry(i, page, off, ln, pages) struct.pack_into(" (записи, pages). Проверки те же, что у Z80.""" if len(data) != SECTOR: raise IdxError("размер %d, а должен быть сектор %d" % (len(data), SECTOR)) if data[0:4] != SND_MAGIC: raise IdxError("не PSI1: %r" % (bytes(data[0:4]),)) if data[4] != SND_COUNT: raise IdxError("записей %d, ожидалось %d" % (data[4], SND_COUNT)) if data[5] != SND_REC: raise IdxError("размер записи %d, ожидался %d" % (data[5], SND_REC)) if data[6] != BLOCK_LOG2: raise IdxError("log2 блока %d, ожидался %d" % (data[6], BLOCK_LOG2)) if data[7] != PCM_U8_MONO_10K9: raise IdxError("формат PCM %d — насос умеет только %d" % (data[7], PCM_U8_MONO_10K9)) pages = data[8] if not 1 <= pages <= SND_MAX_PAGES: raise IdxError("страниц %d — вне 1..%d" % (pages, SND_MAX_PAGES)) entries = [] for i in range(SND_COUNT): page, off, ln = struct.unpack_from("= pages: raise IdxError("запись %d: page %d >= pages %d" % (i, page, pages)) if off >= PAGE: raise IdxError("запись %d: off %d вне страницы" % (i, off)) if off % BLOCK or ln % BLOCK: raise IdxError("запись %d: off/len не кратны блоку %d" % (i, BLOCK)) if page == 0 and off < BLOCK: raise IdxError("запись %d залезает в блок тишины (стр.0, off 0)" % i) # Длинный эффект переливается на следующие страницы — они обязаны быть. last = (page * PAGE + off + ln - 1) // PAGE if last >= pages: raise IdxError("запись %d выходит за набор: кончается в стр.%d из %d" % (i, last, pages))