Playing/Loading an external .WAV file from included files

poliver

Member
GM Version: Studio 2
Target Platform: Windows
Download: n/a
Links: n/a

Summary:
Load and play .WAV file from an included file

I've had a bit of trouble of figuring this out at first so thought it might be a good idea to leave this out in the open in case somebody's ever looking for a solution.
In short, GameMaker lets you load and playback included .WAV files from a buffer without the need of creating a "GameMaker Asset".
Why would you want that?

1. It lets you use audio_create_play_queue and audio_queue_sound functions in which audio files can be pushed into the queue asynchronosly.
2. For you audio nerds out there, it lets you manipulate the sound on per sample basis.
3. It makes your audio files accessible to the player from the included files folder (but you could encrypt them if wanted).

To make the .WAV file usable in the GameMaker we need to strip it of it's .WAV header.

wave-bytes.gif

A Header is everything that precedes 'sample 1' in the data subchunk.
Header sizes may differ depending on the software that was used when creating a .WAV file.



If you need to convert a different audio format to .WAV or want to change the sample rate I suggest using REAPER.




There are at least 2 ways of removing a header:

1. Removing the .WAV header using an external HEX editor.
Open up your .WAV file in the HEX editor. Find the "data" string. And remove everything preceding the data string, data string itself and 4 bytes following the data string. What you'll be left with would be just sample data.

cold.jpg

To playback your newly trimmed .WAV file add it to the project.

include.jpg

Load it into the buffer -
GML:
newAudioFile = buffer_load("newAudioFile.wav");
Create a buffer sound -
As parameters pass your buffer name
set buffer type to buffer_s16 (standard for audio)
set sample rate to .WAV files original files sample rate (44100, 22050 etc.) //must be same as sample rate in your wav file or file will be played back at a different speed
set the size of the buffer by using buffer_get_size()
and set the audio width to be the same as in your original .WAV file (mono, stereo).
GML:
newSound = audio_create_buffer_sound(newAudioFile, buffer_s16, 44100, 0, buffer_get_size(newAudioFile), audio_mono);
Play it like you would play a regular asset sound -
GML:
audio_play_sound(newSound, 100, false);



2. Removing the .WAV header in GameMaker.

Add your .WAV file straight to the project.

include.jpg

Load it into the buffer -
GML:
newAudioFile = buffer_load("newAudioFile.wav");
If you examine the buffer in the debugger, you'll see that it still has the header which we don't need. If you try to create a buffer sound from the file in the state it currently is you'll get a really harsh noise.

wav.jpg

Start a buffer seek
GML:
buffer_seek(newAudioFile , buffer_seek_start, 0);
We know that the sample data starts at the 5th byte after the "data" string.
To find the starting byte of the sample data we will run a for loop inside of which we'll be searching for the "data" string as we know that it is constant across different .WAV files.
The byte values of the "data" string are 100, 97, 116, 100. You can check that by examining the buffer in 1-byte hexadecimal mode.
We'll check every byte using buffer_peek() and upon hitting the right byte value move on to looking for next value.
If the next byte doesn't contain the right next value we'll revert to looking for the first value again till all the values are found in order.
Once the string is found since we know the offsets now we'll create an audio file from buffer using those offsets.
And we'll also break out of the loop.

GML:
var data_found = 0;
for (i = 0; i < buffer_get_size(newAudioFile ); i++) {
    if (data_found == 4)
        break;
    switch(data_found) {
        case 0:
            if(buffer_peek(newAudioFile , i, buffer_u8) == 100) { data_found++; } else { data_found := 0; }
            break;
        case 1:
            if(buffer_peek(newAudioFile , i, buffer_u8) == 97)  { data_found++; } else { data_found := 0; }
            break;
        case 2:
            if(buffer_peek(newAudioFile , i, buffer_u8) == 116) { data_found++; } else { data_found := 0; }
            break;
        case 3:
            if(buffer_peek(newAudioFile , i, buffer_u8) == 97) {
                data_found++;
                newSound = audio_create_buffer_sound(newAudioFile , buffer_s16, 44100, i + 5, buffer_get_size(newAudioFile) - i - 5, audio_mono);
            }
            else {
                data_found := 0;
            }
            break;
    }
}
The "i + 5" in the offset when creating is buffer sound is needed cause at the moment of finding the last character of the "data" string iterator is located on the "a" character. We know that samples start at the location of 4 bytes after the "data".
The "- i - 5" when specifying the size of the buffer for buffer sound is needed for same reason. Since samples start at i + 5 location of the original buffer, the new buffer needs to be i + 5 shorter than the original one.

After we play the sound

GML:
audio_play_sound(newSound, 100, false);
What you could do instead of creating the buffer sound in the for loop, upon finding the offset you could create another buffer and start dumping the clean trimmed data into it for later use.

