Музыку в студию!.. или введение в DirectAudio
Известно, что человек воспринимает окружающую действительность на 90% через зрение, а "всего" 10% от общей информационной картины приходится на слух, осязание и обоняние. Но представьте хоть на минуту, как бы выглядела любая игра, да и вообще работа на компьютере, без всевозможного звукового сопровождения. Эффект был бы таким, как если бы графика отображалась только в оттенках серого. Сколько бы мы потеряли! К счастью, для пользователей были созданы звуковые карты, а нам, DirectX-программистам был дан полный доступ к возможностям этих карт, а также множество вспомогательных функций.
DirectAudio
Традиционно в DirectX, за низкоуровневые операциям со звуком отвечает DirectSound, а все остальное сосредоточено в DirectMusic, причем в DirectX 8.0 эти два компонента объединены вместе в DirectAudio. Другими словами DirectSound позволяет работать непосредственно со звуковыми данными, что бывает полезно, когда нужно сделать со звуком что-то нестандартное, например, очистить звук от шумов (применив свой алгоритм шумоочистки). С другой стороны DirectMusic используется, когда нужно просто воспроизвести звуковой файл. В нашей игре нам вряд ли придется делать что-то более сложное, чем изменение громкости звучания, а поэтому наш выбор - DirectMusic. Тем более, в DirectMusic есть несколько стандартных эффектов (эхо, дисторшн, изменение темпа) , которые можно применить к любому звуку.
tSounds
Приступим к написанию класса звуков. Назовем его tSounds. Хотя такое название наводит на мысли о DirectSound, на самом деле практически вся реализация построена именно на DirectMusic. Теперь взгляните на рисунок:
Из рисунка видно, что tSounds является частью движка, и как все остальные классы cделан производным от tLogFile. Т.к. звуки, в рамках нашего проекта, - это ресурсы, такие же ресурсы как, например, текстуры, поэтому класс tSounds - менеджер звуков во многом напоминает класс tTextures - менеджер текстур.
Посмотрим, какой интерфейс предоставляет нам менеджер звуков:
class TENGINE_DLL tSounds : virtual public
tLogFile
{
public:
tSounds();
virtual ~tSounds();
void InitDAudio(void);
void ReleaseDAudio(void);
int LoadSound(char *fileName);
int GetSoundsCount(void) const { return m_soundsCounter; }
void StopSound(int id) const;
void PlaySound(int id, bool isLooped=false) const;
protected:
IDirectMusicLoader8 *m_loader;
IDirectMusicPerformance8 *m_performance;
vector<IDirectMusicSegment8*> m_sounds;
int m_soundsCounter;
private:
vector<string> m_deviceNames;
vector<string> m_driverNames;
static INT_PTR CALLBACK DSEnumCallback(GUID* pGUID,
LPSTR strDesc,
LPSTR strDrvName,
VOID* pContext);
};
Получив первое представление о возможностях менеджера звуков, рассмотрим более подробно детали реализации.
В конструкторе и деструкторе класса tSounds нет ничего интересного, поэтому начнем мы с функции InitDAudio(), которая является общей для всех звуков (объектов класса), и должна быть вызвана в программе всего один раз. Код функции содержит подробные комментарии, но есть два момента, на которые хочется обратить особое внимание.
Во-первых, посмотрим на тот участок кода, где мы получаем информацию о доступных аудио устройствах, т.е. о звуковых картах:
tLogFile::LogMessage( " Available audio devices:\n" );
DirectSoundEnumerate( (LPDSENUMCALLBACK)DSEnumCallback, this );
for( int i=0; i<m_deviceNames.size(); i++ )
{
tLogFile::LogMessage( " Device name #%d: %s\n", i,
m_deviceNames[i].c_str() );
tLogFile::LogMessage( " Driver name #%d: %s\n", i,
m_driverNames[i].c_str() );
}
Здесь вызывается функция DirectSoundEnumerate(), с помощью которой мы получаем от DirectSound список установленных звуковых карт, а также названия драйверов, соответствующие этим картам. Как это принято в Windows, система возвращает нам информацию через callback - специальную функцию, которую вызывает сама Windows, а точнее DirectSound. В tSounds роль callback-а играет функция DSEnumCallback(), кстати именно поэтому она сделена статической. DirectSound вызывает наш callback передав ему, в качестве параметров, имя звуковой карты и ее драйвера , которые функция InitDAudio() выводит в log-файл.
Следом за этим, мы создаем так называемый проигрыватель (m_perfomance), путем вызова IDirectMusicPerformance8::InitAudio():
hResult = m_performance->InitAudio( NULL, NULL, NULL,
DMUS_APATH_DYNAMIC_STEREO,
64, DMUS_AUDIOF_ALL, NULL );
if( hResult == DSERR_NODRIVER )
{
m_performance = NULL;
tLogFile::LogMessage("Create DirectAudio - False. NO SOUND CARD\n");
return;
}
Помимо проигрывателя, InitAudio() создает некий "стандартный AudioPath", т.е. набор настроек параметров звука. Про AudioPath я обязательно расскажу более подробно как-нибудь в другой раз, а пока достаточно запомнить, что стандартный AudioPath позволяет всего лишь проигрывать звук, в то время как AudioPath создаваемый вручную позволяет изменять, например громкость звука, его темп, добавлять эхо, менять настройки 3DSound и т.п.
Далее мы создаем загрузчик(m_loader), т.е. устанавливаем директорию из которой DirectMedia будет читать звуковые файлы.
Вот и все, что касается InitDAudio(). В обратной ей функции, т.е. в ReleaseDAudio() просто удаляется все, что было создано в InitDAudio(), освобождаются все ресурсы. Чуть выше по тексту, я уже сказал о том, что tSounds является частью движка. Это действительно так, потому, что класс менеджера звуков на самом деле один из базовых классов для tEngine. А значит самое логичное место, из которого нужно выполнять инициализацию и де инициализацию tSounds, это функции tEngine::InitEngine() и tEngine::ReleaseEngine() соответственно. На этом и остановимся...
Переходим к следующей функции класса tSounds, а именно к LoadSound(). Получив в качестве параметра имя звукового файла, эта функция совершает два простых действия:
1. Добавляет очередной элемент в массив звуков (m_sounds).
2. Вызывает функцию LoadObjectFromFile(), которая читает заданный звуковой файл и создает на
его основе звуковой фрагмент(IDirectMusicSegment8) - фактически весь звук целиком. При этом указатель
на звуковой фрагмент записывается в только что созданный элемент массива m_sounds.
Теперь переходим к самому главному, ради чего была проделана вся предыдущая работа. Я имею в виду, конечно же, воспроизведение звука.
void tSounds::PlaySound(int id, bool isLooped/*=false*/) const
{
HRESULT hResult;
if( id < 0 || id > m_soundsCounter-1 )
return;
if( !m_performance )
return;
hResult = m_sounds[id]->Download( m_performance );
if( hResult != S_OK )
throw tError( __FILE__, __LINE__, "tSounds::PlaySound",
"Fail to download sound segment (%s)",
DXGetErrorString8( hResult ) );
if( isLooped )
{
hResult = m_sounds[id]->SetRepeats( DMUS_SEG_REPEAT_INFINITE );
if( hResult != S_OK )
throw tError( __FILE__, __LINE__, "tSounds::PlaySound",
"Fail to set repeat mode (%s)",
DXGetErrorString8( hResult ) );
}
hResult = m_performance->PlaySegmentEx( m_sounds[id], NULL, NULL,
DMUS_SEGF_SECONDARY, 0,
NULL, NULL, NULL );
if( hResult != S_OK )
throw tError( __FILE__, __LINE__, "tSounds::PlaySound",
"Fail to play sound (%s)", DXGetErrorString8( hResult ) );
}
Функция PlaySound() имеет два агрумента. Первый - это идентификатор того звука который требуется воспроизвести, т.е. это то число, которое возвращает LoadSound(). Второй аргумент позволяет задать режим проигрывания: либо одиночный, либо циклический (когда звук доигрывает до конца он не останавливается, а начинает играть сначала).
Взглянув на код, мы увидим, что в начале PlaySound() проверяет идентификатор звука. Затем используя этот идентификатор получает указатель на объект интерфейса IDirectMusicSegment8 и с помощью метода Download() загружает звук в проигрыватель. Далее проверяется второй аргумент, переданный PlaySound() и если он равен true, включается режим циклического прослушивания. После этого вызывается функция PlaySegmentEx(), которая запускает проиграватель на воспроизведение.
А остановить звук можно с помощью StopSound(). Код функции не должен вызвать трудностей, поэтому я заканчиваю с описанием менеджера звуков, и переходу к примеру его использования.
Пример
Вообще, первым делом нужно произвести инициализацию tSound вызвав функцию InitDAudio() из этого класса, причем сделать это надо всего один раз, а не для каждого звука в отдельности. В нашем проекте InitDAudio() вызывается из tEngine::InitEngine() (почему именно так см. выше). Затем нужно загрузить звуки и запустить их воспроизведение. Код выполняющий все это помещен в Main():
snd1 = engine->LoadSound("Base\\Sounds\\drums.wav");
engine->PlaySound(snd1,true);
На сегодня это все. Но продолжение следует: