
Преамбула
Знаете, случаются в жизни иногда такие ситуации, когда человеку внезапно как вдарит что-нибудь в голову, увесистое такое, и ему захочется сотворить какую-нибудь такую несусветную чушь, какой заниматься никому в здравом уме и в голову не придет. Пример такого тяжелого случая перед вами — взбрело в голову мне переозвучить некоторые диалоги в God of War 2018 (около двух тысяч аудиодорожек), и в моменте внезапно обнаружилось, что аудиомоддинг игры может быть слегка сложнее, чем я предполагал. Итогом стало желание написать серию статей на эту тему — от анализа файлов игры до конечного результата. Будет ли это руководством или примером того, как делать не надо — решать вам.
Пишу я это всё прямо в процессе, так что советы и иного рода фидбэк, который может мне помочь с этим абсурдом, буду рад увидеть в комментариях.
Дисклеймер: это мое первое погружение в моддинг игр в принципе, так что тапками бейте, но только не резиновыми и не сильно (пожалуйста).
Что имеем?
В начале было слово, и слово было "BOY"
Цель: перезаписать несколько аудиодорожек с диалогами и вшить их вместо оригинальных в английскую озвучку игры.
Нюанс: есть вероятность, что некоторые из переозвучиваемых диалогов я на ухо восприму слабо (особенно учитывая, что аудиофайлы с озвучкой идут вразброс и вне контекста), так что нам необходимо вычленить из игры еще и субтитры.
Имеется: папка игры, куча файлов в ней.
Опыт создания или хотя бы использования модов: отсутствует.
Теоретические знания о том, как работает звук (да хоть что-нибудь) в игре: отсутствуют.

