Разбор форматов: звуковой пакет в Unreal Engine
В прошлый раз я рассказал об исследовании связи звука с текстом в движке UE3. Сегодня рассмотрю подробнее более простой случай — разбор пакета звуков Bioshock Infinite, с которого начиналась полная локализация этой игры на русский язык.
Разбор ресурсов — это как разгадывание ребуса или кроссворда: начинаем с одной стороны, и постепенно узнавая значение все новых полей, разгадываем всё остальное. Сначала структура ресурсов игры была для нас как тёмный лес. Но постепенно мы узнавали о них всё больше и больше, одно тянуло за собой другое, и в итоге были исследованы и модифицированы практически все ресурсы игры: звук, текстуры, 3D-модели. Пример надписи, изменённой в виде модели вместе с текстурой, картами освещения и отражения, светящимися лампочками, бликами и тенью, можно видеть на картинке.
Но началось всё это с простой на первый взгляд задачи: найти, где в игре находится звук, и как его можно заменить.Бегло просмотрев содержимое папки с игрой, обнаруживаем, что в папке «audio» находится большой файл.рck — пакет, в котором скорее всего и содержатся все звуки (фразы) из игры, нужно разобрать его формат, извлечь звуки, заменить все (или некоторые из них), и собрать файл обратно.
Открыв его в Hex-редакторе и промотав туда-сюда, можно предположить, что в начале имеется заголовок, затем таблица с данными по каждому файлу, а потом собственно все эти файлы, склеенные друг за другом. Чтобы не запутаться, будем далее называть большой файл (весь пакет) «файлом», а маленькие файлы, из которых он состоит — «звуками». Посмотрим теперь подробнее:
…
Начнём с заголовка. В нём мы видим сначала код АКРК (признак типа файла, или как некоторые его называют, волшебное слово), потом какие-то цифры, и надписи «english (us)», «sfx». Мы не знаем значения этих цифр, но они, как выяснится в дальнейшем, нам и не нужны. Это различные свойства пакета, размер заголовка, размер его отдельных частей, и т.д. Поэтому, так как мы собираемся изменить содержимое пакета (звуковые файлы), не меняя ничего другого, мы можем просто оставить заголовок, как он был. В нём часто бывает ещё общая длина файла, и тогда, конечно, пришлось бы её менять, но в данном случае её нет. Иначе мы бы сразу заметили наличие этого числа (0хF49668E) в заголовке.
Теперь перейдем к таблице. Её всегда можно распознать по повторяющейся структуре, при «проматывании» файла она характерно «переливается». Даже не зная её содержимого, можно обычно определить число элементов в строке, но мы не знаем точно, где она начинается, и сколько в ней элементов (сколько звуков в пакете). Однако, проанализировав содержимое, мы сможем это определить.
В таблице, как правило, записано число элементов, а также адрес (смещение) и длина для каждого из них. Сами звуковые файлы, как видно, имеют стандартные RIFF-заголовки, поэтому можно сразу узнать, что первый звук начинается с адреса 0×2ED58 (зеленым) и имеет длину 0×981C (желтым), второй — с адреса 0×38574 и имеет длину 0×20B3, и т.д. Также можно узнать, сколько их всего, просто посчитав количество этих RIFF-заголовков: 9587 (0×2573). Это число мы видим в файле по адресу 0×54 (выделено красным).
Если бы формат звуков был нестандартный, то определить это было бы сложнее, не говоря уже о том, что пришлось бы писать программу для преобразования в нужный формат, или даже специальный кодек, как это было сделано для Dead Space, но об этом в другой раз.
Теперь мы можем взять общую длину заголовка пакета вместе с таблицей и поделить на число звуков. Первый звук начинается с адреса 0×2ED58 (191832). 191832/9587=20.01 Число получилось не целым, потому что мы не знаем длину заголовка. Таким образом, на каждый звук в таблице приходится по 20 байтов, т.е. пять 32-битных слов, и общая длина таблицы = 9587×20 = 191740. И тем не менее, мы по-прежнему не знаем, где начинается таблица, ведь она может быть где-то в середине, а после неё могут быть еще какие-то параметры. Мы знаем только длину таблицы, а остальные 191832–191740=92 байта остаётся на заголовок. 191740 = 0×2ECFC — похожее число (Ох2ED00) мы видим в заголовке по адресу 0×14, почему-то оно меньше на 4).
Займёмся теперь содержимым таблицы. Мы знаем длину и адрес первого звука — 0×981C и 0×2ED58, и находим их по адресу 0×60. Предположим, что таблица начинается с этого места. Дальше у нас идёт единичка, потом какое-то число, и потом опять единичка. Потом идут длина и адрес второго звука и т.д. до конца таблицы:
0000981C 0002ED58 00000001 00068064 00000001 000020B3 00038574 00000001 0007С532 00000001 00003587 0003A627 00000001 0008A458 00000001 … 00013D1B 0F482973 00000001 00000000 Видно, что последний звук как раз идёт до конца файла (его длина 0×13D1B плюс адрес начала 0хF482973 как раз дают общую длину файла-пакета 0хF49668E), но на этом таблица обрывается, дальше уже идёт первый звук с RIFF-заголовком (выделен розовым). Значит наше предположение было неверным, и таблица начинается раньше, сразу после количества звуков. Сместимся назад до адреса 0×58 и получим следующее: 00023E36 00000001 0000981C 0002ED58 00000001 00068064 00000001 000020B3 00038574 00000001 0007С532 00000001 00003587 0003A627 00000001 … 3FFE9BEF 00000001 00013D1B 0F482973 00000001 00000000 То есть в таблице 9587 строк, для каждого звука: какое-то число, потом единичка, потом адрес и длина звука, и в конце единичка. После таблицы ноль, видимо признак того, что таблица закончилась, или еще зачем-то, мы не знаем, поэтому просто оставим его как есть. Этот ноль — четыре байта, добавив которые к 0×2ECFC мы как раз получим общую длину таблицы, которая записана в начале файла по адресу 0×14.Мы могли бы пойти другим путём, например, найдя, где в таблице записан адрес первого (0×64) и второго (0×78) звука, сразу вычислить размер записи об одном звуке — 20 байт. Затем разделить примерный размер таблицы на 20, получить примерное количество записей, а потом убедиться, что оно действительно записано в файле по адресу 0×54. Или еще более другим путём: просто попробовать найти в начале файла число звуков, так как в игре явно несколько тысяч звуков, единственным подходящим числом будет 9587 — все остальные числа либо слишком маленькие, либо слишком большие. Затем поделить размер заголовка на это число, и т.д. В общем, откуда бы мы не начали копать, в итоге должны разобрать заголовок и таблицу звуков, определить количество элементов, размер и структуру её записей.
Теперь мы можем написать простенькую программу, которая попробует «нарезать» пакет на отдельные звуки. Сделав это и сконвертировав их из формата wwise в обычный ogg, мы сможем их все послушать. Но теперь неплохо бы убедиться, что у нас получится запихнуть их обратно. Запускаем игру. В самом начале, сразу после заставки, экран темнеет, и женский голос что-то произносит. Найдём, в каком файле находится эта фраза, заменим её на свою и закодируем с помощью wwise, который мы скачали с официального сайта (пакет бесплатен для некоммерческого использования). Пока не будем пересобирать пакет, а вставим фразу прямо в то же место, где она была. Запускаем игру — и она действительно работает. Отлично, значит продолжим исследования.
Вернемся к структуре таблицы. 3 и 4 числа — это, как мы уже определили, длина и адрес. Что же означают остальные? На самом деле это нам тоже не нужно. Можно проверить, что 2 и 5 числа — это всегда единички, а первое число — разное для каждого звука, то есть нам будет достаточно запомнить эти числа, и при сборке нового файла поместить их в таблицу как были. Если уж игра не будет работать — тогда можно разбираться, что это такое и зачем они нужны.
Позже, по ходу работы с озвучкой, мы выяснили, что это за числа. Оказалось, что в игре 2 пакета звуков — один с голосом, другой со звуковыми эффектами. Последнее число в строке — это тип звука (0-эффект, 1-голос). Значение другой единички стало ясно, когда мы посмотрели содержимое того же самого пакета для XBOX:
Видно, что вместо единичек тут везде 0×800. Посмотрев, как расположены звуки, можно догадаться, что это величина выравнивания в байтах. В пакете для XBOX звуки внутри файла выравнены по границам 2048 байт. То есть если длина звука не кратна 2048, он добивается нулями. Однажды я видел, как принцип этого «добоя» объяснялся аж несколькими абзацами, но мне кажется это проще всего показать наглядно. Содержимое звуков выделено зеленым, а между ними — нули, это тот самый добой, который продолжается до ближайшей границы в 2048 байт.
Теперь остаётся неизвестным только первое число в строках таблицы. Оно похоже на уникальное значение, монотонно возрастающее в диапазоне от 0 до 230. Это уникальные идентификаторы звуков, хеш, по которому игра находит их в пакете. Известна даже формула, по которой он подсчитывается.
Таким образом, чтобы изменить звуки в игре, нам надо написать программу, которая разберет файл-пакет на отдельные файлы-звуки, запомнив при этом идентификаторы, и потом соберет его обратно, сделав точно такой же заголовок, новую таблицу, где будут новые адреса и длины звуков, и затем слеплены вместе все звуки подряд. Где запоминать идентификаторы — это все делают кому как нравится. Можно в текстовом виде, XML, или даже подписывать число в конец имени файла. Вот так примерно будет выглядеть этот список:
146998 vo_fndr_female_02_taunt_creepy_20943 426084 vo_vox_male_02_gammaReact_close_20235 509234 vo_vox_male_06_emotion_scream_21209 566360 vo_vox_female_04_taunt_22534 612724 vo_vox_male_10_reload_21286 971110 vo_vox_male_06_death_21181 При сборке обязательно надо сохранить точно такой же порядок файлов. В принципе можно не задумываться, зачем, действуя просто из обычного принципа — сделать так, как было. Но на самом деле поиск звука в пакете происходит с помощью старого, но до сих пор незаменимого метода половинного деления — для этого они отсортированы по возрастанию. В нашем примере максимум за 14 операций можно найти нужный звук из всех 9587, вместо того, чтобы перебирать их по одному. Если мы перепутаем порядок, алгоритм перестанет работать, и некоторые звуки просто не найдутся, и не воспроизведутся, когда они будут нужны.Допустим, мы написали программу, которая производит все нужные операции. С учетом того, что мы знаем структуру файлов, сделать это несложно. И вот наступает волнующий момент первого запуска игры с пересобранным звуком. Проходят заставки, темнеет экран, музыка стихает, и после небольшой паузы в полной тишине звучит уже русский голос. Прекрасно, значит мы всё сделали правильно. К тому же нам повезло: похоже данный .pck файл оказался самодостаточным — нужная информация о звуках хранится только в нём, поэтому от того, что мы изменили длину и содержание звуков, ничего не сломалось. Можно начинать озвучку — готовить материал и отдавать актёрам.
Могло быть и хуже. В некоторых играх (в том числе и на движке UE3) ссылки на смещение звуков внутри пакета находятся в других файлах, причем в худшем случае во многих местах. То есть если какой-то звук используется в 10 местах в игре, то будет 10 ссылок, и тогда у нас было бы 2 варианта: либо искать все эти места и заменять ссылки, либо сделать звуки таким образом, чтобы их длина была точно как в оригинале. А это сложно, а иногда и невозможно. Но, как уже говорилось ранее, нам повезло, звуки заменились при произвольной длине, и игра заработала.
Через несколько месяцев, когда работа уже вовсю шла, и было озвучено довольно много ролей, при тестировании мы с удивлением обнаружили, что один из персонажей говорит одну фразу на английском, хотя мы все фразы заменили, а в другом месте другой персонаж вообще молчит, и просто открывает рот. То есть работает всё, кроме нескольких звуков. Вот например такая сцена:
Здесь Роберт и Розалинда предлагают игроку кинуть монетку, и угадать, что выпадет, орёл или решка. До этого момента всё шло замечательно, весь диалог уже был озвучен, обработан и вставлен в игру. И вот, Роберт спокойно и невозмутимо, как будто так и надо, начинает: «Heads…», а Розалинда, ничуть не удивившись, выпаливает:»… или решка?»
Как же так? почему не сработали эти фразы и почему по-разному? Надо сказать, что для UE3 это обычная ситуация, он полон подобных сюрпризов. Чтобы устранить проблему, нам пришлось разобрать еще несколько файлов других форматов, но это уже другая история.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.