Sources
Delphi Russian Knowledge Base
DRKB - база знаний по Дельфи в рунете, составленная Виталием Невзоровым

DirectX для начинающих — Direct Sound

08.04.2002
Виктор Кода

Одним из достоинств компоненты DirectSound является то, что она имеет прямой (ну, почти прямой) доступ к аппаратному обеспечению звуковой карты. Среди интересных возможностей следует выделить размещение данных в памяти звуковой платы, использование аппаратного микширования звука, возможность создавать объёмный звук, используя специальные алгоритмы (это нужно для 3D-игр) - всё это мультимедиа-средства Windows обеспечить не в состоянии.

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

Этот пример использует только общие возможности компонента DirectSound (DirectXAudio), в частности, не используется механизм подкачки звуковых данных, не используются трёхмерные эффекты для придания "объёмности" звуку и т. д. Зато к несомненным достоинствам следует отнести универсальность алгоритма считывания звуковых данных из wav-файлов - читается любой формат.

Пускай не обижаются Денис Гончаров и Тимур Салихов, по книге которых я и изучал DirectSound, но поставлять учебные примеры в стадии "обмоченные детские пелёнки" просто стыдно. Дело в том, что учебный пример по воспроизведению звуковых файлов, который поставлялся вместе в книгой, открывал только те wav-файлы, которые поставлялись на том же CD-ROM. С другими, в частности, из каталога Windows\Media, пример работать наотрез отказывался - программа просто "вылетала". Поэтому пришлось прибегнуть к SDK и воспользоваться его примерами - благо, они работают корректно со всеми wav-файлами в формате PCM (я не встретил ни одного, который бы вызвал ошибку). Всё-таки парни из Microsoft знают, что делают.

К сожалению, за универсальность приходится платить, поэтому данный пример по объёму мало смахивает на учебный. К тому же, он недописан до конца :-( - надеюсь, кто-нибудь поможет мне одолеть возникшие проблемы.

Вот файлы проекта:

Итак, кликаем два раза по форме - попадаем в процедуру FormCreate().

Певой вызывается функция InitDirectSound() - обычный подход. В ней создаётся главный объект DirectSound и устанавливается уровень кооперации. Для установки эксклюзивного уровня кооперации в DirectX 7 использовался флаг DSSCL_EXCLUSIVE. В документации SDK 8 сказано, что этот флаг уже устарел и не предоставит приложению эксклюзивного режима. Для этого теперь используется флаг DSSCL_PRIORITY. Вообще, эксклюзивный режим подразумевает приглушение всех других звуков, если наше приложение активно и само воспроизводит какой-либо звук. Я пробовал устанавливать оба флага, и в обоих случаях помимо собственного вывода был слышен и вывод других программ, запускаемых параллельно, даже если окно приложения было активно. Что это, мои ошибки или ошибки в DirectSound - я не знаю. Если у вас всё работает корректно, напишите мне.

Теперь программа ничего не делает, а ждёт, когда пользователь нажмёт кнопку "Открыть...". По щелчку на ней открывается диалоговое окно выбора звукового файла. После успешного выбора вызывается функция CreateStaticBuffer(), единственный параметр которой - имя открываемого wav-файла. Эта функция создаёт специальный звуковой буфер.

Звуковой буфер - это область памяти, создаваемая в RAM или памяти звуковой карты, в которой будут размещены сэмплы - звуковы данные, считанные из wav-файла. Используя этот буфер, DirectSound сможет воспроизвести звук.

Для создания звукового буфера служит метод IDirectSound8.CreateSoundBuffer().

Первый параметр - адрес структуры типа TDSBUFFERDESC. Её поля необходимо предварительно заполнить.

Второй - указатель на интерфейс IDirectSoundBuffer, третий - вездесущий параметр агрегирования - передаём nil.

Теперь разберёмся со структурой TDSBUFFERSDESC.

Она указывает DirectSound, какой тип буфера мы хотим создать. Как обычно, системную память, где расположены поля структуры, необходимо заполнить нулевыми значениями функцией ZeroMemory() (или FillChar()).

Поле dwSize должно содержать размер самой структуры в байтах.

Поле dwFlags определяет тип создаваемого звукового буфера. Вот некоторые флаги:

Это только половина возможных флагов. Подробнее о них можно узнать из справки SDK.

Теперь необходимо назначить требуемый размер для буфера в байтах, чтобы в нём поместились все данные из wav-файла - заполнить поле dwBufferBytes.

Вся работа по открытию wav-файлов, загрузке из них данных и завершению работы с ними возлагается на класс TWaveSoundRead - можно пользоваться им как "чёрным ящиком" - ничего увлекательного в реализации методов класса нет - сплошные операции с указателями и вызов функций из модуля lowfunc.pas.

Вообще DirectSound создавался лишь для прямого доступа к звуковой карте, и в его задачу не входит чтение звуковых данных из файлов, будь то даже несжатые wav-файлы - мультимедиа-стандарт Windows. Вся работа по загрузке данных в буфер ложится на программиста. К сожалению, это сильно усложняет даже простейшие проекты, т. к. функции для работы с wav-файлами довольно громоздки, а о работе с другими форматами и говорить не приходится. О классе я ещё упомяну, а сейчас лишь скажу, что при вызове метода TWaveSoundRead.Open() при успешном открытии звукового файла поле m_ckIn.cksize класса содержит размер прочитанных данных в байтах - это значение и указываем в поле dwBufferBytes структуры TDSBUFFERDESC. Также необходимо указать формат данных в звуковом файле - ведь запись может быть стерео- или монофонической, с разной частотой дискретизации и т. д. Узнать это DirectSound сможет из поля lpwfxFormat типа TWAVEFORMATEX структуры TDSBUFFERDESC. Его необходимо приравнять к полю m_pwfx класса TWaveSoundRead - оно как раз и содержит эти данные.

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

На случай, если DirectSound не сможет создать буфер заданого размера - например, большая часть памяти звуковой карты или RAM занята - необходимо запомнить реальный размер созданного буфера - он (размер) будет помещён в поле dwBufferBytes той же струтуры, что передавалась в метод IDirectSound8.CreateSoundBuffer().

Итак, буфер создан, теперь необходимо его заполнить звуковыми данными. В этом снова поможет класс TWaveSoundRead. Заполнение буфера данными происходит в функции FillBuffer().

Сперва необходимо прочитать данные из звукового файла. Процедурой GetMem() выделяем облать памяти нужного размера и получаем на неё указатель - переменная pbWavData. Затем записываем данные в память при помощи метода TWaveSoundRead.Read().

Теперь необходимо скопировать данные в буфер DirectSound.

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

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

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

Далее необходимо передать адреса двух пар "указатель-размер", в которые метод помещает адреса блокированных областей и их размер.

Последний параметр - 0.

Данные в буфер копируются процедурой CopyMemory() - указываем, куда копировать, откуда копировать и сколько копировать :-).

