Adicionando a Biblioteca de Som
Para adicionar o som vamos usar a API antiga do Windows: o DirectSound. Essa biblioteca foi desenvolvida com o paradigma orientado à objetos.
Precisaremos criar dois buffers para trabalhar com o DirectSound. O primeiro buffer é padrão, o seguindo buffer é onde de fato vamos escrever.
Criando o DirectSound
1
#include <dsound.h>
Vamos usar o LoadLibrary e um ponteiro para uma função que será carregada da mesma maneira que o xinput (gamepad).
1
2
#define DIRECT_SOUND_CREATE(name) HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS, LPUNKNOWN pUnkOuter)
typedef DIRECT_SOUND_CREATE(direct_sound_create_func);
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
// NOTE: Sound - 48khz hj em dia - 48k samples/segundo
// 48000 / 60 fps = 800 samples/frame (para alinhar o som com o video)
internal void
init_dsound(HWND window, s32 samples_second, s32 buffer_size)
{
HMODULE lib = LoadLibraryA("dsound.dll");
if (!lib) {
// TODO: log
return;
}
direct_sound_create_func *direct_sound_create =
(direct_sound_create_func *) GetProcAddress(lib, "DirectSoundCreate");
if (!direct_sound_create) {
// TODO: log
return;
}
LPDIRECTSOUND direct_sound;
if (!SUCCEEDED(direct_sound_create(0, &direct_sound, 0))) {
// TODO: log
return;
}
WAVEFORMATEX wave_format = {};
wave_format.wFormatTag = WAVE_FORMAT_PCM;
wave_format.nChannels = 2;
wave_format.wBitsPerSample = 16;
wave_format.nBlockAlign = wave_format.nChannels * wave_format.wBitsPerSample / 8;
wave_format.nSamplesPerSec = samples_second;
wave_format.nAvgBytesPerSec = wave_format.nSamplesPerSec * wave_format.nBlockAlign;
wave_format.cbSize = 0;
if (!SUCCEEDED(direct_sound->SetCooperativeLevel(window, DSSCL_PRIORITY))) {
// TODO: log
return;
}
DSBUFFERDESC primary_desc = {};
primary_desc.dwSize = sizeof(primary_desc);
primary_desc.dwFlags = DSBCAPS_PRIMARYBUFFER;
//
// Create Primary Buffer
//
LPDIRECTSOUNDBUFFER primary_buffer;
if (!SUCCEEDED(direct_sound->CreateSoundBuffer(&primary_desc, &primary_buffer, 0))) {
// TODO: log
return;
}
if (!SUCCEEDED(primary_buffer->SetFormat(&wave_format))) {
// TODO: log
return;
}
OutputDebugStringA("Primary buffer format was set.\n");
//
// Create Secondary Buffer
//
DSBUFFERDESC secondary_desc = {};
secondary_desc.dwSize = sizeof(secondary_desc);
secondary_desc.dwFlags = DSBCAPS_GETCURRENTPOSITION2;
secondary_desc.dwBufferBytes = buffer_size;
secondary_desc.lpwfxFormat = &wave_format;
if (!SUCCEEDED(direct_sound->CreateSoundBuffer(&secondary_desc, &global_secondary_buffer, 0))) {
// TODO: log
return;
}
OutputDebugStringA("Secondary buffer created successfully.\n");
}
O PrimaryBuffer é obrigatório somente para informar o formato do som (
primary_buffer->SetFormat
- PCM) mais nada! Ele não será de fato um buffer onde vamos escrever. Para isso usaremos o SecondaryBuffer.
Os canais são empacotados juntos, ou seja, teremos no mesmo pacote o padrão [LEFT RIGHT] [LEFT RIGHT] [… …] Sendo que o valor calculado de bytes por amostra é:
1
2
3
4
5
6
7
8
9
10
WAVEFORMATEX wave_format = {};
wave_format.wFormatTag = WAVE_FORMAT_PCM;
wave_format.nChannels = 2;
wave_format.wBitsPerSample = 16;
// esses params poderiam ser calculados automaticamente pela lib, mas não são.
wave_format.nBlockAlign = wave_format.nChannels * wave_format.wBitsPerSample / 8; // 4 bytes per unit (including left and right)
wave_format.nSamplesPerSec = samples_per_second; // 48khz == 800 samples per frame
wave_format.nAvgBytesPerSec = samples_per_second * wave_format.nBlockAlign; // 192kb
wave_format.cbSize = 0;
Inicializando o Som
1
2
3
4
5
int samples_per_second = 48000;
int channels = 2;
int byte_size = samples_per_second * sizeof(int16) * channels;
init_dsound(window, samples_per_second, byte_size);
Notas Importantes de C++
Em C++ quando estamos usando OOP estamos definindo um escopo para funções que existem em classes/structs.
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Foo
{
int x;
void bar(int c);
}
void Foo::bar(int c)
{
x += c;
}
Foo foo;
foo.bar(5);
No exemplo acima, a função bar pertence ao escopo da struct e não ao escopo do arquivo. Logo, não podemos chamar bar(5);
direto.
Por baixo, o que o compilador faz é gerar um “OOP” com C. Veja:
1
2
3
4
void bar(Foo *this, int c)
{
this->x = c;
}
Virtual functions
Quando declaramos uma função com virtual
bascimante o compilador irá inserir um vtable pointer na struct e declarar um vtable pointer global para aquela função da struct e para outras caso haja.
Depois, quando precisamos chamar aquela função marcada com vtable, o que ocorre é que primeiro o programa irá olhar para o ponteiro global e acessar aquele valor (exemplo foo->vtable
). É semelhante com que fizemos para o XInput com funções stub.
É por isso que quando usamos o DirectSound precisamos carregar apenas o objeto (GetProcAddress) e não todas as funções para poder chamá-las.
Se olharmos no disassembly, veremos quem uma função normal é acessada pelo call <function_name> (<HEX_VALUE>)
enquanto quando estamos em objeto, chamamos o endereço da função call qword ptr [rax+30h]
.
Virtual é mais custoso por manter referências em tabelas e ter várias chamadas.
Geralmente, usado com herança em OOP.