And there you go. You've now loaded a .wav file into a buffer which you can manipulate any way you want to your hearts content, and from which you can create a buffer sound whenever you'd want to play it back.
 

Attachments

Last edited:

kupo15

Member
Thanks for this guide! There needs to be an update to your script because GM doesn't accept buffer_grow as a type which buffer_load() uses. So it needs to be copied into a buffer_fast type. I also refactored the code that suits my eye

GML:
function import_wav_trimmed(filename) {
   
    var buffer_init = buffer_load(filename);
    var buffer_size = buffer_get_size(buffer_init);
    var channels = buffer_peek(buffer_init,22,buffer_u8);
    var sampleRate = wav_get_sample_rate(buffer_init);

    // seek to the beginning
    buffer_seek(buffer_init,buffer_seek_start,0);
   
    var arr = [100,97,116,97];
    var arrIndex = 0;
    var offset = 0;
   
    // loop through each buffer byte
    for (var i=0;i<buffer_size;i++) {
       
        var arrValue = arr[arrIndex]
        var dataValue = buffer_peek(buffer_init,i,buffer_u8);
        var dataFound = (dataValue == arrValue);
       
        //if i >=25 && i <= 28
        db(string(i+1)+": "+string(dataValue))
       
        arrIndex = pick(0,arrIndex+1,dataFound);
       
        // found end of header
        if (arrIndex == array_length(arr)) {
           
            var offset = i+5;
            break;
            }
        }
       
    // use buffer fast instead
    buffer_size -= offset;
    var buffer = buffer_create(buffer_size,buffer_fast,1);
   
    buffer_copy(buffer_init,offset,buffer_size,buffer,0);
    buffer_delete(buffer_init);
       
    var channelType = audio_mono;

    if (channels == 2)
    channelType = audio_stereo;

    var snd = audio_create_buffer_sound(buffer,buffer_s16,sampleRate,0,buffer_size,channelType);
    return snd;
    }
GML:
function wav_get_sample_rate(buffer) {
    
    var _b_25 = buffer_peek(buffer,24,buffer_u8);
    var _b_26 = buffer_peek(buffer,25,buffer_u8);
    var _b_27 = buffer_peek(buffer,26,buffer_u8);
    var _b_28 = buffer_peek(buffer,27,buffer_u8);

    var _sr = (_b_28 << 24) | (_b_27 << 16) | (_b_26 << 8) | _b_25;
    return _sr;
    }
I'm trying to figure out how to convert the header information for Samples so you don't have to specify but having an issue with the conversion. I need to understand how to convert
1281 8700 to 44100 for this particular example
Help on this was provided by @fujj
 
Last edited:

FrostyCat

Redemption Seeker
I agree with @kupo15 that the next progression for this article is to properly parse the RIFF header and NOT blindly assume 44100 Hz 8-bit mono PCM. Though I do not have direct experience with loading WAV files, I do have a study on saving WAV files, and the saving code may offer clues as to how the reverse process could proceed.
 

Gradius

Member
Actually parsing the header isn't hard at all, so I don't think there's a specific need to 'strip it' instead of just reading it to determine it's size and then parsing the data properly according to it.

I made a rudementary one (that bothers parsing the whole thing including plenty of unnecessary info) for the sake of auto-generating a reverb effect (now thankfully unnecessary): https://pastebin.com/XLS9NNe4

This does mean it should handle different wave formats more gracefully.
 

kupo15

Member
Great, thanks @FrostyCat. I was able to get the sample rate with help in another thread I made related to this. I just updated my post to include that part of it

Actually parsing the header isn't hard at all, so I don't think there's a specific need to 'strip it' instead of just reading it to determine it's size and then parsing the data properly according to it.

I made a rudementary one (that bothers parsing the whole thing including plenty of unnecessary info) for the sake of auto-generating a reverb effect (now thankfully unnecessary): https://pastebin.com/XLS9NNe4

This does mean it should handle different wave formats more gracefully.
Nice, that's a good script list you are making there. We will need to strip the header at the end though otherwise playing the sound will try to play the header as data which won't sound good lol
 

JeffJ

Member
Thanks for this guide! There needs to be an update to your script because GM doesn't accept buffer_grow as a type which buffer_load() uses. So it needs to be copied into a buffer_fast type. I also refactored the code that suits my eye

GML:
function import_wav_trimmed(filename) {
  
    var buffer_init = buffer_load(filename);
    var buffer_size = buffer_get_size(buffer_init);
    var channels = buffer_peek(buffer_init,22,buffer_u8);
    var sampleRate = wav_get_sample_rate(buffer_init);

    // seek to the beginning
    buffer_seek(buffer_init,buffer_seek_start,0);
  
    var arr = [100,97,116,97];
    var arrIndex = 0;
    var offset = 0;
  
    // loop through each buffer byte
    for (var i=0;i<buffer_size;i++) {
      
        var arrValue = arr[arrIndex]
        var dataValue = buffer_peek(buffer_init,i,buffer_u8);
        var dataFound = (dataValue == arrValue);
      
        //if i >=25 && i <= 28
        db(string(i+1)+": "+string(dataValue))
      
        arrIndex = pick(0,arrIndex+1,dataFound);
      
        // found end of header
        if (arrIndex == array_length(arr)) {
          
            var offset = i+5;
            break;
            }
        }
      
    // use buffer fast instead
    buffer_size -= offset;
    var buffer = buffer_create(buffer_size,buffer_fast,1);
  
    buffer_copy(buffer_init,offset,buffer_size,buffer,0);
    buffer_delete(buffer_init);
      
    var channelType = audio_mono;

    if (channels == 2)
    channelType = audio_stereo;

    var snd = audio_create_buffer_sound(buffer,buffer_s16,sampleRate,0,buffer_size,channelType);
    return snd;
    }
GML:
function wav_get_sample_rate(buffer) {
   
    var _b_25 = buffer_peek(buffer,24,buffer_u8);
    var _b_26 = buffer_peek(buffer,25,buffer_u8);
    var _b_27 = buffer_peek(buffer,26,buffer_u8);
    var _b_28 = buffer_peek(buffer,27,buffer_u8);

    var _sr = (_b_28 << 24) | (_b_27 << 16) | (_b_26 << 8) | _b_25;
    return _sr;
    }
I'm trying to figure out how to convert the header information for Samples so you don't have to specify but having an issue with the conversion. I need to understand how to convert
1281 8700 to 44100 for this particular example
Help on this was provided by @fujj
Sorry for the necro, but I'm having some trouble getting buffer imported wavs to play consistently and this looks useful, but there's a few parts of this I can't quite decipher the intention of:

First, "db(string(i+1)+": "+string(dataValue))" - what is db supposed to reference in this context?
Second, "arrIndex = pick(0,arrIndex+1,dataFound);" - is the pick() function something defined in your own project or is it supposed to be pseudo code for something else?

Thirdly, if I'm reading this correctly, it should be able to dynamically read the wavs data and alter the various attributes on load, but for some reason, I can't get it to consistently load all wavs; some are successful, others are not - what is the typical culprit of this?

Sorry, just now trying to learn about buffers and wav headers and such, still a bit confused.
 

kupo15

Member
Sorry for the necro, but I'm having some trouble getting buffer imported wavs to play consistently and this looks useful, but there's a few parts of this I can't quite decipher the intention of:

First, "db(string(i+1)+": "+string(dataValue))" - what is db supposed to reference in this context?
Second, "arrIndex = pick(0,arrIndex+1,dataFound);" - is the pick() function something defined in your own project or is it supposed to be pseudo code for something else?

Thirdly, if I'm reading this correctly, it should be able to dynamically read the wavs data and alter the various attributes on load, but for some reason, I can't get it to consistently load all wavs; some are successful, others are not - what is the typical culprit of this?

Sorry, just now trying to learn about buffers and wav headers and such, still a bit confused.
Hi, sorry about that, I forgot that I did use custom scripts I created
  • db() is a macro I use for show_debug_message()
  • pick(val1,val2,condition) is a custom code that returns the val1 if the the condition is false and val2 if true. It's a short hand way for me to do certain types of if elses

Thirdly, if I'm reading this correctly, it should be able to dynamically read the wavs data and alter the various attributes on load, but for some reason, I can't get it to consistently load all wavs; some are successful, others are not - what is the typical culprit of this?
I just noticed this too yesterday. I get static with a 41k sample and had to reformat down to 48k I believe to get it to work. I need to look into why all sample rates are not working as expected
 
Last edited:

kupo15

Member
I discovered another issue that needs to be addressed with this Tutorial, can someone help how I'm am to fix this issue? I notice that when I'm not using a 16 bit signed PCM, I get white noise, corrupted sounds from this import method, but 16 bit wav files do work. My assumption is the file size is different or the header information is different so I'm not parsing it out correctly.

I need a way to determine the bit type and adjust the parsing so I don't get white noise

1709581576745.png
 

FrostyCat

Redemption Seeker
I need a way to determine the bit type and adjust the parsing so I don't get white noise
Among other information, the header specifies how many bits are in each sample. That information is found at bytes 35-36 (source). The tutorial threw it all away without even a quick glance at it.

This is why you can't just strip the header from the file as described in the tutorial's current iteration. You have to read the specs from the buffer, then use that to set the properties and find the PCM section of the WAV file.

I'm not so sure about the ongoing relevance of this article now that the official FMOD extension is out, however. It might be a good case study in binary file handling, but that's about it.
 
Last edited:
Top