/* * 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" 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; } void pop_music_stop(void) __banked { pop_mus_left = 0; } void pop_music_free(void) __banked { 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 (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 (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; 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]; } void pop_music_service(void) __banked { /* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены. * Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала), * а диск трогаем только здесь. */ 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); }