Post

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.

Esta postagem está licenciada sob CC BY 4.0 pelo autor.