netlib.narod.ru< Назад | Оглавление | Далее >

Использование DirectSound

Хотя на первый взгляд DirectSound может показаться сложным, в реальности он не столь труден для понимания и использования. Вы создаете COM-объект, являющийся интерфейсом для звукового оборудования. Этот COM-объект дает вам возможность создавать индивидуальные звуковые буферы (называемые вторичными звуковыми буферами, secondary sound buffer) для хранения звуковых данных.

Данные из этих звуковых буферов смешиваются вместе в главном буфере микширования (называемом первичный звуковой буфер, primary sound buffer) и воспроизводятся в указанном вами звуковом формате. Эти форматы воспроизведения могут отличаться по частоте, количеству каналов и разрядности выборки. Допустимые частоты — 8 000 Гц, 11 025 Гц, 22 050 Гц и 44 100 Гц (CD-качество).

Для количества каналов есть два варианта: один канал для монофонического звука или два канала для стереофонического. Параметры разрядности также ограничиваются двумя вариантами: 8 бит для низкокачественного воспроизведения звука и 16 бит для высококачественного воспроизведения. По умолчанию, если вы не выполняли настройку вручную, для первичного звукового буфера DirectSound установлены следующие параметры: 22 050 Гц, 8-разрядная выборка, стереофонический звук.

Вы можете модифицировать звуковые каналы для воспроизведения с другой частотой (меняя высоту звука), менять в ходе воспроизведения громкость и панорамирование звука, и даже зацикливать звук. Но это не все — звуки могут воспроизводиться в виртуальном трехмерном окружении, имитирующем перемещающиеся вокруг вас реальные звуки.

Ваша задача — взять звуки и поместить их в звуковые буферы. Для очень больших звуков вы можете использовать метод потокового воспроизведения при котором загружается небольшой фрагмент звуковых данных, а когда его воспроизведение закончится, вы подгружаете в звуковой буфер следующий фрагмент данных. Этот процесс продолжается, пока весь звуковой файл не будет воспроизведен.

Вы реализуете потоковое воспроизведение, устанавливая позицию в звуковом буфере при достижении которой приложению будет отправлен сигнал о том, что пришло время обновить звуковые данные. Этот процесс передачи сигналов об обновлении называют уведомлением (notification). Нет никаких ограничений на то, сколько буферов могут воспроизводиться одновременно, но вам следует следить, чтобы их было мало, поскольку каждый буфер создает дополнительную нагрузку на процессор и память.

В действительности работать с DirectSound не так уж трудно. Фактически, в этой книге мы будем работать только с тремя интерфейсами, перечисленными в таблице 4.1.

ПРИМЕЧАНИЕ
Чтобы использовать в вашем проекте DirectSound и DirectMusic надо включить заголовочные файлы dsound.h и dmusici.h и добавить в список компоновки файл библиотеки dsound.lib. Также хорошо бы указать при компоновке и библиотеку dxguid.lib, поскольку в ней определены некоторые полезные элементы, которые используются DirectSound.

Таблица 4.1. COM-интерфейсы DirectSound



Интерфейс Описание

IDirectSound8 Интерфейс главного объекта DirectSound.
IDirectSoundBuffer8 Объект первичного и вторичного звуковых буферов. Хранит данные и управляет воспроизведением.
IDirectSoundNotify8 Объект уведомления. Сообщает приложению о достижении заданной позиции в звуковом буфере.


На рис. 4.4 показаны взаимоотношения между этими объектами. IDirectSound8 — это главный интерфейс из которого вы создаете звуковые буфера (IDirectSoundBuffer8). Затем звуковой буфер может создать собственный интерфейс уведомления (IDirectSoundNotify8), который вы используете для отметки позиции в звуковом буфере по достижении которой будет отправлено уведомление. Интерфейс уведомлений полезен при потоковом воспроизведении звука.


Рис. 4.4. Вы получаете звуковой буфер через объект IDirectSound8

Рис. 4.4. Вы получаете звуковой буфер через объект IDirectSound8. Объект IDirectSoundNotify8 создается через его родителя — объект IDirectSoundBuffer8


Инициализация DirectSound

Перед тем, как делать что-либо еще, вам необходимо включить заголовочный файл dsound.h и указать в списке компоновки библиотеку dsound.lib. После этого можно приступать к первому этапу использования DirectSound — созданию объекта IDirectSound8, который является главным интерфейсом, представляющим звуковое оборудование. Делается это с помощью функции DirectSoundCreate8:

HRESULT WINAPI DirectSoundCreate8(
     LPCGUID lpcGuidDevice, // Укажите NULL (звуковое устройство по умолчанию)
     LPDIRECTSOUND8 *ppDS8, // Создаваемый объект
     LPUNKNOWN pUnkOuter);  // NULL - не используется

 

ПРИМЕЧАНИЕ
Функция DirectSoundCreate8, подобно всем другим функциям DirectSound, возвращает DS_OK, если завершилась успешно, или код ошибки в противном случае. Чтобы контроль ошибок был проще можно для проверки возвращаемого значения использовать макросы FAILED и SUCCEEDED.

Используя функцию DirectSoundCreate8 и глобальный экземпляр объекта IDirectSound8, можно инициализировать объект звуковой системы следующим образом:

IDirectSound8 *g_pDS; // Глобальный объект IDirectSound8

if(FAILED(DirectSoundCreate8(NULL, &g_pDS, NULL))) {
    // Произошла ошибка
}

Установка уровня кооперации

Следующий этап инициализации — установка уровня кооперации (cooperative level) объекта IDirectSound8. Вы используете уровень кооперации, чтобы определить как будет осуществляться совместное с другими приложениями использование ресурсов звуковой карты. Хотите ли вы, чтобы карта была полностью в вашем распоряжении, не позволяя никому другому выполнять воспроизведение; или наоборот — хотите разрешить совместный доступ? Или вам нужен специальный формат воспроизведения, который не совпадает с установленным по умолчанию?

Установка уровня кооперации — это работа функции IDirectSound8::SetCooperativeLevel. Есть четыре возможных уровня кооперации, и они перечислены в таблице 4.2. Каждому соответствует собственный макрос, определенный в DirectSound.


Таблица 4.2. Уровни кооперации DirectSound



Уровень Макрос Описание

Нормальный DSSCL_NORMAL Обычный уровень; позволяет всем программам обращаться к звуковой карте, используя формат воспроизведения по умолчанию: 8 бит, 11025 Гц, 1 канал (моно). Этот формат не может быть изменен.
Приоритетный DSSCL_PRIORITY То же, что и нормальный, но позволяет вам менять формат воспроизведения.
Монопольный DSSCL_EXCLUSIVE Монопольное использование звуковой карты; никакие другие приложения не могут использовать звуковое устройство, пока ваша программа активна. Вы задаете формат воспроизведения.
Первичная запись DSSCL_WRITEPRIMARY Профессиональный уровень, предоставляющий вам полный контроль над системой. Вы получаете доступ только к первичному звуковому буферу (вторичные звуковые буферы отключены). Режим предназначен для программистов, которые хотят в своем коде реализовать собственный алгоритм микширования, поэтому в дальнейшем я не буду его рассматривать.


Вот прототип функции IDirectSound8::SetCooperativeLevel:

IDirectSound8::SetCooperativeLevel(
    HWND  hwnd,     // Дескриптор родительского окна
    DWORD dwLevel); // Уровень кооперации из таблицы 4.2

Какой уровень кооперации использовать? Вообще-то это зависит от типа создаваемого приложения. Для полноэкранных приложений используйте монопольный уровень. В других случаях я рекомендую приоритетный уровень. Будьте бдительны, когда используете уровень отличный от нормального — помните, что в таком случае вам надо задать формат воспроизведения. Я покажу вам, как это делается в следующем разделе, «Установка формата воспроизведения».

СОВЕТ
Я настоятельно рекомендую использовать приоритетный уровень кооперации предоставляющий вам контроль над первичным буфером, даже если вы не хотите менять формат воспроизведения. Благодаря этому вы сможете легко регулировать громкость, а также управлять панорамированием во время воспроизведения.

А теперь пример установки приоритетного уровня кооперации с использованием ранее инициализированного объекта IDirectSound8:

// g_pDS = ранее инициализированный объект IDirectSound8
// hWnd = ранее инициализированный дескриптор родительского окна

if(FAILED(g_pDS->SetCooperativeLevel(hWnd, DSSCL_PRIORITY))) {
    // Произошла ошибка
}

Установка формата воспроизведения

Последний этап инициализации DirectSound, выполняемый только если вы используете уровень кооперации, отличный от нормального, — захват управления первичным звуковым буфером и установка формата воспроизведения для системы. Процесс делится на две части: сначала вы используете объект IDirectSound8 для создания интерфейса буфера, а затем используете интерфейс для модификации формата.

Создание объекта первичного звукового буфера

Объект IDirectSoundBuffer представляет первичный звуковой буфер. Здесь не требуется интерфейс восьмой версии, поскольку система микширования в разных версиях DX не менялась. Звуковой буфер (как первичный, так и вторичный) создает функция IDirectSound8::CreateSoundBuffer, которая выглядит так:

HRESULT IDirectSound8::CreateSoundBuffer(
    LPCDSBUFFERDESC pcDSBufferDesc,   // Описание буфера
    LPDIRECTSOUNDBUFFER *ppDSBuffer,  // Создаваемый объект буфера
    LPUNKNOWN pUnkOuter);             // NULL - не используется

В параметре pcDSBufferDesc передается указатель на структуру DSBUFFERDESC, хранящую различную информацию о создаваемом буфере. Для первичного буфера вы не будете использовать все возможности, предоставляемые объектом звукового буфера, но тем не менее, взглянем на всю структуру целиком:

typedef struct {
    DWORD dwSize;               // Размер структуры
    DWORD dwFlags;              // Флаги, описывающие возможности буфера
    DWORD dwBufferBytes;        // Размер звукового буфера
    DWORD dwReserved;           // Не используется, установите 0
    LPWAVEFORMATEX lpwfxFormat; // Формат воспроизвелдения
    GUID guid3DAlgorithm;       // GUID_NULL (Алгоритм 3D-воспроизведения)
} DSBUFFERDESC, *LPDSBUFFERDESC;

Назначение полей ясно из их названий, за исключением lpwfxFormat. Это указатель на структуру, описывающую формат воспроизведения создаваемого буфера. Поскольку пока мы не имеем дела с ней, оставим подробное описание на потом. Что касается dwBufferBytes, первичный звуковой буфер уже существует, так что присвойте dwBufferBytes значение 0.

ПРИМЕЧАНИЕ
DirectSound автоматически создает буфер данных, используемый как первичный звуковой буфер, поэтому нет никаких указаний расположен ли этот буфер в системной памяти или в памяти звуковой карты. Кроме того, установка формата воспроизведения для первичного звукового буфера выполняется немного по другому, поэтому вам надо присвоить указателю lpwfxFormat значение NULL.

Сейчас вам придется иметь дело только с переменной dwFlags, которая содержит набор флагов, определяющих возможности создаваемого буфера. В таблице 4.3 перечислены все доступные для использования флаги.


Таблица 4.3. Флаги для создания звукового буфера



Флаг Описание

DSBCAPS_CTRL3D У буфера есть трехмерные возможности.
DSBCAPS_CTRLFREQUENCY Позволяет менять частоту в ходе воспроизведения буфера.
DSBCAPS_CTRLFX Буфер позволяет обработку эффектов.
DSBCAPS_CTRLPAN У буфера есть возможность позиционирования.
DSBCAPS_CTRLPOSITIONNOTIFY Буфер может использовать уведомления.
DSBCAPS_CTRLVOLUME Позволяет на лету регулировать громкость для буфера.
DSBCAPS_GETCURRENTPOSITION2 Позволяет запрашивать буфер о местонахождении позиции воспроизведения.
DSBCAPS_GLOBALFOCUS Создает глобальный звуковой буфер; это значит, что воспроизводимый звук будет слышно даже если активна другая программа.
DSBCAPS_LOCDEFER Позволяет буферу использовать аппаратные и программные ресурсы.
DSBCAPS_LOCHARDWARE Заставляет использовать аппаратные ресурсы, такие как микшер и память звуковой карты.
DSBCAPS_LOCSOFTWARE Заставляет использовать программные ресурсы, такие как программное микширование и системная память.
DSBCAPS_MUTE3DATMAXDISTANCE Выключает трехмерный звук, если его источник находится слишком далеко от слушателя.
DSBCAPS_PRIMARYBUFFER Делает создаваемый буфер первичным звуковым буфером. Используйте этот флаг только один раз и только тогда, когда устанавливаете уровень кооперации отличный от нормального.
DSBCAPS_STATIC Если возможно, буфер будет размещен в памяти звуковой карты. Используйте флаг только для небольших звуков.
DSBCAPS_STICKYFOCUS Заставляет буфер продолжать воспроизведение, когда пользователь переключается на другое приложение, не использующее DirectSound. Звук будет отключен, независимо от наличия флага.


Сейчас мы будем использовать только флаги DSBCAPS_PRIMARYBUFFER и DSBCAPS_CTRLVOLUME. Эти флаги сообщают DirectSound, что вы хотите создать интерфейс первичного звукового буфера и обеспечить возможность регулировать громкость воспроизведения. Позднее мы поговорим об оставшихся флагах.

ВНИМАНИЕ!
Некоторые флаги, такие как флаг контроля частоты, нельзя использовать для первичного буфера. Указание таких флагов приведет к возникновению ошибки при создании первичного буфера.

Давайте пойдем дальше и создадим интерфейс первичного звукового буфера:

IDirectSoundBuffer g_pDSPrimary; // Глобальный доступ
DSBUFFERDESC       dsbd;         // Описание буфера

ZeroMemory(&dsbd, sizeof(DSBUFFERDESC)); // Обнуляем структуру

dsbd.dwSize        = sizeof(DSBUFFERDESC); // Устанавливаем размер структуры
dsbd.dwFlags       = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRLVOLUME;
dsbd.dwBufferBytes = 0;                    // Не задаем размер буфера
dsbd.lpwfxFormat   = NULL;                 // Не задаем формат
if(FAILED(g_pDS->CreateSoundBuffer(&dsbd,
          &g_pDSPrimary, NULL))) {
    // Произошла ошибка
}

Установка формата

Теперь, когда вы получили возможность управлять первичным звуковым буфером, пришло время установить формат воспроизведения. У вас есть несколько вариантов, но я рекомендую использовать осмысленные значения, такие как 11 025 Гц, 16-разрядная выборка, моно или 22 050 Гц, 16-разрядная выборка, моно.

Выбирая формат, постарайтесь не использовать стереофонию. Она создает ненужную нагрузку на процессор, поскольку настоящие стереофонические звуки достаточно сложно записать. Также старайтесь всегда использовать 16-разрядную выборку, поскольку ее качество значительно выше, чем у 8-разрядной. Никогда не соглашайтесь на меньшее! Что касается частоты, то чем она выше — тем лучше, но не стоит выбирать частоту выше 22 050 Гц. Даже звуки с CD-качеством прекрасно воспроизводятся на 22 050 Гц без заметных потерь.

