[DirectX11] Tutorial 31 - 3D 사운드
Tutorial 31 - 3D 사운드
원문 : http://www.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 세계에서만 쓸 수 있는 것들도 조절할 수 있습니다. 같은 음향 버퍼에 두 가지 목적의 컨트롤러가 있다고 생각하셔도 좋습니다.
프레임워크
Soundclass.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #pragma once 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(const char*, IDirectSoundBuffer8**, IDirectSound3DBuffer8**); void ShutdownWaveFile(IDirectSoundBuffer8**, IDirectSound3DBuffer8**); bool PlayWaveFile(); private: IDirectSound8* m_DirectSound = nullptr; IDirectSoundBuffer* m_primaryBuffer = nullptr; IDirectSound3DListener8* m_listener = nullptr; IDirectSoundBuffer8* m_secondaryBuffer1 = nullptr; IDirectSound3DBuffer8* m_secondary3DBuffer1 = nullptr; }; | cs |
Soundclass.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | #include "stdafx.h" #include "SoundClass.h" SoundClass::SoundClass() { } SoundClass::SoundClass(const SoundClass& other) { } SoundClass::~SoundClass() { } bool SoundClass::Initialize(HWND hwnd) { // 직접 사운드와 기본 사운드 버퍼를 초기화합니다. if (!InitializeDirectSound(hwnd)) { return false; } // 웨이브 오디오 파일을 2 차 버퍼에로드한다. if (!LoadWaveFile("../Dx11Demo_31/data/sound02.wav", &m_secondaryBuffer1, &m_secondary3DBuffer1)) { return false; } // 로드 된 웨이브 파일을 재생합니다. if(!PlayWaveFile()) { return false; } return true; } void SoundClass::Shutdown() { // 2 차 버퍼를 해제한다. ShutdownWaveFile(&m_secondaryBuffer1, &m_secondary3DBuffer1); // Direct Sound API를 셧다운한다. ShutdownDirectSound(); } bool SoundClass::InitializeDirectSound(HWND hwnd) { // 기본 사운드 장치에 대한 직접 사운드 인터페이스 포인터를 초기화합니다. if (FAILED(DirectSoundCreate8(NULL, &m_DirectSound, NULL))) { return false; } // 협조 레벨을 우선 순위로 설정하여 기본 사운드 버퍼의 형식을 수정할 수 있습니다. if (FAILED(m_DirectSound->SetCooperativeLevel(hwnd, DSSCL_PRIORITY))) { return false; } // 기본 버퍼 설명을 설정합니다. DSBUFFERDESC bufferDesc; 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; // 기본 사운드 장치에서 기본 사운드 버퍼를 제어합니다. if (FAILED(m_DirectSound->CreateSoundBuffer(&bufferDesc, &m_primaryBuffer, NULL))) { return false; } // 기본 사운드 버퍼의 형식을 설정합니다. // 이 경우 16 비트 스테레오 (cd 오디오 형식)에서 44,100 샘플 / 초로 기록되는 .WAV 파일입니다. WAVEFORMATEX waveFormat; 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; // 기본 버퍼를 지정된 파형 형식으로 설정합니다. if (FAILED(m_primaryBuffer->SetFormat(&waveFormat))) { return false; } // 리스너 인터페이스를 얻습니다. if(FAILED(m_primaryBuffer->QueryInterface(IID_IDirectSound3DListener8, (LPVOID*)&m_listener))) { return false; } // 리스너의 초기 위치를 장면의 중간에 설정합니다. m_listener->SetPosition(0.0f, 0.0f, 0.0f, DS3D_IMMEDIATE); return true; } void SoundClass::ShutdownDirectSound() { // 리스너 인터페이스를 해제합니다. if(m_listener) { m_listener->Release(); m_listener = 0; } // 기본 사운드 버퍼 포인터를 놓습니다. if (m_primaryBuffer) { m_primaryBuffer->Release(); m_primaryBuffer = 0; } // 직접 사운드 인터페이스 포인터를 놓습니다. if (m_DirectSound) { m_DirectSound->Release(); m_DirectSound = 0; } } bool SoundClass::LoadWaveFile(const char * filename, IDirectSoundBuffer8** secondaryBuffer, IDirectSound3DBuffer8** secondary3DBuffer) { // 웨이브 파일을 바이너리 모드로 엽니다. FILE* filePtr = nullptr; int error = fopen_s(&filePtr, filename, "rb"); if (error != 0) { return false; } // 웨이브 파일 헤더를 읽는다 WaveHeaderType waveFileHeader; unsigned int count = fread(&waveFileHeader, sizeof(waveFileHeader), 1, filePtr); if (count != 1) { return false; } // RIFF 포멧 chunk id 체크한다. if ((waveFileHeader.chunkId[0] != 'R') || (waveFileHeader.chunkId[1] != 'I') || (waveFileHeader.chunkId[2] != 'F') || (waveFileHeader.chunkId[3] != 'F')) { return false; } // 파일포멧이 wave 인지 체크 한다 if ((waveFileHeader.format[0] != 'W') || (waveFileHeader.format[1] != 'A') || (waveFileHeader.format[2] != 'V') || (waveFileHeader.format[3] != 'E')) { return false; } // fmt 포멧 chunk id 체크한다. if ((waveFileHeader.subChunkId[0] != 'f') || (waveFileHeader.subChunkId[1] != 'm') || (waveFileHeader.subChunkId[2] != 't') || (waveFileHeader.subChunkId[3] != ' ')) { return false; } // 오디오 형식이 WAVE_FORMAT_PCM인지 체크한다 if (waveFileHeader.audioFormat != WAVE_FORMAT_PCM) { return false; } // 웨이브 파일이 모노 형식인지 스테레오인지 체크한다. if (waveFileHeader.numChannels != 1) { return false; } // 웨이브 파일이 44.1 KHz의 샘플 속도로 레코딩되었는지 확인합니다. if (waveFileHeader.sampleRate != 44100) { return false; } // 웨이브 파일이 16 비트 형식으로 레코딩되었는지 확인합니다. if (waveFileHeader.bitsPerSample != 16) { return false; } // 헤더 데이터의 chunk를 확인한다. if ((waveFileHeader.dataChunkId[0] != 'd') || (waveFileHeader.dataChunkId[1] != 'a') || (waveFileHeader.dataChunkId[2] != 't') || (waveFileHeader.dataChunkId[3] != 'a')) { return false; } // 이 웨이브 파일이로드 될 보조 버퍼의 웨이브 형식을 설정합니다. WAVEFORMATEX waveFormat; 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; // 웨이브 파일이로드 될 2 차 사운드 버퍼의 버퍼 설명을 설정합니다. DSBUFFERDESC bufferDesc; bufferDesc.dwSize = sizeof(DSBUFFERDESC); bufferDesc.dwFlags = DSBCAPS_CTRLVOLUME | DSBCAPS_CTRL3D; bufferDesc.dwBufferBytes = waveFileHeader.dataSize; bufferDesc.dwReserved = 0; bufferDesc.lpwfxFormat = &waveFormat; bufferDesc.guid3DAlgorithm = GUID_NULL; // 특정 버퍼 설정으로 임시 사운드 버퍼를 만듭니다. IDirectSoundBuffer* tempBuffer = nullptr; if (FAILED(m_DirectSound->CreateSoundBuffer(&bufferDesc, &tempBuffer, NULL))) { return false; } // 다이렉트 사운드 인터페이스에 대해 버퍼 형식을 테스트하고 보조 버퍼를 만듭니다. if (FAILED(tempBuffer->QueryInterface(IID_IDirectSoundBuffer8, (void**)&*secondaryBuffer))) { return false; } // 임시 버퍼를 해제한다. tempBuffer->Release(); tempBuffer = 0; // 데이터 청크 헤더의 끝에서 시작하는 웨이브 데이터의 시작 부분으로 이동합니다. fseek(filePtr, sizeof(WaveHeaderType), SEEK_SET); // 웨이브 파일 데이터를 저장할 임시 버퍼를 만듭니다. unsigned char* waveData = new unsigned char[waveFileHeader.dataSize]; if (!waveData) { return false; } // 웨이브 파일 데이터를 새로 생성 된 버퍼로 읽어들입니다. count = fread(waveData, 1, waveFileHeader.dataSize, filePtr); if (count != waveFileHeader.dataSize) { return false; } // 읽고 나면 파일을 닫는다. error = fclose(filePtr); if (error != 0) { return false; } // 웨이브 데이터를 쓰기 위해 2차 버퍼를 잠급니다. unsigned char* bufferPtr = nullptr; unsigned long bufferSize = 0; if (FAILED((*secondaryBuffer)->Lock(0, waveFileHeader.dataSize, (void**)&bufferPtr, (DWORD*)&bufferSize, NULL, 0, 0))) { return false; } // 웨이브 데이터를 버퍼에 복사합니다. memcpy(bufferPtr, waveData, waveFileHeader.dataSize); // 데이터가 쓰여진 후 보조 버퍼의 잠금을 해제합니다. if (FAILED((*secondaryBuffer)->Unlock((void*)bufferPtr, bufferSize, NULL, 0))) { return false; } // 보조 버퍼에 복사 된 웨이브 데이터를 해제합니다. delete[] waveData; waveData = 0; // 2 차 사운드 버퍼에 3D 인터페이스 가져 오기. if(FAILED((*secondaryBuffer)->QueryInterface(IID_IDirectSound3DBuffer8, (void**)&*secondary3DBuffer))) { return false; } return true; } void SoundClass::ShutdownWaveFile(IDirectSoundBuffer8** secondaryBuffer, IDirectSound3DBuffer8** secondary3DBuffer) { // 3D 인터페이스를 2 차 사운드 버퍼에 놓습니다. if(*secondary3DBuffer) { (*secondary3DBuffer)->Release(); *secondary3DBuffer = 0; } // 2 차 사운드 버퍼를 해제한다. if (*secondaryBuffer) { (*secondaryBuffer)->Release(); *secondaryBuffer = 0; } } bool SoundClass::PlayWaveFile() { // 사운드를 배치 할 위치의 3D 위치를 설정합니다. float positionX = -2.0f; float positionY = 0.0f; float positionZ = 0.0f; // 사운드 버퍼의 시작 부분에 위치를 설정합니다. if (FAILED(m_secondaryBuffer1->SetCurrentPosition(0))) { return false; } // 버퍼의 볼륨을 100 %로 설정합니다. if (FAILED(m_secondaryBuffer1->SetVolume(DSBVOLUME_MAX))) { return false; } // 사운드의 3D 위치를 설정합니다. m_secondary3DBuffer1->SetPosition(positionX, positionY, positionZ, DS3D_IMMEDIATE); // 2 차 사운드 버퍼의 내용을 재생합니다. if (FAILED(m_secondaryBuffer1->Play(0, 0, 0))) { return false; } return true; } | cs |
마치면서
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 모노 사운드를 모두 사용하도록 프로그램을 고쳐 보십시오.
소스코드
소스코드 : Dx11Demo_31.zip
'DirectX 11 > Basic' 카테고리의 다른 글
[DirectX11] Tutorial 33 - 불효과 (1) | 2017.12.31 |
---|---|
[DirectX11] Tutorial 32 - 유리, 얼음 (0) | 2017.12.30 |
[DirectX11] Tutorial 30 - 다중 포인트 조명 (0) | 2017.12.27 |
[DirectX11] Tutorial 29 - 물 (0) | 2017.12.26 |
[DirectX11] Tutorial 28 - 페이드 효과 (0) | 2017.12.25 |