godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Easily load external audio (WAV) files at run-time #732

Open Gianclgar opened 4 years ago

Gianclgar commented 4 years ago

Describe the project you are working on: A game able to load an play with external audio files at runtime.

Describe the problem or limitation you are having in your project: I'm opening the file this way:

file.open(filepath, file.READ)
var buffer = file.get_buffer(file.get_len())
#put data into audiostreamsample
var stream = AudioStreamSample.new()
stream.data = buffer

The problem I'm having is that this method adds 44 bytes of data at the beginning of the audio stream.

Seems to be the WAV header. After a lot of research this can be figured and find a way to remove those first 44 bytes, but it seems that the WAV header could be bigger in some files. So far I've not been able to find a way to "detect" that header size and remove it so only the audio data gets in the stream.

I don't know if this is the right way to do it, is the only one I've found so far looking at Godot documentation and Q&A.

Describe the feature / enhancement and how it helps to overcome the problem or limitation:

I think it could be some method like file.get_audio(), file.get_buffer_as_audio or similar, that automatically removes the audio header, or format the PoolByteArray it in a way that AudioStreamSample gets it right.

Or maybe it would be even easier if added a method to AudioStreamSample that loads a wav file, e.g. AudioStreamSample.load_from_wav("filepath/filename.wav")

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams: In the first case, somehow detecting the size of the header, and pass relevant data to AudioStreamSample as well as the audio data.

In second case, would be automating all this process into a single method inAudioStreamSample.

If this enhancement will not be used often, can it be worked around with a few lines of script?:

I guess, but after many hours trying I still don't know how.

Is there a reason why this should be core and not an add-on in the asset library?:

Is a very simple and useful improvement. I think it would also be useful to audio files generated at runtime, and get the audio files metadata.

Gianclgar commented 4 years ago

Well... I finally managed to create a GDScript that somehow parses the .wav header and gets the right data into AudioStreamSample.

https://github.com/Gianclgar/GDScriptAudioImport

Would still be nice to have something like this built-in to be as easy as importing .ogg files

bluenote10 commented 4 years ago

The goal should not be to "remove" the wave header. The header contains valuable information, and in order to support arbitrary wave files it is needed for further processing. For instance, without the header information it isn't even possible to handle both mono and stereo files, unless you want to guess if your stream of samples may have been mono or stereo originally. The "solution" of ignoring the wave header is getting posted a lot in the Godot community (1, 2, 3), but it simply doesn't work in general.

In my opinion a AudioStreamSample.load_from_wav("filepath/filename.wav") which abstracts away the handling of wave file format details (e.g. different bit sizes / sampling rates etc.) is the best solution. This would also nicely fit to the existing AudioStreamSample.save_to_wav.

On the other hand, attaching the logic to the File type would be a bit weird: A general purpose class like File should not have knowledge about the intricacies of the wave file standard.

Xrayez commented 3 years ago

The title got renamed but I think the import part is the key to solving this perhaps, see #1632.

What we basically need is to retrieve the import functionality as implemented by internal ResourceImporterWAV class.

The functionality could be either extracted from there and refactored into the proposed AudioStreamSample.load_from_wav, or as proposed in #1632 to allow import any resource type at run-time this way without reinventing anything, although you'd have to compile the engine from source to benefit from such a feature anyway (the export templates would also need import functionality, it's very unlikely that official export templates would have that because this can potentially bloat the binary sizes somewhat). But there are not that many importers there as you think, so could add up to 3MB at most? If not less.

Dimev commented 3 years ago

There's this, but it got reverted sadly: godotengine/godot#42524

ondesic commented 3 years ago

I must say this one surprises me. Audio is SO important in video games. The easier the better. I am still scratching my head over how tedious it is to load an AudioStream by code. I hope this gets implemented.

Calinou commented 3 years ago

@ondesic Please don't bump issues without contributing significant new information. Use the :+1: reaction button on the first post instead.

Xrayez commented 3 years ago

Another use case popped up: #2632.

DomiStyle commented 1 year ago

Well... I finally managed to create a GDScript that somehow parses the .wav header and gets the right data into AudioStreamSample.

https://github.com/Gianclgar/GDScriptAudioImport

Would still be nice to have something like this built-in to be as easy as importing .ogg files

