Last active
April 29, 2025 05:56
-
-
Save kevinmoran/3d05e190fb4e7f27c1043a3ba321cede to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Simple example code to load a Wav file and play it with WASAPI | |
// This is NOT complete Wav loading code. It is a barebones example | |
// that makes a lot of assumptions, see the assert() calls for details | |
// | |
// References: | |
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html | |
// Handmade Hero Day 138: Loading WAV Files | |
#include <windows.h> | |
#include <mmdeviceapi.h> | |
#include <audioclient.h> | |
#include <assert.h> | |
#include <stdint.h> | |
// Struct to get data from loaded WAV file. | |
// NB: This will only work for WAV files containing PCM (non-compressed) data | |
// otherwise the layout will be different. | |
#pragma warning(disable : 4200) | |
struct WavFile { | |
// RIFF Chunk | |
uint32_t riffId; | |
uint32_t riffChunkSize; | |
uint32_t waveId; | |
// fmt Chunk | |
uint32_t fmtId; | |
uint32_t fmtChunkSize; | |
uint16_t formatCode; | |
uint16_t numChannels; | |
uint32_t sampleRate; | |
uint32_t byteRate; | |
uint16_t blockAlign; | |
uint16_t bitsPerSample; | |
// These are not present for PCM Wav Files | |
// uint16_t cbSize; | |
// uint16_t wValidBitsPerSample; | |
// uint32_t dwChannelMask; | |
// char subFormatGUID[16]; | |
// data Chunk | |
uint32_t dataId; | |
uint32_t dataChunkSize; | |
uint16_t samples[]; // actual samples start here | |
}; | |
#pragma warning(default : 4200) | |
bool win32LoadEntireFile(const char* filename, void** data, uint32_t* numBytesRead) | |
{ | |
HANDLE file = CreateFileA(filename, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); | |
if((file == INVALID_HANDLE_VALUE)) return false; | |
DWORD fileSize = GetFileSize(file, 0); | |
if(!fileSize) return false; | |
*data = HeapAlloc(GetProcessHeap(), 0, fileSize+1); | |
if(!*data) return false; | |
if(!ReadFile(file, *data, fileSize, (LPDWORD)numBytesRead, 0)) | |
return false; | |
CloseHandle(file); | |
((uint8_t*)*data)[fileSize] = 0; | |
return true; | |
} | |
void Win32FreeFileData(void *data) | |
{ | |
HeapFree(GetProcessHeap(), 0, data); | |
} | |
int main() | |
{ | |
void* fileBytes; | |
uint32_t fileSize; | |
bool result = win32LoadEntireFile("filename.wav", &fileBytes, &fileSize); | |
assert(result); | |
WavFile* wav = (WavFile*)fileBytes; | |
// Check the Chunk IDs to make sure we loaded the file correctly | |
assert(wav->riffId == 1179011410); | |
assert(wav->waveId == 1163280727); | |
assert(wav->fmtId == 544501094); | |
assert(wav->dataId == 1635017060); | |
// Check data is in format we expect | |
assert(wav->formatCode == 1); // Only support PCM data | |
assert(wav->numChannels == 2); // Only support 2-channel data | |
assert(wav->fmtChunkSize == 16); // This should be true for PCM data | |
assert(wav->sampleRate == 44100); // Only support 44100Hz data | |
assert(wav->bitsPerSample == 16); // Only support 16-bit samples | |
// This is how these fields are defined, no harm to assert that they're what we expect | |
assert(wav->blockAlign == wav->numChannels * wav->bitsPerSample/8); | |
assert(wav->byteRate == wav->sampleRate * wav->blockAlign); | |
uint32_t numWavSamples = wav->dataChunkSize / sizeof(uint16_t); | |
uint16_t* wavSamples = wav->samples; | |
HRESULT hr = CoInitializeEx(nullptr, COINIT_SPEED_OVER_MEMORY); | |
assert(hr == S_OK); | |
IMMDeviceEnumerator* deviceEnumerator; | |
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (LPVOID*)(&deviceEnumerator)); | |
assert(hr == S_OK); | |
IMMDevice* audioDevice; | |
hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &audioDevice); | |
assert(hr == S_OK); | |
deviceEnumerator->Release(); | |
IAudioClient2* audioClient; | |
hr = audioDevice->Activate(__uuidof(IAudioClient2), CLSCTX_ALL, nullptr, (LPVOID*)(&audioClient)); | |
assert(hr == S_OK); | |
audioDevice->Release(); | |
// WAVEFORMATEX* defaultMixFormat = NULL; | |
// hr = audioClient->GetMixFormat(&defaultMixFormat); | |
// assert(hr == S_OK); | |
WAVEFORMATEX mixFormat = {}; | |
mixFormat.wFormatTag = WAVE_FORMAT_PCM; | |
mixFormat.nChannels = 2; | |
mixFormat.nSamplesPerSec = 44100;//defaultMixFormat->nSamplesPerSec; | |
mixFormat.wBitsPerSample = 16; | |
mixFormat.nBlockAlign = (mixFormat.nChannels * mixFormat.wBitsPerSample) / 8; | |
mixFormat.nAvgBytesPerSec = mixFormat.nSamplesPerSec * mixFormat.nBlockAlign; | |
const float BUFFER_SIZE_IN_SECONDS = 2.0f; | |
const int64_t REFTIMES_PER_SEC = 10000000; // hundred nanoseconds | |
REFERENCE_TIME requestedSoundBufferDuration = (REFERENCE_TIME)(REFTIMES_PER_SEC * BUFFER_SIZE_IN_SECONDS); | |
DWORD initStreamFlags = ( AUDCLNT_STREAMFLAGS_RATEADJUST | |
| AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | |
| AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY ); | |
hr = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, | |
initStreamFlags, | |
requestedSoundBufferDuration, | |
0, &mixFormat, nullptr); | |
assert(hr == S_OK); | |
IAudioRenderClient* audioRenderClient; | |
hr = audioClient->GetService(__uuidof(IAudioRenderClient), (LPVOID*)(&audioRenderClient)); | |
assert(hr == S_OK); | |
UINT32 bufferSizeInFrames; | |
hr = audioClient->GetBufferSize(&bufferSizeInFrames); | |
assert(hr == S_OK); | |
hr = audioClient->Start(); | |
assert(hr == S_OK); | |
int wavPlaybackSample = 0; | |
while (true) | |
{ | |
// Padding is how much valid data is queued up in the sound buffer | |
// if there's enough padding then we could skip writing more data | |
UINT32 bufferPadding; | |
hr = audioClient->GetCurrentPadding(&bufferPadding); | |
assert(hr == S_OK); | |
// How much padding we want our sound buffer to have after writing to it. | |
// Needs to be enough so that the playback doesn't reach garbage data | |
// but we get less latency the lower it is (i.e. how long does it take | |
// between pressing jump and hearing the sound effect) | |
// Try setting this to e.g. 1/250.f to hear what happens when | |
// we're not writing enough data to stay ahead of playback! | |
const float TARGET_BUFFER_PADDING_IN_SECONDS = 1/60.f; | |
UINT32 targetBufferPadding = UINT32(bufferSizeInFrames * TARGET_BUFFER_PADDING_IN_SECONDS); | |
UINT32 numFramesToWrite = targetBufferPadding - bufferPadding; | |
int16_t* buffer; | |
hr = audioRenderClient->GetBuffer(numFramesToWrite, (BYTE**)(&buffer)); | |
assert(hr == S_OK); | |
for (UINT32 frameIndex = 0; frameIndex < numFramesToWrite; ++frameIndex) | |
{ | |
*buffer++ = wavSamples[wavPlaybackSample++]; // left | |
*buffer++ = wavSamples[wavPlaybackSample++]; // right | |
wavPlaybackSample %= numWavSamples; // Loop if we reach end of wav file | |
} | |
hr = audioRenderClient->ReleaseBuffer(numFramesToWrite, 0); | |
assert(hr == S_OK); | |
// Get playback cursor position | |
// This is good for visualising playback and seeing the reading/writing in action! | |
IAudioClock* audioClock; | |
audioClient->GetService(__uuidof(IAudioClock), (LPVOID*)(&audioClock)); | |
UINT64 audioPlaybackFreq; | |
UINT64 audioPlaybackPos; | |
audioClock->GetFrequency(&audioPlaybackFreq); | |
audioClock->GetPosition(&audioPlaybackPos, 0); | |
audioClock->Release(); | |
// UINT64 audioPlaybackPosInSeconds = audioPlaybackPos/audioPlaybackFreq; | |
// UINT64 audioPlaybackPosInSamples = audioPlaybackPosInSeconds*mixFormat.nSamplesPerSec; | |
} | |
audioClient->Stop(); | |
audioClient->Release(); | |
audioRenderClient->Release(); | |
Win32FreeFileData(fileBytes); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm glad it helped! That's definitely a way more elegant solution, it seems like I really wasn't keeping the bigger picture in mind. I'll have to update this in the code I adapted, this solution is way more flexible when it comes to the amount of channels in the wave file.