feat/godot: src audio handler works

This commit is contained in:
Leon van Kammen 2024-05-16 19:11:43 +02:00
parent d51c7fe69a
commit 09d13984a1
3 changed files with 298 additions and 94 deletions

View File

@ -0,0 +1,250 @@
# GDScriptAudioImport v0.1
# https://github.com/Gianclgar/GDScriptAudioImport/pull/20
# 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, bytes = null):
var file = null
if bytes == null:
file = FileAccess.open(filepath, FileAccess.READ)
var err = file.get_error()
if err != OK:
report_errors(err, filepath)
file.close()
return AudioStreamWAV.new()
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.slice(data_entry_point, data_entry_point + audio_data_size)
if bits_per_sample in [24, 32]:
newstream.data = convert_to_16bit(data, bits_per_sample)
else:
newstream.data = data
# end of parsing
#---------------------------
#Calculate the size of each sample based on bits_per_sample
var sample_size = bits_per_sample / 8
#get samples and set loop end
var samplenum = newstream.data.size() / sample_size
newstream.loop_end = samplenum
newstream.loop_mode = 0 #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.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.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")
if file != null:
file.close()
# 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()
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)
if from == 32:
var spb := StreamPeerBuffer.new()
var single_float: float
var value: int
for i in range(0, data.size(), 4):
var sub_array = data.slice(i, i+4)
spb.data_array = PackedByteArray(sub_array)
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.

View File

@ -7,11 +7,11 @@ var player:CharacterBody3D
func _ready():
xrf = preload("res://xrfragment.gd").new()
print( xrf.parseURL2("https://foo.com/abc.gltf#foo=2") )
return
xrf.src.addExtension.call("wav", xrf.src.audio ) # extensible support for
xrf.src.addExtension.call("ogg", xrf.src.audio ) # src-metadata (a la carte)
xrf.src.addExtension.call("mp3", xrf.src.audio ) #
add_child(xrf)
#xrf.to("https://xrfragment.org/other.glb", _onXRF )
xrf.to("http://localhost:8080/example/assets/other.glb", _onXRF )

View File

