DirectX11 Tutorial 31: 3차원 음향

강좌번역/DirectX 11 2017. 9. 8. 00:35 by 빠재

Tutorial 31: 3D Sound

원문: http://rastertek.com/dx11tut31.html

이번 예제에서는 C++와 DirectX 11의 Direct Sound를 이용하여 3D 음향 효과를 구현하는 방법을 다룹니다. 코드는 이전 Direct Sound 예제에서 이어집니다. 그때는 2D 사운드였었지만 이번에는 3D 사운드가 되도록 코드를 수정합니다.

3D 음향효과의 첫 번째 개념은 모든 소리가 월드에서 3차원의 위치를 가지게 된다는 점입니다. 음향효과에서 사용하는 x, y, z좌표는 기존 DirectX 그래픽에서 사용했던 것과 같은 왼손 좌표계를 사용합니다. 그 덕분에 3D 모델 주변에 “사운드 버블”을 만드는 것이 쉽게 가능합니다. 예를 들어 월드 안 어느 지점에 강이 있다고 생각해 봅시다. 그렇다면 여러분은 강 주변 지역에 공 모양의 경계를 만들어서 그 안에 들어가 있는 사람만 강물이 흐르는 소리를 듣게 할 수 있을 것입니다. 그리고 그 중심에 가까이 갈수록 그 사운드 버블은 더 큰 소리를 낼 것입니다.

Direct Sound를 이용한 3D 음향효과의 또다른 중요한 개념은 청자(listener)입니다. 청자는 3D 월드의 특정 지점에 위치하여 소리를 듣는 사람이라고 생각하면 됩니다. Direct Sound는 그 청자와 3D 사운드 사이의 거리를 이용하여 그 상황에 맞는 크기의 소리가 재생되도록 합니다. 청자는 하나만 존재할 수도 있습니다. 대부분의 3D 어플리케이션은 청자의 위치와 카메라 뷰의 위치가 동일합니다. 그리고 카메라가 움직이면 청자의 위치도 같이 바뀌어 Direct Sound가 자동으로 바뀐 위치에 맞게 3D 음향을 믹싱해줄 것입니다.

3D 음향효과에 쓰일 수 있는 오디오 포맷은 2D의 경우와 같이 불러오는 코드만 작성한다면 아무것이나 다 가능합니다. 하나 제약이 있다면 모노 사운드(1채널 사운드)여야 한다는 것입니다. 스테레오(2채널 사운드)는 Direct Sound가 에러를 일으킬 것입니다. 이 예제에서는 16bit 44100KHz 모노의 wav파일을 사용합니다.

마지막으로 알아야 할 개념은 IDirectSound3DBuffer8 인터페이스입니다. 이전 예제와 같이 음향효과는 IDirectSoundBuffer8 타입의 보조 음향 버퍼에 로드됩니다. 한 가지 다른 것이라면 DSBCAPS_CTRL3D 비트 플래그를 추가하여 Direct Sound가 이를 3D 사운드로 인식시키게 할 것입니다. 그렇게 음향이 보조 음향 버퍼에 들어가게 되면 IDirectSound3DBuffer8 인터페이스 객체를 얻을 수 있습니다. 이를 이용하여 볼륨과 같은 일반적인 소리의 속성에서부터 소리의 위치와 같은 3D 세계에서만 쓸 수 있는 것들도 조절할 수 있습니다. 같은 음향 버퍼에 두 가지 목적의 컨트롤러가 있다고 생각하셔도 좋습니다.

프레임워크

프레임워크는 이전 Direct Sound 예제와 동일합니다.

Soundclass.h

///////////////////////////////////////////////////////////////////////////////
// Filename: soundclass.h
///////////////////////////////////////////////////////////////////////////////
#ifndef _SOUNDCLASS_H_
#define _SOUNDCLASS_H_


/////////////
// LINKING //
/////////////
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "winmm.lib")


//////////////
// INCLUDES //
//////////////
#include <windows.h>
#include <mmsystem.h>
#include <dsound.h>
#include <stdio.h>


