/* * pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL. * Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая * половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c. */ #include #include #include #include #include #include #include "pop_sfx.h" #include "pop_arc.h" #include "_pop_sfx.h" #include "_pop_file.h" /* POP_PATH_CALL — старые DSS и пути */ #include /* IRQ_DISABLE вокруг записи в W0 */ #include /* gfx_w0_map — залить блок тишины */ static uint8_t snd_blk; /* блок EMM под весь набор */ /* --- ИНДЕКС НАБОРА (SND/snd.idx, формат PSI1) ----------------------- * * * Раскладка эффектов приезжает с диска, а не из EXE: так пара * snd.idx + snd.arc заменяется целиком (SDLPoP <-> MSDOS) без * перекомпиляции игры. Писатель формата — tools/pop_idx.py, он же * покрыт тестом; проверки ниже повторяют его один в один, потому что * доверять содержимому файла нельзя: битый индекс увёл бы насос за * пределы выделенного блока EMM. * * Заголовок: 16 байт, дальше POP_SND_COUNT записей по 5 байт подряд — * ровно в том виде, в каком лежит pop_snd_tbl[], поэтому читаются они * ОДНИМ read'ом прямо туда, без разбора по полям. */ #define IDX_HDR_SIZE 16 #define IDX_BLOCK_LOG2 7 /* 1 << 7 == POP_SND_BLOCK */ #define IDX_PCM_FMT 1 /* u8 mono, CBL_FREQ_10K9 */ #define IDX_TBL_SIZE (POP_SND_COUNT * (int)sizeof(pop_snd_ent_t)) /* Имя знает сам модуль — литерал лежит в rodata банка 8, и open() читает * его, пока в W3 стоит этот же банк (тот же приём, что в pop_arc.c). */ static const char idx_path[] = "SND\\snd.idx"; /* Одна запись: ровно те проверки, что делает упаковщик. * * СЧИТАЕМ В БЛОКАХ, А НЕ В БАЙТАХ. Байтовый адрес конца эффекта * (page * 16384 + off + len) не влезает в uint16, а 32-битная арифметика * на Z80 дорога (memory avoid_32bit_arith_z80). В блоках по 128 весь * набор — не больше POP_SND_MAX_PAGES * 128 = 2048, и всё считается * шестнадцатью битами. */ static int8_t idx_entry_ok(const pop_snd_ent_t *e, uint8_t pages) { uint16_t start, end; if (e->len == 0) /* нет оцифровки — это музыка */ return (e->page == 0 && e->off == 0) ? 0 : -1; if (e->page >= pages) return -1; if (e->off >= 0x4000) return -1; if ((e->off | e->len) & (POP_SND_BLOCK - 1)) return -1; /* Блок тишины неприкосновенен: насос льёт его, когда играть нечего. */ if (e->page == POP_SND_SILENCE_PAGE && e->off < POP_SND_BLOCK) return -1; start = (uint16_t)((uint16_t)e->page << 7) + (uint16_t)(e->off >> 7); end = (uint16_t)(start + (uint16_t)(e->len >> 7)); /* Длинный эффект переливается на следующие страницы — они обязаны * быть загружены, иначе насос уйдёт в чужую память. */ if (end > (uint16_t)((uint16_t)pages << 7)) return -1; return 0; } /* Прочитать и проверить индекс. Возвращает число страниц набора либо 0. */ static uint8_t idx_load(void) { uint8_t head[IDX_HDR_SIZE]; uint8_t pages, i; int fd; POP_PATH_CALL(fd, open(idx_path, O_RDONLY)); if (fd < 0) return 0; if (read(fd, head, IDX_HDR_SIZE) != IDX_HDR_SIZE || memcmp(head, "PSI1", 4) != 0 || head[4] != POP_SND_COUNT || head[5] != (uint8_t)sizeof(pop_snd_ent_t) || head[6] != IDX_BLOCK_LOG2 || head[7] != IDX_PCM_FMT) { close(fd); return 0; } pages = head[8]; if (pages == 0 || pages > POP_SND_MAX_PAGES) { close(fd); return 0; } /* Записи ложатся прямо в резидентную таблицу: дисковый формат и * структура совпадают побайтово (проверено статически в * pop_snd_data.c и полем head[5] выше). */ if (read(fd, pop_snd_tbl, IDX_TBL_SIZE) != IDX_TBL_SIZE) { close(fd); return 0; } close(fd); for (i = 0; i < POP_SND_COUNT; i++) if (idx_entry_ok(&pop_snd_tbl[i], pages) != 0) return 0; return pages; } /* НАБОРА НЕТ — НО НАСОС ВСЁ РАВНО ОБЯЗАН РАБОТАТЬ. * * Тишину в паузах насос льёт из ПЕРВОГО БЛОКА НАБОРА, поэтому без набора * CBL не открывался вовсе. А вместе с ним вставала и МУЗЫКА: её блоки * отсчитывает тот же насос (pop_mus_left убывает только в pop_sfx_fill). * Заставка при этом висла намертво — ждала конца трека, который не * начинал играть, и pop_music_busy() не гас никогда (поймано * пользователем 2026-08-31 на образе без snd.idx). * * Поэтому при отказе индекса поднимаем ПУСТОЙ набор: одна страница EMM с * блоком тишины. Эффектов нет — все len в таблице нулевые, и * pop_sfx_play уходит по ветке «нет оцифровки», — а музыка играет. * * ЗАЛИВАЕМ ТОЛЬКО ПЕРВЫЙ БЛОК, и это не экономия: gfx_w0_page_prepare * ставит в страницу IRQ/NMI-стабы, чтобы прерывание при замапленном W0 * нашло валидный код. Заливка ВСЕЙ страницы затирает их, и первое же * прерывание уводит процессор в мусор — чёрный экран на старте (поймано * при первой попытке этого фикса). Насосу больше 128 байт и не нужно: * он берёт ровно блок тишины, да и то через W3, а не через W0. * Запись — под DI: 128 байт накрывают стаб RST38, и до unmap прерываний * быть не должно. */ static int8_t snd_silence_only(void) { uint8_t blk, page; uint8_t *w0 = (uint8_t *)0x0000; /* окно 0 — сюда ляжет страница */ memset(pop_snd_tbl, 0, sizeof(pop_snd_tbl)); blk = mem_alloc_pages(1); if (!blk) return -1; page = mem_get_page(blk, 0); if (!page) { mem_free_block(blk); return -1; } gfx_w0_page_prepare(page); IRQ_DISABLE(); gfx_w0_map(page); memset(w0, 0x80, POP_SND_BLOCK); /* 0x80 — тишина в 8-бит без знака */ gfx_w0_unmap(); IRQ_ENABLE(); pop_snd_page[POP_SND_SILENCE_PAGE] = page; snd_blk = blk; pop_snd_pages = 1; return 0; } int pop_sfx_init(void) __banked { uint8_t i, blk, pages; if (pop_snd_ok) return 0; /* СНАЧАЛА ИНДЕКС, ПОТОМ ДАННЫЕ: сколько страниц читать и куда какой * эффект лёг — знает только он. Набора без индекса не бывает: * держать в EXE запасную таблицу нельзя, иначе привязка бинарника к * конкретному набору вернётся через чёрный ход. */ pages = idx_load(); if (!pages) { /* Набор эффектов не поднялся — но звук как таковой поднять надо, * иначе встанет музыка (см. snd_silence_only). Возвращаем -1: * эффектов действительно нет, и вызывающий вправе об этом знать. */ if (snd_silence_only() != 0) return -1; pop_snd_want = 1; return -1; } /* Одним блоком: страницы блока идут подряд по логическому индексу, и * таблица звуков адресует их именно так (page = смещение >> 14). * * Из АРХИВА, а не десятью файлами: `open` стоит 51,4 мс против 32,6 мс * на чтение 16 КБ (замер 2026-08-25), то есть девять лишних открытий — * это почти полсекунды на ровном месте. Таблица живёт на стеке, см. * pop_arc.h. */ { pop_arc_t arc; uint8_t tbl[POP_SND_MAX_PAGES * POP_ARC_REC]; /* Архив обязан содержать РОВНО столько страниц, сколько обещал * индекс: расходящейся паре доверять нельзя — она означает, что * snd.idx и snd.arc из разных наборов. */ if (pop_arc_open_id(&arc, POP_ARC_SND, tbl, POP_SND_MAX_PAGES) != (int8_t)pages) return -1; blk = mem_alloc_pages(pages); if (!blk) { pop_arc_close(&arc); return -1; } for (i = 0; i < pages; i++) { pop_snd_page[i] = mem_get_page(blk, i); if (!pop_snd_page[i] || pop_arc_read(&arc, tbl, i, 0, pop_snd_page[i], 0, 0) != 16384) { pop_arc_close(&arc); mem_free_block(blk); return -1; } } pop_arc_close(&arc); } snd_blk = blk; /* Число страниц ставим ПОСЛЕДНИМ: до этого момента насос обязан * видеть 0 и не пытаться ходить по полузаполненной таблице. */ pop_snd_pages = pages; pop_snd_want = 1; /* набор есть — звук по умолчанию включён */ return 0; } /* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика. * * Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в * диск надолго, насос не успевает долить очередные 128 байт, и железо * крутит по кругу хвост своего 256-байтового буфера. На слух это * сплошной скрежет (поймано пользователем на старте, 2026-08-20). * Поэтому загрузка страниц и открытие CBL разведены: грузим молча, * включаем звук последним действием, а на время загрузки уровня глушим * (pop_sfx_pause). * * Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один * (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит. * cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок * набора), и просить у libc буфер через malloc не надо — куча в резиденте * W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не * приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */ int pop_sfx_start(void) __banked { if (pop_snd_ok) return 0; if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */ if (!snd_blk) return -1; /* набор не загружен */ if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR, pop_sfx_fill) != 0) return -1; pop_snd_ok = 1; return 0; } void pop_sfx_pause(void) __banked { if (!pop_snd_ok) return; pop_sfx_stop(); cbl_close(); pop_snd_ok = 0; } void pop_sfx_close(void) __banked { pop_sfx_pause(); if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; } } /* Ctrl+S — порт `turn_sound_on_off((!is_sound_on) * 15)` из SDLPoP * (seg000:657). Выключение реально ЗАКРЫВАЕТ CBL, а не глушит сэмпл: * иначе насос продолжал бы отдавать блоки тишины 85 раз в секунду, а это * 3,3 % процессорного времени ни за что (замер: 8 001 такт на прерывание * при периоде 245 759). Открытие обратно дешёвое — cbl_open только * настраивает железо, страницы набора никуда не девались. */ void pop_sfx_toggle(void) __banked { if (pop_snd_want) { pop_sfx_pause(); /* pause сам не трогает want... */ pop_snd_want = 0; /* ...поэтому гасим его следом */ } else { pop_snd_want = 1; /* Набора может не быть вовсе (файлы не прочитались, страницы не * дались) — тогда откатываем флаг, чтобы индикация не врала. */ if (pop_sfx_start() != 0) pop_snd_want = 0; } }