@ -1,5 +1,7 @@
# https://xrfragment.org"
# SPDX-License-Identifier: MPL-2.0"
# author: Leon van Kammen
# date: 16-05-2024
extends Node
@ -32,102 +34,47 @@ func _process(delta):
####################################################################################################
# URI Related functions
# based on https://gist.github.com/coderofsalvation/b2b111a2631fbdc8e76d6cab3bea8f17
####################################################################################################
func parseURL2(url: String) -> Dictionary:
var regex:RegEx = RegEx.new()
regex.compile("\\S+") # Negated whitespace character class.
var results = []
for result in regex.search_all(url.replace("/"," ")):
results.push_back(result.get_string())
print(results)
return {} # Return empty dictionary if URL doesn't match
var groups = regex.get_group_list(url)
var parsed_url = {
"source": url,
"scheme": groups[1] ||"", # Use nullish coalescing for optional scheme
"authority": groups[2] ||"",
"userInfo": (groups[4]||"") + ":" + (groups[5]||""), # Combine user and password
"user": groups[4] ||"",
"password": groups[5] ||"",
"host": groups[6] ||"",
"port": groups[7] || "", # Convert port to int or default to 0
"relative": groups[8] ||"",
"path": groups[9] ||"",
"directory": (groups[9]||"").split("/")[0] || "", # Extract directory path
"file": groups[9].split("/")[-1] ||"", # Extract filename
"query": groups[10] ||"",
"fragment": groups[11] ||"",
}
return parsed_url
func parseURL(url: String) -> Dictionary:
var URI = {"string":url, "next": null}
# Split URL by '://' to get protocol and the rest of the URL
var parts = url.split("://")
if parts.size() > 1:
URI["protocol"] = parts[0]
url = parts[1]
else:
URI["protocol"] = "http" # Default to http if protocol is missing
# Split URL by '/' to separate domain, path, and file
parts = url.split("/")
URI["domain"] = parts[0]
parts.remove_at(0)
if parts.size() > 0:
var path_and_file = "/".join(parts)
path_and_file = path_and_file.split("?")[0]
path_and_file = path_and_file.split("#")[0]
var path_and_file_parts = path_and_file.split("/")
if path_and_file_parts.size() > 1:
URI["path"] = "/".join(path_and_file_parts)
else:
URI["path"] = path_and_file
if !URI.has("path"):
URI["path"] = self.URI.path # copy from current URI
if !URI.has("domain"):
URI["domain"] = self.URI.domain
# Check if there's a query string
if url.find("?") != -1:
parts = url.split("?")
URI["path"] = parts[0]
var args = parts[1]
if args.find("#"):
args = args.split("#")[0]
URI["query"] = parseArgs(args)
else:
URI["query"] = {}
# Check if there's a fragment
if url.find("#") != -1:
parts = url.split("#")
URI["fragment"] = parseArgs(parts[1])
else:
URI["fragment"] = {}
URI['isLocal'] = url[0] == '#'
var URI = {"domain":"","fragment":"","file":"","URN":""}
var parts = ["string","protocol","path","query","hash"]
var urlregex = RegEx.new()
urlregex.compile("(\\w+:\\/\\/)?([^#\\?]+)?(\\?[^#]+)?(#.*)?")
var match = urlregex.search(url)
for i in range(0,parts.size()):
URI[ parts[i] ] = match.strings[i] if match.strings[i] else ""
if URI["path"]:
var pathParts:Array = URI["path"].split("/")
if pathParts.size() > 1 and (pathParts[0].find(".") != -1 || pathParts[0].find(":") != -1):
URI["domain"] = pathParts.pop_front()
URI["path"] = "/".join(pathParts)
pathParts = URI["path"].split("/")
if pathParts[-1].find(".") != -1:
URI["file"] = pathParts[-1]
URI["path"] = "/".join(pathParts)
URI["protocol"] = URI["protocol"].replace("://","") if URI["protocol"] else ""
URI["fragment"] = parseArgs( URI["hash"].substr(1) ) if URI["hash"] else {}
URI["query"] = parseArgs( URI["query"].substr(1) ) if URI["query"] else {}
URI["URN"] = URI["string"].replace("\\?.*","") if URI["domain"] else ""
URI["isLocal"] = true if !URI["domain"] else false
# make relative URL's absolute
if URI["isLocal"]:
URI["domain"] = self.URI["domain"]
URI["protocol"] = self.URI["protocol"]
if URI["path"].match("\\/"):
URI["path"] = self.URI["path"] + URI["path"]
return URI
func parseArgs(fragment: String) -> Dictionary:
var ARG = {}
# Split fragment by '&' to separate items
var items = fragment.split("&")
for item in items:
# Split item by '=' to separate key and value
var key_value = item.split("=")
if key_value.size() > 1:
ARG[key_value[0]] = guess_type(key_value[1])
else:
ARG[key_value[0]] = ""
return ARG
func guess_type(str: String) -> Dictionary:
@ -170,6 +117,7 @@ func forward():
# Download model by HTTP and run `downloadModelSuccess` if OK
func to(url, f:Callable ):
print("navigating to "+url)
cleanup()
var URI = self.parseURL(url)
callback = f
@ -193,9 +141,10 @@ func fetchURL(url:String, f:Callable) -> HTTPRequest:
var http_request = HTTPRequest.new()
_orphans.push_back(http_request)
add_child(http_request)
http_request.request_completed.connect(downloadModelSuccess)
http_request.request_completed.connect(f)
var error = http_request.request(url)
if error != OK:
print("could not request "+url)
push_error("An error occurred in the HTTP request.")
return http_request
@ -216,7 +165,6 @@ func downloadModelSuccess(result, response_code, headers, body):
traverse( scene, href.init )
traverse( scene, src.init )
setPredefinedSceneView()
cleanup()
callback.call("scene_loaded", scene)
func loadModelFromBufferByGLTFDocument(body):
@ -388,16 +336,22 @@ var src = {
for ext in src.extension:
_regex.compile("^.*."+ext+"$")
if _regex.search(XRF.src.path):
var url:String = XRF.src.protocol+"://"+XRF.src.domain+"/"+XRF.src.path
var url:String = XRF.src.protocol+"://"+XRF.src.domain+XRF.src.path
print("src: fetching "+url)
print(XRF.src)
fetchURL(url, src.extension[ext] )
var handler:Callable = src.extension[ext].call(node,ext)
fetchURL(url, handler )
callback.call("src", {"node":node,"XRF":XRF} ),
# some builtin handlers
"audio": func audio(node:Node, extension:String):
"audio": func audio(node:Node, extension:String) -> Callable:
return func onFile(result, response_code, headers, body):
var src = node.XRF.src
print(src.string+" audioooo "+extension+" "+response_code)
var src = node.get_meta("XRF").src
var music = AudioStreamPlayer.new()
add_child(music)
var audio_loader = AudioLoader.new()
music.set_stream( audio_loader.loadfile( src.file, body ) )
music.volume_db = 1
music.pitch_scale = 1
music.play()
add_child(music)
}