Thinking Different




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, NULL00)))
    {
        return false;
    }
 
    // 웨이브 데이터를 버퍼에 복사합니다.
    memcpy(bufferPtr, waveData, waveFileHeader.dataSize);
 
    // 데이터가 쓰여진 후 보조 버퍼의 잠금을 해제합니다.
    if (FAILED((*secondaryBuffer)->Unlock((void*)bufferPtr, bufferSize, NULL0)))
    {
        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(000)))
    {
        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