Is there an alternative for this in Godot 4?

Looking at the documentation it seems like AudioStreamOGGVorbis.data was replaced in favor of AudioStreamOGGVorbis.packet_sequence.

OggPacketSequence doesn't look it can be set without parsing the ogg file. Looking at the source code I'm not really getting any smarter on what Godot expects for OggPacketSequence.granule_positions and OggPacketSequence.packet_data.

AudioStreamWAV.data still seems to be a thing (for now?) so hitting up ffmpeg to convert to wav might be the best solution right now?

pwab commented 1 year ago

Hey @DomiStyle if I understand you correctly, you are also searching for a possibility to load ogg files. This might help (starting from Godot 4.2): https://github.com/godotengine/godot/pull/78084

CDcruzCode commented 8 months ago

Hello,

I'm unsure if this issue is still being looked into, but since I needed Wav files to be loaded at runtime, I have created my own GDscript function for this. I've only supported PCM wav files, however, I expose all header information so if anyone else is more knowledgeable on other wav audio formats, they should be able to take what I've done and add to it.

This function returns an AudioStreamWAV with all required parameters like bit-rate and sample rate changed depending on the given file from the file path.

#Take a Packed Byte Array and reverse it to read little endian data to an integer
func read_le_int(file:FileAccess, byte_size:int):
    var file_buffer:PackedByteArray = file.get_buffer(byte_size)
    file_buffer.reverse()
    return file_buffer.hex_encode().hex_to_int()

func load_wav(path:String):
    var wav_file:AudioStreamWAV = AudioStreamWAV.new()
    var file:FileAccess = FileAccess.open(path, FileAccess.READ)

    #CHUNK ID
    var file_buffer:PackedByteArray = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "RIFF"):
        push_error("[load_wav] Invalid file type - not RIFF")
        return false
    #CHUNK SIZE - Full byte size minus first 8 bytes
    var chunk_size:int = read_le_int(file, 4)
    var real_size:int = file.get_length()-8
    if(chunk_size != real_size):
        push_error("[load_wav] Chunk size does not match. Chunk: ", chunk_size,". Expected: ",real_size)
        return false
    #FORMAT
    file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "WAVE"):
        push_error("[load_wav] Invalid file type - not WAVE")
        return false
    #SUB CHUNK1 ID
    file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "fmt "):
        push_error("[load_wav] Invalid file type - not fmt")
        return false
    #SUB CHUNK1 SIZE
    var s_chunk1_size:int = read_le_int(file, 4)
    if(s_chunk1_size != 16):
        push_error("[load_wav] Unsupported type. Only supports PCM.")
        return false
    #AUDIO FORMAT
    var audio_format:int = read_le_int(file, 2)
    if(audio_format != 1):
        push_error("[load_wav] Unsupported type. Only supports PCM.")
        return false
    #NUMBER OF CHANNELS
    var channels:int = read_le_int(file, 2)
    if(channels > 2):
        push_error("[load_wav] Unsupported channel amount. Only supports Mono or Stereo.")
        return false
    #SAMPLE RATE
    var sample_rate:int = read_le_int(file, 4)
    #BYTE RATE = SampleRate*NumChannels*BitsPerSample/8
    var byte_rate:int = read_le_int(file, 4)
    #Block Align = NumChannels*BitsPerSample/8
    var block_align:int = read_le_int(file, 2)
    #BITS PER SAMPLE
    var bit_rate:int = read_le_int(file, 2)
    #"DATA" TEXT
    file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "data"):
        push_error("[load_wav] Invalid file type - not 'data'")
        return false
    #AUDIO DATA SIZE
    var audio_data_size:int = read_le_int(file, 4)

    #Confirming values
    var expected_byte_rate:float = sample_rate * channels * bit_rate / 8.0
    if(byte_rate != expected_byte_rate):
        push_error("[load_wav] Invalid formatting, byte rate incorrect.")
        return false

    var expected_block_align:float = channels * bit_rate / 8.0
    if(block_align != expected_block_align):
        push_error("[load_wav] Invalid formatting, block align incorrect.")
        return false
    ####Adding Data to AudioStreamWAV####
    match(bit_rate):
        8:
            wav_file.format = AudioStreamWAV.FORMAT_8_BITS
        16:
            wav_file.format = AudioStreamWAV.FORMAT_16_BITS
        _:
            push_error("[load_wav] Unsupported bit rate")
            return false

    wav_file.mix_rate = sample_rate
    if(channels == 2):
        wav_file.stereo = true
    else:
        wav_file.stereo = false

    #Audio Data's starting offset is the full file size minus the difference between chunk size and audio data size, minus 8 for the 8 bytes not included in chunk size
    wav_file.data = file.get_buffer(file.get_length()-(chunk_size-audio_data_size)-8)

    return wav_file