Немного порывшись по интернету, в котором информации по данной теме не так уж и много, можно выяснить, что интересуют нас конкретно две директории и один файл:
%GAMEDIR%\exec\sound\pc_le\
%GAMEDIR%\exec\wad\pc_le\soundbanks\
%GAMEDIR%\exec\wad\pc_le\r_lang_en.wad
В первой папке хранится куча файлов формата .wem, во второй — файлы .sbp. Также в каждой из папок есть директории с языками озвучек. Третий товарищ — файл формата .wad со сжатыми данными.
Немного о том, с чем мы вообще имеем дело
(Не)интересные спойлерные детали
Для того, чтобы записать звук и встроить его в создаваемую игру, используется аудиодвижок под названием Audiokinetic Wwise от компании Audiokinetic. Он позволяет трансформировать Wave-звук (формата .wav) в формат "Wwise Encoded Media" (.wem), с которым уже ведется работа внутри движка — создаются события для звука, действия, эффекты накладываются и так далее. На выходе получается SoundBank-файл с расширением .bnk. В нем содержится как раз вся информация о звуках, событиях, эффектах, действиях — и даже сами WEM-файлы по итогу оказываются вшиты внутрь так называемой "банки", которые в конце концов и остаются в файлах игры.
Но студия-разработчик God of War, Santa Monica Studio, пошла немного другим путем. Она создала для BNK-файлов собственный контейнер. По своей сути товарищи просто добавили сверху BNK-файла еще немного метаданных по звуковым событиям и получившийся формат назвала SBP (SoundBank Package). При всем при этом большее количество WEM-файлов, которые хранились внутри банок, они оттуда извлекли и отправили на хранение в папку №1, наименовав каждый извлеченный файл его id-шником.
WAD — расширение с довольно старой историей, которая идет аж от оригинальных игр серии Doom, расшифровывается как "Where's All the Data". Конечно, файлы, о которых мы говорим, по своей внутренней структуре с WAD-ами старых Doom-ов ничего общего не имеют. В God of War это архив всевозможных скриптов, моделек и многого другого; конкретно наш файл в себе хранит весь текст игры на английском языке в несжатом формате, включая субтитры — вот поэтому он-то нам и нужен.
Если коротко и по факту:
WEM-файл — аудиодорожка, закодированная аудиодвижком Wwise;
BNK-файл (банка) — контейнер, содержащий в себе WEM-файлы и метаданные о том, где и как эти WEM-файлы применяются;
SBP-файл — контейнер для банки, где есть дополнительные метаданные о звуковых событиях, которые записаны в этой самой банке;
WAD-файл — своего рода архив всевозможных игровых данных. В частности
r_lang_en.wad
хранит в себе весь внутриигровой текст в несжатом формате.
С этим уже можно работать. Начнем с самого простого
Вычленяем субтитры
Ничто не ново под Луной, и всё придумано до нас
Выяснилось, что существует уже великое множество инструментов, которыми можно дербанить игровые файлы и вытаскивать из них всякие нужные вещи — в том числе для рассматриваемой игры. Например, один хороший человек с помощью других хороших людей написал на С++ целую тулзу конкретно для распаковки данных GoW2018 (по каким-то причинам не работающую с нужным нам WAD-ником, но товарищ все равно молодец!)
Также есть прекрасная штука под названием QuickBMS, в которой можно создавать кастомные скрипты для дробления файлов по байтам на языке с несложным синтаксисом. Для парсинга банок и вычленения из них WEM-файлов тоже есть соответствующий простейший инструмент. Или даже вот, еще удобнее — с прямой функцией замены файла внутри банки.
BMS-скрипт
Несмотря на то, что на каждую задачу я находил штуки три существующих решения, раздербанить WAD-ник мне удалось только с помощью QuickBMS-скрипта, который я обнаружил на одном из форумов.
Последовательность действий довольно проста:
Качаем QuickBMS с официального сайта, распаковываем;
Создаем файл
somemakingsensename.bms
;Вставляем туда код для вычленения всех файлов, которые были положены внутрь WAD-ника, сохраняем;
Запускаем
quickbms.exe
;Выбираем только что созданный файл BMS-скрипта;
Выбираем
r_lang_en.wad
;Выбираем папку, куда сохранять извлекаемые из WAD-ника файлы;
Наслаждаемся жизнью в течение половины секунды, пока файлы извлекаются.
somemakingsensename.bms
open FDDE "wad" 0
get WAD_SIZE asize 0
savepos OFFSET
do
goto OFFSET
get TYPE long
get SIZE long
get WAD03 long
get WAD04 long
get WAD05 long
get WAD06 long
getdstring NAME 0x48
savepos OFFSET
if SIZE != 0
log NAME OFFSET SIZE
endif
math OFFSET += SIZE
math OFFSET x= 0x10
while OFFSET < WAD_SIZE
После данной процедуры у нас появляется папка с кучкой файлов без расширений, один из которых именуется MSGS_TXT
и весит около двух мегабайт. Открываем его с помощью любого текстового редактора и видим, как и было сказано, весь текст, который появляется в игре, в приблизительно следующем формате:
*порядковый номер текстового элемента*
Текст текстового элемента
И ровно на 100000-ом текстовом элементе мы видим первый субтитр. Собственно, начинаем его анализировать.
Что нам дали субтитры?
Все субтитры в данном текстовом файле выглядят следующим образом:
*100000*
[[S::vo_lvl_pro_s010_010_kra_stem:1537-3447]]
(angry scream)
У нас, как уже было сказано, есть порядковый номер, у нас есть текст реплики — но самое главное, что мы видим, это название звукового события (sound event, далее "саунд-ивент"), которое является триггером для этого самого субтитра — vo_lvl_pro_s010_010_kra_stem
, а также тайм-коды его показа и скрытия в миллисекундах от начала этого самого саунд-ивента — 1537-3447
. Наша конечная цель в рамках данной части статьи — найти этот angry scream в аудио-формате, т.е. найти соответствующий WEM-файл. И каждый этап мы разберем подробно.
Расчленяем упаковки банок
Поиск нужного файла
Логично предположить, что, раз vo_lvl_pro_s010_010_kra_stem
в рамках субтитров не только является названием, но и играет роль идентификатора саунд-ивента, значит, его можно встретить где-то еще во внутриигровых бинарниках. Поэтому мы делаем самый очевидный шаг: выполняем поиск этого сочетания символов внутри всех файлов игры.
Готовых приложений для поиска файлов по их содержимому туча, но если вам, как и мне, слишком лень скачивать и устанавливать, чтобы единожды использовать и удалить, можно написать простецкий скрипточек на питоне на один раз.
binary_search(not_algo).py
import os
import sys
found_in = []
def search_in_file(path, pattern):
global found_in
f = open(path, 'rb')
if pattern in f.read():
found_in.append(path)
def walk_and_search(root, pattern):
for curdir, dirs, files in os.walk(root):
for filename in files:
filepath = os.path.join(curdir, filename)
print(f'Checking file {filepath}')
search_in_file(filepath, pattern)
def main():
if len(sys.argv) != 3:
print("Usage: python search_bin.py <path> <hex_pattern>")
return
target_path = sys.argv[1]
pattern = bytes.fromhex(sys.argv[2])
if os.path.isfile(target_path):
search_in_file(target_path, pattern)
elif os.path.isdir(target_path):
walk_and_search(target_path, pattern)
else:
print("Invalid path")
if len(found_in) > 0:
print(f"Found the pattern \"{sys.argv[2]}\" in next files:")
for filepath in found_in:
print(filepath)
else:
print("Found no pattern within specified path")
main()
Учитывая информацию, которую мы узнавали ранее о том, что в каких файлах содержится, можно не заставлять несчастного питона колупать все 60 с копейками гигабайт игровых файлов и сразу отослать в нужную папку. Вспоминая, что WEM-файлы это чисто аудио, а банки — информация об аудио, логично будет предположить, что информация о саунд-ивентах хранится в банках (а точнее, в упаковках с банками). Так что запускаем поиск по папке с SBP-файлами, предварительно переведя название нужного нам ивента в HEX-формат:
python main.py "%GAMEDIR%\exec\wad\pc_le\soundbanks" 766F5F6C766C5F70726F5F733031305F3031305F6B72615F7374656D
Скрипт благополучно завершил свою работу и показал, что нашел данное сочетание байт во всех файлах vo_lvl_forest.sbp
внутри папок с озвучкой на разных языках — что доказывает наше предположение о местонахождении информации о саунд-ивентах внутри баночных пакетов. Так что идем заниматься расчлененкой найденного SBP-файла.
Что такое SBP-файл
Поиски готовой инфы (успехом не увенчались)
Вот это было на самом деле интересно, потому что если по WAD-ам и BNK-ам информации мало, но она хотя бы есть, то информации о SBP-файлах нет практически от слова "совсем". Большая часть ссылок содержит рассуждения о SBP-файлах внутри игры Metal Gear Solid V, где постоянно ссылаются на утилиту GzsTool, но в той игре, судя по всему, SBP-файлы устроены совершенно по-другому, и данная тулза с SBP-шниками GoW-а не работала.
И все-таки в одном-единственном форуме нашлись товарищи, которые рассуждали о баночных пакетах бога войны, и там же был архив под названием GOW SBP Tool v1.0.rar
. Однако много полезного там не нашлось — лишь HEX-редактор XVI32, скрипты к нему и батник, автоматически запускающий эти скрипты. После просмотра скриптов и перечтения форума стало понятно, что данная "тулза" (товарищ это еще тулзой обозвал, во дает) просто вырезает у каждого SBP-файла определенное количество байт в начале, без которых баночная упаковка становится обычным BNK-файлом с соответствующей структурой.
Это, конечно, здорово, но так, как я нигде не нашел ни единого упоминания про саунд-ивенты и их названия, я посмотрел на XVI32, который лежал в архиве "тулзы", и понял, что пора его запускать и копаться в SBP-шнике самостоятельно. В принципе, достойный повод для первого опыта с HEX-редактором.
Чебурашимся в SBP-шнике

