diff --git a/editor/icons/FadeCross.svg b/editor/icons/FadeCross.svg
new file mode 100644
index 000000000000..2d4f05883876
--- /dev/null
+++ b/editor/icons/FadeCross.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/FadeDisabled.svg b/editor/icons/FadeDisabled.svg
new file mode 100644
index 000000000000..2333335dcdd3
--- /dev/null
+++ b/editor/icons/FadeDisabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/FadeIn.svg b/editor/icons/FadeIn.svg
new file mode 100644
index 000000000000..3144e07d23d0
--- /dev/null
+++ b/editor/icons/FadeIn.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/FadeOut.svg b/editor/icons/FadeOut.svg
new file mode 100644
index 000000000000..4ce90b58aa95
--- /dev/null
+++ b/editor/icons/FadeOut.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/modules/interactive_music/SCsub b/modules/interactive_music/SCsub
new file mode 100644
index 000000000000..2950a30854a3
--- /dev/null
+++ b/modules/interactive_music/SCsub
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+Import("env")
+Import("env_modules")
+
+env_interactive_music = env_modules.Clone()
+
+# Godot's own source files
+env_interactive_music.add_source_files(env.modules_sources, "*.cpp")
+if env.editor_build:
+ env_interactive_music.add_source_files(env.modules_sources, "editor/*.cpp")
diff --git a/modules/interactive_music/audio_stream_interactive.cpp b/modules/interactive_music/audio_stream_interactive.cpp
new file mode 100644
index 000000000000..ddac458463e0
--- /dev/null
+++ b/modules/interactive_music/audio_stream_interactive.cpp
@@ -0,0 +1,1030 @@
+/**************************************************************************/
+/* audio_stream_interactive.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 "audio_stream_interactive.h"
+
+#include "core/math/math_funcs.h"
+#include "core/string/print_string.h"
+
+AudioStreamInteractive::AudioStreamInteractive() {
+}
+
+Ref AudioStreamInteractive::instantiate_playback() {
+ Ref playback_transitioner;
+ playback_transitioner.instantiate();
+ playback_transitioner->stream = Ref(this);
+ return playback_transitioner;
+}
+
+String AudioStreamInteractive::get_stream_name() const {
+ return "Transitioner";
+}
+
+void AudioStreamInteractive::set_clip_count(int p_count) {
+ ERR_FAIL_COND(p_count < 0 || p_count > MAX_CLIPS);
+
+ AudioServer::get_singleton()->lock();
+
+ if (p_count < clip_count) {
+ // Removing should stop players.
+ version++;
+ }
+
+#ifdef TOOLS_ENABLED
+ stream_name_cache = "";
+ if (p_count < clip_count) {
+ for (int i = 0; i < clip_count; i++) {
+ if (clips[i].auto_advance_next_clip >= p_count) {
+ clips[i].auto_advance_next_clip = 0;
+ clips[i].auto_advance = AUTO_ADVANCE_DISABLED;
+ }
+ }
+
+ for (KeyValue &K : transition_map) {
+ if (K.value.filler_clip >= p_count) {
+ K.value.use_filler_clip = false;
+ K.value.filler_clip = 0;
+ }
+ }
+ if (initial_clip >= p_count) {
+ initial_clip = 0;
+ }
+ }
+#endif
+ clip_count = p_count;
+ AudioServer::get_singleton()->unlock();
+
+ notify_property_list_changed();
+ emit_signal(SNAME("parameter_list_changed"));
+}
+
+void AudioStreamInteractive::set_initial_clip(int p_clip) {
+ ERR_FAIL_INDEX(p_clip, clip_count);
+ initial_clip = p_clip;
+}
+
+int AudioStreamInteractive::get_initial_clip() const {
+ return initial_clip;
+}
+
+int AudioStreamInteractive::get_clip_count() const {
+ return clip_count;
+}
+
+void AudioStreamInteractive::set_clip_name(int p_clip, const StringName &p_name) {
+ ERR_FAIL_INDEX(p_clip, MAX_CLIPS);
+ clips[p_clip].name = p_name;
+}
+
+StringName AudioStreamInteractive::get_clip_name(int p_clip) const {
+ ERR_FAIL_COND_V(p_clip < -1 || p_clip >= MAX_CLIPS, StringName());
+ if (p_clip == CLIP_ANY) {
+ return RTR("All Clips");
+ }
+ return clips[p_clip].name;
+}
+
+void AudioStreamInteractive::set_clip_stream(int p_clip, const Ref &p_stream) {
+ ERR_FAIL_INDEX(p_clip, MAX_CLIPS);
+ AudioServer::get_singleton()->lock();
+ if (clips[p_clip].stream.is_valid()) {
+ version++;
+ }
+ clips[p_clip].stream = p_stream;
+ AudioServer::get_singleton()->unlock();
+#ifdef TOOLS_ENABLED
+ if (Engine::get_singleton()->is_editor_hint()) {
+ if (clips[p_clip].name == StringName() && p_stream.is_valid()) {
+ String n;
+ if (!clips[p_clip].stream->get_name().is_empty()) {
+ n = clips[p_clip].stream->get_name().replace(",", " ");
+ } else if (clips[p_clip].stream->get_path().is_resource_file()) {
+ n = clips[p_clip].stream->get_path().get_file().get_basename().replace(",", " ");
+ n = n.capitalize();
+ }
+
+ if (n != "") {
+ clips[p_clip].name = n;
+ }
+ }
+ }
+#endif
+
+#ifdef TOOLS_ENABLED
+ stream_name_cache = "";
+ notify_property_list_changed(); // Hints change if stream changes.
+ emit_signal(SNAME("parameter_list_changed"));
+#endif
+}
+
+Ref AudioStreamInteractive::get_clip_stream(int p_clip) const {
+ ERR_FAIL_INDEX_V(p_clip, MAX_CLIPS, Ref());
+ return clips[p_clip].stream;
+}
+
+void AudioStreamInteractive::set_clip_auto_advance(int p_clip, AutoAdvanceMode p_mode) {
+ ERR_FAIL_INDEX(p_clip, MAX_CLIPS);
+ ERR_FAIL_INDEX(p_mode, 3);
+ clips[p_clip].auto_advance = p_mode;
+ notify_property_list_changed();
+}
+
+AudioStreamInteractive::AutoAdvanceMode AudioStreamInteractive::get_clip_auto_advance(int p_clip) const {
+ ERR_FAIL_INDEX_V(p_clip, MAX_CLIPS, AUTO_ADVANCE_DISABLED);
+ return clips[p_clip].auto_advance;
+}
+
+void AudioStreamInteractive::set_clip_auto_advance_next_clip(int p_clip, int p_index) {
+ ERR_FAIL_INDEX(p_clip, MAX_CLIPS);
+ clips[p_clip].auto_advance_next_clip = p_index;
+}
+
+int AudioStreamInteractive::get_clip_auto_advance_next_clip(int p_clip) const {
+ ERR_FAIL_INDEX_V(p_clip, MAX_CLIPS, -1);
+ return clips[p_clip].auto_advance_next_clip;
+}
+
+// TRANSITIONS
+
+void AudioStreamInteractive::_set_transitions(const Dictionary &p_transitions) {
+ List keys;
+ p_transitions.get_key_list(&keys);
+ for (const Variant &K : keys) {
+ Vector2i k = K;
+ Dictionary data = p_transitions[K];
+ ERR_CONTINUE(!data.has("from_time"));
+ ERR_CONTINUE(!data.has("to_time"));
+ ERR_CONTINUE(!data.has("fade_mode"));
+ ERR_CONTINUE(!data.has("fade_beats"));
+ bool use_filler_clip = false;
+ int filler_clip = 0;
+ if (data.has("use_filler_clip") && data.has("filler_clip")) {
+ use_filler_clip = data["use_filler_clip"];
+ filler_clip = data["filler_clip"];
+ }
+ bool hold_previous = data.has("hold_previous") ? bool(data["hold_previous"]) : false;
+
+ add_transition(k.x, k.y, TransitionFromTime(int(data["from_time"])), TransitionToTime(int(data["to_time"])), FadeMode(int(data["fade_mode"])), data["fade_beats"], use_filler_clip, filler_clip, hold_previous);
+ }
+}
+
+Dictionary AudioStreamInteractive::_get_transitions() const {
+ Vector keys;
+
+ for (const KeyValue &K : transition_map) {
+ keys.push_back(Vector2i(K.key.from_clip, K.key.to_clip));
+ }
+ keys.sort();
+ Dictionary ret;
+ for (int i = 0; i < keys.size(); i++) {
+ const Transition &tr = transition_map[TransitionKey(keys[i].x, keys[i].y)];
+ Dictionary data;
+ data["from_time"] = tr.from_time;
+ data["to_time"] = tr.to_time;
+ data["fade_mode"] = tr.fade_mode;
+ data["fade_beats"] = tr.fade_beats;
+ if (tr.use_filler_clip) {
+ data["use_filler_clip"] = true;
+ data["filler_clip"] = tr.filler_clip;
+ }
+ if (tr.hold_previous) {
+ data["hold_previous"] = true;
+ }
+
+ ret[keys[i]] = data;
+ }
+ return ret;
+}
+
+bool AudioStreamInteractive::has_transition(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ return transition_map.has(tk);
+}
+
+void AudioStreamInteractive::erase_transition(int p_from_clip, int p_to_clip) {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND(!transition_map.has(tk));
+ AudioDriver::get_singleton()->lock();
+ transition_map.erase(tk);
+ AudioDriver::get_singleton()->unlock();
+}
+
+PackedInt32Array AudioStreamInteractive::get_transition_list() const {
+ PackedInt32Array ret;
+
+ for (const KeyValue &K : transition_map) {
+ ret.push_back(K.key.from_clip);
+ ret.push_back(K.key.to_clip);
+ }
+ return ret;
+}
+
+void AudioStreamInteractive::add_transition(int p_from_clip, int p_to_clip, TransitionFromTime p_from_time, TransitionToTime p_to_time, FadeMode p_fade_mode, float p_fade_beats, bool p_use_filler_flip, int p_filler_clip, bool p_hold_previous) {
+ ERR_FAIL_COND(p_from_clip < CLIP_ANY || p_from_clip >= clip_count);
+ ERR_FAIL_COND(p_to_clip < CLIP_ANY || p_to_clip >= clip_count);
+ ERR_FAIL_UNSIGNED_INDEX(p_from_time, TRANSITION_FROM_TIME_MAX);
+ ERR_FAIL_UNSIGNED_INDEX(p_to_time, TRANSITION_TO_TIME_MAX);
+ ERR_FAIL_UNSIGNED_INDEX(p_fade_mode, FADE_MAX);
+
+ Transition tr;
+ tr.from_time = p_from_time;
+ tr.to_time = p_to_time;
+ tr.fade_mode = p_fade_mode;
+ tr.fade_beats = p_fade_beats;
+ tr.use_filler_clip = p_use_filler_flip;
+ tr.filler_clip = p_filler_clip;
+ tr.hold_previous = p_hold_previous;
+
+ TransitionKey tk(p_from_clip, p_to_clip);
+
+ AudioDriver::get_singleton()->lock();
+ transition_map[tk] = tr;
+ AudioDriver::get_singleton()->unlock();
+}
+
+AudioStreamInteractive::TransitionFromTime AudioStreamInteractive::get_transition_from_time(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), TRANSITION_FROM_TIME_END);
+ return transition_map[tk].from_time;
+}
+
+AudioStreamInteractive::TransitionToTime AudioStreamInteractive::get_transition_to_time(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), TRANSITION_TO_TIME_START);
+ return transition_map[tk].to_time;
+}
+
+AudioStreamInteractive::FadeMode AudioStreamInteractive::get_transition_fade_mode(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), FADE_DISABLED);
+ return transition_map[tk].fade_mode;
+}
+
+float AudioStreamInteractive::get_transition_fade_beats(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), -1);
+ return transition_map[tk].fade_beats;
+}
+
+bool AudioStreamInteractive::is_transition_using_filler_clip(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), false);
+ return transition_map[tk].use_filler_clip;
+}
+
+int AudioStreamInteractive::get_transition_filler_clip(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), -1);
+ return transition_map[tk].filler_clip;
+}
+
+bool AudioStreamInteractive::is_transition_holding_previous(int p_from_clip, int p_to_clip) const {
+ TransitionKey tk(p_from_clip, p_to_clip);
+ ERR_FAIL_COND_V(!transition_map.has(tk), false);
+ return transition_map[tk].hold_previous;
+}
+
+#ifdef TOOLS_ENABLED
+
+PackedStringArray AudioStreamInteractive::_get_linked_undo_properties(const String &p_property, const Variant &p_new_value) const {
+ PackedStringArray ret;
+
+ if (p_property.begins_with("clip_") && p_property.ends_with("/stream")) {
+ int clip = p_property.get_slicec('_', 1).to_int();
+ if (clip < clip_count) {
+ ret.push_back("clip_" + itos(clip) + "/name");
+ }
+ }
+
+ if (p_property == "clip_count") {
+ int new_clip_count = p_new_value;
+
+ if (new_clip_count < clip_count) {
+ for (int i = 0; i < clip_count; i++) {
+ if (clips[i].auto_advance_next_clip >= new_clip_count) {
+ ret.push_back("clip_" + itos(i) + "/auto_advance");
+ ret.push_back("clip_" + itos(i) + "/next_clip");
+ }
+ }
+
+ ret.push_back("_transitions");
+ if (initial_clip >= new_clip_count) {
+ ret.push_back("initial_clip");
+ }
+ }
+ }
+ return ret;
+}
+
+template
+static void _test_and_swap(T &p_elem, uint32_t p_a, uint32_t p_b) {
+ if ((uint32_t)p_elem == p_a) {
+ p_elem = p_b;
+ } else if (uint32_t(p_elem) == p_b) {
+ p_elem = p_a;
+ }
+}
+
+void AudioStreamInteractive::_inspector_array_swap_clip(uint32_t p_item_a, uint32_t p_item_b) {
+ ERR_FAIL_INDEX(p_item_a, (uint32_t)clip_count);
+ ERR_FAIL_UNSIGNED_INDEX(p_item_b, (uint32_t)clip_count);
+
+ for (int i = 0; i < clip_count; i++) {
+ _test_and_swap(clips[i].auto_advance_next_clip, p_item_a, p_item_b);
+ }
+
+ Vector to_remove;
+ HashMap to_add;
+
+ for (KeyValue &K : transition_map) {
+ if (K.key.from_clip == p_item_a || K.key.from_clip == p_item_b || K.key.to_clip == p_item_a || K.key.to_clip == p_item_b) {
+ to_remove.push_back(K.key);
+ TransitionKey new_key = K.key;
+ _test_and_swap(new_key.from_clip, p_item_a, p_item_b);
+ _test_and_swap(new_key.to_clip, p_item_a, p_item_b);
+ to_add[new_key] = K.value;
+ }
+ }
+
+ for (int i = 0; i < to_remove.size(); i++) {
+ transition_map.erase(to_remove[i]);
+ }
+
+ for (KeyValue &K : to_add) {
+ transition_map.insert(K.key, K.value);
+ }
+
+ SWAP(clips[p_item_a], clips[p_item_b]);
+
+ stream_name_cache = "";
+
+ notify_property_list_changed();
+ emit_signal(SNAME("parameter_list_changed"));
+}
+
+String AudioStreamInteractive::_get_streams_hint() const {
+ if (!stream_name_cache.is_empty()) {
+ return stream_name_cache;
+ }
+
+ for (int i = 0; i < clip_count; i++) {
+ if (i > 0) {
+ stream_name_cache += ",";
+ }
+ String n = String(clips[i].name).replace(",", " ");
+
+ if (n == "" && clips[i].stream.is_valid()) {
+ if (!clips[i].stream->get_name().is_empty()) {
+ n = clips[i].stream->get_name().replace(",", " ");
+ } else if (clips[i].stream->get_path().is_resource_file()) {
+ n = clips[i].stream->get_path().get_file().replace(",", " ");
+ }
+ }
+
+ if (n == "") {
+ n = "Clip " + itos(i);
+ }
+
+ stream_name_cache += n;
+ }
+
+ return stream_name_cache;
+}
+
+#endif
+void AudioStreamInteractive::_validate_property(PropertyInfo &r_property) const {
+ String prop = r_property.name;
+
+#ifdef TOOLS_ENABLED
+ if (prop == "switch_to") {
+ r_property.hint_string = _get_streams_hint();
+ return;
+ }
+#endif
+
+ if (prop == "initial_clip") {
+#ifdef TOOLS_ENABLED
+ r_property.hint_string = _get_streams_hint();
+#endif
+ } else if (prop.begins_with("clip_") && prop != "clip_count") {
+ int clip = prop.get_slicec('_', 1).to_int();
+ if (clip >= clip_count) {
+ r_property.usage = PROPERTY_USAGE_INTERNAL;
+ } else if (prop == "clip_" + itos(clip) + "/next_clip") {
+ if (clips[clip].auto_advance != AUTO_ADVANCE_ENABLED) {
+ r_property.usage = 0;
+ } else {
+#ifdef TOOLS_ENABLED
+ r_property.hint_string = _get_streams_hint();
+#endif
+ }
+ }
+ }
+}
+
+void AudioStreamInteractive::get_parameter_list(List *r_parameters) {
+ String clip_names;
+ for (int i = 0; i < clip_count; i++) {
+ clip_names += ",";
+ clip_names += clips[i].name;
+ }
+ r_parameters->push_back(Parameter(PropertyInfo(Variant::STRING, "switch_to_clip", PROPERTY_HINT_ENUM, clip_names, PROPERTY_USAGE_EDITOR), ""));
+}
+
+void AudioStreamInteractive::_bind_methods() {
+#ifdef TOOLS_ENABLED
+ ClassDB::bind_method(D_METHOD("_get_linked_undo_properties", "for_property", "for_value"), &AudioStreamInteractive::_get_linked_undo_properties);
+ ClassDB::bind_method(D_METHOD("_inspector_array_swap_clip", "a", "b"), &AudioStreamInteractive::_inspector_array_swap_clip);
+#endif
+
+ // CLIPS
+
+ ClassDB::bind_method(D_METHOD("set_clip_count", "clip_count"), &AudioStreamInteractive::set_clip_count);
+ ClassDB::bind_method(D_METHOD("get_clip_count"), &AudioStreamInteractive::get_clip_count);
+
+ ClassDB::bind_method(D_METHOD("set_initial_clip", "clip_index"), &AudioStreamInteractive::set_initial_clip);
+ ClassDB::bind_method(D_METHOD("get_initial_clip"), &AudioStreamInteractive::get_initial_clip);
+
+ ClassDB::bind_method(D_METHOD("set_clip_name", "clip_index", "name"), &AudioStreamInteractive::set_clip_name);
+ ClassDB::bind_method(D_METHOD("get_clip_name", "clip_index"), &AudioStreamInteractive::get_clip_name);
+
+ ClassDB::bind_method(D_METHOD("set_clip_stream", "clip_index", "stream"), &AudioStreamInteractive::set_clip_stream);
+ ClassDB::bind_method(D_METHOD("get_clip_stream", "clip_index"), &AudioStreamInteractive::get_clip_stream);
+
+ ClassDB::bind_method(D_METHOD("set_clip_auto_advance", "clip_index", "mode"), &AudioStreamInteractive::set_clip_auto_advance);
+ ClassDB::bind_method(D_METHOD("get_clip_auto_advance", "clip_index"), &AudioStreamInteractive::get_clip_auto_advance);
+
+ ClassDB::bind_method(D_METHOD("set_clip_auto_advance_next_clip", "clip_index", "auto_advance_next_clip"), &AudioStreamInteractive::set_clip_auto_advance_next_clip);
+ ClassDB::bind_method(D_METHOD("get_clip_auto_advance_next_clip", "clip_index"), &AudioStreamInteractive::get_clip_auto_advance_next_clip);
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "initial_clip", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT), "set_initial_clip", "get_initial_clip");
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "clip_count", PROPERTY_HINT_RANGE, "1," + itos(MAX_CLIPS), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Clips,clip_,page_size=999,unfoldable,numbered,swap_method=_inspector_array_swap_clip,add_button_text=" + String(RTR("Add Clip"))), "set_clip_count", "get_clip_count");
+ for (int i = 0; i < MAX_CLIPS; i++) {
+ ADD_PROPERTYI(PropertyInfo(Variant::STRING_NAME, "clip_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_clip_name", "get_clip_name", i);
+ ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "clip_" + itos(i) + "/stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_clip_stream", "get_clip_stream", i);
+ ADD_PROPERTYI(PropertyInfo(Variant::INT, "clip_" + itos(i) + "/auto_advance", PROPERTY_HINT_ENUM, "Disabled,Enabled,ReturnToHold", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_clip_auto_advance", "get_clip_auto_advance", i);
+ ADD_PROPERTYI(PropertyInfo(Variant::INT, "clip_" + itos(i) + "/next_clip", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_clip_auto_advance_next_clip", "get_clip_auto_advance_next_clip", i);
+ }
+
+ // TRANSITIONS
+
+ ClassDB::bind_method(D_METHOD("add_transition", "from_clip", "to_clip", "from_time", "to_time", "fade_mode", "fade_beats", "use_filler_clip", "filler_clip", "hold_previous"), &AudioStreamInteractive::add_transition, DEFVAL(false), DEFVAL(-1), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("has_transition", "from_clip", "to_clip"), &AudioStreamInteractive::has_transition);
+ ClassDB::bind_method(D_METHOD("erase_transition", "from_clip", "to_clip"), &AudioStreamInteractive::erase_transition);
+ ClassDB::bind_method(D_METHOD("get_transition_list"), &AudioStreamInteractive::get_transition_list);
+
+ ClassDB::bind_method(D_METHOD("get_transition_from_time", "from_clip", "to_clip"), &AudioStreamInteractive::get_transition_from_time);
+ ClassDB::bind_method(D_METHOD("get_transition_to_time", "from_clip", "to_clip"), &AudioStreamInteractive::get_transition_to_time);
+ ClassDB::bind_method(D_METHOD("get_transition_fade_mode", "from_clip", "to_clip"), &AudioStreamInteractive::get_transition_fade_mode);
+ ClassDB::bind_method(D_METHOD("get_transition_fade_beats", "from_clip", "to_clip"), &AudioStreamInteractive::get_transition_fade_beats);
+ ClassDB::bind_method(D_METHOD("is_transition_using_filler_clip", "from_clip", "to_clip"), &AudioStreamInteractive::is_transition_using_filler_clip);
+ ClassDB::bind_method(D_METHOD("get_transition_filler_clip", "from_clip", "to_clip"), &AudioStreamInteractive::get_transition_filler_clip);
+ ClassDB::bind_method(D_METHOD("is_transition_holding_previous", "from_clip", "to_clip"), &AudioStreamInteractive::is_transition_holding_previous);
+
+ ClassDB::bind_method(D_METHOD("_set_transitions", "transitions"), &AudioStreamInteractive::_set_transitions);
+ ClassDB::bind_method(D_METHOD("_get_transitions"), &AudioStreamInteractive::_get_transitions);
+
+ ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_transitions", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_transitions", "_get_transitions");
+
+ BIND_ENUM_CONSTANT(TRANSITION_FROM_TIME_IMMEDIATE);
+ BIND_ENUM_CONSTANT(TRANSITION_FROM_TIME_NEXT_BEAT);
+ BIND_ENUM_CONSTANT(TRANSITION_FROM_TIME_NEXT_BAR);
+ BIND_ENUM_CONSTANT(TRANSITION_FROM_TIME_END);
+
+ BIND_ENUM_CONSTANT(TRANSITION_TO_TIME_SAME_POSITION);
+ BIND_ENUM_CONSTANT(TRANSITION_TO_TIME_START);
+
+ BIND_ENUM_CONSTANT(FADE_DISABLED);
+ BIND_ENUM_CONSTANT(FADE_IN);
+ BIND_ENUM_CONSTANT(FADE_OUT);
+ BIND_ENUM_CONSTANT(FADE_CROSS);
+ BIND_ENUM_CONSTANT(FADE_AUTOMATIC);
+
+ BIND_ENUM_CONSTANT(AUTO_ADVANCE_DISABLED);
+ BIND_ENUM_CONSTANT(AUTO_ADVANCE_ENABLED);
+ BIND_ENUM_CONSTANT(AUTO_ADVANCE_RETURN_TO_HOLD);
+
+ BIND_CONSTANT(CLIP_ANY);
+}
+
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
+AudioStreamPlaybackInteractive::AudioStreamPlaybackInteractive() {
+}
+
+AudioStreamPlaybackInteractive::~AudioStreamPlaybackInteractive() {
+}
+
+void AudioStreamPlaybackInteractive::stop() {
+ if (!active) {
+ return;
+ }
+
+ active = false;
+
+ for (int i = 0; i < AudioStreamInteractive::MAX_CLIPS; i++) {
+ if (states[i].playback.is_valid()) {
+ states[i].playback->stop();
+ }
+ states[i].fade_speed = 0.0;
+ states[i].fade_volume = 0.0;
+ states[i].fade_wait = 0.0;
+ states[i].reset_fade();
+ states[i].active = false;
+ states[i].auto_advance = -1;
+ states[i].first_mix = true;
+ }
+}
+
+void AudioStreamPlaybackInteractive::start(double p_from_pos) {
+ if (active) {
+ stop();
+ }
+
+ if (version != stream->version) {
+ for (int i = 0; i < AudioStreamInteractive::MAX_CLIPS; i++) {
+ Ref src_stream;
+ if (i < stream->clip_count) {
+ src_stream = stream->clips[i].stream;
+ }
+ if (states[i].stream != src_stream) {
+ states[i].stream.unref();
+ states[i].playback.unref();
+
+ states[i].stream = src_stream;
+ states[i].playback = src_stream->instantiate_playback();
+ }
+ }
+
+ version = stream->version;
+ }
+
+ int current = stream->initial_clip;
+ if (current < 0 || current >= stream->clip_count) {
+ return; // No playback possible.
+ }
+ if (!states[current].playback.is_valid()) {
+ return; //no playback possible
+ }
+ active = true;
+
+ _queue(current, false);
+}
+
+void AudioStreamPlaybackInteractive::_queue(int p_to_clip_index, bool p_is_auto_advance) {
+ ERR_FAIL_INDEX(p_to_clip_index, stream->clip_count);
+ ERR_FAIL_COND(states[p_to_clip_index].playback.is_null());
+
+ if (playback_current == -1) {
+ // Nothing to do, start.
+ int current = p_to_clip_index;
+ State &state = states[current];
+ state.active = true;
+ state.fade_wait = 0;
+ state.fade_volume = 1.0;
+ state.fade_speed = 0;
+ state.first_mix = true;
+
+ state.playback->start(0);
+
+ playback_current = current;
+
+ if (stream->clips[current].auto_advance == AudioStreamInteractive::AUTO_ADVANCE_ENABLED && stream->clips[current].auto_advance_next_clip >= 0 && stream->clips[current].auto_advance_next_clip < stream->clip_count && stream->clips[current].auto_advance_next_clip != current) {
+ //prepare auto advance
+ state.auto_advance = stream->clips[current].auto_advance_next_clip;
+ }
+ return;
+ }
+
+ for (int i = 0; i < stream->clip_count; i++) {
+ if (i == playback_current || i == p_to_clip_index) {
+ continue;
+ }
+ if (states[i].active && states[i].fade_wait > 0) { // Waiting to kick in, terminate because change of plans.
+ states[i].playback->stop();
+ states[i].reset_fade();
+ states[i].active = false;
+ }
+ }
+
+ State &from_state = states[playback_current];
+ State &to_state = states[p_to_clip_index];
+
+ AudioStreamInteractive::Transition transition; // Use an empty transition by default
+
+ AudioStreamInteractive::TransitionKey tkeys[4] = {
+ AudioStreamInteractive::TransitionKey(playback_current, p_to_clip_index),
+ AudioStreamInteractive::TransitionKey(playback_current, AudioStreamInteractive::CLIP_ANY),
+ AudioStreamInteractive::TransitionKey(AudioStreamInteractive::CLIP_ANY, p_to_clip_index),
+ AudioStreamInteractive::TransitionKey(AudioStreamInteractive::CLIP_ANY, AudioStreamInteractive::CLIP_ANY)
+ };
+
+ for (int i = 0; i < 4; i++) {
+ if (stream->transition_map.has(tkeys[i])) {
+ transition = stream->transition_map[tkeys[i]];
+ break;
+ }
+ }
+
+ if (transition.fade_mode == AudioStreamInteractive::FADE_AUTOMATIC) {
+ // Adjust automatic mode based on context.
+ if (transition.to_time == AudioStreamInteractive::TRANSITION_TO_TIME_START) {
+ transition.fade_mode = AudioStreamInteractive::FADE_OUT;
+ } else {
+ transition.fade_mode = AudioStreamInteractive::FADE_CROSS;
+ }
+ }
+
+ if (p_is_auto_advance) {
+ transition.from_time = AudioStreamInteractive::TRANSITION_FROM_TIME_END;
+ if (transition.to_time == AudioStreamInteractive::TRANSITION_TO_TIME_SAME_POSITION) {
+ transition.to_time = AudioStreamInteractive::TRANSITION_TO_TIME_START;
+ }
+ }
+
+ // Prepare the fadeout
+ float current_pos = from_state.playback->get_playback_position();
+
+ float src_fade_wait = 0;
+ float dst_seek_to = 0;
+ float fade_speed = 0;
+ bool src_no_loop = false;
+
+ if (from_state.stream->get_bpm()) {
+ // Check if source speed has BPM, if so, transition syncs to BPM
+ float beat_sec = 60 / float(from_state.stream->get_bpm());
+ switch (transition.from_time) {
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_IMMEDIATE: {
+ src_fade_wait = 0;
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BEAT: {
+ float remainder = Math::fmod(current_pos, beat_sec);
+ src_fade_wait = beat_sec - remainder;
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BAR: {
+ float bar_sec = beat_sec * from_state.stream->get_bar_beats();
+ float remainder = Math::fmod(current_pos, bar_sec);
+ src_fade_wait = bar_sec - remainder;
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_END: {
+ float end = from_state.stream->get_beat_count() > 0 ? float(from_state.stream->get_beat_count() * beat_sec) : from_state.stream->get_length();
+ if (end == 0) {
+ // Stream does not have a length.
+ src_fade_wait = 0;
+ } else {
+ src_fade_wait = end - current_pos;
+ }
+
+ if (!from_state.stream->has_loop()) {
+ src_no_loop = true;
+ }
+
+ } break;
+ default: {
+ }
+ }
+ // Fade speed also aligned to BPM
+ fade_speed = 1.0 / (transition.fade_beats * beat_sec);
+ } else {
+ // Source has no BPM, so just simple transition.
+ if (transition.from_time == AudioStreamInteractive::TRANSITION_FROM_TIME_END && from_state.stream->get_length() > 0) {
+ float end = from_state.stream->get_length();
+ src_fade_wait = end - current_pos;
+ if (!from_state.stream->has_loop()) {
+ src_no_loop = true;
+ }
+ } else {
+ src_fade_wait = 0;
+ }
+ fade_speed = 1.0 / transition.fade_beats;
+ }
+
+ if (transition.to_time == AudioStreamInteractive::TRANSITION_TO_TIME_PREVIOUS_POSITION && to_state.stream->get_length() > 0.0) {
+ dst_seek_to = to_state.previous_position;
+ } else if (transition.to_time == AudioStreamInteractive::TRANSITION_TO_TIME_SAME_POSITION && transition.from_time != AudioStreamInteractive::TRANSITION_FROM_TIME_END && to_state.stream->get_length() > 0.0) {
+ // Seeking to basically same position as when we start fading.
+ dst_seek_to = current_pos + src_fade_wait;
+ float end;
+ if (to_state.stream->get_bpm() > 0 && to_state.stream->get_beat_count()) {
+ float beat_sec = 60 / float(to_state.stream->get_bpm());
+ end = to_state.stream->get_beat_count() * beat_sec;
+ } else {
+ end = to_state.stream->get_length();
+ }
+
+ if (dst_seek_to > end) {
+ // Seeking too far away.
+ dst_seek_to = 0; //past end, loop to beginning.
+ }
+
+ } else {
+ // Seek to Start
+ dst_seek_to = 0.0;
+ }
+
+ if (transition.fade_mode == AudioStreamInteractive::FADE_DISABLED || transition.fade_mode == AudioStreamInteractive::FADE_IN) {
+ if (src_no_loop) {
+ // If there is no fade in the source stream, then let it continue until it ends.
+ from_state.fade_wait = 0;
+ from_state.fade_speed = 0;
+ } else {
+ // Otherwise force a very quick fade to avoid clicks
+ from_state.fade_wait = src_fade_wait;
+ from_state.fade_speed = 1.0 / -0.001;
+ }
+ } else {
+ // Regular fade.
+ from_state.fade_wait = src_fade_wait;
+ from_state.fade_speed = -fade_speed;
+ }
+ // keep volume, since it may have been fading in from something else.
+
+ to_state.playback->start(dst_seek_to);
+ to_state.active = true;
+ to_state.fade_volume = 0.0;
+ to_state.first_mix = true;
+
+ int auto_advance_to = -1;
+
+ if (stream->clips[p_to_clip_index].auto_advance == AudioStreamInteractive::AUTO_ADVANCE_ENABLED) {
+ int next_clip = stream->clips[p_to_clip_index].auto_advance_next_clip;
+ if (next_clip >= 0 && next_clip < (int)stream->clip_count && states[next_clip].playback.is_valid() && next_clip != p_to_clip_index && next_clip != playback_current && (!transition.use_filler_clip || next_clip != transition.filler_clip)) {
+ auto_advance_to = next_clip;
+ }
+ }
+
+ if (return_memory != -1 && stream->clips[p_to_clip_index].auto_advance == AudioStreamInteractive::AUTO_ADVANCE_RETURN_TO_HOLD) {
+ auto_advance_to = return_memory;
+ return_memory = -1;
+ }
+
+ if (transition.hold_previous) {
+ return_memory = playback_current;
+ }
+
+ if (transition.use_filler_clip && transition.filler_clip >= 0 && transition.filler_clip < (int)stream->clip_count && states[transition.filler_clip].playback.is_valid() && playback_current != transition.filler_clip && p_to_clip_index != transition.filler_clip) {
+ State &filler_state = states[transition.filler_clip];
+
+ filler_state.playback->start(0);
+ filler_state.active = true;
+
+ // Filler state does not fade (bake fade in the audio clip if you want fading.
+ filler_state.fade_volume = 1.0;
+ filler_state.fade_speed = 0.0;
+
+ filler_state.fade_wait = src_fade_wait;
+ filler_state.first_mix = true;
+
+ float filler_end;
+ if (filler_state.stream->get_bpm() > 0 && filler_state.stream->get_beat_count() > 0) {
+ float filler_beat_sec = 60 / float(filler_state.stream->get_bpm());
+ filler_end = filler_beat_sec * filler_state.stream->get_beat_count();
+ } else {
+ filler_end = filler_state.stream->get_length();
+ }
+
+ if (!filler_state.stream->has_loop()) {
+ src_no_loop = true;
+ }
+
+ if (transition.fade_mode == AudioStreamInteractive::FADE_DISABLED || transition.fade_mode == AudioStreamInteractive::FADE_OUT) {
+ // No fading, immediately start at full volume.
+ to_state.fade_volume = 0.0;
+ to_state.fade_speed = 1.0; //start at full volume, as filler is meant as a transition.
+ } else {
+ // Fade enable, prepare fade.
+ to_state.fade_volume = 0.0;
+ to_state.fade_speed = fade_speed;
+ }
+
+ to_state.fade_wait = src_fade_wait + filler_end;
+
+ } else {
+ to_state.fade_wait = src_fade_wait;
+
+ if (transition.fade_mode == AudioStreamInteractive::FADE_DISABLED || transition.fade_mode == AudioStreamInteractive::FADE_OUT) {
+ to_state.fade_volume = 1.0;
+ to_state.fade_speed = 0.0;
+ } else {
+ to_state.fade_volume = 0.0;
+ to_state.fade_speed = fade_speed;
+ }
+
+ to_state.auto_advance = auto_advance_to;
+ }
+}
+
+void AudioStreamPlaybackInteractive::seek(double p_time) {
+ // Seek not supported
+}
+
+int AudioStreamPlaybackInteractive::mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) {
+ if (active && version != stream->version) {
+ stop();
+ }
+
+ if (switch_request != -1) {
+ _queue(switch_request, false);
+ switch_request = -1;
+ }
+
+ if (!active) {
+ for (int i = 0; i < p_frames; i++) {
+ p_buffer[i] = AudioFrame(0.0, 0.0);
+ }
+ return p_frames;
+ }
+
+ int todo = p_frames;
+
+ while (todo) {
+ int to_mix = MIN(todo, BUFFER_SIZE);
+ _mix_internal(to_mix);
+ for (int i = 0; i < to_mix; i++) {
+ p_buffer[i] = mix_buffer[i];
+ }
+ p_buffer += to_mix;
+ todo -= to_mix;
+ }
+
+ return p_frames;
+}
+
+void AudioStreamPlaybackInteractive::_mix_internal(int p_frames) {
+ for (int i = 0; i < p_frames; i++) {
+ mix_buffer[i] = AudioFrame(0, 0);
+ }
+
+ for (int i = 0; i < stream->clip_count; i++) {
+ if (!states[i].active) {
+ continue;
+ }
+
+ _mix_internal_state(i, p_frames);
+ }
+}
+
+void AudioStreamPlaybackInteractive::_mix_internal_state(int p_state_idx, int p_frames) {
+ State &state = states[p_state_idx];
+ double mix_rate = double(AudioServer::get_singleton()->get_mix_rate());
+ double frame_inc = 1.0 / mix_rate;
+
+ int from_frame = 0;
+ int queue_next = -1;
+
+ if (state.first_mix) {
+ // Did not start mixing yet, wait.
+ double mix_time = p_frames * frame_inc;
+ if (state.fade_wait < mix_time) {
+ // time to start!
+ from_frame = state.fade_wait * mix_rate;
+ state.fade_wait = 0;
+ queue_next = state.auto_advance;
+ playback_current = p_state_idx;
+ state.first_mix = false;
+ } else {
+ // This is for fade in of new stream.
+ state.fade_wait -= mix_time;
+ return; // Nothing to do
+ }
+ }
+
+ state.previous_position = state.playback->get_playback_position();
+ state.playback->mix(temp_buffer + from_frame, 1.0, p_frames - from_frame);
+
+ double frame_fade_inc = state.fade_speed * frame_inc;
+
+ for (int i = from_frame; i < p_frames; i++) {
+ if (state.fade_wait) {
+ // This is for fade out of existing stream;
+ state.fade_wait -= frame_inc;
+ if (state.fade_wait < 0.0) {
+ state.fade_wait = 0.0;
+ }
+ } else if (frame_fade_inc > 0) {
+ state.fade_volume += frame_fade_inc;
+ if (state.fade_volume >= 1.0) {
+ state.fade_speed = 0.0;
+ frame_fade_inc = 0.0;
+ state.fade_volume = 1.0;
+ }
+ } else if (frame_fade_inc < 0.0) {
+ state.fade_volume += frame_fade_inc;
+ if (state.fade_volume <= 0.0) {
+ state.fade_speed = 0.0;
+ frame_fade_inc = 0.0;
+ state.fade_volume = 0.0;
+ state.playback->stop(); // Stop playback and break, no point to continue mixing
+ break;
+ }
+ }
+
+ mix_buffer[i] += temp_buffer[i] * state.fade_volume;
+ state.previous_position += frame_inc;
+ }
+
+ if (!state.playback->is_playing()) {
+ // It finished because it either reached end or faded out, so deactivate and continue.
+ state.active = false;
+ }
+ if (queue_next != -1) {
+ _queue(queue_next, true);
+ }
+}
+
+void AudioStreamPlaybackInteractive::tag_used_streams() {
+ for (int i = 0; i < stream->clip_count; i++) {
+ if (states[i].active && !states[i].first_mix && states[i].playback->is_playing()) {
+ states[i].stream->tag_used(states[i].playback->get_playback_position());
+ }
+ }
+ stream->tag_used(0);
+}
+
+void AudioStreamPlaybackInteractive::switch_to_clip_by_name(const StringName &p_name) {
+ if (p_name == StringName()) {
+ switch_request = -1;
+ return;
+ }
+
+ for (int i = 0; i < stream->get_clip_count(); i++) {
+ if (stream->get_clip_name(i) == p_name) {
+ switch_request = i;
+ return;
+ }
+ }
+ ERR_FAIL_MSG("Clip not found: " + String(p_name));
+}
+
+void AudioStreamPlaybackInteractive::set_parameter(const StringName &p_name, const Variant &p_value) {
+ if (p_name == SNAME("switch_to_clip")) {
+ switch_to_clip_by_name(p_value);
+ }
+}
+
+Variant AudioStreamPlaybackInteractive::get_parameter(const StringName &p_name) const {
+ if (p_name == SNAME("switch_to_clip")) {
+ for (int i = 0; i < stream->get_clip_count(); i++) {
+ if (switch_request != -1) {
+ if (switch_request == i) {
+ return String(stream->get_clip_name(i));
+ }
+ } else if (playback_current == i) {
+ return String(stream->get_clip_name(i));
+ }
+ }
+ return "";
+ }
+
+ return Variant();
+}
+
+void AudioStreamPlaybackInteractive::switch_to_clip(int p_index) {
+ switch_request = p_index;
+}
+
+int AudioStreamPlaybackInteractive::get_loop_count() const {
+ return 0; // Looping not supported
+}
+
+double AudioStreamPlaybackInteractive::get_playback_position() const {
+ return 0.0;
+}
+
+bool AudioStreamPlaybackInteractive::is_playing() const {
+ return active;
+}
+
+void AudioStreamPlaybackInteractive::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("switch_to_clip_by_name", "clip_name"), &AudioStreamPlaybackInteractive::switch_to_clip_by_name);
+ ClassDB::bind_method(D_METHOD("switch_to_clip", "clip_index"), &AudioStreamPlaybackInteractive::switch_to_clip);
+}
diff --git a/modules/interactive_music/audio_stream_interactive.h b/modules/interactive_music/audio_stream_interactive.h
new file mode 100644
index 000000000000..12d3ce8aad86
--- /dev/null
+++ b/modules/interactive_music/audio_stream_interactive.h
@@ -0,0 +1,270 @@
+/**************************************************************************/
+/* audio_stream_interactive.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. */
+/**************************************************************************/
+
+#ifndef AUDIO_STREAM_INTERACTIVE_H
+#define AUDIO_STREAM_INTERACTIVE_H
+
+#include "servers/audio/audio_stream.h"
+
+class AudioStreamPlaybackInteractive;
+
+class AudioStreamInteractive : public AudioStream {
+ GDCLASS(AudioStreamInteractive, AudioStream)
+ OBJ_SAVE_TYPE(AudioStream)
+public:
+ enum TransitionFromTime {
+ TRANSITION_FROM_TIME_IMMEDIATE,
+ TRANSITION_FROM_TIME_NEXT_BEAT,
+ TRANSITION_FROM_TIME_NEXT_BAR,
+ TRANSITION_FROM_TIME_END,
+ TRANSITION_FROM_TIME_MAX
+ };
+
+ enum TransitionToTime {
+ TRANSITION_TO_TIME_SAME_POSITION,
+ TRANSITION_TO_TIME_START,
+ TRANSITION_TO_TIME_PREVIOUS_POSITION,
+ TRANSITION_TO_TIME_MAX,
+ };
+
+ enum FadeMode {
+ FADE_DISABLED,
+ FADE_IN,
+ FADE_OUT,
+ FADE_CROSS,
+ FADE_AUTOMATIC,
+ FADE_MAX
+ };
+
+ enum AutoAdvanceMode {
+ AUTO_ADVANCE_DISABLED,
+ AUTO_ADVANCE_ENABLED,
+ AUTO_ADVANCE_RETURN_TO_HOLD,
+ };
+
+ enum {
+ CLIP_ANY = -1
+ };
+
+private:
+ friend class AudioStreamPlaybackInteractive;
+ int sample_rate = 44100;
+ bool stereo = true;
+ int initial_clip = 0;
+
+ double time = 0;
+
+ enum {
+ MAX_CLIPS = 63, // Because we use bitmasks for transition matching.
+ MAX_TRANSITIONS = 63,
+ };
+
+ struct Clip {
+ StringName name;
+ Ref stream;
+
+ AutoAdvanceMode auto_advance = AUTO_ADVANCE_DISABLED;
+ int auto_advance_next_clip = 0;
+ };
+
+ Clip clips[MAX_CLIPS];
+
+ struct Transition {
+ TransitionFromTime from_time = TRANSITION_FROM_TIME_NEXT_BEAT;
+ TransitionToTime to_time = TRANSITION_TO_TIME_START;
+ FadeMode fade_mode = FADE_AUTOMATIC;
+ int fade_beats = 1;
+ bool use_filler_clip = false;
+ int filler_clip = 0;
+ bool hold_previous = false;
+ };
+
+ struct TransitionKey {
+ uint32_t from_clip;
+ uint32_t to_clip;
+ bool operator==(const TransitionKey &p_key) const {
+ return from_clip == p_key.from_clip && to_clip == p_key.to_clip;
+ }
+ TransitionKey(uint32_t p_from_clip = 0, uint32_t p_to_clip = 0) {
+ from_clip = p_from_clip;
+ to_clip = p_to_clip;
+ }
+ };
+
+ struct TransitionKeyHasher {
+ static _FORCE_INLINE_ uint32_t hash(const TransitionKey &p_key) {
+ uint32_t h = hash_murmur3_one_32(p_key.from_clip);
+ return hash_murmur3_one_32(p_key.to_clip, h);
+ }
+ };
+
+ HashMap transition_map;
+
+ uint64_t version = 1; // Used to stop playback instances for incompatibility.
+ int clip_count = 0;
+
+ HashSet playbacks;
+
+#ifdef TOOLS_ENABLED
+
+ mutable String stream_name_cache;
+ String _get_streams_hint() const;
+ PackedStringArray _get_linked_undo_properties(const String &p_property, const Variant &p_new_value) const;
+ void _inspector_array_swap_clip(uint32_t p_item_a, uint32_t p_item_b);
+
+#endif
+
+ void _set_transitions(const Dictionary &p_transitions);
+ Dictionary _get_transitions() const;
+
+public:
+ // CLIPS
+ void set_clip_count(int p_count);
+ int get_clip_count() const;
+
+ void set_initial_clip(int p_clip);
+ int get_initial_clip() const;
+
+ void set_clip_name(int p_clip, const StringName &p_name);
+ StringName get_clip_name(int p_clip) const;
+
+ void set_clip_stream(int p_clip, const Ref &p_stream);
+ Ref get_clip_stream(int p_clip) const;
+
+ void set_clip_auto_advance(int p_clip, AutoAdvanceMode p_mode);
+ AutoAdvanceMode get_clip_auto_advance(int p_clip) const;
+
+ void set_clip_auto_advance_next_clip(int p_clip, int p_index);
+ int get_clip_auto_advance_next_clip(int p_clip) const;
+
+ // TRANSITIONS
+
+ void add_transition(int p_from_clip, int p_to_clip, TransitionFromTime p_from_time, TransitionToTime p_to_time, FadeMode p_fade_mode, float p_fade_beats, bool p_use_filler_flip = false, int p_filler_clip = -1, bool p_hold_previous = false);
+ TransitionFromTime get_transition_from_time(int p_from_clip, int p_to_clip) const;
+ TransitionToTime get_transition_to_time(int p_from_clip, int p_to_clip) const;
+ FadeMode get_transition_fade_mode(int p_from_clip, int p_to_clip) const;
+ float get_transition_fade_beats(int p_from_clip, int p_to_clip) const;
+ bool is_transition_using_filler_clip(int p_from_clip, int p_to_clip) const;
+ int get_transition_filler_clip(int p_from_clip, int p_to_clip) const;
+ bool is_transition_holding_previous(int p_from_clip, int p_to_clip) const;
+
+ bool has_transition(int p_from_clip, int p_to_clip) const;
+ void erase_transition(int p_from_clip, int p_to_clip);
+
+ PackedInt32Array get_transition_list() const;
+
+ virtual Ref instantiate_playback() override;
+ virtual String get_stream_name() const override;
+ virtual double get_length() const override { return 0; }
+ AudioStreamInteractive();
+
+protected:
+ virtual void get_parameter_list(List *r_parameters) override;
+
+ static void _bind_methods();
+ void _validate_property(PropertyInfo &r_property) const;
+};
+
+VARIANT_ENUM_CAST(AudioStreamInteractive::TransitionFromTime)
+VARIANT_ENUM_CAST(AudioStreamInteractive::TransitionToTime)
+VARIANT_ENUM_CAST(AudioStreamInteractive::AutoAdvanceMode)
+VARIANT_ENUM_CAST(AudioStreamInteractive::FadeMode)
+
+class AudioStreamPlaybackInteractive : public AudioStreamPlayback {
+ GDCLASS(AudioStreamPlaybackInteractive, AudioStreamPlayback)
+ friend class AudioStreamInteractive;
+
+private:
+ Ref stream;
+ uint64_t version = 0;
+
+ enum {
+ BUFFER_SIZE = 1024
+ };
+
+ AudioFrame mix_buffer[BUFFER_SIZE];
+ AudioFrame temp_buffer[BUFFER_SIZE];
+
+ struct State {
+ Ref stream;
+ Ref playback;
+ bool active = false;
+ double fade_wait = 0; // Time to wait until fade kicks-in
+ double fade_volume = 1.0;
+ double fade_speed = 0; // Fade speed, negative or positive
+ int auto_advance = -1;
+ bool first_mix = true;
+ double previous_position = 0;
+
+ void reset_fade() {
+ fade_wait = 0;
+ fade_volume = 1.0;
+ fade_speed = 0;
+ }
+ };
+
+ State states[AudioStreamInteractive::MAX_CLIPS];
+ int playback_current = -1;
+
+ bool active = false;
+ int return_memory = -1;
+
+ void _mix_internal(int p_frames);
+ void _mix_internal_state(int p_state_idx, int p_frames);
+
+ void _queue(int p_to_clip_index, bool p_is_auto_advance);
+
+ int switch_request = -1;
+
+protected:
+ static void _bind_methods();
+
+public:
+ virtual void start(double p_from_pos = 0.0) override;
+ virtual void stop() override;
+ virtual bool is_playing() const override;
+ virtual int get_loop_count() const override; // times it looped
+ virtual double get_playback_position() const override;
+ virtual void seek(double p_time) override;
+ virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
+
+ virtual void tag_used_streams() override;
+
+ void switch_to_clip_by_name(const StringName &p_name);
+ void switch_to_clip(int p_index);
+
+ virtual void set_parameter(const StringName &p_name, const Variant &p_value) override;
+ virtual Variant get_parameter(const StringName &p_name) const override;
+
+ AudioStreamPlaybackInteractive();
+ ~AudioStreamPlaybackInteractive();
+};
+
+#endif // AUDIO_STREAM_INTERACTIVE_H
diff --git a/modules/interactive_music/audio_stream_playlist.cpp b/modules/interactive_music/audio_stream_playlist.cpp
new file mode 100644
index 000000000000..f47035b30cf1
--- /dev/null
+++ b/modules/interactive_music/audio_stream_playlist.cpp
@@ -0,0 +1,406 @@
+/**************************************************************************/
+/* audio_stream_playlist.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 "audio_stream_playlist.h"
+
+#include "core/math/math_funcs.h"
+#include "core/string/print_string.h"
+
+Ref AudioStreamPlaylist::instantiate_playback() {
+ Ref playback_playlist;
+ playback_playlist.instantiate();
+ playback_playlist->playlist = Ref(this);
+ playback_playlist->_update_playback_instances();
+ playbacks.insert(playback_playlist.operator->());
+ return playback_playlist;
+}
+
+String AudioStreamPlaylist::get_stream_name() const {
+ return "Playlist";
+}
+
+void AudioStreamPlaylist::set_list_stream(int p_stream_index, Ref p_stream) {
+ ERR_FAIL_COND(p_stream == this);
+ ERR_FAIL_INDEX(p_stream_index, MAX_STREAMS);
+
+ AudioServer::get_singleton()->lock();
+ audio_streams[p_stream_index] = p_stream;
+ for (AudioStreamPlaybackPlaylist *E : playbacks) {
+ E->_update_playback_instances();
+ }
+ AudioServer::get_singleton()->unlock();
+}
+
+Ref AudioStreamPlaylist::get_list_stream(int p_stream_index) const {
+ ERR_FAIL_INDEX_V(p_stream_index, MAX_STREAMS, Ref());
+
+ return audio_streams[p_stream_index];
+}
+
+double AudioStreamPlaylist::get_bpm() const {
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ double bpm = audio_streams[i]->get_bpm();
+ if (bpm != 0.0) {
+ return bpm;
+ }
+ }
+ }
+ return 0.0;
+}
+
+double AudioStreamPlaylist::get_length() const {
+ double total_length = 0.0;
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ double bpm = audio_streams[i]->get_bpm();
+ int beat_count = audio_streams[i]->get_beat_count();
+ if (bpm > 0.0 && beat_count > 0) {
+ total_length += beat_count * 60.0 / bpm;
+ } else {
+ total_length += audio_streams[i]->get_length();
+ }
+ }
+ }
+ return total_length;
+}
+
+void AudioStreamPlaylist::set_stream_count(int p_count) {
+ ERR_FAIL_COND(p_count < 0 || p_count > MAX_STREAMS);
+ AudioServer::get_singleton()->lock();
+ stream_count = p_count;
+ AudioServer::get_singleton()->unlock();
+ notify_property_list_changed();
+}
+
+int AudioStreamPlaylist::get_stream_count() const {
+ return stream_count;
+}
+
+void AudioStreamPlaylist::set_fade_time(float p_time) {
+ fade_time = p_time;
+}
+
+float AudioStreamPlaylist::get_fade_time() const {
+ return fade_time;
+}
+
+void AudioStreamPlaylist::set_shuffle(bool p_shuffle) {
+ shuffle = p_shuffle;
+}
+
+bool AudioStreamPlaylist::get_shuffle() const {
+ return shuffle;
+}
+
+void AudioStreamPlaylist::set_loop(bool p_loop) {
+ loop = p_loop;
+}
+
+bool AudioStreamPlaylist::has_loop() const {
+ return loop;
+}
+
+void AudioStreamPlaylist::_validate_property(PropertyInfo &r_property) const {
+ String prop = r_property.name;
+ if (prop != "stream_count" && prop.begins_with("stream_")) {
+ int stream = prop.get_slicec('/', 0).get_slicec('_', 1).to_int();
+ if (stream >= stream_count) {
+ r_property.usage = PROPERTY_USAGE_INTERNAL;
+ }
+ }
+}
+
+void AudioStreamPlaylist::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_stream_count", "stream_count"), &AudioStreamPlaylist::set_stream_count);
+ ClassDB::bind_method(D_METHOD("get_stream_count"), &AudioStreamPlaylist::get_stream_count);
+
+ ClassDB::bind_method(D_METHOD("get_bpm"), &AudioStreamPlaylist::get_bpm);
+
+ ClassDB::bind_method(D_METHOD("set_list_stream", "stream_index", "audio_stream"), &AudioStreamPlaylist::set_list_stream);
+ ClassDB::bind_method(D_METHOD("get_list_stream", "stream_index"), &AudioStreamPlaylist::get_list_stream);
+
+ ClassDB::bind_method(D_METHOD("set_shuffle", "shuffle"), &AudioStreamPlaylist::set_shuffle);
+ ClassDB::bind_method(D_METHOD("get_shuffle"), &AudioStreamPlaylist::get_shuffle);
+
+ ClassDB::bind_method(D_METHOD("set_fade_time", "dec"), &AudioStreamPlaylist::set_fade_time);
+ ClassDB::bind_method(D_METHOD("get_fade_time"), &AudioStreamPlaylist::get_fade_time);
+
+ ClassDB::bind_method(D_METHOD("set_loop", "loop"), &AudioStreamPlaylist::set_loop);
+ ClassDB::bind_method(D_METHOD("has_loop"), &AudioStreamPlaylist::has_loop);
+
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shuffle"), "set_shuffle", "get_shuffle");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fade_time", PROPERTY_HINT_RANGE, "0,1,0.01,suffix:s"), "set_fade_time", "get_fade_time");
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "stream_count", PROPERTY_HINT_RANGE, "0," + itos(MAX_STREAMS), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Streams,stream_,unfoldable,page_size=999,add_button_text=" + String(RTR("Add Stream"))), "set_stream_count", "get_stream_count");
+
+ for (int i = 0; i < MAX_STREAMS; i++) {
+ ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "stream_" + itos(i), PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_list_stream", "get_list_stream", i);
+ }
+
+ BIND_CONSTANT(MAX_STREAMS);
+}
+
+//////////////////////
+//////////////////////
+
+AudioStreamPlaybackPlaylist::~AudioStreamPlaybackPlaylist() {
+ if (playlist.is_valid()) {
+ playlist->playbacks.erase(this);
+ }
+}
+
+void AudioStreamPlaybackPlaylist::stop() {
+ active = false;
+ for (int i = 0; i < playlist->stream_count; i++) {
+ if (playback[i].is_valid()) {
+ playback[i]->stop();
+ }
+ }
+}
+
+void AudioStreamPlaybackPlaylist::_update_order() {
+ for (int i = 0; i < playlist->stream_count; i++) {
+ play_order[i] = i;
+ }
+
+ if (playlist->shuffle) {
+ for (int i = 0; i < playlist->stream_count; i++) {
+ int swap_with = Math::rand() % uint32_t(playlist->stream_count);
+ SWAP(play_order[i], play_order[swap_with]);
+ }
+ }
+}
+
+void AudioStreamPlaybackPlaylist::start(double p_from_pos) {
+ if (active) {
+ stop();
+ }
+
+ p_from_pos = MAX(0, p_from_pos);
+
+ float pl_length = playlist->get_length();
+ if (p_from_pos >= pl_length) {
+ if (!playlist->loop) {
+ return; // No loop, end.
+ }
+ p_from_pos = Math::fmod((float)p_from_pos, (float)pl_length);
+ }
+
+ _update_order();
+
+ play_index = -1;
+
+ double play_ofs = p_from_pos;
+ for (int i = 0; i < playlist->stream_count; i++) {
+ int idx = play_order[i];
+ if (playlist->audio_streams[idx].is_valid()) {
+ double bpm = playlist->audio_streams[idx]->get_bpm();
+ int beat_count = playlist->audio_streams[idx]->get_beat_count();
+ double length;
+ if (bpm > 0.0 && beat_count > 0) {
+ length = beat_count * 60.0 / bpm;
+ } else {
+ length = playlist->audio_streams[idx]->get_length();
+ }
+ if (play_ofs < length) {
+ play_index = i;
+ stream_todo = length - play_ofs;
+ break;
+ } else {
+ play_ofs -= length;
+ }
+ }
+ }
+
+ if (play_index == -1) {
+ return;
+ }
+
+ playback[play_order[play_index]]->start(play_ofs);
+ fade_index = -1;
+ loop_count = 0;
+
+ active = true;
+}
+
+void AudioStreamPlaybackPlaylist::seek(double p_time) {
+ stop();
+ start(p_time);
+}
+
+int AudioStreamPlaybackPlaylist::mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) {
+ if (!active) {
+ for (int i = 0; i < p_frames; i++) {
+ p_buffer[i] = AudioFrame(0.0, 0.0);
+ }
+ return p_frames;
+ }
+
+ double time_dec = (1.0 / AudioServer::get_singleton()->get_mix_rate());
+ double fade_dec = (1.0 / playlist->fade_time) / AudioServer::get_singleton()->get_mix_rate();
+
+ int todo = p_frames;
+
+ while (todo) {
+ int to_mix = MIN(todo, MIX_BUFFER_SIZE);
+
+ playback[play_order[play_index]]->mix(mix_buffer, 1.0, to_mix);
+ if (fade_index != -1) {
+ playback[fade_index]->mix(fade_buffer, 1.0, to_mix);
+ }
+
+ offset += time_dec * to_mix;
+
+ for (int i = 0; i < to_mix; i++) {
+ *p_buffer = mix_buffer[i];
+ stream_todo -= time_dec;
+ if (stream_todo < 0) {
+ //find next stream.
+ int prev = play_order[play_index];
+
+ for (int j = 0; j < playlist->stream_count; j++) {
+ play_index++;
+ if (play_index >= playlist->stream_count) {
+ // No loop, exit.
+ if (!playlist->loop) {
+ for (int k = i; k < todo - i; k++) {
+ p_buffer[k] = AudioFrame(0, 0);
+ }
+ todo = to_mix;
+ active = false;
+ break;
+ }
+
+ _update_order();
+ play_index = 0;
+ loop_count++;
+ offset = time_dec * (to_mix - i);
+ }
+ if (playback[play_order[play_index]].is_valid()) {
+ break;
+ }
+ }
+
+ if (!active) {
+ break;
+ }
+
+ if (!playback[play_order[play_index]].is_valid()) {
+ todo = to_mix; // Weird error.
+ active = false;
+ break;
+ }
+
+ bool restart = true;
+ if (prev == play_order[play_index]) {
+ // Went back to the same one, continue loop (if it loops) or restart if it does not.
+ if (playlist->audio_streams[prev]->has_loop()) {
+ restart = false;
+ }
+ fade_index = -1;
+ } else {
+ // Move current mixed data to fade buffer.
+ for (int j = i; j < to_mix; j++) {
+ fade_buffer[j] = mix_buffer[j];
+ }
+
+ fade_index = prev;
+ fade_volume = 1.0;
+ }
+
+ int idx = play_order[play_index];
+
+ if (restart) {
+ playback[idx]->start(0); // No loop, just cold-restart.
+ playback[idx]->mix(mix_buffer + i, 1.0, to_mix - i); // Fill rest of mix buffer
+ }
+
+ // Update fade todo.
+ double bpm = playlist->audio_streams[idx]->get_bpm();
+ int beat_count = playlist->audio_streams[idx]->get_beat_count();
+
+ if (bpm > 0.0 && beat_count > 0) {
+ stream_todo = beat_count * 60.0 / bpm;
+ } else {
+ stream_todo = playlist->audio_streams[idx]->get_length();
+ }
+ }
+
+ if (fade_index != -1) {
+ *p_buffer += fade_buffer[i] * fade_volume;
+ fade_volume -= fade_dec;
+ if (fade_volume <= 0.0) {
+ playback[fade_index]->stop();
+ fade_index = -1;
+ }
+ }
+
+ p_buffer++;
+ }
+
+ todo -= to_mix;
+ }
+
+ return p_frames;
+}
+
+void AudioStreamPlaybackPlaylist::tag_used_streams() {
+ if (active) {
+ playlist->audio_streams[play_order[play_index]]->tag_used(playback[play_order[play_index]]->get_playback_position());
+ }
+
+ playlist->tag_used(0);
+}
+
+int AudioStreamPlaybackPlaylist::get_loop_count() const {
+ return loop_count;
+}
+
+double AudioStreamPlaybackPlaylist::get_playback_position() const {
+ return offset;
+}
+
+bool AudioStreamPlaybackPlaylist::is_playing() const {
+ return active;
+}
+
+void AudioStreamPlaybackPlaylist::_update_playback_instances() {
+ stop();
+
+ for (int i = 0; i < playlist->stream_count; i++) {
+ if (playlist->audio_streams[i].is_valid()) {
+ playback[i] = playlist->audio_streams[i]->instantiate_playback();
+ } else {
+ playback[i].unref();
+ }
+ }
+}
diff --git a/modules/interactive_music/audio_stream_playlist.h b/modules/interactive_music/audio_stream_playlist.h
new file mode 100644
index 000000000000..a30f97b7aff2
--- /dev/null
+++ b/modules/interactive_music/audio_stream_playlist.h
@@ -0,0 +1,125 @@
+/**************************************************************************/
+/* audio_stream_playlist.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. */
+/**************************************************************************/
+
+#ifndef AUDIO_STREAM_PLAYLIST_H
+#define AUDIO_STREAM_PLAYLIST_H
+
+#include "servers/audio/audio_stream.h"
+
+class AudioStreamPlaybackPlaylist;
+
+class AudioStreamPlaylist : public AudioStream {
+ GDCLASS(AudioStreamPlaylist, AudioStream)
+ OBJ_SAVE_TYPE(AudioStream)
+
+private:
+ friend class AudioStreamPlaybackPlaylist;
+
+ enum {
+ MAX_STREAMS = 64
+ };
+
+ bool shuffle = false;
+ bool loop = true;
+ double fade_time = 0.3;
+
+ int stream_count = 0;
+ Ref audio_streams[MAX_STREAMS];
+ HashSet playbacks;
+
+public:
+ virtual double get_bpm() const override;
+ void set_stream_count(int p_count);
+ int get_stream_count() const;
+ void set_fade_time(float p_time);
+ float get_fade_time() const;
+ void set_shuffle(bool p_shuffle);
+ bool get_shuffle() const;
+ void set_loop(bool p_loop);
+ virtual bool has_loop() const override;
+ void set_list_stream(int p_stream_index, Ref p_stream);
+ Ref get_list_stream(int p_stream_index) const;
+
+ virtual Ref instantiate_playback() override;
+ virtual String get_stream_name() const override;
+ virtual double get_length() const override;
+
+protected:
+ static void _bind_methods();
+ void _validate_property(PropertyInfo &r_property) const;
+};
+
+///////////////////////////////////////
+
+class AudioStreamPlaybackPlaylist : public AudioStreamPlayback {
+ GDCLASS(AudioStreamPlaybackPlaylist, AudioStreamPlayback)
+ friend class AudioStreamPlaylist;
+
+private:
+ enum {
+ MIX_BUFFER_SIZE = 128
+ };
+ AudioFrame mix_buffer[MIX_BUFFER_SIZE];
+ AudioFrame fade_buffer[MIX_BUFFER_SIZE];
+
+ Ref playlist;
+ Ref playback[AudioStreamPlaylist::MAX_STREAMS];
+
+ int play_order[AudioStreamPlaylist::MAX_STREAMS] = {};
+
+ double stream_todo = 0.0;
+ int fade_index = -1;
+ double fade_volume = 1.0;
+ int play_index = 0;
+ double offset = 0.0;
+
+ int loop_count = 0;
+
+ bool active = false;
+
+ void _update_order();
+
+ void _update_playback_instances();
+
+public:
+ virtual void start(double p_from_pos = 0.0) override;
+ virtual void stop() override;
+ virtual bool is_playing() const override;
+ virtual int get_loop_count() const override; // times it looped
+ virtual double get_playback_position() const override;
+ virtual void seek(double p_time) override;
+ virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
+
+ virtual void tag_used_streams() override;
+
+ ~AudioStreamPlaybackPlaylist();
+};
+
+#endif // AUDIO_STREAM_PLAYLIST_H
diff --git a/modules/interactive_music/audio_stream_synchronized.cpp b/modules/interactive_music/audio_stream_synchronized.cpp
new file mode 100644
index 000000000000..d0d58fac161f
--- /dev/null
+++ b/modules/interactive_music/audio_stream_synchronized.cpp
@@ -0,0 +1,312 @@
+/**************************************************************************/
+/* audio_stream_synchronized.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 "audio_stream_synchronized.h"
+
+#include "core/math/math_funcs.h"
+#include "core/string/print_string.h"
+
+AudioStreamSynchronized::AudioStreamSynchronized() {
+}
+
+Ref AudioStreamSynchronized::instantiate_playback() {
+ Ref playback_playlist;
+ playback_playlist.instantiate();
+ playback_playlist->stream = Ref(this);
+ playback_playlist->_update_playback_instances();
+ playbacks.insert(playback_playlist.operator->());
+ return playback_playlist;
+}
+
+String AudioStreamSynchronized::get_stream_name() const {
+ return "Synchronized";
+}
+
+void AudioStreamSynchronized::set_sync_stream(int p_stream_index, Ref p_stream) {
+ ERR_FAIL_COND(p_stream == this);
+ ERR_FAIL_INDEX(p_stream_index, MAX_STREAMS);
+
+ AudioServer::get_singleton()->lock();
+ audio_streams[p_stream_index] = p_stream;
+ for (AudioStreamPlaybackSynchronized *E : playbacks) {
+ E->_update_playback_instances();
+ }
+ AudioServer::get_singleton()->unlock();
+}
+
+Ref AudioStreamSynchronized::get_sync_stream(int p_stream_index) const {
+ ERR_FAIL_INDEX_V(p_stream_index, MAX_STREAMS, Ref());
+
+ return audio_streams[p_stream_index];
+}
+
+void AudioStreamSynchronized::set_sync_stream_volume(int p_stream_index, float p_db) {
+ ERR_FAIL_INDEX(p_stream_index, MAX_STREAMS);
+ audio_stream_volume_db[p_stream_index] = p_db;
+}
+
+float AudioStreamSynchronized::get_sync_stream_volume(int p_stream_index) const {
+ ERR_FAIL_INDEX_V(p_stream_index, MAX_STREAMS, 0);
+ return audio_stream_volume_db[p_stream_index];
+}
+
+double AudioStreamSynchronized::get_bpm() const {
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ double bpm = audio_streams[i]->get_bpm();
+ if (bpm != 0.0) {
+ return bpm;
+ }
+ }
+ }
+ return 0.0;
+}
+
+int AudioStreamSynchronized::get_beat_count() const {
+ int max_beats = 0;
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ max_beats = MAX(max_beats, audio_streams[i]->get_beat_count());
+ }
+ }
+ return max_beats;
+}
+
+bool AudioStreamSynchronized::has_loop() const {
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ if (audio_streams[i]->has_loop()) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+double AudioStreamSynchronized::get_length() const {
+ double max_length = 0.0;
+ for (int i = 0; i < stream_count; i++) {
+ if (audio_streams[i].is_valid()) {
+ max_length = MAX(max_length, audio_streams[i]->get_length());
+ }
+ }
+ return max_length;
+}
+
+void AudioStreamSynchronized::set_stream_count(int p_count) {
+ ERR_FAIL_COND(p_count < 0 || p_count > MAX_STREAMS);
+ AudioServer::get_singleton()->lock();
+ stream_count = p_count;
+ AudioServer::get_singleton()->unlock();
+ notify_property_list_changed();
+}
+
+int AudioStreamSynchronized::get_stream_count() const {
+ return stream_count;
+}
+
+void AudioStreamSynchronized::_validate_property(PropertyInfo &property) const {
+ String prop = property.name;
+ if (prop != "stream_count" && prop.begins_with("stream_")) {
+ int stream = prop.get_slicec('/', 0).get_slicec('_', 1).to_int();
+ if (stream >= stream_count) {
+ property.usage = PROPERTY_USAGE_INTERNAL;
+ }
+ }
+}
+
+void AudioStreamSynchronized::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_stream_count", "stream_count"), &AudioStreamSynchronized::set_stream_count);
+ ClassDB::bind_method(D_METHOD("get_stream_count"), &AudioStreamSynchronized::get_stream_count);
+
+ ClassDB::bind_method(D_METHOD("set_sync_stream", "stream_index", "audio_stream"), &AudioStreamSynchronized::set_sync_stream);
+ ClassDB::bind_method(D_METHOD("get_sync_stream", "stream_index"), &AudioStreamSynchronized::get_sync_stream);
+ ClassDB::bind_method(D_METHOD("set_sync_stream_volume", "stream_index", "volume_db"), &AudioStreamSynchronized::set_sync_stream_volume);
+ ClassDB::bind_method(D_METHOD("get_sync_stream_volume", "stream_index"), &AudioStreamSynchronized::get_sync_stream_volume);
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "stream_count", PROPERTY_HINT_RANGE, "0," + itos(MAX_STREAMS), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Streams,stream_,unfoldable,page_size=999,add_button_text=" + String(RTR("Add Stream"))), "set_stream_count", "get_stream_count");
+
+ for (int i = 0; i < MAX_STREAMS; i++) {
+ ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "stream_" + itos(i) + "/stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_sync_stream", "get_sync_stream", i);
+ ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "stream_" + itos(i) + "/volume", PROPERTY_HINT_RANGE, "-60,12,0.01,suffix:db", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_sync_stream_volume", "get_sync_stream_volume", i);
+ }
+
+ BIND_CONSTANT(MAX_STREAMS);
+}
+
+//////////////////////
+//////////////////////
+
+AudioStreamPlaybackSynchronized::AudioStreamPlaybackSynchronized() {
+}
+
+AudioStreamPlaybackSynchronized::~AudioStreamPlaybackSynchronized() {
+ if (stream.is_valid()) {
+ stream->playbacks.erase(this);
+ }
+}
+
+void AudioStreamPlaybackSynchronized::stop() {
+ active = false;
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid()) {
+ playback[i]->stop();
+ }
+ }
+}
+
+void AudioStreamPlaybackSynchronized::start(double p_from_pos) {
+ if (active) {
+ stop();
+ }
+
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid()) {
+ playback[i]->start(p_from_pos);
+ active = true;
+ }
+ }
+}
+
+void AudioStreamPlaybackSynchronized::seek(double p_time) {
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid()) {
+ playback[i]->seek(p_time);
+ }
+ }
+}
+
+int AudioStreamPlaybackSynchronized::mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) {
+ if (active != true) {
+ for (int i = 0; i < p_frames; i++) {
+ p_buffer[i] = AudioFrame(0.0, 0.0);
+ }
+ return p_frames;
+ }
+
+ int todo = p_frames;
+
+ bool any_active = false;
+ while (todo) {
+ int to_mix = MIN(todo, MIX_BUFFER_SIZE);
+
+ bool first = true;
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid() && playback[i]->is_playing()) {
+ float volume = Math::db_to_linear(stream->audio_stream_volume_db[i]);
+ if (first) {
+ playback[i]->mix(p_buffer, p_rate_scale, to_mix);
+ for (int j = 0; j < to_mix; j++) {
+ p_buffer[j] *= volume;
+ }
+ first = false;
+ any_active = true;
+ } else {
+ playback[i]->mix(mix_buffer, p_rate_scale, to_mix);
+ for (int j = 0; j < to_mix; j++) {
+ p_buffer[j] += mix_buffer[j] * volume;
+ }
+ }
+ }
+ }
+
+ if (first) {
+ // Nothing mixed, put zeroes.
+ for (int j = 0; j < to_mix; j++) {
+ p_buffer[j] = AudioFrame(0, 0);
+ }
+ }
+
+ p_buffer += to_mix;
+ todo -= to_mix;
+ }
+
+ if (!any_active) {
+ active = false;
+ }
+ return p_frames;
+}
+
+void AudioStreamPlaybackSynchronized::tag_used_streams() {
+ if (active) {
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid() && playback[i]->is_playing()) {
+ stream->audio_streams[i]->tag_used(playback[i]->get_playback_position());
+ }
+ }
+ stream->tag_used(0);
+ }
+}
+
+int AudioStreamPlaybackSynchronized::get_loop_count() const {
+ int min_loops = 0;
+ bool min_loops_found = false;
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid() && playback[i]->is_playing()) {
+ int loops = playback[i]->get_loop_count();
+ if (!min_loops_found || loops < min_loops) {
+ min_loops = loops;
+ min_loops_found = true;
+ }
+ }
+ }
+ return min_loops;
+}
+
+double AudioStreamPlaybackSynchronized::get_playback_position() const {
+ float max_pos = 0;
+ bool pos_found = false;
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (playback[i].is_valid() && playback[i]->is_playing()) {
+ float pos = playback[i]->get_playback_position();
+ if (!pos_found || pos > max_pos) {
+ max_pos = pos;
+ pos_found = true;
+ }
+ }
+ }
+ return max_pos;
+}
+
+bool AudioStreamPlaybackSynchronized::is_playing() const {
+ return active;
+}
+
+void AudioStreamPlaybackSynchronized::_update_playback_instances() {
+ stop();
+
+ for (int i = 0; i < stream->stream_count; i++) {
+ if (stream->audio_streams[i].is_valid()) {
+ playback[i] = stream->audio_streams[i]->instantiate_playback();
+ } else {
+ playback[i].unref();
+ }
+ }
+}
diff --git a/modules/interactive_music/audio_stream_synchronized.h b/modules/interactive_music/audio_stream_synchronized.h
new file mode 100644
index 000000000000..a2d8c5540443
--- /dev/null
+++ b/modules/interactive_music/audio_stream_synchronized.h
@@ -0,0 +1,119 @@
+/**************************************************************************/
+/* audio_stream_synchronized.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. */
+/**************************************************************************/
+
+#ifndef AUDIO_STREAM_SYNCHRONIZED_H
+#define AUDIO_STREAM_SYNCHRONIZED_H
+
+#include "servers/audio/audio_stream.h"
+
+class AudioStreamPlaybackSynchronized;
+
+class AudioStreamSynchronized : public AudioStream {
+ GDCLASS(AudioStreamSynchronized, AudioStream)
+ OBJ_SAVE_TYPE(AudioStream)
+
+private:
+ friend class AudioStreamPlaybackSynchronized;
+
+ enum {
+ MAX_STREAMS = 32
+ };
+
+ int stream_count = 0;
+ Ref audio_streams[MAX_STREAMS];
+ float audio_stream_volume_db[MAX_STREAMS] = {};
+ HashSet playbacks;
+
+public:
+ virtual double get_bpm() const override;
+ virtual int get_beat_count() const override;
+ virtual bool has_loop() const override;
+ void set_stream_count(int p_count);
+ int get_stream_count() const;
+ void set_sync_stream(int p_stream_index, Ref p_stream);
+ Ref get_sync_stream(int p_stream_index) const;
+ void set_sync_stream_volume(int p_stream_index, float p_db);
+ float get_sync_stream_volume(int p_stream_index) const;
+
+ virtual Ref instantiate_playback() override;
+ virtual String get_stream_name() const override;
+ virtual double get_length() const override;
+ AudioStreamSynchronized();
+
+protected:
+ static void _bind_methods();
+ void _validate_property(PropertyInfo &property) const;
+};
+
+///////////////////////////////////////
+
+class AudioStreamPlaybackSynchronized : public AudioStreamPlayback {
+ GDCLASS(AudioStreamPlaybackSynchronized, AudioStreamPlayback)
+ friend class AudioStreamSynchronized;
+
+private:
+ enum {
+ MIX_BUFFER_SIZE = 128
+ };
+ AudioFrame mix_buffer[MIX_BUFFER_SIZE];
+
+ Ref stream;
+ Ref playback[AudioStreamSynchronized::MAX_STREAMS];
+
+ int play_order[AudioStreamSynchronized::MAX_STREAMS];
+
+ double stream_todo = 0.0;
+ int fade_index = -1;
+ double fade_volume = 1.0;
+ int play_index = 0;
+ double offset = 0.0;
+
+ int loop_count = 0;
+
+ bool active = false;
+
+ void _update_playback_instances();
+
+public:
+ virtual void start(double p_from_pos = 0.0) override;
+ virtual void stop() override;
+ virtual bool is_playing() const override;
+ virtual int get_loop_count() const override; // times it looped
+ virtual double get_playback_position() const override;
+ virtual void seek(double p_time) override;
+ virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
+
+ virtual void tag_used_streams() override;
+
+ AudioStreamPlaybackSynchronized();
+ ~AudioStreamPlaybackSynchronized();
+};
+
+#endif // AUDIO_STREAM_SYNCHRONIZED_H
diff --git a/modules/interactive_music/config.py b/modules/interactive_music/config.py
new file mode 100644
index 000000000000..fb327f137280
--- /dev/null
+++ b/modules/interactive_music/config.py
@@ -0,0 +1,21 @@
+def can_build(env, platform):
+ return True
+
+
+def configure(env):
+ pass
+
+
+def get_doc_classes():
+ return [
+ "AudioStreamPlaylist",
+ "AudioStreamPlaybackPlaylist",
+ "AudioStreamInteractive",
+ "AudioStreamPlaybackInteractive",
+ "AudioStreamSynchronized",
+ "AudioStreamPlaybackSynchronized",
+ ]
+
+
+def get_doc_path():
+ return "doc_classes"
diff --git a/modules/interactive_music/doc_classes/AudioStreamInteractive.xml b/modules/interactive_music/doc_classes/AudioStreamInteractive.xml
new file mode 100644
index 000000000000..e8f8e7b76049
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamInteractive.xml
@@ -0,0 +1,229 @@
+
+
+
+ Audio stream that can playback music interactively, combining clips and a transition table.
+
+
+ This is an audio stream that can playback music interactively, combining clips and a transition table. Clips must be added first, and the transition rules via the [method add_transition]. Additionally, this stream export a property parameter to control the playback via [AudioStreamPlayer], [AudioStreamPlayer2D], or [AudioStreamPlayer3D].
+ The way this is used is by filling a number of clips, then configuring the transition table. From there, clips are selected for playback and the music will smoothly go from the current to the new one while using the corresponding transition rule defined in the transition table.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add a transition between two clips. Provide the indices of the source and destination clips, or use the [constant CLIP_ANY] constant to indicate that transition happens to/from any clip to this one.
+ * [param from_time] indicates the moment in the current clip the transition will begin after triggered.
+ * [param to_time] indicates the time in the next clip that the playback will start from.
+ * [param fade_mode] indicates how the fade will happen between clips. If unsure, just use [constant FADE_AUTOMATIC] which uses the most common type of fade for each situation.
+ * [param fade_beats] indicates how many beats the fade will take. Using decimals is allowed.
+ * [param use_filler_clip] indicates that there will be a filler clip used between the source and destination clips.
+ * [param filler_clip] the index of the filler clip.
+ * If [param hold_previous] is used, then this clip will be remembered. This can be used together with [constant AUTO_ADVANCE_RETURN_TO_HOLD] to return to this clip after another is done playing.
+
+
+
+
+
+
+
+ Erase a transition by providing [param from_clip] and [param to_clip] clip indices. [constant CLIP_ANY] can be used for either argument or both.
+
+
+
+
+
+
+ Return whether a clip has auto-advance enabled. See [method set_clip_auto_advance].
+
+
+
+
+
+
+ Return the clip towards which the clip referenced by [param clip_index] will auto-advance to.
+
+
+
+
+
+
+ Return the name of a clip.
+
+
+
+
+
+
+ Return the [AudioStream] associated with a clip.
+
+
+
+
+
+
+
+ Return the time (in beats) for a transition (see [method add_transition]).
+
+
+
+
+
+
+
+ Return the mode for a transition (see [method add_transition]).
+
+
+
+
+
+
+
+ Return the filler clip for a transition (see [method add_transition]).
+
+
+
+
+
+
+
+ Return the source time position for a transition (see [method add_transition]).
+
+
+
+
+
+ Return the list of transitions (from, to interleaved).
+
+
+
+
+
+
+
+ Return the destination time position for a transition (see [method add_transition]).
+
+
+
+
+
+
+
+ Return true if a given transition exists (was added via [method add_transition]).
+
+
+
+
+
+
+
+ Return whether a transition uses the [i]hold previous[/i] functionality (see [method add_transition]).
+
+
+
+
+
+
+
+ Return whether a transition uses the [i]filler clip[/i] functionality (see [method add_transition]).
+
+
+
+
+
+
+
+ Set whether a clip will auto-advance by changing the auto-advance mode.
+
+
+
+
+
+
+
+ Set the index of the next clip towards which this clip will auto advance to when finished. If the clip being played loops, then auto-advance will be ignored.
+
+
+
+
+
+
+
+ Set the name of the current clip (for easier identification).
+
+
+
+
+
+
+
+ Set the [AudioStream] associated with the current clip.
+
+
+
+
+
+ Amount of clips contained in this interactive player.
+
+
+ Index of the initial clip, which will be played first when this stream is played.
+
+
+
+
+ Start transition as soon as possible, don't wait for any specific time position.
+
+
+ Transition when the clip playback position reaches the next beat.
+
+
+ Transition when the clip playback position reaches the next bar.
+
+
+ Transition when the current clip finished playing.
+
+
+ Transition to the same position in the destination clip. This is useful when both clips have exactly the same length and the music should fade between them.
+
+
+ Transition to the start of the destination clip.
+
+
+ Do not use fade for the transition. This is useful when transitioning from a clip-end to clip-beginning, and each clip has their begin/end.
+
+
+ Use a fade-in in the next clip, let the current clip finish.
+
+
+ Use a fade-out in the current clip, the next clip will start by itself.
+
+
+ Use a cross-fade between clips.
+
+
+ Use automatic fade logic depending on the transition from/to. It is recommended to use this by default.
+
+
+ Disable auto-advance (default).
+
+
+ Enable auto-advance, a clip must be specified.
+
+
+ Enable auto-advance, but instead of specifying a clip, the playback will return to hold (see [method add_transition]).
+
+
+ This constant describes that any clip is valid for a specific transition as either source or destination.
+
+
+
diff --git a/modules/interactive_music/doc_classes/AudioStreamPlaybackInteractive.xml b/modules/interactive_music/doc_classes/AudioStreamPlaybackInteractive.xml
new file mode 100644
index 000000000000..c87d7c8fcbf3
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamPlaybackInteractive.xml
@@ -0,0 +1,27 @@
+
+
+
+ Playback component of [AudioStreamInteractive].
+
+
+ Playback component of [AudioStreamInteractive]. Contains functions to change the currently played clip.
+
+
+
+
+
+
+
+
+ Switch to a clip (by index).
+
+
+
+
+
+
+ Switch to a clip (by name).
+
+
+
+
diff --git a/modules/interactive_music/doc_classes/AudioStreamPlaybackPlaylist.xml b/modules/interactive_music/doc_classes/AudioStreamPlaybackPlaylist.xml
new file mode 100644
index 000000000000..25535630712a
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamPlaybackPlaylist.xml
@@ -0,0 +1,10 @@
+
+
+
+ Playback class used for [AudioStreamPlaylist].
+
+
+
+
+
+
diff --git a/modules/interactive_music/doc_classes/AudioStreamPlaybackSynchronized.xml b/modules/interactive_music/doc_classes/AudioStreamPlaybackSynchronized.xml
new file mode 100644
index 000000000000..adaae0fef99f
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamPlaybackSynchronized.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/modules/interactive_music/doc_classes/AudioStreamPlaylist.xml b/modules/interactive_music/doc_classes/AudioStreamPlaylist.xml
new file mode 100644
index 000000000000..1d429a932fd3
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamPlaylist.xml
@@ -0,0 +1,52 @@
+
+
+
+ [AudioStream] that includes sub-streams and plays them back like a playslit.
+
+
+
+
+
+
+
+
+
+ Return the bpm of the playlist, which can vary depending on the clip being played.
+
+
+
+
+
+
+ Get the stream at playback position index.
+
+
+
+
+
+
+
+ Set the stream at playback position index.
+
+
+
+
+
+ Fade time used when a stream ends, when going to the next one. Streams are expected to have an extra bit of audio after the end to help with fading.
+
+
+ If true, the playlist will loop, otherwise the playlist when end when the last stream is played.
+
+
+ Shuffle the playlist. Streams are played in random order.
+
+
+ Amount of streams in the playlist.
+
+
+
+
+ Maximum amount of streams supported in the playlist.
+
+
+
diff --git a/modules/interactive_music/doc_classes/AudioStreamSynchronized.xml b/modules/interactive_music/doc_classes/AudioStreamSynchronized.xml
new file mode 100644
index 000000000000..ea914715a3ec
--- /dev/null
+++ b/modules/interactive_music/doc_classes/AudioStreamSynchronized.xml
@@ -0,0 +1,53 @@
+
+
+
+ Stream that can be fitted with sub-streams, which will be played in-sync.
+
+
+ This is a stream that can be fitted with sub-streams, which will be played in-sync. The streams being at exactly the same time when play is pressed, and will end when the last of them ends. If one of the sub-streams loops, then playback will continue.
+
+
+
+
+
+
+
+
+ Get one of the synchronized streams, by index.
+
+
+
+
+
+
+ Get the volume of one of the synchronized streams, by index.
+
+
+
+
+
+
+
+ Set one of the synchronized streams, by index.
+
+
+
+
+
+
+
+ Set the volume of one of the synchronized streams, by index.
+
+
+
+
+
+ Set the total amount of streams that will be played back synchronized.
+
+
+
+
+ Maximum amount of streams that can be synchrohized.
+
+
+
diff --git a/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.cpp b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.cpp
new file mode 100644
index 000000000000..9960c4e07c8d
--- /dev/null
+++ b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.cpp
@@ -0,0 +1,416 @@
+/**************************************************************************/
+/* audio_stream_interactive_editor_plugin.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 "audio_stream_interactive_editor_plugin.h"
+
+#include "../audio_stream_interactive.h"
+#include "core/input/input.h"
+#include "core/os/keyboard.h"
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/check_box.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/spin_box.h"
+#include "scene/gui/split_container.h"
+#include "scene/gui/tree.h"
+
+void AudioStreamInteractiveTransitionEditor::_notification(int p_what) {
+ if (p_what == NOTIFICATION_READY || p_what == NOTIFICATION_THEME_CHANGED) {
+ fade_mode->clear();
+ fade_mode->add_icon_item(get_editor_theme_icon(SNAME("FadeDisabled")), TTR("Disabled"), AudioStreamInteractive::FADE_DISABLED);
+ fade_mode->add_icon_item(get_editor_theme_icon(SNAME("FadeIn")), TTR("Fade-In"), AudioStreamInteractive::FADE_IN);
+ fade_mode->add_icon_item(get_editor_theme_icon(SNAME("FadeOut")), TTR("Fade-Out"), AudioStreamInteractive::FADE_OUT);
+ fade_mode->add_icon_item(get_editor_theme_icon(SNAME("FadeCross")), TTR("Cross-Fade"), AudioStreamInteractive::FADE_CROSS);
+ fade_mode->add_icon_item(get_editor_theme_icon(SNAME("AutoPlay")), TTR("Automatic"), AudioStreamInteractive::FADE_AUTOMATIC);
+ }
+}
+
+void AudioStreamInteractiveTransitionEditor::_bind_methods() {
+ ClassDB::bind_method("_update_transitions", &AudioStreamInteractiveTransitionEditor::_update_transitions);
+}
+
+void AudioStreamInteractiveTransitionEditor::_edited() {
+ if (updating) {
+ return;
+ }
+
+ bool enabled = transition_enabled->is_pressed();
+ AudioStreamInteractive::TransitionFromTime from = AudioStreamInteractive::TransitionFromTime(transition_from->get_selected());
+ AudioStreamInteractive::TransitionToTime to = AudioStreamInteractive::TransitionToTime(transition_to->get_selected());
+ AudioStreamInteractive::FadeMode fade = AudioStreamInteractive::FadeMode(fade_mode->get_selected());
+ float beats = fade_beats->get_value();
+ bool use_filler = filler_clip->get_selected() > 0;
+ int filler = use_filler ? filler_clip->get_selected() - 1 : 0;
+ bool hold = hold_previous->is_pressed();
+
+ EditorUndoRedoManager::get_singleton()->create_action("Edit Transitions");
+ for (int i = 0; i < selected.size(); i++) {
+ if (!enabled) {
+ if (audio_stream_interactive->has_transition(selected[i].x, selected[i].y)) {
+ EditorUndoRedoManager::get_singleton()->add_do_method(audio_stream_interactive, "erase_transition", selected[i].x, selected[i].y);
+ }
+ } else {
+ EditorUndoRedoManager::get_singleton()->add_do_method(audio_stream_interactive, "add_transition", selected[i].x, selected[i].y, from, to, fade, beats, use_filler, filler, hold);
+ }
+ }
+ EditorUndoRedoManager::get_singleton()->add_undo_property(audio_stream_interactive, "_transitions", audio_stream_interactive->get("_transitions"));
+ EditorUndoRedoManager::get_singleton()->add_do_method(this, "_update_transitions");
+ EditorUndoRedoManager::get_singleton()->add_undo_method(this, "_update_transitions");
+ EditorUndoRedoManager::get_singleton()->commit_action();
+}
+
+void AudioStreamInteractiveTransitionEditor::_update_selection() {
+ updating_selection = false;
+ int clip_count = audio_stream_interactive->get_clip_count();
+ selected.clear();
+ Vector2i editing;
+ int editing_order = -1;
+ for (int i = 0; i <= clip_count; i++) {
+ for (int j = 0; j <= clip_count; j++) {
+ if (rows[i]->is_selected(j)) {
+ Vector2i meta = rows[i]->get_metadata(j);
+ if (selection_order.has(meta)) {
+ int order = selection_order[meta];
+ if (order > editing_order) {
+ editing = meta;
+ }
+ }
+ selected.push_back(meta);
+ }
+ }
+ }
+
+ transition_enabled->set_disabled(selected.is_empty());
+ transition_from->set_disabled(selected.is_empty());
+ transition_to->set_disabled(selected.is_empty());
+ fade_mode->set_disabled(selected.is_empty());
+ fade_beats->set_editable(!selected.is_empty());
+ filler_clip->set_disabled(selected.is_empty());
+ hold_previous->set_disabled(selected.is_empty());
+
+ if (selected.size() == 0) {
+ return;
+ }
+
+ updating = true;
+ if (!audio_stream_interactive->has_transition(editing.x, editing.y)) {
+ transition_enabled->set_pressed(false);
+ transition_from->select(0);
+ transition_to->select(0);
+ fade_mode->select(AudioStreamInteractive::FADE_AUTOMATIC);
+ fade_beats->set_value(1.0);
+ filler_clip->select(0);
+ hold_previous->set_pressed(false);
+ } else {
+ transition_enabled->set_pressed(true);
+ transition_from->select(audio_stream_interactive->get_transition_from_time(editing.x, editing.y));
+ transition_to->select(audio_stream_interactive->get_transition_to_time(editing.x, editing.y));
+ fade_mode->select(audio_stream_interactive->get_transition_fade_mode(editing.x, editing.y));
+ fade_beats->set_value(audio_stream_interactive->get_transition_fade_beats(editing.x, editing.y));
+ if (audio_stream_interactive->is_transition_using_filler_clip(editing.x, editing.y)) {
+ filler_clip->select(audio_stream_interactive->get_transition_filler_clip(editing.x, editing.y) + 1);
+ } else {
+ filler_clip->select(0);
+ }
+ hold_previous->set_pressed(audio_stream_interactive->is_transition_holding_previous(editing.x, editing.y));
+ }
+ updating = false;
+}
+
+void AudioStreamInteractiveTransitionEditor::_cell_selected(TreeItem *p_item, int p_column, bool p_selected) {
+ int to = p_item->get_meta("to");
+ int from = p_column == audio_stream_interactive->get_clip_count() ? AudioStreamInteractive::CLIP_ANY : p_column;
+ if (p_selected) {
+ selection_order[Vector2i(from, to)] = order_counter++;
+ }
+
+ if (!updating_selection) {
+ MessageQueue::get_singleton()->push_callable(callable_mp(this, &AudioStreamInteractiveTransitionEditor::_update_selection));
+ updating_selection = true;
+ }
+}
+
+void AudioStreamInteractiveTransitionEditor::_update_transitions() {
+ if (!is_visible()) {
+ return;
+ }
+ int clip_count = audio_stream_interactive->get_clip_count();
+ Color font_color = tree->get_theme_color("font_color", "Tree");
+ Color font_color_default = font_color;
+ font_color_default.a *= 0.5;
+ Ref fade_icons[5] = {
+ get_editor_theme_icon(SNAME("FadeDisabled")),
+ get_editor_theme_icon(SNAME("FadeIn")),
+ get_editor_theme_icon(SNAME("FadeOut")),
+ get_editor_theme_icon(SNAME("FadeCross")),
+ get_editor_theme_icon(SNAME("AutoPlay"))
+ };
+ for (int i = 0; i <= clip_count; i++) {
+ for (int j = 0; j <= clip_count; j++) {
+ String txt;
+ int from = i == clip_count ? AudioStreamInteractive::CLIP_ANY : i;
+ int to = j == clip_count ? AudioStreamInteractive::CLIP_ANY : j;
+
+ bool exists = audio_stream_interactive->has_transition(from, to);
+ String tooltip;
+ Ref icon;
+ if (!exists) {
+ if (audio_stream_interactive->has_transition(AudioStreamInteractive::CLIP_ANY, to)) {
+ from = AudioStreamInteractive::CLIP_ANY;
+ tooltip = "Using Any Clip -> " + audio_stream_interactive->get_clip_name(to) + ".";
+ } else if (audio_stream_interactive->has_transition(from, AudioStreamInteractive::CLIP_ANY)) {
+ to = AudioStreamInteractive::CLIP_ANY;
+ tooltip = "Using " + audio_stream_interactive->get_clip_name(from) + " -> Any Clip.";
+ } else if (audio_stream_interactive->has_transition(AudioStreamInteractive::CLIP_ANY, AudioStreamInteractive::CLIP_ANY)) {
+ from = to = AudioStreamInteractive::CLIP_ANY;
+ tooltip = "Using All CLips -> Any Clip.";
+ } else {
+ tooltip = "No transition available.";
+ }
+ }
+
+ if (audio_stream_interactive->has_transition(from, to)) {
+ icon = fade_icons[audio_stream_interactive->get_transition_fade_mode(from, to)];
+ switch (audio_stream_interactive->get_transition_from_time(from, to)) {
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_IMMEDIATE: {
+ txt += TTR("Immediate");
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BEAT: {
+ txt += TTR("Next Beat");
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BAR: {
+ txt += TTR("Next Bar");
+ } break;
+ case AudioStreamInteractive::TRANSITION_FROM_TIME_END: {
+ txt += TTR("Clip End");
+ } break;
+ default: {
+ }
+ }
+
+ switch (audio_stream_interactive->get_transition_to_time(from, to)) {
+ case AudioStreamInteractive::TRANSITION_TO_TIME_SAME_POSITION: {
+ txt += TTR(L"⮕ Same");
+ } break;
+ case AudioStreamInteractive::TRANSITION_TO_TIME_START: {
+ txt += TTR(L"⮕ Start");
+ } break;
+ case AudioStreamInteractive::TRANSITION_TO_TIME_PREVIOUS_POSITION: {
+ txt += TTR(L"⮕ Prev");
+ } break;
+ default: {
+ }
+ }
+ }
+
+ rows[j]->set_icon(i, icon);
+ rows[j]->set_text(i, txt);
+ rows[j]->set_tooltip_text(i, tooltip);
+ if (exists) {
+ rows[j]->set_custom_color(i, font_color);
+ rows[j]->set_icon_modulate(i, Color(1, 1, 1, 1));
+ } else {
+ rows[j]->set_custom_color(i, font_color_default);
+ rows[j]->set_icon_modulate(i, Color(1, 1, 1, 0.5));
+ }
+ }
+ }
+}
+
+void AudioStreamInteractiveTransitionEditor::edit(Object *p_obj) {
+ audio_stream_interactive = Object::cast_to(p_obj);
+ if (!audio_stream_interactive) {
+ return;
+ }
+
+ Ref header_font = get_theme_font("bold", "EditorFonts");
+ int header_font_size = get_theme_font_size("bold_size", "EditorFonts");
+
+ tree->clear();
+ rows.clear();
+ selection_order.clear();
+ selected.clear();
+
+ int clip_count = audio_stream_interactive->get_clip_count();
+ tree->set_columns(clip_count + 2);
+ TreeItem *root = tree->create_item();
+ TreeItem *header = tree->create_item(root); // Header
+ int header_index = clip_count + 1;
+ header->set_text(header_index, TTR("From / To"));
+ header->set_editable(0, false);
+
+ filler_clip->clear();
+ filler_clip->add_item("Disabled", -1);
+
+ Color header_color = get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor));
+
+ int max_w = 0;
+
+ updating = true;
+ for (int i = 0; i <= clip_count; i++) {
+ int cell_index = i;
+ int clip_i = i == clip_count ? AudioStreamInteractive::CLIP_ANY : i;
+ header->set_editable(cell_index, false);
+ header->set_selectable(cell_index, false);
+ header->set_custom_font(cell_index, header_font);
+ header->set_custom_font_size(cell_index, header_font_size);
+ header->set_custom_bg_color(cell_index, header_color);
+
+ String name;
+ if (i == clip_count) {
+ name = TTR("Any Clip");
+ } else {
+ name = audio_stream_interactive->get_clip_name(i);
+ }
+
+ int min_w = header_font->get_string_size(name + "XX").width;
+ tree->set_column_expand(cell_index, false);
+ tree->set_column_custom_minimum_width(cell_index, min_w);
+ max_w = MAX(max_w, min_w);
+
+ header->set_text(cell_index, name);
+
+ TreeItem *row = tree->create_item(root);
+ row->set_text(header_index, name);
+ row->set_selectable(header_index, false);
+ row->set_custom_font(header_index, header_font);
+ row->set_custom_font_size(header_index, header_font_size);
+ row->set_custom_bg_color(header_index, header_color);
+ row->set_meta("to", clip_i);
+ for (int j = 0; j <= clip_count; j++) {
+ int clip_j = j == clip_count ? AudioStreamInteractive::CLIP_ANY : j;
+ row->set_metadata(j, Vector2i(clip_j, clip_i));
+ }
+ rows.push_back(row);
+
+ if (i < clip_count) {
+ filler_clip->add_item(name, i);
+ }
+ }
+
+ tree->set_column_expand(header_index, false);
+ tree->set_column_custom_minimum_width(header_index, max_w);
+ selection_order.clear();
+ _update_selection();
+ popup_centered_ratio(0.6);
+ updating = false;
+ _update_transitions();
+}
+
+AudioStreamInteractiveTransitionEditor::AudioStreamInteractiveTransitionEditor() {
+ set_title(TTR("AudioStreamInteractive Transition Editor"));
+ split = memnew(HSplitContainer);
+ add_child(split);
+ tree = memnew(Tree);
+ tree->set_hide_root(true);
+ tree->add_theme_constant_override("draw_guides", 1);
+ tree->set_select_mode(Tree::SELECT_MULTI);
+ split->add_child(tree);
+
+ tree->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ tree->connect("multi_selected", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_cell_selected));
+ VBoxContainer *edit_vb = memnew(VBoxContainer);
+ split->add_child(edit_vb);
+
+ transition_enabled = memnew(CheckBox);
+ transition_enabled->set_text(TTR("Use Transition"));
+ edit_vb->add_margin_child(TTR("Transition Enabled:"), transition_enabled);
+ transition_enabled->connect("pressed", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited));
+
+ transition_from = memnew(OptionButton);
+ edit_vb->add_margin_child(TTR("Transition From:"), transition_from);
+ transition_from->add_item(TTR("Immediate"), AudioStreamInteractive::TRANSITION_FROM_TIME_IMMEDIATE);
+ transition_from->add_item(TTR("Next Beat"), AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BEAT);
+ transition_from->add_item(TTR("Next Bar"), AudioStreamInteractive::TRANSITION_FROM_TIME_NEXT_BAR);
+ transition_from->add_item(TTR("Clip End"), AudioStreamInteractive::TRANSITION_FROM_TIME_END);
+
+ transition_from->connect("item_selected", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited).unbind(1));
+
+ transition_to = memnew(OptionButton);
+ edit_vb->add_margin_child(TTR("Transition To:"), transition_to);
+ transition_to->add_item(TTR("Same Position"), AudioStreamInteractive::TRANSITION_TO_TIME_SAME_POSITION);
+ transition_to->add_item(TTR("Clip Start"), AudioStreamInteractive::TRANSITION_TO_TIME_START);
+ transition_to->add_item(TTR("Prev Position"), AudioStreamInteractive::TRANSITION_TO_TIME_PREVIOUS_POSITION);
+ transition_to->connect("item_selected", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited).unbind(1));
+
+ fade_mode = memnew(OptionButton);
+ edit_vb->add_margin_child(TTR("Fade Mode:"), fade_mode);
+ fade_mode->connect("item_selected", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited).unbind(1));
+
+ fade_beats = memnew(SpinBox);
+ edit_vb->add_margin_child(TTR("Fade Beats:"), fade_beats);
+ fade_beats->set_max(16);
+ fade_beats->set_step(0.1);
+ fade_beats->connect("value_changed", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited).unbind(1));
+
+ filler_clip = memnew(OptionButton);
+ edit_vb->add_margin_child(TTR("Filler Clip:"), filler_clip);
+ filler_clip->connect("item_selected", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited).unbind(1));
+
+ hold_previous = memnew(CheckBox);
+ hold_previous->set_text(TTR("Enabled"));
+ hold_previous->connect("pressed", callable_mp(this, &AudioStreamInteractiveTransitionEditor::_edited));
+ edit_vb->add_margin_child(TTR("Hold Previous:"), hold_previous);
+
+ set_exclusive(true);
+}
+
+////////////////////////
+
+bool EditorInspectorPluginAudioStreamInteractive::can_handle(Object *p_object) {
+ return Object::cast_to(p_object);
+}
+
+void EditorInspectorPluginAudioStreamInteractive::_edit(Object *p_object) {
+ audio_stream_interactive_transition_editor->edit(p_object);
+}
+
+void EditorInspectorPluginAudioStreamInteractive::parse_end(Object *p_object) {
+ if (Object::cast_to(p_object)) {
+ Button *button = EditorInspector::create_inspector_action_button(TTR("Edit Transitions"));
+ button->set_icon(audio_stream_interactive_transition_editor->get_editor_theme_icon(SNAME("Blend")));
+ button->connect("pressed", callable_mp(this, &EditorInspectorPluginAudioStreamInteractive::_edit).bind(p_object));
+ add_custom_control(button);
+ }
+}
+
+EditorInspectorPluginAudioStreamInteractive::EditorInspectorPluginAudioStreamInteractive() {
+ audio_stream_interactive_transition_editor = memnew(AudioStreamInteractiveTransitionEditor);
+ EditorNode::get_singleton()->get_gui_base()->add_child(audio_stream_interactive_transition_editor);
+}
+
+AudioStreamInteractiveEditorPlugin::AudioStreamInteractiveEditorPlugin() {
+ Ref inspector_plugin;
+ inspector_plugin.instantiate();
+ add_inspector_plugin(inspector_plugin);
+}
diff --git a/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h
new file mode 100644
index 000000000000..730d1ca83bc4
--- /dev/null
+++ b/modules/interactive_music/editor/audio_stream_interactive_editor_plugin.h
@@ -0,0 +1,110 @@
+/**************************************************************************/
+/* audio_stream_interactive_editor_plugin.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. */
+/**************************************************************************/
+
+#ifndef AUDIO_STREAM_INTERACTIVE_EDITOR_PLUGIN_H
+#define AUDIO_STREAM_INTERACTIVE_EDITOR_PLUGIN_H
+
+#include "editor/editor_inspector.h"
+#include "editor/editor_plugin.h"
+#include "scene/gui/dialogs.h"
+
+class CheckBox;
+class HSplitContainer;
+class VSplitContainer;
+class Tree;
+class TreeItem;
+class AudioStreamInteractive;
+
+class AudioStreamInteractiveTransitionEditor : public AcceptDialog {
+ GDCLASS(AudioStreamInteractiveTransitionEditor, AcceptDialog);
+
+ AudioStreamInteractive *audio_stream_interactive = nullptr;
+
+ HSplitContainer *split = nullptr;
+ Tree *tree = nullptr;
+
+ Vector rows;
+
+ CheckBox *transition_enabled = nullptr;
+ OptionButton *transition_from = nullptr;
+ OptionButton *transition_to = nullptr;
+ OptionButton *fade_mode = nullptr;
+ SpinBox *fade_beats = nullptr;
+ OptionButton *filler_clip = nullptr;
+ CheckBox *hold_previous = nullptr;
+
+ bool updating_selection = false;
+ int order_counter = 0;
+ HashMap selection_order;
+
+ Vector selected;
+ bool updating = false;
+ void _cell_selected(TreeItem *p_item, int p_column, bool p_selected);
+ void _update_transitions();
+
+ void _update_selection();
+ void _edited();
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void edit(Object *p_obj);
+
+ AudioStreamInteractiveTransitionEditor();
+};
+
+//
+
+class EditorInspectorPluginAudioStreamInteractive : public EditorInspectorPlugin {
+ GDCLASS(EditorInspectorPluginAudioStreamInteractive, EditorInspectorPlugin);
+
+ AudioStreamInteractiveTransitionEditor *audio_stream_interactive_transition_editor = nullptr;
+
+ void _edit(Object *p_object);
+
+public:
+ virtual bool can_handle(Object *p_object) override;
+ virtual void parse_end(Object *p_object) override;
+
+ EditorInspectorPluginAudioStreamInteractive();
+};
+
+class AudioStreamInteractiveEditorPlugin : public EditorPlugin {
+ GDCLASS(AudioStreamInteractiveEditorPlugin, EditorPlugin);
+
+public:
+ virtual String get_name() const override { return "AudioStreamInteractive"; }
+
+ AudioStreamInteractiveEditorPlugin();
+};
+
+#endif // AUDIO_STREAM_INTERACTIVE_EDITOR_PLUGIN_H
diff --git a/modules/interactive_music/register_types.cpp b/modules/interactive_music/register_types.cpp
new file mode 100644
index 000000000000..5baea13f8173
--- /dev/null
+++ b/modules/interactive_music/register_types.cpp
@@ -0,0 +1,60 @@
+/**************************************************************************/
+/* register_types.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 "register_types.h"
+
+#include "audio_stream_interactive.h"
+#include "audio_stream_playlist.h"
+#include "audio_stream_synchronized.h"
+#include "core/object/class_db.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/audio_stream_interactive_editor_plugin.h"
+#endif
+
+void initialize_interactive_music_module(ModuleInitializationLevel p_level) {
+ if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
+ GDREGISTER_CLASS(AudioStreamPlaylist);
+ GDREGISTER_VIRTUAL_CLASS(AudioStreamPlaybackPlaylist);
+ GDREGISTER_CLASS(AudioStreamInteractive);
+ GDREGISTER_VIRTUAL_CLASS(AudioStreamPlaybackInteractive);
+ GDREGISTER_CLASS(AudioStreamSynchronized);
+ GDREGISTER_VIRTUAL_CLASS(AudioStreamPlaybackSynchronized);
+ }
+#ifdef TOOLS_ENABLED
+ if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
+ EditorPlugins::add_by_type();
+ }
+#endif
+}
+
+void uninitialize_interactive_music_module(ModuleInitializationLevel p_level) {
+ // Nothing to do here.
+}
diff --git a/modules/interactive_music/register_types.h b/modules/interactive_music/register_types.h
new file mode 100644
index 000000000000..5625e28b6441
--- /dev/null
+++ b/modules/interactive_music/register_types.h
@@ -0,0 +1,39 @@
+/**************************************************************************/
+/* register_types.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. */
+/**************************************************************************/
+
+#ifndef INTERACTIVE_MUSIC_REGISTER_TYPES_H
+#define INTERACTIVE_MUSIC_REGISTER_TYPES_H
+
+#include "modules/register_module_types.h"
+
+void initialize_interactive_music_module(ModuleInitializationLevel p_level);
+void uninitialize_interactive_music_module(ModuleInitializationLevel p_level);
+
+#endif // INTERACTIVE_MUSIC_REGISTER_TYPES_H