A few helpful resources for me were this Youtube video by Low Byte Productions https://www.youtube.com/watch?v=udbA7u1zYfc and http://soundfile.sapp.org/doc/WaveFormat/ I based my code on what Low Byte Production explained in his video for Javascript. Another useful link: https://isip.piconepress.com/projects/speech/software/tutorials/production/fundamentals/v1.0/section_02/s02_01_p05.html#:~:text=The%20WAV%20audio%20format%20was,quality%2016%2Dbit%20audio%20format.

EDIT: Modified the function a bit to include the "Data" text present in the Wav header, and the Audio Data Size chunk to properly know the audio data size to exclude any metadata at the end of the file. Also cleaned up the function at put the little endian conversion into its own function.

Stalker2106x commented 7 months ago

Hello, Looking forward to get a merge request for this, I am writing a software that dynamically loads resources. It works for images which are available via "Image.load(path)" I know ogg vorbis are loadable as well, but MP3s and more importantly here WAVs aren't.

Is this being looked into? Based on the fact imports do work for wav, it shouldnt be much hassle replicating what have been done for ogg vorbis, or am i mistaken?

Best regards,

Calinou commented 7 months ago

Is this being looked into?

To my knowledge, nobody is currently working on implementing this, but a PR is welcome.

Based on the fact imports do work for wav, it shouldnt be much hassle replicating what have been done for ogg vorbis, or am i mistaken?

The way the importer code is organized makes this nontrivial, as there are many differences between each audio file type's handling.

SirLich commented 6 months ago

Would it at least be feasible to expose the import process to tool scripts, or GDExtension?

jitspoe commented 4 months ago

So I tried a couple of these gdscripts to load wav files and they don't work consistently. They assume the data in the wav file is a certain way, but the header isn't always the same size and whatnot. Specifically, I was having trouble with files output from Reaper, as they include extra data.

This is a hot mess, but here's something I cobbled together from the various scripts with some tweaks to handle the extended header:

#GDScriptAudioImport v0.1

#MIT License
#
#Copyright (c) 2020 Gianclgar (Giannino Clemente) gianclgar@gmail.com
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

#I honestly don't care that much, Kopimi ftw, but it's my little baby and I want it to look nice :3

extends Node

#Take a Packed Byte Array and reverse it to read little endian data to an integer
# TODO: Can't we just do .read_32 or whatever?
func read_le_int(file:FileAccess, byte_size:int):
    var file_buffer:PackedByteArray = file.get_buffer(byte_size)
    file_buffer.reverse()
    return file_buffer.hex_encode().hex_to_int()

func report_errors(err, filepath):
    # See: https://docs.godotengine.org/en/latest/classes/class_@globalscope.html#enum-globalscope-error
    var result_hash = {
        ERR_FILE_NOT_FOUND: "File: not found",
        ERR_FILE_BAD_DRIVE: "File: Bad drive error",
        ERR_FILE_BAD_PATH: "File: Bad path error.",
        ERR_FILE_NO_PERMISSION: "File: No permission error.",
        ERR_FILE_ALREADY_IN_USE: "File: Already in use error.",
        ERR_FILE_CANT_OPEN: "File: Can't open error.",
        ERR_FILE_CANT_WRITE: "File: Can't write error.",
        ERR_FILE_CANT_READ: "File: Can't read error.",
        ERR_FILE_UNRECOGNIZED: "File: Unrecognized error.",
        ERR_FILE_CORRUPT: "File: Corrupt error.",
        ERR_FILE_MISSING_DEPENDENCIES: "File: Missing dependencies error.",
        ERR_FILE_EOF: "File: End of file (EOF) error."
    }
    if err in result_hash:
        print("Error: ", result_hash[err], " ", filepath)
    else:
        print("Unknown error with file ", filepath, " error code: ", err)