///////////////////////////////////////////////////////////////////////////////
// Class name: SoundClass
///////////////////////////////////////////////////////////////////////////////
class SoundClass
{
private:
    struct WaveHeaderType
    {
        char chunkId[4];
        unsigned long chunkSize;
        char format[4];
        char subChunkId[4];
        unsigned long subChunkSize;
        unsigned short audioFormat;
        unsigned short numChannels;
        unsigned long sampleRate;
        unsigned long bytesPerSecond;
        unsigned short blockAlign;
        unsigned short bitsPerSample;
        char dataChunkId[4];
        unsigned long dataSize;
    };

public:
    SoundClass();
    SoundClass(const SoundClass&);
    ~SoundClass();

    bool Initialize(HWND);
    void Shutdown();

private:
    bool InitializeDirectSound(HWND);
    void ShutdownDirectSound();

    bool LoadWaveFile(char*, IDirectSoundBuffer8**, IDirectSound3DBuffer8**);
    void ShutdownWaveFile(IDirectSoundBuffer8**, IDirectSound3DBuffer8**);

    bool PlayWaveFile();

private:
    IDirectSound8* m_DirectSound;
    IDirectSoundBuffer* m_primaryBuffer;

여기에 청자 객체를 선언합니다.

    IDirectSound3DListener8* m_listener;
    IDirectSoundBuffer8* m_secondaryBuffer1;

또한 3D 사운드를 조절할 3D 음향 버퍼를 선언합니다.

    IDirectSound3DBuffer8* m_secondary3DBuffer1;
};

#endif

Soundclass.cpp

이전 Direct Sound 예제에서 변경된 부분만 설명하겠습니다.

///////////////////////////////////////////////////////////////////////////////
// Filename: soundclass.cpp
///////////////////////////////////////////////////////////////////////////////
#include "soundclass.h"


SoundClass::SoundClass()
{
    m_DirectSound = 0;
    m_primaryBuffer = 0;

생성자에서 청자 객체를 null로 초기화합니다.

    m_listener = 0;
    m_secondaryBuffer1 = 0;

생성자에서 3D 음향 보조 버퍼를 null로 초기화합니다.

    m_secondary3DBuffer1 = 0;
}


SoundClass::SoundClass(const SoundClass& other)
{
}


SoundClass::~SoundClass()
{
}


bool SoundClass::Initialize(HWND hwnd)
{
    bool result;


    // Initialize direct sound and the primary sound buffer.
    result = InitializeDirectSound(hwnd);
    if(!result)
    {
        return false;
    }

보조 음향 버퍼와 3D 음향 버퍼 모두에게 필요한 3D 음향 파일을 로드합니다. 두 버퍼 객체는 모두 음향 버퍼를 조절하는 데 사용될 것입니다.

    // Load a wave audio file onto a secondary buffer.
    result = LoadWaveFile("../Engine/data/sound02.wav", &m_secondaryBuffer1, &m_secondary3DBuffer1);
    if(!result)
    {
        return false;
    }

    // Play the wave file now that it has been loaded.
    result = PlayWaveFile();
    if(!result)
    {
        return false;
    }

    return true;
}


void SoundClass::Shutdown()
{

ShutdownWaveFile 함수에서 보조 음향 버퍼와 보조 3D 음향 버퍼 객체를 해제합니다.

    // Release the secondary buffer.
    ShutdownWaveFile(&m_secondaryBuffer1, &m_secondary3DBuffer1);

    // Shutdown the Direct Sound API.
    ShutdownDirectSound();

    return;
}


bool SoundClass::InitializeDirectSound(HWND hwnd)
{
    HRESULT result;
    DSBUFFERDESC bufferDesc;
    WAVEFORMATEX waveFormat;


    // Initialize the direct sound interface pointer for the default sound device.
    result = DirectSoundCreate8(NULL, &m_DirectSound, NULL);
    if(FAILED(result))
    {
        return false;
    }

    // Set the cooperative level to priority so the format of the primary sound buffer can be modified.
    result = m_DirectSound->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
    if(FAILED(result))
    {
        return false;
    }

주 음향 버퍼는 디스크립션에 DSBCAPS_CTRL3D 마스크를 사용하여 이 음향 효과가 3D 기능을 가짐을 알려줍니다.

    // Setup the primary buffer description.
    bufferDesc.dwSize = sizeof(DSBUFFERDESC);
    bufferDesc.dwFlags = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRLVOLUME | DSBCAPS_CTRL3D;
    bufferDesc.dwBufferBytes = 0;
    bufferDesc.dwReserved = 0;
    bufferDesc.lpwfxFormat = NULL;
    bufferDesc.guid3DAlgorithm = GUID_NULL;

    // Get control of the primary sound buffer on the default sound device.
    result = m_DirectSound->CreateSoundBuffer(&bufferDesc, &m_primaryBuffer, NULL);
    if(FAILED(result))
    {
        return false;
    }

주 음향 버퍼의 소리 포맷은 동일하게 두고, 보조 음향 버퍼만 모노 사운드를 사용하도록 합니다. Direct Sound는 자동으로 서로 다른 여러 보조 버퍼들을 주 버퍼로 합쳐줄 것입니다.

    // Setup the format of the primary sound bufffer.
    // In this case it is a .WAV file recorded at 44,100 samples per second in 16-bit stereo (cd audio format).
    waveFormat.wFormatTag = WAVE_FORMAT_PCM;
    waveFormat.nSamplesPerSec = 44100;
    waveFormat.wBitsPerSample = 16;
    waveFormat.nChannels = 2;
    waveFormat.nBlockAlign = (waveFormat.wBitsPerSample / 8) * waveFormat.nChannels;
    waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
    waveFormat.cbSize = 0;

    // Set the primary buffer to be the wave format specified.
    result = m_primaryBuffer->SetFormat(&waveFormat);
    if(FAILED(result))
    {
        return false;
    }

주 음향 버퍼가 만들어지면 여기에서 청자 인터페이스를 가져올 수 있습니다. 이를 통하여 3차원 세계에 배치된 소리를 듣는 청자를 위치시킬 수 있게 됩니다.

    // Obtain a listener interface.
    result = m_primaryBuffer->QueryInterface(IID_IDirectSound3DListener8, (LPVOID*)&m_listener);
    if(FAILED(result))
    {
        return false;
    }

일단 청자의 초기 위치를 원점으로 설정합니다. DS3D_IMMEDIATE 인자는 Direct Sound가 위치의 변경 내용을 나중에 처리하지 않고 지금 바로 적용하도록 합니다.

    // Set the initial position of the listener to be in the middle of the scene.
    m_listener->SetPosition(0.0f, 0.0f, 0.0f, DS3D_IMMEDIATE);

    return true;
}


void SoundClass::ShutdownDirectSound()
{

ShutdownDirectSound 함수에서 청자 객체를 해제합니다.

    // Release the listener interface.
    if(m_listener)
    {
        m_listener->Release();
        m_listener = 0;
    }

    // Release the primary sound buffer pointer.
    if(m_primaryBuffer)
    {
        m_primaryBuffer->Release();
        m_primaryBuffer = 0;
    }

    // Release the direct sound interface pointer.
    if(m_DirectSound)
    {
        m_DirectSound->Release();
        m_DirectSound = 0;
    }

    return;
}

LoadWaveFile 함수는 IDirectSound3DBuffer8 포인터를 받습니다.