Вся SBP-обертка над банкой состоит, по сути, из двух секций.
Первая секция имеет длину в 96 байт и хранит в себе метаданные о самом SBP-файле. Вторая секция имеет заголовок на 104 байта — данные о содержимом этой самой секции — и кучу записей одного и того же формата в качестве содержимого секции. Нам эти первые 200 байт не критично нужны, но, учитывая, что информации о том, что они значат, в Интернете не существует, оставить на просторах Всемирной Паутины результаты моих умственных потуг лишним не будет.
Заглавная секция SBP-файла
1) uint16 — Мажорная версия SBP
2) uint16 — Минорная версия SBP
3) uint32 — длина SBP-файла (от начала след. секции)
4) char[16] — заглушка
5) char[72] — идентификатор текущего SBP-файла
Заголовок второй секции SBP-файла
1) uint16 — 0x0B (const)
2) uint16 — 0x01 (const)
3) char[8] — заглушка из нулей
4) uint32 — количество записей в следующей секции
5) uint32 — длина текущей секции (начиная с первого байта 0x0B)
6) uint32 — длина BNK-файла
7) uint32 — 0x0000, 0x0001 или 0x0002 (неизвестно)
8) char[8] — заглушка из нулей
9) uint32 — 0x0004 (const)
10) char[8] — заглушка из нулей
11) char[56] — название SBP-файла
Если пойти сверять эти данные в почти любом SBP-файле игры, то можно заметить, что некоторые значения из этой секции не соответствуют действительности (например, длина SBP-файла). К этому еще вернемся позже.
Так или иначе, самое важное для нас хранится в содержимом второй секции этой обертки. Это определенное количество записей одинаковой длины и структуры. Каждая запись хранит в себе данные о саунд-ивентах в следующем формате:
1) char[16] — неизвестно
2) uint32 — 0x0000, 0x0001 или 0x0002 (неизвестно)
3) uint32 — числовой идентификатор саунд-ивента
4) char[56] — название саунд-ивента
В случае нашей искомой записи со злостным ревом выглядит это примерно так:
Запись 2-й секции (на примере angry scream)
Байт-код в HEX-виде:4A E1 F2 0A 37 16 6F 92 01 00 00 00 7D CF A3 9B 53 4E 44 5F 76 6F 5F 6C 76 6C 5F 70 72 6F 5F 73 30 31 30 5F 30 31 30 5F 6B 72 61 5F 73 74 65 6D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
1) 4A E1 F2 0A 37 16 6F 92 | ——
2) 01 00 00 00 | ——
3) 7D CF A3 9B | 2611203965
4) 53 4E <...> 00 00 | SND_vo_lvl_pro_s010_010_kra_stem
Итак, мы узнали числовой идентификатор саунд-ивента и разобрались в принципе с SBP-оберткой над банками. Теперь пришло время обратиться к копиям древних манускриптов, оставленных нашими реверсниками-предшественниками.
Копошимся в банке
Некогда люди из одного известного сайта по реверс-инженирингу изучили BNK-файл и сделали по нему шпаргалку, где разобрали почти все его вариации по байтам. Ныне ссылка на шпаргалку приведет на страницу 404, но веб-архив хранит в себе несколько копий данной веб-страницы, так что пользуемся на здоровье.
Если вбить в поиск по бинарнику тот числовой идентификатор, который мы вычленили из списка саунд-ивентов, то нас перекинет на самый-самый конец файла. Мы уже знаем, что этот кусок данных является частью BNK-файла, а шпаргалка позволит понять, что смотрим мы на весьма специфичную часть секции HIRC.

