Музыка по ходу игры: смерть, зелья, меч, конец уровня, катсцены

Звуки 24..43 в наборе оцифровки пустые — в оригинале это Adlib-музыка, и
в digisnd её нет. На этом и построено подключение: pop_sfx_play для звука
с нулевой длиной кладёт номер в pop_mus_req, а разбирает заявку
pop_music_service() — один вызов на кадр из любого цикла (игра, интро,
катсцена). Всё чтение с диска живёт там.

Ждать полной загрузки джингла нельзя — это фриз посреди игры. Поэтому
pop_music_stream читает первую страницу (33 мс) и сразу пускает трек: она
звучит 1,5 с, а следующая читается те же 33 мс. Остальные доливаются по
одной за кадр.

Расставлено по местам оригинала: смерть (24/28/32), вступление первого
уровня и тень шестого (25), сцены перед уровнями (27/35/40), встреча с
Джафаром (29), зелья (30/33/39), время вышло (36), меч и смерть стража
(37), смерть Джафара (43), конец уровня (41/32), встреча с принцессой
(26). Темы «раз за уровень» сбрасывает pop_music_level_start.

Упакован 21 трек (2,4 МБ). Не озвучен только финал: won — 115 с и 78
страниц EMM, ему нужен кольцевой стриминг (задача MUS-WON на доске).