bool SoundClass::LoadWaveFile(char* filename, IDirectSoundBuffer8** secondaryBuffer, IDirectSound3DBuffer8** secondary3DBuffer)
{
    int error;
    FILE* filePtr;
    unsigned int count;
    WaveHeaderType waveFileHeader;
    WAVEFORMATEX waveFormat;
    DSBUFFERDESC bufferDesc;
    HRESULT result;
    IDirectSoundBuffer* tempBuffer;
    unsigned char* waveData;
    unsigned char* bufferPtr;
    unsigned long bufferSize;


    // Open the wave file in binary.
    error = fopen_s(&filePtr, filename, "rb");
    if(error != 0)
    {
        return false;
    }

    // Read in the wave file header.
    count = fread(&waveFileHeader, sizeof(waveFileHeader), 1, filePtr);
    if(count != 1)
    {
        return false;
    }

    // Check that the chunk ID is the RIFF format.
    if((waveFileHeader.chunkId[0] != 'R') || (waveFileHeader.chunkId[1] != 'I') || 
       (waveFileHeader.chunkId[2] != 'F') || (waveFileHeader.chunkId[3] != 'F'))
    {
        return false;
    }

    // Check that the file format is the WAVE format.
    if((waveFileHeader.format[0] != 'W') || (waveFileHeader.format[1] != 'A') ||
       (waveFileHeader.format[2] != 'V') || (waveFileHeader.format[3] != 'E'))
    {
        return false;
    }

    // Check that the sub chunk ID is the fmt format.
    if((waveFileHeader.subChunkId[0] != 'f') || (waveFileHeader.subChunkId[1] != 'm') ||
       (waveFileHeader.subChunkId[2] != 't') || (waveFileHeader.subChunkId[3] != ' '))
    {
        return false;
    }

    // Check that the audio format is WAVE_FORMAT_PCM.
    if(waveFileHeader.audioFormat != WAVE_FORMAT_PCM)
    {
        return false;
    }

3D 음향 파일은 반드시 단일 채널이어야 합니다(모노)

    // Check that the wave file was recorded in mono format.
    if(waveFileHeader.numChannels != 1)
    {
        return false;
    }

    // Check that the wave file was recorded at a sample rate of 44.1 KHz.
    if(waveFileHeader.sampleRate != 44100)
    {
        return false;
    }

    // Ensure that the wave file was recorded in 16 bit format.
    if(waveFileHeader.bitsPerSample != 16)
    {
        return false;
    }

    // Check for the data chunk header.
    if((waveFileHeader.dataChunkId[0] != 'd') || (waveFileHeader.dataChunkId[1] != 'a') ||
       (waveFileHeader.dataChunkId[2] != 't') || (waveFileHeader.dataChunkId[3] != 'a'))
    {
        return false;
    }

보조 음향 버퍼는 2개 채널(스테레오)이 아니라 단일 채널(모노)을 사용하도록 합니다.

    // Set the wave format of secondary buffer that this wave file will be loaded onto.
    waveFormat.wFormatTag = WAVE_FORMAT_PCM;
    waveFormat.nSamplesPerSec = 44100;
    waveFormat.wBitsPerSample = 16;
    waveFormat.nChannels = 1;
    waveFormat.nBlockAlign = (waveFormat.wBitsPerSample / 8) * waveFormat.nChannels;
    waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
    waveFormat.cbSize = 0;

버퍼에 3D 옵션을 주기 위해서는 dwFlags인자에 DSBCAPS_CTRL3D 비트마스크를 사용하여 알려줍니다.

    // Set the buffer description of the secondary sound buffer that the wave file will be loaded onto.
    bufferDesc.dwSize = sizeof(DSBUFFERDESC);
    bufferDesc.dwFlags = DSBCAPS_CTRLVOLUME | DSBCAPS_CTRL3D;
    bufferDesc.dwBufferBytes = waveFileHeader.dataSize;
    bufferDesc.dwReserved = 0;
    bufferDesc.lpwfxFormat = &waveFormat;
    bufferDesc.guid3DAlgorithm = GUID_NULL;

    // Create a temporary sound buffer with the specific buffer settings.
    result = m_DirectSound->CreateSoundBuffer(&bufferDesc, &tempBuffer, NULL);
    if(FAILED(result))
    {
        return false;
    }

    // Test the buffer format against the direct sound 8 interface and create the secondary buffer.
    result = tempBuffer->QueryInterface(IID_IDirectSoundBuffer8, (void**)&*secondaryBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // Release the temporary buffer.
    tempBuffer->Release();
    tempBuffer = 0;

    // Move to the beginning of the wave data which starts at the end of the data chunk header.
    fseek(filePtr, sizeof(WaveHeaderType), SEEK_SET);

    // Create a temporary buffer to hold the wave file data.
    waveData = new unsigned char[waveFileHeader.dataSize];
    if(!waveData)
    {
        return false;
    }

    // Read in the wave file data into the newly created buffer.
    count = fread(waveData, 1, waveFileHeader.dataSize, filePtr);
    if(count != waveFileHeader.dataSize)
    {
        return false;
    }

    // Close the file once done reading.
    error = fclose(filePtr);
    if(error != 0)
    {
        return false;
    }

    // Lock the secondary buffer to write wave data into it.
    result = (*secondaryBuffer)->Lock(0, waveFileHeader.dataSize, (void**)&bufferPtr, (DWORD*)&bufferSize, NULL, 0, 0);
    if(FAILED(result))
    {
        return false;
    }

    // Copy the wave data into the buffer.
    memcpy(bufferPtr, waveData, waveFileHeader.dataSize);

    // Unlock the secondary buffer after the data has been written to it.
    result = (*secondaryBuffer)->Unlock((void*)bufferPtr, bufferSize, NULL, 0);
    if(FAILED(result))
    {
        return false;
    }
    
    // Release the wave data since it was copied into the secondary buffer.
    delete [] waveData;
    waveData = 0;

wav파일을 보조 음향 버퍼로 불러왔으므로 여기에서 3D 인터페이스를 얻어올 수 있습니다. 하지만 볼륨 조절과 같은 기존 음향 기능은 secondaryBuffer을 사용해야 합니다.

    // Get the 3D interface to the secondary sound buffer.
    result = (*secondaryBuffer)->QueryInterface(IID_IDirectSound3DBuffer8, (void**)&*secondary3DBuffer);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}


void SoundClass::ShutdownWaveFile(IDirectSoundBuffer8** secondaryBuffer, IDirectSound3DBuffer8** secondary3DBuffer)
{

사운드 버퍼와 같이 새로 추가한 3D 인터페이스도 같이 해제해 주어야 합니다.

    // Release the 3D interface to the secondary sound buffer.
    if(*secondary3DBuffer)
    {
        (*secondary3DBuffer)->Release();
        *secondary3DBuffer = 0;
    }

    // Release the secondary sound buffer.
    if(*secondaryBuffer)
    {
        (*secondaryBuffer)->Release();
        *secondaryBuffer = 0;
    }

    return;
}


bool SoundClass::PlayWaveFile()
{
    HRESULT result;
    float positionX, positionY, positionZ;

3D 음향을 배치해야 할 곳으로 위치를 설정합니다. 여기에서는 왼쪽으로 하겠습니다.

    // Set the 3D position of where the sound should be located.
    positionX = -2.0f;
    positionY = 0.0f;
    positionZ = 0.0f;

    // Set position at the beginning of the sound buffer.
    result = m_secondaryBuffer1->SetCurrentPosition(0);
    if(FAILED(result))
    {
        return false;
    }

    // Set volume of the buffer to 100%.
    result = m_secondaryBuffer1->SetVolume(DSBVOLUME_MAX);
    if(FAILED(result))
    {
        return false;
    }

3D 인터페이스 음향의 위치를 3차원 좌표로 설정합니다. DS3D_IMMEDIATE플래그는 음향 위치의 변경을 조금 미루어 처리하지 않고 즉시 처리하도록 합니다.

    // Set the 3D position of the sound.
    m_secondary3DBuffer1->SetPosition(positionX, positionY, positionZ, DS3D_IMMEDIATE);

    // Play the contents of the secondary sound buffer.
    result = m_secondaryBuffer1->Play(0, 0, 0);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}

정리하며

Direct Sound를 이용하여 음향 엔진이 3D 기능을 갖게 하였습니다.

연습 문제

  1. 프로그램을 다시 컴파일하여 실행해 보십시오. 입체 음향이 왼쪽에서 들릴 것입니다. ESC키로 종료합니다.
  2. 음향의 위치를 다른 곳으로 바꾸어 보십시오. 음향 설비가 잘 갖추어져 있다면 정말 입체감이 느껴지는 소리가 들리겠지만, 단지 헤드셋이거나 스피커가 2개일 뿐이라면 큰 효과가 없을 수 있습니다.
  3. 청자의 위치를 바꾸어 보십시오. 음향의 위치에 따라 달라지는 변화를 들어 보십시오.
  4. 4개의 다른 소리를 로드하여 청자 주위의 서로 다른 곳 4방향에서 재생하여 보십시오. 일례로 청자를 (0,0, 0)위치에 놓고 음향들을 각각 (-1, 0, -1), (1, 0, -1), (-1, 0, 1), (1, 0, 1)위치에 놓아볼 수 있습니다.
  5. 여러분의 음향 포맷으로 소리를 로드해 보십시오(mp3, 22050KHz, 24bit 등등)
  6. 2D 스테레오와 3D 모노 사운드를 모두 사용하도록 프로그램을 고쳐 보십시오.

소스 코드

Visual Studio 2008 프로젝트: dx11tut31.zip

소스 코드만: dx11src31.zip

실행 파일만: dx11exe31.zip

'강좌번역 > DirectX 11' 카테고리의 다른 글

DirectX11 Tutorial 33: 불꽃  (0) 2017.10.25
DirectX11 Tutorial 32: 유리, 얼음  (0) 2017.09.23
DirectX11 Tutorial 30: 다중 점조명  (0) 2017.09.03
DirectX11 Tutorial 29: 물  (2) 2017.09.01
DirectX11 Tutorial 28: 페이드 효과  (2) 2015.09.06
Nav