-
-
Notifications
You must be signed in to change notification settings - Fork 98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Easily load external audio (WAV) files at run-time #732
Comments
Well... I finally managed to create a GDScript that somehow parses the .wav header and gets the right data into https://github.com/Gianclgar/GDScriptAudioImport Would still be nice to have something like this built-in to be as easy as importing .ogg files |
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 On the other hand, attaching the logic to the |
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 The functionality could be either extracted from there and refactored into the proposed |
There's this, but it got reverted sadly: godotengine/godot#42524 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Another use case popped up: #2632. |
Is there an alternative for this in Godot 4? Looking at the documentation it seems like
|
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): |
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/ 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. |
Hello, Is this being looked into? Best regards, |
To my knowledge, nobody is currently working on implementing this, but a PR is welcome.
The way the importer code is organized makes this nontrivial, as there are many differences between each audio file type's handling. |
Would it at least be feasible to expose the import process to tool scripts, or GDExtension? |
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) [email protected]
#
#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/[email protected]#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. |
Hi! |
I've cleaned up the audio importer script a bit, but I still have one bug I can't figure out how to fix. For some reason low def sounds (like 8bit 11khz or 8bit 8khz) are horribly corrupted and will destroy your ears, even though it looks like all the values are set properly and they will import into Godot normally OK. Would be really nice to get this run-time loading implemented for better perf, ease of use, and handling these edge cases! #GDScriptAudioImport v0.1
#MIT License
#
#Copyright (c) 2020 Gianclgar (Giannino Clemente) [email protected]
#
#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
func report_errors(err, filepath):
# See: https://docs.godotengine.org/en/latest/classes/[email protected]#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 ogg
if filepath.ends_with(".ogg"):
var newstream := AudioStreamOggVorbis.load_from_buffer(bytes)
newstream.loop = false
return newstream
#if file is mp3
elif filepath.ends_with(".mp3"):
var newstream := AudioStreamMP3.new()
newstream.loop = false
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 = file.get_32()
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 = file.get_32()
if(s_chunk1_size != 16):
push_error("[load_wav] Unsupported type. Only supports PCM.")
return null
#AUDIO FORMAT
var audio_format:int = file.get_16()
if(audio_format != 1):
push_error("[load_wav] Unsupported type. Only supports PCM.")
return null
#NUMBER OF CHANNELS
var channels:int = file.get_16()
if(channels > 2):
push_error("[load_wav] Unsupported channel amount. Only supports Mono or Stereo.")
return null
#SAMPLE RATE
var sample_rate:int = file.get_32()
#BYTE RATE = SampleRate*NumChannels*BitsPerSample/8
var byte_rate:int = file.get_32()
#Block Align = NumChannels*BitsPerSample/8
var block_align:int = file.get_16()
#BITS PER SAMPLE
var bit_rate:int = file.get_16()
#"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 = file.get_32()
#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####
var needs_convert := false
match(bit_rate):
8:
wav_file.format = AudioStreamWAV.FORMAT_8_BITS
16:
wav_file.format = AudioStreamWAV.FORMAT_16_BITS
24:
wav_file.format = AudioStreamWAV.FORMAT_16_BITS
needs_convert = true
32:
wav_file.format = AudioStreamWAV.FORMAT_16_BITS
needs_convert = true
_:
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
var data := file.get_buffer(file.get_length()-(chunk_size-audio_data_size)-8)
if (needs_convert):
wav_file.data = convert_to_16bit(data, bit_rate)
else:
wav_file.data = data
return wav_file
# 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()
@warning_ignore("narrowing_conversion")
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. Here are some example sounds that don't work properly: |
@jitspoe I'm afraid for the problem of conversion from 8bit to 16 it might be a matter of adding stuff where there's none instead of removing from where there's plenty. For what concerned my problem I ended up using a C# script with NAudio library dependency (I needed it for other reasons too to manipulate the audio files) to prepare files so that the Godot import logic would work properly. I'm now wondering would it be possible to expose some of the NAudio library functionalities through Godot language? |
|
the "format" tells the system the amount of bits used to represent a frame of sound information, if the original information is represented with 8 bits it's not going to be able to reconstruct it in a 16 bit format because it's "missing" half the information, it would need to manipulate the file data itself in some way i guess. Instead if the frame is represented with 32 bits and you set the format to 16 it's just a matter of ignoring half the information, maybe that's why it works for you? Still when I tried in GD it wouldn't work even from 32 to 16 (using your old code, didn't try the new update you posted yet), but maybe other factors too were in play, I'm not sure, I just know that the NAudio C# library has very useful functionalities to manipulate audio files and let me achieve what I was after. It's just a bit of pain to setup the project for it but it's no more than 2/3 h of work at most. |
Right, but I'm trying to use 8 bits per frame in an 8 bit format. There's not a 24 or 32 bit format natively supported in Godot, so the data needs to be converted to 16 bit, and we set |
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:
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 thePoolByteArray
it in a way thatAudioStreamSample
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 in
AudioStreamSample
.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.
The text was updated successfully, but these errors were encountered: