feat/godot: src audio handler works
This commit is contained in:
parent
d51c7fe69a
commit
09d13984a1
|
@ -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.
|
|
@ -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 )
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue