c++audiowavopenal

Loading a .WAV file for OpenAL


I am trying to load .WAV files to be played with OpenAL. I am following an example I found on the internet, but it is acting strangely. Here is the code:

struct RIFF_Header {
    char chunkID[4];
    long chunkSize;//size not including chunkSize or chunkID
    char format[4];
};

/*
 * Struct to hold fmt subchunk data for WAVE files.
 */
struct WAVE_Format {
    char subChunkID[4];
    long subChunkSize;
    short audioFormat;
    short numChannels;
    long sampleRate;
    long byteRate;
    short blockAlign;
    short bitsPerSample;
};

/*
* Struct to hold the data of the wave file
*/
struct WAVE_Data {
    char subChunkID[4]; //should contain the word data
    long subChunk2Size; //Stores the size of the data block
};

bool loadWavFile(std::string filename, ALuint* buffer,
                 ALsizei* size, ALsizei* frequency,
                 ALenum* format) {
    //Local Declarations
    FILE* soundFile = NULL;
    WAVE_Format wave_format;
    RIFF_Header riff_header;
    WAVE_Data wave_data;
    unsigned char* data;

    *size = wave_data.subChunk2Size;
    *frequency = wave_format.sampleRate;
    if (wave_format.numChannels == 1) {
        if (wave_format.bitsPerSample == 8 )
            *format = AL_FORMAT_MONO8;
        else if (wave_format.bitsPerSample == 16)
            *format = AL_FORMAT_MONO16;
    } else if (wave_format.numChannels == 2) {
        if (wave_format.bitsPerSample == 8 )
            *format = AL_FORMAT_STEREO8;
        else if (wave_format.bitsPerSample == 16)
            *format = AL_FORMAT_STEREO16;
    }

    try {
        soundFile = fopen(filename.c_str(), "rb");
        if (!soundFile)
            throw (filename);

        // Read in the first chunk into the struct
        fread(&riff_header, sizeof(RIFF_Header), 1, soundFile);

        //check for RIFF and WAVE tag in memeory
        if ((riff_header.chunkID[0] != 'R' ||
             riff_header.chunkID[1] != 'I' ||
             riff_header.chunkID[2] != 'F' ||
             riff_header.chunkID[3] != 'F') ||
            (riff_header.format[0] != 'W' ||
             riff_header.format[1] != 'A' ||
             riff_header.format[2] != 'V' ||
             riff_header.format[3] != 'E'))
            throw ("Invalid RIFF or WAVE Header");

        //Read in the 2nd chunk for the wave info
        fread(&wave_format, sizeof(WAVE_Format), 1, soundFile);
        //check for fmt tag in memory
        if (wave_format.subChunkID[0] != 'f' ||
            wave_format.subChunkID[1] != 'm' ||
            wave_format.subChunkID[2] != 't' ||
            wave_format.subChunkID[3] != ' ')
            throw ("Invalid Wave Format");

        //check for extra parameters;
        if (wave_format.subChunkSize > 16)
            fseek(soundFile, sizeof(short), SEEK_CUR);

        //Read in the the last byte of data before the sound file
        fread(&wave_data, sizeof(WAVE_Data), 1, soundFile);

        //check for data tag in memory
        if (wave_data.subChunkID[0] != 'd' ||
            wave_data.subChunkID[1] != 'a' ||
            wave_data.subChunkID[2] != 't' ||
            wave_data.subChunkID[3] != 'a')
            throw ("Invalid data header");

        //Allocate memory for data
        data = new unsigned char[wave_data.subChunk2Size];

        // Read in the sound data into the soundData variable
        if (!fread(data, wave_data.subChunk2Size, 1, soundFile))
            throw ("error loading WAVE data into struct!");

        //Now we set the variables that we passed in with the
        //data from the structs
        *size = wave_data.subChunk2Size;
        *frequency = wave_format.sampleRate;
        //The format is worked out by looking at the number of
        //channels and the bits per sample.
        if (wave_format.numChannels == 1) {
            if (wave_format.bitsPerSample == 8 )
                *format = AL_FORMAT_MONO8;
            else if (wave_format.bitsPerSample == 16)
                *format = AL_FORMAT_MONO16;
        } else if (wave_format.numChannels == 2) {
            if (wave_format.bitsPerSample == 8 )
                *format = AL_FORMAT_STEREO8;
            else if (wave_format.bitsPerSample == 16)
                *format = AL_FORMAT_STEREO16;
        }
        //create our openAL buffer and check for success
        alGenBuffers(2, buffer);
        if(alGetError() != AL_NO_ERROR) {
            std::cerr << alGetError() << std::endl;
        }
        //now we put our data into the openAL buffer and
        //check for success
        alBufferData(*buffer, *format, (void*)data,
                     *size, *frequency);
        if(alGetError() != AL_NO_ERROR) {
            std::cerr << alGetError() << std::endl;
        }
        //clean up and return true if successful
        fclose(soundFile);
        delete data;
        return true;
    } catch(const char* error) {
        //our catch statement for if we throw a string
        std::cerr << error << " : trying to load "
        << filename << std::endl;
        //clean up memory if wave loading fails
        if (soundFile != NULL)
            fclose(soundFile);
        //return false to indicate the failure to load wave
        delete data;
        return false;
    }
}

int main() {
    ALuint buffer, source;
    ALint state;
    ALsizei size;
    ALsizei frequency;
    ALenum format;

    ALCcontext *context;
    ALCdevice *device;

    device = alcOpenDevice(nullptr);
    if (device == NULL)
    {
        cerr << "Error finding default Audio Output Device" << endl;
    }

    context = alcCreateContext(device,NULL);

    alcMakeContextCurrent(context);

    alGetError();

    loadWavFile("test.wav", &buffer, &size, &frequency, &format);

    alGenSources(1, &source);
    alSourcei(source, AL_BUFFER, buffer);

    // Play
    alSourcePlay(source);

    // Wait for the song to complete
    do {
        alGetSourcei(source, AL_SOURCE_STATE, &state);
    } while (state == AL_PLAYING);

    // Clean up sources and buffers
    alDeleteSources(1, &source);
    alDeleteBuffers(1, &buffer);
    return 0;
}

I have several WAV files both around 50kb. They load and play just fine. However, when I try to load a whole song (yes, I verified the file was correctly formatted using VLC Media Player and MusicBee) it returns 'Invalid data header : trying to load test.wav', which is caused by this chunk right here:

if (wave_data.subChunkID[0] != 'd' ||
wave_data.subChunkID[1] != 'a' ||
wave_data.subChunkID[2] != 't' ||
wave_data.subChunkID[3] != 'a')
throw ("Invalid data header");

I suspect that it is something size-related that is throwing off that header, as it seems that things only under 1000kb work (haven't totally tested that, its hard to find perfectly sized sound files floating around on my computer and on the internet). That is only a guess though, I really haven't a clue what is going on. Help is appreciated!


Solution

  • This question is a bit old, yet unanswered. I happen to have written a WAV file loader, and I've stumbled upon the exact same issue as you did.

    As a matter of fact, the "data" part is not guaranteed to exist where you expect it to be. There are other blocks that may be specified, like in my case a "cue" one. This is kind of a really hidden information, and I've spent much time trying to find this: https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#cue

    In my case, I then simply check if there is a "cue" part and simply ignore its data. I do not (yet) check for other block types, since I do not have any testing material for it.

    In C++ code, this does the job:

    // std::ifstream file("...", std::ios_base::in | std::ios_base::binary);
    // std::array<uint8_t, 4> bytes {};
    
    file.read(reinterpret_cast<char*>(bytes.data()), 4); // Supposed to be "data"
    
    // A "cue " field can be specified; if so, the given amount of bytes will be ignored
    if (bytes[0] == 'c' && bytes[1] == 'u' && bytes[2] == 'e' && bytes[3] == ' ') {
      file.read(reinterpret_cast<char*>(bytes.data()), 4); // Cue data size
    
      const uint32_t cueDataSize = fromLittleEndian(bytes);
      file.ignore(cueDataSize);
    
      file.read(reinterpret_cast<char*>(bytes.data()), 4); // "data"
    }
    
    // A LIST segment may be specified; see the edit below
    
    // "data" is now properly expected
    if (bytes[0] != 'd' && bytes[1] != 'a' && bytes[2] != 't' && bytes[3] != 'a')
      return false;
    

    Edit: I now have a "LIST" tag too, which is to be expected. This can be ignored the same way as the cue:

    if (bytes[0] == 'L' && bytes[1] == 'I' && bytes[2] == 'S' && bytes[3] == 'T') {
      file.read(reinterpret_cast<char*>(bytes.data()), 4); // List data size
    
      const uint32_t listDataSize = fromLittleEndian(bytes);
      file.ignore(listDataSize);
    
      file.read(reinterpret_cast<char*>(bytes.data()), 4);
    }