Дата публикации статьи: 12.08.2004 15:21

Алексаднр Егоров (sanches / AMEgo)
12 июня 2004 г.

Захват звука при помощи DirectX8

Введение

    В этом уроке я рассмотрю нечасто встречающуюся проблему: как записать звук с микрофона в WAV файл.

    Что нам понадобится? Во-первых Visual Basic 6.0, во-вторых установленный DirectX8, ну и наконец микрофон для того, чтобы проверить, что все сказанное здесь работает :).

    Для захвата звука в DirectX предусмотрены несколько классов. Стартовым является класс DirectSoundCapture8 – он олицетворяет устройство захвата*. Он не “богат”: у него всего два метода и нет свойств и событий. Основным же является класс DirectSoundCaptureBuffer8. Именно через него идет управление захватом.

* В данном уроке будет рассмотрено использование стандартного устройства захвата.

Порядок работы

Ход работы при захвате звука следующий:

  1. Создаем объект DirectX8
  2. Выбираем устройство захвата и создаем объект DirectSoundCapture8, указав выбранное устройство.
  3. Создаем буфер (DirectSoundCaptureBuffer8), куда будет записываться звук. При создании этого буфера указываются различные параметры, например формат звука и длина буфера.
  4. Создаем WAV файл из записываем в него соответствующие заголовки.
  5. Начинаем захват. Периодически переписываем накопившуюся в буфере информацию в файл.
  6. Заканчиваем захват и закрываем файл.

А теперь подробнее

    Первый шаг самый простой. Для того чтобы создать объект DirectX8 выполняем следующую строку:

Set objDX8 = New DirectX8

    Создавать объект DirectSoundCapture8 нужно вызовом специального метода объекта DirectX8 - DirectSoundCaptureCreate. У этого метода единственный параметр - GUID выбранного устройства захвата. Для того, чтобы использовать стандартное утсройство, мы просто передадим пустую строку - vbNullString. Итак:

Set objDSCapture = objDX8.DirectSoundCaptureCreate(vbNullString)

    Теперь нам необходимо создать “хранилище” поступающего звука – буфер. Этот объект тоже должен создаваться вызовом специального метода, на сей раз объекта DirectSoundCapture8, - CreateCaptureBuffer. У этого метода тоже один параметр, опредяющий свойства буфера. Этот параметр имеет тип DSCBUFFERDESC. Рассмотрим теперь поля это типа:

  • FxFormat. Имеет тип WAVEFORMATEX и содержит информацию о формате захватываемого звука.
  • guid3Dalgorithm. Не используется.
  • LBufferBytes. Размер буфера (в байтах).
  • lFlags. Этот флаг определяет возможности устройства. В нашем уроке используется значение 0.
Рассмотрим теперь поля WAVEFORMATEX:
  • nFormatTag. Определяет формат аудио. Если используется одновременно с DirectSound, то должно быть WAVE_FORMAT_PCM.
  • nChannels. 1 - моно, 2 - стерео звук.
  • nBitsPerSample. Если используется WAVE_FORMAT_PCM, то должно быть 8 или 16.
  • nBlockAlign. Размер блока (в байтах). Блок - минимальная единица данных для выбранного формата аудио (nFormatTag). Если используется WAVE_FORMAT_PCM, то это поле должно равняться (nChannles * nBitsPerSmaple)/8. Запись или чтение из буфера производится блоками. Недопустимо считывать или записывать, допустим, с середины блока.
  • lSamplesPerSec. Частота звука (в герцах). Как правлио это 8000, 11025, 22050, 44100.
  • lAvgBytesPerSec. Число байтов на секунду. Если используется WAVE_FORMAT_PCM, то это поле должно равняться (lSamplesPerSec * nBlockAlign).
  • lExtra. Не используется.
  • nSize. Игнорируется при использовании WAVE_FORMAT_PCM.

    Итак, с параметрами буфера зазобрались. Для примера создадим буфер 22.05кГц, стерео, 16-бит (здесь objDSCaptureBuffer – объект DirectSoundCaptureBuffer, а dscbd – DSCBUFFERDESC):

 

With dscbd.fxFormat
    .nFormatTag = WAVE_FORMAT_PCM
    .nChannels = 2 ‘стерео
    .lSamplesPerSec = 22050 ’22.05кГц
    .nBitsPerSample = 16 ’16-бит
    .nBlockAlign = (.nBitsPerSample * nChannels) / 8
    .lAvgBytesPerSec = .lSamplesPerSec * .nBlockAlign
    .nSize=0
