Музыку в студию!.. или введение в 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);

На сегодня это все. Но продолжение следует:

Сайт создан в системе uCoz