Files
Sprinter-SDCC/applications/SprPoP/tools/paths.py
T
snark13 81e8ed4676 SprPoP: pop_quiet.py — тихая копия готовых наборов звука и музыки
Берёт то, что уже лежит в assets/packed, делает тише и кладёт в dist/ в
раскладке диска, перезаписывая прежнее.  Оригиналы не трогаются, поэтому
скрипт идемпотентен: сколько раз ни запусти, громкость упадёт один раз.

Формула — та, что предложил пользователь: PCM у нас 8 бит без знака с
тишиной 0x80, значит вдвое тише это out = in/2 + 0x40 (беззнаковый сдвиг).
Таблица на 256 значений строится floor-делением отклонения от центра, что
при gain = 1/2 совпадает с этой формулой байт в байт — то есть результат
можно сверять с реализацией на Z80 (srl a / add a,#0x40).  Тишина остаётся
тишиной при любом gain, клиппинга нет по построению.

Пересчитываются: MUS/m*.bin целиком (чистый PCM) и ТОЛЬКО тела записей
PBA1 в SND/snd.arc — заголовок и выравнивающие хвосты остаются как есть,
там нули упаковщика, и пересчёт превратил бы их в 0x40.  Индексы
копируются без изменений (длины те же), но с проверкой магии.

Прогон на текущих наборах: 10 записей архива и 22 трека, пик 128 -> 64,
размеры байт в байт прежние, заголовок PBA1 не тронут.

Тест закрепляет формулу и неприкосновенность служебных байт; make
test-tools — 45 тестов.  Раскладка dist/ добавлена в tools/paths.py, по
правилу «пути знает один файл».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 14:16:07 +03:00

151 lines
7.5 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.
"""paths.py — единственное место, где инструменты SprPoP знают раскладку
каталогов приложения. Все упаковщики берут пути отсюда, поэтому папку
SprPoP можно перенести куда угодно: важно лишь, что рядом с tools/ лежат
assets/, gen/ и build/.
Раскладка:
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, записи музыки и,
необязательно, MSDOS). В репозиторий не идут —
копирайт; выкачивает их tools/fetch_orig.py
(`make fetch`), см. assets/orig/README.md.
build/atl/<set>/ промежуточный выхлоп упаковщиков: отдельные атласы,
из которых потом склеиваются архивы. Пересоздаётся,
в репозиторий не идёт.
assets/packed/ ровно то, что ложится на диск игры, уже в раскладке
каталогов диска (BG/, KID/, ...). В репозитории —
иначе из чистого клона без assets/orig/ ничего не
собрать.
gen/ генерируемые C-заголовки. Тоже в репозитории и по
той же причине: это такой же производный артефакт,
как .arc, и без него не скомпилировать src/.
"""
from pathlib import Path
TOOLS = Path(__file__).resolve().parent
APP = TOOLS.parent
# --- вход: оригинальные данные -------------------------------------------
ORIG = APP / "assets" / "orig"
SDLPOP = ORIG / "SDLPoP"
SDLPOP_DATA = SDLPOP / "data"
SDLPOP_SRC = SDLPOP / "src"
MSDOS = ORIG / "MSDOS"
MUSIC_ROOT = ORIG / "PoP1_DOS_music"
# Записи DOS-версии лежат в четырёх вариантах. Имена треков во всех наборах
# одинаковые, различаются каталог и расширение; ogg_MT-32 — та же музыка в
# исполнении Roland MT-32, а не другой кодек. Ключ = имя формата в Makefile.
MUSIC_FORMATS = {
"flac": ("flac", ".flac"),
"mp3": ("mp3", ".mp3"),
"ogg": ("ogg", ".ogg"),
"mt32": ("ogg_MT-32", ".ogg"),
}
def music_src(fmt="flac"):
"""(каталог, расширение) исходников музыки для формата."""
if fmt not in MUSIC_FORMATS:
raise SystemExit(
"неизвестный формат музыки %r; есть: %s"
% (fmt, ", ".join(sorted(MUSIC_FORMATS))))
sub, ext = MUSIC_FORMATS[fmt]
return MUSIC_ROOT / sub, ext
# --- источники звуковых эффектов ------------------------------------------
# Контейнеры digisnd1..3 есть в ДВУХ поставках, и они не одинаковы: в SDLPoP
# лежит версия из репозитория (её и качает fetch_orig.py), в MSDOS/ — из
# оригинального дистрибутива 1.0, если пользователь его себе положил.
# ПО УМОЛЧАНИЮ БЕРЁМ SDLPoP: сборка обязана работать без MSDOS вообще.
# Ключ = значение SND_SRC в Makefile; регистр имён в поставках разный.
SND_SOURCES = {
"sdlpop": (SDLPOP_DATA, "DIGISND%d.DAT"),
"msdos": (MSDOS, "digisnd%d.dat"),
}
# Ноты PC-СПИКЕРА и МЕЛОДИИ — два других набора того же движка. Нужны нам
# по разным причинам: мелодии мы играем отдельно (MUS/), а из нот спикера
# добираем те номера, которых нет НИ в оцифровке, НИ среди мелодий — сейчас
# это ровно звук 38 (мигание надписи «Press Button»). В поставке SDLPoP
# наборы лежат распакованными каталогами res1XXXX.bin, в дистрибутиве DOS —
# контейнерами .DAT; поддерживаем оба вида.
SND_MIDI = {
"sdlpop": (SDLPOP_DATA, "MIDISND%d.DAT", 2),
"msdos": (MSDOS, "midisnd%d.dat", 2),
}
SND_SPEAKER = {
"sdlpop": (SDLPOP_DATA, "IBM_SND%d", 2),
"msdos": (MSDOS, "ibm_snd%d.dat", 2),
}
def snd_extra(kind, name="sdlpop"):
"""(каталог, шаблон, сколько контейнеров) для набора нот или мелодий.
kind — "midi" или "speaker". Набора может не быть вовсе (у пользователя
нет MSDOS, поставка неполная) — это НЕ ошибка: вызывающий просто
пропускает добор. Возвращает None, если источник неизвестен.
"""
table = {"midi": SND_MIDI, "speaker": SND_SPEAKER}.get(kind)
if table is None:
raise SystemExit("неизвестный набор %r; есть: midi, speaker" % kind)
return table.get(name)
def snd_src(name="sdlpop"):
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
if name not in SND_SOURCES:
raise SystemExit(
"неизвестный источник звука %r; есть: %s"
% (name, ", ".join(sorted(SND_SOURCES))))
return SND_SOURCES[name]
def find_ci(directory, name):
"""Путь к файлу без оглядки на регистр (FAT-имена приезжают по-разному).
Возвращает None, если файла нет. Точное совпадение проверяется первым,
чтобы на case-insensitive ФС не читать каталог зря.
"""
p = directory / name
if p.exists():
return p
if not directory.is_dir():
return None
low = name.lower()
for e in directory.iterdir():
if e.name.lower() == low:
return e
return None
# --- выход ----------------------------------------------------------------
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
PACKED = APP / "assets" / "packed" # раскладка диска игры
GEN = APP / "gen" # генерируемые заголовки
DIST = APP / "dist" # ручное содержимое дистрибутива (README и
# производные наборы, напр. тихий звук)
def atl(name):
"""Каталог промежуточного выхлопа набора; создаётся при обращении."""
p = ATL / name
p.mkdir(parents=True, exist_ok=True)
return p
def packed(name):
"""Каталог диска игры (BG, KID, ...); создаётся при обращении."""
p = PACKED / name
p.mkdir(parents=True, exist_ok=True)
return p
def dist(name):
"""Каталог внутри dist/ в раскладке диска; создаётся при обращении."""
p = DIST / name
p.mkdir(parents=True, exist_ok=True)
return p