Найти длину одной записи особого труда не составляет. Куда интереснее задача понять, где ее начало и где конец. Впрочем, если посмотреть, чем именно этот кусок секции начинается и чем оканчивается, то тоже все становится предельно понятно: мы смотрим на последовательно записанные объекты типов Event и Event Action. Не буду копировать лишний раз кучу HEX-а и сразу предоставлю распарсенный вариант того, что нас когда-нибудь приведет к истошно орущему Кратосу.
"Event Action"-object
1) 03 — идентификатор типа *Event Action*
2) 12 00 00 00 — размер данного объекта *18 байт*
3) D1 50 4B 20 — идентификатор данного объекта
4) 03 — тип исполнителя *игровой объект по id*
5) 04 — тип действия *воспроизвести объект*
6) BF C0 C5 05 — id игрового объекта
7) 00 — вечный нуль (константа)
8) 00 — количество доп. параметров
9) —— — *здесь могли быть доп параметры, но их 0*
10) 00 — вечный нуль (константа)
11) FD DC D8 6B — локальная константа (в шпаргалке не указана)
"Event"-object
1) 04 — идентификатор типа *Event*
2) 0C 00 00 00 — размер данного объекта *12 байт*
3) 7D CF A3 9B — идентификатор данного объекта
4) 01 00 00 00 — количество действий, выполняемых после данного события
5) D1 50 4B 20 — идентификатор действия, выполняемого после события
То есть тот id-шник являлся числовым идентификатором объекта события (логично — мы ведь и искали саунд-ивент). В записи об этом объекте есть идентификатор действия, которое следует за этим событием. Это действие в файле записано буквально прямо перед записью о самом ивенте, и в нем мы видим еще один идентификатор, который нам может пригодиться — id игрового объекта. Действие, которое следует за саунд-ивентом истерящего деда — "воспроизведение" этого объекта. Ищем!
После поиска нас мотнуло вверх по HIRC-секции, и мы наткнулись на очередной непонятный кусок байт-кода. Хотя из шпаргалки паттерн понятен: любая запись о любом объекте начинается с типа объекта, его размера и айдишника. Таким образом расчлененка информации становится уже совсем простым делом, а мы тут же видим, что пришли к звуковому объекту. Уже совсем близко!
"Sound"-object
1) 02 — идентификатор типа *Sound SFX/Sound Voice*
2) 3D 00 00 00 — размер данного объекта *61 байт*
3) BF C0 C5 05 — идентификатор данного объекта
4) 04 00 01 00 — неизвестная константа
5) 01 — где находится звук *транслируется извне банки*
6) CC D7 C1 16 — идентификатор аудиофайла (.wem)
7) D0 4F 00 00 — размер файла внутри банки (секция DATA)
8) 01 — тип звука *речь*
9) byte[43] — запись типа "Sound structure"
Вот и наш идентификатор файла, который мы так долго искали: CC D7 C1 16
Вспоминаем, что у нас процессор Малой Индианы, меняем очередность байт, переводим в десятичную систему — и на выходе у нас есть название файла: 381802444.wem
Заходим в директорию %GAMEDIR%\exec\sound\pc_le\
, судорожно выполняем поиск и... да! Мы нашли нужный файл!
Финал (теоретическо-ручной части)
Для приличия стоит проверить себя — мы, конечно, все сделали логично и будто бы правильно, но от случайных ошибок никто не застрахован.
Для того, чтобы воспроизвести WEM-файл, можно скачать плеер foobar2000, а затем к нему прицепить соответствующий плагин. Проигрываем и слышим наш долгожданный яростный вопль бога войны! А если подождать еще с минуту, то можно услышать и его указание садиться в лодку (это, кстати, следующий субтитр в MSGS_TXT
— к слову, с тем же саунд-ивентом).
По сути, цель данной части нашего путешествия можно считать успешно завершенной. На следующем этапе планируется исследование того, как можно аудиодорожки заменять, и создание соответствующей тулзы.

Комментарии (3)
Mingun
11.07.2025 10:25Может быть полезным оформить ваши изыскания формата в виде ksy файла с сделать PR в репозиторий с форматами. Шанс что примут 0, но вероятно последующие первопроходцы найдут вашу информацию и скажут спасибо.
Заодно, раз уж питон не чужд, можете проверить ваши находки, сгенерировав парсер и запустив его на всех файлах.
Tkachov
О, знакомо. Разбирался с портом Marvel's Spider-Man Remastered, там тоже Wwise, тоже своя обертка над .bnk, тоже wem'ы отдельно (хотя не все, бывают и вложенные в сам .bnk или "prefetch", когда в .bnk только первый кусочек).
Сильно в бинарную кашу Wwise не погружался, благо всяких скриптов и тулов для него много. Например, wwiser умеет генерить .txtp, по которым можно и понять что-то попробовать, и все тем же foobar2000 проиграть.
P.S. На хабре ссылка на телегу в конце статьи автоматически умножает ценность прочитанного на ноль, так что поакуратнее с этим (=
Gaymocoder Автор
Буду знать, спасибо)