Проверено в MAME: вступительная тема первого уровня отыгрывается на
старте (курсор дошёл до конца, id 25). Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 21:52:37 +03:00
parent 65797af44c
commit 7e6b38cca6
33 changed files with 300 additions and 35 deletions
+43
View File
@@ -845,3 +845,46 @@ EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_mu
курсор `pop_mus_id` меняется ровно на своих кадрах, интро доходит до курсор `pop_mus_id` меняется ровно на своих кадрах, интро доходит до
демо-режима. Слуховая проверка (нет ли хрипа от диска при играющей демо-режима. Слуховая проверка (нет ли хрипа от диска при играющей
музыке) — за пользователем. музыке) — за пользователем.
## 7. МУЗЫКА ПО ХОДУ ИГРЫ (2026-08-26)
Звуки 24..43 в наборе оцифровки ПУСТЫЕ — в оригинале это Adlib-музыка, и
в digisnd её нет вовсе. На этом и построено подключение: `pop_sfx_play`
для звука с нулевой длиной не пытается его играть, а кладёт НОМЕР в
`pop_mus_req` (один байт). Заявку разбирает `pop_music_service()` — один
вызов на кадр из любого цикла (игрового, интро, катсцены); всё чтение с
диска живёт там.
**Стриминг вместо загрузки.** Ждать полной загрузки джингла нельзя — это
фриз на треть секунды посреди игры. `pop_music_stream` читает ПЕРВУЮ
страницу (33 мс) и сразу пускает трек: она звучит 1,5 с, а следующая
читается те же 33 мс — запас сорокакратный. Остальные доливаются по
одной за кадр, пока `pop_music_loading()`. Номера страниц известны сразу
после `mem_alloc_pages`, поэтому таблица для насоса заполняется целиком —
данные появятся раньше, чем насос до них дойдёт.
**Что и где играет** (номера и места — из SDLPoP):
| трек | событие | место у нас |
|------|---------|-------------|
| 24 / 28 / 32 | смерть: обычная / в бою / от руки тени | `ctrl_kid_death` |
| 25 | вступление 1-го уровня (Кид сидит), тень 6-го | `control_crouched`, `guards.c` |
| 27 / 35 / 40 | сцены перед 2/4/6/12, 8/9, «времени мало» | `pop_pre_cutscene_show` |
| 29 | встреча с Джафаром | `pop_meet_jaffar` |
| 30 / 33 | большая склянка / малая | `pop_proc_get_object` |
| 36 | время вышло | `pop_time_expired_show` |
| 37 / 43 | меч найден, страж убит / смерть Джафара | `pop_proc_get_object`, `on_guard_killed` |
| 39 | перо (медленное падение) | `pop_proc_get_object` |
| 41 / 32 | конец уровня / конец 4-го (тень) | опкод SND_LEVEL в `play_seq` |
| 26 | встреча с принцессой | `cut_ending` |
Темы, которые звучат один раз за заход на уровень (вступление 1-го, тень
6-го), сбрасывает `pop_music_level_start()` из `pop_start_level`. Оригинал
для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это
есть свои два байта.
**Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM —
в `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
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
+7 -5
View File
@@ -207,12 +207,14 @@ SND_ARC := $(SND_DIR)/snd.arc
SND_DATA := $(SND_ARC) SND_DATA := $(SND_ARC)
# МУЗЫКА: записи DOS-версии, приведённые к формату CBL # МУЗЫКА: записи DOS-версии, приведённые к формату CBL
# (toolchain/pop_pack_music.py). Сейчас озвучена вся заставка: # (toolchain/pop_pack_music.py). Номера — оригинала, чтобы игровой код
# 54 — тема титров, 55 — «In the absence», 50/53/52 — три реплики сцены # просил трек тем же числом, каким SDLPoP зовёт play_sound.
# с принцессой и Джафаром. Игровые джинглы (смерть, зелья) пойдут # заставка 54 55 50 53 52
# отдельной задачей: там музыка спорит с эффектами за единственный CBL. # катсцены 26 27 35 36 40
# игра 24 25 28 29 30 32 33 37 39 41 43
# Нет только 56 (won, 115 с / 1,2 МБ) — финалу нужен стриминг с диска.
MUS_DIR := $(CURDIR)/../poc/res/music MUS_DIR := $(CURDIR)/../poc/res/music
MUS_IDS := 50 52 53 54 55 MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55
MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin) MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin)
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы: # UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
+16
View File
@@ -1206,6 +1206,22 @@ N навык M» — отдельная надобность, если пона
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке. Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг с диска
Трек 56 (`won`, 115 с) — это 1,2 МБ, 78 страниц EMM. Ни в `POP_MUS_PAGES`
(20), ни в разумный бюджет памяти он не влезает, поэтому единственный
оставшийся неозвученным кусок игры — экран победы.
**Что нужно.** Кольцевой буфер: держать в EMM несколько страниц (8 = 128
КБ хватит с запасом — это 12 с звучания), а загрузчик дочитывает в те, что
насос уже прошёл. Курсор насоса (`pop_mus_pg`) при этом обязан
заворачиваться, а загрузчик — держать дистанцию от него и не обгонять на
круг. Чтение страницы 33 мс против 1,5 с её звучания — запаса вдоволь,
вся сложность в аккуратности индексов, а не в скорости.
**Заодно.** Тот же механизм снимает ограничение `POP_MUS_PAGES` со всех
треков: сейчас каждый должен помещаться в память целиком.
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1, как звук и музыка ### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1, как звук и музыка
**Что сделано.** Формат PBA1 и читатель готовы и работают на звуке: **Что сделано.** Формат PBA1 и читатель готовы и работают на звуке:
+28 -6
View File
@@ -46,6 +46,18 @@
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и /* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его * «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
* через границу банка дороже, чем повторить. */ * через границу банка дороже, чем повторить. */
/* Темы, которые звучат ОДИН раз за заход на уровень: тень шестого уровня
* (seg002:0090) и вступление первого. Оригинал отмечает «уже играли»
* через leveldoor_open = 0x4D, то есть портит переменную двери; у нас на
* это есть свои байты, а сбрасывает их старт уровня. */
static uint8_t shadow6_music;
void pop_music_level_start(void) __banked
{
shadow6_music = 0;
pop_ctrl_level_start();
}
static uint8_t tile_is_floor(uint8_t t) static uint8_t tile_is_floor(uint8_t t)
{ {
switch (t) { switch (t) {
@@ -343,12 +355,16 @@ uint8_t pop_check_shadow(uint8_t room) __banked
return 1; return 1;
} }
/* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе — /* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе —
* тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет * тень встаёт при КАЖДОМ входе в комнату 1. Оригинал играет здесь
* sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D` * sound_25_presentation, помечая «уже играли» через leveldoor_open =
* (переменная двери используется как флаг); звука у нас нет, а чужую * 0x4D (переменную двери он использует как флаг). Чужую переменную не
* переменную портить незачем — TODO вместе со звуком. */ * трогаем: у нас для этого свой однобайтовый флаг. */
if (pop_current_level == SHADOW_STEP_LEVEL) { if (pop_current_level == SHADOW_STEP_LEVEL) {
if (room != SHADOW_STEP_ROOM) return 0; if (room != SHADOW_STEP_ROOM) return 0;
if (!shadow6_music) {
shadow6_music = 1;
pop_sfx_play(25);
}
do_init_shad(init_shad_6, room, SEQ_2_STAND); do_init_shad(init_shad_6, room, SEQ_2_STAND);
return 1; return 1;
} }
@@ -384,7 +400,6 @@ void pop_sword_disappears(void) __banked
* комнаты 3 и попадает прямо к Джафару. Всё событие — фора: пока * комнаты 3 и попадает прямо к Джафару. Всё событие — фора: пока
* pop_guard_notice_timer идёт, Джафар не встаёт в боевую стойку * pop_guard_notice_timer идёт, Джафар не встаёт в боевую стойку
* (autocontrol_guard_inactive), то есть у Кида есть ~2 секунды достать меч. * (autocontrol_guard_inactive), то есть у Кида есть ~2 секунды достать меч.
* В оригинале здесь же играет sound_29_meet_Jaffar — звука у нас нет.
* *
* `pop_leveldoor_open == 0` в условии не косметика: вернувшись к Джафару * `pop_leveldoor_open == 0` в условии не косметика: вернувшись к Джафару
* после его смерти (дверь уже открыта), форы быть не должно. * после его смерти (дверь уже открыта), форы быть не должно.
@@ -396,6 +411,7 @@ void pop_meet_jaffar(void) __banked
if (pop_current_level != JAFFAR_LEVEL) return; if (pop_current_level != JAFFAR_LEVEL) return;
if (pop_leveldoor_open != 0) return; if (pop_leveldoor_open != 0) return;
if (Kid.room != JAFFAR_MEET_ROOM) return; if (Kid.room != JAFFAR_MEET_ROOM) return;
pop_sfx_play(29); /* тема встречи с Джафаром */
pop_guard_notice_timer = JAFFAR_NOTICE_TIME; pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
} }
@@ -426,7 +442,13 @@ void pop_jaffar_exit(void) __banked
* Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */ * Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */
static void on_guard_killed(void) static void on_guard_killed(void)
{ {
if (pop_current_level != JAFFAR_LEVEL) return; if (pop_current_level != JAFFAR_LEVEL) {
/* seg006:1940 — победный трек за стража; за тень его нет: она не
* «убита», а слилась с Кидом. */
if (Char.charid != CHARID_1_SHADOW) pop_sfx_play(37);
return;
}
pop_sfx_play(43); /* смерть Джафара — свой трек */
pop_flash_color = POP_FLASH_WHITE; pop_flash_color = POP_FLASH_WHITE;
pop_flash_time = JAFFAR_FLASH_TIME; pop_flash_time = JAFFAR_FLASH_TIME;
pop_leveldoor_open = 2; pop_leveldoor_open = 2;
+23
View File
@@ -22,6 +22,7 @@
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */ #include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_map.h" #include "pop_map.h"
#include "pop_level.h" /* pop_current_level — вступительная тема 1-го */
#include "pop_guard.h" /* charid, состояние меча, seq стража */ #include "pop_guard.h" /* charid, состояние меча, seq стража */
/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */ /* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */
@@ -254,9 +255,24 @@ static void safe_step(void)
} }
} }
/* Вступительная тема уровня 1 играется один раз за заход на уровень;
* сбрасывает флаг pop_ctrl_level_start (её зовёт запуск уровня). */
static uint8_t lvl1_music;
void pop_ctrl_level_start(void) __banked { lvl1_music = 0; }
/* ---- control_* обработчики состояний ------------------------------ */ /* ---- control_* обработчики состояний ------------------------------ */
static void control_crouched(void) static void control_crouched(void)
{ {
/* «Presentation» (seg005:02EB): на первом уровне Кид начинает игру
* сидя, и пока он сидит, звучит вступительная тема. Оригинал ждёт
* тишины (`if (!check_sound_playing())`) — у нас достаточно одного
* запуска: заявку разберёт главный цикл, а насос сам придержит музыку,
* пока доигрывает эффект. */
if (pop_current_level == 1 && !lvl1_music) {
lvl1_music = 1;
pop_sfx_play(25);
}
/* Shift в приседе — поднять предмет (seg005 control_crouched). */ /* Shift в приседе — поднять предмет (seg005 control_crouched). */
if (control_shift2 == CONTROL_HELD && get_item_action()) return; if (control_shift2 == CONTROL_HELD && get_item_action()) return;
if (control_y != CONTROL_HELD_DOWN) { if (control_y != CONTROL_HELD_DOWN) {
@@ -711,6 +727,13 @@ static void ctrl_kid_death(void)
if (Kid.alive < 0 && hitp_curr == 0) { if (Kid.alive < 0 && hitp_curr == 0) {
Kid.alive = 0; Kid.alive = 0;
pop_kid_dead = 1; pop_kid_dead = 1;
/* play_death_music (seg006:15B5): траурный трек зависит от того,
* КАК умер Кид — в бою, от руки тени или просто так. Оригинал ждёт
* шести тиков, чтобы доиграл звук удара; у нас ждать не нужно —
* насос держит курсор музыки, пока звучит эффект. */
if (Guard.charid == CHARID_1_SHADOW) pop_sfx_play(32);
else if (holding_sword) pop_sfx_play(28);
else pop_sfx_play(24);
} }
} }
+3
View File
@@ -71,4 +71,7 @@ extern int8_t control_forward, control_backward, control_up, control_down,
* своего тика напрямую. */ * своего тика напрямую. */
void pop_control(void) __banked; void pop_control(void) __banked;
/* Снять флаг «вступительная тема уровня 1 уже играла». */
void pop_ctrl_level_start(void) __banked;
#endif #endif
+4
View File
@@ -332,6 +332,10 @@ int pop_guard_load(void) __banked;
* инверсии; зовётся из pop_vflip_load_all (pop_vflip.h). Живёт рядом с * инверсии; зовётся из pop_vflip_load_all (pop_vflip.h). Живёт рядом с
* pop_guard_load: там же gp[] и выбор GUARD/SKEL. */ * pop_guard_load: там же gp[] и выбор GUARD/SKEL. */
int pop_guard_vflip_load(void) __banked; int pop_guard_vflip_load(void) __banked;
/* Сброс «одноразовых» музыкальных тем уровня (вступление 1-го, тень 6-го).
* Зовёт pop_start_level. */
void pop_music_level_start(void) __banked;
void pop_guard_free(void) __banked; void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard, /* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
+35 -1
View File
@@ -831,6 +831,7 @@ static int intro_pv_animated(void)
torch_right, sand_frame, flash, torch_right, sand_frame, flash,
render_tick); render_tick);
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */ pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
pop_music_service(); /* заявки на музыку из сцены (эффект без оцифровки) */
/* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра: /* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра:
* 33 мс диска укладываются в паузу логического кадра (133 мс) и * 33 мс диска укладываются в паузу логического кадра (133 мс) и
* анимации не стоят. */ * анимации не стоят. */
@@ -1252,6 +1253,8 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
gfx_set_visible_page(back); gfx_set_visible_page(back);
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync(); for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
pop_sfx_tick();
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9); cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9); cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
cut_sand = (uint8_t)((cut_sand + 1) % 3); cut_sand = (uint8_t)((cut_sand + 1) % 3);
@@ -1519,8 +1522,24 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
const pop_cs_cmd_t *script = 0; const pop_cs_cmd_t *script = 0;
pop_sfx_pause(); pop_sfx_pause();
/* Музыка сцены — как в оригинале: 27 для 2/4/6/12 (seg001:401),
* 35 для 8/9 (seg001:298), 40 для короткой «времени мало» (seg001:388).
* Экран в этот момент чёрный, поэтому читаем трек целиком: пауза на
* диск здесь никому не мешает. */
{
uint8_t track = 27;
if (scene == POP_PRE_8 || scene == POP_PRE_9) track = 35;
else if (scene == POP_PRE_12_SHORT) track = 40;
if (pop_music_load(track) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
}
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) { if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
if (pre_princess_animated(scene) >= 0) { if (pre_princess_animated(scene) >= 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
return; return;
} }
@@ -1530,6 +1549,8 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
if (scene == POP_PRE_8 || scene == POP_PRE_9) { if (scene == POP_PRE_8 || scene == POP_PRE_9) {
int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9(); int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9();
if (done == 0) { if (done == 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
return; return;
} }
@@ -1539,16 +1560,29 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
else if (scene == POP_PRE_8) script = pre_8; else if (scene == POP_PRE_8) script = pre_8;
else if (scene == POP_PRE_9) script = pre_9; else if (scene == POP_PRE_9) script = pre_9;
else if (scene == POP_PRE_12_SHORT) script = pre_12_short; else if (scene == POP_PRE_12_SHORT) script = pre_12_short;
if (!script) return; if (!script) {
pop_music_free();
pop_sfx_pause();
return;
}
intro_run(script); intro_run(script);
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
} }
void pop_time_expired_show(void) __banked void pop_time_expired_show(void) __banked
{ {
pop_sfx_pause(); pop_sfx_pause();
/* time_expired (seg001:365) начинается с play_sound(sound_36). */
if (pop_music_load(36) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
intro_run(time_expired_script); intro_run(time_expired_script);
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
} }
+8 -2
View File
@@ -345,8 +345,7 @@ void play_seq(void)
* 2 BUMP звук 8, и шум; * 2 BUMP звук 8, и шум;
* 3 DRINK звук 18, не шум; * 3 DRINK звук 18, не шум;
* 4 LEVEL МУЗЫКА конца уровня (32 на 4-м, иначе 41, * 4 LEVEL МУЗЫКА конца уровня (32 на 4-м, иначе 41,
* на 13/15 молчок). Оцифровки у неё нет, * на 13/15 молчок).
* это путь A на AY пока тишина.
* Второй смысл опкода («Кид нашумел») терять нельзя даже * Второй смысл опкода («Кид нашумел») терять нельзя даже
* без звука, см. BUG-GUARD-DEAF-1. */ * без звука, см. BUG-GUARD-DEAF-1. */
{ {
@@ -356,6 +355,13 @@ void play_seq(void)
if (ev == 1) pop_sfx_play(23); if (ev == 1) pop_sfx_play(23);
else if (ev == 2) pop_sfx_play(8); else if (ev == 2) pop_sfx_play(8);
else if (ev == 3) pop_sfx_play(18); else if (ev == 3) pop_sfx_play(18);
else if (ev == 4) {
/* seg006:646. На четвёртом уровне уход за дверь — это
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
* 15-м оригинал молчит. */
if (pop_current_level == 4) pop_sfx_play(32);
else if (pop_current_level != 13) pop_sfx_play(41);
}
} }
break; break;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден. case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
+7
View File
@@ -1330,6 +1330,7 @@ void pop_proc_get_object(void) __banked
if (Char.charid != CHARID_0_KID) return; if (Char.charid != CHARID_0_KID) return;
if (pickup_obj_type == -1) { if (pickup_obj_type == -1) {
pop_have_sword = 1; /* в оригинале have_sword = -1 */ pop_have_sword = 1; /* в оригинале have_sword = -1 */
pop_sfx_play(37); /* победный трек «меч найден» */
/* Вспышка фона: flash_color = color_14_brightyellow, flash_time = 8 /* Вспышка фона: flash_color = color_14_brightyellow, flash_time = 8
* (seg006 proc_get_object). do_flash = set_bg_attr(0, color) * (seg006 proc_get_object). do_flash = set_bg_attr(0, color)
* оригинал подменяет НУЛЕВУЮ запись палитры, т.е. вспыхивает всё * оригинал подменяет НУЛЕВУЮ запись палитры, т.е. вспыхивает всё
@@ -1343,12 +1344,16 @@ void pop_proc_get_object(void) __banked
switch (pickup_obj_type) { switch (pickup_obj_type) {
case 1: /* здоровье: +1 HP, красная вспышка */ case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) { if (hitp_curr != hitp_max) {
pop_sfx_stop(); /* seg006:1871 — stop_sounds */
pop_sfx_play(33);
hitp_delta = 1; hitp_delta = 1;
pop_flash_color = POP_FLASH_RED; pop_flash_color = POP_FLASH_RED;
pop_flash_time = 2; pop_flash_time = 2;
} }
break; break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */ case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_sfx_stop(); /* seg006:1878 — stop_sounds */
pop_sfx_play(30);
pop_kid_add_life(); pop_kid_add_life();
pop_flash_color = POP_FLASH_RED; pop_flash_color = POP_FLASH_RED;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */ pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
@@ -1359,6 +1364,8 @@ void pop_proc_get_object(void) __banked
* и играет sound_39_low_weight по нему же меряет срок; у нас * и играет sound_39_low_weight по нему же меряет срок; у нас
* срок задан кадрами (POP_FEATHER_FRAMES). */ * срок задан кадрами (POP_FEATHER_FRAMES). */
pop_feather = POP_FEATHER_FRAMES; pop_feather = POP_FEATHER_FRAMES;
pop_sfx_stop();
pop_sfx_play(39); /* «низкий вес» — весь эффект */
pop_flash_color = POP_FLASH_GREEN; pop_flash_color = POP_FLASH_GREEN;
pop_flash_time = 3; pop_flash_time = 3;
break; break;
+47
View File
@@ -159,6 +159,38 @@ int8_t pop_music_load(uint8_t id) __banked
return (r < 0) ? -1 : 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 void pop_music_play(void) __banked
{ {
uint8_t n, i; uint8_t n, i;
@@ -182,6 +214,21 @@ void pop_music_play(void) __banked
pop_mus_left = slot_blocks[n]; 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 uint8_t pop_music_busy(void) __banked
{ {
return (uint8_t)(pop_mus_left != 0); return (uint8_t)(pop_mus_left != 0);
+11
View File
@@ -72,11 +72,22 @@ uint8_t pop_music_loading(void) __banked;
/* Отпустить ОБА слота. Останавливает проигрывание, если оно шло. */ /* Отпустить ОБА слота. Останавливает проигрывание, если оно шло. */
void pop_music_free(void) __banked; void pop_music_free(void) __banked;
/* Пустить трек СРАЗУ, дочитывая его по ходу звучания: первая страница
* читается здесь (33 мс), остальные вызывающим, по одной за кадр, пока
* pop_music_loading(). Для игровых джинглов, где паузы на диск быть не
* может. 0 играет, -1 трека нет либо не хватило памяти. */
int8_t pop_music_stream(uint8_t id) __banked;
/* Пустить последний загруженный трек с начала; слот предыдущего при этом /* Пустить последний загруженный трек с начала; слот предыдущего при этом
* освобождается. Без парного load ничего не делает. */ * освобождается. Без парного load ничего не делает. */
void pop_music_play(void) __banked; void pop_music_play(void) __banked;
void pop_music_stop(void) __banked; void pop_music_stop(void) __banked;
/* ОБСЛУЖИВАНИЕ МУЗЫКИ — один вызов на кадр из любого цикла (игра, интро,
* катсцена): разбирает заявку pop_mus_req и доливает страницы начатого
* стрима. Всё чтение с диска живёт здесь. */
void pop_music_service(void) __banked;
/* Трек ещё звучит? 0 — доиграл или не запускался. */ /* Трек ещё звучит? 0 — доиграл или не запускался. */
uint8_t pop_music_busy(void) __banked; uint8_t pop_music_busy(void) __banked;
+16 -16
View File
@@ -46,26 +46,26 @@ static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
{ 0, 0 }, /* 21 */ { 0, 0 }, /* 21 */
{ 0, 0 }, /* 22 */ { 0, 0 }, /* 22 */
{ 0, 0 }, /* 23 */ { 0, 0 }, /* 23 */
{ 0, 0 }, /* 24 */ { 3, 305 }, /* 24 regular_death */
{ 0, 0 }, /* 25 */ { 2, 196 }, /* 25 find_mirror_and_find_shadow */
{ 0, 0 }, /* 26 */ { 9, 1132 }, /* 26 embrace */
{ 0, 0 }, /* 27 */ { 8, 915 }, /* 27 cutscene_before_2_4_6_12 */
{ 0, 0 }, /* 28 */ { 6, 656 }, /* 28 fight_death */
{ 0, 0 }, /* 29 */ { 4, 428 }, /* 29 meet_Jaffar */
{ 0, 0 }, /* 30 */ { 3, 299 }, /* 30 life_potion */
{ 0, 0 }, /* 31 */ { 0, 0 }, /* 31 */
{ 0, 0 }, /* 32 */ { 5, 624 }, /* 32 end_level_4_and_killed_by_shadow */
{ 0, 0 }, /* 33 */ { 2, 205 }, /* 33 potion */
{ 0, 0 }, /* 34 */ { 0, 0 }, /* 34 */
{ 0, 0 }, /* 35 */ { 7, 888 }, /* 35 cutscene_before_8_9 */
{ 0, 0 }, /* 36 */ { 10, 1256 }, /* 36 out_of_time */
{ 0, 0 }, /* 37 */ { 3, 280 }, /* 37 guard_death_and_obtaining_the_sword */
{ 0, 0 }, /* 38 */ { 0, 0 }, /* 38 */
{ 0, 0 }, /* 39 */ { 13, 1591 }, /* 39 floating */
{ 0, 0 }, /* 40 */ { 5, 596 }, /* 40 cutscene_not_much_time_left */
{ 0, 0 }, /* 41 */ { 9, 1043 }, /* 41 story_5_level_end_and_merge_with_shadow */
{ 0, 0 }, /* 42 */ { 0, 0 }, /* 42 */
{ 0, 0 }, /* 43 */ { 3, 285 }, /* 43 killed_Jaffar */
{ 0, 0 }, /* 44 */ { 0, 0 }, /* 44 */
{ 0, 0 }, /* 45 */ { 0, 0 }, /* 45 */
{ 0, 0 }, /* 46 */ { 0, 0 }, /* 46 */
+15 -4
View File
@@ -55,6 +55,10 @@ uint8_t pop_mus_id;
/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */ /* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */
volatile uint8_t pop_snd_tick; volatile uint8_t pop_snd_tick;
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
uint8_t pop_mus_req;
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */ static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */ static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */ static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
@@ -225,10 +229,17 @@ static uint8_t snd_interruptible(uint8_t id)
void pop_sfx_play(uint8_t id) void pop_sfx_play(uint8_t id)
{ {
if (!pop_snd_ok || id >= POP_SND_COUNT) return; if (!pop_snd_ok || id >= POP_SND_COUNT) return;
/* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки /* НЕТ ОЦИФРОВКИ — ЗНАЧИТ ЭТО МУЗЫКА. В оригинале звуки 24..43 играет
* `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id * Adlib по MIDI, и в digisnd их нет вовсе; у нас на их месте записи
* выигрывал бы конкурс и глушил живой эффект. */ * DOS-версии, которые надо читать с диска. В прерывании и в горячем
if (pop_snd_tbl[id].len == 0) return; * коде этого делать нельзя, поэтому здесь только ЗАЯВКА одним байтом
* её разбирает главный цикл (pop_music_stream, банк 9).
* В конкурсе эффектов такой id не участвует: он не глушит живой звук
* (порт проверки `if (NULL == sound_pointers[id]) return;`). */
if (pop_snd_tbl[id].len == 0) {
pop_mus_req = id;
return;
}
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1]) if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
snd_next1 = (uint8_t)(id + 1); snd_next1 = (uint8_t)(id + 1);
} }
+6
View File
@@ -59,6 +59,12 @@ extern uint8_t pop_snd_want;
* запасной путь по кадрам луча. */ * запасной путь по кадрам луча. */
extern volatile uint8_t pop_snd_tick; extern volatile uint8_t pop_snd_tick;
/* ЗАЯВКА НА МУЗЫКУ. pop_sfx_play(id) для звука без оцифровки (24..43 —
* в оригинале это Adlib-музыка) кладёт сюда номер трека, а разбирает
* заявку главный цикл: чтение с диска ни в прерывании, ни в горячем коде
* недопустимо. 0 заявки нет. Снимать обязан тот, кто обработал. */
extern uint8_t pop_mus_req;
/* Заявить эффект с номером id (0..56 — нумерация оригинала, см. /* Заявить эффект с номером id (0..56 — нумерация оригинала, см.
* sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий: * sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий:
* это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на * это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на
+7 -1
View File
@@ -18,7 +18,8 @@
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h" #include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */ #include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_sfx.h" /* pop_sfx_tick — решение о запуске звука за кадр */ #include "pop_sfx.h"
#include "pop_music.h" /* pop_sfx_tick — решение о запуске звука за кадр */
#include "pop_guard.h" #include "pop_guard.h"
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */ #include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h" #include "pop_cheat.h"
@@ -995,6 +996,11 @@ int main(void)
* кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается * кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается
* важнейшая. Раньше по кадру нельзя потеряются поздние заявки. */ * важнейшая. Раньше по кадру нельзя потеряются поздние заявки. */
pop_sfx_tick(); pop_sfx_tick();
/* МУЗЫКА ПО ХОДУ ИГРЫ. Заявку положил pop_sfx_play (звук без
* оцифровки это Adlib-трек оригинала), а диск трогаем здесь:
* первая страница читается сразу и трек уже звучит, остальные
* доливаются по одной за кадр 33 мс на кадр, пока догоняем. */
pop_music_service();
/* Обе служебные полосы — нижняя статус-строка (сообщения оригинала) /* Обе служебные полосы — нижняя статус-строка (сообщения оригинала)
* и верхняя отладочная. Всё change-driven и целиком в банке 9, * и верхняя отладочная. Всё change-driven и целиком в банке 9,
@@ -94,6 +94,7 @@ void pop_start_level(void) __banked
int8_t dir; int8_t dir;
pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */
pop_music_level_start(); /* одноразовые темы уровня — заново */
pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */
/* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если /* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если
* картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной * картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной