Добавлены треки 54 (тема титров), 50/53/52 (три реплики сцены с
принцессой и Джафаром) — заставка звучит целиком, как в оригинале.
pop_music: два слота EMM. Реплики идут встык, паузы под загрузку нет,
поэтому следующая читается в НЕ играющий слот, а play подменяет
резидентную таблицу страниц. Своей копии таблицы слот не держит — её
хранит блок EMM. Плюс постраничная загрузка (load_begin/load_step):
страница стоит 33 мс, четверть логического кадра сцены, и подкачка
посреди анимации не видна.
Тайминги: все длительности сцен — в тиках оригинала (60 Гц), перевод в
кадры луча делает POP_T60. Пока сцены были немыми, разбег в 20 % был
незаметен; с музыкой реплика кончалась раньше картинки. Переведены
титры, история, хвост после PV и пейсинг самой PV-сцены.
Паузы, которые оригинал отмеряет концом сэмпла, теперь равны реальной
длине наших записей: конец m50 на тике 846, вход Джафара 1046, уход
2073, конец сцены 2500 (было 1959). Fade перед PV сдвинут так, чтобы
полная темнота наступала в тот же момент трека, что у SDLPoP: их fade
длиннее нашего на 123 тика, наш полосовой переход длиннее на 16.
Финальная сцена: убраны песочные часы — end_sequence_anim не трогает
hourglass_state, а reset_cutscene его обнулил. Уровень пройден, время
больше не идёт.
Проверено в MAME: цепочка 54 -> 55 -> 50 -> 53 -> 52 отыгрывается
целиком, интро доходит до демо-режима. Host-тесты: 15 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
СИМПТОМ: затемнение (fade) хрипело — одинаково с играющей музыкой и в
тишине. Ключевое наблюдение пользователя: повтор тишины обязан звучать
тишиной, значит дело не в недоливе буфера.
ПОИСК: отладочные клавиши, каждая делала ровно один кусок fade. Ожидание
кадров — чисто; чтение палитры, запись палитры и 512 вызовов
bios_get_place() (видео вообще не трогает) — скрежет во всех трёх.
Последнее и решило: виновата не палитра, а ЛЮБОЙ вызов BIOS.
ПРИЧИНА: `rst 8` раскрывается в `out ($7C),a`, который включает системное
ПЗУ и перестраивает окно 0 (MAME sprinter.cpp, update_memory: m_pages[0] +
m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому
запись в порт 0x82 из прерывания бесполезна — OTIR вычитывает ПЗУ и
отдаёт его в звук. Отсюда же старое правило «глушить CBL на время
загрузки файлов»: причина была не в том, что ESTEX долго занимает CPU.
РЕШЕНИЕ (идея пользователя): качать через W3. Он управляется только
портом 0xE2, подмену из прерывания никто не перекрывает, а BIOS во время
нашего ISR не исполняется — окно возвращается до выхода, и для него
подмена невидима. После этого BIOS безопасен везде.
* pop_sfx.c — насос берёт взаймы W3 вместо W0, чтение по 0xC000 + смещение.
* pop_ui.c — буфер палитры по фиксированному 0x4000 (эти 256 байт DSS
занимает только при загрузке программы): 256 байт со стека долой.
* libbgi/common/gfx_pal_write.c — запись палитры прямо в видеопамять,
минуя BIOS. Писалась как обход скрежета, после переноса насоса не
нужна; оставлена как более быстрый примитив (2,5 тыс. тактов на 64
цвета против 10,8 тыс. у BIOS) с честной шапкой. Адресация разобрана
по исходникам BIOS (FUNC_SCREEN.ASM): Port_Y = индекс цвета, адрес
0xC3E0 + pal*4, порядок R/G/B/Y.
* libc/video/pal_get.c, pal_load.c — в шапках зафиксировано, что BIOS
выбирает окно ПО АДРЕСУ БУФЕРА (`BIT 7,H`).
* pop_intro.c — при пропуске интро клавишей не глушился CBL, и следующая
загрузка уровня шла с открытым буфером; добавлен pop_sfx_pause.
* pop_ctrl.c — убраны отладочные «осторожные шаги» на J/L (эмуляция
Shift+стрелка для MAME), у них и стоял TODO.
Разбор целиком — docs/sound_plan.md §5. Бюджет: _CODE 23966, куча 245 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Путь C из sound_plan.md вместо ранее выбранного A (ноты спикера на AY).
Причина смены: появились готовые записи DOS-версии, и строка «нужен
синтезатор на хосте», из-за которой C считался дорогим, отпала —
осталось `ffmpeg -ac 1 -ar 10937 -f u8`. Оцифровки музыки в самой игре
нет вовсе: digisnd*.dat содержит только эффекты, музыка лежит MIDI под
Adlib, так что конвертировать ресурс игры всё равно было не из чего.
* toolchain/pop_pack_music.py — запись flac -> сырой u8 на 10 937,5 Гц
(частота CBL, чтобы её не переключать никогда), нарезка на куски
MUS\m<id>_<nn>.bin по 16 КБ: bank_load_file читает файл только целиком
и только в одну страницу. Каталог -> pop_music_tbl.h; длина трека
хранится ПОРЦИЯМИ по 128 байт — 169 КБ в uint16 не влезает, 1350
блоков влезает (32-бит арифметики на Z80 избегаем).
* pop_music.c (банк 9) — загрузка трека в EMM и курсор; pop_sfx_fill
(резидент) получил третий источник: эффект важнее музыки, музыка
важнее тишины. Эффект музыку не сбрасывает — её курсор стоит, пока
тот доигрывает, и она продолжается с места.
* story_1_absence звучит на первом экране истории, где была тишина.
Грузим ПОСЛЕ сборки картинки: чтение идёт через ESTEX, и при открытом
CBL насос не успел бы долить блок.
Проверено записью звука из MAME: корреляция огибающих с эталоном 0,836
при сдвиге 1,8 с, RMS 22,6 против 18,3 (разница — 8-битное квантование).
Эффекты двери в PV-сцене после трека звучат по-прежнему.
Бюджет: резидент +83 Б кода и +25 Б данных, запас W2 151 Б (кучи в
приложении нет, malloc не слинкован); банк 9 занят на 88 %.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
(UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
ABOUT. About показывает тот же текст, что стартовый Sprinter screen,
минус строка про клавиши — общий about_text(), чтобы экраны не
разъехались. CONTROLS собирается таблицей и центрируется по
фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
(SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
а не пункт меню. POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах. Нижняя:
порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
24 тика). Верхняя отладочная переведена с палочек на текст
«Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
своим форматированием чисел (без printf и без деления).
Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
не растёт, весь рендер в банке. Бюджет после правок не изменился
(_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
не блитятся вовсе, а вход в комнату заливает только игровое поле
(pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
статус-строке: геометрия, семантика text_time_total как идентификатора
сообщения, мигание, рестарт по истечении 36/288.
Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка на сцене 9/9 (кнопка 1,8; челюсти 1,2; ворота — в комнате 4,
тайл 1,9) вскрыла три расхождения.
1. Гейт слышимости стоял как «ворота в текущей комнате», а у оригинала
(play_door_sound_if_visible, seg007:1239) слышны ещё и ворота в комнате
СЛЕВА, если они в колонке 9; и НЕ слышны в колонке 9 своей комнаты; плюс
особый случай «уровень 3, комната 2». Сцена 9/9 — ровно первый пункт,
поэтому спуск решётки молчал. Подъём совпадал, потому что звук открытия
идёт без гейта — эта асимметрия и была подсказкой.
2. Потерян звук 7 «решётка встала»: gate_stop (seg007:05E3) зовётся из трёх
мест animate_door и каждый раз играет его через гейт. У нас во всех трёх
стояло только type = -1.
3. У trigger_button оригинала есть параметр playsound, нулевой в трёх
местах (вход на уровень, выход Джаффара, зелье «открыть»). Добавлен.
Щелчок кнопки слышен через раз — это НЕ баг: prio 0x66 против 0x10 у
челюстей, а укус занимает 465 мс из цикла 1229 мс (замерено). Разбор с
цифрами — sound_plan.md §12.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась
приземлением Кида, в оригинале доигрывает до конца, а приземления не
слышно. Оказалось, упущен целый механизм.
play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на
кадр — из нескольких выживает важнейший (меньше prio = важнее, при
равенстве последний). play_next_sound (seg000:1304) раз в кадр решает,
запускать ли: можно, только если ничего не играет ЛИБО текущий помечен
перебиваемым и новый не менее важен. Иначе номинант выбрасывается —
очереди в оригинале нет.
Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает
целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому
решётка звучит только в паузах между укусами.
Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он
определён безусловно). Створка двери уровня — единственная запись,
правимая на ходу, вынесена в отдельный байт. Добавлен пропущенный
stop_sounds на завершении открытия двери (seg007:455).
Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная
на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со
звуком 6.
Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске
после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно
только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Две разные болячки, обе разобраны записью звука MAME в WAV.
libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает
воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то,
что лежало раньше. Своими данными звук идёт лишь с третьей половины:
прерывание приходит на 128-м слоте и ставит указатель на противоположную
половину. _cbl_prime заливает буфер тишиной сразу после включения (раньше
нельзя — запись проходит только при поднятом bit7). В MAME это немо
(эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе
это ровно тот мусор, что ловился на тестовых примерах CBL.
PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один
пропущенный блок (128 сэмплов). Причина: pop_shutdown освобождал атласы и
графику через ESTEX при открытом звуке, насос не успевал долить. Звук
гасим первым действием. Проверено записью — всплеска больше нет.
Заодно записан разбор стартового всплеска (sound_plan.md §10): это не
мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land. Обрыв
одноголосьем — поведение оригинала (play_digi_sound начинается с
stop_digi, seg009.c:2402).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт Ctrl+S из SDLPoP (seg000:657). Выключение реально закрывает CBL, а
не глушит сэмпл: иначе насос продолжал бы отдавать блоки тишины и платить
те же 3,26 % процессорного времени.
Флаг намерения pop_snd_want отдельно от pop_snd_ok: последний гасит
служебная пауза на время загрузки уровня, и правь Ctrl+S только его —
первая же смена уровня вернула бы выключенный звук.
Индикация: пурпурная палочка в борте, когда звука НЕТ (включённый слышно
и так, а молчание неотличимо от «нечему звучать»).
Чит S «выдать меч» был отладочным и больше не нужен — снят, клавиша ушла
под звук.
Замер цены звука — sound_plan.md §9: 8 001 такт на прерывание при периоде
245 759 (3,26 % времени), +2,2 % к работе кадра в сцене 11/15, период
кадра не сдвинулся ни разу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота
железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 —
размер блока запроса CBL, поэтому ни один блок не пересекает границу
страницы и проигрыватель не знает слова «стык».
Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих
мест физики и из play_seq. Тишину льём свою (первый блок набора), а не
через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная.
Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от
pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и
железо крутит хвост буфера — на слух скрежет.
Разведены все места play_sound() SDLPoP, у которых есть оцифровка
(id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и
провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч,
зеркало, скелет, зелье. Таблица соответствий — docs/sound_plan.md §8.
Музыкальные id остаются с нулевой длиной до фазы музыки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Эффекты берём из MSDOS/digisnd*.dat: 28 из 31 звука совпадают побайтно с
набором SDLPoP, но три различаются не в пользу последнего — у него
sword_vs_sword короче, sword_moving другой, а spiked вообще пустой
(7 сэмплов против 5 069).
MIDI наоборот: у MSDOS формат 0 (всё слито в одну дорожку), у SDLPoP
формат 1 с 8-9 дорожками — готовое разделение голосов, если дойдём до
пути B (MIDI -> три канала AY).
Упаковщик: 31 эффект -> 10 937,5 Гц, 131 072 Б = 8 EMM-страниц. Начало
каждого звука выровнено на 128 (блок запроса CBL), а страница кратна 128,
поэтому ни один блок не пересекает границу страницы — проигрывателю не
нужна логика стыка.
Формат оригинала проверен по convert_digi_sound: один байт на кадр (моно),
байт беззнаковый с центром 0x80 — ровно формат нашего CBL. Стерео в
данных нет, каналы размножаются на выходе.
Живой замер памяти из работающей программы: занято 124 страницы из 256
(система с exe 43, наши ассеты 81), свободно 132 = 2,06 МБ. Самый
крупный ассет теперь набор Тени — 32 страницы. Эффекты WAV займут 8.
Эффекты — WAV через CBL; музыка первым заходом путь A (ноты на AY);
заставки потом WAV; музыка по ходу игры — открыто (WAV с гашением
эффектов либо путь B).
Единую частоту берём не 11 000, а ровно частоту CBL 10 937,5: тогда тон
точен (иначе −9,9 цента), а пересчитывать три файла всё равно надо.
112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц; рост целиком от
leveldoor_sliding (источник 2 750 Гц). Взамен CBL открывается один раз и
частота не меняется никогда.
Эффекты все 31 есть в WAV (digisnd, 8 бит PCM) — просьба «не спикер, а
wav» для них уже выполнена исходным планом. mt32snd оказался НЕ музыкой,
а теми же эффектами в MIDI для Roland MT-32.
У музыки WAV нет ни в одном наборе: только ноты спикера (7 КБ) или MIDI
(27 КБ). Посчитал третий путь — рендер MIDI в WAV на хосте: игровые
треки 74,7 с = 803 КБ = 50 EMM-страниц (влезает), заставки 248 с = 167
страниц (только стрим с диска).
Только спикером во всей игре остаётся один звук — blink (4 ноты).
Замеры по ассетам: 31 эффект уже 8-битным PCM на 11 000 Гц (у CBL есть
10 937,5 — расхождение 0,6 %, формат сэмпла совпадает байт в байт, то
есть конверсии нет вовсе), 103 941 Б = 6,3 EMM-страницы.
Вся музыка есть нотами PC-спикера — 7 КБ на 57 звуков, и нота там задана
в ГЕРЦАХ напрямую (проверено по speaker_callback), а не делителем PIT,
как кажется по числам. MIDI разбирать не нужно.
Отдельный таймер не нужен: секвенсор музыки двигает CBL-callback раз в
11,7 мс, а короче 12 мс во всей музыке 2 ноты из 1469.