Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 |
Binary file not shown.
Binary file not shown.
@@ -17,6 +17,7 @@
|
|||||||
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
|
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
|
||||||
#include "pop_music.h"
|
#include "pop_music.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
|
#include "_pop_sfx.h" /* pop_snd_ok — открыт ли вывод (см. pop_music_busy) */
|
||||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||||
#include "_pop_file.h"
|
#include "_pop_file.h"
|
||||||
|
|
||||||
@@ -454,7 +455,15 @@ void pop_music_service(void) __banked
|
|||||||
|
|
||||||
uint8_t pop_music_busy(void) __banked
|
uint8_t pop_music_busy(void) __banked
|
||||||
{
|
{
|
||||||
return (uint8_t)(pop_mus_left != 0);
|
/* «ЗВУЧИТ», А НЕ «ЕСТЬ КУРСОР». Курсор трека двигает насос CBL, и пока
|
||||||
|
* вывод закрыт (pop_sfx_pause на загрузке уровня, Ctrl+S, показ заставки
|
||||||
|
* после Restart Game) pop_mus_left не убывает НИКОГДА. Вызывающий,
|
||||||
|
* который ждёт конца мелодии, в этом состоянии висит вечно — ровно так
|
||||||
|
* заставка после Restart Game вставала на экране Prince of Persia
|
||||||
|
* (поймано пользователем 2026-09-02). Корень того случая был в другом
|
||||||
|
* (CBL не открывался заново, см. pop_boot_sound), но ждать неиграющую
|
||||||
|
* музыку нельзя в принципе, поэтому признак спрашивает и про вывод. */
|
||||||
|
return (uint8_t)(pop_snd_ok && pop_mus_left != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
|
||||||
#include "pop_shadow.h" /* запечённый набор Тени */
|
#include "pop_shadow.h" /* запечённый набор Тени */
|
||||||
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
#include "pop_sfx.h" /* звуковые эффекты через CBL */
|
||||||
|
#include <cbl.h> /* cbl_shutdown — погасить железо звука на выходе */
|
||||||
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
#include "pop_music.h" /* pop_music_busy/free — ждём трек заставки */
|
||||||
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
|
||||||
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
|
||||||
@@ -1205,7 +1206,7 @@ int pop_boot(void) __banked
|
|||||||
* полсекунды, и лучше, чтобы она прошла под картинкой. */
|
* полсекунды, и лучше, чтобы она прошла под картинкой. */
|
||||||
int pop_boot_sound(void) __banked
|
int pop_boot_sound(void) __banked
|
||||||
{
|
{
|
||||||
if (boot_sound_done) return 0;
|
if (!boot_sound_done) {
|
||||||
boot_sound_done = 1;
|
boot_sound_done = 1;
|
||||||
(void)pop_sfx_init_begin(); /* эффектов может и не быть — не фатально */
|
(void)pop_sfx_init_begin(); /* эффектов может и не быть — не фатально */
|
||||||
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
|
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
|
||||||
@@ -1213,6 +1214,15 @@ int pop_boot_sound(void) __banked
|
|||||||
* это необязательный ресурс, как и сами треки. */
|
* это необязательный ресурс, как и сами треки. */
|
||||||
(void)pop_music_index_load();
|
(void)pop_music_index_load();
|
||||||
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
|
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
|
||||||
|
}
|
||||||
|
/* ОТКРЫТИЕ CBL — ВНЕ РАЗОВОГО ГАРДА, и это не мелочь. Загрузка нужна
|
||||||
|
* один раз за запуск, а вот вывод к моменту следующего показа заставки
|
||||||
|
* почти наверняка закрыт: pop_title_show начинается с pop_sfx_pause, да
|
||||||
|
* и уровень оставляет CBL закрытым. Пока start стоял под флагом,
|
||||||
|
* Restart Game приводил к заставке БЕЗ музыки (поймано пользователем
|
||||||
|
* 2026-09-02), а заодно подвешивал ожидания «дослушать тему»: с
|
||||||
|
* выключенным насосом pop_mus_left не убывает, и pop_music_busy()
|
||||||
|
* истинен вечно. Идемпотентно и уважает Ctrl+S (pop_snd_want). */
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1481,4 +1491,14 @@ void pop_shutdown(void) __banked
|
|||||||
pop_guard_free();
|
pop_guard_free();
|
||||||
pop_kid_data_free();
|
pop_kid_data_free();
|
||||||
pop_level_free();
|
pop_level_free();
|
||||||
|
/* ЖЕЛЕЗО ЗВУКА ГАСИМ ПОСЛЕДНИМ ДЕЙСТВИЕМ, и pop_sfx_close выше для этого
|
||||||
|
* НЕ ДОСТАТОЧНО: он закрывает сессию, а bit7 порта CBL остаётся включён,
|
||||||
|
* пока его держит gfx_wait_vsync ради бита луча. Дальше main просто
|
||||||
|
* возвращается, а crt0 при возврате уходит прямо в ESTEX EXIT, минуя
|
||||||
|
* цепочку atexit (runtime/crt0_banked.s) — то есть vsync-ссылку снять
|
||||||
|
* уже некому. В результате мы уходили в шелл с включённым CBL, и он
|
||||||
|
* крутил свои 256 слотов: тихо до первой чужой записи в порт данных, а
|
||||||
|
* дальше она зацикливалась (поймано пользователем на железе 2026-09-02;
|
||||||
|
* в MAME не воспроизводится). */
|
||||||
|
cbl_shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ def find_ci(directory, name):
|
|||||||
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
||||||
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
||||||
GEN = APP / "gen" # генерируемые заголовки
|
GEN = APP / "gen" # генерируемые заголовки
|
||||||
|
DIST = APP / "dist" # ручное содержимое дистрибутива (README и
|
||||||
|
# производные наборы, напр. тихий звук)
|
||||||
|
|
||||||
|
|
||||||
def atl(name):
|
def atl(name):
|
||||||
@@ -139,3 +141,10 @@ def packed(name):
|
|||||||
p = PACKED / name
|
p = PACKED / name
|
||||||
p.mkdir(parents=True, exist_ok=True)
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def dist(name):
|
||||||
|
"""Каталог внутри dist/ в раскладке диска; создаётся при обращении."""
|
||||||
|
p = DIST / name
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|||||||
Executable
+163
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""pop_quiet.py — тише в N раз: пережать ГОТОВЫЕ наборы звука и музыки.
|
||||||
|
|
||||||
|
Берёт то, что уже лежит в assets/packed (SND/snd.arc + MUS/m*.bin), делает
|
||||||
|
тише и кладёт в dist/ в той же раскладке диска, ПЕРЕЗАПИСЫВАЯ то, что там
|
||||||
|
было. Оригиналы в assets/packed не трогаются никогда — значит скрипт
|
||||||
|
идемпотентен: сколько раз ни запусти, громкость упадёт ровно один раз.
|
||||||
|
|
||||||
|
ФОРМУЛА. PCM у нас 8 бит БЕЗ ЗНАКА, тишина = 0x80 (см. cbl.h и
|
||||||
|
docs/sound_plan.md), то есть байт — это знаковое отклонение от 0x80.
|
||||||
|
Тише вдвое:
|
||||||
|
|
||||||
|
out = 0x80 + (in - 0x80) / 2 = in / 2 + 0x40
|
||||||
|
|
||||||
|
Правая форма — та самая, что делается на Z80 двумя командами
|
||||||
|
(`srl a` / `add a,#0x40`), поэтому таблицу строим floor-делением: тогда
|
||||||
|
результат совпадает с ней БАЙТ В БАЙТ и его можно сверять с железной
|
||||||
|
реализацией. Тишина остаётся тишиной (0x80 -> 0x80) при любом gain, а
|
||||||
|
клиппинга не бывает по построению: |отклонение| только уменьшается.
|
||||||
|
|
||||||
|
ЧТО ИМЕННО ПЕРЕСЧИТЫВАЕТСЯ.
|
||||||
|
|
||||||
|
MUS/m*.bin — файл целиком: это чистый PCM без заголовка, загрузчик
|
||||||
|
читает его страницами с нулевого смещения (pop_music.c).
|
||||||
|
SND/snd.arc — ТОЛЬКО тела записей архива PBA1. Заголовок (512 Б) и
|
||||||
|
выравнивающий хвост между записями остаются как есть:
|
||||||
|
там нули упаковщика, а не звук, и пересчёт превратил бы
|
||||||
|
их в 0x40 — мусор на месте служебных байт.
|
||||||
|
*.idx — копируются без изменений: длины и смещения не поменялись,
|
||||||
|
меняются только сами отсчёты. Магию всё же проверяем,
|
||||||
|
чтобы молча не скопировать файл чужого формата.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
python3 tools/pop_quiet.py # вдвое тише, packed -> dist
|
||||||
|
python3 tools/pop_quiet.py --gain 0.25 # вчетверо тише
|
||||||
|
python3 tools/pop_quiet.py --src DIR --out DIR
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import paths
|
||||||
|
|
||||||
|
SILENCE = 0x80 # центр 8-битного беззнакового PCM
|
||||||
|
ARC_MAGIC = b"PBA1" # архив ресурсов, см. src/pop_arc.h
|
||||||
|
ARC_HEADER = 512
|
||||||
|
ARC_SECTOR = 512
|
||||||
|
SND_IDX_MAGIC = b"PSI1"
|
||||||
|
MUS_IDX_MAGIC = b"PMI1"
|
||||||
|
|
||||||
|
|
||||||
|
def gain_table(gain: float) -> bytes:
|
||||||
|
"""Таблица перекодировки байта: 256 значений, готовые для bytes.translate.
|
||||||
|
|
||||||
|
floor, а не округление к ближайшему: при gain = 1/2 это ровно
|
||||||
|
`in >> 1 + 0x40`, то есть побайтово то же, что сделал бы Z80.
|
||||||
|
"""
|
||||||
|
if not 0.0 <= gain <= 1.0:
|
||||||
|
raise SystemExit("gain обязан лежать в [0, 1]; громче делать нечем — "
|
||||||
|
"запас по амплитуде в исходнике уже выбран")
|
||||||
|
out = bytearray(256)
|
||||||
|
for v in range(256):
|
||||||
|
dev = v - SILENCE # знаковое отклонение
|
||||||
|
out[v] = max(0, min(255, SILENCE + _floor_div(dev, gain)))
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _floor_div(dev: int, gain: float) -> int:
|
||||||
|
"""dev * gain, округлённое ВНИЗ (к минус бесконечности)."""
|
||||||
|
scaled = dev * gain
|
||||||
|
i = int(scaled)
|
||||||
|
return i - 1 if scaled < i else i # int() режет к нулю
|
||||||
|
|
||||||
|
|
||||||
|
def quiet_raw(src: Path, dst: Path, table: bytes) -> int:
|
||||||
|
"""Файл целиком — PCM (музыкальный трек). Возвращает размер."""
|
||||||
|
data = src.read_bytes()
|
||||||
|
dst.write_bytes(data.translate(table))
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
|
||||||
|
def quiet_arc(src: Path, dst: Path, table: bytes) -> int:
|
||||||
|
"""Архив PBA1: пересчитываем только тела записей."""
|
||||||
|
raw = bytearray(src.read_bytes())
|
||||||
|
if raw[0:4] != ARC_MAGIC:
|
||||||
|
raise SystemExit(f"{src}: не {ARC_MAGIC.decode()} — это не наш архив")
|
||||||
|
count = raw[4]
|
||||||
|
if not 1 <= count <= (ARC_HEADER - 8) // 4:
|
||||||
|
raise SystemExit(f"{src}: count = {count}, вне 1..126")
|
||||||
|
for i in range(count):
|
||||||
|
off_sec, size = struct.unpack_from("<HH", raw, 8 + i * 4)
|
||||||
|
off = off_sec * ARC_SECTOR
|
||||||
|
if off + size > len(raw):
|
||||||
|
raise SystemExit(f"{src}: запись {i} выходит за файл "
|
||||||
|
f"({off} + {size} > {len(raw)})")
|
||||||
|
raw[off:off + size] = bytes(raw[off:off + size]).translate(table)
|
||||||
|
dst.write_bytes(bytes(raw))
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def copy_idx(src: Path, dst: Path, magic: bytes) -> None:
|
||||||
|
"""Индекс переносим как есть — но убеждаемся, что он наш."""
|
||||||
|
head = src.read_bytes()[:4]
|
||||||
|
if head != magic:
|
||||||
|
raise SystemExit(f"{src}: магия {head!r}, ожидалась {magic!r}")
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def peak(data: bytes) -> int:
|
||||||
|
"""Максимальное отклонение от тишины — для отчёта «стало тише»."""
|
||||||
|
return max(abs(b - SILENCE) for b in data) if data else 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
ap.add_argument("--gain", type=float, default=0.5,
|
||||||
|
help="множитель амплитуды (по умолчанию 0.5 — вдвое тише)")
|
||||||
|
ap.add_argument("--src", type=Path, default=paths.PACKED,
|
||||||
|
help="откуда брать готовые наборы (умолчание assets/packed)")
|
||||||
|
ap.add_argument("--out", type=Path, default=paths.DIST,
|
||||||
|
help="куда класть (умолчание dist/)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
table = gain_table(args.gain)
|
||||||
|
src_snd, src_mus = args.src / "SND", args.src / "MUS"
|
||||||
|
out_snd, out_mus = args.out / "SND", args.out / "MUS"
|
||||||
|
if not src_snd.is_dir() or not src_mus.is_dir():
|
||||||
|
raise SystemExit(f"{args.src}: нет SND/ или MUS/ — сначала "
|
||||||
|
"`make resources`")
|
||||||
|
out_snd.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_mus.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"тише в {1 / args.gain:g} раз(а): {args.src} -> {args.out}")
|
||||||
|
|
||||||
|
arc = src_snd / "snd.arc"
|
||||||
|
before = peak(arc.read_bytes()[ARC_HEADER:])
|
||||||
|
n = quiet_arc(arc, out_snd / "snd.arc", table)
|
||||||
|
after = peak((out_snd / "snd.arc").read_bytes()[ARC_HEADER:])
|
||||||
|
copy_idx(src_snd / "snd.idx", out_snd / "snd.idx", SND_IDX_MAGIC)
|
||||||
|
print(f" SND/snd.arc {n} записей, пик {before} -> {after}")
|
||||||
|
|
||||||
|
tracks = sorted(src_mus.glob("m*.bin"))
|
||||||
|
if not tracks:
|
||||||
|
raise SystemExit(f"{src_mus}: нет ни одного m*.bin")
|
||||||
|
total = 0
|
||||||
|
worst_before = worst_after = 0
|
||||||
|
for t in tracks:
|
||||||
|
total += quiet_raw(t, out_mus / t.name, table)
|
||||||
|
worst_before = max(worst_before, peak(t.read_bytes()))
|
||||||
|
worst_after = max(worst_after, peak((out_mus / t.name).read_bytes()))
|
||||||
|
copy_idx(src_mus / "mus.idx", out_mus / "mus.idx", MUS_IDX_MAGIC)
|
||||||
|
print(f" MUS/ {len(tracks)} треков, {total // 1024} КБ, "
|
||||||
|
f"пик {worst_before} -> {worst_after}")
|
||||||
|
print("индексы (snd.idx, mus.idx) скопированы без изменений — длины те же")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""test_pop_quiet — правило громкости и неприкосновенность служебных байт.
|
||||||
|
|
||||||
|
Скрипт трогает данные, которые потом играет насос на Z80, поэтому тест
|
||||||
|
закрепляет ровно две вещи: формулу (она обязана совпадать с беззнаковым
|
||||||
|
`A/2 + 0x40`, чтобы результат можно было сверять с железной реализацией) и
|
||||||
|
то, что заголовок архива с выравнивающим хвостом остаются как были.
|
||||||
|
|
||||||
|
Запуск: make test-tools (либо python3 -m unittest discover tools/tests)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import pop_quiet as X
|
||||||
|
|
||||||
|
|
||||||
|
class Formula(unittest.TestCase):
|
||||||
|
"""Таблица громкости: то же, что сделал бы Z80."""
|
||||||
|
|
||||||
|
def test_half_is_shift_plus_40(self):
|
||||||
|
t = X.gain_table(0.5)
|
||||||
|
for v in range(256):
|
||||||
|
self.assertEqual(t[v], (v >> 1) + 0x40, f"байт {v:#04x}")
|
||||||
|
|
||||||
|
def test_silence_stays_silence(self):
|
||||||
|
for gain in (0.5, 0.25, 0.1, 1.0):
|
||||||
|
self.assertEqual(X.gain_table(gain)[X.SILENCE], X.SILENCE)
|
||||||
|
|
||||||
|
def test_no_clipping_and_range_shrinks(self):
|
||||||
|
t = X.gain_table(0.5)
|
||||||
|
self.assertEqual(t[0x00], 0x40)
|
||||||
|
self.assertEqual(t[0xFF], 0xBF)
|
||||||
|
for v in range(256):
|
||||||
|
self.assertLessEqual(abs(t[v] - X.SILENCE), abs(v - X.SILENCE))
|
||||||
|
|
||||||
|
def test_gain_one_is_identity(self):
|
||||||
|
self.assertEqual(X.gain_table(1.0), bytes(range(256)))
|
||||||
|
|
||||||
|
def test_gain_out_of_range_rejected(self):
|
||||||
|
for bad in (-0.1, 1.5):
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
X.gain_table(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def make_arc(bodies):
|
||||||
|
"""Собрать PBA1 ровно так, как это делает tools/pop_pack_arc.py."""
|
||||||
|
raw = bytearray(X.ARC_HEADER)
|
||||||
|
raw[0:4] = X.ARC_MAGIC
|
||||||
|
raw[4] = len(bodies)
|
||||||
|
blob = bytearray()
|
||||||
|
for i, body in enumerate(bodies):
|
||||||
|
off = X.ARC_HEADER + len(blob)
|
||||||
|
struct.pack_into("<HH", raw, 8 + i * 4, off // X.ARC_SECTOR, len(body))
|
||||||
|
blob += body
|
||||||
|
blob += bytes(-len(blob) % X.ARC_SECTOR) # хвост выравнивания — нули
|
||||||
|
return bytes(raw + blob)
|
||||||
|
|
||||||
|
|
||||||
|
class Archive(unittest.TestCase):
|
||||||
|
"""PBA1: тела пересчитаны, служебные байты — нет."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.dir = Path(tempfile.mkdtemp())
|
||||||
|
self.bodies = [bytes([0x80, 0xFF, 0x00, 0x81, 0x7F]), bytes([0xC0] * 3)]
|
||||||
|
self.src = self.dir / "in.arc"
|
||||||
|
self.dst = self.dir / "out.arc"
|
||||||
|
self.src.write_bytes(make_arc(self.bodies))
|
||||||
|
|
||||||
|
def test_bodies_scaled(self):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
raw = self.dst.read_bytes()
|
||||||
|
off = X.ARC_HEADER
|
||||||
|
got = raw[off:off + len(self.bodies[0])]
|
||||||
|
self.assertEqual(got, bytes([0x80, 0xBF, 0x40, 0x80, 0x7F]))
|
||||||
|
|
||||||
|
def test_header_and_padding_untouched(self):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
src, dst = self.src.read_bytes(), self.dst.read_bytes()
|
||||||
|
self.assertEqual(len(src), len(dst))
|
||||||
|
self.assertEqual(src[:X.ARC_HEADER], dst[:X.ARC_HEADER])
|
||||||
|
pad_at = X.ARC_HEADER + len(self.bodies[0])
|
||||||
|
pad_len = -len(self.bodies[0]) % X.ARC_SECTOR
|
||||||
|
self.assertEqual(dst[pad_at:pad_at + pad_len], bytes(pad_len))
|
||||||
|
|
||||||
|
def test_foreign_magic_rejected(self):
|
||||||
|
self.src.write_bytes(b"NOPE" + bytes(X.ARC_HEADER))
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
X.quiet_arc(self.src, self.dst, X.gain_table(0.5))
|
||||||
|
|
||||||
|
|
||||||
|
class Track(unittest.TestCase):
|
||||||
|
"""Музыкальный трек — чистый PCM, пересчитывается целиком."""
|
||||||
|
|
||||||
|
def test_whole_file(self):
|
||||||
|
d = Path(tempfile.mkdtemp())
|
||||||
|
src, dst = d / "m54.bin", d / "out.bin"
|
||||||
|
src.write_bytes(bytes([0x00, 0x80, 0xFF] * 4))
|
||||||
|
n = X.quiet_raw(src, dst, X.gain_table(0.5))
|
||||||
|
self.assertEqual(n, 12)
|
||||||
|
self.assertEqual(dst.read_bytes(), bytes([0x40, 0x80, 0xBF] * 4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+45
-45
@@ -1,67 +1,67 @@
|
|||||||
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
||||||
accfill 3783
|
accfill 3797
|
||||||
accop 7067
|
accop 7067
|
||||||
argv 3431
|
argv 3445
|
||||||
assrtest 3847
|
assrtest 3861
|
||||||
atlas 9193
|
atlas 9193
|
||||||
attrprob 4087
|
attrprob 4101
|
||||||
banked 1056
|
banked 1070
|
||||||
bankedbg 1067
|
bankedbg 1081
|
||||||
banklocl 4683
|
banklocl 4697
|
||||||
banktest 3767
|
banktest 3781
|
||||||
bgi_img 8677
|
bgi_img 8691
|
||||||
bgitest 3748
|
bgitest 3762
|
||||||
bios_text 4461
|
bios_text 4475
|
||||||
blitperf 5860
|
blitperf 5860
|
||||||
blitw 4939
|
blitw 4939
|
||||||
cat 913
|
cat 927
|
||||||
cblstream 5862
|
cblstream 5944
|
||||||
cbltest 6008
|
cbltest 6090
|
||||||
cblwav 6101
|
cblwav 6183
|
||||||
conio 4605
|
conio 4619
|
||||||
conio2 3929
|
conio2 3943
|
||||||
convbench 3508
|
convbench 3508
|
||||||
dec_test 860
|
dec_test 874
|
||||||
errno 5966
|
errno 5980
|
||||||
fbench 8085
|
fbench 8149
|
||||||
fdmax 6023
|
fdmax 6087
|
||||||
filetest 10547
|
filetest 10611
|
||||||
fpsdiv 4870
|
fpsdiv 4870
|
||||||
gets 509
|
gets 523
|
||||||
gfx_dbuf 5142
|
gfx_dbuf 5207
|
||||||
gfx_demo 4114
|
gfx_demo 4128
|
||||||
gfxbanks 6044
|
gfxbanks 6044
|
||||||
hello 4167
|
hello 4181
|
||||||
hello2 4244
|
hello2 4258
|
||||||
irqtest 6690
|
irqtest 6754
|
||||||
kbdpoll 1282
|
kbdpoll 1282
|
||||||
kbdraw 4911
|
kbdraw 4911
|
||||||
ls 4835
|
ls 4849
|
||||||
malloc 4461
|
malloc 4475
|
||||||
mem_test 4555
|
mem_test 4569
|
||||||
mouse 4382
|
mouse 4396
|
||||||
openenv 6124
|
openenv 6138
|
||||||
pageflip 6334
|
pageflip 6334
|
||||||
palfile 5406
|
palfile 5406
|
||||||
ptime 5744
|
ptime 5758
|
||||||
rt_test 5197
|
rt_test 5216
|
||||||
scroll 6744
|
scroll 6744
|
||||||
seek 4188
|
seek 4202
|
||||||
simple 955
|
simple 969
|
||||||
solidt 11531
|
solidt 11595
|
||||||
spranim 9431
|
spranim 9431
|
||||||
spriteclip 4119
|
spriteclip 4119
|
||||||
sprites 6791
|
sprites 6791
|
||||||
stattest 7549
|
stattest 7563
|
||||||
stdlib 6643
|
stdlib 6657
|
||||||
stest2 3644
|
stest2 3658
|
||||||
strtest 1340
|
strtest 1354
|
||||||
text_palette 5024
|
text_palette 5038
|
||||||
timedir 5454
|
timedir 5468
|
||||||
w0page 8831
|
w0page 8831
|
||||||
w3bgfx 5243
|
w3bgfx 5243
|
||||||
w3big 3673
|
w3big 3673
|
||||||
w3huge 3700
|
w3huge 3700
|
||||||
w3probe 3556
|
w3probe 3556
|
||||||
w3tiny 3471
|
w3tiny 3471
|
||||||
winrest 4459
|
winrest 4473
|
||||||
|
|||||||
|
@@ -61,7 +61,12 @@ int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
|||||||
static uint8_t atexit_armed;
|
static uint8_t atexit_armed;
|
||||||
if (!atexit_armed) {
|
if (!atexit_armed) {
|
||||||
atexit_armed = 1;
|
atexit_armed = 1;
|
||||||
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
|
/* ИМЕННО shutdown, а не close: close оставляет bit7 включённым,
|
||||||
|
* пока его держит gfx_wait_vsync, и железо крутит буфер уже под
|
||||||
|
* шеллом. Подстраховка неполная — цепочку atexit разматывает
|
||||||
|
* только exit(), а crt0 при возврате из main уходит прямо в ESTEX
|
||||||
|
* EXIT, — поэтому программа обязана звать cbl_shutdown() сама. */
|
||||||
|
atexit(cbl_shutdown); /* CBL обязан умолкнуть до шелла */
|
||||||
}
|
}
|
||||||
|
|
||||||
IRQ_DISABLE();
|
IRQ_DISABLE();
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* cbl_shutdown — ЖЁСТКО погасить звук перед возвратом в шелл.
|
||||||
|
*
|
||||||
|
* ЧЕМ ОТЛИЧАЕТСЯ ОТ cbl_close(). Тот закрывает СЕССИЮ: снимает насос и
|
||||||
|
* пересчитывает порт по текущим держателям. А держатель обычно есть —
|
||||||
|
* gfx_wait_vsync() занимает bit7 навсегда ради бита луча (см. _cbl_port.c),
|
||||||
|
* — и порт остаётся включённым в "немом" режиме. Для работающей программы
|
||||||
|
* это правильно, но при выходе означает, что железо продолжает крутить свои
|
||||||
|
* 256 слотов уже под шеллом: тихо ровно до первой чужой записи в порт
|
||||||
|
* данных, а дальше эта запись зацикливается навсегда.
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ НЕЛЬЗЯ ПОЛОЖИТЬСЯ НА atexit. Цепочку atexit разматывает только
|
||||||
|
* exit() (libc/sys/exit.c), а crt0 при ВОЗВРАТЕ ИЗ main уходит прямо в
|
||||||
|
* ESTEX EXIT (runtime/crt0_banked.s) — ни один хендлер не выполняется.
|
||||||
|
* Значит и снятие vsync-ссылки, и cbl_close из atexit при обычном
|
||||||
|
* `return 0;` из main не происходят. Поэтому выключение должно быть
|
||||||
|
* ЯВНЫМ действием программы, а не следствием чужой уборки.
|
||||||
|
*
|
||||||
|
* Что делает: закрывает сессию (если была), обнуляет счётчик держателей —
|
||||||
|
* программа уходит, держать порт больше некому, — гасит bit7 и центрует
|
||||||
|
* ЦАП обычного COVOX (при выключенном CBL записи в порт данных идут прямо
|
||||||
|
* в него, см. _cbl_prime.c), чтобы вместо щелчка осталась ровная тишина.
|
||||||
|
*
|
||||||
|
* Идемпотентно; звать последним действием завершения.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "_cbl.h"
|
||||||
|
#include "../irq/_irq.h" /* IRQ_DISABLE/IRQ_ENABLE */
|
||||||
|
|
||||||
|
void cbl_shutdown(void)
|
||||||
|
{
|
||||||
|
cbl_close(); /* снять насос и отпустить IM2-таблицу */
|
||||||
|
IRQ_DISABLE();
|
||||||
|
_cbl_port_refs = 0; /* держателей больше нет: программа уходит */
|
||||||
|
_cbl_ctrl(0); /* bit7 = 0 — CBL выключен целиком */
|
||||||
|
_cbl_prime(0x80); /* ...и ЦАП COVOX в центре, без щелчка */
|
||||||
|
IRQ_ENABLE();
|
||||||
|
}
|
||||||
+13
-3
@@ -55,9 +55,14 @@
|
|||||||
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
||||||
* поведение не меняет.
|
* поведение не меняет.
|
||||||
*
|
*
|
||||||
* cbl_close() обязателен (atexit подстрахует) — иначе CBL продолжит
|
* cbl_close() обязателен — иначе CBL продолжит прерывать шелл после
|
||||||
* прерывать шелл после выхода; заодно освобождает буфер тишины, если
|
* выхода. НО ОН НЕ ГАСИТ ЖЕЛЕЗО ПОЛНОСТЬЮ: пока bit7 держит
|
||||||
* он был аллоцирован.
|
* gfx_wait_vsync() ради бита луча, порт остаётся включённым в "немом"
|
||||||
|
* режиме, и буфер крутится под шеллом. Перед возвратом из main звать
|
||||||
|
* cbl_shutdown() — он выключает всё и центрует ЦАП.
|
||||||
|
*
|
||||||
|
* На atexit тут полагаться НЕЛЬЗЯ: цепочку разматывает только exit(), а
|
||||||
|
* crt0 при возврате из main уходит прямо в ESTEX EXIT.
|
||||||
*
|
*
|
||||||
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
|
* Требования как у <irq.h>: код/данные в W2 (tiny/big).
|
||||||
*/
|
*/
|
||||||
@@ -67,6 +72,11 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Выключить звук ПОЛНОСТЬЮ перед завершением программы: закрыть сессию,
|
||||||
|
* снять bit7 независимо от держателей и оставить ЦАП в центре. Звать
|
||||||
|
* последним действием завершения; идемпотентно. */
|
||||||
|
void cbl_shutdown(void);
|
||||||
|
|
||||||
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
|
/* Коды частоты дискретизации (bits 3..0 порта 0x4E; из форума
|
||||||
* Sprinter Team; коды 0/1 — legacy, не использовать). */
|
* Sprinter Team; коды 0/1 — legacy, не использовать). */
|
||||||
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
|
#define CBL_FREQ_7K8 0x8 /* 7.8125 кГц */
|
||||||
|
|||||||
@@ -10,4 +10,11 @@
|
|||||||
extern void (*_atexit_stack[ATEXIT_MAX])(void);
|
extern void (*_atexit_stack[ATEXIT_MAX])(void);
|
||||||
extern int _atexit_top;
|
extern int _atexit_top;
|
||||||
|
|
||||||
|
/* Разматыватель цепочки (LIFO) — общее тело exit() и возврата из main. */
|
||||||
|
void _atexit_run(void);
|
||||||
|
|
||||||
|
/* Указатель на него для crt0; ставит atexit() при первой регистрации.
|
||||||
|
* Подробности и мотивация косвенности — в шапке _atexit_hook.c. */
|
||||||
|
extern void (*_atexit_hook)(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* _atexit_hook — указатель на разматыватель цепочки atexit; его дёргает
|
||||||
|
* crt0 при ВОЗВРАТЕ ИЗ main.
|
||||||
|
*
|
||||||
|
* ЗАЧЕМ КОСВЕННОСТЬ. По стандарту C возврат из main эквивалентен
|
||||||
|
* exit(status), то есть цепочка atexit обязана размотаться. Но прямая
|
||||||
|
* ссылка crt0 на разматыватель притащила бы в КАЖДУЮ программу и его, и
|
||||||
|
* весь стек хендлеров — включая те, что atexit() не зовут вовсе. Поэтому
|
||||||
|
* crt0 ссылается на этот модуль (два байта _DATA, ни байта кода), а
|
||||||
|
* заполняет указатель сам atexit() при первой регистрации: нет
|
||||||
|
* регистраций — указатель нулевой, разматыватель в программу не приезжает,
|
||||||
|
* crt0 просто уходит в ESTEX EXIT как раньше. Тот же приём, что у
|
||||||
|
* _irq_cbl_hook в libc/irq.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "_atexit.h"
|
||||||
|
|
||||||
|
void (*_atexit_hook)(void);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* _atexit_run — размотать цепочку atexit-хендлеров (LIFO).
|
||||||
|
*
|
||||||
|
* Общее тело для двух входов: явного exit() и возврата из main (там его
|
||||||
|
* зовёт crt0 через _atexit_hook). Отдельным модулем — чтобы программа,
|
||||||
|
* не зарегистрировавшая ни одного хендлера, не получала этот код вовсе.
|
||||||
|
*
|
||||||
|
* Вершина сбрасывается ПЕРЕД вызовом каждого хендлера: если хендлер сам
|
||||||
|
* позовёт exit(), цепочка не пойдёт по второму кругу.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "_atexit.h"
|
||||||
|
|
||||||
|
void _atexit_run(void)
|
||||||
|
{
|
||||||
|
while (_atexit_top > 0) {
|
||||||
|
void (*fn)(void) = _atexit_stack[--_atexit_top];
|
||||||
|
if (fn) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-3
@@ -3,9 +3,11 @@
|
|||||||
* завершении (exit()), максимум 8, порядок LIFO. Возвращает 0 или -1
|
* завершении (exit()), максимум 8, порядок LIFO. Возвращает 0 или -1
|
||||||
* при переполнении стека хендлеров.
|
* при переполнении стека хендлеров.
|
||||||
*
|
*
|
||||||
* Внимание: возврат из main() без явного exit() уходит в inline RST
|
* ВОЗВРАТ ИЗ main ТОЖЕ РАЗМАТЫВАЕТ ЦЕПОЧКУ (с 2026-09-02), как и требует
|
||||||
* 10h #41 в crt0.s и цепочку atexit НЕ выполняет — программам с
|
* стандарт C. Раньше crt0 уходил из main прямо в ESTEX EXIT, и хендлеры
|
||||||
* хендлерами нужно завершаться через exit().
|
* не выполнялись: программа теряла и сброс файловых буферов (_fclosall), и
|
||||||
|
* гашение звука, если не звала exit() явно. Теперь первая регистрация
|
||||||
|
* ставит _atexit_hook, а crt0 его дёргает — см. _atexit_hook.c.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -17,5 +19,8 @@ int atexit(void (*fn)(void))
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
_atexit_stack[_atexit_top++] = fn;
|
_atexit_stack[_atexit_top++] = fn;
|
||||||
|
/* Хендлеры появились — значит crt0 обязан размотать цепочку и при
|
||||||
|
* простом `return` из main. Ставим каждый раз: проверка дороже. */
|
||||||
|
_atexit_hook = _atexit_run;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -9,11 +9,6 @@
|
|||||||
|
|
||||||
void exit(int code)
|
void exit(int code)
|
||||||
{
|
{
|
||||||
while (_atexit_top > 0) {
|
_atexit_run();
|
||||||
void (*fn)(void) = _atexit_stack[--_atexit_top];
|
|
||||||
if (fn) {
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_exit(code);
|
_exit(code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
.module crt0
|
.module crt0
|
||||||
.globl _main
|
.globl _main
|
||||||
|
.globl __atexit_hook
|
||||||
|
|
||||||
;; Linker-emitted symbols (resolved at link time).
|
;; Linker-emitted symbols (resolved at link time).
|
||||||
.globl s__INITIALIZER
|
.globl s__INITIALIZER
|
||||||
@@ -88,6 +89,22 @@ _start::
|
|||||||
ld de, (_argv)
|
ld de, (_argv)
|
||||||
call _main
|
call _main
|
||||||
|
|
||||||
|
;; Returning from main is exit(status) per the C standard, so the
|
||||||
|
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||||
|
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||||
|
;; that never register anything keep it NULL and link neither the
|
||||||
|
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||||
|
push de ; DE = int return of main
|
||||||
|
ld hl, (__atexit_hook)
|
||||||
|
ld a, h
|
||||||
|
or a, l
|
||||||
|
jr Z, atexit_done
|
||||||
|
ld de, #atexit_done
|
||||||
|
push de ; return address: Z80 has no `call (hl)`
|
||||||
|
jp (hl)
|
||||||
|
atexit_done:
|
||||||
|
pop de
|
||||||
|
|
||||||
;; SDCC's int return → DE. Low byte is the exit code.
|
;; SDCC's int return → DE. Low byte is the exit code.
|
||||||
ld a, e
|
ld a, e
|
||||||
ld b, a
|
ld b, a
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
.module crt0_banked
|
.module crt0_banked
|
||||||
.globl _main
|
.globl _main
|
||||||
|
.globl __atexit_hook
|
||||||
.globl _n_banks
|
.globl _n_banks
|
||||||
.globl _bank_pages
|
.globl _bank_pages
|
||||||
|
|
||||||
@@ -218,6 +219,22 @@ skip_bank_load:
|
|||||||
ld de, (_argv)
|
ld de, (_argv)
|
||||||
call _main
|
call _main
|
||||||
|
|
||||||
|
;; Returning from main is exit(status) per the C standard, so the
|
||||||
|
;; atexit chain must run here too. The call is INDIRECT on purpose:
|
||||||
|
;; atexit() fills __atexit_hook on its first registration, so programs
|
||||||
|
;; that never register anything keep it NULL and link neither the
|
||||||
|
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
|
||||||
|
push de ; DE = int return of main
|
||||||
|
ld hl, (__atexit_hook)
|
||||||
|
ld a, h
|
||||||
|
or a, l
|
||||||
|
jr Z, atexit_done
|
||||||
|
ld de, #atexit_done
|
||||||
|
push de ; return address: Z80 has no `call (hl)`
|
||||||
|
jp (hl)
|
||||||
|
atexit_done:
|
||||||
|
pop de
|
||||||
|
|
||||||
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
|
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
|
||||||
ld a, e
|
ld a, e
|
||||||
jr exit_with_a
|
jr exit_with_a
|
||||||
|
|||||||
Reference in New Issue
Block a user