Все сказано, пойдем дальше. Вы устанавливаете формат воспроизведения через вызов IDirectSoundBuffer::SetFormat:

HRESULT IDirectSoundBuffer::SetFormat(
    LPCWAVEFORMATEX pcfxFormat);

 

ВНИМАНИЕ!
Функция SetFormat должна вызываться только для объекта первичного звукового буфера.

Первый и единственный аргумент — это указатель на структуру WAVEFORMATEX, содержащую сведения о формате, который вы хотите установить:

typedef struct {
    WORD  wFormatTag;      // Укажите WAVE_FORMAT_PCM
    WORD  nChannels;       // 1 для моно, 2 для стерео
    DWORD nSamplesPerSec;  // Частота дискретизации
    DWORD nAvgBytesPerSec; // Количество байт в секунду для формата
    WORD  nBlockAlign;     // Размер данных выборки
    WORD  wBitsPerSample;  // 8 или 16
    WORD  cbSize;          // Не используется
} WAVEFORMATEX;

Вы должны без труда заполнить эту структуру, за исключением полей nBlockAlign и nAvgBytesPerSec. Переменная nBlockAlign — это количество байт, используемых для каждой выборки звука. Она инициализируется так:

nBlockAlign = (nBitsPerSample / 8) * nChannels;

Переменная nAvgBytesPerSec — это количество байт, необходимых для хранения одной секунды звука. Здесь надо принять во внимание частоту дискретизации и размер данных выборки, что приводит к следующей формуле:

nAvgBytesPerSec = nSamplesPerSec * nBlockAlign;

Теперь, вооружившись этими сведениями, пришло время попытаться применить их! Вот пример установки формата 22 050 Гц, 16 бит, моно:

// g_pDSPrimary = ранее инициализированный глобальный объект
//                первичного звукового буфера
WAVEFORMATEX wfex;
ZeroMemory(&wfex, sizeof(WAVEFORMATEX));

wfex.wFormatTag      = WAVE_FORMAT_PCM;
wfex.nChannels       = 1;           // моно
wfex.nSamplesPerSec  = 22050;       // 22050 Гц
wfex.wBitsPerSample  = 16;          // 16 бит
wfex.nBlockAlign     = (wfex.wBitsPerSample / 8) * wfex.nChannels;
wfex.nAvgBytesPerSec = wfex.nSamplesPerSec * wfex.nBlockAlign;

if(FAILED(g_pDSPrimary->SetFormat(&wfex))) {
    // Произошла ошибка
}

Запуск первичного звукового буфера

Наконец вы получили контроль над звуковой системой, и готовы играть рок! Осталось только включить воспроизведение для первичного звукового буфера. Несмотря на то, что у вас еще нет звуков, включить воспроизведение буфера лучше сейчас, а потом не отключать его, пока вы не закончите использование звуковой системы. Запуск воспроизведения первичного буфера в начале приложения позволит сэкономить время процессора, которое иначе тратилось бы на запуски и остановки.

Перед тем, как показать запуск воспроизведения буфера, я должен рассказать вам о первичном звуковом буфере. Поскольку ресурсы памяти ограничены, особенно у звуковой карты, используемый буфер данных может быть любого размера (даже всего несколько сотен байт). По этой причине в качестве первичного и вторичных звуковых буферов используются кольцевые буферы (circular buffer).

Пример кольцевого буфера показан на рис. 4.5. Несмотря на то, что буфер данных — это одномерный массив, его конец замыкается на начало. Это мощная техника, позволяющая экономить значительные объемы памяти.


Рис. 4.5. Кольцевые буферы всегда замкнуты

Рис. 4.5. Кольцевые буферы всегда замкнуты, соединяя начало с концом буфера, чтобы звуки могли постоянно ходить по кругу для непрерывного воспроизведения


Воспроизводимые звуки смешиваются в первичном звуковом буфере, который является кольцевым буфером данных. Когда будет достигнут конец звукового буфера, звук зацикливается на начало буфера и его воспроизведение продолжается без паузы. Чтобы использовать возможность зацикливания буфера вы должны явно включить зацикленное воспроизведение; иначе воспроизведение буфера будет остановлено по достижении его конца.

Чтобы начать воспроизведение звукового буфера (с необязательной возможностью зацикленного воспроизведения), вы должны вызвать функцию звукового буфера Play, которая выглядит так:

HRESULT IDirectSoundBuffer8::Play(
    DWORD Reserved1, // Должен быть 0
    DWORD Priority,  // Приоритет микширования - используйте 0
    DWORD dwFlags);  // Флаги воспроизведения

Единственный представляющий интерес аргумент — dwFlags, который может принимать два значения: 0 для однократного воспроизведения звукового буфера и остановки при достижении конца, и DSBPLAY_LOOPING сообщающий о необходимости постоянно при достижении конца возвращаться к началу буфера для непрерывного воспроизведения.

Для первичного звукового буфера вам практически всегда будет требоваться зацикленное воспроизведение, и вот как это можно сделать:

if(FAILED(g_pDSPrimary->Play(0, 0, DSBPLAY_LOOPING))) {
    // Произошла ошибка
}

Когда вы закончите работать с первичным звуковым буфером (и со звуковой системой в целом), надо остановить ее, вызвав функцию IDirectSoundBuffer::Stop, у которой нет аргументов:

if(FAILED(g_pDSPrimary->Stop())) {
    // Произошла ошибка
}

 

ВНИМАНИЕ!
Не надо останавливать первичный звуковой буфер, пока не будут остановлены все вторичные звуковые буферы. Когда воспроизводится вторичный звуковой буфер, автоматически запускается воспроизведение первичного буфера.

Использование вторичных звуковых буферов

Далее на очереди создание вторичных звуковых буферов, содержащих реальные звуковые данные, которые вы хотите воспроизвести. Нет никаких ограничений на количество создаваемых вторичных звуковых буферов, и возможности DirectSound позволяют, если хотите, воспроизводить их все одновременно.

Вы достигаете этого, заполняя первичный звуковой буфер звуковыми данными, находящимися во вторичных звуковых буферах (этот процесс показан на рис. 4.6). Эти данные микшируются между собой, так что если записать звук в то место первичного звукового буфера, куда уже был записан другой звук, то будут воспроизводиться оба звука одновременно.


Рис. 4.6. Перед началом воспроизведения вторичные звуковые буферы микшируются вместе в первичном звуковом буфере

Рис. 4.6. Перед началом воспроизведения вторичные звуковые буферы микшируются вместе в первичном звуковом буфере


Вторичные звуковые буферы используют объект IDirectSoundBuffer8, очень похожий на объект IDirectSoundBuffer. Фактически, для создания восьмой версии интерфейса вы должны сперва создать объект IDirectSoundBuffer и запросить через него новый интерфейс.

В создании вторичных звуковых буферов есть одно отличие — вы должны при инициализации установить формат воспроизведения. Это значит, что буфер может использовать только один формат. Если вам надо сменить формат, освободите буфер и создайте другой.

Снова вы используете структуру WAVEFORMATEX для хранения формата и структуру DSBUFFERDESC для описания возможностей буфера. Однако на этот раз вы указываете указатель на структуру WAVEFORMATEX внутри структуры DSBUFFERDESC.

Вот пример создания вторичного звукового буфера, использующего формат 22 050 Гц, 16 бит, моно. Я создаю буфер достаточный для хранения двухсекундного звука (поскольку сейчас я не буду помещать в него никаких реальных звуков), с возможностью регулировки громкости, позиционирования и частоты.

