SprPoP: музыка без перелинковки — длины и длительности уехали на диск
Часть I плана music_runtime_index_plan.md (MI0..MI5). gen/pop_music_tbl.h и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в make test-tools). Один и тот же sprpop.exe работает с любым из четырёх наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется. ГДЕ ЖИВЁТ ИНДЕКС. 228 байт таблицы в W2 не положить (свободной кучи там порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в резиденте от него два байта. Данные в странице — со смещения 0x100: gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля они попали бы прямо в записи id 10 и 21. Со смещением работает штатная защита, а не запрет прерываний (тот же приём, что CFG_BASE в pop_config.c). Число страниц в индексе не хранится — считается из blocks, чтобы не разъехалось. ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ. pop_endmus_left и POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() — «заявка лежит, идёт загрузка или трек звучит». Одного busy мало: между заявкой и первой нотой 190-230 мс (замер в sound_plan §9). Прежний счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3 кадра луча, а не 4). Побочно исправилось расхождение с SDLPoP: при выключенном звуке заявка не кладётся, и уровень меняется сразу, как в оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный уровень лишние 12 секунд в тишине. PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из индекса при входе в сцену; прежние выражения шкалы не изменились. План предлагал протащить структуру времён через пять функций — для сцены, которая идёт раз за запуск, это того не стоит. ПАМЯТЬ. За обе фазы резидент не вырос, а освободился: _CODE 23865 -> 23544, куча 239 -> 256 Б. Банк 9 похудел на 118 Б (ушла pop_mus_tbl из rodata), банк 11 — на длительности реплик. ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в индексах различаются, и контрольные значения совпали с предсказанными планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462 блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его длительностям; без mus.idx музыки нет, эффекты работают, игра проходима. НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в игре. Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
@@ -163,6 +163,7 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
||||
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||
| `pop_sfx.c` + `pop_sfx_cold.c` | Звук: насос CBL в резиденте + холодная загрузка набора. Раскладка эффектов НЕ компилируется в EXE — читается с диска (`SND/snd.idx`, формат `PSI1`); см. `docs/sound_plan.md` §10. |
|
||||
| `pop_music.c` | Музыка (банк 9): загрузка треков, кольцо для финальной темы. Длины и длительности — из `MUS/mus.idx` (EMM-страница + accessor `pop_music_info`), в EXE их нет; см. `docs/sound_plan.md` §11. |
|
||||
| `pop_snd_tbl.h` + `pop_snd_data.c` | Тип записи набора и инварианты (рукописный заголовок) + резидентные `pop_snd_tbl`/`pop_snd_page`/`pop_snd_pages`. Размер записи 5 байт — часть дискового контракта, проверяется статически. |
|
||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ DISK := README_E.TXT README_R.TXT \
|
||||
KID/kid.arc KID/kid.pal KID/kid.ani \
|
||||
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
||||
SND/snd.arc SND/snd.idx \
|
||||
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
|
||||
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) MUS/mus.idx \
|
||||
FONT/font.atl \
|
||||
TITLE/title.arc TITLE/title.pal \
|
||||
PV/pv.arc PV/story.pal PV/pv.pal \
|
||||
@@ -236,9 +236,11 @@ GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
||||
# pop_sound_tbl.h здесь БОЛЬШЕ НЕТ: раскладка эффектов уехала на диск
|
||||
# (SND/snd.idx), и EXE от звукового набора не зависит — пару
|
||||
# snd.idx + snd.arc можно менять без перекомпиляции.
|
||||
# Ни звукового, ни музыкальных заголовков здесь БОЛЬШЕ НЕТ: раскладка
|
||||
# эффектов и длины треков уехали на диск (SND/snd.idx, MUS/mus.idx), и EXE
|
||||
# не зависит ни от звукового набора, ни от набора записей.
|
||||
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
||||
pop_guard_pal.h pop_shadow_atlas.h \
|
||||
pop_music_tbl.h pop_music_ticks.h pop_font.h)
|
||||
pop_guard_pal.h pop_shadow_atlas.h pop_font.h)
|
||||
|
||||
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
||||
@@ -406,7 +408,7 @@ $(STAMP)/sound-$(SND_SRC): $(TOOLS)/pop_pack_sound.py $(TOOLS)/pop_idx.py \
|
||||
$(ARC) $(PACKED)/SND/snd.arc $$(ls $(ATL)/sound/s*.bin | sort -V)
|
||||
@cp $(ATL)/sound/snd.idx $(PACKED)/SND/snd.idx
|
||||
@touch $@
|
||||
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py $(STAMP)/music.variant,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
||||
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/pop_idx.py $(TOOLS)/paths.py $(STAMP)/music.variant,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
||||
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
||||
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
||||
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
||||
@@ -426,7 +428,7 @@ $(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
|
||||
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
||||
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
||||
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
||||
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,music-$(MUSIC_FMT)))
|
||||
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) $(ATL)/music/mus.idx,music-$(MUSIC_FMT)))
|
||||
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
||||
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
||||
$(eval $(call from_stamp,$(PV_ATL) $(ATL)/pv/story.pal $(ATL)/pv/pv.pal,pv))
|
||||
@@ -475,6 +477,7 @@ $(eval $(call copy_rule,TITLE/title.pal,$(ATL)/title/title.pal))
|
||||
$(eval $(call copy_rule,PV/story.pal,$(ATL)/pv/story.pal))
|
||||
$(eval $(call copy_rule,PV/pv.pal,$(ATL)/pv/pv.pal))
|
||||
$(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).bin)))
|
||||
$(eval $(call copy_rule,MUS/mus.idx,$(ATL)/music/mus.idx))
|
||||
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
|
||||
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
||||
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
||||
@@ -509,7 +512,7 @@ resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.
|
||||
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
||||
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
||||
resources-sound: $(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx
|
||||
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
|
||||
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin) $(PACKED)/MUS/mus.idx
|
||||
resources-font: $(PACKED)/FONT/font.atl
|
||||
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
||||
resources-pv: $(PACKED)/PV/pv.arc $(PACKED)/PV/story.pal $(PACKED)/PV/pv.pal
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,8 @@
|
||||
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||||
|
||||
> Статус: **часть II (эффекты, SI0..SI4) СДЕЛАНА 2026-08-31** — разбор и
|
||||
> результаты в `sound_plan.md` §10. Часть I (музыка, MI0..MI5) — план.
|
||||
> Статус: **ОБЕ ЧАСТИ СДЕЛАНЫ 2026-08-31** — эффекты (SI0..SI4) и музыка
|
||||
> (MI0..MI5). Разбор и результаты: `sound_plan.md` §10 (эффекты) и §11
|
||||
> (музыка). Ниже — исходный план; расхождения перечислены следом.
|
||||
>
|
||||
> Что в реализации разошлось с планом ниже: файл назван `SND/snd.idx`
|
||||
> (единый basename с `snd.arc`, оба в нижнем регистре), а при отсутствии
|
||||
@@ -9,6 +10,16 @@
|
||||
> и игра непроходима (§10.5 sound_plan). Про `ticks60` в `PMI1` решено:
|
||||
> поле в формате оставить даже если сценарии перейдут на ожидание звука —
|
||||
> загрузчик волен не тащить его в память.
|
||||
>
|
||||
> Расхождения части I с планом ниже:
|
||||
> * файл назван `MUS/mus.idx` (единый стиль с `snd.idx`);
|
||||
> * пауза конца уровня переведена НЕ на runtime-тики, а на состояние
|
||||
> «заявка/загрузка/звучание» (`pop_music_active`) — §11.2: это снимает и
|
||||
> зависимость от набора, и враньё делителя `/4` в быстрых режимах, и
|
||||
> расхождение с SDLPoP при выключенном звуке;
|
||||
> * PV-сцена (§8 плана) — не структура времён через пять функций, а четыре
|
||||
> якоря-статики (8 байт), от которых отсчитываются прежние выражения;
|
||||
> * `pages` в PMI1 не хранится — считается из `blocks`.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
|
||||
@@ -1194,3 +1194,93 @@ sdlpop` перепаковывал `build/atl`, а в `assets/packed` остав
|
||||
сцене с принцессой) лежит у наборов по РАЗНЫМ страницам — 8 у SDLPoP,
|
||||
9 у MSDOS. А вот 4 `gate_closing` из той же сцены совпадает побайтно и
|
||||
для проверки подмены не годится.
|
||||
|
||||
## 11. МУЗЫКА БЕЗ ПЕРЕЛИНКОВКИ: ДЛИНЫ И ДЛИТЕЛЬНОСТИ УЕХАЛИ НА ДИСК (2026-08-31)
|
||||
|
||||
Сделана часть I плана `music_runtime_index_plan.md` (этапы MI0..MI5, у нас
|
||||
B0..B7). `gen/pop_music_tbl.h` и `gen/pop_music_ticks.h` УДАЛЕНЫ: длины
|
||||
треков и длительности реплик читаются из `MUS/mus.idx` (формат `PMI1`).
|
||||
Один и тот же `sprpop.exe` работает с любым из четырёх наборов записей.
|
||||
|
||||
### 11.1 Где живёт индекс и почему именно там
|
||||
|
||||
228 байт таблицы в W2 положить нельзя — там свободной кучи порядка двух
|
||||
сотен. Поэтому индекс лежит в ОДНОЙ странице EMM, а в резиденте от него
|
||||
остаются два байта (номер блока и страницы). Наружу указатель на EMM не
|
||||
выдаётся: `pop_music_info()` подключает страницу в W0, копирует четыре
|
||||
байта в буфер вызывающего и сразу снимает окно.
|
||||
|
||||
**Данные в странице лежат со смещения `POP_MUS_IDX_BASE` (0x100), а не с
|
||||
нуля.** Причина в `gfx_w0_page_prepare`: он делает страницу безопасной
|
||||
для W0, записывая В НЕЁ переход на IRQ-восстановитель (0x38) и RETN для
|
||||
NMI (0x66). С нуля эти байты легли бы прямо в записи (id 10 и 21) и
|
||||
испортили индекс; со смещением стабы и данные не пересекаются, и работает
|
||||
штатная защита от прерываний, а не запрет их. Тот же приём и по той же
|
||||
причине — в `pop_config.c` (`CFG_BASE`).
|
||||
|
||||
Числа страниц в индексе НЕТ: это производная от `blocks`, и дублирование
|
||||
однажды разъехалось бы с ним. Считает `pop_music_pages()`.
|
||||
|
||||
### 11.2 Пауза конца уровня: состояние вместо счётчика
|
||||
|
||||
`pop_endmus_left` и `POP_MUS_TICKS_32/41` УДАЛЕНЫ. Главный цикл ждёт
|
||||
`pop_music_active()` — «заявка лежит, идёт загрузка или трек звучит».
|
||||
|
||||
Одного `pop_music_busy()` там мало: между заявкой и первой нотой проходит
|
||||
190–230 мс (§9), и в этой щели он ложен. Раньше щель закрывал счётчик по
|
||||
длине записи, но он тянул за собой две беды: зависимость EXE от набора и
|
||||
жёсткий делитель `/4`, который врал в быстрых режимах (там логический кадр
|
||||
3 кадра луча, а не 4). Три признака подряд закрывают щель без единой
|
||||
подобранной константы, потому что `pop_music_service` обнуляет `req` и
|
||||
тут же взводит `ld_busy`, а тот держится до записи `pop_mus_left`.
|
||||
|
||||
Побочно исправилось расхождение с оригиналом: при ВЫКЛЮЧЕННОМ звуке
|
||||
`pop_sfx_play` не кладёт заявку, все три признака ложны, и уровень
|
||||
меняется сразу — как в SDLPoP, где трек ставится под `if (is_sound_on)`
|
||||
(seg006:651), а ожидание это `check_sound_playing()` (seg003:387).
|
||||
Прежний счётчик взводился безусловно и держал пройденный уровень лишние
|
||||
12 секунд в тишине.
|
||||
|
||||
### 11.3 PV-сцена: четыре якоря вместо compile-time шкалы
|
||||
|
||||
Шкала сцены осталась абсолютной (переход на ожидание звука — отдельный
|
||||
разговор, см. план), но точки, зависящие от длины реплик, считаются при
|
||||
входе в сцену: `pv_m50_end`, `pv_dialog1`, `pv_exit`, `pv_anim_end`.
|
||||
Остальные выражения не изменились — они отсчитываются от якорей.
|
||||
|
||||
Восемь байт статики вместо протаскивания структуры времён через пять
|
||||
функций (`pv_princess_step`, `pv_jaffar_step`, `pv_seq_period`,
|
||||
`intro_pv_draw_frame`, `intro_pv_animated`), как предлагал план: сцена
|
||||
одна на запуск, и переделка всех сигнатур того не стоит.
|
||||
|
||||
Нет индекса — длительности нулевые, и сцена просто идёт без пауз на
|
||||
реплики: ждать нечего, звука-то нет.
|
||||
|
||||
### 11.4 Цена в памяти
|
||||
|
||||
За обе фазы (эффекты и музыка) резидент не вырос, а освободился:
|
||||
|
||||
| | до фазы A | после фазы B |
|
||||
|---|---:|---:|
|
||||
| `_CODE` | 23865 | 23544 |
|
||||
| куча резидента | 239 Б | 256 Б |
|
||||
|
||||
Банк 9 похудел на 118 Б (ушла `pop_mus_tbl` из его rodata), банк 11 — на
|
||||
длительности реплик. Прибавка: 2 байта на страницу индекса музыки и 8 на
|
||||
якоря PV.
|
||||
|
||||
### 11.5 Что проверено на живой машине
|
||||
|
||||
* `sprpop.exe` ПОБАЙТОВО одинаков для наборов flac и mt32;
|
||||
* все 22 трека в индексах различаются, и контрольные значения совпали с
|
||||
тем, что план предсказывал по замерам: m32 438→449, m41 732→685,
|
||||
m50 831→867, m53 985→1044, m56 9865→10462 блоков (78→82 страницы);
|
||||
* на mt32-наборе PV-сцена проходит целиком: m50 → m53 → m52 звучат по
|
||||
длительностям mt32, хотя в EXE их больше нет;
|
||||
* без `mus.idx` музыки нет, эффекты работают, игра проходима — сцены
|
||||
проскакивают без пауз на реплики.
|
||||
|
||||
**НЕ ПРОВЕРЕНО:** потоковый трек m56 на 82 страницах (у flac 78).
|
||||
Кольцевой проигрыватель получает число страниц из индекса, и код к этому
|
||||
готов, но живого прогона финала на mt32 не было — до него надо дойти в
|
||||
игре. Это оставшийся пункт приёмки.
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.
|
||||
* Источник: assets/orig/PoP1_DOS_music/flac (записи DOS-версии).
|
||||
* Генератор: tools/pop_pack_music.py. Руками не править.
|
||||
*
|
||||
* Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —
|
||||
* сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека
|
||||
* в порциях насоса по 128 байт (в байтах не влезло бы в uint16).
|
||||
*
|
||||
* Длительности реплик в тиках вынесены в pop_music_ticks.h: таблица
|
||||
* ниже — 171 байт ДАННЫХ, и модулю, которому нужна одна константа,
|
||||
* платить за неё копией таблицы незачем (поймано на pop_intro.c:
|
||||
* +172 байта в банк 11).
|
||||
*
|
||||
* POP_MUS_TICKS_<id> — та же длина в ТИКАХ СЦЕНЫ (60 Гц оригинала).
|
||||
* Нужна там, где сценарий ждёт окончания реплики: у оригинала это
|
||||
* `do { frame(1) } while (check_sound_playing())`, у нас — пауза
|
||||
* длиной в нашу запись. Считается ЗДЕСЬ, а не в C, по двум
|
||||
* причинам: blocks*40 переполняет uint16 на won (9855 порций), и
|
||||
* число обязано соответствовать НАБОРУ, которым собрали (flac и
|
||||
* mt32 — разные исполнения разной длины).
|
||||
*
|
||||
* Отношение 40/57 — обратное к тому, которым шкала сцены переводит
|
||||
* тики в порции насоса (57/40, см. pop_intro.c). Берём именно его,
|
||||
* а не точное 10937,5/128/60, чтобы длина реплики и ход шкалы
|
||||
* считались одной и той же арифметикой. Округление — к ближайшему
|
||||
* (+28 перед делением): отбрасывание вниз теряло по тику на реплику.
|
||||
*/
|
||||
#ifndef POP_MUSIC_TBL_H
|
||||
#define POP_MUSIC_TBL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_MUS_COUNT 57
|
||||
#define POP_MUS_BLOCK 128
|
||||
#define POP_MUS_MAX_PAGES 78
|
||||
|
||||
|
||||
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] = {
|
||||
{ 0, 0 }, /* 0 */
|
||||
{ 0, 0 }, /* 1 */
|
||||
{ 0, 0 }, /* 2 */
|
||||
{ 0, 0 }, /* 3 */
|
||||
{ 0, 0 }, /* 4 */
|
||||
{ 0, 0 }, /* 5 */
|
||||
{ 0, 0 }, /* 6 */
|
||||
{ 0, 0 }, /* 7 */
|
||||
{ 0, 0 }, /* 8 */
|
||||
{ 0, 0 }, /* 9 */
|
||||
{ 0, 0 }, /* 10 */
|
||||
{ 0, 0 }, /* 11 */
|
||||
{ 0, 0 }, /* 12 */
|
||||
{ 0, 0 }, /* 13 */
|
||||
{ 0, 0 }, /* 14 */
|
||||
{ 0, 0 }, /* 15 */
|
||||
{ 0, 0 }, /* 16 */
|
||||
{ 0, 0 }, /* 17 */
|
||||
{ 0, 0 }, /* 18 */
|
||||
{ 0, 0 }, /* 19 */
|
||||
{ 0, 0 }, /* 20 */
|
||||
{ 0, 0 }, /* 21 */
|
||||
{ 0, 0 }, /* 22 */
|
||||
{ 0, 0 }, /* 23 */
|
||||
{ 3, 305 }, /* 24 regular_death */
|
||||
{ 2, 196 }, /* 25 find_mirror_and_find_shadow */
|
||||
{ 9, 1132 }, /* 26 embrace */
|
||||
{ 8, 915 }, /* 27 cutscene_before_2_4_6_12 */
|
||||
{ 6, 656 }, /* 28 fight_death */
|
||||
{ 4, 428 }, /* 29 meet_Jaffar */
|
||||
{ 3, 299 }, /* 30 life_potion */
|
||||
{ 0, 0 }, /* 31 */
|
||||
{ 5, 624 }, /* 32 end_level_4_and_killed_by_shadow */
|
||||
{ 2, 205 }, /* 33 potion */
|
||||
{ 0, 0 }, /* 34 */
|
||||
{ 7, 888 }, /* 35 cutscene_before_8_9 */
|
||||
{ 10, 1256 }, /* 36 out_of_time */
|
||||
{ 3, 280 }, /* 37 guard_death_and_obtaining_the_sword */
|
||||
{ 0, 0 }, /* 38 */
|
||||
{ 13, 1591 }, /* 39 floating */
|
||||
{ 5, 596 }, /* 40 cutscene_not_much_time_left */
|
||||
{ 9, 1043 }, /* 41 story_5_level_end_and_merge_with_shadow */
|
||||
{ 0, 0 }, /* 42 */
|
||||
{ 3, 285 }, /* 43 killed_Jaffar */
|
||||
{ 0, 0 }, /* 44 */
|
||||
{ 0, 0 }, /* 45 */
|
||||
{ 0, 0 }, /* 46 */
|
||||
{ 0, 0 }, /* 47 */
|
||||
{ 0, 0 }, /* 48 */
|
||||
{ 0, 0 }, /* 49 */
|
||||
{ 10, 1184 }, /* 50 story_2_princess */
|
||||
{ 0, 0 }, /* 51 */
|
||||
{ 20, 2443 }, /* 52 story_4_Jaffar_leaves */
|
||||
{ 11, 1403 }, /* 53 story_3_Jaffar_enters */
|
||||
{ 17, 2064 }, /* 54 intro_theme */
|
||||
{ 11, 1350 }, /* 55 story_1_absence */
|
||||
{ 78, 9865 }, /* 56 won */
|
||||
};
|
||||
|
||||
#endif /* POP_MUSIC_TBL_H */
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* pop_music_ticks.h — длительности треков в ТИКАХ СЦЕНЫ, СГЕНЕРИРОВАН.
|
||||
* Набор: flac. Генератор: tools/pop_pack_music.py.
|
||||
* Руками не править.
|
||||
*
|
||||
* Нужны там, где сценарий ждёт окончания реплики: у оригинала это
|
||||
* `do { frame(1) } while (check_sound_playing())`, у нас — пауза длиной
|
||||
* в НАШУ запись. Число обязано соответствовать набору, которым собрана
|
||||
* сборка: одна и та же реплика на flac и mt32 разной длины (m53 — 985
|
||||
* тиков против 1044), и захардкоженная константа молча ломает сцену.
|
||||
*
|
||||
* Считается здесь, а не в C: blocks*40 переполняет uint16 на won
|
||||
* (9855 порций). Отношение 40/57 — обратное к тому, которым шкала
|
||||
* сцены переводит тики в порции насоса (57/40, pop_intro.c); берём
|
||||
* именно его, чтобы длина реплики и ход шкалы считались одной
|
||||
* арифметикой. Округление к ближайшему: вниз теряло по тику.
|
||||
*/
|
||||
#ifndef POP_MUSIC_TICKS_H
|
||||
#define POP_MUSIC_TICKS_H
|
||||
|
||||
#define POP_MUS_TICKS_24 214 /* regular_death */
|
||||
#define POP_MUS_TICKS_25 138 /* find_mirror_and_find_shadow */
|
||||
#define POP_MUS_TICKS_26 794 /* embrace */
|
||||
#define POP_MUS_TICKS_27 642 /* cutscene_before_2_4_6_12 */
|
||||
#define POP_MUS_TICKS_28 460 /* fight_death */
|
||||
#define POP_MUS_TICKS_29 300 /* meet_Jaffar */
|
||||
#define POP_MUS_TICKS_30 210 /* life_potion */
|
||||
#define POP_MUS_TICKS_32 438 /* end_level_4_and_killed_by_shadow */
|
||||
#define POP_MUS_TICKS_33 144 /* potion */
|
||||
#define POP_MUS_TICKS_35 623 /* cutscene_before_8_9 */
|
||||
#define POP_MUS_TICKS_36 881 /* out_of_time */
|
||||
#define POP_MUS_TICKS_37 196 /* guard_death_and_obtaining_the_sword */
|
||||
#define POP_MUS_TICKS_39 1116 /* floating */
|
||||
#define POP_MUS_TICKS_40 418 /* cutscene_not_much_time_left */
|
||||
#define POP_MUS_TICKS_41 732 /* story_5_level_end_and_merge_with_shadow */
|
||||
#define POP_MUS_TICKS_43 200 /* killed_Jaffar */
|
||||
#define POP_MUS_TICKS_50 831 /* story_2_princess */
|
||||
#define POP_MUS_TICKS_52 1714 /* story_4_Jaffar_leaves */
|
||||
#define POP_MUS_TICKS_53 985 /* story_3_Jaffar_enters */
|
||||
#define POP_MUS_TICKS_54 1448 /* intro_theme */
|
||||
#define POP_MUS_TICKS_55 947 /* story_1_absence */
|
||||
#define POP_MUS_TICKS_56 6923 /* won */
|
||||
|
||||
#endif /* POP_MUSIC_TICKS_H */
|
||||
@@ -15,7 +15,6 @@
|
||||
#include "pop_shadow.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_music.h"
|
||||
#include "pop_music_ticks.h" /* POP_MUS_TICKS_<id> — длина реплик набора */
|
||||
#include "pop_arc.h"
|
||||
#include "pv_arc.h"
|
||||
#include "pop_cutscene.h"
|
||||
@@ -60,9 +59,10 @@
|
||||
* своего куска (например, всё поведение Джафара — от PV_DIALOG1_START).
|
||||
* 2) «ЖДЁМ КОНЦА РЕПЛИКИ» — PV_M50_END и PV_EXIT_START. Это целиком
|
||||
* длина НАШЕЙ записи, и она у каждого набора своя: m50 — 831 тик на
|
||||
* flac против 867 на mt32, m53 — 985 против 1044. Поэтому берём их
|
||||
* из POP_MUS_TICKS_<id>, которые печатает упаковщик того набора,
|
||||
* которым собрана сборка (gen/pop_music_tbl.h).
|
||||
* flac против 867 на mt32, m53 — 985 против 1044. Раньше их
|
||||
* печатал упаковщик в gen/pop_music_ticks.h, из-за чего EXE был
|
||||
* привязан к набору; теперь они приезжают из индекса набора
|
||||
* (MUS/mus.idx) при входе в сцену — см. ЯКОРЯ ниже.
|
||||
*
|
||||
* Всё, что стоит ПОСЛЕ реплики, сдвигается вместе с её концом — отсюда
|
||||
* сложения от PV_M50_END и PV_EXIT_START вместо литералов. */
|
||||
@@ -72,46 +72,76 @@ enum {
|
||||
/* Конец m50: кадр становится 8. Прежний литерал 846 отвечал длине 834
|
||||
* тика — на три больше нашей записи (831); расхождение осталось с
|
||||
* времён, когда длину подставляли руками. */
|
||||
PV_M50_END = PV_MUS_2_START + POP_MUS_TICKS_50,
|
||||
PV_WAIT_END = PV_M50_END + 40, /* +frame(5) -> створка ворот */
|
||||
PV_GATE_END = PV_WAIT_END + 48,
|
||||
PV_DOOR_END = PV_GATE_END + 24,
|
||||
PV_TURN_START = PV_DOOR_END,
|
||||
PV_WALK1_START = PV_DOOR_END + 40,
|
||||
PV_DIALOG1_START = PV_WALK1_START + 48, /* Джафар входит — реплика m53 */
|
||||
/* Ниже — биты сценария внутри реплики Джафара, все от её начала. */
|
||||
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
|
||||
PV_WALK2_START = PV_DIALOG1_START + 176,
|
||||
PV_DIALOG2_START = PV_DIALOG1_START + 416,
|
||||
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||||
*
|
||||
* История: сцена была render-bound — кадр рисовался дороже своего
|
||||
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||||
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||||
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||||
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||||
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||||
* 100 тиков.
|
||||
*
|
||||
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||||
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||||
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||||
* После этого подгонка стала вредной: молния била больше чем на
|
||||
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||||
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||||
* разъедется, крутить надо её, а не тайминги сценария. */
|
||||
PV_MAGIC_LEAD = 0,
|
||||
PV_RAISE_START = PV_DIALOG1_START + 696 - PV_MAGIC_LEAD,
|
||||
PV_STEPBACK_START = PV_DIALOG1_START + 703 - PV_MAGIC_LEAD,
|
||||
PV_MAGIC_START = PV_DIALOG1_START + 822 - PV_MAGIC_LEAD,
|
||||
/* Конец m53 — снова длина записи, а не бит сценария. */
|
||||
PV_EXIT_START = PV_DIALOG1_START + POP_MUS_TICKS_53,
|
||||
PV_MUS_4_START = PV_EXIT_START + 42, /* +frame(6) -> «Джафар уходит» */
|
||||
PV_GLASS_DONE = PV_EXIT_START + 210,
|
||||
PV_SLUMP_START = PV_EXIT_START + 273,
|
||||
PV_ANIM_TICKS = PV_EXIT_START + 469
|
||||
PV_MAGIC_LEAD = 0
|
||||
};
|
||||
|
||||
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||||
*
|
||||
* История: сцена была render-bound — кадр рисовался дороже своего
|
||||
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||||
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||||
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||||
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||||
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||||
* 100 тиков.
|
||||
*
|
||||
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||||
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||||
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||||
* После этого подгонка стала вредной: молния била больше чем на
|
||||
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||||
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||||
* разъедется, крутить надо её, а не тайминги сценария. */
|
||||
|
||||
/* ЯКОРЯ ШКАЛЫ. Четыре точки, от которых отсчитывается всё остальное;
|
||||
* считаются ОДИН РАЗ при входе в сцену из длительностей набора
|
||||
* (pop_music_info). Держать их статикой, а не тащить структуру времён
|
||||
* через пять функций: восемь байт против переделки всех сигнатур, а
|
||||
* сцена всё равно одна на запуск.
|
||||
*
|
||||
* Нет индекса или трека — длительность нулевая, и сцена просто проходит
|
||||
* без пауз на реплики: ждать нечего, звука-то нет. */
|
||||
static uint16_t pv_m50_end; /* конец «принцесса ждёт» (m50) */
|
||||
static uint16_t pv_dialog1; /* Джафар входит — начало реплики m53 */
|
||||
static uint16_t pv_exit; /* конец m53: Джафар уходит */
|
||||
static uint16_t pv_anim_end; /* конец всей сцены */
|
||||
|
||||
/* Ниже — те же выражения, что были в enum: биты сценария отсчитываются от
|
||||
* своего якоря и от записи НЕ зависят. */
|
||||
#define PV_M50_END pv_m50_end
|
||||
#define PV_WAIT_END ((uint16_t)(pv_m50_end + 40))
|
||||
#define PV_GATE_END ((uint16_t)(pv_m50_end + 88))
|
||||
#define PV_DOOR_END ((uint16_t)(pv_m50_end + 112))
|
||||
#define PV_TURN_START PV_DOOR_END
|
||||
#define PV_WALK1_START ((uint16_t)(pv_m50_end + 152))
|
||||
#define PV_DIALOG1_START pv_dialog1
|
||||
#define PV_MUS_4_LOAD ((uint16_t)(pv_dialog1 + 54))
|
||||
#define PV_WALK2_START ((uint16_t)(pv_dialog1 + 176))
|
||||
#define PV_DIALOG2_START ((uint16_t)(pv_dialog1 + 416))
|
||||
#define PV_RAISE_START ((uint16_t)(pv_dialog1 + 696 - PV_MAGIC_LEAD))
|
||||
#define PV_STEPBACK_START ((uint16_t)(pv_dialog1 + 703 - PV_MAGIC_LEAD))
|
||||
#define PV_MAGIC_START ((uint16_t)(pv_dialog1 + 822 - PV_MAGIC_LEAD))
|
||||
#define PV_EXIT_START pv_exit
|
||||
#define PV_MUS_4_START ((uint16_t)(pv_exit + 42))
|
||||
#define PV_GLASS_DONE ((uint16_t)(pv_exit + 210))
|
||||
#define PV_SLUMP_START ((uint16_t)(pv_exit + 273))
|
||||
#define PV_ANIM_TICKS pv_anim_end
|
||||
|
||||
/* Собрать шкалу под НАШ набор записей. 200 — цепочка битов сценария от
|
||||
* конца m50 до входа Джафара (40+48+24+40+48), 469 — хвост после его
|
||||
* ухода; оба от записи не зависят. */
|
||||
static void pv_timing_init(void)
|
||||
{
|
||||
pop_mus_info_t m50, m53;
|
||||
|
||||
(void)pop_music_info(POP_MUS_STORY_2, &m50);
|
||||
(void)pop_music_info(POP_MUS_STORY_3, &m53);
|
||||
pv_m50_end = (uint16_t)(PV_MUS_2_START + m50.ticks);
|
||||
pv_dialog1 = (uint16_t)(pv_m50_end + 200);
|
||||
pv_exit = (uint16_t)(pv_dialog1 + m53.ticks);
|
||||
pv_anim_end = (uint16_t)(pv_exit + 469);
|
||||
}
|
||||
|
||||
/* Возврат долга шкалы (см. цикл сцены): сколько порций насоса отдаём за один
|
||||
* кадр сцены и сколько их вообще имеет смысл копить. 1 порция — 11,7 мс;
|
||||
* 128 порций — полторы секунды, дальше догонять уже нечего. */
|
||||
@@ -870,6 +900,7 @@ static int intro_pv_animated(void)
|
||||
jaffar_actor.visible = 0;
|
||||
jaffar_actor.step = 0;
|
||||
tick = 0;
|
||||
pv_timing_init(); /* шкала под длительности НАШЕГО набора */
|
||||
intro_skip_begin(&skip);
|
||||
while (tick < PV_ANIM_TICKS) {
|
||||
if (intro_skip_requested(&skip)) {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
||||
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||||
#include "pop_music_ticks.h" /* длина мелодии конца уровня */
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter_mem.h>
|
||||
@@ -38,14 +37,6 @@ int8_t knock;
|
||||
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||
* легли сюда же без переделки. */
|
||||
uint8_t pop_next_level;
|
||||
/* СКОЛЬКО ЛОГИЧЕСКИХ КАДРОВ ЕЩЁ ДЕРЖАТЬ УРОВЕНЬ, пока играет мелодия конца.
|
||||
* У оригинала (seg003:387, play_level_2) уровень не сменяется, пока
|
||||
* `check_sound_playing()`: экран пройденного уровня живёт с анимацией
|
||||
* факелов ровно столько, сколько звучит трек. У нас признак «звучит» —
|
||||
* pop_music_busy(), но он ложен при ВЫКЛЮЧЕННОЙ музыке, а оригинал
|
||||
* выдерживает паузу и молча. Поэтому здесь взводится счётчик по длине
|
||||
* записи (gen/pop_music_ticks.h), а главный цикл ждёт большего из двух. */
|
||||
uint8_t pop_endmus_left;
|
||||
|
||||
|
||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
|
||||
@@ -367,17 +358,13 @@ void play_seq(void)
|
||||
else if (ev == 4) {
|
||||
/* seg006:646. На четвёртом уровне уход за дверь — это
|
||||
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
||||
* 15-м оригинал молчит. Вместе с треком взводим
|
||||
* задержку смены уровня (см. pop_endmus_left): кадров
|
||||
* = длина записи в тиках -> кадры луча -> делённое на
|
||||
* длину логического кадра (4 кадра луча вне боя). */
|
||||
if (pop_current_level == 4) {
|
||||
pop_sfx_play(32);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_32) / 4);
|
||||
} else if (pop_current_level != 13) {
|
||||
pop_sfx_play(41);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_41) / 4);
|
||||
}
|
||||
* 15-м оригинал молчит. Держать уровень на экране,
|
||||
* пока трек звучит, — забота главного цикла: он ждёт
|
||||
* pop_music_active(), и своей задержки здесь больше
|
||||
* нет (была по длине записи из gen/pop_music_ticks.h,
|
||||
* из-за чего EXE зависел от набора музыки). */
|
||||
if (pop_current_level == 4) pop_sfx_play(32);
|
||||
else if (pop_current_level != 13) pop_sfx_play(41);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -32,7 +32,6 @@ extern int8_t knock;
|
||||
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
||||
extern uint8_t pop_next_level;
|
||||
/* Задержка смены уровня на время мелодии конца (см. pop_kid.c). */
|
||||
extern uint8_t pop_endmus_left;
|
||||
|
||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||
* 0 — OK, -1 — ошибка. */
|
||||
|
||||
@@ -13,14 +13,81 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <string.h>
|
||||
#include <sprite.h> /* gfx_w0_map/unmap — доступ к странице индекса */
|
||||
#include "pop_music.h"
|
||||
#include "pop_music_tbl.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||
#include "_pop_file.h"
|
||||
|
||||
uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||
|
||||
/* --- ИНДЕКС НАБОРА ---------------------------------------------------- *
|
||||
*
|
||||
* Страница EMM с прочитанным mus.idx; 0 — индекса нет. В W2 от него
|
||||
* остаются ровно эти два байта, сама таблица (228 Б) лежит в странице.
|
||||
*
|
||||
* Данные лежат со смещения POP_MUS_IDX_BASE, а не с начала страницы:
|
||||
* gfx_w0_page_prepare пишет туда стабы прерываний (0x38 и 0x66), и с
|
||||
* нуля они попали бы прямо в записи. Со смещением всё расходится, и
|
||||
* страница подключается штатно — без запрета прерываний. */
|
||||
static uint8_t mus_idx_blk;
|
||||
static uint8_t mus_idx_page;
|
||||
|
||||
/* "MUS\mus.idx" — имя знает сам модуль (тот же приём, что в pop_arc.c:
|
||||
* литерал лежит в rodata банка 9, и open() читает его, пока в W3 стоит
|
||||
* этот банк). */
|
||||
static const char mus_idx_path[] = "MUS\\mus.idx";
|
||||
|
||||
int8_t pop_music_index_load(void) __banked
|
||||
{
|
||||
uint8_t *w0 = (uint8_t *)POP_MUS_IDX_BASE;
|
||||
uint8_t blk, page, ok;
|
||||
int fd;
|
||||
|
||||
if (mus_idx_page) return 0;
|
||||
POP_PATH_CALL(fd, open(mus_idx_path, O_RDONLY));
|
||||
if (fd < 0) return -1;
|
||||
blk = mem_alloc_pages(1);
|
||||
if (!blk) { close(fd); return -1; }
|
||||
page = mem_get_page(blk, 0);
|
||||
if (!page ||
|
||||
bank_read_page(fd, page, POP_MUS_IDX_BASE, POP_MUS_IDX_SIZE) < 0) {
|
||||
close(fd);
|
||||
mem_free_block(blk);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
gfx_w0_page_prepare(page); /* стабы лягут вне данных, см. BASE */
|
||||
gfx_w0_map(page);
|
||||
ok = (uint8_t)(memcmp(w0, "PMI1", 4) == 0 &&
|
||||
w0[4] == POP_MUS_COUNT &&
|
||||
w0[5] == POP_MUS_IDX_REC &&
|
||||
w0[6] == 7 && /* log2(128) — блок насоса */
|
||||
w0[7] == 1); /* u8 mono, CBL_FREQ_10K9 */
|
||||
gfx_w0_unmap();
|
||||
if (!ok) { mem_free_block(blk); return -1; }
|
||||
mus_idx_blk = blk;
|
||||
mus_idx_page = page;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t pop_music_info(uint8_t id, pop_mus_info_t *out) __banked
|
||||
{
|
||||
const uint8_t *rec;
|
||||
const uint8_t *w0 = (const uint8_t *)POP_MUS_IDX_BASE;
|
||||
|
||||
out->blocks = 0;
|
||||
out->ticks = 0;
|
||||
if (!mus_idx_page || id >= POP_MUS_COUNT) return -1;
|
||||
gfx_w0_map(mus_idx_page);
|
||||
rec = w0 + POP_MUS_IDX_HDR + (uint16_t)id * POP_MUS_IDX_REC;
|
||||
out->blocks = (uint16_t)(rec[0] | ((uint16_t)rec[1] << 8));
|
||||
out->ticks = (uint16_t)(rec[2] | ((uint16_t)rec[3] << 8));
|
||||
gfx_w0_unmap();
|
||||
return out->blocks ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ
|
||||
* держит — её и так хранит блок EMM, и mem_get_page отдаёт номер по
|
||||
* индексу. Резидентный pop_mus_page[] заполняется на pop_music_play, для
|
||||
@@ -53,6 +120,9 @@ static uint8_t ld_next; /* следующая страница к
|
||||
static uint8_t ld_half; /* 0 = впереди первая половина страницы */
|
||||
static uint8_t ld_pages;
|
||||
static uint8_t ld_id;
|
||||
/* Длина трека из индекса, снятая при открытии: лезть в страницу индекса
|
||||
* на каждом шаге загрузки незачем — она не меняется. */
|
||||
static uint16_t ld_blocks;
|
||||
|
||||
static void slot_release(uint8_t n)
|
||||
{
|
||||
@@ -126,13 +196,17 @@ static void load_abort(void)
|
||||
int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
{
|
||||
char name[12];
|
||||
pop_mus_info_t info;
|
||||
uint8_t pages, n;
|
||||
|
||||
if (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
|
||||
if (ld_busy) load_abort();
|
||||
if (id >= POP_MUS_COUNT) return -1;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
if (!pages || pages > POP_MUS_PAGES) return -1;
|
||||
/* Длину и число страниц знает ИНДЕКС НАБОРА, а не EXE: у mt32 те же
|
||||
* треки другой длины (docs/music_runtime_index_plan.md). */
|
||||
if (pop_music_info(id, &info) != 0) return -1;
|
||||
pages = pop_music_pages(info.blocks);
|
||||
if (pages > POP_MUS_PAGES) return -1;
|
||||
|
||||
/* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы
|
||||
* из прерывания, и отобрать их у него нельзя. */
|
||||
@@ -152,6 +226,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
ld_half = 0;
|
||||
ld_pages = pages;
|
||||
ld_id = id;
|
||||
ld_blocks = info.blocks;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -183,7 +258,7 @@ int8_t pop_music_load_step(void) __banked
|
||||
close(ld_fd);
|
||||
ld_busy = 0;
|
||||
slot_pages[ld_slot] = ld_pages;
|
||||
slot_blocks[ld_slot] = pop_mus_tbl[ld_id].blocks;
|
||||
slot_blocks[ld_slot] = ld_blocks;
|
||||
slot_id[ld_slot] = ld_id;
|
||||
mus_ready = (uint8_t)(ld_slot + 1);
|
||||
return 1;
|
||||
@@ -229,7 +304,7 @@ int8_t pop_music_stream(uint8_t id) __banked
|
||||
if (n != mus_cur) slot_release(mus_cur);
|
||||
mus_cur = n;
|
||||
slot_pages[n] = ld_pages;
|
||||
slot_blocks[n] = pop_mus_tbl[id].blocks;
|
||||
slot_blocks[n] = ld_blocks;
|
||||
slot_id[n] = id;
|
||||
pop_mus_id = id;
|
||||
pop_mus_pg = 0;
|
||||
@@ -283,9 +358,15 @@ void pop_music_ring_stop(void) __banked
|
||||
int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
{
|
||||
char name[12];
|
||||
pop_mus_info_t info;
|
||||
uint8_t i, page;
|
||||
|
||||
if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1;
|
||||
if (id >= POP_MUS_COUNT || pop_music_info(id, &info) != 0) return -1;
|
||||
/* Потоковый трек НЕ ограничен POP_MUS_PAGES: он и не помещается в
|
||||
* память целиком, ради того и кольцо. Предел здесь — восьмибитные
|
||||
* счётчики страниц (у mt32 финальная тема на 82 страницы против 78
|
||||
* у flac). */
|
||||
if (pop_music_pages(info.blocks) > 255) return -1;
|
||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||
pop_music_ring_stop();
|
||||
mus_name(name, id);
|
||||
@@ -294,8 +375,8 @@ int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
ring_fd_open = 1;
|
||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||
if (!ring_blk) { ring_close_file(); return -1; }
|
||||
ring_file_pages = pop_mus_tbl[id].pages;
|
||||
ring_total = pop_mus_tbl[id].blocks;
|
||||
ring_file_pages = (uint8_t)pop_music_pages(info.blocks);
|
||||
ring_total = info.blocks;
|
||||
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
|
||||
* форы, и первая же тяжёлая сцена финала не догонит насос. */
|
||||
for (i = 0; i < RING_PAGES; i++) {
|
||||
@@ -375,3 +456,24 @@ uint8_t pop_music_busy(void) __banked
|
||||
{
|
||||
return (uint8_t)(pop_mus_left != 0);
|
||||
}
|
||||
|
||||
/* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит.
|
||||
*
|
||||
* Одного pop_music_busy() там, где ждут КОНЦА мелодии, мало: между
|
||||
* заявкой (pop_sfx_play кладёт pop_mus_req) и первым звуком проходит
|
||||
* 190-230 мс — открытие файла и чтение первой полустраницы (замер и
|
||||
* разбор — docs/sound_plan.md §9). В этой щели busy ещё ложен, и
|
||||
* ожидание закончилось бы, не начавшись.
|
||||
*
|
||||
* Три признака подряд закрывают её без единой подобранной константы:
|
||||
* pop_music_service обнуляет req и тут же взводит ld_busy в load_begin,
|
||||
* а тот держится до самой записи pop_mus_left. Снаружи переход
|
||||
* «запрошен -> грузится -> звучит» непрерывен, потому что между этими
|
||||
* операторами главный цикл не выполняется.
|
||||
*
|
||||
* И наоборот: если трека нет или память кончилась, все три ложны сразу —
|
||||
* ждать нечего, как и должно быть. */
|
||||
uint8_t pop_music_active(void) __banked
|
||||
{
|
||||
return (uint8_t)(pop_mus_req || ld_busy || pop_mus_left);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,47 @@
|
||||
* либо резать, либо стримить, и это отдельная задача. */
|
||||
#define POP_MUS_PAGES 20
|
||||
|
||||
/* --- ИНДЕКС НАБОРА (MUS/mus.idx, формат PMI1) ------------------------- *
|
||||
*
|
||||
* Длины треков и длительности реплик приезжают С ДИСКА, а не из EXE: так
|
||||
* набор записей (flac/mp3/ogg/mt32) меняется без перекомпиляции игры.
|
||||
* Формат пишет tools/pop_idx.py, он же покрыт make test-tools.
|
||||
*
|
||||
* Индекс живёт в ОДНОЙ странице EMM, а не в W2: таблица на 57 записей —
|
||||
* 228 байт, а свободной кучи в резиденте порядка двух сотен. Наружу
|
||||
* указатель на EMM не выдаётся: pop_music_info копирует запись в буфер
|
||||
* вызывающего, на короткое время подключая страницу в W0. */
|
||||
#define POP_MUS_COUNT 57 /* пространство id оригинала, 0..56 */
|
||||
#define POP_MUS_IDX_REC 4 /* blocks (u16) + ticks60 (u16) */
|
||||
#define POP_MUS_IDX_HDR 16
|
||||
#define POP_MUS_IDX_SIZE 512 /* сектор DSS */
|
||||
/* Индекс кладётся в страницу СО СМЕЩЕНИЯ 0x100, а не с нуля. Причина в
|
||||
* gfx_w0_page_prepare: он делает страницу безопасной в W0, записывая в
|
||||
* неё переход на IRQ-восстановитель (0x38) и RETN для NMI (0x66). С
|
||||
* нуля эти байты легли бы прямо в записи (id 10 и 21) и испортили
|
||||
* индекс; со смещением 0x100 стабы и данные не пересекаются, и работает
|
||||
* штатная защита от прерываний, а не запрет их. Тот же приём и по той
|
||||
* же причине — в pop_config.c (CFG_BASE). */
|
||||
#define POP_MUS_IDX_BASE 0x100
|
||||
|
||||
typedef struct {
|
||||
uint16_t blocks; /* длина трека в порциях насоса по 128 байт */
|
||||
uint16_t ticks; /* та же длина в тиках сцены (60 Гц оригинала) */
|
||||
} pop_mus_info_t;
|
||||
|
||||
/* Поднять индекс (один раз за запуск). 0 — ок, -1 — нет файла либо он
|
||||
* не той версии; во втором случае музыки не будет, но игра работает. */
|
||||
int8_t pop_music_index_load(void) __banked;
|
||||
|
||||
/* Запись трека в буфер вызывающего. -1 — индекса нет, id вне диапазона
|
||||
* или трека нет в наборе (тогда поля нулевые). */
|
||||
int8_t pop_music_info(uint8_t id, pop_mus_info_t *out) __banked;
|
||||
|
||||
/* Сколько EMM-страниц занимает трек такой длины. Считается, а НЕ хранится
|
||||
* в индексе: дублирование однажды разъедется с blocks. */
|
||||
#define pop_music_pages(blocks) \
|
||||
(uint16_t)(((blocks) >> 7) + (((blocks) & 127) != 0))
|
||||
|
||||
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
|
||||
#define POP_MUS_INTRO 54 /* тема заставки, звучит на титрах */
|
||||
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
|
||||
@@ -102,4 +143,8 @@ void pop_music_ring_stop(void) __banked;
|
||||
/* Трек ещё звучит? 0 — доиграл или не запускался. */
|
||||
uint8_t pop_music_busy(void) __banked;
|
||||
|
||||
/* «Трек в работе»: заявка/загрузка/звучание. Для мест, где ждут КОНЦА
|
||||
* мелодии, — busy там ложен в окне запуска (см. pop_music.c). */
|
||||
uint8_t pop_music_active(void) __banked;
|
||||
|
||||
#endif /* POP_MUSIC_H */
|
||||
|
||||
@@ -366,7 +366,6 @@ int main(void)
|
||||
* девелоперская сборка может до title держать дворец LEVEL=11. */
|
||||
if (pop_app_state == POP_APP_DEMO) {
|
||||
pop_next_level = 0;
|
||||
pop_endmus_left = 0;
|
||||
pop_seamless = 0;
|
||||
pop_checkpoint = 0;
|
||||
if (pop_level_switch() != 0) {
|
||||
@@ -787,14 +786,22 @@ int main(void)
|
||||
* сколько звучит трек конца уровня; мы же уходили на смену
|
||||
* уровня сразу и обрывали мелодию на первых нотах.
|
||||
*
|
||||
* «Звучит» проверяем двумя способами: pop_music_busy() пока
|
||||
* трек играет и счётчик pop_endmus_left по длине записи (его
|
||||
* взводит play_seq вместе с самим треком) — иначе с
|
||||
* ВЫКЛЮЧЕННОЙ музыкой паузы не было бы вовсе, а оригинал
|
||||
* выдерживает её и молча. */
|
||||
if (pop_endmus_left) pop_endmus_left--;
|
||||
if (!pop_endmus_left && !pop_music_busy() &&
|
||||
pop_level_change(back) != 0) {
|
||||
* «Звучит» — это pop_music_active(), а не busy: между заявкой
|
||||
* на трек и первой нотой проходит 190-230 мс на открытие
|
||||
* файла и чтение первой полустраницы, и в этой щели busy ещё
|
||||
* ложен (замер — docs/sound_plan.md §9). Раньше щель
|
||||
* закрывал счётчик по длине записи, но он же тянул за собой
|
||||
* зависимость EXE от набора музыки и жёсткий делитель `/4`,
|
||||
* враньё которого вылезало в быстрых режимах (там кадр 3
|
||||
* луча, а не 4). Состояние «заявка/загрузка/звучание»
|
||||
* закрывает щель без единой константы.
|
||||
*
|
||||
* ЗВУК ВЫКЛЮЧЕН — ПАУЗЫ НЕТ, и это тоже верно: pop_sfx_play
|
||||
* не кладёт заявку при !pop_snd_ok, все три признака ложны, и
|
||||
* уровень меняется сразу. Ровно так в оригинале, где трек
|
||||
* ставится под `if (is_sound_on)` (seg006:651), а ожидание —
|
||||
* `check_sound_playing()` (seg003:387). */
|
||||
if (!pop_music_active() && pop_level_change(back) != 0) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,6 +1157,10 @@ int pop_boot(void) __banked
|
||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
|
||||
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
|
||||
/* Индекс музыкального набора — одна страница EMM. Без него музыки не
|
||||
* будет (pop_music_info вернёт -1 на любой трек), но игра работает:
|
||||
* это необязательный ресурс, как и сами треки. */
|
||||
(void)pop_music_index_load();
|
||||
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
|
||||
if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
|
||||
puts("kid atlas load failed");
|
||||
|
||||
@@ -30,6 +30,30 @@ ABI SDCC z80 (uint8 + uint16 + uint16 без выравнивания). Заг
|
||||
молча разъедет всю раскладку; на стороне C это проверяется статически.
|
||||
|
||||
Нулевая `len` означает, что у этого id нет оцифровки (это музыка).
|
||||
|
||||
PMI1 — индекс МУЗЫКИ, тоже один сектор:
|
||||
|
||||
0..3 'PMI1' магия и версия
|
||||
4 57 число плотных записей, id 0..56
|
||||
5 4 размер записи
|
||||
6 7 log2 размера блока
|
||||
7 1 формат PCM
|
||||
8..15 0 резерв
|
||||
16..243 57 записей по 4 байта: blocks (u16), ticks60 (u16)
|
||||
244..511 0 добивка до сектора
|
||||
|
||||
Числа страниц здесь НЕТ намеренно: это производная от `blocks`, и
|
||||
дублирование однажды разъедется с ним. Загрузчик считает сам:
|
||||
|
||||
pages = blocks / 128 + ((blocks & 127) != 0)
|
||||
|
||||
`ticks60` — длительность в тиках сцены (60 Гц оригинала), нужная там, где
|
||||
сценарий ждёт конца реплики. Считается из blocks отношением 40/57 —
|
||||
обратным к тому, которым шкала сцены переводит тики в порции насоса.
|
||||
Хранится ОТДЕЛЬНО, а не выводится в рантайме: так момент окончания
|
||||
реплики можно будет уточнить независимо от технического хвоста PCM.
|
||||
|
||||
Нулевой `blocks` означает, что трека с таким id в наборе нет.
|
||||
"""
|
||||
|
||||
import struct
|
||||
@@ -124,3 +148,83 @@ def _check_entry(i, page, off, ln, pages):
|
||||
if last >= pages:
|
||||
raise IdxError("запись %d выходит за набор: кончается в стр.%d из %d"
|
||||
% (i, last, pages))
|
||||
|
||||
|
||||
# --- PMI1: музыка ----------------------------------------------------------
|
||||
MUS_MAGIC = b"PMI1"
|
||||
MUS_COUNT = 57 # то же пространство id оригинала
|
||||
MUS_REC = 4
|
||||
MUS_HDR = 16
|
||||
# Потоковый трек (финальная тема) заведомо длиннее обычных, и предел ему
|
||||
# ставят не страницы загрузчика, а восьмибитные счётчики страниц в коде.
|
||||
MUS_MAX_PAGES = 255
|
||||
|
||||
|
||||
def mus_ticks(blocks):
|
||||
"""Длительность в тиках сцены (60 Гц) по длине в блоках.
|
||||
|
||||
Отношение 40/57 — обратное к тому, которым шкала сцены переводит тики
|
||||
в порции насоса (57/40, pop_intro.c). Берём именно его, а не точное
|
||||
10937,5/128/60: длина реплики и ход шкалы обязаны считаться ОДНОЙ
|
||||
арифметикой, иначе они разъедутся на длинных треках. Округление к
|
||||
ближайшему: отбрасывание вниз теряло по тику на реплику.
|
||||
"""
|
||||
return (blocks * 40 + 28) // 57
|
||||
|
||||
|
||||
def mus_pack(entries):
|
||||
"""Собрать PMI1 из 57 записей (blocks, ticks60)."""
|
||||
if len(entries) != MUS_COUNT:
|
||||
raise IdxError("записей должно быть %d, а не %d"
|
||||
% (MUS_COUNT, len(entries)))
|
||||
out = bytearray(SECTOR)
|
||||
out[0:4] = MUS_MAGIC
|
||||
out[4] = MUS_COUNT
|
||||
out[5] = MUS_REC
|
||||
out[6] = BLOCK_LOG2
|
||||
out[7] = PCM_U8_MONO_10K9
|
||||
for i, (blocks, ticks) in enumerate(entries):
|
||||
_check_mus(i, blocks, ticks)
|
||||
struct.pack_into("<HH", out, MUS_HDR + i * MUS_REC, blocks, ticks)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def mus_unpack(data):
|
||||
"""Разобрать PMI1 -> записи. Проверки те же, что у Z80."""
|
||||
if len(data) != SECTOR:
|
||||
raise IdxError("размер %d, а должен быть сектор %d" % (len(data), SECTOR))
|
||||
if data[0:4] != MUS_MAGIC:
|
||||
raise IdxError("не PMI1: %r" % (bytes(data[0:4]),))
|
||||
if data[4] != MUS_COUNT:
|
||||
raise IdxError("записей %d, ожидалось %d" % (data[4], MUS_COUNT))
|
||||
if data[5] != MUS_REC:
|
||||
raise IdxError("размер записи %d, ожидался %d" % (data[5], MUS_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))
|
||||
entries = []
|
||||
for i in range(MUS_COUNT):
|
||||
blocks, ticks = struct.unpack_from("<HH", data, MUS_HDR + i * MUS_REC)
|
||||
_check_mus(i, blocks, ticks)
|
||||
entries.append((blocks, ticks))
|
||||
return entries
|
||||
|
||||
|
||||
def mus_pages(blocks):
|
||||
"""Сколько EMM-страниц занимает трек такой длины."""
|
||||
return (blocks + 127) // 128
|
||||
|
||||
|
||||
def _check_mus(i, blocks, ticks):
|
||||
if blocks == 0:
|
||||
if ticks:
|
||||
raise IdxError("запись %d: трека нет, но тики ненулевые" % i)
|
||||
return
|
||||
if mus_pages(blocks) > MUS_MAX_PAGES:
|
||||
raise IdxError("запись %d: %d блоков — больше %d страниц, счётчики "
|
||||
"страниц в коде восьмибитные"
|
||||
% (i, blocks, MUS_MAX_PAGES))
|
||||
if ticks == 0:
|
||||
raise IdxError("запись %d: трек есть, а длительность нулевая" % i)
|
||||
|
||||
@@ -23,7 +23,7 @@ open 51,4 мс против 32,6 мс на чтение 16 КБ. Этот же
|
||||
блоками по 128 байт, и остаток «не полного блока» ему пришлось бы
|
||||
досчитывать в прерывании.
|
||||
|
||||
Длина трека уходит в pop_music_tbl.h блоками по 128 байт, а не байтами:
|
||||
Длина трека уходит в MUS/mus.idx блоками по 128 байт, а не байтами:
|
||||
самый длинный трек (won, 115 с) — это 1,2 МБ, что не влезает в uint16, а
|
||||
в блоках даёт 9855. 32-битной арифметики на Z80 избегаем (см. memory
|
||||
avoid_32bit_arith_z80).
|
||||
@@ -35,14 +35,14 @@ from pathlib import Path
|
||||
import sys as _sys, os as _os
|
||||
_sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
|
||||
import paths as P # раскладка каталогов SprPoP (tools/paths.py)
|
||||
import pop_idx # формат дискового индекса PMI1 (tools/pop_idx.py)
|
||||
|
||||
# Формат источника выбирается ключом --format (по умолчанию flac — он же
|
||||
# лучший по качеству исходник). Каталог и расширение знает tools/paths.py.
|
||||
FMT = "flac"
|
||||
SRC_DIR, SRC_EXT = P.music_src(FMT)
|
||||
OUT_DIR = P.atl("music")
|
||||
OUT_H = P.GEN / "pop_music_tbl.h"
|
||||
OUT_TICKS = P.GEN / "pop_music_ticks.h"
|
||||
OUT_IDX = "mus.idx" # дисковый индекс набора, ложится в MUS/ рядом с треками
|
||||
|
||||
CBL_RATE = 10937.5 # CBL_FREQ_10K9 — частота железа, не 11 000
|
||||
PAGE = 16384 # EMM-страница
|
||||
@@ -125,98 +125,19 @@ def main() -> None:
|
||||
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 — каталог музыкальных треков, СГЕНЕРИРОВАН.",
|
||||
f" * Источник: assets/orig/PoP1_DOS_music/{P.MUSIC_FORMATS[FMT][0]}"
|
||||
" (записи DOS-версии).",
|
||||
" * Генератор: tools/pop_pack_music.py. Руками не править.",
|
||||
" *",
|
||||
" * Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —",
|
||||
" * сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека",
|
||||
" * в порциях насоса по 128 байт (в байтах не влезло бы в uint16).",
|
||||
" *",
|
||||
" * Длительности реплик в тиках вынесены в pop_music_ticks.h: таблица",
|
||||
" * ниже — 171 байт ДАННЫХ, и модулю, которому нужна одна константа,",
|
||||
" * платить за неё копией таблицы незачем (поймано на pop_intro.c:",
|
||||
" * +172 байта в банк 11).",
|
||||
" *",
|
||||
" * POP_MUS_TICKS_<id> — та же длина в ТИКАХ СЦЕНЫ (60 Гц оригинала).",
|
||||
" * Нужна там, где сценарий ждёт окончания реплики: у оригинала это",
|
||||
" * `do { frame(1) } while (check_sound_playing())`, у нас — пауза",
|
||||
" * длиной в нашу запись. Считается ЗДЕСЬ, а не в C, по двум",
|
||||
" * причинам: blocks*40 переполняет uint16 на won (9855 порций), и",
|
||||
" * число обязано соответствовать НАБОРУ, которым собрали (flac и",
|
||||
" * mt32 — разные исполнения разной длины).",
|
||||
" *",
|
||||
" * Отношение 40/57 — обратное к тому, которым шкала сцены переводит",
|
||||
" * тики в порции насоса (57/40, см. pop_intro.c). Берём именно его,",
|
||||
" * а не точное 10937,5/128/60, чтобы длина реплики и ход шкалы",
|
||||
" * считались одной и той же арифметикой. Округление — к ближайшему",
|
||||
" * (+28 перед делением): отбрасывание вниз теряло по тику на реплику.",
|
||||
" */",
|
||||
"#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)}",
|
||||
"",
|
||||
]
|
||||
lines += [
|
||||
"",
|
||||
"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))
|
||||
|
||||
# Отдельный заголовок ТОЛЬКО с длительностями реплик. Данных в нём нет,
|
||||
# поэтому его может включать кто угодно, ничего не утаскивая в свой банк.
|
||||
tk = [
|
||||
"/*",
|
||||
" * pop_music_ticks.h — длительности треков в ТИКАХ СЦЕНЫ, СГЕНЕРИРОВАН.",
|
||||
f" * Набор: {P.MUSIC_FORMATS[FMT][0]}. Генератор: tools/pop_pack_music.py.",
|
||||
" * Руками не править.",
|
||||
" *",
|
||||
" * Нужны там, где сценарий ждёт окончания реплики: у оригинала это",
|
||||
" * `do { frame(1) } while (check_sound_playing())`, у нас — пауза длиной",
|
||||
" * в НАШУ запись. Число обязано соответствовать набору, которым собрана",
|
||||
" * сборка: одна и та же реплика на flac и mt32 разной длины (m53 — 985",
|
||||
" * тиков против 1044), и захардкоженная константа молча ломает сцену.",
|
||||
" *",
|
||||
" * Считается здесь, а не в C: blocks*40 переполняет uint16 на won",
|
||||
" * (9855 порций). Отношение 40/57 — обратное к тому, которым шкала",
|
||||
" * сцены переводит тики в порции насоса (57/40, pop_intro.c); берём",
|
||||
" * именно его, чтобы длина реплики и ход шкалы считались одной",
|
||||
" * арифметикой. Округление к ближайшему: вниз теряло по тику.",
|
||||
" */",
|
||||
"#ifndef POP_MUSIC_TICKS_H",
|
||||
"#define POP_MUSIC_TICKS_H",
|
||||
"",
|
||||
]
|
||||
for tid, name, _c, blocks in rows:
|
||||
tk.append(f"#define POP_MUS_TICKS_{tid:<3d} {(blocks * 40 + 28) // 57:5d}"
|
||||
f" /* {name} */")
|
||||
tk += ["", "#endif /* POP_MUSIC_TICKS_H */", ""]
|
||||
OUT_TICKS.write_text("\n".join(tk))
|
||||
# ДИСКОВЫЙ ИНДЕКС набора: длины треков и длительности реплик. Пока
|
||||
# печатаются и заголовки выше — переход на runtime идёт по шагам, и
|
||||
# индекс обязан описывать ровно то же, что они (docs/music_runtime_index_plan.md).
|
||||
# Тики считает pop_idx.mus_ticks — общая формула для обеих сторон.
|
||||
if len(rows) != len(TRACKS):
|
||||
print(f" ВНИМАНИЕ: упаковано {len(rows)} треков из {len(TRACKS)} — "
|
||||
f"индекс описывает только их")
|
||||
entries = [(0, 0)] * pop_idx.MUS_COUNT
|
||||
for tid, _name, _chunks, blocks in rows:
|
||||
entries[tid] = (blocks, pop_idx.mus_ticks(blocks))
|
||||
(OUT_DIR / OUT_IDX).write_bytes(pop_idx.mus_pack(entries))
|
||||
print(f"индекс: {OUT_DIR / OUT_IDX}")
|
||||
print(f"итого {total / 1024:.0f} КБ в {OUT_DIR}")
|
||||
print(f"каталог: {OUT_H}")
|
||||
print(f"тики: {OUT_TICKS}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -140,5 +140,105 @@ class SndPackGuards(unittest.TestCase):
|
||||
X.snd_pack(e, 1)
|
||||
|
||||
|
||||
def mus_empty():
|
||||
return [(0, 0)] * X.MUS_COUNT
|
||||
|
||||
|
||||
class MusFormat(unittest.TestCase):
|
||||
"""PMI1: раскладка байтов индекса музыки."""
|
||||
|
||||
def test_size_is_one_sector(self):
|
||||
self.assertEqual(len(X.mus_pack(mus_empty())), X.SECTOR)
|
||||
|
||||
def test_header_fields(self):
|
||||
d = X.mus_pack(mus_empty())
|
||||
self.assertEqual(d[0:4], b"PMI1")
|
||||
self.assertEqual(d[4], 57)
|
||||
self.assertEqual(d[5], 4)
|
||||
self.assertEqual(d[6], 7)
|
||||
self.assertEqual(d[7], 1)
|
||||
self.assertEqual(d[8:16], b"\0" * 8)
|
||||
|
||||
def test_entry_is_little_endian_at_fixed_offset(self):
|
||||
e = mus_empty()
|
||||
e[41] = (1043, 732)
|
||||
d = X.mus_pack(e)
|
||||
at = X.MUS_HDR + 41 * X.MUS_REC
|
||||
self.assertEqual(d[at:at + 2], b"\x13\x04") # blocks LE
|
||||
self.assertEqual(d[at + 2:at + 4], b"\xdc\x02") # ticks60 LE
|
||||
self.assertEqual(struct.unpack_from("<HH", d, at), (1043, 732))
|
||||
|
||||
def test_roundtrip(self):
|
||||
e = mus_empty()
|
||||
e[24] = (305, X.mus_ticks(305))
|
||||
e[56] = (9865, X.mus_ticks(9865))
|
||||
self.assertEqual(X.mus_unpack(X.mus_pack(e)), e)
|
||||
|
||||
|
||||
class MusTicks(unittest.TestCase):
|
||||
"""Формула тиков закреплена ЗНАЧЕНИЯМИ нынешней сборки (набор flac).
|
||||
|
||||
Она переехала из упаковщика в общий модуль, и разойтись ей нельзя:
|
||||
по этим числам сцены отмеряют паузы, пока они не перешли на ожидание
|
||||
звука. Слева — blocks из gen/pop_music_tbl.h, справа — тики из
|
||||
gen/pop_music_ticks.h.
|
||||
"""
|
||||
|
||||
CASES = ((624, 438), # m32 end_level_4
|
||||
(1043, 732), # m41 end_level
|
||||
(1184, 831), # m50 princess
|
||||
(1403, 985), # m53 Jaffar
|
||||
(9865, 6923), # m56 won — самый длинный
|
||||
(305, 214), # m24 regular_death — самый короткий из игровых
|
||||
(196, 138)) # m25
|
||||
|
||||
def test_matches_current_build(self):
|
||||
for blocks, ticks in self.CASES:
|
||||
self.assertEqual(X.mus_ticks(blocks), ticks, "blocks=%d" % blocks)
|
||||
|
||||
def test_no_uint16_overflow_on_longest(self):
|
||||
"""blocks*40 переполнил бы uint16 — формула обязана это переживать."""
|
||||
self.assertLess(X.mus_ticks(65535), 65536)
|
||||
|
||||
def test_pages_rounds_up(self):
|
||||
self.assertEqual(X.mus_pages(128), 1)
|
||||
self.assertEqual(X.mus_pages(129), 2)
|
||||
self.assertEqual(X.mus_pages(9865), 78) # m56 flac
|
||||
self.assertEqual(X.mus_pages(10462), 82) # m56 mt32 — длиннее
|
||||
|
||||
|
||||
class MusRejects(unittest.TestCase):
|
||||
def bad(self, mutate):
|
||||
e = mus_empty()
|
||||
e[41] = (1043, 732)
|
||||
d = bytearray(X.mus_pack(e))
|
||||
mutate(d)
|
||||
with self.assertRaises(X.IdxError):
|
||||
X.mus_unpack(bytes(d))
|
||||
|
||||
def test_wrong_magic(self):
|
||||
self.bad(lambda d: d.__setitem__(slice(0, 4), b"PSI1"))
|
||||
|
||||
def test_wrong_count(self):
|
||||
self.bad(lambda d: d.__setitem__(4, 56))
|
||||
|
||||
def test_wrong_record_size(self):
|
||||
self.bad(lambda d: d.__setitem__(5, 5))
|
||||
|
||||
def test_unknown_pcm_format(self):
|
||||
self.bad(lambda d: d.__setitem__(7, 3))
|
||||
|
||||
def test_ticks_without_track(self):
|
||||
self.bad(lambda d: struct.pack_into("<HH", d, X.MUS_HDR + 5 * 4, 0, 100))
|
||||
|
||||
def test_track_without_ticks(self):
|
||||
self.bad(lambda d: struct.pack_into("<HH", d, X.MUS_HDR + 5 * 4, 500, 0))
|
||||
|
||||
def test_track_longer_than_page_counters(self):
|
||||
"""Больше 255 страниц не переживут восьмибитные счётчики в коде."""
|
||||
self.bad(lambda d: struct.pack_into("<HH", d, X.MUS_HDR + 5 * 4,
|
||||
65535, 1000))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user