Теперь разблокируем буфер методом IDirectSoundBuffer.Unlock() и удалим область, откуда копировали процедурой FreeMem().

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

Первый параметр зарезервирован - нужно указать 0.

Второй указывает на приоритет звукового буфера - что это за зверь, я и сам толком не знаю. Значение может меняться от 0 до $FFFFFFFF. Если при создании звукового буфера не указывался флаг DSBCAPS_LOCDEFER, то необходимо передать 0.

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

В окне есть два регулятора TScrollBar. Левый предназначается для изменения громкости звука, второй - для изменения баланса в колонках.

Изменение громкости производится методом IDirectSoundBuffer.SetVolume(), единственным параметром которого является значение в интервале от 0 до -10000 - определено константами DSBVOLUME_MAX и DSBVOLUME_MIN. Да-да, изменение громкости звука производится только в сторону приглушения, даже в восьмой версии этот недостаток не устранён :(

Изменение баланса в колонках производится методом IDirectSoundBuffer.SetPan(), единственным параметром которого является значение в интервале от 10000 до -10000 - определено константами DSBPAN_LEFT и DSBPAN_RIGHT. Расставьте колонки (если они есть) пошире, запустите пример и попробуйте подвигать скроллбар - слушайте сами. Конечно, это не 3D-эффекты, но зато очень просто.

Теперь самое время заглянуть в файл wavread.pas - если у кого есть желание копаться с указателями, можете поработать с ним. Я не буду описывать его методы, т. к. всё это довольно сложно и требует несколько страниц текста. Этот класс предельно прост, но не он работает с wav-файлами - идёт вызов функций из lowfunc.pas. Эти два файла являются МОИМ переложением с DirectX SDK 7 для C++ - в SDK 8 всё существенно переделано, и как мне кажется, усложнено.

Теперь о не слишком приятном. Полагаю, не очень хорошо показывать людям, как работает DirectSound, не разобравшись со всем до конца самому. Проблема в том, что одна из функций из lowfunc.pas не работает, а другая недописана до конца. Я не смог перевести некоторые сложные операции над указателями в контекст языка Object Pascal, поэтому пришлось создать динамическую библиотеку на C++ для работоспособности примера. Если кто-то сможет помочь мне перевести всё на Pascal - буду премного благодарен. Я оставил исходный код из SDK в отдельном каталоге.

Previous page:
DirectX для начинающих — Sprite
Top:
DRKB
Next page:
DXInput