// g_pDS = ранее инициализированный объект IDirectSound8
IDirectSoundBuffer8 *g_pDSBuffer; // Глобальный объект 8 версии
IDirectSoundBuffer  *pDSB;        // Локальный звуковой буфер

// Инициализируем структуру WAVEFORMATEX

WAVEFORMATEX wfex;
ZeroMemory(&wfex, sizeof(WAVEFORMATEX));

wfex.wFormatTag      = WAVE_FORMAT_PCM;
wfex.nChannels       = 1;        // моно
wfex.nSamplesPerSec  = 22050;    // 22050 Гц
wfex.wBitsPerSample  = 16;       // 16 бит
wfex.nBlockAlign     = (wfex.wBitsPerSample / 8) * wfex.nChannels;
wfex.nAvgBytesPerSec = wfex.nSamplesPerSec * wfex.nBlockAlign;

// Инициализируем структуру DSBUFFERDESC
DSBUFFERDESC dsbd;
ZeroMemory(&dsbd, sizeof(DSBUFFERDESC)); // Обнуляем структуру

dsbd.dwSize        = sizeof(DSBUFFERDESC); // Задаем размер
dsbd.dwFlags       = DSBCAPS_CTRLFREQUENCY | DSBCAPS_CTRLVOLUME |
                     DSBCAPS_CTRLPAN;
dsbd.dwBufferBytes = wfex.nAvgBytesPerSec * 2; // 2 секунды
dsbd.lpwfxFormat   = &wfex;

// Создаем первую версию объекта
if(FAILED(g_pDS->CreateSoundBuffer(&dsbd, &pDSB, NULL))) {
    // Произошла ошибка
} else {
    // Получаем 8 версию интерфейса
    if(FAILED(pDSB->QueryInterface(IID_IDirectSoundBuffer8,
                                  (void**)&g_pDSBuffer))) {
        // Произошла ошибка - прежде чем делать
        // что-либо еще освобождаем первый интерфейс
        pDSB->Release();
    } else {
        // Освобождаем исходный интерфейс - все успешно!
        pDSB->Release();
    }
}

Блокируй и загружай — загрузка звуковых данных в буфер

Великолепно! Звуковой буфер создан, и теперь мы готовы воспроизводить звуки! Осталась одна задача — поместить звуковые данные в буфер. В распоряжении звуковых буферов есть пара функций: IDirectSoundBuffer8::Lock, которая блокирует буфер звуковых данных и возвращает указатель на область данных, и IDirectSoundBuffer8::Unlock, освобождающая ресурсы, используемые во время операции блокировки.

Блокируя буфер вы подготавливаете его для записи. Вы сообщаете буферу смещение (в байтах), начиная с которого вы хотите начать доступ, и количество байтов к которым необходим доступ. В свою очередь вы получаете два указателя на данные и две переменных, сообщающих сколько данных доступно.

Почему вы получаете два указателя и два размера? Потому что звуковой буфер является кольцевым и для блокировки требуемого количества байтов вам может потребоваться вернуться к началу буфера. Первый указатель — это запрошенная вами позиция, а первый размер — это количество байтов от этой позиции до конца буфера. Второй указатель обычно устанавливается на начало буфера, а второй размер — это оставшееся количество блокируемых байтов, которые простираются за конец звукового буфера. На рис. 4.7 показан пример буфера данных с двумя указателями и размерами.


Рис. 4.7. Буфер данных, размером 65536 байт

Рис. 4.7. Буфер данных, размером 65536 байт, в котором блокируются для доступа 62000 байт. Первый указатель используется для доступа к 600000 байт, а второй указатель — для доступа к оставшимся 2000 байт


Вот как выглядит прототип функции блокировки:

HRESULT IDirectSoundBuffer8::Lock(
    DWORD   dwOffset,        // Смещение в буфере до начала
                             // блокируемой области
    DWORD   dwBytes,         // Количество блокируемых байтов
    LPVOID  *ppvAudioPtr1 ,  // Указатель на указатель на первый блок данных
    LPDWORD *ppwAudioBytes1, // Указатель на размер первого блока
    LPVOID  *ppvAudioPtr2,   // Указатель на указатель на второй блок данных
    LPDWORD *ppwAudioBytes2, // Указатель на размер второго блока
    DWORD   dwFlags);        // Флаги блокировки

Здесь следует обратить внимание на несколько вещей. Во-первых вам надо передать пару указателей (приведенных к типу LPVOID*), которые будут указывать на соответствующие данные в заблокированном звуковом буфере. Помните, если размер блокируемого пространства превышает размер оставшегося фрагмента буфера и возникает эффект закольцовывания, то используется два указателя. Также вам надо предоставить два указателя на пару переменных типа DWORD, в которые будет занесено (функцией Lock) количество байт звукового буфера, к которым предоставляется доступ через два различных указателя на данные.

Во-вторых, посмотрите на переменную dwFlags, которая может принимать три значения: 0 для блокировки запрашиваемого фрагмента, DSBLOCK_FROMWRITECURCOR для блокировки от текущей позиции записи и DSBLOCK_ENTIREBUFFER для блокировки всего буфера, игнорируя указанные смещение и размер.

ВНИМАНИЕ!
Когда вы блокируете звуковой буфер, убедитесь, что разблокируете его как можно быстрее. Длительная блокировка буфера приводит к нежелательным эффектам. Также не пытайтесь блокировать фрагмент данных, который воспроизводится в текущий момент.

 

СОВЕТ
Лучше всего присваивать переменной dwFlags значение 0 - это гарантирует, что будет заблокировано только указанное количество байтов, находящихся в заданном месте. Также, если вы не хотите использовать второй указатель или размер, присвойте соответствующим переменным значения NULL и 0.

Давайте, продолжим, заблокируем весь буфер данный и заполним его случайными числами:

