Музыка: PCM-трек через CBL, первый экран истории озвучен
Путь 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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Звук в порте PoP — разбор и план
|
||||
|
||||
Дата: 2026-08-20. Статус: **PCM-эффекты реализованы; музыка/AY не реализованы.**
|
||||
Дата: 2026-08-20, музыка дописана 2026-08-25. Статус: **PCM-эффекты
|
||||
реализованы; музыка — путь C (PCM через CBL), первый трек играет.**
|
||||
|
||||
Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если
|
||||
найдётся способ. Эффекты — **обязательно WAV, а не PC-спикер**
|
||||
@@ -682,3 +683,54 @@ play_next_sound() seg000:1304 — раз в кадр решает, зап
|
||||
и малое зелья (`30/33`), Shadow (`32`), победа/меч (`37`), перо (`39`),
|
||||
конец уровня (`41`) и победа над Jaffar (`43`). Вызовы и AY-проигрыватель
|
||||
для них ещё не реализованы; наличие всех PCM-эффектов эту задачу не закрывает.
|
||||
|
||||
|
||||
## 4. МУЗЫКА: решение пересмотрено (2026-08-25) — путь C вместо A
|
||||
|
||||
В §1б первым заходом был выбран **путь A** (ноты PC-спикера на AY), а путь
|
||||
C (запись -> WAV -> CBL) стоил дорого из-за строки «нужен синтезатор на
|
||||
хосте». Это обстоятельство отпало: у пользователя есть **готовые записи
|
||||
DOS-версии** — `applications/PoP/PoP1_DOS_music` (flac/mp3/ogg/ogg_MT-32,
|
||||
22 трека). Синтезировать нечего, остаётся `ffmpeg -ac 1 -ar 10937 -f u8`.
|
||||
|
||||
### Что померено по этим записям
|
||||
|
||||
| группа | длительность | PCM 10 937,5 Гц |
|
||||
|---|---:|---:|
|
||||
| всё вместе (22 трека) | 339 с | **3 625 КБ = 227 EMM-страниц** |
|
||||
| игровые джинглы (10) | 57 с | 608 КБ |
|
||||
| заставки и титры | 282 с | 3 017 КБ |
|
||||
| финальный `won` один | 115 с | 1 233 КБ |
|
||||
|
||||
Свободной EMM на старте ~3 440 КБ, так что вся музыка разом в память не
|
||||
влезает и не должна: трек грузится под сцену и освобождается после.
|
||||
|
||||
### Что сделано
|
||||
|
||||
`toolchain/pop_pack_music.py` -> куски `MUS\m<id>_<nn>.bin` по 16 КБ
|
||||
(bank_load_file читает файл только целиком и только в одну страницу) +
|
||||
каталог `pop_music_tbl.h`. Длина трека хранится **порциями по 128 байт**,
|
||||
а не байтами: 169 КБ в uint16 не влезает, 1350 блоков — легко.
|
||||
|
||||
`pop_music.c` (банк 9) грузит трек в EMM и ставит курсор; насос
|
||||
`pop_sfx_fill` (резидент) получил **третий источник**: эффект важнее
|
||||
музыки, музыка важнее тишины. Эффект музыку не сбрасывает — её курсор
|
||||
стоит, пока эффект доигрывает, и она продолжается с места.
|
||||
|
||||
Проверено в MAME записью звука: трек `story_1_absence` на первом экране
|
||||
истории, корреляция огибающих с эталоном **0,836**, RMS 22,6 против 18,3
|
||||
(разница — 8-битное квантование). Эффекты двери в PV-сцене после него
|
||||
звучат как прежде, то есть освобождение страниц и возврат к набору
|
||||
эффектов работают.
|
||||
|
||||
### Цена и что осталось
|
||||
|
||||
* CBL один: пока играет музыка, эффектов нет. Для заставок это не важно
|
||||
(их там не бывает), для игровых джинглов — открытый вопрос §1б.4.
|
||||
* Резидент вырос на 83 байта (третий источник в насосе) плюс 25 байт
|
||||
данных под курсор и таблицу страниц; запас W2 — 151 байт. Кучи в
|
||||
приложении нет (`malloc` не слинкован), так что это чистый запас роста.
|
||||
* Банк 9 занят на 88 %. Следующий модуль туда уже не влезет — либо
|
||||
переносить, либо заводить банк 12.
|
||||
* `won` (77 страниц) в POP_MUS_PAGES=20 не помещается: финал придётся либо
|
||||
резать, либо стримить кусками по ходу.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -57,7 +57,7 @@ BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
|
||||
# Банк 9 — оболочка: menu/settings/config/font, автомат, global timer,
|
||||
# title, attract-demo и таблица рекордов. Здесь остаются небольшие редкие
|
||||
# верхнеуровневые точки входа, но не рендеры крупных заставок.
|
||||
BANK9_SRCS := pop_menu.c pop_status.c pop_config.c pop_app.c pop_timer.c pop_ui.c pop_title.c pop_demo.c \
|
||||
BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_config.c pop_app.c pop_timer.c pop_ui.c pop_title.c pop_demo.c \
|
||||
pop_flow.c pop_hof.c
|
||||
# Банк 10 — единый владелец экранных палитр/fade. Он намеренно отделён от UI:
|
||||
# его будут вызывать и игровой lifecycle, и все cutscene-ветки.
|
||||
@@ -173,6 +173,15 @@ SND_DIR := $(CURDIR)/../poc/res/sound
|
||||
# Список ОБЯЗАН совпадать: недостающий файл — это молча пропавшие эффекты.
|
||||
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
|
||||
|
||||
# МУЗЫКА: записи DOS-версии, приведённые к формату CBL и порезанные на
|
||||
# страницы EMM (toolchain/pop_pack_music.py). Пока в игре звучит один
|
||||
# трек — story_1_absence на первом экране intro; список расширяется
|
||||
# добавлением номеров в MUS_IDS, имена кусков задаёт упаковщик.
|
||||
MUS_DIR := $(CURDIR)/../poc/res/music
|
||||
MUS_IDS := 55
|
||||
MUS_CHUNKS := 00 01 02 03 04 05 06 07 08 09 10
|
||||
MUS_DATA := $(foreach i,$(MUS_IDS),$(foreach c,$(MUS_CHUNKS),$(MUS_DIR)/m$(i)_$(c).bin))
|
||||
|
||||
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
|
||||
# хвост использует menu для временного snapshot палитры (без расхода W2).
|
||||
FONT_DIR := $(CURDIR)/../poc/res/font
|
||||
@@ -203,7 +212,7 @@ PV_DATA := $(PV_DIR)/story.pal $(PV_DIR)/pv.pal \
|
||||
$(PV_ANIM)
|
||||
|
||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA)
|
||||
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(MUS_DATA) $(FONT_DATA) $(TITLE_DATA) $(PV_DATA) $(LVL_DATA)
|
||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||
# чтобы корень диска не зарастал десятками .atl.
|
||||
# BG\ — фон (env/wall/fore/pot)
|
||||
@@ -221,6 +230,7 @@ HDD_PACK_ARGS := $(EXAMPLE).exe \
|
||||
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
|
||||
$(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \
|
||||
$(foreach f,$(SND_DATA),SND:$(f)) \
|
||||
$(foreach f,$(MUS_DATA),MUS:$(f)) \
|
||||
$(foreach f,$(FONT_DATA),FONT:$(f)) \
|
||||
$(foreach f,$(TITLE_DATA),TITLE:$(f)) \
|
||||
$(foreach f,$(PV_DATA),PV:$(f)) \
|
||||
@@ -241,6 +251,7 @@ SKEL_STAMP := $(RESOURCE_STAMP_DIR)/skel
|
||||
VIZIER_STAMP := $(RESOURCE_STAMP_DIR)/vizier
|
||||
SHADOW_STAMP := $(RESOURCE_STAMP_DIR)/shadow
|
||||
SOUND_STAMP := $(RESOURCE_STAMP_DIR)/sound
|
||||
MUSIC_STAMP := $(RESOURCE_STAMP_DIR)/music
|
||||
KID_STAMP := $(RESOURCE_STAMP_DIR)/kid
|
||||
KID_BIN_STAMP := $(RESOURCE_STAMP_DIR)/kid-bin
|
||||
FONT_STAMP := $(RESOURCE_STAMP_DIR)/font
|
||||
@@ -286,6 +297,12 @@ $(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR)
|
||||
$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; }
|
||||
|
||||
$(MUSIC_STAMP): $(TC)/pop_pack_music.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_music.py $(MUS_IDS)
|
||||
touch $@
|
||||
$(MUS_DATA) pop_music_tbl.h: $(MUSIC_STAMP)
|
||||
@test -f $@ || { $(MAKE) -B $(MUSIC_STAMP); test -f $@; }
|
||||
|
||||
$(KID_STAMP): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py | $(RESOURCE_STAMP_DIR)
|
||||
cd $(TC) && python3 pop_pack_kid.py
|
||||
touch $@
|
||||
@@ -349,11 +366,12 @@ resources-bg: $(BG_DATA)
|
||||
resources-kid: $(KID_DATA) $(KID_BIN)
|
||||
resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA)
|
||||
resources-sound: $(SND_DATA)
|
||||
resources-music: $(MUS_DATA)
|
||||
resources-font: $(FONT_DATA)
|
||||
resources-title: $(TITLE_DATA)
|
||||
resources-pv: $(PV_DATA)
|
||||
resources-levels: $(LVL_DATA)
|
||||
resources: resources-bg resources-kid resources-actors resources-sound resources-font resources-title resources-pv resources-levels
|
||||
resources: resources-bg resources-kid resources-actors resources-sound resources-music resources-font resources-title resources-pv resources-levels
|
||||
resources-rebuild:
|
||||
$(MAKE) -B resources
|
||||
|
||||
@@ -378,4 +396,4 @@ clean-resources:
|
||||
rm -rf $(RESOURCE_STAMP_DIR)
|
||||
|
||||
.PHONY: resources resources-rebuild resources-bg resources-kid resources-actors relink \
|
||||
resources-sound resources-font resources-title resources-pv resources-levels clean-resources
|
||||
resources-sound resources-music resources-font resources-title resources-pv resources-levels clean-resources
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "pop_bg.h"
|
||||
#include "pop_shadow.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_music.h"
|
||||
#include "pop_cutscene.h"
|
||||
#include "pop_intro.h"
|
||||
#include "pop_pal.h"
|
||||
@@ -1305,10 +1306,21 @@ uint8_t pop_intro_show(void) __banked
|
||||
if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0)
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
}
|
||||
/* Первый экран истории — единственное место, где музыка уже звучит.
|
||||
* Грузим ПОСЛЕ того, как картинка собрана: чтение кусков идёт через
|
||||
* ESTEX, и при открытом CBL насос не успел бы долить блок (скрежет,
|
||||
* см. pop_sfx_start). Порядок обязателен: load -> start -> play. */
|
||||
if (pop_music_load(POP_MUS_STORY_1) == 0) {
|
||||
(void)pop_sfx_start();
|
||||
pop_music_play();
|
||||
}
|
||||
if (intro_run(intro_before_pv)) {
|
||||
pop_music_free();
|
||||
intro_restore_game_palette();
|
||||
return 1;
|
||||
}
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
pv_result = intro_pv_animated();
|
||||
if (pv_result > 0) {
|
||||
intro_restore_game_palette();
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* pop_music.c — загрузка и запуск музыкального трека, БАНК 9.
|
||||
*
|
||||
* Здесь только холодная часть: выделить страницы EMM, вычитать в них куски
|
||||
* m<id>_<nn>.bin и выставить курсор. Сам вывод делает насос CBL в
|
||||
* резиденте (pop_sfx.c): когда эффект не звучит, он берёт байты отсюда, а
|
||||
* не из блока тишины. Контракт и мотивация — в pop_music.h.
|
||||
*
|
||||
* Имя файла собираем вручную, без sprintf: printf-семейство в резидент не
|
||||
* тянем, а тут и формат-то из двух чисел.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include "pop_music.h"
|
||||
#include "pop_music_tbl.h"
|
||||
#include "pop_sfx.h"
|
||||
|
||||
uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||
static uint8_t mus_blk; /* блок EMM под трек */
|
||||
static uint8_t mus_pages; /* сколько страниц реально занято */
|
||||
static uint16_t mus_blocks; /* длина трека в порциях по 128 Б */
|
||||
|
||||
/* "MUS\mNN_KK.bin" — ровно 8.3, как требует DSS. */
|
||||
static void mus_name(char *buf, uint8_t id, uint8_t chunk)
|
||||
{
|
||||
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] = (char)('0' + chunk / 10);
|
||||
buf[9] = (char)('0' + chunk % 10);
|
||||
buf[10] = '.'; buf[11] = 'b'; buf[12] = 'i'; buf[13] = 'n';
|
||||
buf[14] = 0;
|
||||
}
|
||||
|
||||
void pop_music_stop(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
}
|
||||
|
||||
void pop_music_free(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
if (mus_blk) {
|
||||
mem_free_block(mus_blk);
|
||||
mus_blk = 0;
|
||||
}
|
||||
mus_pages = 0;
|
||||
mus_blocks = 0;
|
||||
}
|
||||
|
||||
int8_t pop_music_load(uint8_t id) __banked
|
||||
{
|
||||
char name[15];
|
||||
uint8_t i, pages;
|
||||
|
||||
if (id >= POP_MUS_COUNT) return -1;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
if (!pages || pages > POP_MUS_PAGES) return -1;
|
||||
|
||||
pop_music_free();
|
||||
mus_blk = mem_alloc_pages(pages);
|
||||
if (!mus_blk) return -1;
|
||||
for (i = 0; i < pages; i++) {
|
||||
pop_mus_page[i] = mem_get_page(mus_blk, i);
|
||||
mus_name(name, id, i);
|
||||
/* Последний кусок короче страницы — читаем сколько есть; ошибку
|
||||
* ловим по отрицательному коду, а не по «не 16384». */
|
||||
if (!pop_mus_page[i] ||
|
||||
bank_load_file(pop_mus_page[i], 0, name, 16384) < 0) {
|
||||
pop_music_free();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
mus_pages = pages;
|
||||
mus_blocks = pop_mus_tbl[id].blocks;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pop_music_play(void) __banked
|
||||
{
|
||||
if (!mus_pages) return;
|
||||
/* Порядок важен: сначала позиция, потом длина. Насос стартует ровно
|
||||
* с той порции, где left стал ненулевым, и промежуточное состояние
|
||||
* «играем, но курсор от прошлого трека» не возникает. */
|
||||
pop_mus_pg = 0;
|
||||
pop_mus_ptr = 0;
|
||||
pop_mus_left = mus_blocks;
|
||||
}
|
||||
|
||||
uint8_t pop_music_busy(void) __banked
|
||||
{
|
||||
return (uint8_t)(pop_mus_left != 0);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* pop_music.h — музыкальный трек через тот же CBL, что и эффекты.
|
||||
*
|
||||
* ПОЧЕМУ PCM, А НЕ AY. Оцифровки музыки в игре нет вовсе: digisnd*.dat
|
||||
* содержит только эффекты, музыка лежит MIDI под Adlib. Зато есть готовые
|
||||
* записи DOS-версии, и путь «запись -> сырой поток CBL» не требует ни
|
||||
* синтезатора на хосте, ни аранжировки под три канала AY (разбор вариантов
|
||||
* — ../docs/sound_plan.md §1). Плата известна: CBL один, поэтому пока
|
||||
* играет музыка, эффектов нет. Оригинал ведёт себя так же — play_sound
|
||||
* там глушит предыдущий звук.
|
||||
*
|
||||
* ГДЕ УМЕСТНО. Заставки и титры: эффектов там не бывает, картинка
|
||||
* статична, и загрузка сотни-другой килобайт с диска никому не мешает.
|
||||
* Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её
|
||||
* придётся либо гасить эффектами, либо уводить на AY.
|
||||
*
|
||||
* ФОРМАТ на диске — пачка кусков m<id>_<nn>.bin по 16 КБ (bank_load_file
|
||||
* читает файл только целиком и только в одну страницу). Каталог с
|
||||
* длинами генерирует toolchain/pop_pack_music.py в pop_music_tbl.h.
|
||||
*/
|
||||
#ifndef POP_MUSIC_H
|
||||
#define POP_MUSIC_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Сколько страниц EMM держим под трек. Двадцать = 320 КБ, это все
|
||||
* заставочные треки, кроме финального «won» (115 с, 1,2 МБ): тот придётся
|
||||
* либо резать, либо стримить, и это отдельная задача. */
|
||||
#define POP_MUS_PAGES 20
|
||||
|
||||
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
|
||||
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
|
||||
|
||||
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
|
||||
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
|
||||
* вместо неё, и у каждого свой источник. */
|
||||
extern uint8_t pop_mus_page[POP_MUS_PAGES]; /* физические страницы EMM */
|
||||
extern volatile uint8_t pop_mus_pg; /* индекс текущей страницы */
|
||||
extern volatile uint16_t pop_mus_ptr; /* адрес в окне W0 */
|
||||
extern volatile uint16_t pop_mus_left; /* байт до конца трека, 0 — молчим */
|
||||
|
||||
/* Загрузить трек в EMM (НЕ запускает). Звать там, где допустима пауза на
|
||||
* диск: CBL в это время обязан быть закрыт, иначе насос не успевает долить
|
||||
* блок и железо повторяет старый — на слух скрежет (pop_sfx.h).
|
||||
* 0 — готово, -1 — нет трека/памяти/файла. */
|
||||
int8_t pop_music_load(uint8_t id) __banked;
|
||||
|
||||
/* Отпустить страницы. Останавливает проигрывание, если оно шло. */
|
||||
void pop_music_free(void) __banked;
|
||||
|
||||
/* Пустить загруженный трек с начала / оборвать. */
|
||||
void pop_music_play(void) __banked;
|
||||
void pop_music_stop(void) __banked;
|
||||
|
||||
/* Трек ещё звучит? 0 — доиграл или не запускался. */
|
||||
uint8_t pop_music_busy(void) __banked;
|
||||
|
||||
#endif /* POP_MUSIC_H */
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.
|
||||
* Источник: applications/PoP/PoP1_DOS_music/flac (записи DOS-версии).
|
||||
* Генератор: toolchain/pop_pack_music.py. Руками не править.
|
||||
*
|
||||
* Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —
|
||||
* сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека
|
||||
* в порциях насоса по 128 байт (в байтах не влезло бы в uint16).
|
||||
*/
|
||||
#ifndef POP_MUSIC_TBL_H
|
||||
#define POP_MUSIC_TBL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_MUS_COUNT 56
|
||||
#define POP_MUS_BLOCK 128
|
||||
#define POP_MUS_MAX_PAGES 11
|
||||
|
||||
typedef struct {
|
||||
uint8_t pages; /* 0 — трека нет */
|
||||
uint16_t blocks; /* длина в порциях по 128 байт */
|
||||
} pop_mus_ent_t;
|
||||
|
||||
static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
|
||||
{ 0, 0 }, /* 0 */
|
||||
{ 0, 0 }, /* 1 */
|
||||
{ 0, 0 }, /* 2 */
|
||||
{ 0, 0 }, /* 3 */
|
||||
{ 0, 0 }, /* 4 */
|
||||
{ 0, 0 }, /* 5 */
|
||||
{ 0, 0 }, /* 6 */
|
||||
{ 0, 0 }, /* 7 */
|
||||
{ 0, 0 }, /* 8 */
|
||||
{ 0, 0 }, /* 9 */
|
||||
{ 0, 0 }, /* 10 */
|
||||
{ 0, 0 }, /* 11 */
|
||||
{ 0, 0 }, /* 12 */
|
||||
{ 0, 0 }, /* 13 */
|
||||
{ 0, 0 }, /* 14 */
|
||||
{ 0, 0 }, /* 15 */
|
||||
{ 0, 0 }, /* 16 */
|
||||
{ 0, 0 }, /* 17 */
|
||||
{ 0, 0 }, /* 18 */
|
||||
{ 0, 0 }, /* 19 */
|
||||
{ 0, 0 }, /* 20 */
|
||||
{ 0, 0 }, /* 21 */
|
||||
{ 0, 0 }, /* 22 */
|
||||
{ 0, 0 }, /* 23 */
|
||||
{ 0, 0 }, /* 24 */
|
||||
{ 0, 0 }, /* 25 */
|
||||
{ 0, 0 }, /* 26 */
|
||||
{ 0, 0 }, /* 27 */
|
||||
{ 0, 0 }, /* 28 */
|
||||
{ 0, 0 }, /* 29 */
|
||||
{ 0, 0 }, /* 30 */
|
||||
{ 0, 0 }, /* 31 */
|
||||
{ 0, 0 }, /* 32 */
|
||||
{ 0, 0 }, /* 33 */
|
||||
{ 0, 0 }, /* 34 */
|
||||
{ 0, 0 }, /* 35 */
|
||||
{ 0, 0 }, /* 36 */
|
||||
{ 0, 0 }, /* 37 */
|
||||
{ 0, 0 }, /* 38 */
|
||||
{ 0, 0 }, /* 39 */
|
||||
{ 0, 0 }, /* 40 */
|
||||
{ 0, 0 }, /* 41 */
|
||||
{ 0, 0 }, /* 42 */
|
||||
{ 0, 0 }, /* 43 */
|
||||
{ 0, 0 }, /* 44 */
|
||||
{ 0, 0 }, /* 45 */
|
||||
{ 0, 0 }, /* 46 */
|
||||
{ 0, 0 }, /* 47 */
|
||||
{ 0, 0 }, /* 48 */
|
||||
{ 0, 0 }, /* 49 */
|
||||
{ 0, 0 }, /* 50 */
|
||||
{ 0, 0 }, /* 51 */
|
||||
{ 0, 0 }, /* 52 */
|
||||
{ 0, 0 }, /* 53 */
|
||||
{ 0, 0 }, /* 54 */
|
||||
{ 11, 1350 }, /* 55 story_1_absence */
|
||||
};
|
||||
|
||||
#endif /* POP_MUSIC_TBL_H */
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <cbl.h>
|
||||
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_music.h"
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
/* Физические страницы набора; заполняет холодная половина. */
|
||||
@@ -42,6 +43,12 @@ uint8_t pop_snd_want;
|
||||
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
|
||||
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
|
||||
* страницу и адрес в окне. */
|
||||
/* Курсор МУЗЫКИ. Лежит здесь, а не в pop_music.c, по той же причине, что
|
||||
* и курсор эффектов: его читает насос из прерывания, то есть резидент. */
|
||||
volatile uint8_t pop_mus_pg;
|
||||
volatile uint16_t pop_mus_ptr;
|
||||
volatile uint16_t pop_mus_left;
|
||||
|
||||
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
|
||||
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
|
||||
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
|
||||
@@ -66,15 +73,39 @@ static volatile uint16_t sfx_left; /* сколько байт осталось
|
||||
* тесном резиденте W2 отказала бы и уронила весь звук. */
|
||||
int pop_sfx_fill(uint16_t n)
|
||||
{
|
||||
uint8_t saved, pg;
|
||||
uint8_t saved, phys;
|
||||
uint16_t ptr;
|
||||
if (sfx_left) { pg = sfx_pg; ptr = sfx_ptr; }
|
||||
else { pg = POP_SND_SILENCE_PAGE; ptr = POP_SND_SILENCE_OFF; }
|
||||
|
||||
/* Три источника по убыванию важности: эффект, музыка, тишина. Эффект
|
||||
* перебивает музыку — как у оригинала, где play_sound глушит всё, что
|
||||
* звучало; голос-то один. Музыка при этом НЕ теряет позицию: её
|
||||
* курсор стоит, пока эффект доигрывает, и она продолжается с места. */
|
||||
if (sfx_left) {
|
||||
phys = pop_snd_page[sfx_pg]; ptr = sfx_ptr;
|
||||
} else if (pop_mus_left) {
|
||||
phys = pop_mus_page[pop_mus_pg]; ptr = pop_mus_ptr;
|
||||
} else {
|
||||
phys = pop_snd_page[POP_SND_SILENCE_PAGE];
|
||||
ptr = POP_SND_SILENCE_OFF;
|
||||
}
|
||||
saved = _io_page_w0;
|
||||
_io_page_w0 = pop_snd_page[pg];
|
||||
_io_page_w0 = phys;
|
||||
cbl_push_otir((const void *)ptr, n);
|
||||
_io_page_w0 = saved;
|
||||
if (!sfx_left) return 1; /* играли тишину — курсор не двигаем */
|
||||
if (!sfx_left) {
|
||||
if (!pop_mus_left) return 1; /* тишина — курсор не двигаем */
|
||||
/* Длину трека считаем ПОРЦИЯМИ насоса, а не байтами: 169 КБ в
|
||||
* uint16 не влезает, а 1350 блоков — легко (32-бит арифметики на
|
||||
* Z80 избегаем, тем более в прерывании). */
|
||||
pop_mus_left--;
|
||||
pop_mus_ptr += n;
|
||||
if (pop_mus_ptr >= 0x4000) {
|
||||
pop_mus_ptr = 0;
|
||||
if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) pop_mus_pg++;
|
||||
else pop_mus_left = 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
sfx_ptr += n;
|
||||
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pop_pack_music.py — музыка PoP в сырой поток для CBL.
|
||||
|
||||
Источник — готовые записи DOS-версии (applications/PoP/PoP1_DOS_music,
|
||||
формат flac). Оцифровки музыки в самой игре НЕТ: в digisnd*.dat лежат
|
||||
только эффекты, а музыка хранится как MIDI под Adlib. Поэтому здесь мы
|
||||
не «конвертируем ресурс игры», а укладываем готовую запись в формат,
|
||||
который наше железо играет без единого преобразования в рантайме.
|
||||
|
||||
ФОРМАТ — тот же, что у эффектов (см. pop_pack_sound.py и sound_plan.md):
|
||||
8 бит БЕЗЗНАКОВЫЙ моно на 10 937,5 Гц — ровно CBL_FREQ_10K9. Частоту
|
||||
берём не 11 000, а частоту железа: тогда CBL открывается ОДИН раз за
|
||||
запуск и не переключается ни между эффектами, ни при переходе на музыку.
|
||||
|
||||
РЕЖЕМ НА СТРАНИЦЫ EMM по 16 КБ. bank_load_file умеет читать файл только
|
||||
целиком и только в одну страницу, поэтому трек на диске лежит пачкой
|
||||
кусков M<id>_<nn>.BIN — ровно как набор эффектов лежит s0..s9.bin.
|
||||
Последний кусок дополняется тишиной (0x80) до кратности 128: насос отдаёт
|
||||
CBL блоками по 128 байт, и остаток «не полного блока» ему пришлось бы
|
||||
досчитывать в прерывании.
|
||||
|
||||
Длина трека уходит в pop_music_tbl.h блоками по 128 байт, а не байтами:
|
||||
самый длинный трек (won, 115 с) — это 1,2 МБ, что не влезает в uint16, а
|
||||
в блоках даёт 9855. 32-битной арифметики на Z80 избегаем (см. memory
|
||||
avoid_32bit_arith_z80).
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
POP = Path(__file__).resolve().parent.parent # applications/PoP
|
||||
SRC_DIR = POP / "PoP1_DOS_music" / "flac"
|
||||
OUT_DIR = POP / "poc" / "res" / "music"
|
||||
OUT_H = POP / "roomtest" / "pop_music_tbl.h"
|
||||
|
||||
CBL_RATE = 10937.5 # CBL_FREQ_10K9 — частота железа, не 11 000
|
||||
PAGE = 16384 # EMM-страница
|
||||
BLOCK = 128 # порция насоса CBL
|
||||
SILENCE = 0x80 # центр беззнакового 8-битного сэмпла
|
||||
|
||||
# id — нумерация звуков ОРИГИНАЛА (SDLPoP data/music/names.txt). Держим её,
|
||||
# чтобы игровой код просил трек тем же номером, каким оригинал зовёт
|
||||
# play_sound, и таблицу можно было сверить глазами.
|
||||
TRACKS = {
|
||||
24: "regular_death",
|
||||
25: "find_mirror_and_find_shadow",
|
||||
26: "embrace",
|
||||
27: "cutscene_before_2_4_6_12",
|
||||
28: "fight_death",
|
||||
29: "meet_Jaffar",
|
||||
30: "life_potion",
|
||||
32: "end_level_4_and_killed_by_shadow",
|
||||
33: "potion",
|
||||
35: "cutscene_before_8_9",
|
||||
36: "out_of_time",
|
||||
37: "guard_death_and_obtaining_the_sword",
|
||||
39: "floating",
|
||||
40: "cutscene_not_much_time_left",
|
||||
41: "story_5_level_end_and_merge_with_shadow",
|
||||
43: "killed_Jaffar",
|
||||
50: "story_2_princess",
|
||||
52: "story_4_Jaffar_leaves",
|
||||
53: "story_3_Jaffar_enters",
|
||||
54: "intro_theme",
|
||||
55: "story_1_absence",
|
||||
56: "won",
|
||||
}
|
||||
|
||||
|
||||
def decode(path: Path) -> bytes:
|
||||
"""flac -> сырой u8 моно на частоте CBL."""
|
||||
out = subprocess.run(
|
||||
["ffmpeg", "-v", "error", "-i", str(path),
|
||||
"-ac", "1", "-ar", str(int(CBL_RATE)), "-f", "u8", "-"],
|
||||
capture_output=True, check=True)
|
||||
return out.stdout
|
||||
|
||||
|
||||
def pack(track_id: int, name: str) -> tuple[int, int]:
|
||||
src = SRC_DIR / f"{name}.flac"
|
||||
if not src.exists():
|
||||
raise SystemExit(f"нет источника: {src}")
|
||||
data = bytearray(decode(src))
|
||||
|
||||
# Добить до кратности блока насоса — хвост тишиной, а не обрезкой:
|
||||
# обрезать значило бы щёлкнуть на последнем сэмпле.
|
||||
tail = len(data) % BLOCK
|
||||
if tail:
|
||||
data += bytes([SILENCE]) * (BLOCK - tail)
|
||||
|
||||
chunks = (len(data) + PAGE - 1) // PAGE
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for i in range(chunks):
|
||||
part = data[i * PAGE:(i + 1) * PAGE]
|
||||
(OUT_DIR / f"m{track_id:02d}_{i:02d}.bin").write_bytes(part)
|
||||
return chunks, len(data) // BLOCK
|
||||
|
||||
|
||||
def main() -> None:
|
||||
wanted = [int(a) for a in sys.argv[1:]] or sorted(TRACKS)
|
||||
rows = []
|
||||
total = 0
|
||||
for tid in wanted:
|
||||
if tid not in TRACKS:
|
||||
raise SystemExit(f"неизвестный трек {tid}")
|
||||
chunks, blocks = pack(tid, TRACKS[tid])
|
||||
rows.append((tid, TRACKS[tid], chunks, blocks))
|
||||
total += blocks * BLOCK
|
||||
print(f" {tid:2d} {TRACKS[tid]:<40} {chunks:2d} стр, "
|
||||
f"{blocks * BLOCK / 1024:7.1f} КБ, {blocks * BLOCK / CBL_RATE:5.1f} с")
|
||||
|
||||
max_id = max(t for t, *_ in rows)
|
||||
lines = [
|
||||
"/*",
|
||||
" * pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.",
|
||||
" * Источник: applications/PoP/PoP1_DOS_music/flac (записи DOS-версии).",
|
||||
" * Генератор: toolchain/pop_pack_music.py. Руками не править.",
|
||||
" *",
|
||||
" * Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —",
|
||||
" * сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека",
|
||||
" * в порциях насоса по 128 байт (в байтах не влезло бы в uint16).",
|
||||
" */",
|
||||
"#ifndef POP_MUSIC_TBL_H",
|
||||
"#define POP_MUSIC_TBL_H",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
f"#define POP_MUS_COUNT {max_id + 1}",
|
||||
f"#define POP_MUS_BLOCK {BLOCK}",
|
||||
f"#define POP_MUS_MAX_PAGES {max(c for _, _, c, _ in rows)}",
|
||||
"",
|
||||
"typedef struct {",
|
||||
" uint8_t pages; /* 0 — трека нет */",
|
||||
" uint16_t blocks; /* длина в порциях по 128 байт */",
|
||||
"} pop_mus_ent_t;",
|
||||
"",
|
||||
"static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {",
|
||||
]
|
||||
by_id = {t: (c, b, n) for t, n, c, b in rows}
|
||||
for i in range(max_id + 1):
|
||||
if i in by_id:
|
||||
c, b, n = by_id[i]
|
||||
lines.append(f" {{ {c:2d}, {b:5d} }}, /* {i:2d} {n} */")
|
||||
else:
|
||||
lines.append(f" {{ 0, 0 }}, /* {i:2d} */")
|
||||
lines += ["};", "", "#endif /* POP_MUSIC_TBL_H */", ""]
|
||||
OUT_H.write_text("\n".join(lines))
|
||||
print(f"итого {total / 1024:.0f} КБ в {OUT_DIR}")
|
||||
print(f"каталог: {OUT_H}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user