Skip to content

Audio and TKM

Владислав edited this page Sep 5, 2026 · 1 revision

Звук и .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), то есть показывает только шкалу эффектов.

.tkm - это MOD

Никакого своего формата: 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 бит, последний аргумент - те самые четыре голоса.

Clone this wiki locally