Звуковые эффекты PoP: оцифровка оригинала через CBL

Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота
железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 —
размер блока запроса CBL, поэтому ни один блок не пересекает границу
страницы и проигрыватель не знает слова «стык».

Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих
мест физики и из play_seq.  Тишину льём свою (первый блок набора), а не
через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная.
Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от
pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и
железо крутит хвост буфера — на слух скрежет.

Разведены все места play_sound() SDLPoP, у которых есть оцифровка
(id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и
провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч,
зеркало, скелет, зелье.  Таблица соответствий — docs/sound_plan.md §8.
Музыкальные id остаются с нулевой длиной до фазы музыки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 15:16:43 +03:00
parent 24ced6058f
commit 8ea4c32e51
25 changed files with 561 additions and 137 deletions
+64 -15
View File
@@ -21,6 +21,7 @@
#include "pop_map.h"
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_sfx.h" /* звуковые эффекты (sound_plan.md) */
#include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
#include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
@@ -143,6 +144,11 @@ uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заб
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
/* is_screaming (seg005:38): крик падения звучит ОДИН раз за полёт, а не
* каждый кадр. Сбрасывается там же, где у оригинала: приземление (land),
* зацеп за карниз (check_grab) и постановка на старт (set_start_pos). */
static uint8_t is_screaming;
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
@@ -666,6 +672,7 @@ static void land(void)
* control_crouched(), а при `sword == sword_2_drawn` управление уходит в
* control_with_sword и до него не доходит — Кид садится в присед
* НАВСЕГДА (BUG-LAND-SWORD-1). */
is_screaming = 0; /* seg005:116 */
if (Char.fall_y < 22) {
soft_land:
if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) {
@@ -674,8 +681,8 @@ static void land(void)
} else {
seq = SEQ_17_SOFT_LAND;
}
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
if (Char.charid == CHARID_0_KID) is_guard_notice = 1;
/* seg005:185 — мягкое приземление слышно И стражам, и игроку. */
if (Char.charid == CHARID_0_KID) { pop_sfx_play(17); is_guard_notice = 1; }
} else {
uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */
if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */
@@ -683,13 +690,14 @@ static void land(void)
Char.fall_x = Char.fall_y = 0;
if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */
pop_take_hp(100);
if (Char.charid == CHARID_0_KID) pop_sfx_play(0); /* разбился */
pop_char_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
return;
}
if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */
if (Char.charid == CHARID_0_KID) { pop_sfx_play(16); is_guard_notice = 1; } /* seg005:195 */
seq = SEQ_20_MEDIUM_LAND;
}
Char.fall_x = Char.fall_y = 0;
@@ -811,7 +819,9 @@ static void check_grab(void)
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq();
determine_col();
grab_timer = 12; /* (TODO: sound grab) */
grab_timer = 12;
pop_sfx_play(9); /* seg006 check_grab */
is_screaming = 0; /* seg006:1219 */
}
}
@@ -882,6 +892,10 @@ static uint8_t check_grab_run_jump(void)
static void do_fall(void)
{
uint8_t nrow = (uint8_t)(Char.curr_row + 1);
if (!is_screaming && Char.fall_y >= 31) {
pop_sfx_play(1); /* seg005:39 — крик падения */
is_screaming = 1;
}
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
check_grab(); /* ещё летит — попытка зацепа */
@@ -1355,6 +1369,8 @@ void pop_proc_get_object(void) __banked
* ставили вспышку руками, то есть красили экран дважды.
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
* запаса, а не одну единицу. */
pop_sfx_stop(); /* seg006:1893 — stop_sounds */
pop_sfx_play(13);
if (pop_current_level == POP_POTIONS_LEVEL)
hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
else
@@ -1508,8 +1524,9 @@ static void bumped_fall(void)
play_seq();
}
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
* стену слышен и в свободном падении. Флаг «Кид нашумел» взводится
* вместе со звуком — см. BUG-GUARD-DEAF-1. */
pop_sfx_play(8);
is_guard_notice = 1;
}
@@ -1560,7 +1577,8 @@ static void bumped_floor(void)
else
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
pop_sfx_play(8); /* bumped_sound (seg004:05F1) */
is_guard_notice = 1;
}
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
@@ -1593,6 +1611,7 @@ static void bump_into_opponent(void)
if (distance > 15) return;
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
pop_sfx_play(16); /* seg003:0654 — толчок о стража */
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
}
@@ -2367,6 +2386,28 @@ static void check_press(void)
}
}
/* ---- Звук дрожащей плиты (loose_shake, seg007:0E55) ------------------ *
* Три сэмпла 20/21/22 вперемешку, но НЕ на каждый кадр фазы: гейт-таблица
* loose_sound (data:2734) даёт звук на фазах 1,2,3,5,8, а на остальных
* молчит — иначе тряска трещит без пауз. Повтор подряд одного и того же
* сэмпла оригинал запрещает (цикл do/while), иначе слышно «заедание».
*
* Сид СВОЙ, а не общий с trob/боёвкой: домены prandom у нас разведены
* (../docs/impl_diff.md), и звук не должен сдвигать чужие розыгрыши. */
static const uint8_t loose_sound[12] = { 0,1,1,1,0,1,0,0,1,0,0,0 };
static pop_rnd_t loose_seed;
static uint8_t last_loose_snd;
static void loose_shake(uint8_t force, uint8_t modif)
{
uint8_t id;
if (!force && !loose_sound[modif & 0x7F]) return;
do { id = (uint8_t)(pop_prandom(&loose_seed, 2) + 20); }
while (id == last_loose_snd);
last_loose_snd = id;
pop_sfx_play(id);
}
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
@@ -2718,11 +2759,13 @@ void pop_loose_tick(void) __banked
/* ждём молча */
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
*lm = 0;
loose_shake(1, 0); /* последний удар — без гейта */
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
* правая грань loose живёт в соседе). ОБЕ страницы —
* иначе на одной застывает дрожащий кадр → мерцание. */
pop_set_redraw(pos, POP_RD_LOOSE, 2);
} else {
loose_shake(0, m);
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
}
} else if (m >= 11 && pop_loose_fell) {
@@ -2753,6 +2796,7 @@ void pop_loose_tick(void) __banked
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
} else { /* отсчёт 1..10 — дрожащий кадр */
loose_shake(0, m);
pop_set_redraw(pos, POP_RD_LOOSE, 1);
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
@@ -2774,9 +2818,10 @@ void pop_loose_tick(void) __banked
* цикл, а не соседний. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
/* ждём молча */
} else if (m >= 0x84) { *lm = 0;
} else if (m >= 0x84) { *lm = 0; loose_shake(1, 0);
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
else { loose_shake(0, m);
pop_set_redraw_above(col, POP_RDA_CEIL, 1); }
} else if (m >= 11 && pop_ceil_fell) {
*lm = 10; /* сигнал занят — кадр подождём */
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
@@ -2786,6 +2831,7 @@ void pop_loose_tick(void) __banked
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else {
loose_shake(0, m);
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
@@ -3005,9 +3051,10 @@ static void start_anim_spike(uint8_t tilepos)
if (!mod) return;
old = (int8_t)mod[tilepos];
if (old <= 0) {
if (old == 0)
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
else if ((uint8_t)old != 0xFF)
if (old == 0) {
pop_add_trob(g_room, tilepos, 1);
pop_sfx_play(49); /* seg007:08F6 — пики пошли */
} else if ((uint8_t)old != 0xFF)
mod[tilepos] = 0x8F;
}
}
@@ -3032,7 +3079,8 @@ static void spiked(uint8_t tilepos)
Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Char.x = (uint8_t)char_dx_forward(8);
Char.fall_y = 0;
pop_take_hp(100); /* (TODO: sound_48_spiked) */
pop_sfx_play(48);
pop_take_hp(100);
pop_char_set_seq(SEQ_51_SPIKED);
play_seq();
determine_col();
@@ -3104,7 +3152,8 @@ static void chomped(uint8_t tilepos, int8_t col)
Char.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
Char.x = (uint8_t)char_dx_forward(7 - !Char.direction);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
pop_take_hp(100); /* (TODO: sound_46_chomped) */
pop_sfx_play(46);
pop_take_hp(100);
pop_char_set_seq(SEQ_54_CHOMPED);
play_seq();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
@@ -3312,7 +3361,7 @@ static void jump_through_mirror(void)
(void)mirror_image();
pop_jumped_mirror = 0;
Char.charid = CHARID_1_SHADOW;
/* play_sound(45) — звука в порте пока нет */
pop_sfx_play(45); /* seg003:0617 — прыжок в зеркало */
pop_saveshad();
guardhp_max = guardhp_curr = hitp_max;
hitp_curr = 1;