/* * pop_music.c — загрузка и запуск музыкального трека, БАНК 9. * * Здесь только холодная часть: выделить страницы EMM, вычитать в них куски * m_.bin и выставить курсор. Сам вывод делает насос CBL в * резиденте (pop_sfx.c): когда эффект не звучит, он берёт байты отсюда, а * не из блока тишины. Контракт и мотивация — в pop_music.h. * * Имя файла собираем вручную, без sprintf: printf-семейство в резидент не * тянем, а тут и формат-то из двух чисел. */ #include #include #include #include #include "pop_music.h" #include "pop_music_tbl.h" #include "pop_sfx.h" #include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */ uint8_t pop_mus_page[POP_MUS_PAGES]; /* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ * держит — её и так хранит блок 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_pages; static uint8_t ld_id; 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 кадрах в секунду сцена встала бы на пять кадров. * Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя- * ется в анимации. Кто может позволить себе паузу (титры, чёрный экран * между сценами), зовёт pop_music_load — тот же цикл, но целиком. */ static void load_abort(void) { if (ld_busy) close(ld_fd); ld_busy = 0; slot_release(ld_slot); } int8_t pop_music_load_begin(uint8_t id) __banked { char name[12]; 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; /* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы * из прерывания, и отобрать их у него нельзя. */ n = (uint8_t)(mus_cur ^ 1); slot_release(n); mus_name(name, id); /* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней * страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс * на 16 КБ), и платить его на каждую страницу — чистые потери. */ 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_pages = pages; ld_id = id; return 0; } 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); /* Последняя страница короче — читаем сколько есть; ошибку ловим по * отрицательному коду, а не по «не 16384». */ if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) { load_abort(); return -1; } if (++ld_next < ld_pages) return 0; close(ld_fd); ld_busy = 0; slot_pages[ld_slot] = ld_pages; slot_blocks[ld_slot] = pop_mus_tbl[ld_id].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] = pop_mus_tbl[id].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]; uint8_t i, page; if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1; pop_music_free(); /* обычные слоты кольцу не нужны */ pop_music_ring_stop(); mus_name(name, id); 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 = pop_mus_tbl[id].pages; ring_total = pop_mus_tbl[id].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 { return (uint8_t)(pop_mus_left != 0); }