Упаковщик звуковых эффектов + сверка наборов MSDOS и SDLPoP
Эффекты берём из MSDOS/digisnd*.dat: 28 из 31 звука совпадают побайтно с набором SDLPoP, но три различаются не в пользу последнего — у него sword_vs_sword короче, sword_moving другой, а spiked вообще пустой (7 сэмплов против 5 069). MIDI наоборот: у MSDOS формат 0 (всё слито в одну дорожку), у SDLPoP формат 1 с 8-9 дорожками — готовое разделение голосов, если дойдём до пути B (MIDI -> три канала AY). Упаковщик: 31 эффект -> 10 937,5 Гц, 131 072 Б = 8 EMM-страниц. Начало каждого звука выровнено на 128 (блок запроса CBL), а страница кратна 128, поэтому ни один блок не пересекает границу страницы — проигрывателю не нужна логика стыка.
This commit is contained in:
@@ -156,6 +156,45 @@ B имеет смысл только если C окажется неподъё
|
||||
зеркальные наборы (`pop_vflip_load_all`: 28 Кид + 5 страж + меч = 34
|
||||
страницы), останется ~98. Проверять запас надо ИМЕННО ТАМ.
|
||||
|
||||
## 1г. MSDOS против SDLPoP: чем отличаются наборы (сверено 2026-08-20)
|
||||
|
||||
У нас лежат ДВЕ копии звука — оригинальные `.dat` в `MSDOS/` (версия
|
||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
||||
есть, и она влияет на выбор источника.
|
||||
|
||||
### Оцифровка: берём MSDOS
|
||||
|
||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||
|
||||
| id | звук | MSDOS | SDLPoP |
|
||||
|---|---|---:|---:|
|
||||
| 10 | `sword_vs_sword` | 5 020 сэмплов | 3 504 |
|
||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
||||
|
||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
||||
где источник наоборот SDLPoP.
|
||||
|
||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
||||
|
||||
Здесь всё наоборот. Содержимое музыкально то же (деление 480, те же
|
||||
каналы 0..7 плюс ударные), но:
|
||||
|
||||
| | MSDOS | SDLPoP |
|
||||
|---|---|---|
|
||||
| формат MIDI | **0** — всё слито в ОДНУ дорожку | **1** — 8-9 дорожек |
|
||||
| размер (звук 24) | 327 Б | 448 Б |
|
||||
| размер (звук 56) | 13 587 Б | 12 773 Б |
|
||||
|
||||
Формат 1 с отдельной дорожкой на инструмент — это готовое разделение
|
||||
голосов. Для пути B (MIDI -> три канала AY) оно решает половину задачи:
|
||||
дорожки можно выбирать напрямую (бас / мелодия / гармония), а не
|
||||
разбирать слитый поток и догадываться, что чем было.
|
||||
|
||||
**Итог: эффекты из MSDOS, музыка (когда дойдёт) из SDLPoP.**
|
||||
|
||||
## 2. Что лежит в ассетах (замерено, а не по памяти)
|
||||
|
||||
Звук в PoP адресуется как ресурс `10000 + N`, N = 0..56 — 57 звуков
|
||||
|
||||
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
Binary file not shown.
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
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* pop_sfx.h — звуковые ЭФФЕКТЫ PoP через COVOX-Blaster.
|
||||
*
|
||||
* Данные — оригинальная оцифровка игры (`digisnd1..3.dat`), приведённая
|
||||
* упаковщиком к 10 937,5 Гц. Конвертировать в рантайме нечего: у PoP
|
||||
* сэмплы 8 бит БЕЗЗНАКОВЫЕ МОНО, и это ровно формат CBL (см. <cbl.h> и
|
||||
* ../docs/sound_plan.md).
|
||||
*
|
||||
* ОДИН ГОЛОС. Оригинал тоже одноголосый: `play_sound` глушит
|
||||
* предыдущий звук (`stop_sounds`, seg009.c), поэтому новый эффект просто
|
||||
* перебивает играющий, и никакого микшера не нужно.
|
||||
*
|
||||
* ПОЧЕМУ ЭТО НЕ ЛОМАЕТ КАДР. Насос живёт в прерывании CBL и отдаёт 128
|
||||
* байт раз в ~11,7 мс. Кадровые прерывания при этом теряются (ветка CBL
|
||||
* в трамплине делает приватный RETI), но нам всё равно: темп кадра
|
||||
* считается ПО ЛУЧУ (pop_pace.h), а не по прерываниям. Если бы пейсинг
|
||||
* остался на них — звук сломал бы игру.
|
||||
*/
|
||||
#ifndef POP_SFX_H
|
||||
#define POP_SFX_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Поднять звук: занять 8 EMM-страниц, вычитать в них набор, открыть CBL.
|
||||
* 0 — OK, -1 — не получилось (тогда pop_sfx_play молча ничего не делает,
|
||||
* игра идёт без звука). Звать один раз при старте. */
|
||||
int pop_sfx_init(void) __banked;
|
||||
|
||||
/* Закрыть CBL и отпустить страницы; звать на выходе. */
|
||||
void pop_sfx_close(void) __banked;
|
||||
|
||||
/* Начать эффект с номером id (0..56 — нумерация оригинала, см.
|
||||
* sound_plan.md). Перебивает играющий. У звуков без оцифровки (это
|
||||
* музыка) длина 0 — вызов просто глушит текущий.
|
||||
*
|
||||
* НЕ банковая и НЕ __banked: зовётся из горячих мест (play_seq в
|
||||
* pop_kid.c, физика, боёвка), и трамплин на каждый шаг Кида не нужен. */
|
||||
void pop_sfx_play(uint8_t id);
|
||||
|
||||
/* Оборвать текущий эффект (порт stop_sounds). */
|
||||
void pop_sfx_stop(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
|
||||
* СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.
|
||||
*
|
||||
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
|
||||
* CBL один в один, конвертировать в рантайме нечего.
|
||||
*
|
||||
* Начало каждого звука кратно 128 — размеру блока запроса CBL, а
|
||||
* страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает
|
||||
* границу страницы, и проигрывателю не нужна логика стыка.
|
||||
*
|
||||
* Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80
|
||||
* 32-битная арифметика дорога, а так всё влезает в 8/16 бит.
|
||||
* Длина округлена вверх до блока — неполных блоков не бывает.
|
||||
*/
|
||||
#ifndef POP_SOUND_TBL_H
|
||||
#define POP_SOUND_TBL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_SND_PAGES 8
|
||||
#define POP_SND_COUNT 57
|
||||
#define POP_SND_BLOCK 128
|
||||
|
||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
||||
|
||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
||||
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0000, 1664 }, /* 0 */
|
||||
{ 0, 0x0680, 12032 }, /* 1 */
|
||||
{ 0, 0x3580, 6272 }, /* 2 */
|
||||
{ 1, 0x0E00, 1280 }, /* 3 */
|
||||
{ 1, 0x1300, 1152 }, /* 4 */
|
||||
{ 1, 0x1780, 1408 }, /* 5 */
|
||||
{ 1, 0x1D00, 5504 }, /* 6 */
|
||||
{ 1, 0x3280, 896 }, /* 7 */
|
||||
{ 1, 0x3600, 1664 }, /* 8 */
|
||||
{ 1, 0x3C80, 2816 }, /* 9 */
|
||||
{ 2, 0x0780, 4992 }, /* 10 */
|
||||
{ 2, 0x1B00, 1280 }, /* 11 */
|
||||
{ 2, 0x2000, 1280 }, /* 12 */
|
||||
{ 2, 0x2500, 3840 }, /* 13 */
|
||||
{ 2, 0x3400, 7552 }, /* 14 */
|
||||
{ 3, 0x1180, 17664 }, /* 15 */
|
||||
{ 4, 0x1680, 4736 }, /* 16 */
|
||||
{ 4, 0x2900, 4224 }, /* 17 */
|
||||
{ 4, 0x3980, 6912 }, /* 18 */
|
||||
{ 5, 0x1480, 2816 }, /* 19 */
|
||||
{ 5, 0x1F80, 1408 }, /* 20 */
|
||||
{ 5, 0x2500, 1152 }, /* 21 */
|
||||
{ 5, 0x2980, 1280 }, /* 22 */
|
||||
{ 5, 0x2E80, 1408 }, /* 23 */
|
||||
{ 0, 0x0000, 0 }, /* 24 */
|
||||
{ 0, 0x0000, 0 }, /* 25 */
|
||||
{ 0, 0x0000, 0 }, /* 26 */
|
||||
{ 0, 0x0000, 0 }, /* 27 */
|
||||
{ 0, 0x0000, 0 }, /* 28 */
|
||||
{ 0, 0x0000, 0 }, /* 29 */
|
||||
{ 0, 0x0000, 0 }, /* 30 */
|
||||
{ 0, 0x0000, 0 }, /* 31 */
|
||||
{ 0, 0x0000, 0 }, /* 32 */
|
||||
{ 0, 0x0000, 0 }, /* 33 */
|
||||
{ 0, 0x0000, 0 }, /* 34 */
|
||||
{ 0, 0x0000, 0 }, /* 35 */
|
||||
{ 0, 0x0000, 0 }, /* 36 */
|
||||
{ 0, 0x0000, 0 }, /* 37 */
|
||||
{ 0, 0x0000, 0 }, /* 38 */
|
||||
{ 0, 0x0000, 0 }, /* 39 */
|
||||
{ 0, 0x0000, 0 }, /* 40 */
|
||||
{ 0, 0x0000, 0 }, /* 41 */
|
||||
{ 0, 0x0000, 0 }, /* 42 */
|
||||
{ 0, 0x0000, 0 }, /* 43 */
|
||||
{ 5, 0x3400, 2688 }, /* 44 */
|
||||
{ 5, 0x3E80, 4224 }, /* 45 */
|
||||
{ 6, 0x0F00, 4608 }, /* 46 */
|
||||
{ 6, 0x2100, 5120 }, /* 47 */
|
||||
{ 6, 0x3500, 5120 }, /* 48 */
|
||||
{ 7, 0x0900, 4480 }, /* 49 */
|
||||
{ 0, 0x0000, 0 }, /* 50 */
|
||||
{ 7, 0x1A80, 4864 }, /* 51 */
|
||||
{ 0, 0x0000, 0 }, /* 52 */
|
||||
{ 0, 0x0000, 0 }, /* 53 */
|
||||
{ 0, 0x0000, 0 }, /* 54 */
|
||||
{ 0, 0x0000, 0 }, /* 55 */
|
||||
{ 0, 0x0000, 0 }, /* 56 */
|
||||
};
|
||||
|
||||
static const char *const pop_snd_files[POP_SND_PAGES] = {
|
||||
"SND\\s0.bin",
|
||||
"SND\\s1.bin",
|
||||
"SND\\s2.bin",
|
||||
"SND\\s3.bin",
|
||||
"SND\\s4.bin",
|
||||
"SND\\s5.bin",
|
||||
"SND\\s6.bin",
|
||||
"SND\\s7.bin",
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pop_pack_sound.py — упаковщик ЗВУКОВЫХ ЭФФЕКТОВ (оцифровка, chtab нет).
|
||||
|
||||
ИСТОЧНИК. `MSDOS/digisnd1..3.dat` — оригинальные контейнеры. Разбор:
|
||||
индекс по 8 байт (id, offset, size), **первый байт ресурса — контрольная
|
||||
сумма**, тело за ней (спецификация `docs/POP-DAT-FormatSpecifications`,
|
||||
§3.1.2). Тело — один из двух вариантов заголовка:
|
||||
|
||||
digi_type (1.0/1.1): word rate, word count, word unk, byte size=8
|
||||
digi_new_type (1.3/1.4): word rate, byte size=8, word count, word, word
|
||||
|
||||
Различаем по позиции байта размера: у нового он на смещении 3, у старого
|
||||
на 7. Сэмплы — 8 бит БЕЗЗНАКОВЫЕ, МОНО (проверено по `convert_digi_sound`,
|
||||
seg009.c:2358: один байт на кадр, `(b | b<<8) - 32768`; стерео в данных
|
||||
нет вовсе, каналы размножаются уже на выходе). Это ровно формат нашего
|
||||
CBL — конвертировать нечего.
|
||||
|
||||
ЧАСТОТА. Приводим ВСЁ к 10 937,5 Гц — это `CBL_FREQ_10K9`, то есть
|
||||
частота железа. Не к 11 000: проигрывание 11 000-данных на 10 937,5 дало
|
||||
бы сдвиг тона −9,9 цента, а пересчитывать три файла (2 750, 8 200,
|
||||
14 000 Гц) всё равно надо. Взамен CBL открывается ОДИН раз и частота не
|
||||
меняется никогда — ни между эффектами, ни при переходе на музыку.
|
||||
|
||||
РАСКЛАДКА. Всё склеивается в EMM-страницы по 16 384 Б, и начало каждого
|
||||
звука ВЫРАВНИВАЕТСЯ НА 128. Это не косметика: блок запроса CBL — ровно
|
||||
128 байт, а 16 384 кратно 128, поэтому при таком выравнивании ни один
|
||||
блок не пересекает границу страницы. Проигрыватель за счёт этого не
|
||||
знает слова «стык»: замапил страницу, отдал 128 байт, сдвинул указатель.
|
||||
Плата — до 127 байт на звук, всего ~2 КБ.
|
||||
|
||||
Выход: poc/res/sound/s0..sN.bin (по 16 384 Б) + roomtest/pop_sound_tbl.h
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DAT_DIR = os.path.join(HERE, "..", "MSDOS")
|
||||
OUT_DIR = os.path.join(HERE, "..", "poc", "res", "sound")
|
||||
HDR = os.path.join(HERE, "..", "roomtest", "pop_sound_tbl.h")
|
||||
|
||||
PAGE = 16384
|
||||
BLOCK = 128 # блок запроса CBL при 8 бит (см. cbl.h)
|
||||
RATE = 10937.5 # CBL_FREQ_10K9
|
||||
NSOUND = 57 # id 0..56; у музыки оцифровки нет — длина 0
|
||||
|
||||
|
||||
def dat_resources(path):
|
||||
"""id -> тело ресурса (без байта контрольной суммы)."""
|
||||
d = open(path, "rb").read()
|
||||
off, _ = struct.unpack("<IH", d[0:6])
|
||||
n = struct.unpack("<H", d[off:off + 2])[0]
|
||||
out = {}
|
||||
for i in range(n):
|
||||
e = off + 2 + i * 8
|
||||
rid = struct.unpack("<H", d[e:e + 2])[0]
|
||||
roff = struct.unpack("<I", d[e + 2:e + 6])[0]
|
||||
rsz = struct.unpack("<H", d[e + 6:e + 8])[0]
|
||||
out[rid - 10000] = d[roff + 1:roff + 1 + rsz]
|
||||
return out
|
||||
|
||||
|
||||
def digi(body):
|
||||
"""(rate, samples) или None, если ресурс не оцифровка."""
|
||||
if not body or (body[0] & 7) != 1:
|
||||
return None
|
||||
if len(body) > 4 and body[3] == 8: # digi_new_type
|
||||
rate = struct.unpack("<H", body[1:3])[0]
|
||||
cnt = struct.unpack("<H", body[4:6])[0]
|
||||
head = 10
|
||||
elif len(body) > 7 and body[7] == 8: # digi_type
|
||||
rate, cnt = struct.unpack("<HH", body[1:5])
|
||||
head = 8
|
||||
else:
|
||||
return None
|
||||
return rate, body[head:head + cnt]
|
||||
|
||||
|
||||
def resample(src, src_rate, dst_rate):
|
||||
"""Линейная интерполяция. Данные 8 бит беззнаковые, поэтому и работаем
|
||||
в беззнаковых — никаких знаковых промежуточных представлений."""
|
||||
if src_rate == dst_rate or not src:
|
||||
return bytes(src)
|
||||
n = int(round(len(src) * dst_rate / src_rate))
|
||||
out = bytearray(n)
|
||||
step = src_rate / dst_rate
|
||||
for i in range(n):
|
||||
x = i * step
|
||||
i0 = int(x)
|
||||
if i0 >= len(src) - 1:
|
||||
out[i] = src[-1]
|
||||
else:
|
||||
a = x - i0
|
||||
out[i] = int(round(src[i0] * (1.0 - a) + src[i0 + 1] * a))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main():
|
||||
res = {}
|
||||
for i in (1, 2, 3):
|
||||
p = os.path.join(DAT_DIR, f"digisnd{i}.dat")
|
||||
if not os.path.exists(p):
|
||||
sys.exit(f"нет файла {p}")
|
||||
res.update(dat_resources(p))
|
||||
|
||||
sounds = {}
|
||||
for sid, body in sorted(res.items()):
|
||||
g = digi(body)
|
||||
if g is None:
|
||||
print(f" звук {sid}: не оцифровка, пропуск")
|
||||
continue
|
||||
rate, samples = g
|
||||
pcm = resample(samples, rate, RATE)
|
||||
sounds[sid] = pcm
|
||||
if rate != 11000:
|
||||
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
|
||||
|
||||
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
|
||||
blob = bytearray()
|
||||
table = {}
|
||||
for sid in sorted(sounds):
|
||||
pad = (-len(blob)) % BLOCK
|
||||
blob += bytes([0x80]) * pad # 0x80 — тишина в беззнаковом 8-бит
|
||||
# Длину округляем ВВЕРХ до блока: хвост последнего блока — это
|
||||
# выравнивающая тишина следующего звука, и проигрыватель за счёт
|
||||
# этого не знает слова «неполный блок».
|
||||
table[sid] = (len(blob), (len(sounds[sid]) + BLOCK - 1) // BLOCK * BLOCK)
|
||||
blob += sounds[sid]
|
||||
blob += bytes([0x80]) * ((-len(blob)) % PAGE)
|
||||
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
npages = len(blob) // PAGE
|
||||
for i in range(npages):
|
||||
with open(os.path.join(OUT_DIR, f"s{i}.bin"), "wb") as f:
|
||||
f.write(blob[i * PAGE:(i + 1) * PAGE])
|
||||
|
||||
lines = [
|
||||
"/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.",
|
||||
" * СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.",
|
||||
" *",
|
||||
" * Данные: 8 бит беззнаковые, МОНО, %g Гц (CBL_FREQ_10K9) — формат" % RATE,
|
||||
" * CBL один в один, конвертировать в рантайме нечего.",
|
||||
" *",
|
||||
" * Начало каждого звука кратно %d — размеру блока запроса CBL, а" % BLOCK,
|
||||
" * страница (%d) кратна ему же. Поэтому НИ ОДИН блок не пересекает" % PAGE,
|
||||
" * границу страницы, и проигрывателю не нужна логика стыка.",
|
||||
" *",
|
||||
" * Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80",
|
||||
" * 32-битная арифметика дорога, а так всё влезает в 8/16 бит.",
|
||||
" * Длина округлена вверх до блока — неполных блоков не бывает.",
|
||||
" */",
|
||||
"#ifndef POP_SOUND_TBL_H",
|
||||
"#define POP_SOUND_TBL_H",
|
||||
"",
|
||||
"#include <stdint.h>",
|
||||
"",
|
||||
"#define POP_SND_PAGES %d" % npages,
|
||||
"#define POP_SND_COUNT %d" % NSOUND,
|
||||
"#define POP_SND_BLOCK %d" % BLOCK,
|
||||
"",
|
||||
"typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;",
|
||||
"",
|
||||
"/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */",
|
||||
"static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {",
|
||||
]
|
||||
for sid in range(NSOUND):
|
||||
off, ln = table.get(sid, (0, 0))
|
||||
lines.append(" { %d, 0x%04X, %5d }, /* %2d */" % (off >> 14, off & 0x3FFF, ln, sid))
|
||||
lines += [
|
||||
"};",
|
||||
"",
|
||||
"static const char *const pop_snd_files[POP_SND_PAGES] = {",
|
||||
]
|
||||
lines += [' "SND\\\\s%d.bin",' % i for i in range(npages)]
|
||||
lines += ["};", "", "#endif", ""]
|
||||
with open(HDR, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
have = sum(1 for s in sounds.values() if s)
|
||||
print(f"ИТОГО: {have} эффектов, {len(blob)} Б = {npages} EMM-страниц -> {OUT_DIR}")
|
||||
print(f" заголовок -> {HDR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user