SprPoP: звук мигания, равномерные часы, полоса HP после рестарта, надпись ждёт мелодию
Четыре правки прогона 2026-08-31. Две ПРОВЕРЕНЫ пользователем в MAME (звук мигания, порядок «мелодия -> надпись»), две ждут проверки — образ собран. ЗВУК МИГАНИЯ (проверено). Упаковщик научился синтезировать ноты PC- спикера в обычный сэмпл: заголовок с темпом, тройки «частота + длина», меандр на нашей частоте вывода. Берём из нот ТОЛЬКО номера, которых нет ни в оцифровке, ни среди мелодий, — иначе синтез перекрыл бы музыку, которую мы играем из MUS/. На поставке SDLPoP это ровно один номер: 38, сигнал под мигание «Press Button»; 31, 34 и 42 там пустые заглушки. Громкость по слуховой проверке снижена вдвое (44 -> 22): на полном размахе сигнал перекрикивал игру. EXE не меняется — раскладка читается с диска, набор занял те же 9 страниц. НАДПИСЬ ЖДЁТ МЕЛОДИЮ (проверено). Порядок оригинала: ветка мёртвого (seg006:1351) на седьмом шаге выходит, пока звук играет, и «Press Button» появляется только после музыки смерти. Чтобы ожидание не было принудительным, три быстрых пути (Ctrl+A, обе быстрые загрузки, пункты меню) музыку глушат — оригинал при Ctrl+A делает то же (seg000:0617). Обычная кнопка во время мелодии не действует: она ответ НА надпись. ЧАСЫ (ждёт проверки). Тик стоит столько кадров ЛУЧА, сколько их в кадре режима NORMAL, поэтому FAST/FASTEST больше не ускоряют время. Считаем ФАКТИЧЕСКИ прошедшие кадры луча, а не ожидаемый делитель: логический кадр не всегда укладывается в бюджет, и часы «по делителю» шли рывками (первый прогон это показал — «несколько секунд быстро, потом притормаживание»). Вклад одного вызова ограничен, иначе пауза и меню прыгнули бы вперёд. Четыре новых теста: NORMAL не сдвинулся ни на тик (и вне боя, и в бою), FAST и FASTEST держат реальное время. ПОЛОСА HP ПОСЛЕ Ctrl+A (ждёт проверки). Корень: счётчик считает СТРАНИЦЫ, а тратился по КАДРАМ — между двумя вызовами переворота может не быть, и оба прохода уходили в одну страницу, вторая оставалась с делениями прошлого боя. Теперь проход тратится только при смене gfx_get_draw_page(). Плюс полная чистка всей ширины при инвалидации: старая полоса могла заходить под статус-текст, где щадящая чистка её не трогала; текст сразу перезапрашивается. Все 16 наборов host-тестов зелёные, check_bank_calls чист. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
@@ -66,6 +66,35 @@ SND_SOURCES = {
|
||||
}
|
||||
|
||||
|
||||
# Ноты PC-СПИКЕРА и МЕЛОДИИ — два других набора того же движка. Нужны нам
|
||||
# по разным причинам: мелодии мы играем отдельно (MUS/), а из нот спикера
|
||||
# добираем те номера, которых нет НИ в оцифровке, НИ среди мелодий — сейчас
|
||||
# это ровно звук 38 (мигание надписи «Press Button»). В поставке SDLPoP
|
||||
# наборы лежат распакованными каталогами res1XXXX.bin, в дистрибутиве DOS —
|
||||
# контейнерами .DAT; поддерживаем оба вида.
|
||||
SND_MIDI = {
|
||||
"sdlpop": (SDLPOP_DATA, "MIDISND%d.DAT", 2),
|
||||
"msdos": (MSDOS, "midisnd%d.dat", 2),
|
||||
}
|
||||
SND_SPEAKER = {
|
||||
"sdlpop": (SDLPOP_DATA, "IBM_SND%d", 2),
|
||||
"msdos": (MSDOS, "ibm_snd%d.dat", 2),
|
||||
}
|
||||
|
||||
|
||||
def snd_extra(kind, name="sdlpop"):
|
||||
"""(каталог, шаблон, сколько контейнеров) для набора нот или мелодий.
|
||||
|
||||
kind — "midi" или "speaker". Набора может не быть вовсе (у пользователя
|
||||
нет MSDOS, поставка неполная) — это НЕ ошибка: вызывающий просто
|
||||
пропускает добор. Возвращает None, если источник неизвестен.
|
||||
"""
|
||||
table = {"midi": SND_MIDI, "speaker": SND_SPEAKER}.get(kind)
|
||||
if table is None:
|
||||
raise SystemExit("неизвестный набор %r; есть: midi, speaker" % kind)
|
||||
return table.get(name)
|
||||
|
||||
|
||||
def snd_src(name="sdlpop"):
|
||||
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
|
||||
if name not in SND_SOURCES:
|
||||
|
||||
@@ -108,6 +108,69 @@ def resample(src, src_rate, dst_rate):
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def load_bundle(dat_dir, pattern, count):
|
||||
"""id -> тело ресурса из набора любого вида.
|
||||
|
||||
Наборы приезжают в двух видах: контейнером `.DAT` (дистрибутив DOS) и
|
||||
распакованным каталогом `res1XXXX.bin` (поставка SDLPoP). Тело в обоих
|
||||
случаях начинается с байта типа, так что дальше разбор общий.
|
||||
"""
|
||||
out = {}
|
||||
for i in range(1, count + 1):
|
||||
p = dat_dir / (pattern % i)
|
||||
if p.is_dir():
|
||||
for f in sorted(p.glob("res1*.bin")):
|
||||
try:
|
||||
rid = int(f.stem[3:]) - 10000
|
||||
except ValueError:
|
||||
continue
|
||||
out[rid] = f.read_bytes()
|
||||
elif p.is_file():
|
||||
out.update(dat_resources(p))
|
||||
return out
|
||||
|
||||
|
||||
def speaker(body):
|
||||
"""(темп, [(частота Гц, длина в долях), ...]) или None — если это не ноты.
|
||||
|
||||
Формат — канон Princed, «Internal PC Speaker»: заголовок 3 байта, из них
|
||||
байт 1 — доли на ДВЕ секунды; дальше тройки «частота (2 байта) + длина
|
||||
(1 байт)»; нулевая частота = пауза, 1 и 2 — маркеры; в конце маркер
|
||||
`12 00`, который сам отсекается недобором до целой тройки.
|
||||
"""
|
||||
if not body or (body[0] & 7) != 0 or len(body) < 6:
|
||||
return None
|
||||
bps = body[1] or 1
|
||||
notes = []
|
||||
i = 3
|
||||
while i + 3 <= len(body):
|
||||
notes.append((struct.unpack("<H", body[i:i + 2])[0], body[i + 2]))
|
||||
i += 3
|
||||
return bps, notes
|
||||
|
||||
|
||||
def speaker_pcm(bps, notes, rate, amp=22):
|
||||
"""Меандр по нотам -> те же 8 бит без знака, что и оцифровка.
|
||||
|
||||
Амплитуда НАМЕРЕННО маленькая: сигналы спикера короткие и резкие, и на
|
||||
полном размахе они колют ухо рядом с оцифровкой, которая заметно тише.
|
||||
Значение снижено вдвое по слуховой проверке в MAME (решение пользователя
|
||||
2026-08-31): на 44 сигнал мигания перекрикивал игру.
|
||||
"""
|
||||
out = bytearray()
|
||||
for freq, beats in notes:
|
||||
n = int(round(beats * (2.0 / bps) * rate))
|
||||
if n <= 0:
|
||||
continue
|
||||
if freq <= 2: # 0 — пауза, 1 и 2 — маркеры
|
||||
out += bytes([0x80]) * n
|
||||
continue
|
||||
period = rate / freq
|
||||
for i in range(n):
|
||||
out.append(0x80 + amp if (i % period) < period / 2 else 0x80 - amp)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
||||
ap.add_argument("--source", default="sdlpop", choices=sorted(P.SND_SOURCES),
|
||||
@@ -151,6 +214,33 @@ def main():
|
||||
if rate != 11000:
|
||||
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
|
||||
|
||||
# ДОБОР ИЗ НОТ PC-СПИКЕРА (docs/TASKS_OPEN.md, SND-SPEAKER-38).
|
||||
# У оригинала три параллельных набора звука, и номера разложены по ним
|
||||
# не подряд: оцифровка, мелодии и ноты спикера. Берём из нот ТОЛЬКО те
|
||||
# номера, которых нет ни там, ни там — иначе синтез перекрыл бы собой
|
||||
# мелодии, которые мы играем отдельно, из MUS/. На поставке SDLPoP это
|
||||
# ровно один номер: 38, сигнал под мигание надписи «Press Button»
|
||||
# (31, 34 и 42 в наборе есть, но они пустые заглушки без нот).
|
||||
midi_cfg = P.snd_extra("midi", args.source)
|
||||
spk_cfg = P.snd_extra("speaker", args.source)
|
||||
midi_ids = set(load_bundle(*midi_cfg)) if midi_cfg else set()
|
||||
added = []
|
||||
for sid, body in sorted((load_bundle(*spk_cfg) if spk_cfg else {}).items()):
|
||||
if sid in sounds or sid in midi_ids:
|
||||
continue
|
||||
sp = speaker(body)
|
||||
if sp is None or not sp[1]:
|
||||
continue
|
||||
pcm = speaker_pcm(sp[0], sp[1], RATE)
|
||||
if not pcm:
|
||||
continue
|
||||
sounds[sid] = pcm
|
||||
added.append("%d (%d нот, %d Б)" % (sid, len(sp[1]), len(pcm)))
|
||||
if added:
|
||||
print(" из нот спикера: " + ", ".join(added))
|
||||
elif spk_cfg:
|
||||
print(" из нот спикера: добирать нечего")
|
||||
|
||||
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
|
||||
# ПЕРВЫЙ БЛОК — ТИШИНА. Он нужен насосу: когда эффект кончился, в CBL
|
||||
# всё равно надо что-то отдавать, иначе железо доигрывает по кругу хвост
|
||||
|
||||
Reference in New Issue
Block a user