SharewareСтатьи

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

Публикую с сокращениями, ибо ограничение по размеру.
Сэмпл - http://zlos.nm.ru/download/ds_for_gd_ru.zip

Обзор DirectSound.
Библиотека DirectSound является частью DirectX. Она, как и весь DirectX, построена на COM. Я не буду касаться этого вопроса подробнее, надеюсь, что вы все это уже знаете.

Инициализация.
Для инициализации DirectSound нам необходимо создать экземпляр IDirectSound8, установить приоритет и параметры первичного буфера. Не самый очевидный и интересный пункт, конечно, но не бывает только интересной работы, иногда необходимо делать и нудную и скучную.
Интерфейс IDirectSound8 может быть создан 2-я путями. Самый простой – DirectSoundCreate8. В SDK он описан так:
HRESULT DirectSoundCreate8(
  LPCGUID lpcGuidDevice,
  LPDIRECTSOUND8 * ppDS8,
  LPUNKNOWN pUnkOuter
);
Первым параметром идет GUID типа устройства. Мы будем использовать устройство по умолчанию, или DSDEVID_DefaultPlayback. Далее передается указатель на указатель интерфейса IDirectSound8 и NULL.
Второй путь лежит через CoCreateInstance, требует некоторых дополнительных телодвижений.
В итоге инициализация типично выглядит следующим образом:
IDirectSound8 *pDS;
HRESULT hr;
if (FAILED(hr = DirectSoundCreate8(&DSDEVID_DefaultPlayback, &pDS, NULL))) {
  return DXTRACE_ERR(_T(“DirectSoundCreate8”));
}
Теперь нам необходимо указать, с каким приоритетом мы хотим воспроизводить звук. Вообще, существует 4 варианта приоритета, я коснусь 2-х.
1. Нормальный – наиболее безопасен, ест наименьший объем ресурсов, но дает низкое качество звука – всего 8 бит.
2. Приоритетный – мы можем самостоятельно указывать параметры основного буфера, задавать частоту и битность.
Я сначала пытаюсь установить приоритетный режим (чтобы обеспечить высокое качество), если не получается – устанавливаю обычный. Игрок в последнем случае будет слышать не самый качественный звук, но ведь будет!
Устанавливается приоритет посредством метода SetCooperativeLevel. Нам надо передать туда окно нашей программы и приоритет:

if (SUCCEEDED(hr = pDS-> SetCooperativeLevel(g_hWnd, DSSCL_PRIORITY))) {
  if (SUCCEEDED(InitPrimaryBuffer(pDS))) {
    m_pDS = pDS;
    return S_OK;
  }
}
if (SUCCEEDE(hr = pDS->SetCooperativeLevel(g_hWnd, DSSCL_NORMAL))) {
  m_pDS = pDS;
  return S_OK;
}
pDS->Release();
return E_FAIL;

Инициализация первичного буфера является простой, но не самой интеллектуальной задачей. Я просто приведу код:
WAVEFORMATEX waveFormat;
ZeroMemory(&waveFormat, sizeof(waveFormat));
const int bitCount = 16;
const int channelCount = 2;
const int sampleRate = 44100;
waveFormat.cbSize = sizeof(WAVEFORMATEX);
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nChannels = channelCount;
waveFormat.nSamplesPerSec = sampleRate;
waveFormat.nBlockAlign = channelCount * bitCount / 8;
waveFormat.nAvgBytesPerSec = sampleRate * waveFormat.nBlockAlign;
waveFormat.wBitsPerSample = sampleRate;
DSBUFFERDESC bufferDesc;
ZeroMemory(&bufferDesc, sizeof(DSBUFFERDESC));
bufferDesc.dwSize = sizeof(DSBUFFERDESC);
bufferDesc.dwFlags = DSBCAPS_PRIMARYBUFFER;
bufferDesc.dwBufferBytes = 0;
bufferDesc.lpwfxFormat = 0;
IDirectSoundBuffer *pdsPrimaryBuffer;
if (SUCCEEDED(hr = pDS->CreateSoundBuffer(&bufferDesc, &pdsPrimaryBuffer, 0))) {
  if (SUCCEEDED(hr = pdsPrimaryBuffer->SetFormat(&waveFormat))) {
    m_pdsPrimaryBuffer = pdsPrimaryBuffer;
    return S_OK;
  }
  pdsPrimaryBuffer->Release();
}
retuen hr;
[/code]
Мы заполняем структуру с описанием звукового формата и описание буфера. Потом мы просим у IDirectSound8 создать нам его, и мы пытаемся установить на нем нужный формат. Если получается – ура, у игрока будет хороший звук!

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

COGGDecoder oggDecoder;
if (oggDecoder.Open(szName)) {
  WAVEFORMATEX waveFormat;
  m_nChannels = oggDecoder.GetChannels();
  m_nFreq     = oggDecoder.GetFreq();
  m_nLength   = oggDecoder.GetLength();
  ZeroMemory(&waveFormat, sizeof(WAVEFORMATEX));
  waveFormat.cbSize = sizeof(WAVEFORMATEX);
  waveFormat.wFormatTag = WAVE_FORMAT_PCM;
  waveFormat.nChannels = m_nChannels;
  waveFormat.nSamplesPerSec = m_nFreq;
  waveFormat.wBitsPerSample = 16; // у OGG всегда так
  waveFormat.nBlockAlign = m_nChannels * waveFormat.wBitsPerSample/8;
  waveFormat.nAvgBytesPerSec = m_nFreq * waveFormat.nBlockAlign;
  const int nSoundLen = m_nLength * waveFormat.nBlockAlign;
  DSBUFFERDESC bufferDesc;
  ZeroMemory(&bufferDesc, sizeof(DSBUFFERDESC));
  bufferDesc.dwSize = sizeof(DSBUFFERDESC);
  bufferDesc.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_STATIC | DSBCAPS_CTRLFREQUENCY;
  bufferDesc.dwBufferBytes = nSoundLen;
  bufferDesc.lpwfxFormat = &waveFormat;
  HRESULT hr;
  IDirectSoundBuffer *pdsBuffer;
  if (SUCCEEDED(hr = pDS->CreateSoundBuffer(&desc, &pdsBuffer, 0))) {
    IDirectSoundBuffer8 *pdsBuffer8;
    pdsBuffer->QueryInterface(IID_IDirectSoundBuffer8, (void**)&pdsBuffer8);
    pdsBuffer->Release();
    m_pdsBuffer = pdsBuffer8;
  }
  else {
    return DXTRACE_ERR(_T(“CreateSoundBuffer”), hr);
  }
}

Если нам удалось создать буфер – мы должны залить в туда звук, который мы хотим воспроизвести. Для этого мы должны залочить часть (а точнее весь) буфер, получить указатель на память, записать в память по этому указателю и разлочить её:

  void *pData;
  DWORD dwBytes;
  if (SUCCEEDED(hr = m_pdsBuffer->Lock(0, m_nSoundLen, &pDdata, &dwBytes, 0, 0, 0)))
  {
    oggDecoder.Decode(pData, dwBytes);
    m_pdsBuffer->Unlock(pData, dwBytes, 0, 0);
    return S_OK;
  }
  m_pdsBuffer->Release();
  m_pdsBuffer = 0;
  return DXTRACE_ERR(_T(“Lock”), hr);
}
return E_FAIL;

Теперь мы можем воспроизвести наш звук методом IDirectSoundBuffer8::Play, остановить проигрывание в любой момент методом IDirectSoundBuffer8::Stop, установить громкость, панораму, частоту. Код там достаточно тривиален, и его можно посмотреть в исходниках.
В случае, если нам надо воспроизводить несколько источников звука, мы должны продублировать исходный буфер.

IDirectSoundBuffer *pdsBuffer;
HRESULT hr;
if (SUCCEEDED(hr = m_pDS->DuplicateSoundBuffer(pBuffer->m_pdsBuffer, &pdsBuffer))) {
  IDirectSoundBuffer8 *pdsBuffer8;
  pdsBuffer->QueryInterface(IID_IDirectSoundBuffer8, (void**)&pdsBuffer8);
  pdsBuffer->Release();
  ppInstance = new CSoundInstance(pdsBuffer8);
  return S_OK;
}

Воспроизведение потокового звука
Но воспроизведения «просто звуков» мало. Нам нужна еще и музыка! Её можно, само собой, поместить в статически буфер. Но это займет очень много драгоценной памяти и потребует много времени на загрузку. В DirectSound есть возможность воспроизводить и потоковые звуки, как бы пропуская их через буфер.
У буфера есть текущая позиция, с которой он в данный момент воспроизводит звук. Если мы поставим его на циклическое воспроизведение, а потом будем заполнять все, что проиграно с прошлого раза новыми звуковыми данными, то это будет выглядеть как воспроизведение длинного звука.
Создание потокового звукового буфера выглядит почти один в один с обычным статическим буфером, я не буду на этом останавливаться. Мы точно так же заливаем начальную часть звука (но не сразу, а как только получили команду «играй»). В дальнейшем, для обеспечения непрерывности воспроизведения, мы должны периодически (желательно несколько раз в секунду, в зависимости от размера буфера) смотреть, как далеко звук ушел и заливать новый фрагмент. Это можно делать как из основного цикла, так и из отдельного потока. В сэмпле используется второй подход.
В коде это выглядит так:

void *pPtr1, *pPtr2;
DWORD dwSize1, dwSize2;
DWORD dwReadPos, dwSize;
HRESULT hr;
if (SUCCEEDED(hr = m_pdsBuffer->GetCurrentPosition(&dwReadPos, 0))) {
  if (dwReadPos > m_dwWritePos) {
    dwsize = dwReadPos – m_dwWritePos;
  } else {
    dwSize = BUFFER_SIZE  - m_dwWritePos + dwReadPos;
  }
  if (!dwSize) {
    return;
  }
  hr = m_pdsBuffer->Lock(m_dwWritePos, dwSize, &pPtr1, &dwSize1, &pPtr2, &dwsize2, 0);
  if (DSERR_BUFFERLOST == hr) {
    buffer->Restore(); // Буфер может быть потерян, но я ни разу не сталкивался – никаких гараний
    hr = buffer->Lock(m_dwWritePos, dwSize, &pPtr1, &dwSize1, &pPtr2, &dwSize2, 0);
  }
  writePos = readPos;
  if (SUCCEEDED(hr)) {
    int nDecoded = m_oggDecoder.Decode(pPtr1, dwSize1);
    if (nDecoded != size1 && nDecoded != 0) {
      FillMemory((char*)pPtr1 + nDecoded, dsSize1 - nDecoded, 0);
      if (m_bLooping) {
        oggDecoder.Reset();
      }
    }
    if (ptr2) {
      decoded = decoder.Decode(pPtr2, dwSize2);
      if (decoded != size2 && decoded != 0) {
        FillMemory((char*)pPtr1 + dwDecoded, dwSize2 - dwDecoded, 0);
        if (m_bLooping) {
          oggDecoder.Reset();
        }
      }
    }
    m_pdsBuffer->Unlock(pPtr1, dwSize1, pPtr2, dwSize2);
  }
}

Заключение
Как видите, никакой rocket science в программировании звука через «страшный и ужасный» DirectSound нет. За кадром я оставил 3х мерный звук и различные эффекты. Думаю, что разобраться с ними не составит особого труда.

#directsound, #DirectX, #звук, #технология

5 марта 2007