func load_file(filepath : String) -> AudioStream:
    # if File is wav
    if filepath.ends_with(".wav"):
        return load_wav(filepath)

    var file := FileAccess.open(filepath, FileAccess.READ)
    if (!file):
        var err := FileAccess.get_open_error()
        report_errors(err, filepath)
        return AudioStreamWAV.new()

    var bytes := file.get_buffer(file.get_length())
    # if File is wav
    #if filepath.ends_with(".wav"):
        #var newstream := AudioStreamWAV.new()
#
        ##---------------------------
        ##parrrrseeeeee!!! :D
        #
        #var bits_per_sample = 0
        #
        #for i in range(0, 100):
            #var those4bytes = str(char(bytes[i])+char(bytes[i+1])+char(bytes[i+2])+char(bytes[i+3]))
            #
            #if those4bytes == "RIFF": 
                #print ("RIFF OK at bytes " + str(i) + "-" + str(i+3))
                ##RIP bytes 4-7 integer for now
            #if those4bytes == "WAVE": 
                #print ("WAVE OK at bytes " + str(i) + "-" + str(i+3))
#
            #if those4bytes == "fmt ":
                #print ("fmt OK at bytes " + str(i) + "-" + str(i+3))
                #
                ##get format subchunk size, 4 bytes next to "fmt " are an int32
                #var formatsubchunksize = bytes[i+4] + (bytes[i+5] << 8) + (bytes[i+6] << 16) + (bytes[i+7] << 24)
                #print ("Format subchunk size: " + str(formatsubchunksize))
                #
                ##using formatsubchunk index so it's easier to understand what's going on
                #var fsc0 = i+8 #fsc0 is byte 8 after start of "fmt "
#
                ##get format code [Bytes 0-1]
                #var format_code = bytes[fsc0] + (bytes[fsc0+1] << 8)
                #var format_name
                #if format_code == 0:
                    #format_name = "8_BITS"
                #elif format_code == 1:
                    #format_name = "16_BITS"
                #elif format_code == 2:
                    #format_name = "IMA_ADPCM"
                #else: 
                    #format_name = "UNKNOWN (trying to interpret as 16_BITS)"
                    #format_code = 1
                #print ("Format: " + str(format_code) + " " + format_name)
                ##assign format to our AudioStreamSample
                #newstream.format = format_code
                #
                ##get channel num [Bytes 2-3]
                #var channel_num = bytes[fsc0+2] + (bytes[fsc0+3] << 8)
                #print ("Number of channels: " + str(channel_num))
                ##set our AudioStreamSample to stereo if needed
                #if channel_num == 2: newstream.stereo = true
                #
                ##get sample rate [Bytes 4-7]
                #var sample_rate = bytes[fsc0+4] + (bytes[fsc0+5] << 8) + (bytes[fsc0+6] << 16) + (bytes[fsc0+7] << 24)
                #print ("Sample rate: " + str(sample_rate))
                ##set our AudioStreamSample mixrate
                #newstream.mix_rate = sample_rate
                #
                ##get byte_rate [Bytes 8-11] because we can
                #var byte_rate = bytes[fsc0+8] + (bytes[fsc0+9] << 8) + (bytes[fsc0+10] << 16) + (bytes[fsc0+11] << 24)
                #print ("Byte rate: " + str(byte_rate))
                #
                ##same with bits*sample*channel [Bytes 12-13]
                #var bits_sample_channel = bytes[fsc0+12] + (bytes[fsc0+13] << 8)
                #print ("BitsPerSample * Channel / 8: " + str(bits_sample_channel))
                #
                ##aaaand bits per sample/bitrate [Bytes 14-15]
                #bits_per_sample = bytes[fsc0+14] + (bytes[fsc0+15] << 8)
                #print ("Bits per sample: " + str(bits_per_sample))
                #
            #if those4bytes == "data":
                #assert(bits_per_sample != 0)
                #
                #var audio_data_size = bytes[i+4] + (bytes[i+5] << 8) + (bytes[i+6] << 16) + (bytes[i+7] << 24)
                #print ("Audio data/stream size is " + str(audio_data_size) + " bytes")
