Repository navigation
Audio and TKM
Звука в игре два вида, и устроены они совершенно по-разному: короткие эффекты -
обычные .wav, а музыка - трекерный модуль ProTracker, которому просто
поменяли расширение на .tkm. Оригинал получал и то и другое от hss.dll
(класс hssSpeaker); порт содержит собственный плеер, переписанный по её
дизассемблеру.
| Слот | Файл | Громкость | Когда |
|---|---|---|---|
JB_SND_BOUNCE 0 |
Sounds/sblam.wav |
0x20 |
мяч коснулся трассы |
JB_SND_MENU 1 |
Sounds/smenu.wav |
0x40 |
перемещение по меню |
| Контекст | Файл | Громкость |
|---|---|---|
| меню | Musics/mainmenu.tkm |
0x14 |
| игра |
Musics/music1.tkm или music2.tkm
|
0x28 / 0x38
|
Игровой трек выбирается rand() % 2 при каждом входе в уровень
(Music_LoadForContext 0x0001db68), у двух файлов разная штатная громкость.
Вся музыка зациклена, оба эффекта - нет.
Звук отбоя играет не физика, а jb_main.c: Player_Step только поднимает флаг
landed, и кадровый цикл дергает Platform_SoundPlay ровно там, где оригинал
звал hssSpeaker::playSound перед записью 0.0 в g_ballY - см. Physics.
Audio_VolumeStep (оригинал Volume_Step 0x00012b54) - один пункт меню на два
независимых регулятора:
if ((unsigned)(step * 2 + jb_volume) < 0x21u) jb_volume += step * 2;
if ((unsigned)(step * 4 + jb_music_vol) < 0x41u) jb_music_vol += step * 4;Эффекты идут шагом 2 до 0x20, музыка шагом 4 до 0x40. Сравнение
беззнаковое, поэтому отрицательный шаг ниже нуля даёт огромное unsigned и
проверка просто не пропускает его - нижняя граница получается бесплатно, без
отдельного if. Полоска в меню рисуется шириной volumeSounds * 2
(Menu_DrawFrame 0x0001cf70), то есть показывает только шкалу эффектов.
Никакого своего формата: Mod_Play (hssSpeaker::playMusic 0x00103f1c) читает
классический ProTracker и отвергает файл, если что-то не сходится.
| Смещение | Что | Проверка |
|---|---|---|
0x000 |
имя песни | не читается |
0x014 |
31 заголовок сэмплов по 30 байт | длина, finetune, громкость, петля |
0x3b6 |
длина песни |
1..0x80, иначе отказ |
0x3b7 |
точка перезапуска |
0x7f или >= songlen -> 0 |
0x3b8 |
порядок паттернов (MOD_ORDER_OFS) |
максимум задаёт число паттернов |
0x438 |
подпись |
TranslateSign, 1..0x20 каналов |
0x43c |
паттерны |
numpat * chans * 0x100 байт |
| дальше | тела сэмплов | обрезаются по фактическому размеру файла |
TranslateSign (hssMusic::translatePTSign 0x00102648) разбирает подпись сама,
без таблицы известных строк: M.K. (и M!K!, потому что сравнение идёт по
p[0] и p[2] с | 0x20) - это 4 канала, а всё остальное вида NNCH/NCHN
даёт число из первых двух цифр. Поэтому 6CHN, 8CHN, 16CH, 32CH работают
без изменений в коде.
Файл ниже 0x43c байт, число каналов вне 1..0x20, длина песни вне 1..0x80
или таблица паттернов, выходящая за конец файла - любое из этого сразу
free(data) и возврат 0. Ни одного «постараемся сыграть, что есть».
Ячейка - четыре байта, адресуется так же, как в оригинале:
p = pat + (cellofs + patno * chans * 0x40 + ch) * 4;0x40 строк на паттерн, отсюда и chans * 0x100 байт на паттерн в таблице выше.
Mod_Init (hssSpeaker::createTables 0x00103664) при открытии устройства
строит две таблицы, и в кадре больше нет ни одного деления:
for (p = 0; p < 0x1000; p++) note[p] = FindBestNote(p);
step[0] = 0;
for (p = 1; p <= MOD_STEPMAX; p++)
step[p] = (unsigned)((double)(MOD_CLOCK / p) * 65536.0 / out_rate);MOD_CLOCK = 7159090 (NTSC-частота Amiga), MOD_STEPMAX = 0x7ff = 2047, шаг
хранится в формате 16.16. Обратите внимание на MOD_CLOCK / p - целочисленное
деление до умножения на 65536.0; так считал оригинал, и порт не «улучшает» это до
одного выражения с плавающей точкой.
note[0x1000] - обратная таблица: для любого периода FindBestNote
(0x00103604) заранее нашёл ближайшую из 36 нот. Нужна она эффекту 0x3 (тон-
портаменто), которому надо превратить период из ячейки в номер ноты, чтобы
применить finetune.
Два независимых числа, ровно как в трекере:
-
speed- сколько тиков в строке, по умолчанию 6, меняется эффектом0xFприparam <= 0x20; -
tickhz- частота тиков в герцах, по умолчанию0x32= 50, меняется тем же эффектом приparam > 0x20по формулеparam * 2 / 5(то есть 125 BPM -> 50 Hz).
Mod_SetTempo переводит герцы в число сэмплов на тик:
d = ((MOD_BASEFREQ << 8) / 0xac44) * tickhz; /* MOD_BASEFREQ == 0xac44 */
spt = (outrate << 8) / d;MOD_BASEFREQ и делитель - одно и то же число 44100, так что первая скобка
всегда даёт 0x100, и всё выражение сводится к outrate / tickhz: при 22050 Hz
и 50 Hz это 441 сэмпл на тик. Порт сохраняет вычисление буквально - деление на
литерал, который равен самому себе, это форма оригинала, а не описка.
Tick() вызывается на каждый выходной сэмпл и решает, что делать:
if (tick == speed) { if (patdelay == 0) { Mod_ProcessRow(); Mod_AdvanceRow(); tick = 0; }
else { tick = 0; patdelay--; } }
else if (sctr < spt) sctr++;
else { sctr = 0; tick++; if (tick != speed) Mod_TickEffects(); }Эффекты разделены на две группы: одни срабатывают один раз при разборе строки,
другие - на каждом тике. Все они выписаны по одному адресу,
hssSpeaker::updateModSFX 0x00104370.
| Код | Название | На строке | На тике |
|---|---|---|---|
0x0 |
арпеджио | заполняет три шага (0/1/2) | крутит фазу, если param != 0
|
0x1 |
портаменто вверх | portabase = period |
-= param, минимум 0xd
|
0x2 |
портаменто вниз | portabase = period |
+= param, максимум MOD_MAXPERIOD
|
0x3 |
тон-портаменто | цель из Mod_Period
|
подтягивает период к цели |
0x4 |
вибрато | скорость parhi, глубина parlo
|
сдвигает vibpos
|
0x5 |
тон-портаменто + слайд | цель + величина слайда |
TickPorta + VolSlide
|
0x6 |
вибрато + слайд | величина слайда |
TickVibrato + VolSlide
|
0x9 |
смещение сэмпла | (parlo + parhi * 0x10) * 0x1000000 |
- |
0xa |
слайд громкости | parhi == 0 ? -parlo : parhi |
VolSlide |
0xb |
переход по позиции | назад без loop = конец песни |
- |
0xc |
установка громкости | mixvol = (param * volbase) >> 6 |
- |
0xd |
обрыв паттерна | jumpcell = (parlo + parhi * 10) * 4 |
- |
0xe |
расширенные |
E6 петля, EA/EB тонкий слайд, EC обрыв ноты, ED задержка ноты, EE задержка паттерна |
TickEffectE |
0xf |
скорость / темп | param > 0x20 ? tickhz : speed |
- |
Две детали, которые видно только в дизассемблере:
- у
0xd(обрыв паттерна) множитель десятичный:parhi * 10, а не* 0x10. В ProTracker номер строки в этом эффекте действительно записан как BCD; -
0x1000000в эффекте0x9- это0x100(шаг сэмпла в 16.16 - 256 сэмплов на единицу параметра), домноженный на0x10000формата позиции.
MixFrame - один моно-сэмпл, накопитель int, никакой панорамы:
if (s->replen != 0 && s->repstart + s->replen <= t->pos) t->pos -= s->replen;
if (t->pos < s->len) { acc += s->data[t->pos >> 16] * t->mixvol; t->pos += t->step; }Петля - вычитание длины петли из позиции, а не возврат на её начало, поэтому
дробная часть позиции переживает переход и петля не «щёлкает». Результат
обрезается в Mod_Render до -0x8000..0x7fff.
Поверх этого jb_audio_sdl2.c держит четыре голоса WAV: Mod_Render
заполняет буфер музыкой, затем SDL_MixAudioFormat подмешивает активные
эффекты с громкостью snd->volume * master / JB_MASTER_VOLUME_MAX. Голос
занимается первым свободным слотом, кончился сэмпл - слот освобождается.
Устройство открывается как 22050 Hz, AUDIO_S16LSB, 1 канал, буфер 512 -
оригинал звал hssSpeaker::open(0x5622, 0x10, false, 1, 4), где 0x5622 = 22050,
0x10 = 16 бит, последний аргумент - те самые четыре голоса.
Устройство порта
Сборка и происхождение