/* * pop_music.c — загрузка и запуск музыкального трека, БАНК 9. * * Здесь только холодная часть: выделить страницы EMM, вычитать в них куски * m_.bin и выставить курсор. Сам вывод делает насос CBL в * резиденте (pop_sfx.c): когда эффект не звучит, он берёт байты отсюда, а * не из блока тишины. Контракт и мотивация — в pop_music.h. * * Имя файла собираем вручную, без sprintf: printf-семейство в резидент не * тянем, а тут и формат-то из двух чисел. */ #include #include #include #include #include #include /* gfx_w0_map/unmap — доступ к странице индекса */ #include "pop_music.h" #include "pop_sfx.h" #include "_pop_sfx.h" /* pop_snd_ok — открыт ли вывод (см. pop_music_busy) */ #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, для * одного слота: 40 байт W2 у игры лишних нет. */ #define MUS_SLOTS 2 static uint8_t slot_blk[MUS_SLOTS]; /* блок EMM, 0 — слот пуст */ static uint8_t slot_pages[MUS_SLOTS]; static uint16_t slot_blocks[MUS_SLOTS]; static uint8_t slot_id[MUS_SLOTS]; static uint8_t mus_cur; /* слот, что играет/играл */ static uint8_t mus_ready; /* слот+1, готовый к запуску */ /* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */ static void mus_name(char *buf, uint8_t id) { buf[0] = 'M'; buf[1] = 'U'; buf[2] = 'S'; buf[3] = '\\'; buf[4] = 'm'; buf[5] = (char)('0' + id / 10); buf[6] = (char)('0' + id % 10); buf[7] = '.'; buf[8] = 'b'; buf[9] = 'i'; buf[10] = 'n'; buf[11] = 0; } /* ld_busy отдельно от ld_fd: инициализаторов у статиков в банке нет * (crt0 зануляет _DATA), а «нет открытого файла» — это не ноль. */ static uint8_t ld_busy; static int ld_fd; static uint8_t ld_slot; 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) { if (slot_blk[n]) { mem_free_block(slot_blk[n]); slot_blk[n] = 0; } slot_pages[n] = 0; slot_blocks[n] = 0; } /* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- * * * Финальная тема (`won`, 115 с) — это 1,2 МБ и 78 страниц EMM. Держать её * в памяти нельзя ни при каком бюджете, поэтому она играется из НЕСКОЛЬКИХ * страниц по кругу: насос идёт по кольцу (pop_mus_ring), а загрузчик * дочитывает файл в те страницы, которые насос уже прошёл. * * Кто кого догоняет. Страница звучит 1,5 с, а читается 33 мс — запас * сорокакратный, так что «не успеть» можно только застряв на секунду. * Дистанцию считаем без отдельных счётчиков: страница ровно 128 блоков * насоса, поэтому проигранных страниц = (всего блоков - осталось) / 128. * Пока прочитано меньше, чем проиграно плюс размер кольца, — в кольце есть * свободный слот, и следующую страницу можно читать. * * Файл читаем ПОСЛЕДОВАТЕЛЬНО, без lseek: страницы идут по порядку, и fd * сам стоит там, где надо. */ #define RING_PAGES 6 /* 96 КБ = 9 с звучания, запас с лихвой */ static uint8_t ring_on; /* идёт потоковый трек */ static uint8_t ring_blk; /* EMM-блок кольца */ static uint8_t ring_fd_open; static int ring_fd; static uint8_t ring_file_pages; /* страниц в файле */ static uint8_t ring_loaded; /* сколько уже прочитано */ static uint16_t ring_total; /* длина трека в блоках насоса */ void pop_music_ring_stop(void) __banked; void pop_music_stop(void) __banked { pop_mus_left = 0; } void pop_music_free(void) __banked { if (ring_on) pop_music_ring_stop(); if (ld_busy) { close(ld_fd); ld_busy = 0; } pop_mus_left = 0; slot_release(0); slot_release(1); mus_ready = 0; } /* --- загрузка --------------------------------------------------------- * * ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и * следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком * это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров. * Шаг стоит 16 мс (полстраницы, см. pop_music_load_step) — меньше одного * кадрового интервала, и подкачка растворяется в анимации. Кто может позволить себе паузу (титры, чёрный экран * между сценами), зовёт pop_music_load — тот же цикл, но целиком. */ static void load_abort(void) { if (ld_busy) close(ld_fd); ld_busy = 0; ld_half = 0; slot_release(ld_slot); } 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; /* Длину и число страниц знает ИНДЕКС НАБОРА, а не 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; /* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы * из прерывания, и отобрать их у него нельзя. */ n = (uint8_t)(mus_cur ^ 1); slot_release(n); mus_name(name, id); /* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней * страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс * на 16 КБ), и платить его на каждую страницу — чистые потери. */ POP_PATH_CALL(ld_fd, open(name, O_RDONLY)); if (ld_fd < 0) return -1; slot_blk[n] = mem_alloc_pages(pages); if (!slot_blk[n]) { close(ld_fd); return -1; } ld_busy = 1; ld_slot = n; ld_next = 0; ld_half = 0; ld_pages = pages; ld_id = id; ld_blocks = info.blocks; return 0; } /* ШАГ = ПОЛСТРАНИЦЫ (8 КБ, ~16 мс), а не страница (~33 мс). * * Шаг вызывают раз в кадр из циклов, где кадр разложен на блоки по одному * кадровому интервалу (20 мс): катсцены после перехода на блочную * отрисовку, см. pop_intro.c/pv_restore_bg. Целая страница в интервал не * влезала и незаметно растягивала кадр сцены; половина влезает, а цена — * вдвое больше вызовов (11 страниц реплики = 22 кадра вместо 11, то есть * меньше полусекунды). Игровому циклу это безразлично: там логический * кадр 80-100 мс. */ int8_t pop_music_load_step(void) __banked { uint8_t p; if (!ld_busy) return 1; /* нечего грузить */ p = mem_get_page(slot_blk[ld_slot], ld_next); /* Последняя страница короче — читаем сколько есть (вторая половина * может дать и ноль байт); ошибку ловим по отрицательному коду. */ if (!p || bank_read_page(ld_fd, p, ld_half ? 8192 : 0, 8192) < 0) { load_abort(); return -1; } if (!ld_half) { ld_half = 1; return 0; } /* дочитаем в следующий раз */ ld_half = 0; if (++ld_next < ld_pages) return 0; close(ld_fd); ld_busy = 0; slot_pages[ld_slot] = ld_pages; slot_blocks[ld_slot] = ld_blocks; slot_id[ld_slot] = ld_id; mus_ready = (uint8_t)(ld_slot + 1); return 1; } uint8_t pop_music_loading(void) __banked { return ld_busy; } int8_t pop_music_load(uint8_t id) __banked { int8_t r; if (pop_music_load_begin(id) != 0) return -1; do { r = pop_music_load_step(); } while (r == 0); return (r < 0) ? -1 : 0; } /* ЗАПУСК С ПОДКАЧКОЙ НА ХОДУ — для игровых джинглов (смерть, зелья, меч). * Ждать полной загрузки нельзя: это фриз на треть секунды посреди игры. * Зато можно начать сразу после ПЕРВОЙ страницы: она звучит 1,5 с, а * следующая читается 33 мс — запас сорокакратный. Дальше вызывающий * доливает по странице за кадр (pop_music_load_step, pop_music_loading). * * Номера страниц известны сразу после mem_alloc_pages, поэтому таблицу для * насоса заполняем целиком: данные в этих страницах появятся раньше, чем * он до них доберётся. * 0 — играет, -1 — трека нет или память кончилась. */ int8_t pop_music_stream(uint8_t id) __banked { uint8_t i, n; if (ring_on) pop_music_ring_stop(); /* см. pop_music_play */ if (pop_music_load_begin(id) != 0) return -1; if (pop_music_load_step() < 0) return -1; /* первая страница */ n = ld_slot; pop_mus_left = 0; /* насос — на тишину */ for (i = 0; i < ld_pages; i++) pop_mus_page[i] = mem_get_page(slot_blk[n], i); if (n != mus_cur) slot_release(mus_cur); mus_cur = n; slot_pages[n] = ld_pages; slot_blocks[n] = ld_blocks; slot_id[n] = id; pop_mus_id = id; pop_mus_pg = 0; pop_mus_ptr = 0; pop_mus_left = slot_blocks[n]; return 0; } void pop_music_play(void) __banked { uint8_t n, i; if (!mus_ready) return; /* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё * ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы * своей первой третью без конца (поймано на титрах после победы). */ if (ring_on) pop_music_ring_stop(); n = (uint8_t)(mus_ready - 1); /* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет * взять порцию по новому индексу из старого курсора. */ pop_mus_left = 0; for (i = 0; i < slot_pages[n]; i++) pop_mus_page[i] = mem_get_page(slot_blk[n], i); if (n != mus_cur) slot_release(mus_cur); mus_cur = n; mus_ready = 0; /* Порядок важен: сначала позиция, потом длина. Насос стартует ровно * с той порции, где left стал ненулевым, и промежуточное состояние * «играем, но курсор от прошлого трека» не возникает. */ pop_mus_id = slot_id[n]; /* до left: менеджер спросит готовое */ pop_mus_pg = 0; pop_mus_ptr = 0; pop_mus_left = slot_blocks[n]; } static void ring_close_file(void) { if (ring_fd_open) { close(ring_fd); ring_fd_open = 0; } } void pop_music_ring_stop(void) __banked { pop_mus_left = 0; pop_mus_ring = 0; ring_close_file(); if (ring_blk) { mem_free_block(ring_blk); ring_blk = 0; } ring_on = 0; } 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_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); POP_PATH_CALL(ring_fd, open(name, O_RDONLY)); if (ring_fd < 0) return -1; ring_fd_open = 1; ring_blk = mem_alloc_pages(RING_PAGES); if (!ring_blk) { ring_close_file(); return -1; } ring_file_pages = (uint8_t)pop_music_pages(info.blocks); ring_total = info.blocks; /* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд * форы, и первая же тяжёлая сцена финала не догонит насос. */ for (i = 0; i < RING_PAGES; i++) { page = mem_get_page(ring_blk, i); pop_mus_page[i] = page; if (!page || (i < ring_file_pages && bank_read_page(ring_fd, page, 0, 16384) < 0)) { pop_music_ring_stop(); return -1; } } ring_loaded = (uint8_t)(ring_file_pages < RING_PAGES ? ring_file_pages : RING_PAGES); if (ring_loaded >= ring_file_pages) ring_close_file(); ring_on = 1; pop_mus_id = id; pop_mus_pg = 0; pop_mus_ptr = 0; pop_mus_ring = RING_PAGES; /* насос пойдёт по кругу */ pop_mus_left = ring_total; return 0; } /* Один шаг подкачки: страница файла в слот, который насос уже прошёл. */ static void ring_step(void) { uint16_t played; uint8_t played_pg, page; if (!ring_on) return; if (!pop_mus_left) { /* трек доиграл — отпустить память */ pop_music_ring_stop(); return; } if (ring_loaded >= ring_file_pages) return; /* файл прочитан весь */ played = (uint16_t)(ring_total - pop_mus_left); played_pg = (uint8_t)(played >> 7); /* 128 блоков на страницу */ if ((uint8_t)(ring_loaded - played_pg) >= RING_PAGES) return; /* полно */ page = pop_mus_page[ring_loaded % RING_PAGES]; if (!page || bank_read_page(ring_fd, page, 0, 16384) < 0) { pop_music_ring_stop(); return; } ring_loaded++; if (ring_loaded >= ring_file_pages) ring_close_file(); } void pop_music_service(void) __banked { /* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены. * Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала), * а диск трогаем только здесь. */ /* В ЗАСТАВОЧНОЙ ДЕМКЕ МУЗЫКИ НЕТ. Точечные условия оригинала (смерть, * убитый страж) расставлены по своим местам, но гейт нужен и общий: * демо проигрывает записанные нажатия, и любой музыкальный повод — * подобранный меч, зелье — зазвучал бы там, где оригинал играет одни * эффекты. */ if (ring_on) { ring_step(); return; } /* потоковый трек — свой путь */ if (!pop_mus_want || pop_app_state == POP_APP_DEMO) { /* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом * продолжают звучать — у них свой тумблер. */ if (pop_mus_req) pop_mus_req = 0; if (ld_busy || pop_mus_left) pop_music_free(); return; } if (pop_mus_req) { uint8_t id = pop_mus_req; pop_mus_req = 0; (void)pop_music_stream(id); } else if (ld_busy) { (void)pop_music_load_step(); } } uint8_t pop_music_busy(void) __banked { /* «ЗВУЧИТ», А НЕ «ЕСТЬ КУРСОР». Курсор трека двигает насос 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); } /* ТРЕК В РАБОТЕ — заявка лежит, идёт загрузка или он звучит. * * Одного 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); }