Я сражаюсь с 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?...