Я сражаюсь с Wwise (фото в цвете, восстановлено)
Я сражаюсь с Wwise (фото в цвете, восстановлено)

Преамбула

Знаете, случаются в жизни иногда такие ситуации, когда человеку внезапно как вдарит что-нибудь в голову, увесистое такое, и ему захочется сотворить какую-нибудь такую несусветную чушь, какой заниматься никому в здравом уме и в голову не придет. Пример такого тяжелого случая перед вами — взбрело в голову мне переозвучить некоторые диалоги в God of War 2018 (около двух тысяч аудиодорожек), и в моменте внезапно обнаружилось, что аудиомоддинг игры может быть слегка сложнее, чем я предполагал. Итогом стало желание написать серию статей на эту тему — от анализа файлов игры до конечного результата. Будет ли это руководством или примером того, как делать не надо — решать вам.

Пишу я это всё прямо в процессе, так что советы и иного рода фидбэк, который может мне помочь с этим абсурдом, буду рад увидеть в комментариях.

Дисклеймер: это мое первое погружение в моддинг игр в принципе, так что тапками бейте, но только не резиновыми и не сильно (пожалуйста).

Что имеем?

В начале было слово, и слово было "BOY"

Цель: перезаписать несколько аудиодорожек с диалогами и вшить их вместо оригинальных в английскую озвучку игры.

Нюанс: есть вероятность, что некоторые из переозвучиваемых диалогов я на ухо восприму слабо (особенно учитывая, что аудиофайлы с озвучкой идут вразброс и вне контекста), так что нам необходимо вычленить из игры еще и субтитры.

Имеется: папка игры, куча файлов в ней.

Опыт создания или хотя бы использования модов: отсутствует.

Теоретические знания о том, как работает звук (да хоть что-нибудь) в игре: отсутствуют.

Я на момент осознания всего масштаба предстоящего весельяМоддим Wwise-озвучку God of War — Часть I (чебурашимся в файлах)
Я на момент осознания всего масштаба предстоящего веселья

Немного порывшись по интернету, в котором информации по данной теме не так уж и много, можно выяснить, что интересуют нас конкретно две директории и один файл:

  1. %GAMEDIR%\exec\sound\pc_le\ 

  2. %GAMEDIR%\exec\wad\pc_le\soundbanks\

  3. %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 это архив всевозможных скриптов, моделек и многого другого; конкретно наш файл в себе хранит весь текст игры на английском языке в несжатом формате, включая субтитры — вот поэтому он-то нам и нужен.

Если коротко и по факту:

  1. WEM-файл — аудиодорожка, закодированная аудиодвижком Wwise;

  2. BNK-файл (банка) — контейнер, содержащий в себе WEM-файлы и метаданные о том, где и как эти WEM-файлы применяются;

  3. SBP-файл — контейнер для банки, где есть дополнительные метаданные о звуковых событиях, которые записаны в этой самой банке;

  4. WAD-файл — своего рода архив всевозможных игровых данных. В частностиr_lang_en.wad хранит в себе весь внутриигровой текст в несжатом формате.

С этим уже можно работать. Начнем с самого простого

Вычленяем субтитры

Ничто не ново под Луной, и всё придумано до нас

Выяснилось, что существует уже великое множество инструментов, которыми можно дербанить игровые файлы и вытаскивать из них всякие нужные вещи — в том числе для рассматриваемой игры. Например, один хороший человек с помощью других хороших людей написал на С++ целую тулзу конкретно для распаковки данных GoW2018 (по каким-то причинам не работающую с нужным нам WAD-ником, но товарищ все равно молодец!)

Также есть прекрасная штука под названием QuickBMS, в которой можно создавать кастомные скрипты для дробления файлов по байтам на языке с несложным синтаксисом. Для парсинга банок и вычленения из них WEM-файлов тоже есть соответствующий простейший инструмент. Или даже вот, еще удобнее — с прямой функцией замены файла внутри банки.

BMS-скрипт

Несмотря на то, что на каждую задачу я находил штуки три существующих решения, раздербанить WAD-ник мне удалось только с помощью QuickBMS-скрипта, который я обнаружил на одном из форумов.

Последовательность действий довольно проста:

  1. Качаем QuickBMS с официального сайта, распаковываем;

  2. Создаем файл somemakingsensename.bms;

  3. Вставляем туда код для вычленения всех файлов, которые были положены внутрь WAD-ника, сохраняем;

  4. Запускаем quickbms.exe;

  5. Выбираем только что созданный файл BMS-скрипта;

  6. Выбираем r_lang_en.wad;

  7. Выбираем папку, куда сохранять извлекаемые из WAD-ника файлы;

  8. Наслаждаемся жизнью в течение половины секунды, пока файлы извлекаются.

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 — к слову, с тем же саунд-ивентом).

По сути, цель данной части нашего путешествия можно считать успешно завершенной. На следующем этапе планируется исследование того, как можно аудиодорожки заменять, и создание соответствующей тулзы.

So... what do you think?...
So... what do you think?...

Комментарии (3)


  1. Tkachov
    11.07.2025 10:25

    О, знакомо. Разбирался с портом Marvel's Spider-Man Remastered, там тоже Wwise, тоже своя обертка над .bnk, тоже wem'ы отдельно (хотя не все, бывают и вложенные в сам .bnk или "prefetch", когда в .bnk только первый кусочек).

    Сильно в бинарную кашу Wwise не погружался, благо всяких скриптов и тулов для него много. Например, wwiser умеет генерить .txtp, по которым можно и понять что-то попробовать, и все тем же foobar2000 проиграть.

    P.S. На хабре ссылка на телегу в конце статьи автоматически умножает ценность прочитанного на ноль, так что поакуратнее с этим (=


    1. Gaymocoder Автор
      11.07.2025 10:25

      P.S. На хабре ссылка на телегу в конце статьи автоматически умножает ценность прочитанного на ноль, так что поакуратнее с этим (=

      Буду знать, спасибо)


  1. Mingun
    11.07.2025 10:25

    Может быть полезным оформить ваши изыскания формата в виде ksy файла с сделать PR в репозиторий с форматами. Шанс что примут 0, но вероятно последующие первопроходцы найдут вашу информацию и скажут спасибо.

    Заодно, раз уж питон не чужд, можете проверить ваши находки, сгенерировав парсер и запустив его на всех файлах.