#
                #var data_entry_point = (i+8)
                #print ("Audio data starts at byte " + str(data_entry_point))
                #
                ##var data = bytes.subarray(data_entry_point, data_entry_point+audio_data_size-1)
                #var data := bytes.slice(data_entry_point, data_entry_point+audio_data_size-1)
                #
                #if bits_per_sample in [24, 32]:
                    #newstream.data = convert_to_16bit(data, bits_per_sample)
                #else:
                    #newstream.data = data
            ## end of parsing
            ##---------------------------
#
        ##get samples and set loop end
        #var samplenum = newstream.data.size() / 4
        #newstream.loop_end = samplenum
        #newstream.loop_mode = 1 #change to 0 or delete this line if you don't want loop, also check out modes 2 and 3 in the docs
        #return newstream  #:D

    #if file is ogg
    if filepath.ends_with(".ogg"):
        var newstream := AudioStreamOggVorbis.new()
        newstream.loop = true #set to false or delete this line if you don't want to loop
        newstream.data = bytes
        return newstream

    #if file is mp3
    elif filepath.ends_with(".mp3"):
        var newstream := AudioStreamMP3.new()
        newstream.loop = true #set to false or delete this line if you don't want to loop
        newstream.data = bytes
        return newstream

    else:
        print ("ERROR: Wrong filetype or format")
    file.close()
    return AudioStreamWAV.new()

func load_wav(path:String) -> AudioStreamWAV:
    var wav_file:AudioStreamWAV = AudioStreamWAV.new()
    var file:FileAccess = FileAccess.open(path, FileAccess.READ)

    #CHUNK ID
    var file_buffer:PackedByteArray = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "RIFF"):
        push_error("[load_wav] Invalid file type - not RIFF")
        return null
    #CHUNK SIZE - Full byte size minus first 8 bytes
    var chunk_size:int = read_le_int(file, 4)
    var real_size:int = file.get_length()-8
    if(chunk_size != real_size):
        push_error("[load_wav] Chunk size does not match. Chunk: ", chunk_size,". Expected: ",real_size)
        return null
    #FORMAT
    file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "WAVE"):
        push_error("[load_wav] Invalid file type - not WAVE")
        return null
    #SUB CHUNK1 ID
    file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "fmt "):
        push_error("[load_wav] Invalid file type - not fmt")
        return null
    #SUB CHUNK1 SIZE
    var s_chunk1_size:int = read_le_int(file, 4)
    if(s_chunk1_size != 16):
        push_error("[load_wav] Unsupported type. Only supports PCM.")
        return null
    #AUDIO FORMAT
    var audio_format:int = read_le_int(file, 2)
    if(audio_format != 1):
        push_error("[load_wav] Unsupported type. Only supports PCM.")
        return null
    #NUMBER OF CHANNELS
    var channels:int = read_le_int(file, 2)
    if(channels > 2):
        push_error("[load_wav] Unsupported channel amount. Only supports Mono or Stereo.")
        return null
    #SAMPLE RATE
    var sample_rate:int = read_le_int(file, 4)
    #BYTE RATE = SampleRate*NumChannels*BitsPerSample/8
    var byte_rate:int = read_le_int(file, 4)
    #Block Align = NumChannels*BitsPerSample/8
    var block_align:int = read_le_int(file, 2)
    #BITS PER SAMPLE
    var bit_rate:int = read_le_int(file, 2)
    #"DATA" TEXT
    file_buffer = file.get_buffer(4)
    while (file_buffer.get_string_from_ascii() != "data"): # Might be some other header junk.
        var block_size := file.get_32()
        print("Extra wav block: ", file_buffer, block_size)
        if (file.eof_reached()):
            push_error("Failed to find data block of wav.")
            return null
        file.seek(file.get_position() + block_size)
        file_buffer = file.get_buffer(4)
    if(file_buffer.get_string_from_ascii() != "data"):
        push_error("[load_wav] Invalid file type - not 'data'")
        return null
    #AUDIO DATA SIZE
    var audio_data_size:int = read_le_int(file, 4)

    #Confirming values
    var expected_byte_rate:float = sample_rate * channels * bit_rate / 8.0
    if(byte_rate != expected_byte_rate):
        push_error("[load_wav] Invalid formatting, byte rate incorrect.")
        return null

    var expected_block_align:float = channels * bit_rate / 8.0
    if(block_align != expected_block_align):
        push_error("[load_wav] Invalid formatting, block align incorrect.")
        return null
    ####Adding Data to AudioStreamWAV####
    match(bit_rate):
        8:
            wav_file.format = AudioStreamWAV.FORMAT_8_BITS
        16:
            wav_file.format = AudioStreamWAV.FORMAT_16_BITS
        _:
            push_error("[load_wav] Unsupported bit rate")
            return null

    wav_file.mix_rate = sample_rate
    if(channels == 2):
        wav_file.stereo = true
    else:
        wav_file.stereo = false

    #Audio Data's starting offset is the full file size minus the difference between chunk size and audio data size, minus 8 for the 8 bytes not included in chunk size
    wav_file.data = file.get_buffer(file.get_length()-(chunk_size-audio_data_size)-8)

    return wav_file

