xiandie/util/audio_loader.gd
2025-07-18 19:52:49 +08:00

259 lines
10 KiB
GDScript

#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
class_name AudioLoader
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 loadfile(filepath):
var file_access = FileAccess.open(filepath, FileAccess.READ)
if file_access.get_error() != OK:
report_errors(file_access.get_error(), filepath)
file_access.close()
return AudioSample.new()
var bytes = FileAccess.get_file_as_bytes(filepath)
# if File is wav
if filepath.ends_with(".wav"):
var newstream = AudioStreamWAV.load_from_buffer(bytes)
newstream.take_over_path(filepath)
# #---------------------------
# #parrrrseeeeee!!! :D
# var bits_per_sample = 0
# var i = 0
# while true:
# if i >= len(bytes) - 4: # Failsafe, if there is no data bytes
# print("Data byte not found")
# break
# 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 AudioSample
# 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 AudioSample 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 AudioSample 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)
# if bits_per_sample in [24, 32]:
# newstream.data = convert_to_16bit(data, bits_per_sample)
# else:
# newstream.data = data
# break # the data will be at the end, end searching here
# i += 1
# # 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
elif filepath.ends_with(".ogg"):
var newstream = AudioStreamOggVorbis.load_from_buffer(bytes)
newstream.take_over_path(filepath)
# newstream.loop = true #set to false or delete this line if you don't want to loop
return newstream
#if file is mp3
elif filepath.ends_with(".mp3"):
var newstream = AudioStreamMP3.load_from_buffer(bytes)
newstream.take_over_path(filepath)
# newstream.loop = true #set to false or delete this line if you don't want to loop
return newstream
else:
print ("ERROR: Wrong filetype or format")
# 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.subarray(i, i+3)
spb.data_array = data.slice(i, i+4)
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.