Упаковщик звуковых эффектов + сверка наборов 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:
@@ -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