End With
dscbd.lflags = 0
dscbd.lBufferBytes = dscbd.fxFormat.lAvgBytesPerSec * 1 ‘вместо единицы можно поставить требуемое количество секунд 
Set objDSCaptureBuffer = objDSCapture.CreateCaptureBuffer(dscbd)

    Теперь создадим файл и запишем заголовки. Поскольку в формате WAV файла я не силен (?), то есть вообще про него ничего не знаю, то просто приведу кусок кода из MSDN:

Private Type FileHeader
    lRiff As Long
    lFileSize As Long
    lWave As Long
    lFormat As Long
    lFormatLength As Long
End Type 
Private Type WaveFormat
    wFormatTag As Integer
    nChannels As Integer
    nSamplesPerSec As Long
    nAvgBytesPerSec As Long
    nBlockAlign As Integer
    wBitsPerSample As Integer 
End Type
Private Type ChunkHeader
    lType As Long
    lLen As Long
End Type 
Dim fh As FileHeader 
Dim wf As WaveFormat 
Dim ch As ChunkHeader 
Private Sub OpenFile(WaveFileName As String)
    Open WaveFileName For Binary Access Write As #1
    With fh 
        .lRiff = &H46464952 ' <RIFF> chunk tag
        .lFileSize = 0 ' Will get later
        .lWave = &H45564157 ' <WAVE> chunk tag
        .lFormat = &H20746D66 ' <fmt > chunk tag
        .lFormatLength = Len(wf) 
    End With 
    Put #1, , fh 
    With wf 
        .wFormatTag = dscbDesc.fxFormat.nFormatTag 
        .nChannels = dscbDesc.fxFormat.nChannels
        .nSamplesPerSec = dscbDesc.fxFormat.lSamplesPerSec
        .wBitsPerSample = dscbDesc.fxFormat.nBitsPerSample
        .nBlockAlign = dscbDesc.fxFormat.nBlockAlign
        .nAvgBytesPerSec = dscbDesc.fxFormat.lAvgBytesPerSec
    End With 
    Put #1, , wf 
    ch.lType = &H61746164 ' <data> chunk tag
    Put #1, , ch 
End Sub

У же почти все готово. Для начала захвата вызывается метод .Start объекта DirectSoundCaptureBuffer8, а чтобы остановить - .Stop (в качестве параметра методу Start лучше передавать DSCBSTART_LOOPING, в этом случае захват будет зацикленным). А как же происходит запись в файл, спросите вы?

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

    Для того,чтобы отловить события объекта DirectX (любого) нам нужно проделать две вещи: во-первых, создать объект, который поддерживал бы интерфейс DirectXEvent8. Делается это просто: пишем в начале модуля класса (или формы) Implements DirectXEvent8. У этого класса есть единственный метод – DXCallBack, который вызывается при появдении события. Второй шаг – нам нужно зарегистрировать этот объект-обработчик событий в главном объекте DirectX8 (в нашем случае objDX8), используя метод CreateEvent. В качестве параметра и передается существующий экземпляр объекта. Этот метод возвращает число, которое является идентификатором регистрации (его необходимо сохранить в какой-нибудь переменной).

    Теперь нам нужно как-то сказать буферу, чтобы тот вызывал события при достижении определенных позиций при записи. Делается это при помощи метода SetNotificationPositions: у этого метода два параметра: во втором – массив типа DSBPOSITIONNOTIFY, а в первом указывается, сколько элементов в этом массиве. Рассмотрим теперь тип DSBPOSITIONNOTIFY. У него два поля: lOffset и hEventNotify. В первом содержится число: при достижении этой позиции и произойдет событие. Однако может быть необходимо, чтобы событие произошло, по окончании записи. Для этого в поле lOffset должно быть равным DSBPN_OFFSETSTOP (или –1). Во второе поле записывается тот самый идентификатор регистрации события, который был полуен ранее. Итак, рассмотрим небольшой пример. Пусть у нас есть форма (код создания объектов пропускается). Прописываем в самом верху кода строку Implements DirectX8Event. Добавим также Dim lngEventStop As Long, lngEventNotify As Long. В этих двух переменных будут храниться идентификаторы регистрации. Далее, после того, как мы создали экземпляр DirectX8 припишем еще две строчки:

lngEventStop = objDX8.CreateEvent(Me)
lngEventNotify = objDX8.CreateEvent(Me)

    Поначалу может показаться странным, что мы два раза регистрируем один и тот же объект, но на это есть свои причины. Это для нас один и тот же объект, а для DirectX нет. Вернемся к этому чуть позже.

    Пусть имеется кнопка cmdSetEvents, при нажатии на нее добавляются события к буферу. Соответствующий код будет таким:

Dim arrNotifications(0 to 2) As DSBPOSITIONNOTIFY 
With arrNotifications(0)
    .lOffset = 10000
    .hEventNotify = lngEventNotify
End With
With arrNotifications(1)
    .lOffset = 20000
    .hEventNotify = lngEventNotify
End With
With arrNotifications(2)
    .lOffset = DSBPN_OFFSETSTOP
    .hEventModify = lngEventStop
End With
objBuffer.SetNotificationPositions 3, arrNotifications

    Обратите внимание, что массив должен быть zero-based, как говорится, то есть первый его элемент должен иметь индекс 0.

    Теперь выбираем в списке объектов (наверху слева) пункт DirectXEvent8 и для нас автоматически создается «заготовка» для реализации метода DXCallBack. Вот здесь и будет основной код, записывающий звук в файл. Как он работает:

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

Ну а теперь я приведу сам код, взятый из MSDN:

Private Sub DirectXEvent8_DXCallback(ByVal eventid As Long) 
' Глобальные переменные:
' lastPos As Long – последняя позиция корсора
' BytesWritten As Long – сколько всего байт записали
' dscb As DirectSoundCaptureBuffer8 – это и есть буфер захвата
' dscbDesc As DSCBUFFERDESC – это дескриптор, по которому создавался буфер
' lngEventStop As Long – а это идентификатор события

    Dim curPos As Long
    Dim curs As DSCURSORS
    Dim dataBuf() As Byte
    Dim dataSize As Long

    'Курсор у буфера не простой, а составной – имеет тип DSCURSORS.
    'Нам нужно именно поле lWrite.
    dscb.GetCurrentPosition curs
    curPos = curs.lWrite ' Position up to which data is valid

    ' прочитаем данный из буфера в локальный массив,а затем допишем 
    ' запишем его в файл
    ' Но для начала определим, сколь байт накопилось, и не перешел ли
    ' курсор на начало

    dataSize = curPos - lastPos
    If dataSize < 0 Then '         curPos wrapped around.
        dataSize = dscbDesc.lBufferBytes - lastPos + curPos
    End If

    ' изменим размер локального буфера
    ReDim dataBuf(dataSize - 1)
    ' прочитаем буфер
    dscb.ReadBuffer lastPos, dataSize, dataBuf(0), DSCBLOCK_DEFAULT

    Put #1, , dataBuf
    BytesWritten = BytesWritten + dataSize
    lastPos = curPos

    ' если это конец захвата, то завершим запись в файл
    If (eventid = EventStop) Then 
        CloseFile
    End If

End Sub

    Для этого кода необходимо, чтобы за один проход буфера (от начало до конца) было как минимум две «точки события» (в нашем случае это arrNotifications(0) и arrNotifications(1) (событие остановки при этом не учитывается, так как в общем случае за один проход оно не выполняется). В противном случае dataSize, равное curPos-lastPos будет всегда (после первого прохода) равно нулю, а значит и данные записываться не будут. Вы наверное обратили внимание, что в параметре eventid передается тот самый идентификатор регистрации события. То есть когда курсор дошел то точки arrNotifications(0), буфер вызвал метод DXCallBack объекта, который был зарегистрирован под номером arrNotifications(0).hEventNotify. То же произошло и со второй точкой, и в конце записи, когда процесс был прерван.

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

  1. Создаем объект DirectX8
  2. Если весь процесс происходит “из формы”, то этой формой “наследуем” DirectXEvent8.
  3. Создаем объект DirectSoundCapture8 (если надо, то получаем поддерживаемые им форматы).
  4. Выбираем параметры захвата и на основе объекта DirectSoundCapture8 создаем DirectSoundCaptureBuffer8.
  5. Регистрируем нашу форму как обработчик событий (дважды) и запинаем идентификаторы.
  6. Добавляем в буфер «точки события». Если используется приведенный код, то таких точек дожно быть как минимум две, не считая точку останова.
  7. Начинаем захват (зацикленный). Здесь же готовим файл. Дальше все пойдет автоматически. Для остановки вызывается метод Stop.

Полностью работающий пример здесь.

(С) 2004 Александр Егоров (sanches/AMEgo), sanches2003@inbox.ru, http://sanchesp.h12.ru
В статье спользовались материалы из MSDN, права на которые принадлежат корпорации Microsoft.