Croaking Kero

Load WAV Files with the C Standard Library

This tutorial is also available as a video. In this tutorial I’ll show you how to load and play single channel, 44100Hz, 16-bit .wav files with the C standard library. This code can easily be adapted to load and play multi-channel .wav files with other frequencies and bit depths. For convenience, here's a single channel, 44100Hz, 16-bit .wav file you can use to test the code: cheers.wav Note: I'm doing this in a Win32 program, linking to WinMM and using that to play the wav sound samples, but no extra linking is required for the .wav loading code itself. I've dimmed the code not directly relevant to the tutorial. Note: I’m compiling this code with GCC. Note: Click any of the hyperlinked words to visit the documentation page for them. main.c #include <stdlib.h> #include <stdint.h> #include <stdbool.h> #include <stdio.h> #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <mmsystem.h> #define PRINT_ERROR(a, args...) printf("ERROR %s() %s Line %d: " a "\n", __FUNCTION__, __FILE__, __LINE__, ##args); #if RAND_MAX == 32767 #define rand32() ((rand()%lt%lt16) + (rand()%lt%lt1) + (rand()&1)) #else #define rand32() rand() #endif typedef struct { uint32_t samples; int16_t *data; } sound_t; sound_t hello; bool LoadWav(const char *filename, sound_t *sound); #define SAMPLING_RATE 44100 #define CHUNK_SIZE 2000 HWAVEOUT wave_out; WAVEHDR header[2] = {0}; int16_t chunks[2][CHUNK_SIZE] = {0}; bool chunk_swap = false; int16_t *to; bool quit = false; void CALLBACK WaveOutProc(HWAVEOUT wave_out_handle, UINT message, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { { WAVEFORMATEX format = { .wFormatTag = WAVE_FORMAT_PCM, .nChannels = 1, .nSamplesPerSec = SAMPLING_RATE, .wBitsPerSample = 16, .cbSize = 0, }; format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; if(waveOutOpen(&wave_out, WAVE_MAPPER, &format, (DWORD_PTR)WaveOutProc, (DWORD_PTR)NULL, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutOpen failed"); return -1; } } if(waveOutSetVolume(wave_out, 0xFFFFFFFF) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutSetVolume failed"); return -1; } for(int i = 0; i < 2; ++i) { header[i].lpData = (CHAR*)chunks[i]; header[i].dwBufferLength = CHUNK_SIZE * 2; if(waveOutPrepareHeader(wave_out, &header[i], sizeof(header[i])) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutPrepareHeader failed"); return -1; } if(waveOutWrite(wave_out, &header[i], sizeof(header[i])) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutWrite[%d] failed", i); return -1; } } if(!LoadWav("cheers.wav", &hello)) { PRINT_ERROR("Failed to load cheers.wav"); return -1; } while(!quit); return 0; } void CALLBACK WaveOutProc(HWAVEOUT wave_out_handle, UINT message, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2) { static uint32_t sound_position = 0; switch(message) { case WOM_CLOSE: { printf("WOM_CLOSE\n"); } break; case WOM_OPEN: { printf("WOM_OPEN\n"); } break; case WOM_DONE: { printf("WOM_DONE\n"); to = chunks[chunk_swap]; for(int j = 0; j < CHUNK_SIZE; ++j) { if(sound_position < hello.samples) { *(to++) = hello.data[sound_position++]; } else { quit = true; *(to++) = 0; } } if(waveOutWrite(wave_out, &header[chunk_swap], sizeof(header[chunk_swap])) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutWrite failed\n"); } chunk_swap = !chunk_swap; } break; } } // Loads ONLY 16-bit 1-channel PCM .WAV files. Allocates sound->data and fills with the pcm data. Fills sound->samples with the number of ELEMENTS in sound->data. EG for 2-bytes per sample single channel, sound->samples = HALF of the number of bytes in sound->data. bool LoadWav(const char *filename, sound_t *sound) { bool return_value = true; FILE *file; char magic[4]; int32_t filesize; int32_t format_length; // 16 int16_t format_type; // 1 = PCM int16_t num_channels; // 1 int32_t sample_rate; // 44100 int32_t bytes_per_second; // sample_rate * num_channels * bits_per_sample / 8 int16_t block_align; // num_channels * bits_per_sample / 8 int16_t bits_per_sample; // 16 int32_t data_size; file = fopen(filename, "rb"); if(file == NULL) { PRINT_ERROR("%s: Failed to open file", filename); return false; } fread(magic, 1, 4, file); if(magic[0] != 'R' || magic[1] != 'I' || magic[2] != 'F' || magic[3] != 'F') { PRINT_ERROR("%s First 4 bytes should be \"RIFF\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(&filesize, 4, 1, file); fread(magic, 1, 4, file); if(magic[0] != 'W' || magic[1] != 'A' || magic[2] != 'V' || magic[3] != 'E') { PRINT_ERROR("%s 4 bytes should be \"WAVE\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(magic, 1, 4, file); if(magic[0] != 'f' || magic[1] != 'm' || magic[2] != 't' || magic[3] != ' ') { PRINT_ERROR("%s 4 bytes should be \"fmt/0\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(&format_length, 4, 1, file); fread(&format_type, 2, 1, file); if(format_type != 1) { PRINT_ERROR("%s format type should be 1, is %d", filename, format_type); return_value = false; goto CLOSE_FILE; } fread(&num_channels, 2, 1, file); if(num_channels != 1) { PRINT_ERROR("%s Number of channels should be 1, is %d", filename, num_channels); return_value = false; goto CLOSE_FILE; } fread(&sample_rate, 4, 1, file); if(sample_rate != 44100) { PRINT_ERROR("%s Sample rate should be 44100, is %d", filename, sample_rate); return_value = false; goto CLOSE_FILE; } fread(&bytes_per_second, 4, 1, file); fread(&block_align, 2, 1, file); fread(&bits_per_sample, 2, 1, file); if(bits_per_sample != 16) { PRINT_ERROR("%s bits per sample should be 16, is %d", filename, bits_per_sample); return_value = false; goto CLOSE_FILE; } fread(magic, 1, 4, file); if(magic[0] != 'd' || magic[1] != 'a' || magic[2] != 't' || magic[3] != 'a') { PRINT_ERROR("%s 4 bytes should be \"data\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(&data_size, 4, 1, file); sound->data = malloc(data_size); if(sound->data == NULL) { PRINT_ERROR("%s Failed to allocate %d bytes for data", filename, data_size); return_value = false; goto CLOSE_FILE; } if(fread(sound->data, 1, data_size, file) != data_size) { PRINT_ERROR("%s Failed to read data bytes", filename); return_value = false; free(sound->data); goto CLOSE_FILE; } sound->samples = data_size / 2; CLOSE_FILE: fclose(file); return return_value; } build.bat gcc main.c -lwinmm

