netlib.narod.ru | < Назад | Оглавление | Далее > |
Хотя на первый взгляд 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.
Таблица 4.1. COM-интерфейсы DirectSound
Интерфейс | Описание |
IDirectSound8 | Интерфейс главного объекта DirectSound. |
IDirectSoundBuffer8 | Объект первичного и вторичного звуковых буферов. Хранит данные и управляет воспроизведением. |
IDirectSoundNotify8 | Объект уведомления. Сообщает приложению о достижении заданной позиции в звуковом буфере. |
На рис. 4.4 показаны взаимоотношения между этими объектами. IDirectSound8 — это главный интерфейс из которого вы создаете звуковые буфера (IDirectSoundBuffer8). Затем звуковой буфер может создать собственный интерфейс уведомления (IDirectSoundNotify8), который вы используете для отметки позиции в звуковом буфере по достижении которой будет отправлено уведомление. Интерфейс уведомлений полезен при потоковом воспроизведении звука.
Рис. 4.4. Вы получаете звуковой буфер через объект IDirectSound8. Объект IDirectSoundNotify8 создается через его родителя — объект IDirectSoundBuffer8
Перед тем, как делать что-либо еще, вам необходимо включить заголовочный файл dsound.h и указать в списке компоновки библиотеку dsound.lib. После этого можно приступать к первому этапу использования DirectSound — созданию объекта IDirectSound8, который является главным интерфейсом, представляющим звуковое оборудование. Делается это с помощью функции DirectSoundCreate8:
HRESULT WINAPI DirectSoundCreate8( LPCGUID lpcGuidDevice, // Укажите NULL (звуковое устройство по умолчанию) LPDIRECTSOUND8 *ppDS8, // Создаваемый объект LPUNKNOWN pUnkOuter); // NULL - не используется
Используя функцию 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.
Сейчас вам придется иметь дело только с переменной 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);
Первый и единственный аргумент — это указатель на структуру 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. Кольцевые буферы всегда замкнуты, соединяя начало с концом буфера, чтобы звуки могли постоянно ходить по кругу для непрерывного воспроизведения
Воспроизводимые звуки смешиваются в первичном звуковом буфере, который является кольцевым буфером данных. Когда будет достигнут конец звукового буфера, звук зацикливается на начало буфера и его воспроизведение продолжается без паузы. Чтобы использовать возможность зацикливания буфера вы должны явно включить зацикленное воспроизведение; иначе воспроизведение буфера будет остановлено по достижении его конца.
Чтобы начать воспроизведение звукового буфера (с необязательной возможностью зацикленного воспроизведения), вы должны вызвать функцию звукового буфера 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. Перед началом воспроизведения вторичные звуковые буферы микшируются вместе в первичном звуковом буфере
Вторичные звуковые буферы используют объект 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 байт, в котором блокируются для доступа 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 для блокировки всего буфера, игнорируя указанные смещение и размер.
Давайте, продолжим, заблокируем весь буфер данный и заполним его случайными числами:
// 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. Обычно динамики воспроизводят звук с одинаковой громкостью (измеряемой в децибелах, или, для краткости, дБ). Позиционирование уменьшает громкость в одном из динамиков и увеличивает в другом, что приводит к возникновению псевдотрехмерных эффектов
А вот волшебная функция:
HRESULT IDirectSoundBuffer8::SetPan(LONG lPan);
Просто укажите в аргументе lPan требуемый уровень позиционирования. Попробуйте в качестве примера установить значение позиционирования буфера равным –5000, что уменьшит громкость правого динамика на 50 дБ:
// g_pDSBuffer = ранее инициализированный звуковой буфер if(FAILED(g_pDSBuffer->SetPan(-5000))) { // Произошла ошибка }
Изменение частоты воспроизведения звукового буфера меняет высоту звука. Представьте, что вы можете превратить голос героя игры в верещание белки слегка изменив частоту! Вы можете использовать ту же запись мужского голоса для имитации женского немного повысив его частоту. Вы не верите? Проверьте сами.
Частота устанавливается следующей функцией:
HRESULT IDirectSoundBuffer8::SetFrequency(DWORD dwFrequency);
Вам необходимо только задать в аргументе dwFrequency желаемое значение, например:
// g_pDSBuffer = ранее инициализированный звуковой буфер if(FAILED(g_pDSBuffer->SetFrequency(22050))) { // Произошла ошибка }
Проницательные читатели заметят, что изменение частоты воспроизведения вызывает эффект сжатия звуковой волны из-за чего она воспроизводится за меньшее время, как показано на рис. 4.9.
Рис. 4.9. Из-за удвоения частоты звук воспроизводится вдвое быстрее и с увеличенной высотой
Время от времени другие приложения могут похитить у вас ресурсы, оставляя вас с устройством, состояние которого было изменено. Это обычная ситуация для звуковых буферов, так что вам надо восстанавливать эти потерянные ресурсы, вызывая функцию IDirectSoundBuffer8::Restore (у которой нет параметров). Например, если у вас есть потерянный буфер, вы можете восстановить его (и всю связанную с буфером память) используя следующий код:
// g_pDSBuffer = ранее инициализированный звуковой буфер, // который был потерян if(FAILED(g_pDSBuffer->Restore())) { // Произошла ошибка }
Самое худший эффект потери ресурсов буфера заключается в потере звуковых данных и необходимости их повторной загрузки.
Как вы уже читали, уведомления — это маркеры в звуковом буфере при достижении которых генерируется сигнал о произошедшем событии. Работая с уведомлениями вы получаете возможность узнать, что воспроизведение звука завершено или приостановлено. Вы будете использовать эти уведомления для потокового воспроизведения больших звуков.
Уведомления используют объект с именем IDirectSoundNotify8. Его единственная цель — отмечать позиции в звуковом буфере и генерировать события для приложения, которые могут обрабатываться в цикле обработки сообщений или в отдельном потоке.
Позиции определяются по их смещению в буфере (как показано на рис. 4.10) или с помощью макроса, обозначающего остановку или завершение воспроизведения. Этот макрос определен в DirectSound как DSBPN_OFFSETSTOP.
Рис. 4.10. Уведомления могут быть размещены (путем указания смещения) в любом месте внутри звукового буфера
Чтобы получить объект IDirectSoundNotify8 вы запрашиваете его через объект IDirectSoundBuffer8:
// g_pDSBuffer = ранее инициализированный вторичный звуковой буфер IDirectSoundNotify8 *g_pDSNotify; if(FAILED(g_pDSBuffer->QueryInterface(IID_IDirectSoundNotify8, (void**)&g_pDSNotify))) { // Произошла ошибка }
У интерфейса уведомления есть только одна функция:
HRESULT IDirectSoundNotify8::SetNotificationPositions( DWORD dwPositionNotifies, // Количество уведомлений LPCDSBPOSITIONNOTIFY pcPositionNotifies); // Массив смещений
Параметр pcPositionNotifies является указателем на массив структур DSBPOSITIONNOTIFY. Взгляните, что представляет собой эта структура и какую информацию содержит:
typedef struct { DWORD dwOffset; // Смещение или макрос DSBPN_OFFSET HANDLE hEventNotify; // Дескриптор события } DSBPOSITIONOTIFY, *LPCDSBPOSITIONNOTIFY;
Ловушка здесь — это использование дескриптора события. У события есть два состояния — произошло (установлено) или не произошло (сброшено). Создавая событие вы объявляете переменную дескриптора и инициализируете ее следующим образом:
HANDLE hEvent; hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
Вы можете создавать столько событий, сколько хотите; важно чтобы в дальнейшем вы могли различать их. Обычно вы создаете одно событие на звуковой канал, но это не строгое правило. Все зависит от вас, и вам решать как вы будете различать, что значит каждое из событий.
Я хочу сделать паузу и показать вам, как установить событие и смещение уведомления. Я буду использовать звуковой буфер размером 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, являющийся массивом дескрипторов событий, которые сканирует функция. Возвратившись из этой функции вы получите номер позиции произошедшего события в массиве.
В действительности, чтобы получить из возвращаемого значения номер события требуется дополнительная работа. Надо просто вычесть значение 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;
Для обработки заголовка надо открыть волновой файл и сразу же считать из него данные. Структура будет содержать всю информацию, необходимую для определения формата звука, а также размер звуковых данных для последующего чтения.
Сейчас вы можете создать звуковой буфер, основываясь на прочитанных данных, и дальше работать с ним. Я предпочитаю написать пару функций, которые загружают волновой файл в создаваемый ими вторичный звуковой буфер. Первая функция читает и анализирует заголовок волнового файла, создавая по ходу звуковой буфер; вторая читает звуковые данные и помещает их в звуковой буфер.
Первая функция, 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. Во вторичном звуковом буфере есть четыре поточных маркера. Когда воспроизведение достигает одного из этих маркеров, звуковой буфер сигнализирует, что надо загружать новые звуковые данные в только что воспроизведенную секцию
Для потокового воспроизведения звука вы сначала загружаете звуковыми данными весь буфер (столько данных, сколько поместится в буфер). Запускаете воспроизведение звука и ждете, пока не будет достигнут первый маркер. В этот момент следующий небольшой фрагмент звуковых данных вставляется в только что воспроизведенную секцию. Воспроизведение продолжается, пока не будет достигнут второй маркер, и в этой точке новые данные загружаются в только что воспроизведенный фрагмент.
Процесс продолжается пока весь звук не будет загружен и воспроизведен до того места, где сработает маркер, оповещающий об остановке звука. Если звук зациклен, воспроизведение продолжается путем повторения последовательности действий, о которых вы прочитали.
В предыдущем разделе, «Загрузка звука в буфер», я написал функцию с именем LoadSoundData, которая загружает звуковые данные в указанный вами буфер. В потоке, обрабатывающем события уведомлений, вы используете функцию загрузки для поддержания потока данных вместо завершенных, обеспечивая заполнение буфера звуковой информацией.
Вот как это все делается:
Создаем звуковой буфер, скажем, размером 65536 байт.
Устанавливаем четыре позиции уведомлений (в конце каждой четверти звукового буфера).
Загружаем в буфер столько данных, сколько поместится.
Запускаем воспроизведение звукового буфера, указав флаг DSBPLAY_LOOP.
При получении уведомления о событии загружаем в только что воспроизведенную секцию новые данные. Продолжаем, пока не достигнем уведомления, после которого не останется данных для загрузки и тогда воспроизводим оставшуюся часть звука.
Чтобы определить, какое событие отмечает конец звука, определите модуль размера звука по размеру буфера (остаток от деления размера звука на размер буфера). Разделите значение модуля на четыре (количество уведомлений) чтобы определить, какое из событий используется для оповещения конечной позиции звука.
Демонстрационная программа Stream анализирует заголовок волнового файла, создает звуковой буфер, устанавливает четыре уведомления и события, а затем воспроизводит звук, буферизуя данные по мере возникновения событий. Этот процесс продолжается, пока не будет достигнут конец звука, и воспроизведение завершается.
Код демонстрационной программы Stream вы найдете в главе 6, «Создаем ядро игры». Там вы обнаружите набор рабочих функций, которые можно использовать для потокового воспроизведения звука в собственном проекте.
netlib.narod.ru | < Назад | Оглавление | Далее > |