# TODO: Convert 24bit

# Converts .wav data from 24 or 32 bits to 16
#
# These conversions are SLOW in GDScript
# on my one test song, 32 -> 16 was around 3x slower than 24 -> 16
#
# I couldn't get threads to help very much
# They made the 24bit case about 2x faster in my test file
# And the 32bit case abour 50% slower
# I don't wanna risk it always being slower on other files
# And really, the solution would be to handle it in a low-level language
func convert_to_16bit(data: PackedByteArray, from: int) -> PackedByteArray:
    print("converting to 16-bit from %d" % from)
    var time = Time.get_ticks_msec()
    # 24 bit .wav's are typically stored as integers
    # so we just grab the 2 most significant bytes and ignore the other
    if from == 24:
        var j = 0
        for i in range(0, data.size(), 3):
            data[j] = data[i+1]
            data[j+1] = data[i+2]
            j += 2
        data.resize(data.size() * 2 / 3)
    # 32 bit .wav's are typically stored as floating point numbers
    # so we need to grab all 4 bytes and interpret them as a float first
    if from == 32:
        var spb := StreamPeerBuffer.new()
        var single_float: float
        var value: int
        for i in range(0, data.size(), 4):
            spb.data_array = data.slice(i, i+3)#data.subarray(i, i+3)
            single_float = spb.get_float()
            value = single_float * 32768
            data[i/2] = value
            data[i/2+1] = value >> 8
        data.resize(data.size() / 2)
    print("Took %f seconds for slow conversion" % ((Time.get_ticks_msec() - time) / 1000.0))
    return data

# ---------- REFERENCE ---------------
# note: typical values doesn't always match

#Positions  Typical Value Description
#
#1 - 4      "RIFF"        Marks the file as a RIFF multimedia file.
#                         Characters are each 1 byte long.
#
#5 - 8      (integer)     The overall file size in bytes (32-bit integer)
#                         minus 8 bytes. Typically, you'd fill this in after
#                         file creation is complete.
#
#9 - 12     "WAVE"        RIFF file format header. For our purposes, it
#                         always equals "WAVE".
#
#13-16      "fmt "        Format sub-chunk marker. Includes trailing null.
#
#17-20      16            Length of the rest of the format sub-chunk below.
#
#21-22      1             Audio format code, a 2 byte (16 bit) integer. 
#                         1 = PCM (pulse code modulation).
#
#23-24      2             Number of channels as a 2 byte (16 bit) integer.
#                         1 = mono, 2 = stereo, etc.
#
#25-28      44100         Sample rate as a 4 byte (32 bit) integer. Common
#                         values are 44100 (CD), 48000 (DAT). Sample rate =
#                         number of samples per second, or Hertz.
#
#29-32      176400        (SampleRate * BitsPerSample * Channels) / 8
#                         This is the Byte rate.
#
#33-34      4             (BitsPerSample * Channels) / 8
#                         1 = 8 bit mono, 2 = 8 bit stereo or 16 bit mono, 4
#                         = 16 bit stereo.
#
#35-36      16            Bits per sample. 
#
#37-40      "data"        Data sub-chunk header. Marks the beginning of the
#                         raw data section.
#
#41-44      (integer)     The number of bytes of the data section below this
#                         point. Also equal to (#ofSamples * #ofChannels *
#                         BitsPerSample) / 8
#
#45+                      The raw audio data.            

I was going to try to clean this up a bit, but I've been forced to close this tab and move on.