Code Walkthrough

#include <stdio.h> We need to include stdio to read files. typedef struct { uint32_t samples; int16_t *data; } sound_t; sound_t hello; I’ve created this sound structure to hold the sound samples and sample count. if(!LoadWav("cheers.wav", &hello)) { PRINT_ERROR("Failed to load cheers.wav"); return -1; } In our main program we call the “LoadWav” function to load the sound file, passing it the filename and sound structure to use. bool LoadWav(const char *filename, sound_t *sound) { bool return_value = true; FILE *file; char magic[4]; int32_t filesize; int32_t format_length; // 16 int16_t format_type; // 1 = PCM int16_t num_channels; // 1 int32_t sample_rate; // 44100 int32_t bytes_per_second; // sample_rate * num_channels * bits_per_sample / 8 int16_t block_align; // num_channels * bits_per_sample / 8 int16_t bits_per_sample; // 16 int32_t data_size; First we declare the return value variable, file pointer, and all the data we’re going to load from the file. Several of these are unnecessary but I included them so you can analyze the wave files you’re loading. file = fopen(filename, "rb"); if(file == NULL) { PRINT_ERROR("%s: Failed to open file", filename); return false; } We open the file with fopen, passing in the filename and “read binary” mode. If fopen failed, file will be NULL in which case we print the error and return false. fread(magic, 1, 4, file); if(magic[0] != 'R' || magic[1] != 'I' || magic[2] != 'F' || magic[3] != 'F') { PRINT_ERROR("%s First 4 bytes should be \"RIFF\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } Next we use fread, giving it the address to put what it reads, the size of each element, the number of elements to read and the file to read from. The first four bytes of a wav file are the letters “RIFF”. If they’re not, we print the error, set the return value to false and goto CLOSE_FILE, which is at the end of the function and closes the file and returns. Most of the error conditions will mimic this behaviour so I’ll skip over it from now on. fread(&filesize, 4, 1, file); fread(magic, 1, 4, file); if(magic[0] != 'W' || magic[1] != 'A' || magic[2] != 'V' || magic[3] != 'E') { PRINT_ERROR("%s 4 bytes should be \"WAVE\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(magic, 1, 4, file); if(magic[0] != 'f' || magic[1] != 'm' || magic[2] != 't' || magic[3] != ' ') { PRINT_ERROR("%s 4 bytes should be \"fmt/0\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } Next we read the file size, the letters “WAVE” and “fmt ”, which marks the beginning of the format data. fread(&format_length, 4, 1, file); fread(&format_type, 2, 1, file); if(format_type != 1) { PRINT_ERROR("%s format type should be 1, is %d", filename, format_type); return_value = false; goto CLOSE_FILE; } fread(&num_channels, 2, 1, file); if(num_channels != 1) { PRINT_ERROR("%s Number of channels should be 1, is %d", filename, num_channels); return_value = false; goto CLOSE_FILE; } fread(&sample_rate, 4, 1, file); if(sample_rate != 44100) { PRINT_ERROR("%s Sample rate should be 44100, is %d", filename, sample_rate); return_value = false; goto CLOSE_FILE; } fread(&bytes_per_second, 4, 1, file); fread(&block_align, 2, 1, file); fread(&bits_per_sample, 2, 1, file); if(bits_per_sample != 16) { PRINT_ERROR("%s bits per sample should be 16, is %d", filename, bits_per_sample); return_value = false; goto CLOSE_FILE; } The format length is always 16, and format type should always be 1 for PCM. The number of channels should be 1, sample rate should be 44100, then we read bytes per second, block align and bits per sample, which should be 16. fread(magic, 1, 4, file); if(magic[0] != 'd' || magic[1] != 'a' || magic[2] != 't' || magic[3] != 'a') { PRINT_ERROR("%s 4 bytes should be \"data\", are \"%4s\"", filename, magic); return_value = false; goto CLOSE_FILE; } fread(&data_size, 4, 1, file); sound->data = malloc(data_size); if(sound->data == NULL) { PRINT_ERROR("%s Failed to allocate %d bytes for data", filename, data_size); return_value = false; goto CLOSE_FILE; } if(fread(sound->data, 1, data_size, file) != data_size) { PRINT_ERROR("%s Failed to read data bytes", filename); return_value = false; free(sound->data); goto CLOSE_FILE; } Next are the letters “data” to mark the data chunk. We read the data size in bytes and allocate enough memory with malloc. Now we can finally read the sound data. Fread always returns the number of bytes read, so we check it’s correct and if not, we free our allocated memory and otherwise handle the error as usual. sound->samples = data_size / 2; CLOSE_FILE: fclose(file); return return_value; } Lastly we set our sound structure’s sample count to half the data size, since each 16-bit sample is two bytes, then close the file and return. On a successful read we wouldn’t have hit any error conditions and so return_value will still be set to true since its declaration, so whenever we call this function we can just check whether it returns true or false. case WOM_DONE: { printf("WOM_DONE\n"); to = chunks[chunk_swap]; for(int j = 0; j < CHUNK_SIZE; ++j) { if(sound_position < hello.samples) { *(to++) = hello.data[sound_position++]; } else { quit = true; *(to++) = 0; } } if(waveOutWrite(wave_out, &header[chunk_swap], sizeof(header[chunk_swap])) != MMSYSERR_NOERROR) { PRINT_ERROR("waveOutWrite failed\n"); } chunk_swap = !chunk_swap; } break; That’s all the loading code, now to play the sound I’m just filling the sound chunks we’re passing to WinMM with the sound samples instead of generating sine wave samples. I’m using this sound_position variable to iterate over the samples and when they’ve all been used I just tell the program to quit. That’s how to load and play .wav files in C. In my next tutorials I’ll show you how to do precise timing in C on Windows and soon I’ll be starting a series of tutorials on how to make a complete small game in C.
If you've got questions about any of the code feel free to e-mail me or comment on the youtube video. I'll try to answer them, or someone else might come along and help you out. If you've got any extra tips about how this code can be better or just more useful info about the code, let me know so I can update the tutorial. Thanks to Froggie717 for criticisms and correcting errors in this tutorial. Cheers.