// g_pDSBuffer = ранее инициализированный
//               вторичный звуковой буфер 
char *Ptr;
DWORD Size;
if(SUCCEEDED(g_pDSBuffer->Lock(0, 0, (void**)&Ptr,
             (DWORD*)&Size, NULL, 0, DSBLOCK_ENTIREBUFFER))) {
for(long i = 0; i < Size; i++)
    Ptr[i] = rand() % 65536;

К данному моменту вы закончили работу с буфером и готовы разблокировать его, чтобы освободить используемые в процессе ресурсы. Это делается с помощью функции IDirectSoundBuffer8::Unlock:

HRESULT IDirectSoundBuffer8::Unlock(
    LPVOID pvAudioPtr1,    // Первый указатель на данные
    DWORD  dwAudioBytes1,  // Первый размер
    LPVOID pvAudioPtr2,    // Второй указатель на данные
    DWORD  dwAutioBytes2); // Второй размер

Вам необходимо передать значения (а не указатели на них), полученные при блокировке буфера:

if(FAILED(g_pDSBuffer->Unlock((void*)Ptr, Size, NULL, 0))) {
    // Произошла ошибка
}

Воспроизведение звукового буфера

Буфер разблокирован и данные загружены — настало время воспроизводить звук. Для воспроизведения вторичного звукового буфера вызывается та же функция, которая применялась для воспроизведения первичного буфера и была описана ранее в этой главе. Однако на этот раз вам не нужно зацикливать звук, так что исключите флаг DSBPLAY_LOOPING.

Мы подошли к одному примечательному различию — вы должны сообщить звуковому буферу с какой позиции внутри него надо начинать воспроизведение. Обычно первое воспроизведение звука вы начинаете с его начала. Однако остановка звука не сбрасывает позицию воспроизведения, поскольку вы можете включить паузу, а затем вызвать функцию воспроизведения снова, чтобы продолжить прослушивание с того места, на котором в последний раз остановились. Установка позиции воспроизведения легко выполняется с помощью следующей функции:

HRESULT IDirectSoundBuffer8::SetCurrentPosition(
    DWORD dwNewPosition);

У функции всего один аргумент — смещение, с которого вы хотите начать воспроизведение звука. Оно должно быть выровнено на размер блока выборки, который был задан при создании буфера. Если каждый раз при воспроизведении вы хотите слушать звук с начала, можно использовать следующий код:

// g_pDSBuffer = инициализированный звуковой буфер
//               с загруженными данными
if(SUCEEDED(g_pDSBuffer->SetCurrentPosition(0))) {
    if(FAILED(g_pDSBuffer->Play(0,0,0))) {
        // Произошла ошибка
    }
}

Чтобы остановить воспроизведение просто используйте функцию IDirectSoundBuffer8::Stop:

if(FAILED(g_pDSBuffer->Stop())) {
    // Произошла ошибка
}

Изменение громкости, позиционирования и частоты

Если при создании звукового буфера были указаны соответствующие флаги, вы можете даже во время воспроизведения менять громкость, позиционирование и частоту звукового буфера! Это означает добавление некоторых великолепных функций к вашей звуковой системе, но не впадайте в излишества — эти возможности увеличивают нагрузку на систему. Исключайте флаги неиспользуемых возможностей при создании буферов.

Регулировка громкости

Работа с громкостью сперва кажется несколько странной. DirectSound воспроизводит звуки с той громкостью, с которой они были записаны. Он не усиливает звуки, чтобы сделать их громче, поскольку это задача аппаратуры.

DirectSound только делает звук тише. Это выполняется путем задания уровня звука в сотых долях децибел в диапазоне от 0 (полная громкость) до –10 000 (тишина). Проблема в том, что громкость звука может понизиться до полной тишины где-нибудь еще, в зависимости от звуковой системы пользователя.

Для изменения громкости вам достаточно вызвать следующую функцию:

HRESULT IDirectSoundBuffer8::SetVolume(LONG lVolume);

Ее единственный аргумент — это уровень громкости в сотых долях децибел, как я уже говорил. В качестве примера настройки громкости взгляните на приведенный ниже код, который уменьшит громкость на 25 децибел:

// g_pDSBuffer = ранее инициализированный звуковой буфер
if(FAILED(g_pDSBuffer->SetVolume(-2500))) {
    // Произошла ошибка
}

Позиционирование

Далее идет позиционирование (panning) — это возможность передвигать центр воспроизведения между правым и левым динамиками (как показано на рис. 4.8). Думайте о позиционировании как о регулировке баланса в вашей стереосистеме. Позиционирование определяет, насколько звук будет смещен влево или вправо. Самой дальней левой позиции (работает только левый динамик) соответствует значение –10 000, а самой дальней правой (работает только правый динамик) соответствует 10 000. Все промежуточные значения соответствуют определенному балансу между правым и левым каналами.


Рис. 4.8. Позиционирование уменьшает громкость в одном из динамиков

Рис. 4.8. Обычно динамики воспроизводят звук с одинаковой громкостью (измеряемой в децибелах, или, для краткости, дБ). Позиционирование уменьшает громкость в одном из динамиков и увеличивает в другом, что приводит к возникновению псевдотрехмерных эффектов


 

ПРИМЕЧАНИЕ
DirectSound определяет два макроса, представляющих крайний левый и крайний правый уровни позиционирования; это DSBPAN_LEFT и DSBPAN_RIGHT соответственно.

А вот волшебная функция:

HRESULT IDirectSoundBuffer8::SetPan(LONG lPan);

Просто укажите в аргументе lPan требуемый уровень позиционирования. Попробуйте в качестве примера установить значение позиционирования буфера равным –5000, что уменьшит громкость правого динамика на 50 дБ:

// g_pDSBuffer = ранее инициализированный звуковой буфер
if(FAILED(g_pDSBuffer->SetPan(-5000))) {
    // Произошла ошибка
}

 

ПРИМЕЧАНИЕ
Чтобы во время воспроизведения можно было управлять позиционированием звука, при создании звукового буфера необходимо указать флаг DSBCAPS_CTRLPAN.

 

ВНИМАНИЕ!
Убедитесь, что первичный звуковой буфер поддерживает 16-разрядный формат, иначе позиционирование может выполняться неточно.

Изменение частоты

Изменение частоты воспроизведения звукового буфера меняет высоту звука. Представьте, что вы можете превратить голос героя игры в верещание белки слегка изменив частоту! Вы можете использовать ту же запись мужского голоса для имитации женского немного повысив его частоту. Вы не верите? Проверьте сами.

Частота устанавливается следующей функцией:

HRESULT IDirectSoundBuffer8::SetFrequency(DWORD dwFrequency);

Вам необходимо только задать в аргументе dwFrequency желаемое значение, например:

// g_pDSBuffer = ранее инициализированный звуковой буфер
if(FAILED(g_pDSBuffer->SetFrequency(22050))) {
    // Произошла ошибка
}

Проницательные читатели заметят, что изменение частоты воспроизведения вызывает эффект сжатия звуковой волны из-за чего она воспроизводится за меньшее время, как показано на рис. 4.9.


Рис. 4.9. Из-за удвоения частоты звук воспроизводится вдвое быстрее и с увеличенной высотой

Рис. 4.9. Из-за удвоения частоты звук воспроизводится вдвое быстрее и с увеличенной высотой


Потеря фокуса

Время от времени другие приложения могут похитить у вас ресурсы, оставляя вас с устройством, состояние которого было изменено. Это обычная ситуация для звуковых буферов, так что вам надо восстанавливать эти потерянные ресурсы, вызывая функцию IDirectSoundBuffer8::Restore (у которой нет параметров). Например, если у вас есть потерянный буфер, вы можете восстановить его (и всю связанную с буфером память) используя следующий код:

// g_pDSBuffer = ранее инициализированный звуковой буфер,
//               который был потерян
if(FAILED(g_pDSBuffer->Restore())) {
    // Произошла ошибка
}

Самое худший эффект потери ресурсов буфера заключается в потере звуковых данных и необходимости их повторной загрузки.

СОВЕТ
При создании звукового буфера используйте флаг DSBCAPS_LOCKSOFTWARE, говорящий DirectSound о необходимости использовать системную память. Это позволит не беспокоиться о потере ресурсов.

 

ПРИМЕЧАНИЕ
Беспокоитесь о падении производительности при отсутствии аппаратной обработки? Система без проблем может работать с несколькими звуковыми буферами. Достаточно попытаться упростить работу системы, задав одинаковый формат воспроизведения для всех звуковых буферов (первичного и вторичных).

Использование уведомлений

Как вы уже читали, уведомления — это маркеры в звуковом буфере при достижении которых генерируется сигнал о произошедшем событии. Работая с уведомлениями вы получаете возможность узнать, что воспроизведение звука завершено или приостановлено. Вы будете использовать эти уведомления для потокового воспроизведения больших звуков.

Уведомления используют объект с именем IDirectSoundNotify8. Его единственная цель — отмечать позиции в звуковом буфере и генерировать события для приложения, которые могут обрабатываться в цикле обработки сообщений или в отдельном потоке.

Позиции определяются по их смещению в буфере (как показано на рис. 4.10) или с помощью макроса, обозначающего остановку или завершение воспроизведения. Этот макрос определен в DirectSound как DSBPN_OFFSETSTOP.


Рис. 4.10. Уведомления могут быть размещены (путем указания смещения) в любом месте внутри звукового буфера

Рис. 4.10. Уведомления могут быть размещены (путем указания смещения) в любом месте внутри звукового буфера


 

ВНИМАНИЕ!
Вы не можете указать произвольное смещение в буфере; оно должно быть выровнено по размеру блока выборки. Также уведомления должны быть упорядочены от меньших смещений к большим и никогда два уведомления не могут использовать одно и то же смещение. Если вы используете макрос DSBPN_OFFSETSTOP, это уведомление должно устанавливаться последним. Например, если размер блока равен 2 (монофонический звук, 16 бит) и вы попытаетесь установить смещения 4 и 5, то произойдет сбой, потому что смещения 4 и 5 соответствуют одной выборке.

Чтобы получить объект IDirectSoundNotify8 вы запрашиваете его через объект IDirectSoundBuffer8:

// g_pDSBuffer = ранее инициализированный вторичный звуковой буфер
IDirectSoundNotify8 *g_pDSNotify;
if(FAILED(g_pDSBuffer->QueryInterface(IID_IDirectSoundNotify8,
                                        (void**)&g_pDSNotify))) {
    // Произошла ошибка
}

 

ПРИМЕЧАНИЕ
Чтобы использовать уведомления, вы должны при создании звукового буфера указать флаг DSBCAPS_CTRLPOSITIONNOTIFY.

У интерфейса уведомления есть только одна функция:

HRESULT IDirectSoundNotify8::SetNotificationPositions(
    DWORD                dwPositionNotifies,  // Количество уведомлений
    LPCDSBPOSITIONNOTIFY pcPositionNotifies); // Массив смещений

 

ВНИМАНИЕ!
Вы не можете вызывать функцию SetNotificationPositions для буфера, который в данный момент воспроизводится. Если вы хотите изменить позицию уведомления, убедитесь сперва, что воспроизведение буфера установлено. Используйте данную функцию только для вторичных звуковых буферов.

Параметр pcPositionNotifies является указателем на массив структур DSBPOSITIONNOTIFY. Взгляните, что представляет собой эта структура и какую информацию содержит:

typedef struct {
    DWORD  dwOffset;     // Смещение или макрос DSBPN_OFFSET
    HANDLE hEventNotify; // Дескриптор события
} DSBPOSITIONOTIFY, *LPCDSBPOSITIONNOTIFY;

Ловушка здесь — это использование дескриптора события. У события есть два состояния — произошло (установлено) или не произошло (сброшено). Создавая событие вы объявляете переменную дескриптора и инициализируете ее следующим образом:

HANDLE hEvent;
hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

 

ПРИМЕЧАНИЕ
Когда вы закончите использовать событие, следует вызвать CloseHandle(hEvent) для освобождения ресурсов.

Вы можете создавать столько событий, сколько хотите; важно чтобы в дальнейшем вы могли различать их. Обычно вы создаете одно событие на звуковой канал, но это не строгое правило. Все зависит от вас, и вам решать как вы будете различать, что значит каждое из событий.

СОВЕТ
Попытайтесь хранить все события в массиве; позднее вы поймете важность их организации подобным образом.

Я хочу сделать паузу и показать вам, как установить событие и смещение уведомления. Я буду использовать звуковой буфер размером 65 536 байт и создам два события, представляющие середину и конец буфера соответственно:

// g_pDSBNotify = ранее инициализированный объект уведомления
HANDLE            g_hEvents[2]; // Глобальный дескриптор
DSBPOSITIONNOTIFY dspn[2];      // Два локальных смещения
                                // для установки

g_hEvents[0] = CreateEvent(NULL,FALSE,FALSE,NULL);
g_hEvents[1] = CreateEvent(NULL,FALSE,FALSE,NULL);
dspn[0].dwOffset     = 32768;            // Маркер середины
dspn[0].hEventNotify = g_hEvents[0];
dspn[1].dwOffset     = DSBPN_OFFSETSTOP; // Маркер конца звука
dspn[1].hEventNotify = g_hEvents[1];

if(FAILED(g_pDSBNotify->SetNotificationPositions(2, dspn))) {
    // Произошла ошибка
}

Теперь буфер готов, можно двигаться дальше, запустить воспроизведение звукового буфера и позволить событиям сыпаться. Далее следует просто поглядывать на события, ожидая их сигнала. Ожидание событий — это работа функции WaitForMultipleObjects:

DWORD WaitForMultipleObjects(
    DWORD        nCount,          // Количество ожидаемых событий
                                  // (не больше 64)
    CONST HANDLE *lpHandles,      // Массив дескрипторов ожидаемых
                                  // событий
    BOOL         fWaitAll,        // FALSE (не ждать все)
    DWORD        dwMilliseconds); // INFINITE (ждать всегда)

Здесь интерес представляют только два аргумента — nCount, который хранит количество сканируемых событий, и lpHandles, являющийся массивом дескрипторов событий, которые сканирует функция. Возвратившись из этой функции вы получите номер позиции произошедшего события в массиве.

ВНИМАНИЕ!
Функция WaitForMultipleObject может сканировать одновременно только 64 объекта, и вам надо следить, чтобы не превысить этот лимит. Функция может также вернуть значение WAIT_FAILED, указывающее что во время ожидания события произошла ошибка. В таком случае просто перезапустите ожидание, и все будет хорошо.

В действительности, чтобы получить из возвращаемого значения номер события требуется дополнительная работа. Надо просто вычесть значение WAIT_OBJECT_0 из возвращенного функцией значения, и вы получите число из диапазона от 0 до количества событий минус единица.

Теперь вы видите, почему надо помещать события в массив. Благодаря этому, вы можете быстро определить какое событие произошло, а вот функция, которая воспроизводит звуковой буфер, и ждет возникновения ранее установленных событий:

// Функции передается инициализированный звуковой буфер
// с установленными событиями
// g_Events = ранее инициализированный массив событий
//            из двух элементов: HANDLE g_Events[2];
void PlayItAndWait(IDirectSoundBuffer8 *pDSB)
{
    DWORD RetVal, EventNum;

    // Начинаем воспроизведение звука с начала буфера
    pDSBuffer->SetCurrentPosition(0);
    pDSBuffer->Play(0,0,0);

    while(1) {
        while((RetVal = WaitForMultipleObjects(2, g_Events,
                         FALSE, INFINITE) ) != WAIT_FAILED) {
            EventNum = RetVal - WAIT_OBJECT_0;

            // Проверяем событие завершения звука
            // и прерываем цикл
            if(EventNum == 1)
                break;
        }
    }
    // Остановка воспроизведения
    pDSBuffer->Stop();
}

Единственная проблема с приведенным выше примером воспроизведения звука заключается в том, что код должен быть помещен в главном цикле обработки сообщений вашей программы и, таким образом, в нем надо сканировать стандартные сообщения Windows. Это возможно, и именно такой способ предпочитает Microsoft в примерах к DirectX SDK.

Проблема с показанной выше постоянной проверкой состояния звукового буфера состоит в том, что она не вписывается в техники модульного проектирования, которые я предпочитаю использовать. Чтобы система работала лучше, создайте отдельный поток, который будет заниматься показанным выше циклом сканирования сообщений.

Использование потоков для сканирования событий

Мне кажется, я слышал вздох. Не волнуйтесь, использовать потоки для работы с событиями не так трудно. Если вы читали главу 1, «Подготовка к работе с книгой», то уже убедились, что создать поток легко. Трудность в том, как обращаться с таким типом установки.

В главе 1 я писал, что для закрытия потока он должен внутри себя вызвать функцию ExitThread. Но как поток узнает, когда это надо делать, если он занят бесконечным сканированием списка сообщений? Решение — добавить еще одно событие, которое будет сообщать о том, что надо закрыть поток!

Чтобы вручную сгенерировать событие вы используете следующий вызов с дескриптором события:

SetEvent(hEvent);

Чтобы сбросить событие используйте следующий вызов функции:

ResetEvent(hEvent);

Давайте снова взглянем на цикл сканирования событий, но теперь добавим функции для воспроизведения звукового буфера, для остановки воспроизведения и поддержку потоков:

HANDLE              g_Events[2];   // Глобальные события
IDirectSoundNotify8 *g_pDSBNotify; // Глобальный объект уведомления

HANDLE g_hThread;                  // Дескриптор потока

BOOL g_Active  = FALSE;            // Флаг активности потока
BOOL g_Playing = FALSE;            // Флаг воспроизведения звука

// Передайте ранее инициализированный звуковой буфер
// с возможностью уведомлений и данная фукция установит
// уведомления для вас.
void PlaySound(IDirectSoundBufffer8 *pDSBuffer)
{
    DSBPOSITIONNOTIFY dspn[1];
    DWORD ThreadId;

    // Останавливаем звук, если он уже воспроизводится
    if(g_Playing == TRUE)
        StopSound(pDSBuffer);

    // Получаем оъект уведомления
    pDSBuffer->QueryInterface(IID_IDirectSoundNotify8,
                                    (void**)&g_pDSBNotify);
    // Создаем события и поток
    g_hEvents[0] = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_hEvents[1] = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_hThread    = CreateThread(NULL, 0,
        (LPTHREAD_START_ROUTINE)MyThread, NULL, 0, &ThreadId);

    // Устанавливаем позицию уведомления
    dspn[0].dwOffset     = DSBPN_OFFSETSTOP;
    dspn[0].hEventNotify = g_hEvents[0];
    g_pDSBNotify->SetNotificationPositions(1, dspn);

    // Начинаем воспроизведение и устанавливаем флаг
    pDSBuffer->SetCurrentPosition(0);
    pDSBuffer->Play(0, 0, 0);
    g_Playing = TRUE;
}

void StopSound(IDirectSoundBuffer8 *pDSBuffer)
{
    pDSBuffer->Stop();
    g_Playing = FALSE;

    // Очишаем событие звукового буфера и
    // сигнализируем о закрытии потока
    while(g_Active == TRUE) {
        ResetEvent(g_Events[0]);
        SetEvent(g_Events[1]);
    }

    // Освобождаем все ресурсы
    g_pDSBNotify->Release();
    CloseHandle(g_hEvents[0]);
    CloseHandle(g_hEvents[1]);
    CloseHandle(g_hThread);
}

DWORD WINAPI MyThread(void *lpParameter)
{
    DWORD RetVal, EventNum;

    g_Active = TRUE;
    while(1) {
        while((RetVal = WaitForMultipleObjects(2, g_Events,
                             FALSE, INFINITE) != WAIT_FAILED) {
            EventNum = RetVal - WAIT_OBJECT_0;

            // Проверяем, надо ли закрывать поток
            if(EventNum == 1)
                ExitThread(0);

            // Звук остановлен - сбросим флаг
            if(EventNum == 1) {
                g_Playing = FALSE;
            }
        }
    }
}

Вот и все. Вам остается только вызвать функцию PlaySound, передав ей звуковой буфер с имеющимся в нем звуком; потом вы ждете пока звук не закончится или вызываете функцию StopSound для принудительной остановки. Вы можете освободить ресурсы и закрыть поток, вызвав функцию StopSound даже если воспроизведение звука уже завершено.

Загрузка звука в буфер

Теперь, когда у вас есть доступ к звуковому буферу, где взять звуковые данные? Простейший способ - воспользоваться широко распространенным форматом звуковых файлов от Microsoft, называемым волновые файлы (wave files), и использующим расширение файла .WAV.

Волновые файлы начинаются с небольшого заголовка, а за ним следуют необработанные звуковые данные, которые могут быть как сжатыми (с ними работать труднее), так и несжатыми (с ними иметь дело проще). В данном разделе вы узнаете как прочитать и проанализировать заголовок файла, как прочитать несжатые звуковые данные и как поместить их в звуковой буфер.

Взгляните на структуру, которую я создал чтобы хранить заголовок волнового файла для последующего использования:

typedef struct sWaveHeader {
{
    char  RiffSig[4];        // 'RIFF'
    long  WaveformChunkSize; // 8
    char  WaveSig[4];        // 'WAVE'
    char  FormatSig[4];      // 'fmt ' (обратите внимание
                             //     на завершающий пробел)
    long  FormatChunkSize;   // 16
    short FormatTag;         // WAVE_FORMAT_PCM
    short Channels;          // количество каналов
    long  SampleRate;        // частота выборки
    long  BytesPerSec;       // байт на секунду
    short BlockAlign;        // выравнивание блока выборки
    short BitsPerSample;     // бит в выборке
    char  DataSig[4];        // 'data'
    long  DataSize;          // размер волновых данных
} sWaveHeader;

 

ВНИМАНИЕ!
Большинство волновых файлов при сохранении используют заголовок, аналогичный показанной структуре sWaveHeader, но иногда в этот заголовок вставляются дополнительные блоки, что может вызвать путаницу. Например, перед блоком волновых данных может быть вставлен блок комментария. Постарайтесь считывать волновые файлы, которые содержат только один звук, и все должно быть прекрасно.

Для обработки заголовка надо открыть волновой файл и сразу же считать из него данные. Структура будет содержать всю информацию, необходимую для определения формата звука, а также размер звуковых данных для последующего чтения.

ПРИМЕЧАНИЕ
Вы можете определить, соответствует ли звуковой заголовок реальному звуковому файлу, проверив различные поля сигнатур (*Sig) в структуре sWaveHeader. Посмотрите комментарии, в которых указано что каждое из полей должно содержать, и убедитесь при загрузке заголовка, что значения в полях соответствуют указанным. Если в каком-либо из полей значение не совпадает, то, скорее всего, вы не сможете правильно загрузить звук.

Сейчас вы можете создать звуковой буфер, основываясь на прочитанных данных, и дальше работать с ним. Я предпочитаю написать пару функций, которые загружают волновой файл в создаваемый ими вторичный звуковой буфер. Первая функция читает и анализирует заголовок волнового файла, создавая по ходу звуковой буфер; вторая читает звуковые данные и помещает их в звуковой буфер.

Первая функция, CreateBufferFromWAV, получает указатель на открытый волновой файл, а также указатель на структуру sWaveHeader, которая будет заполнена данными заголовка, полученными из волнового файла. По возвращении из функции CreateBufferFromWAV вы получаете указатель на новый созданный объект IDirectSoundBuffer8, который готов принять звуковые данные, получаемые вызовом функции LoadSoundData. Взгляните на код этих двух функций:

// g_pDS = ранее инициализированный объект IDirectSound8
IDirectSounndBuffer8 *CreateBufferFromWAV(FILE *fp,
                                          sWaveHeader *Hdr)
{
    IDirectSoundBuffer  *pDSB;
    IDirectSoundBuffer8 *pDSBuffer;
    DSBUFFERDESC        dsbd;
    WAVEFORMATEX        wfex;

    // Читаем заголовок с начала файла
    fseek(fp, 0, SEEK_SET);
    fread(Hdr, 1, sizeof(sWaveHeader), fp);

    // Проверяем поля сигнатур,
    // возвращаемся в случае ошибки
    if(memcmp(Hdr->RiffSig, "RIFF", 4) ||
       memcmp(Hdr->WaveSig, "WAVE", 4) ||
       memcmp(Hdr->FormatSig, "fmt ", 4) ||
       memcmp(Hdr->DataSig, "data", 4))
        return NULL;

    // Устанавливаем формат воспроизведения
    ZeroMemory(&wfex, sizeof(WAVEFORMATEX));
    wfex.wFormatTag      = WAVE_FORMAT_PCM;
    wfex.nChannels       = Hdr->Channels;
    wfex.nSamplesPerSec  = Hdr->SampleRate;
    wfex.wBitsPerSample  = Hdr->BitsPerSample;
    wfex.nBlockAlign     = wfex.wBitsPerSample / 8 * wfex.nChannels;
    wfex.nAvgBytesPerSec = wfex.nSamplesPerSec * wfex.nBlockAlign;

    // Создаем звуковой буфер, используя данные заголовка
    ZeroMemory(&dsbd, sizeof(DSBUFFERDESC));
    dsbd.dwSize = sizeof(DSBUFFERDESC);
    dsbd.Flags = DSBCAPS_CTRLVOLUME | DSBCAPS_CTRLPAN |
                 DSBCAPS_CTRLFREQUENCY;
    dsbd.dwBufferBytes = Hdr->DataSize;
    dsbd.lpwfxFormat = &wfex;
    if(FAILED(g_pDS->CreateSoundBuffer(&dsbd, &pDSB, NULL)))
        return NULL;

    // Получаем новую версию интерфейса
    if(FAILED(pDSB->QueryInterface(IID_IDirectSoundBuffer8,
                                      (void**)&pDSBuffer))) {
        pDSB->Release();
        return NULL;
    }

    // Возвращаем интерфейс
    return pDSBuffer;
}

BOOL LoadSoundData(IDirectSoundBuffer8 *pDSBuffer,
                   long LockPos, FILE *fp, long Size)
{
    BYTE  *Ptr1, *Ptr2;
    DWORD Size1, Size2;

    if(!Size)
        return FALSE;

    // Блокируем звуковой буфер с заданной позиции
    if(FAILED(pDSBuffer->Lock(LockPos, Size,
                             (void**)&Ptr1, &Size1,
                             (void**)&Ptr2, &Size2, 0)))
        return FALSE;

    // Читаем данные
    fread(Ptr1, 1, Size1, fp);
    if(Ptr2 != NULL)
        fread(Ptr2, 1, Size2, fp);

    // Разблокируем буфер
    pDSBuffer->Unlock(Ptr1, Size1, Ptr2, Size2);
    // Возвращаем флаг успеха
    return TRUE;
}

А вот пример функции, которая использует функции CreateBufferFromWAV и LoadSoundData для загрузки волнового файла. После возврата из показанной ниже функции LoadWAV вы получаете готовый для работы звуковой буфер:

IDirectSoundBuffer8 *LoadWAV(char *Filename)
{
    IDirectSoundBuffer8 *pDSBuffer;
    sWaveHeader Hdr;
    FILE *fp;

    // Открываем исходный файл
    if((fp=fopen(Filename, "rb"))==NULL)
        return NULL;

    // Создаем звуковой буфер
    if((pDSBuffer = CreateBufferFromWAV(fp, &Hdr)) == NULL) {
        fclose(fp);
        return NULL;
    }

    // Читаем данные
    fseek(fp, sizeof(sWaveHeader), SEEK_SET);
    LoadSoundData(pDSBuffer, 0, fp, Hdr.DataSize);

    // Закрываем исходный файл
    fclose(fp);
    // Возвращаем новый звуковой буфер с
    // записанным в него звуком
    return pDSBuffer;
}

Потоковое воспроизведение звука

Открою вам маленький секрет — потоковое воспроизведение звука это простой процесс. Секрет заключается в использовании зацикленного воспроизведения, обеспечивающего непрерывный звук без пауз, и постоянную загрузку новых звуковых данных на замену тем, которые уже были воспроизведены.

Секрет трюка заключается в установке нескольких маркеров в звуковом буфере. Когда воспроизводимый звук проходит один из этих маркеров, вы получаете сигнал, что пришло время загружать следующую порцию звуковых данных, вместо воспроизведенной. Таким образом, вы гарантируете, что когда воспроизведение вернется к началу буфера там будут находиться новые звуковые данные. На рис. 4.11 показан звуковой буфер с четырьмя потоковыми маркерами, сообщающими когда должны быть загружены новые звуковые данные.


Рис. 4.11. Во вторичном звуковом буфере есть четыре поточных маркера.

Рис. 4.11. Во вторичном звуковом буфере есть четыре поточных маркера. Когда воспроизведение достигает одного из этих маркеров, звуковой буфер сигнализирует, что надо загружать новые звуковые данные в только что воспроизведенную секцию


Для потокового воспроизведения звука вы сначала загружаете звуковыми данными весь буфер (столько данных, сколько поместится в буфер). Запускаете воспроизведение звука и ждете, пока не будет достигнут первый маркер. В этот момент следующий небольшой фрагмент звуковых данных вставляется в только что воспроизведенную секцию. Воспроизведение продолжается, пока не будет достигнут второй маркер, и в этой точке новые данные загружаются в только что воспроизведенный фрагмент.

Процесс продолжается пока весь звук не будет загружен и воспроизведен до того места, где сработает маркер, оповещающий об остановке звука. Если звук зациклен, воспроизведение продолжается путем повторения последовательности действий, о которых вы прочитали.

В предыдущем разделе, «Загрузка звука в буфер», я написал функцию с именем LoadSoundData, которая загружает звуковые данные в указанный вами буфер. В потоке, обрабатывающем события уведомлений, вы используете функцию загрузки для поддержания потока данных вместо завершенных, обеспечивая заполнение буфера звуковой информацией.

Вот как это все делается:

  1. Создаем звуковой буфер, скажем, размером 65536 байт.

  2. Устанавливаем четыре позиции уведомлений (в конце каждой четверти звукового буфера).

  3. Загружаем в буфер столько данных, сколько поместится.

  4. Запускаем воспроизведение звукового буфера, указав флаг DSBPLAY_LOOP.

  5. При получении уведомления о событии загружаем в только что воспроизведенную секцию новые данные. Продолжаем, пока не достигнем уведомления, после которого не останется данных для загрузки и тогда воспроизводим оставшуюся часть звука.

  6. Чтобы определить, какое событие отмечает конец звука, определите модуль размера звука по размеру буфера (остаток от деления размера звука на размер буфера). Разделите значение модуля на четыре (количество уведомлений) чтобы определить, какое из событий используется для оповещения конечной позиции звука.

Демонстрационная программа Stream анализирует заголовок волнового файла, создает звуковой буфер, устанавливает четыре уведомления и события, а затем воспроизводит звук, буферизуя данные по мере возникновения событий. Этот процесс продолжается, пока не будет достигнут конец звука, и воспроизведение завершается.

Код демонстрационной программы Stream вы найдете в главе 6, «Создаем ядро игры». Там вы обнаружите набор рабочих функций, которые можно использовать для потокового воспроизведения звука в собственном проекте.

ПРИМЕЧАНИЕ
Весь код примера потокового воспроизведения (называемого Stream) находится на прилагаемом к книге CD-ROM (загляните в папку BookCode\Chap04\Stream). Из-за его длины я не могу привести весь код здесь, но он следует представленной в данном разделе методике. Для потокового воспроизведения звука вызовите функцию PlayStreamedSound с именем звукового файла, который вы хотите воспроизвести, и вункция позаботится за вас обо всем остальном.

netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz