Files
Sprinter-SDCC/applications/PoP/toolchain/pop_pack_music.py
T
snark13 ba3aca07bf Архивы ресурсов PBA1: звук одним файлом, музыка потоком; ALLOCS 6000
ЗАМЕР, из которого всё выросло (MAME, 2026-08-25): `open` стоит 51,4 мс,
а чтение 16 КБ из уже открытого файла — 32,6 мс.  То есть 61 % времени
загрузки уходит на ОТКРЫТИЕ, а не на данные, и чистая скорость чтения —
491 КБ/с, а не 260.  На диске игры 343 файла; одна только стартовая пачка
(kid+shadow+bg+sound+guard, ~142 файла) — это 7,3 секунды чистого open.

* toolchain/pop_pack_arc.py + pop_arc.c/h (банк 8) — формат PBA1:
  заголовок 512 Б, магия, count, записи по 4 байта (offset в СЕКТОРАХ —
  архив бывает больше 64 КБ; size в байтах — ресурс всегда <= 16 КБ,
  он обязан лезть в EMM-страницу).  Таблицу держит вызывающий на своём
  стеке: в W2 её класть нельзя, а стека в точке загрузки израсходовано
  75 байт из 1279 (замер там же).
* libc: bank_read_page — чтение из УЖЕ ОТКРЫТОГО файла в EMM-страницу.
  bank_load_file переписана через неё, поэтому дублирования работы с W3
  не осталось и резидент почти не вырос.
* Набор эффектов: 10 файлов -> SND\snd.arc, один open вместо десяти.
* Музыка: трек лежит ОДНИМ файлом и читается порциями по странице —
  нарезка на куски больше не нужна.  Этот же путь понадобится финалу
  `won` (1,2 МБ), который в память целиком не влезает.
* Менеджер звука приведён к оригиналу: играющая музыка участвует в
  таблицах приоритета наравне с эффектами (seg000:1672 + data.h:433).
  Почти вся музыка НЕперебиваема, поэтому поверх неё эффект не стартует —
  ровно как в SDLPoP; раньше эффект её перебивал.
* ALLOCS 3000 -> 6000 (решение пользователя): сборка +20 с, резидент
  -161 байт.  Куча 87 -> 248 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:13:25 +03:00

156 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""pop_pack_music.py — музыка PoP в сырой поток для CBL.
Источник — готовые записи DOS-версии (applications/PoP/PoP1_DOS_music,
формат flac). Оцифровки музыки в самой игре НЕТ: в digisnd*.dat лежат
только эффекты, а музыка хранится как MIDI под Adlib. Поэтому здесь мы
не «конвертируем ресурс игры», а укладываем готовую запись в формат,
который наше железо играет без единого преобразования в рантайме.
ФОРМАТ — тот же, что у эффектов (см. pop_pack_sound.py и sound_plan.md):
8 бит БЕЗЗНАКОВЫЙ моно на 10 937,5 Гц — ровно CBL_FREQ_10K9. Частоту
берём не 11 000, а частоту железа: тогда CBL открывается ОДИН раз за
запуск и не переключается ни между эффектами, ни при переходе на музыку.
ОДИН ФАЙЛ НА ТРЕК. Резать его на куски не нужно: читаем порциями по
16 КБ из ОДНОГО открытого файла (`bank_read_page` + `lseek`), и «дорогой»
open платится раз на трек, а не на каждую страницу — замер 2026-08-25:
open 51,4 мс против 32,6 мс на чтение 16 КБ. Этот же путь работает для
финального `won` (1,2 МБ), который в память целиком не помещается и
обязан подгружаться по ходу.
Хвост дополняется тишиной (0x80) до кратности 128: насос отдаёт CBL
блоками по 128 байт, и остаток «не полного блока» ему пришлось бы
досчитывать в прерывании.
Длина трека уходит в pop_music_tbl.h блоками по 128 байт, а не байтами:
самый длинный трек (won, 115 с) — это 1,2 МБ, что не влезает в uint16, а
в блоках даёт 9855. 32-битной арифметики на Z80 избегаем (см. memory
avoid_32bit_arith_z80).
"""
import subprocess
import sys
from pathlib import Path
POP = Path(__file__).resolve().parent.parent # applications/PoP
SRC_DIR = POP / "PoP1_DOS_music" / "flac"
OUT_DIR = POP / "poc" / "res" / "music"
OUT_H = POP / "roomtest" / "pop_music_tbl.h"
CBL_RATE = 10937.5 # CBL_FREQ_10K9 — частота железа, не 11 000
PAGE = 16384 # EMM-страница
BLOCK = 128 # порция насоса CBL
SILENCE = 0x80 # центр беззнакового 8-битного сэмпла
# id — нумерация звуков ОРИГИНАЛА (SDLPoP data/music/names.txt). Держим её,
# чтобы игровой код просил трек тем же номером, каким оригинал зовёт
# play_sound, и таблицу можно было сверить глазами.
TRACKS = {
24: "regular_death",
25: "find_mirror_and_find_shadow",
26: "embrace",
27: "cutscene_before_2_4_6_12",
28: "fight_death",
29: "meet_Jaffar",
30: "life_potion",
32: "end_level_4_and_killed_by_shadow",
33: "potion",
35: "cutscene_before_8_9",
36: "out_of_time",
37: "guard_death_and_obtaining_the_sword",
39: "floating",
40: "cutscene_not_much_time_left",
41: "story_5_level_end_and_merge_with_shadow",
43: "killed_Jaffar",
50: "story_2_princess",
52: "story_4_Jaffar_leaves",
53: "story_3_Jaffar_enters",
54: "intro_theme",
55: "story_1_absence",
56: "won",
}
def decode(path: Path) -> bytes:
"""flac -> сырой u8 моно на частоте CBL."""
out = subprocess.run(
["ffmpeg", "-v", "error", "-i", str(path),
"-ac", "1", "-ar", str(int(CBL_RATE)), "-f", "u8", "-"],
capture_output=True, check=True)
return out.stdout
def pack(track_id: int, name: str) -> tuple[int, int]:
src = SRC_DIR / f"{name}.flac"
if not src.exists():
raise SystemExit(f"нет источника: {src}")
data = bytearray(decode(src))
# Добить до кратности блока насоса — хвост тишиной, а не обрезкой:
# обрезать значило бы щёлкнуть на последнем сэмпле.
tail = len(data) % BLOCK
if tail:
data += bytes([SILENCE]) * (BLOCK - tail)
chunks = (len(data) + PAGE - 1) // PAGE
OUT_DIR.mkdir(parents=True, exist_ok=True)
(OUT_DIR / f"m{track_id:02d}.bin").write_bytes(bytes(data))
return chunks, len(data) // BLOCK
def main() -> None:
wanted = [int(a) for a in sys.argv[1:]] or sorted(TRACKS)
rows = []
total = 0
for tid in wanted:
if tid not in TRACKS:
raise SystemExit(f"неизвестный трек {tid}")
chunks, blocks = pack(tid, TRACKS[tid])
rows.append((tid, TRACKS[tid], chunks, blocks))
total += blocks * BLOCK
print(f" {tid:2d} {TRACKS[tid]:<40} {chunks:2d} стр, "
f"{blocks * BLOCK / 1024:7.1f} КБ, {blocks * BLOCK / CBL_RATE:5.1f} с")
max_id = max(t for t, *_ in rows)
lines = [
"/*",
" * pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.",
" * Источник: applications/PoP/PoP1_DOS_music/flac (записи DOS-версии).",
" * Генератор: toolchain/pop_pack_music.py. Руками не править.",
" *",
" * Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —",
" * сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека",
" * в порциях насоса по 128 байт (в байтах не влезло бы в uint16).",
" */",
"#ifndef POP_MUSIC_TBL_H",
"#define POP_MUSIC_TBL_H",
"",
"#include <stdint.h>",
"",
f"#define POP_MUS_COUNT {max_id + 1}",
f"#define POP_MUS_BLOCK {BLOCK}",
f"#define POP_MUS_MAX_PAGES {max(c for _, _, c, _ in rows)}",
"",
"typedef struct {",
" uint8_t pages; /* 0 — трека нет */",
" uint16_t blocks; /* длина в порциях по 128 байт */",
"} pop_mus_ent_t;",
"",
"static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {",
]
by_id = {t: (c, b, n) for t, n, c, b in rows}
for i in range(max_id + 1):
if i in by_id:
c, b, n = by_id[i]
lines.append(f" {{ {c:2d}, {b:5d} }}, /* {i:2d} {n} */")
else:
lines.append(f" {{ 0, 0 }}, /* {i:2d} */")
lines += ["};", "", "#endif /* POP_MUSIC_TBL_H */", ""]
OUT_H.write_text("\n".join(lines))
print(f"итого {total / 1024:.0f} КБ в {OUT_DIR}")
print(f"каталог: {OUT_H}")
if __name__ == "__main__":
main()