diff --git a/modules/gltf/SCsub b/modules/gltf/SCsub index 107511686366..79acb87aaa76 100644 --- a/modules/gltf/SCsub +++ b/modules/gltf/SCsub @@ -4,6 +4,23 @@ from misc.utility.scons_hints import * Import("env") Import("env_modules") +# For MP3 support in the GLTFDocumentExtensionAudio. +if env["module_minimp3_enabled"]: + thirdparty_dir = "#thirdparty/minimp3/" + if not env.msvc: + env_modules.Append(CPPFLAGS=["-isystem", Dir(thirdparty_dir).path]) + else: + env_modules.Prepend(CPPPATH=[thirdparty_dir]) + if not env["minimp3_extra_formats"]: + env_modules.Append(CPPDEFINES=["MINIMP3_ONLY_MP3"]) + +# For OGG Vorbis support in the GLTFDocumentExtensionAudio. +if env["module_vorbis_enabled"]: + thirdparty_dir = "#thirdparty/libvorbis/" + env_modules.Prepend(CPPPATH=[thirdparty_dir]) + if env["builtin_libogg"]: + env_modules.Prepend(CPPPATH=["#thirdparty/libogg"]) + env_gltf = env_modules.Clone() # Godot source files diff --git a/modules/gltf/config.py b/modules/gltf/config.py index c2f909dacf4e..54b93810a5f8 100644 --- a/modules/gltf/config.py +++ b/modules/gltf/config.py @@ -13,6 +13,7 @@ def get_doc_classes(): "EditorSceneFormatImporterGLTF", "GLTFAccessor", "GLTFAnimation", + "GLTFAudioPlayer", "GLTFBufferView", "GLTFCamera", "GLTFDocument", diff --git a/modules/gltf/doc_classes/GLTFAudioPlayer.xml b/modules/gltf/doc_classes/GLTFAudioPlayer.xml new file mode 100644 index 000000000000..daa48c560bfb --- /dev/null +++ b/modules/gltf/doc_classes/GLTFAudioPlayer.xml @@ -0,0 +1,119 @@ + + + + Represents a GLTF audio player. + + + GLTFAudioPlayer is an intermediary between GLTF audio and Godot's audio player nodes. + The KHR_audio_emitter GLTF extension includes MP3 and WAV formats in the base spec. Godot supports saving and loading both of these formats. The MP3 format is available when Godot is compiled with the MiniMP3 module enabled (default). Additionally, Godot can load Ogg Vorbis audio files but not save them when Godot is compiled with the Vorbis module enabled (default). + + + https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/KHR_audio_emitter + + + + + + + Create a new GLTFAudioPlayer instance from the given [Dictionary] containing GLTF audio emitter data as defined by KHR_audio_emitter. + + + + + + + Create a new GLTFAudioPlayer instance from the given Godot [AudioStreamPlayer] or [AudioStreamPlayer3D] node. + + + + + + + Create a new GLTFAudioPlayer instance from the given Godot [AudioStreamPlayer] node. + + + + + + + Create a new GLTFAudioPlayer instance from the given Godot [AudioStreamPlayer3D] node. + + + + + + Converts this GLTFAudioPlayer to a [Dictionary] containing GLTF audio emitter data as defined by KHR_audio_emitter. + + + + + + Converts this GLTFAudioPlayer to a Godot [AudioStreamPlayer] or [AudioStreamPlayer3D] node. + + + + + + Converts this GLTFAudioPlayer to a Godot [AudioStreamPlayer] node. + + + + + + Converts this GLTFAudioPlayer to a Godot [AudioStreamPlayer3D] node. + + + + + + Indices of the audio sources in the GLTF file that are used by this player. This property is used by the [Dictionary] conversion methods, but not the [Node] conversion methods. + + + The audio stream used by this player. This property is used by the [Node] conversion methods, but not the [Dictionary] conversion methods. + + + If [code]true[/code], the audio will automatically start playing when the audio player node is added to the scene tree. This corresponds to the [code skip-lint]autoplay[/code] property of the audio source in the GLTF file (not the audio emitter). + + + The inner angle of the audio cone's angular diameter in radians. An angle of [constant @GDScript.TAU] or greater means the audio is emitted in all directions. This corresponds to the [code]coneInnerAngle[/code] property of the audio emitter in the GLTF file. + + + The outer angle of the audio cone's angular diameter in radians. This corresponds to the [code]coneOuterAngle[/code] property of the audio emitter in the GLTF file. + + + The linear volume gain multiplier of the audio applied when outside the outer cone angle. This is multiplied with [member volume_gain]. This corresponds to the [code]coneOuterGain[/code] property of the audio emitter in the GLTF file. + + + The distance model used to calculate the volume of the audio. Godot only supports the [code]"inverse"[/code] distance model. This corresponds to the [code]distanceModel[/code] property of the audio emitter in the GLTF file. + + + The emitter type of audio player. This corresponds to the [code]type[/code] property of the audio emitter in the GLTF file. + + + The maximum distance from the audio source, beyond which the audio cannot be heard. This corresponds to the [code]maxDistance[/code] property of the audio emitter in the GLTF file. + + + The combined pitch and playback rate without resampling, as a multiplier of the audio sample's sample rate. This corresponds to the [code]playbackRate[/code] property of the audio source in the GLTF file (not the audio emitter), and the [member AudioStreamPlayer.pitch_scale] and [member AudioStreamPlayer3D.pitch_scale] properties in Godot. + + + The rate at which the volume decreases between [member unit_distance] and [member max_distance]. Godot only supports values of [code]0.0[/code] (no distance attenuation), [code]1.0[/code] (inverse distance), and [code]2.0[/code] (inverse squared distance). This corresponds to the [code]rolloffFactor[/code] property of the audio emitter in the GLTF file. + + + The shape of the audio emitter. May be [code]"omnidirectional"[/code] or [code]"cone"[/code]. This corresponds to the [code]shapeType[/code] property of the audio emitter in the GLTF file. + + + The distance in meters where the volume is heard at 100% of its original volume. If closer than this distance, the volume will be [member volume_gain]. If between this distance and [member max_distance], the volume will decrease at a rate determined by [member rolloff_factor]. This corresponds to the [code]refDistance[/code] property of the audio emitter in the GLTF file. + + + The linear volume gain multiplier of the audio. This value is linear, a value of [code]0.0[/code] means silence, [code]1.0[/code] is the original volume, [code]2.0[/code] is twice the volume, etc. This corresponds to the [code]gain[/code] property of the audio source in the GLTF file (not the audio emitter). + + + + + Global emitter type, played everywhere. Audio players with the global emitter type will be imported as [AudioStreamPlayer] nodes, or exported as [code]"type": "global"[/code] in the GLTF file. + + + Positional emitter type, played at a specific position, either omnidirectionally or in a cone. Audio players with the positional emitter type will be imported as [AudioStreamPlayer3D] nodes, or exported as [code]"type": "positional"[/code] in the GLTF file. + + + diff --git a/modules/gltf/extensions/SCsub b/modules/gltf/extensions/SCsub index 310fdcbcae0d..f0a2ad1f641f 100644 --- a/modules/gltf/extensions/SCsub +++ b/modules/gltf/extensions/SCsub @@ -9,5 +9,6 @@ env_gltf = env_modules.Clone() # Godot source files env_gltf.add_source_files(env.modules_sources, "*.cpp") +env_gltf.add_source_files(env.modules_sources, "audio/*.cpp") if not env["disable_physics_3d"]: env_gltf.add_source_files(env.modules_sources, "physics/*.cpp") diff --git a/modules/gltf/extensions/audio/gltf_audio_player.cpp b/modules/gltf/extensions/audio/gltf_audio_player.cpp new file mode 100644 index 000000000000..5092f8b93c97 --- /dev/null +++ b/modules/gltf/extensions/audio/gltf_audio_player.cpp @@ -0,0 +1,453 @@ +/**************************************************************************/ +/* gltf_audio_player.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#include "gltf_audio_player.h" + +#include "../../gltf_state.h" +#include "../../gltf_template_convert.h" + +#include "scene/3d/audio_stream_player_3d.h" +#include "scene/audio/audio_stream_player.h" + +void GLTFAudioPlayer::_bind_methods() { + // Constructors and converters. + ClassDB::bind_static_method("GLTFAudioPlayer", D_METHOD("from_node_0d", "audio_node"), &GLTFAudioPlayer::from_node_0d); + ClassDB::bind_static_method("GLTFAudioPlayer", D_METHOD("from_node_3d", "audio_node"), &GLTFAudioPlayer::from_node_3d); + ClassDB::bind_static_method("GLTFAudioPlayer", D_METHOD("from_node", "audio_node"), &GLTFAudioPlayer::from_node); + + ClassDB::bind_method(D_METHOD("to_node_0d"), &GLTFAudioPlayer::to_node_0d); + ClassDB::bind_method(D_METHOD("to_node_3d"), &GLTFAudioPlayer::to_node_3d); + ClassDB::bind_method(D_METHOD("to_node"), &GLTFAudioPlayer::to_node); + + ClassDB::bind_static_method("GLTFAudioPlayer", D_METHOD("from_dictionary", "dictionary"), &GLTFAudioPlayer::from_dictionary); + ClassDB::bind_method(D_METHOD("to_dictionary"), &GLTFAudioPlayer::to_dictionary); + + // General audio properties. + ClassDB::bind_method(D_METHOD("get_emitter_type"), &GLTFAudioPlayer::get_emitter_type); + ClassDB::bind_method(D_METHOD("set_emitter_type", "emitter_type"), &GLTFAudioPlayer::set_emitter_type); + + ClassDB::bind_method(D_METHOD("get_audio_sources"), &GLTFAudioPlayer::get_audio_sources); + ClassDB::bind_method(D_METHOD("set_audio_sources", "audio_sources"), &GLTFAudioPlayer::set_audio_sources); + + ClassDB::bind_method(D_METHOD("get_audio_stream"), &GLTFAudioPlayer::get_audio_stream); + ClassDB::bind_method(D_METHOD("set_audio_stream", "audio_stream"), &GLTFAudioPlayer::set_audio_stream); + + ClassDB::bind_method(D_METHOD("get_pitch_playback_rate"), &GLTFAudioPlayer::get_pitch_playback_rate); + ClassDB::bind_method(D_METHOD("set_pitch_playback_rate", "pitch_playback_rate"), &GLTFAudioPlayer::set_pitch_playback_rate); + + ClassDB::bind_method(D_METHOD("get_volume_gain"), &GLTFAudioPlayer::get_volume_gain); + ClassDB::bind_method(D_METHOD("set_volume_gain", "volume_gain"), &GLTFAudioPlayer::set_volume_gain); + + ClassDB::bind_method(D_METHOD("get_autoplay"), &GLTFAudioPlayer::get_autoplay); + ClassDB::bind_method(D_METHOD("set_autoplay", "autoplay"), &GLTFAudioPlayer::set_autoplay); + + // Distance attenuation. + ClassDB::bind_method(D_METHOD("get_distance_model"), &GLTFAudioPlayer::get_distance_model); + ClassDB::bind_method(D_METHOD("set_distance_model", "distance_model"), &GLTFAudioPlayer::set_distance_model); + + ClassDB::bind_method(D_METHOD("get_max_distance"), &GLTFAudioPlayer::get_max_distance); + ClassDB::bind_method(D_METHOD("set_max_distance", "max_distance"), &GLTFAudioPlayer::set_max_distance); + + ClassDB::bind_method(D_METHOD("get_unit_distance"), &GLTFAudioPlayer::get_unit_distance); + ClassDB::bind_method(D_METHOD("set_unit_distance", "unit_distance"), &GLTFAudioPlayer::set_unit_distance); + + ClassDB::bind_method(D_METHOD("get_rolloff_factor"), &GLTFAudioPlayer::get_rolloff_factor); + ClassDB::bind_method(D_METHOD("set_rolloff_factor", "rolloff_factor"), &GLTFAudioPlayer::set_rolloff_factor); + + // Cone attenuation. All angles are in radians. + ClassDB::bind_method(D_METHOD("get_shape_type"), &GLTFAudioPlayer::get_shape_type); + ClassDB::bind_method(D_METHOD("set_shape_type", "shape_type"), &GLTFAudioPlayer::set_shape_type); + + ClassDB::bind_method(D_METHOD("get_cone_inner_angle"), &GLTFAudioPlayer::get_cone_inner_angle); + ClassDB::bind_method(D_METHOD("set_cone_inner_angle", "cone_inner_angle"), &GLTFAudioPlayer::set_cone_inner_angle); + + ClassDB::bind_method(D_METHOD("get_cone_outer_angle"), &GLTFAudioPlayer::get_cone_outer_angle); + ClassDB::bind_method(D_METHOD("set_cone_outer_angle", "cone_outer_angle"), &GLTFAudioPlayer::set_cone_outer_angle); + + ClassDB::bind_method(D_METHOD("get_cone_outer_gain"), &GLTFAudioPlayer::get_cone_outer_gain); + ClassDB::bind_method(D_METHOD("set_cone_outer_gain", "cone_outer_gain"), &GLTFAudioPlayer::set_cone_outer_gain); + + // General audio properties. + ADD_PROPERTY(PropertyInfo(Variant::INT, "emitter_type"), "set_emitter_type", "get_emitter_type"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "audio_sources"), "set_audio_sources", "get_audio_sources"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "audio_stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_audio_stream", "get_audio_stream"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay"), "set_autoplay", "get_autoplay"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_playback_rate"), "set_pitch_playback_rate", "get_pitch_playback_rate"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_gain"), "set_volume_gain", "get_volume_gain"); + + // Distance attenuation. + ADD_PROPERTY(PropertyInfo(Variant::STRING, "distance_model"), "set_distance_model", "get_distance_model"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_distance"), "set_max_distance", "get_max_distance"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "unit_distance"), "set_unit_distance", "get_unit_distance"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rolloff_factor"), "set_rolloff_factor", "get_rolloff_factor"); + + // Cone attenuation. All angles are in radians. + ADD_PROPERTY(PropertyInfo(Variant::STRING, "shape_type"), "set_shape_type", "get_shape_type"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cone_inner_angle"), "set_cone_inner_angle", "get_cone_inner_angle"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cone_outer_angle"), "set_cone_outer_angle", "get_cone_outer_angle"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cone_outer_gain"), "set_cone_outer_gain", "get_cone_outer_gain"); + + // Emitter type enum. + BIND_ENUM_CONSTANT(EMITTER_TYPE_GLOBAL); + BIND_ENUM_CONSTANT(EMITTER_TYPE_POSITIONAL); +} + +// General audio properties. + +GLTFAudioPlayer::EmitterType GLTFAudioPlayer::get_emitter_type() const { + return emitter_type; +} + +void GLTFAudioPlayer::set_emitter_type(EmitterType p_emitter_type) { + emitter_type = p_emitter_type; +} + +Vector GLTFAudioPlayer::get_audio_sources() const { + return audio_sources; +} + +void GLTFAudioPlayer::set_audio_sources(const Vector &p_audio_sources) { + audio_sources = p_audio_sources; +} + +Ref GLTFAudioPlayer::get_audio_stream() const { + return audio_stream; +} + +void GLTFAudioPlayer::set_audio_stream(const Ref p_audio_stream) { + audio_stream = p_audio_stream; +} + +bool GLTFAudioPlayer::get_autoplay() const { + return autoplay; +} + +void GLTFAudioPlayer::set_autoplay(bool p_autoplay) { + autoplay = p_autoplay; +} + +real_t GLTFAudioPlayer::get_pitch_playback_rate() const { + return pitch_playback_rate; +} + +void GLTFAudioPlayer::set_pitch_playback_rate(real_t p_pitch_playback_rate) { + pitch_playback_rate = p_pitch_playback_rate; +} + +real_t GLTFAudioPlayer::get_volume_gain() const { + return volume_gain; +} + +void GLTFAudioPlayer::set_volume_gain(real_t p_volume_gain) { + volume_gain = p_volume_gain; +} + +// Distance attenuation. + +String GLTFAudioPlayer::get_distance_model() const { + return distance_model; +} + +void GLTFAudioPlayer::set_distance_model(const String &p_distance_model) { + distance_model = p_distance_model; +} + +real_t GLTFAudioPlayer::get_max_distance() const { + return max_distance; +} + +void GLTFAudioPlayer::set_max_distance(real_t p_max_distance) { + max_distance = p_max_distance; +} + +real_t GLTFAudioPlayer::get_unit_distance() const { + return unit_distance; +} + +void GLTFAudioPlayer::set_unit_distance(real_t p_unit_distance) { + unit_distance = p_unit_distance; +} + +real_t GLTFAudioPlayer::get_rolloff_factor() const { + return rolloff_factor; +} + +void GLTFAudioPlayer::set_rolloff_factor(real_t p_rolloff_factor) { + rolloff_factor = p_rolloff_factor; +} + +// Cone attenuation. All angles are in radians. + +String GLTFAudioPlayer::get_shape_type() const { + return shape_type; +} + +void GLTFAudioPlayer::set_shape_type(const String &p_shape_type) { + shape_type = p_shape_type; +} + +real_t GLTFAudioPlayer::get_cone_inner_angle() const { + return cone_inner_angle; +} + +void GLTFAudioPlayer::set_cone_inner_angle(real_t p_cone_inner_angle) { + cone_inner_angle = p_cone_inner_angle; +} + +real_t GLTFAudioPlayer::get_cone_outer_angle() const { + return cone_outer_angle; +} + +void GLTFAudioPlayer::set_cone_outer_angle(real_t p_cone_outer_angle) { + cone_outer_angle = p_cone_outer_angle; +} + +real_t GLTFAudioPlayer::get_cone_outer_gain() const { + return cone_outer_gain; +} + +void GLTFAudioPlayer::set_cone_outer_gain(real_t p_cone_outer_gain) { + cone_outer_gain = p_cone_outer_gain; +} + +Ref GLTFAudioPlayer::from_node_0d(const AudioStreamPlayer *p_audio_player_node) { + Ref audio_player; + audio_player.instantiate(); + audio_player->set_emitter_type(GLTFAudioPlayer::EmitterType::EMITTER_TYPE_GLOBAL); + audio_player->set_audio_stream(p_audio_player_node->get_stream()); + audio_player->set_pitch_playback_rate(p_audio_player_node->get_pitch_scale()); + audio_player->set_volume_gain(Math::db_to_linear(p_audio_player_node->get_volume_db())); + audio_player->set_autoplay(p_audio_player_node->is_autoplay_enabled()); + audio_player->set_name(p_audio_player_node->get_name()); + return audio_player; +} + +Ref GLTFAudioPlayer::from_node_3d(const AudioStreamPlayer3D *p_audio_player_node) { + Ref audio_player; + audio_player.instantiate(); + audio_player->set_emitter_type(GLTFAudioPlayer::EmitterType::EMITTER_TYPE_POSITIONAL); + audio_player->set_audio_stream(p_audio_player_node->get_stream()); + audio_player->set_pitch_playback_rate(p_audio_player_node->get_pitch_scale()); + audio_player->set_volume_gain(Math::db_to_linear(p_audio_player_node->get_volume_db())); + audio_player->set_autoplay(p_audio_player_node->is_autoplay_enabled()); + audio_player->set_name(p_audio_player_node->get_name()); + // Distance attenuation. + audio_player->set_max_distance(p_audio_player_node->get_max_distance()); + audio_player->set_unit_distance(p_audio_player_node->get_unit_size()); + audio_player->set_distance_model("inverse"); + const AudioStreamPlayer3D::AttenuationModel attenuation_model = p_audio_player_node->get_attenuation_model(); + switch (attenuation_model) { + case AudioStreamPlayer3D::ATTENUATION_INVERSE_DISTANCE: + audio_player->set_rolloff_factor(1.0); + break; + case AudioStreamPlayer3D::ATTENUATION_INVERSE_SQUARE_DISTANCE: + audio_player->set_rolloff_factor(2.0); + break; + case AudioStreamPlayer3D::ATTENUATION_LOGARITHMIC: + ERR_PRINT("GLTF audio: Logarithmic attenuation is not supported by GLTF audio. Falling back to inverse."); + audio_player->set_rolloff_factor(1.0); + break; + case AudioStreamPlayer3D::ATTENUATION_DISABLED: + audio_player->set_rolloff_factor(0.0); + break; + } + // Cone attenuation. Godot only has one cone angle. + if (p_audio_player_node->is_emission_angle_enabled()) { + audio_player->set_shape_type("cone"); + const real_t cone_angle = Math::deg_to_rad(p_audio_player_node->get_emission_angle()) * 2.0f; + audio_player->set_cone_inner_angle(cone_angle); + audio_player->set_cone_outer_angle(cone_angle); + audio_player->set_cone_outer_gain(Math::db_to_linear(p_audio_player_node->get_emission_angle_filter_attenuation_db())); + } + return audio_player; +} + +Ref GLTFAudioPlayer::from_node(const Node *p_audio_player_node) { + Ref audio_player; + if (cast_to(p_audio_player_node)) { + audio_player = from_node_0d(cast_to(p_audio_player_node)); + } else if (cast_to(p_audio_player_node)) { + audio_player = from_node_3d(cast_to(p_audio_player_node)); + } else { + ERR_PRINT("Tried to create a GLTFAudioPlayer from a node, but the given node was not an AudioStreamPlayer or AudioStreamPlayer3D."); + } + return audio_player; +} + +AudioStreamPlayer *GLTFAudioPlayer::to_node_0d() { + AudioStreamPlayer *audio_node = memnew(AudioStreamPlayer); + audio_node->set_stream(audio_stream); + audio_node->set_pitch_scale(pitch_playback_rate); + audio_node->set_volume_db(Math::linear_to_db(volume_gain)); + audio_node->set_autoplay(autoplay); + if (get_name().is_empty()) { + audio_node->set_name("GlobalAudioPlayer"); + } else { + audio_node->set_name(get_name()); + } + return audio_node; +} + +AudioStreamPlayer3D *GLTFAudioPlayer::to_node_3d() { + AudioStreamPlayer3D *audio_node = memnew(AudioStreamPlayer3D); + audio_node->set_stream(audio_stream); + audio_node->set_pitch_scale(pitch_playback_rate); + audio_node->set_volume_db(Math::linear_to_db(volume_gain)); + audio_node->set_max_db(Math::linear_to_db(volume_gain)); + audio_node->set_autoplay(autoplay); + if (get_name().is_empty()) { + audio_node->set_name("PositionalAudioPlayer"); + } else { + audio_node->set_name(get_name()); + } + // Distance attenuation. + audio_node->set_max_distance(max_distance); + audio_node->set_unit_size(unit_distance); + if (distance_model != "inverse") { + WARN_PRINT("GLTF audio: A distance model of '" + distance_model + "' was specified in the GLTF data, but Godot only supports 'inverse'. Falling back to 'inverse'."); + } + if (rolloff_factor < 0.25f) { + audio_node->set_attenuation_model(AudioStreamPlayer3D::ATTENUATION_DISABLED); + if (!Math::is_zero_approx(rolloff_factor)) { + WARN_PRINT("GLTF audio: A rolloff factor of '" + rtos(rolloff_factor) + "' was specified in the GLTF data, but Godot only supports 0, 1, and 2. Falling back to 0 (no attenuation)."); + } + } else if (rolloff_factor < 1.5f) { + audio_node->set_attenuation_model(AudioStreamPlayer3D::ATTENUATION_INVERSE_DISTANCE); + if (!Math::is_equal_approx(rolloff_factor, 1)) { + WARN_PRINT("GLTF audio: A rolloff factor of '" + rtos(rolloff_factor) + "' was specified in the GLTF data, but Godot only supports 0, 1, and 2. Falling back to 1 (inverse attenuation)."); + } + } else { + audio_node->set_attenuation_model(AudioStreamPlayer3D::ATTENUATION_INVERSE_SQUARE_DISTANCE); + if (!Math::is_equal_approx(rolloff_factor, 2)) { + WARN_PRINT("GLTF audio: A rolloff factor of '" + rtos(rolloff_factor) + "' was specified in the GLTF data, but Godot only supports 0, 1, and 2. Falling back to 2 (inverse squared attenuation)."); + } + } + // Cone attenuation. Godot only has one cone angle. + real_t emission_angle_radians = (cone_inner_angle + cone_outer_angle) / 4.0f; + // Note: Don't use TAU or PI in checks to account for floating point errors. + if (emission_angle_radians < 3.14f) { + if (emission_angle_radians > 1.58f) { + WARN_PRINT("GLTF audio: An emission angular radius of " + rtos(emission_angle_radians) + " radians was determined from the GLTF data (half the average of cone inner angular diameter " + rtos(cone_inner_angle) + " and cone outer angular diameter " + rtos(cone_outer_angle) + "), but Godot only supports 0 to 90 degrees (1.570796... radians). Falling back to 90 degrees."); + } + audio_node->set_emission_angle(MIN(Math::rad_to_deg(emission_angle_radians), 90.0f)); + if (shape_type == "cone") { + audio_node->set_emission_angle_enabled(true); + } + } + audio_node->set_emission_angle_filter_attenuation_db(Math::linear_to_db(cone_outer_gain)); + return audio_node; +} + +Node *GLTFAudioPlayer::to_node() { + if (emitter_type == EmitterType::EMITTER_TYPE_GLOBAL) { + return to_node_0d(); + } + return to_node_3d(); +} + +Ref GLTFAudioPlayer::from_dictionary(const Dictionary &p_dictionary) { + ERR_FAIL_COND_V_MSG(!p_dictionary.has("type"), Ref(), "Failed to parse GLTF audio, missing required field 'type'."); + Ref audio; + audio.instantiate(); + const String &emitter_type_string = p_dictionary["type"]; + // KHR_audio_emitter has "global" and "positional". + if (emitter_type_string == "global") { + audio->set_emitter_type(EmitterType::EMITTER_TYPE_GLOBAL); + } else if (emitter_type_string == "positional") { + audio->set_emitter_type(EmitterType::EMITTER_TYPE_POSITIONAL); + } else { + ERR_PRINT("Error parsing GLTF audio: Player type '" + emitter_type_string + "' is unknown."); + } + Vector audio_sources; + Array sources = p_dictionary["sources"]; + GLTFTemplateConvert::set_from_array(audio_sources, sources); + audio->set_audio_sources(audio_sources); + audio->set_volume_gain(p_dictionary.get("gain", 1.0)); + audio->set_name(p_dictionary.get("name", "")); + if (p_dictionary.has("positional")) { + const Dictionary &positional = p_dictionary["positional"]; + // Distance attenuation. + audio->set_distance_model(positional.get("distanceModel", "inverse")); + audio->set_max_distance(positional.get("maxDistance", 0.0)); + audio->set_unit_distance(positional.get("refDistance", 1.0)); + audio->set_rolloff_factor(positional.get("rolloffFactor", 1.0)); + // Cone attenuation. + audio->set_cone_inner_angle(positional.get("coneInnerAngle", Math::TAU)); + audio->set_cone_outer_angle(positional.get("coneOuterAngle", Math::TAU)); + audio->set_cone_outer_gain(positional.get("coneOuterGain", 0.0)); + if (positional.has("shapeType")) { + audio->set_shape_type(positional["shapeType"]); + } else if (audio->get_cone_outer_angle() < 6.28) { + audio->set_shape_type("cone"); + } + } + return audio; +} + +Dictionary GLTFAudioPlayer::to_dictionary() const { + Dictionary dict; + dict["sources"] = GLTFTemplateConvert::to_array(audio_sources); + dict["gain"] = volume_gain; + if (!get_name().is_empty()) { + dict["name"] = get_name(); + } + if (emitter_type == EmitterType::EMITTER_TYPE_POSITIONAL) { + Dictionary positional; + if (cone_inner_angle < Math::TAU) { + positional["coneInnerAngle"] = cone_inner_angle; + } + if (cone_outer_angle < Math::TAU) { + positional["coneOuterAngle"] = cone_outer_angle; + } + if (cone_outer_gain != 0.0) { + positional["coneOuterGain"] = cone_outer_gain; + } + if (distance_model != "inverse") { + positional["distanceModel"] = distance_model; + } + if (max_distance > 0.0) { + positional["maxDistance"] = max_distance; + } + if (unit_distance != 1.0) { + positional["refDistance"] = unit_distance; + } + if (rolloff_factor != 1.0) { + positional["rolloffFactor"] = rolloff_factor; + } + if (shape_type != "omnidirectional") { + positional["shapeType"] = shape_type; + } + dict["positional"] = positional; + dict["type"] = "positional"; + } else { + dict["type"] = "global"; + } + return dict; +} diff --git a/modules/gltf/extensions/audio/gltf_audio_player.h b/modules/gltf/extensions/audio/gltf_audio_player.h new file mode 100644 index 000000000000..64877520f080 --- /dev/null +++ b/modules/gltf/extensions/audio/gltf_audio_player.h @@ -0,0 +1,135 @@ +/**************************************************************************/ +/* gltf_audio_player.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#pragma once + +#include "../../gltf_defines.h" +#include "core/io/resource.h" + +class AudioStream; +class AudioStreamPlayer; +class AudioStreamPlayer3D; + +using GLTFAudioSourceIndex = int; + +// GLTFAudioPlayer is an intermediary between GLTF audio and Godot's audio player nodes. +// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/KHR_audio + +class GLTFAudioPlayer : public Resource { + GDCLASS(GLTFAudioPlayer, Resource) + +public: + enum EmitterType { + EMITTER_TYPE_GLOBAL, + EMITTER_TYPE_POSITIONAL, + }; + +protected: + static void _bind_methods(); + +private: + // General audio properties. + EmitterType emitter_type = EmitterType::EMITTER_TYPE_POSITIONAL; + Vector audio_sources; + Ref audio_stream; + real_t pitch_playback_rate = 1.0; + real_t volume_gain = 1.0; + bool autoplay = false; + // Distance attenuation. + String distance_model = "inverse"; + real_t max_distance = 0.0; + real_t unit_distance = 1.0; + real_t rolloff_factor = 1.0; + // Cone attenuation. All angles are in radians. + String shape_type = "omnidirectional"; + real_t cone_inner_angle = Math::TAU; + real_t cone_outer_angle = Math::TAU; + real_t cone_outer_gain = 0.0; + +public: + // General audio properties. + EmitterType get_emitter_type() const; + void set_emitter_type(EmitterType p_emitter_type); + + Vector get_audio_sources() const; + void set_audio_sources(const Vector &p_audio_sources); + + Ref get_audio_stream() const; + void set_audio_stream(const Ref p_audio_stream); + + bool get_autoplay() const; + void set_autoplay(bool p_autoplay); + + real_t get_pitch_playback_rate() const; + void set_pitch_playback_rate(real_t p_pitch_playback_rate); + + real_t get_volume_gain() const; + void set_volume_gain(real_t p_volume_gain); + + // Distance attenuation. + String get_distance_model() const; + void set_distance_model(const String &p_distance_model); + + real_t get_max_distance() const; + void set_max_distance(real_t p_max_distance); + + real_t get_unit_distance() const; + void set_unit_distance(real_t p_unit_distance); + + real_t get_rolloff_factor() const; + void set_rolloff_factor(real_t p_rolloff_factor); + + // Cone attenuation. All angles are in radians. + String get_shape_type() const; + void set_shape_type(const String &p_shape_type); + + real_t get_cone_inner_angle() const; + void set_cone_inner_angle(real_t p_cone_inner_angle); + + real_t get_cone_outer_angle() const; + void set_cone_outer_angle(real_t p_cone_outer_angle); + + real_t get_cone_outer_gain() const; + void set_cone_outer_gain(real_t p_cone_outer_gain); + + // Constructors and converters. + static Ref from_node_0d(const AudioStreamPlayer *p_audio_node); + static Ref from_node_3d(const AudioStreamPlayer3D *p_audio_node); + static Ref from_node(const Node *p_audio_node); + + AudioStreamPlayer *to_node_0d(); + AudioStreamPlayer3D *to_node_3d(); + Node *to_node(); + + static Ref from_dictionary(const Dictionary &p_dictionary); + Dictionary to_dictionary() const; +}; + +VARIANT_ENUM_CAST(GLTFAudioPlayer::EmitterType); diff --git a/modules/gltf/extensions/audio/gltf_document_extension_audio.cpp b/modules/gltf/extensions/audio/gltf_document_extension_audio.cpp new file mode 100644 index 000000000000..f695bd44e693 --- /dev/null +++ b/modules/gltf/extensions/audio/gltf_document_extension_audio.cpp @@ -0,0 +1,675 @@ +/**************************************************************************/ +/* gltf_document_extension_audio.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#include "gltf_document_extension_audio.h" + +#include "core/core_bind.h" +#include "core/io/file_access.h" +#include "modules/modules_enabled.gen.h" +#include "scene/3d/audio_stream_player_3d.h" +#include "scene/audio/audio_stream_player.h" +#include "scene/resources/audio_stream_wav.h" + +#ifdef MODULE_MINIMP3_ENABLED +#include "modules/minimp3/audio_stream_mp3.h" +#endif // MODULE_MINIMP3_ENABLED + +#ifdef MODULE_VORBIS_ENABLED +#include "modules/vorbis/audio_stream_ogg_vorbis.h" +#endif // MODULE_VORBIS_ENABLED + +// Import process. +Error GLTFDocumentExtensionAudio::import_preflight(Ref p_state, const Vector &p_extensions) { + if (!p_extensions.has("KHR_audio_emitter")) { + return ERR_SKIP; + } + Dictionary state_json = p_state->get_json(); + if (state_json.has("extensions")) { + Dictionary state_extensions = state_json["extensions"]; + if (state_extensions.has("KHR_audio_emitter")) { + // KHR_audio_emitter's data is all defined in the document-level + // extensions and is designed to be highly reusable. + Dictionary khr_audio_ext = state_extensions["KHR_audio_emitter"]; + Array audio_sources; + if (khr_audio_ext.has("sources")) { + audio_sources = khr_audio_ext["sources"]; + Array audio_data = khr_audio_ext.get("audio", Array()); + for (int i = 0; i < audio_sources.size(); i++) { + Dictionary audio_source_dict = audio_sources[i]; + int audio_data_index = audio_source_dict.get("audio", -1); + if (audio_data_index == -1) { + continue; + } + ERR_FAIL_INDEX_V_MSG(audio_data_index, audio_data.size(), ERR_PARSE_ERROR, "GLTF audio: KHR_audio_emitter source audio data index " + itos(audio_data_index) + " is not in the KHR_audio_emitter audio data array (size=" + itos(audio_data.size()) + ")."); + Dictionary audio_data_dict = audio_data[audio_data_index]; + } + p_state->set_additional_data(StringName("GLTFAudioData"), audio_data); + p_state->set_additional_data(StringName("GLTFAudioSources"), audio_sources); + } + if (khr_audio_ext.has("emitters")) { + Array audio_emitter_dicts = khr_audio_ext["emitters"]; + Array audio_emitters; + for (int emitter_index = 0; emitter_index < audio_emitter_dicts.size(); emitter_index++) { + Ref audio_emitter = GLTFAudioPlayer::from_dictionary(audio_emitter_dicts[emitter_index]); + audio_emitters.append(audio_emitter); + } + p_state->set_additional_data(StringName("GLTFAudioEmitters"), audio_emitters); + } + } + } + return OK; +} + +Vector GLTFDocumentExtensionAudio::get_supported_extensions() { + Vector ret; + ret.push_back("KHR_audio_emitter"); + ret.push_back("OMI_audio_ogg_vorbis"); + return ret; +} + +Error GLTFDocumentExtensionAudio::parse_node_extensions(Ref p_state, Ref p_gltf_node, const Dictionary &p_extensions) { + if (p_extensions.has("KHR_audio_emitter")) { + Dictionary khr_audio_ext = p_extensions["KHR_audio_emitter"]; + if (khr_audio_ext.has("emitter")) { + int emitter_index = khr_audio_ext["emitter"]; + p_gltf_node->set_additional_data(StringName("GLTFAudioEmitterIndex"), emitter_index); + } + } + return OK; +} + +// Helper methods for import_post_parse. +String _determine_mime_type_for_audio_dict(const Dictionary &p_audio_data_dict) { + String mime_type = p_audio_data_dict.get("mimeType", ""); + if (!mime_type.is_empty()) { + return mime_type; + } + // Determine the MIME type from the URI. + String uri = p_audio_data_dict.get("uri", ""); + if (uri.begins_with("data:")) { + return uri.substr(5, uri.find(";") - 5); + } else if (uri.ends_with(".mp3")) { + return "audio/mpeg"; + } else if (uri.ends_with(".wav")) { + return "audio/wav"; + } else if (uri.ends_with(".ogg")) { + return "audio/ogg"; + } + // No MIME type was found, return an empty string. + return mime_type; +} + +Vector _load_audio_bytes(Ref p_state, Dictionary &p_audio_data_dict) { + if (p_audio_data_dict.has("uri")) { + String uri = p_audio_data_dict["uri"]; + if (uri.begins_with("data:")) { + int comma = uri.find(","); + ERR_FAIL_COND_V_MSG(comma == -1, Vector(), "GLTF audio: Could not load audio URI data, no base64 data separator was found."); + return CoreBind::Marshalls::get_singleton()->base64_to_raw(uri.substr(comma + 1)); + } + Error err; + Vector bytes = FileAccess::get_file_as_bytes(p_state->get_base_path().path_join(uri), &err); + if (err == OK) { + return bytes; + } + } + if (p_audio_data_dict.has("bufferView")) { + const GLTFBufferViewIndex bvi = p_audio_data_dict["bufferView"]; + const TypedArray &buffer_views = p_state->get_buffer_views(); + ERR_FAIL_INDEX_V(bvi, buffer_views.size(), Vector()); + Ref bv = buffer_views[bvi]; + return bv->load_buffer_view_data(p_state); + } + ERR_PRINT("GLTF audio: Could not load audio data, neither uri nor bufferView was valid."); + return Vector(); +} + +void _load_audio_data(Ref p_state, Dictionary &p_audio_data_dict, Dictionary &p_audio_source_dict) { + Ref audio_stream; + String mime_type = _determine_mime_type_for_audio_dict(p_audio_data_dict); + ERR_FAIL_COND_MSG(mime_type.is_empty(), "GLTF audio: Unable to determine the MIME type for the audio data."); + Vector audio_bytes = _load_audio_bytes(p_state, p_audio_data_dict); + ERR_FAIL_COND_MSG(audio_bytes.is_empty(), "GLTF audio: Unable to load audio data, no data bytes were found."); + const bool loop = p_audio_source_dict.get("loop", false); + if (mime_type == "audio/wav") { + Ref audio_stream_wav; + audio_stream_wav.instantiate(); + audio_stream_wav->set_data(audio_bytes); + audio_stream_wav->set_loop_mode(loop ? AudioStreamWAV::LoopMode::LOOP_FORWARD : AudioStreamWAV::LoopMode::LOOP_DISABLED); + audio_stream = audio_stream_wav; +#ifdef MODULE_MINIMP3_ENABLED + } else if (mime_type == "audio/mpeg") { + Ref audio_stream_mp3; + audio_stream_mp3.instantiate(); + audio_stream_mp3->set_data(audio_bytes); + audio_stream_mp3->set_loop(loop); + audio_stream = audio_stream_mp3; +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + } else if (mime_type == "audio/ogg") { + Ref audio_stream_ogg = AudioStreamOggVorbis::load_from_buffer(audio_bytes); + audio_stream_ogg->set_loop(loop); + audio_stream = audio_stream_ogg; +#endif // MODULE_VORBIS_ENABLED + } else { + ERR_PRINT("GLTF audio: Unable to load audio data, the given MIME type is not supported: '" + mime_type + "'."); + return; + } + ERR_FAIL_COND_MSG(audio_stream.is_null(), "GLTF audio: Unknown error loading audio with MIME type: '" + mime_type + "'."); + audio_stream->set_name(p_audio_source_dict.get("name", "")); + // Use snake_case because audio_stream only exists in memory, not in the exported JSON. + p_audio_data_dict["audio_stream"] = audio_stream; + p_audio_source_dict["audio_stream"] = audio_stream; +} + +Array _generate_players_for_emitter(const Ref p_audio_emitter, const Array &p_all_audio_sources) { + // Generate audio players for each audio source in this audio emitter. + Array audio_players; + const Vector audio_source_indices = p_audio_emitter->get_audio_sources(); + for (int source_number = 0; source_number < audio_source_indices.size(); source_number++) { + int audio_source_index = audio_source_indices[source_number]; + ERR_FAIL_INDEX_V_MSG(audio_source_index, p_all_audio_sources.size(), audio_players, "GLTF audio: Emitter source index " + itos(audio_source_index) + " is not in the sources array (size=" + itos(p_all_audio_sources.size()) + ")."); + const Dictionary &audio_source_dict = p_all_audio_sources[audio_source_index]; + Ref audio_player = p_audio_emitter->duplicate(); + if (likely(audio_source_dict.has("audio_stream"))) { + audio_player->set_audio_stream(audio_source_dict["audio_stream"]); + } + audio_player->set_autoplay(audio_source_dict.get("autoplay", false)); + audio_player->set_pitch_playback_rate(audio_source_dict.get("playbackRate", 1.0)); + const real_t source_gain = audio_source_dict.get("gain", 1.0); + audio_player->set_volume_gain(source_gain * audio_player->get_volume_gain()); + audio_players.append(audio_player); + } + // If no audio source was present, still generate an empty audio player for this emitter. + if (audio_players.is_empty()) { + audio_players.append(p_audio_emitter->duplicate()); + } + return audio_players; +} + +Array _generate_players_for_all_emitters(Ref p_state) { + // Generate audio players for each audio emitter + source pair. + Array audio_sources = p_state->get_additional_data(StringName("GLTFAudioSources")); + Array audio_emitters = p_state->get_additional_data(StringName("GLTFAudioEmitters")); + Array audio_players; + for (int emitter_index = 0; emitter_index < audio_emitters.size(); emitter_index++) { + Ref audio_emitter = audio_emitters[emitter_index]; + audio_players.append(_generate_players_for_emitter(audio_emitter, audio_sources)); + } + p_state->set_additional_data(StringName("GLTFAudioPlayers"), audio_players); + return audio_players; +} + +Dictionary _get_main_scene_dictionary(Ref p_state) { + Dictionary state_json = p_state->get_json(); + int scene_index = state_json.get("scene", 0); + Array scenes = state_json["scenes"]; + ERR_FAIL_INDEX_V_MSG(scene_index, scenes.size(), Dictionary(), "GLTF audio: Scene index " + itos(scene_index) + " is not in the scenes array (size=" + itos(scenes.size()) + ")."); + return scenes[scene_index]; +} + +Array _get_scene_level_audio_emitter_indices(Ref p_state) { + Dictionary scene_dict = _get_main_scene_dictionary(p_state); + if (scene_dict.has("extensions")) { + Dictionary scene_extensions = scene_dict["extensions"]; + if (scene_extensions.has("KHR_audio_emitter")) { + Dictionary scene_khr_audio_ext = scene_extensions["KHR_audio_emitter"]; + if (scene_khr_audio_ext.has("emitters")) { + return scene_khr_audio_ext["emitters"]; + } + } + } + return Array(); +} + +Error GLTFDocumentExtensionAudio::import_post_parse(Ref p_state) { + // Load the audio bytes as audio streams and reference in the audio data and sources. + Array audio_data = p_state->get_additional_data(StringName("GLTFAudioData")); + Array audio_sources = p_state->get_additional_data(StringName("GLTFAudioSources")); + for (int i = 0; i < audio_sources.size(); i++) { + Dictionary audio_source_dict = audio_sources[i]; + int audio_data_index = audio_source_dict.get("audio", -1); + if (audio_source_dict.has("extensions")) { + Dictionary audio_source_extensions = audio_source_dict["extensions"]; + if (audio_source_extensions.has("OMI_audio_ogg_vorbis")) { + Dictionary ogg_vorbis_ext = audio_source_extensions["OMI_audio_ogg_vorbis"]; + if (ogg_vorbis_ext.has("audio")) { + audio_data_index = ogg_vorbis_ext["audio"]; + } + } + } + if (audio_data_index == -1) { + continue; + } + ERR_FAIL_INDEX_V_MSG(audio_data_index, audio_data.size(), ERR_PARSE_ERROR, "GLTF audio: Audio data index " + itos(audio_data_index) + " is not in the audio data array (size=" + itos(audio_data.size()) + ")."); + Dictionary audio_data_dict = audio_data[audio_data_index]; + _load_audio_data(p_state, audio_data_dict, audio_source_dict); + } + Array audio_players = _generate_players_for_all_emitters(p_state); + // Set up audio players for the scene-level audio emitters. + Array scene_emitter_indices = _get_scene_level_audio_emitter_indices(p_state); + Array scene_audio_players; + for (int i = 0; i < scene_emitter_indices.size(); i++) { + int emitter_index = scene_emitter_indices[i]; + // Note: The size of the emitters array and players array will be the same. + ERR_FAIL_INDEX_V_MSG(emitter_index, audio_players.size(), ERR_PARSE_ERROR, "GLTF audio: Scene-level emitter index " + itos(emitter_index) + " is not in the emitters array (size=" + itos(audio_players.size()) + ")."); + scene_audio_players.append_array(audio_players[emitter_index]); + } + p_state->set_additional_data(StringName("GLTFSceneAudioPlayers"), scene_audio_players); + return OK; +} + +Node3D *GLTFDocumentExtensionAudio::generate_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_parent) { + Variant emitter_index_variant = p_gltf_node->get_additional_data(StringName("GLTFAudioEmitterIndex")); + if (emitter_index_variant.get_type() != Variant::INT) { + return nullptr; + } + // Grab the audio players for this node's emitter index from the state. + int emitter_index = emitter_index_variant; + Array state_all_audio_emitter_players = p_state->get_additional_data(StringName("GLTFAudioPlayers")); + if (state_all_audio_emitter_players.is_empty()) { + state_all_audio_emitter_players = _generate_players_for_all_emitters(p_state); + } + ERR_FAIL_INDEX_V_MSG(emitter_index, state_all_audio_emitter_players.size(), nullptr, "GLTF audio: Node emitter index " + itos(emitter_index) + " is not in the GLTF emitters array (size=" + itos(state_all_audio_emitter_players.size()) + ")."); + Array audio_players = state_all_audio_emitter_players[emitter_index]; + // Generate Godot nodes for the audio players. + Node3D *ret = nullptr; + for (int i = 0; i < audio_players.size(); i++) { + Ref audio_player = audio_players[i]; + if (audio_player.is_valid()) { + Node *audio_node = audio_player->to_node(); + if (ret) { + ret->add_child(audio_node); + } else { + Node3D *audio_node_3d = Object::cast_to(audio_node); + if (audio_node_3d) { + ret = audio_node_3d; + } else { + // The generated node must be a Node3D to ensure it has a transform, + // otherwise the GLTF node transform hierarchy will be broken. + ret = memnew(Node3D); + ret->add_child(audio_node); + } + } + } + } + return ret; +} + +Error GLTFDocumentExtensionAudio::import_post(Ref p_state, Node *p_root_node) { + // Instantiate the scene-level audio players as children of the root node. + Array scene_audio_players = p_state->get_additional_data(StringName("GLTFSceneAudioPlayers")); + for (int i = 0; i < scene_audio_players.size(); i++) { + Ref audio_player = scene_audio_players[i]; + if (audio_player.is_valid()) { + Node *audio_node = audio_player->to_node(); + p_root_node->add_child(audio_node); + audio_node->set_owner(p_root_node); + } + } + return OK; +} + +// Export process. +Array _get_or_create_audio_data_in_state(Ref p_state) { + Variant state_data_variant = p_state->get_additional_data(StringName("GLTFAudioData")); + if (state_data_variant.get_type() == Variant::ARRAY) { + return state_data_variant; + } + Array state_data; + p_state->set_additional_data(StringName("GLTFAudioData"), state_data); + return state_data; +} + +Array _get_or_create_audio_sources_in_state(Ref p_state) { + Variant state_sources_variant = p_state->get_additional_data(StringName("GLTFAudioSources")); + if (state_sources_variant.get_type() == Variant::ARRAY) { + return state_sources_variant; + } + Array state_sources; + p_state->set_additional_data(StringName("GLTFAudioSources"), state_sources); + return state_sources; +} + +Array _get_or_create_audio_emitters_in_state(Ref p_state) { + Variant state_emitters_variant = p_state->get_additional_data(StringName("GLTFAudioEmitters")); + if (state_emitters_variant.get_type() == Variant::ARRAY) { + return state_emitters_variant; + } + Array state_emitters; + p_state->set_additional_data(StringName("GLTFAudioEmitters"), state_emitters); + return state_emitters; +} + +int _get_or_insert_audio_stream_in_state(Ref p_state, Ref p_audio_stream) { + Array audio_data = _get_or_create_audio_data_in_state(p_state); + for (int i = 0; i < audio_data.size(); i++) { + Dictionary audio_data_dict = audio_data[i]; + if (audio_data_dict.get("audio_stream", Variant()) == p_audio_stream) { + return i; + } + } + const int new_index = audio_data.size(); + Dictionary audio_data_dict; + // Use snake_case because audio_stream only exists in memory, not in the exported JSON. + audio_data_dict["audio_stream"] = p_audio_stream; + audio_data.push_back(audio_data_dict); + return new_index; +} + +int _get_or_insert_audio_source_in_state(Ref p_state, Dictionary &p_audio_source) { + Array state_sources = _get_or_create_audio_sources_in_state(p_state); + for (int i = 0; i < state_sources.size(); i++) { + Dictionary state_source = state_sources[i]; + if (state_source == p_audio_source) { + return i; + } + } + const int new_index = state_sources.size(); + state_sources.push_back(p_audio_source); + return new_index; +} + +int _get_or_insert_audio_player_in_state(Ref p_state, Ref p_audio_player) { + Array state_emitters = _get_or_create_audio_emitters_in_state(p_state); + for (int i = 0; i < state_emitters.size(); i++) { + Ref state_emitter = state_emitters[i]; + if (state_emitter->to_dictionary() == p_audio_player->to_dictionary()) { + return i; + } + } + const int new_index = state_emitters.size(); + state_emitters.push_back(p_audio_player); + return new_index; +} + +void _copy_audio_stream_properties_to_audio_source(const Ref p_audio_stream, Dictionary &p_audio_source) { + Ref audio_stream_wav = p_audio_stream; +#ifdef MODULE_MINIMP3_ENABLED + Ref audio_stream_mp3 = p_audio_stream; +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + Ref audio_stream_ogg = p_audio_stream; +#endif // MODULE_VORBIS_ENABLED + if (audio_stream_wav.is_valid()) { + const bool loop = audio_stream_wav->get_loop_mode() != AudioStreamWAV::LoopMode::LOOP_DISABLED; + if (loop) { + // If false, we don't need to write false, since that is the default value. + p_audio_source["loop"] = true; + } +#ifdef MODULE_MINIMP3_ENABLED + } else if (audio_stream_mp3.is_valid()) { + if (audio_stream_mp3->has_loop()) { + // If false, we don't need to write false, since that is the default value. + p_audio_source["loop"] = true; + } +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + } else if (audio_stream_ogg.is_valid()) { + if (audio_stream_ogg->has_loop()) { + // If false, we don't need to write false, since that is the default value. + p_audio_source["loop"] = true; + } +#endif // MODULE_VORBIS_ENABLED + } + if (!p_audio_source.has("name")) { + if (!p_audio_stream->get_name().is_empty()) { + p_audio_source["name"] = p_audio_stream->get_name(); + } else if (!p_audio_stream->get_path().is_empty()) { + p_audio_source["name"] = p_audio_stream->get_path().get_file(); + } + } +} + +void GLTFDocumentExtensionAudio::convert_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_node) { + Ref audio_player; + Ref audio_stream; + Dictionary audio_source; + if (cast_to(p_scene_node)) { + AudioStreamPlayer *audio_player_node = Object::cast_to(p_scene_node); + audio_player = GLTFAudioPlayer::from_node_0d(audio_player_node); + if (audio_player_node->is_autoplay_enabled()) { + // If false, we don't need to write false, since that is the default value. + audio_source["autoplay"] = true; + } + if (audio_player_node->get_pitch_scale() != 1.0f) { + audio_source["playbackRate"] = audio_player_node->get_pitch_scale(); + } + audio_stream = audio_player_node->get_stream(); + } else if (cast_to(p_scene_node)) { + AudioStreamPlayer3D *audio_player_node = Object::cast_to(p_scene_node); + audio_player = GLTFAudioPlayer::from_node_3d(audio_player_node); + if (audio_player_node->is_autoplay_enabled()) { + // If false, we don't need to write false, since that is the default value. + audio_source["autoplay"] = true; + } + if (audio_player_node->get_pitch_scale() != 1.0f) { + audio_source["playbackRate"] = audio_player_node->get_pitch_scale(); + } + audio_stream = audio_player_node->get_stream(); + } + if (audio_player.is_null()) { + return; // Not an audio node or could not convert. + } + if (audio_stream.is_valid()) { + _copy_audio_stream_properties_to_audio_source(audio_stream, audio_source); + audio_source["audio"] = _get_or_insert_audio_stream_in_state(p_state, audio_stream); + } + if (!audio_source.is_empty()) { + const GLTFAudioSourceIndex source_index = _get_or_insert_audio_source_in_state(p_state, audio_source); + Vector audio_sources; + audio_sources.push_back(source_index); + audio_player->set_audio_sources(audio_sources); + } + const int emitter_index = _get_or_insert_audio_player_in_state(p_state, audio_player); + p_gltf_node->set_additional_data(StringName("GLTFAudioEmitterIndex"), emitter_index); +} + +GLTFBufferViewIndex _save_audio_data_to_buffer(Ref p_state, Ref p_audio_stream) { + // Save to bytes in memory. Assign Ref<> types to perform a cast. + Ref audio_stream_wav = p_audio_stream; +#ifdef MODULE_MINIMP3_ENABLED + Ref audio_stream_mp3 = p_audio_stream; +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + Ref audio_stream_ogg = p_audio_stream; +#endif // MODULE_VORBIS_ENABLED + Vector audio_bytes; + if (audio_stream_wav.is_valid()) { + audio_bytes = audio_stream_wav->get_data(); +#ifdef MODULE_MINIMP3_ENABLED + } else if (audio_stream_mp3.is_valid()) { + audio_bytes = audio_stream_mp3->get_data(); +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + } else if (audio_stream_ogg.is_valid()) { + ERR_PRINT("GLTF audio: Unable to export audio data, Godot is not capable of saving Ogg Vorbis files yet."); + return -1; +#endif // MODULE_VORBIS_ENABLED + } else { + ERR_PRINT("GLTF audio: Unable to export audio data, unknown AudioStream class '" + p_audio_stream->get_class() + "'."); + return -1; + } + ERR_FAIL_COND_V_MSG(audio_bytes.is_empty(), -1, "GLTF audio: Unable to export audio data, no data bytes were found."); + // Write the bytes to a buffer. + return p_state->append_data_to_buffers(audio_bytes, true); +} + +String _determine_mime_type_for_audio_stream(const Ref p_audio_stream) { + Ref audio_stream_wav = p_audio_stream; + if (audio_stream_wav.is_valid()) { + return "audio/wav"; + } +#ifdef MODULE_MINIMP3_ENABLED + Ref audio_stream_mp3 = p_audio_stream; + if (audio_stream_mp3.is_valid()) { + return "audio/mpeg"; + } +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + Ref audio_stream_ogg = p_audio_stream; + if (audio_stream_ogg.is_valid()) { + return "audio/ogg"; +#endif // MODULE_VORBIS_ENABLED + } + ERR_PRINT("GLTF audio: Unable to determine MIME type for AudioStream, unknown class '" + p_audio_stream->get_class() + "'."); + return ""; +} + +Dictionary _save_audio_data(const Ref p_state, const Dictionary &p_audio_data_dict, const int p_audio_data_index) { + Dictionary serialized_audio_data; + if (!p_audio_data_dict.has("audio_stream")) { + return serialized_audio_data; + } + // Use snake_case because audio_stream only exists in memory, not in the exported JSON. + Ref audio_stream = p_audio_data_dict["audio_stream"]; + serialized_audio_data["mimeType"] = _determine_mime_type_for_audio_stream(audio_stream); + String filename = p_state->get_filename(); + String base_path = p_state->get_base_path(); + // If .gltf, it's text, save audio to a separate file. + // If .glb, it's binary. If empty, it's a buffer, also binary. + if (!filename.ends_with(".gltf")) { + GLTFBufferViewIndex bvi = _save_audio_data_to_buffer(p_state, audio_stream); + if (bvi >= 0) { + serialized_audio_data["bufferView"] = bvi; + } + return serialized_audio_data; + } + String save_filename = p_audio_data_dict.get("uri", ""); + if (save_filename.is_empty()) { + save_filename = audio_stream->get_name(); + if (save_filename.is_empty()) { + save_filename = audio_stream->get_path().get_file().get_basename(); + if (save_filename.is_empty()) { + save_filename = "audio_" + itos(p_audio_data_index); + } + } + } + // Save to a file. Assign Ref<> types to perform a cast. + Ref audio_stream_wav = audio_stream; +#ifdef MODULE_MINIMP3_ENABLED + Ref audio_stream_mp3 = audio_stream; +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + Ref audio_stream_ogg = audio_stream; +#endif // MODULE_VORBIS_ENABLED + if (audio_stream_wav.is_valid()) { + if (!save_filename.ends_with(".wav")) { + save_filename += ".wav"; + } + audio_stream_wav->save_to_wav(base_path + save_filename); + serialized_audio_data["uri"] = save_filename; +#ifdef MODULE_MINIMP3_ENABLED + } else if (audio_stream_mp3.is_valid()) { + if (!save_filename.ends_with(".mp3")) { + save_filename += ".mp3"; + } + Ref file = FileAccess::open(base_path.path_join(save_filename), FileAccess::WRITE); + file->store_buffer(audio_stream_mp3->get_data()); + file->close(); + serialized_audio_data["uri"] = save_filename; +#endif // MODULE_MINIMP3_ENABLED +#ifdef MODULE_VORBIS_ENABLED + } else if (audio_stream_ogg.is_valid()) { + ERR_PRINT("GLTF audio: Unable to export audio data, Godot is not capable of saving Ogg Vorbis files yet."); +#endif // MODULE_VORBIS_ENABLED + } else { + ERR_PRINT("GLTF audio: Unable to export audio data, unknown AudioStream class '" + audio_stream->get_class() + "'."); + } + return serialized_audio_data; +} + +void _insert_scene_level_audio_emtters_if_any(Ref p_state) { + Array scene_audio_players = p_state->get_additional_data(StringName("GLTFSceneAudioPlayers")); + Array scene_audio_emitter_indices; + for (int i = 0; i < scene_audio_players.size(); i++) { + const int emitter_index = _get_or_insert_audio_player_in_state(p_state, scene_audio_players[i]); + scene_audio_emitter_indices.push_back(emitter_index); + } + p_state->set_additional_data(StringName("GLTFSceneAudioEmitterIndices"), scene_audio_emitter_indices); +} + +Error GLTFDocumentExtensionAudio::export_preserialize(Ref p_state) { + _insert_scene_level_audio_emtters_if_any(p_state); + Array audio_data = p_state->get_additional_data(StringName("GLTFAudioData")); + Array audio_sources = p_state->get_additional_data(StringName("GLTFAudioSources")); + Array audio_emitters = p_state->get_additional_data(StringName("GLTFAudioEmitters")); + if (audio_sources.is_empty() && audio_emitters.is_empty()) { + return OK; // No audio data to export. + } + p_state->add_used_extension("KHR_audio_emitter"); + Dictionary state_json = p_state->get_json(); + Dictionary serialized_extensions = state_json.get_or_add("extensions", Dictionary()); + Dictionary khr_audio_emitter_ext = serialized_extensions.get_or_add("KHR_audio_emitter", Dictionary()); + Array serialized_audio_data; + Array serialized_sources; + Array serialized_emitters; + for (int i = 0; i < audio_data.size(); i++) { + Dictionary serialized = _save_audio_data(p_state, audio_data[i], i); + serialized_audio_data.push_back(serialized); + } + for (int i = 0; i < audio_sources.size(); i++) { + Dictionary serialized = Dictionary(audio_sources[i]).duplicate(false); + Ref audio_stream = serialized["audio_stream"]; + serialized.erase("audio_stream"); + serialized_sources.push_back(serialized); + } + for (int i = 0; i < audio_emitters.size(); i++) { + Ref audio_emitter = audio_emitters[i]; + serialized_emitters.push_back(audio_emitter->to_dictionary()); + } + khr_audio_emitter_ext["audio"] = serialized_audio_data; + khr_audio_emitter_ext["sources"] = serialized_sources; + khr_audio_emitter_ext["emitters"] = serialized_emitters; + return OK; +} + +Error GLTFDocumentExtensionAudio::export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_node_json, Node *p_node) { + Variant emitter_index = p_gltf_node->get_additional_data(StringName("GLTFAudioEmitterIndex")); + if (emitter_index.get_type() != Variant::INT) { + return OK; + } + Dictionary node_extensions = r_node_json["extensions"]; + Dictionary khr_audio_emitter_ext = node_extensions.get_or_add("KHR_audio_emitter", Dictionary()); + khr_audio_emitter_ext["emitter"] = emitter_index; + return OK; +} + +Error GLTFDocumentExtensionAudio::export_post(Ref p_state) { + Array scene_audio_emitter_indices = p_state->get_additional_data(StringName("GLTFSceneAudioEmitterIndices")); + if (scene_audio_emitter_indices.is_empty()) { + return OK; + } + Dictionary scene_dictionary = _get_main_scene_dictionary(p_state); + Dictionary scene_extensions = scene_dictionary.get_or_add("extensions", Dictionary()); + Dictionary khr_audio_emitter_ext = scene_extensions.get_or_add("KHR_audio_emitter", Dictionary()); + khr_audio_emitter_ext["emitters"] = scene_audio_emitter_indices; + return OK; +} diff --git a/modules/gltf/extensions/audio/gltf_document_extension_audio.h b/modules/gltf/extensions/audio/gltf_document_extension_audio.h new file mode 100644 index 000000000000..9e6a4092358a --- /dev/null +++ b/modules/gltf/extensions/audio/gltf_document_extension_audio.h @@ -0,0 +1,53 @@ +/**************************************************************************/ +/* gltf_document_extension_audio.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#pragma once + +#include "../gltf_document_extension.h" + +#include "gltf_audio_player.h" + +class GLTFDocumentExtensionAudio : public GLTFDocumentExtension { + GDCLASS(GLTFDocumentExtensionAudio, GLTFDocumentExtension); + +public: + // Import process. + Error import_preflight(Ref p_state, const Vector &p_extensions) override; + Vector get_supported_extensions() override; + Error parse_node_extensions(Ref p_state, Ref p_gltf_node, const Dictionary &p_extensions) override; + Error import_post_parse(Ref p_state) override; + Node3D *generate_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_parent) override; + Error import_post(Ref p_state, Node *p_root_node) override; + // Export process. + void convert_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_node) override; + Error export_preserialize(Ref p_state) override; + Error export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_node_json, Node *p_scene_node) override; + Error export_post(Ref p_state) override; +}; diff --git a/modules/gltf/register_types.cpp b/modules/gltf/register_types.cpp index 8a211649e99e..018918a571f5 100644 --- a/modules/gltf/register_types.cpp +++ b/modules/gltf/register_types.cpp @@ -30,6 +30,7 @@ #include "register_types.h" +#include "extensions/audio/gltf_document_extension_audio.h" #include "extensions/gltf_document_extension_convert_importer_mesh.h" #include "extensions/gltf_document_extension_texture_ktx.h" #include "extensions/gltf_document_extension_texture_webp.h" @@ -108,6 +109,7 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) { // glTF API available at runtime. GDREGISTER_CLASS(GLTFAccessor); GDREGISTER_CLASS(GLTFAnimation); + GDREGISTER_CLASS(GLTFAudioPlayer); GDREGISTER_CLASS(GLTFBufferView); GDREGISTER_CLASS(GLTFCamera); GDREGISTER_CLASS(GLTFDocument); @@ -132,6 +134,7 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) { // Ensure physics is first in this list so that physics nodes are created before other nodes. GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionPhysics); #endif // PHYSICS_3D_DISABLED + GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionAudio); GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionTextureKTX); GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionTextureWebP); bool is_editor = Engine::get_singleton()->is_editor_hint();