Generating audio with pure C
28 August 2022 → 8 September 2022

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 structs and fwrite.

Choosing a format

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 header

I decided to put everything in the header into nested structs in order to follow the specifications as closely as possible. I also used typedefs and field names straight out of the documentation in order to keep things orderly.

wave.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; }; ...
A note about byte order

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; }
Putting it all together

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); }