Search code examples
cfilestructbytewav

Handling multiple byte lengths within a WAV struct


I'm writing a .wav PCM file header in C, but one part I'm still stumped on handling is the bits per sample format parameter of a file. Right now I've only added support for 8-bit and 16-bit PCMs (storing audio sample data in a int16_t* or int8_t* byteArray), but it would be nice to be able to read the sample size of a file before loading its data into memory, and writing a bunch of functions to handle every single byte size (writeToWav8(), writeToWav16(), writeToWav32()) feels incredibly naive.

The only solutions to this I could think of were

  1. Using a fixed array of 1-byte integers and finding a way to dynamically "pack" larger samples into the array (e.g, fitting 01000110 into 0100, 0110), which I tried earlier but ultimately ended up struggling with since it kept causing some kind of precision loss that I couldn't find the root cause of (additionally considering I'd have to write different many handlers for different byte sizes anyway)
  2. Restricting the program to only being able to read/write 8, 16, and 32-bit PCMs, which feels kind of like a cop-out.

Is there a way to more cleanly handle this issue?

The struct looks like this right now:

typedef struct RiffHeader {
    // basic formatting info for the file, not relevant
} RIFF_CHUNK;

typedef struct FmtHeader {
    uint32_t Subchunk1ID;
    // all of these determine how to read the audio
    uint32_t Subchunk1Size;
    uint16_t AudioFormat;
    uint16_t NumChannels;
    uint32_t SampleRate;
    uint32_t ByteRate;
    uint16_t BlockAlign;
    uint16_t BitsPerSample; // bits per sample, determines how large each bit is in the data array
} FMT_CHUNK;

typedef struct DataChunk8 {
    uint32_t Subchunk2ID;
    uint32_t Subchunk2Size;
    int8_t *byteArray; // pointer to start of data
    // data size is equal to subchunk2size
} DATA_CHUNK_8;

typedef struct DataChunk16 {
    uint32_t Subchunk2ID;
    uint32_t Subchunk2Size;
    int16_t *byteArray; // pointer to start of data
    // data size is equal to subchunk2size

} DATA_CHUNK_16;

typedef struct WaveFile8 {
    RIFF_CHUNK RIFF;
    FMT_CHUNK  FMT;
    DATA_CHUNK_8 DATA;
} WAV_FILE_8;

typedef struct WaveFile_16 {
    RIFF_CHUNK RIFF;
    FMT_CHUNK  FMT;
    DATA_CHUNK_16 DATA;
} WAV_FILE_16;

followed by a bunch of helper functions that input and/or output WAV_FILE_8* or WAV_FILE_16*

*Additionally, I'm kind of stumped on file reading, since I'm currently using two functions:

WAV_FILE_8* readFromFile8(const char* path)
WAV_FILE_16* readFromFile16(const char* path)

which are intended to do what you'd expect, but I'm not sure how to determine and decide to return the appropriately-sized WAV (and use the appropriate function) when reading from an unknown file.


Solution

  • You can simply use flexible array member for the data part. For the remaining part there's a lot of duplication in your code so just put them in a tagged union

    typedef struct DataChunk {
        uint32_t Subchunk2ID;
        uint32_t Subchunk2Size;
        union {
            int8_t byteArray[];
            int16_t byteArray[];
        };
    } DATA_CHUNK;
    
    typedef struct WaveFile {
        RIFF_CHUNK RIFF;
        FMT_CHUNK  FMT;
        DATA_CHUNK DATA;
    } WAV_FILE;
    

    This feature was common before it even entered the C standard. For example in GCC it's array with zero length at the end of the struct, and in MSVC you can't have array with zero length so they used array with size 1 instead. I've also made use the anonymous struct feature to avoid another unnecessary field reference

    When allocating the struct you'll need to specify enough memory for the whole header plus data

    // Allocate memory for the header
    WAV_FILE *wav = malloc(sizeof(*wav));
    
    // Read the header
    if (fread(wav, sizeof(WAV_FILE), 1, inputfile) == 1) {
        // Resize the struct to make room for the data
        WAV_FILE *newWav = realloc(sizeof(*wav) + wav->DATA.Subchunk2Size);
        if (newWav) {
            wav = newWav;
            // Do something with the data
        }
    }
    

    I'm not a language lawyer so I'm not sure if the flexible array member is in a union or nested struct is allowed or not. But if it's not then you can easily regroup the structs to move the array to the end of the outermost struct, for example like this

    typedef struct DataChunk {
        uint32_t Subchunk2ID;
        uint32_t Subchunk2Size;
    } DATA_CHUNK_HEADER;
    
    typedef struct WaveFile {
        RIFF_CHUNK        RIFF;
        FMT_CHUNK         FMT;
        DATA_CHUNK_HEADER DATA_HEADER;
        union {
            int8_t byteArray[];
            int16_t byteArray[];
        };
    } WAV_FILE;
    
    // or
    
    typedef union WaveFile2 {
        struct {
            RIFF_CHUNK        RIFF;
            FMT_CHUNK         FMT;
            DATA_CHUNK_HEADER DATA_HEADER;
            int8_t            byteArray[];
        };
        struct {
            RIFF_CHUNK        RIFF;
            FMT_CHUNK         FMT;
            DATA_CHUNK_HEADER DATA_HEADER;
            int16_t           byteArray[];
        };
    } WAV_FILE2;
    

    Alternatively you can remove the int8_t *byteArray; completely and use only int16_t *byteArray; which can be cast to char* or int8_t* when the file is a 8-bit one without violating the aliasing rule

    Of course you also need to take care of the endianness as nielsen commented