I've been working on a project that requires the ability to generate uncompressed PCM audio. Luckily, C lets us do this very easily just using struct
s and fwrite
.
Although there are a lot of different digital audio formats out there, I decided to use the Waveform Audio File Format (WAVE) over the Audio Interchange File Format (or AIFF), since it's very well-documented and has been around a while. In fact, its original specification was written back in 1991 by IBM and Microsoft in a document that you can read here. Both formats are widely-supported (since they're based on the ancient IFF specification) and can store PCM audio, but WAVE seems to be used more since Windows supports it out of the box. (Recent versions of Windows probably support AIFF too, but I neither know nor care. Sorry.)
Implementing the headerI decided to put everything in the header into nested struct
s in order to follow the specifications as closely as possible. I also used typedef
s and field names straight out of the documentation in order to keep things orderly.
A note about byte orderwave.h... #define WAVE_FORMAT_PCM ((WORD)(0x0001)) typedef unsigned short WORD; #if LONG_BIT == 32 typedef unsigned long DWORD; #else typedef unsigned int DWORD; #endif typedef DWORD FOURCC; typedef FOURCC CKID; typedef DWORD CKSIZE; struct WAVE { CKID ckID; CKSIZE ckSize; FOURCC formType; struct ckFmt { CKID ckID; CKSIZE ckSize; WORD wFormatTag; WORD wChannels; DWORD dwSamplesPerSec; DWORD dwAvgBytesPerSec; WORD wBlockAlign; WORD wBitsPerSample; } ckFmt; struct ckWaveData { CKID ckID; CKSIZE ckSize; } ckWaveData; }; ...
The primary drawback to using the WAVE format is that its header data uses a combination of both big- and little-endian data fields, which means we'll have to be able to specify the correct byte order for each field. I wrote a few functions based on this IBM article that let us detect the host byte order and force a different one if necessary:
endian.h/* * Apparently, LITTLE_ENDIAN and BIG_ENDIAN are already defined in my macOS * environment, so we'll remove their definitions just in case. */ #ifdef LITTLE_ENDIAN #undef LITTLE_ENDIAN #endif #ifdef BIG_ENDIAN #undef BIG_ENDIAN #endif #define LITTLE_ENDIAN 0 #define BIG_ENDIAN 1 ...
endian.c#include "endian.h" #include "wave.h" /* * [U]se a character pointer to the bytes of an int and then check its first * byte to see if it is 0 or 1. */ int endian(void) { int i = 1; char *p = (char *)&i; if (p[0] == 1) return LITTLE_ENDIAN; else return BIG_ENDIAN; } WORD htobw(WORD w) { if (endian() == LITTLE_ENDIAN) return (WORD)(((w & UCHAR_MAX) << 8) + ((w >> 8) & UCHAR_MAX)); return w; } DWORD htobdw(DWORD dw) { if (endian() == LITTLE_ENDIAN) return (DWORD)(((dw & 0xff) << 24) | ((dw & 0xff00) << 8) | ((dw & 0xff0000) >> 8) | ((dw & 0xff000000) >> 24)); return dw; } WORD htolw(WORD w) { if (endian() == BIG_ENDIAN) return (WORD)(((w & UCHAR_MAX) << 8) + ((w >> 8) & UCHAR_MAX)); return w; } DWORD htoldw(DWORD dw) { if (endian() == BIG_ENDIAN) return (DWORD)(((dw & 0xff) << 24) | ((dw & 0xff00) << 8) | ((dw & 0xff0000) >> 8) | ((dw & 0xff000000) >> 24)); return dw; }
Now that we have a header that can store information and some helper functions to deal with byte order, it's time to write the WAVE header into a file. The following function, wave_write()
, writes the audio sample data pointed to by *ptr
to the file pointed to by *stream
. Alternatively, passing a null pointer just writes the header.
wave.c#include "wave.h" void wave_write(void *ptr, WORD wChannels, DWORD dwSamplesPerSec, WORD wBitsPerSample, size_t nitems, FILE *stream) { struct WAVE wave; wave.ckID = htobdw(0x52494646); wave.ckSize = htoldw((CKSIZE)nitems + 36); wave.formType = htobdw(0x57415645); wave.ckFmt.ckID = htobdw(0x666d7420); wave.ckFmt.ckSize = 16; wave.ckFmt.wFormatTag = htolw(WAVE_FORMAT_PCM); wave.ckFmt.wChannels = htolw(wChannels); wave.ckFmt.dwSamplesPerSec = htolw(dwSamplesPerSec); wave.ckFmt.dwAvgBytesPerSec = htoldw(wChannels * dwSamplesPerSec * (wBitsPerSample / 8)); wave.ckFmt.wBlockAlign = htolw(wChannels * (wBitsPerSample / 8)); wave.ckFmt.wBitsPerSample = htolw(wBitsPerS wave.ckWaveData.ckID = htobdw(0x64617461); wave.ckWaveData.ckSize = htoldw((CKSIZE)nitems); fwrite(&wave, sizeof(struct WAVE), 1, stream); if (ptr != NULL) fwrite(ptr, sizeof(WORD), nitems, stream); }