Ремонтируем музыку в старой игре
Вторник, 1. Январь 2013
Раздел: C/C++, Windows, автор: Kaimi
Недавно игрался в забавную игру-головоломку на iPad под названием The Incredible Machine, игра понравилась, поэтому решил поискать что-то аналогичное на PC. Обнаружилось, что эта игра является переделкой старой серии игр. Окинув взглядом серию, решил скачать The Incredible Machine 3 под Windows, обладающую довольно сносной графикой на мой вкус.
Отличная игра, но обнаружился небольшой негативный момент, состоящий в том, что в качестве саундтрека выступали MIDI-файлы, несмотря на наличие качественных композиций в CD-версии игры (согласно Wiki). Неприятность была списана на недосмотр со стороны разработчиков, разместивших игру на GOG.com (откуда она и была взята изначально), однако качественный саундтрек с CD-версии всё-таки поставлялся в виде набора MP3-файлов, но без очевидной возможности интеграции его в игру. Я решил исправить это досадное упущение и реализовать костыль, позволяющий играть в игру и наслаждаться качественным звуком.
Для начала нам необходимо провести небольшое исследование игры, чтобы определить, где хранится информация о текущем проигрываемом треке, чтобы потом реализовать тривиальную программу, которая будет считывать её и воспроизводить необходимый MP3-файл.
Начнем с наивного пути, запустим игру под отладчиком (например, OllyDbg), начнем проходить какой-нибудь уровень и посмотрим имя проигрываемого в данный момент трека. На уровне, где в данный момент находился я, это был трек с названием Pictures. Откроем карту памяти в отладчике и поищем это название. Натыкаемся на любопытную таблицу:
Мы видим перечень треков игры, хотя часть названий из списка не встречается в меню выборе трека для текущего уровня. Интересных мест с упоминанием названия трека в памяти больше не находится. Сделаем логичное предположение, что игра оперирует ID трека, а эта таблица необходима для сопоставления ID - Название. Сохраним эту табличку куда-нибудь, она нам ещё пригодится.
1000 TIM
1001 Unplugged
1007 Steel Drums
1002 New Age
1003 Hay Seed
1004 Progressive Rock
1005 Salsa
1006 Techno Rave
1013 1959 Prom
1011 Bongo Bango
1021 Ragtime
1014 Hip Hop
1012 Keep Tryin'
1017 Detective Theme
1015 Dreams
1016 Tuna Loaf
1018 60's Rock
1019 Pictures
1020 Huey Dewey
Кстати, если отвлечься и посмотреть содержимое архива с саундтреком, то мы увидим, что треки в нём идут в том же порядке, несмотря на непоследовательность ID.
Продолжим исследование. Попробуем оттолкнуться от того, как игра воспроизводит MIDI-файлы. MSDN намекает, на возможное использование WinAPI-функции midiOutOpen перед непосредственным проигрыванием композиции, также dx подсказал, что функция waveOutWrite тоже является возможным кандидатом на использование. Поставим точки останова на эти функции и попробуем поиграть. Ловим срабатывание точки на функции waveOutWrite.
Видим, что обращение произошло из модуля SOS9502. Действительно, в директории с игрой присутствует SOS9502.DLL, которая используется для проигрывания треков. Посмотрим таблицу экспортов этой библиотеки:
Солидная таблица, а в самом низу находится функция с занимательным именем sosMIDIStartSong (мы ведь помним, что в игре используется MIDI-саундтрек). Попробуем поставить на этой функции точку останова. Анализируем стек вызовов:
Отмечаем подозрительный аргумент 000003EA, который в десятичном виде соответствует числу 1002 или треку New Age из таблицы выше. Рассмотрим функцию по адресу 0041126D подробнее. В ней мы видим следующую конструкцию:
Перед нами простой switch-case, который объясняет отсутствие части треков в настройках звука в игре. Его можно представить следующим кодом:
switch(track_id){case1007: track_id =1011;break;case1005: track_id =1021;break;case1014: track_id =1017;break;case1015: track_id =1002;break;} |
Треки с идентификаторами 1007, 1005, 1014 и 1015 действительно отсутствуют в настройках, хотя имеются в официальном саундтреке с диска. Также обращаем внимание на строку:
MOVDWORD PTR DS:[481390],ESI |
Налицо работа с глобальной переменной, куда сохраняется идентификатор проигрываемого трека. В этом нетрудно убедиться опытным путем. Исследование завершено.
Теперь напишем простую программу, которая будет читать эту переменную и проигрывать нужный MP3-файл. Но для начала давайте переименуем файлы саундтрека, чтобы имя состояло только из идентификатора и расширения. Руками это утомительно, поэтому сделаем простенький скрипт на Perl:
use strict;use warnings;use File::Copy; # Ищем все .mp3-файлы в текущей директорииmy@files=glob"*.mp3";# Составляем список идентификаторов трековmy@ids=map{/^(\d+)\s/&&$1}<DATA>; # Переименовываем файлы последовательно,# так как порядок треков в саундтреке и директории соответствует порядку в таблицеfor(my$i=0;$i<scalar@files;$i++){ move $files[$i],$ids[$i].'.mp3';} __DATA__1000 TIM 1001 Unplugged 1007 Steel Drums 1002 New Age 1003 Hay Seed 1004 Progressive Rock 1005 Salsa 1006 Techno Rave 10131959 Prom 1011 Bongo Bango 1021 Ragtime 1014 Hip Hop 1012 Keep Tryin' 1017 Detective Theme 1015 Dreams 1016 Tuna Loaf 1018 60's Rock 1019 Pictures 1020 Huey Dewey |
Запускаем скрипт в директории с треками и получаем то, что хотели. Вернемся к программе.
Так как нам необходимо проигрывать mp3, то надо озаботиться выбором какой-нибудь библиотеки, которая позволяет это делать (проигрывание средствами Windows, например, с помощью mciSendString - не слишком надежное и плохо переваривает некоторые файлы). Я выбрал BASS Audio Library. Переходим к коду:
#include <stdio.h>#include <Windows.h>#include <Tlhelp32.h>#include "bass.h" #pragma comment(lib, "bass.lib") /* Имя директории, где будут лежать файлы саундтрека */#define MUSIC_DIR L"music"/* Адрес, содержащий ID проигрываемого трека */#define MEMORY_OFFSET 0x481390 typedefstruct{ DWORD id;char name[32];} track_entry; /* Список треков и их ID */ track_entry track_list[]={{1000,"TIM"},{1001,"Unplugged"},{1007,"Steel Drums"},{1002,"New Age"},{1003,"Hay Seed"},{1004,"Progressive Rock"},{1005,"Salsa"},{1006,"Techno Rave"},{1013,"1959 Prom"},{1011,"Bongo Bango"},{1021,"Ragtime"},{1014,"Hip Hop"},{1012,"Keep Tryin'"},{1017,"Detective Theme"},{1015,"Dreams"},{1016,"Tuna Loaf"},{1018,"60's Rock"},{1019,"Pictures"},{1020,"Huey Dewey"}}; /* Вспомогательная функция для получения PID процесса */ DWORD get_pid_by_name(LPCTSTR name){ PROCESSENTRY32 pe32; DWORD pid =0; HANDLE ss; ss = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPMODULE32,0);if(ss == INVALID_HANDLE_VALUE)return0; pe32.dwSize=sizeof(PROCESSENTRY32); if(!Process32First(ss,&pe32)){ CloseHandle(ss);return0;} do{if(!lstrcmp(name, pe32.szExeFile)){ pid = pe32.th32ProcessID;break;}}while(Process32Next(ss,&pe32)); CloseHandle(ss); return pid;} /* Функция поиска имени трека по ID в списке треков */constchar* get_track_name_by_id(DWORD id){int i; for(i =0; i < _countof(track_list); i++){if(id == track_list[i].id)return track_list[i].name;} return"unknown";} /* Функция проигрывания трека *//* Да, да, очевидные комментарии */void play_track(LPCTSTR current_dir, DWORD id){static HSTREAM sm =0; TCHAR file_path[MAX_PATH]; /* Формируем полный путь к треку */ wsprintf(file_path, L"%s\\%s\\%d.mp3", current_dir, MUSIC_DIR, id); if(sm !=0){/* Завершаем проигрывание текущего трека */ BASS_StreamFree(sm); sm =0;}/* Запускаем трек на проигрывание, указав, что его необходимо зациклить */ sm = BASS_StreamCreateFile(FALSE, file_path,0,0, BASS_UNICODE); BASS_ChannelFlags(sm, BASS_SAMPLE_LOOP, BASS_SAMPLE_LOOP); BASS_ChannelPlay(sm, FALSE);} int main(){ HANDLE pr; DWORD pid, track_id =0, last_track =0; TCHAR current_dir[MAX_PATH]={0}; /* Инициализируем библиотеку */if(!BASS_Init(-1,44100,0, NULL, NULL)){printf("Failed to init BASS library\n");return-1;} /* Получаем путь, откуда запущен наш файл, *//* чтобы в дальнейшем сформировать полный путь к mp3-файлу */ GetCurrentDirectory(_countof(current_dir), current_dir); /* Получаем PID целевого процесса */ pid = get_pid_by_name(TEXT("TIMWIN.EXE"));if(pid ==0){printf("Can't find TIMWIN.EXE process\n"); BASS_Free(); return-1;} /* Открываем процесс игры с правами на чтение памяти */ pr = OpenProcess(PROCESS_VM_READ, FALSE, pid);if(pr == NULL){printf("Failed to open process (pid=%d)\n", pid); BASS_Free(); return-1;} while(TRUE){/* Читаем память процесса и получаем ID трека */if(!ReadProcessMemory(pr,(LPCVOID) MEMORY_OFFSET,&track_id,sizeof(DWORD), NULL)){printf("Can't read process memory\n");break;} /* Выводим информацию о текущем треке */printf("Current track - %08X - %-32s\r", track_id, get_track_name_by_id(track_id)); /* Если проигрываемый трек поменялся, то запустим новый трек на проигрывание и сохраним его ID */if(track_id != last_track){ play_track(current_dir, track_id); last_track = track_id;} Sleep(100);} CloseHandle(pr); BASS_Free(); return0;} |
Предельно простой код. Теперь запустим игру (не забудьте отключить музыку в игре, чтобы MIDI-треки не мешались), нашу программу, не забыв положить рядом саундтрек, и оценим результат:
Всё отлично работает. Теперь можно насладиться головоломками игры, слушая качественный звук.
Проект для MSVC и Perl-скрипт: скачать.
P.S. Один из треков игры: