Add interactive music support

This PR adds 3 types of audio streams used for interactive music support.

* AudioStreamInteractive: Allows setting several sub-streams and transition between them with many options.
* AudioStreamPlaylist: Allows sequential or shuffled playback of a list of streams.
* AudioStreamSynchronized: Allows synchronous playback of several streams, the volume of each can be controlled.

Theese three stream types can be combined to create complex, layered interactive music and transitions between them, similar to software such as WWise.
This commit is contained in:
Juan Linietsky 2022-08-16 11:27:58 +02:00
parent 810f127022
commit 43b78cd2ad
22 changed files with 3303 additions and 0 deletions

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m9 1v2h2.586l-3.586 3.586-4.293-4.293-1.414 1.414 4.293 4.293-4.293 4.293 1.414 1.414 4.293-4.293 3.586 3.586h-2.586v2h5a1 1 0 0 0 1-1v-5h-2v2.586l-3.586-3.586 3.586-3.586v2.586h2v-5a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 314 B

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m11.032091 3.1108593-1.4142134 1.4142136 1.8285784 1.8285781h-9.8600786v1.999698h9.8600786l-1.8285784 1.828578 1.4142134 1.414214 3.535534-3.5355342c.390524-.3905243.390525-1.0236891 0-1.4142136z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 311 B

1
editor/icons/FadeIn.svg Normal file
View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m9 1v2h2.586c-9.293 9.293-.896067.8960673-9.293 9.293l1.414 1.414c9.293-9.293 3.7072139-3.7072139 9.293-9.293v2.586h2v-5c0-.5522847-.447715-1-1-1z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 262 B

1
editor/icons/FadeOut.svg Normal file
View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m3.707 2.293-1.414 1.414c9.293 9.293 1.4503342 1.4503342 9.293 9.293h-2.586v2h5c.552285 0 1-.447715 1-1v-5h-2v2.586c-9.293-9.293-2.832166-2.8321664-9.293-9.293z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View file

@ -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")

File diff suppressed because it is too large Load diff

View file

@ -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<AudioStream> 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<TransitionKey, Transition, TransitionKeyHasher> transition_map;
uint64_t version = 1; // Used to stop playback instances for incompatibility.
int clip_count = 0;
HashSet<AudioStreamPlaybackInteractive *> 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<AudioStream> &p_stream);
Ref<AudioStream> 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<AudioStreamPlayback> 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<Parameter> *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<AudioStreamInteractive> stream;
uint64_t version = 0;
enum {
BUFFER_SIZE = 1024
};
AudioFrame mix_buffer[BUFFER_SIZE];
AudioFrame temp_buffer[BUFFER_SIZE];
struct State {
Ref<AudioStream> stream;
Ref<AudioStreamPlayback> 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

View file

@ -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<AudioStreamPlayback> AudioStreamPlaylist::instantiate_playback() {
Ref<AudioStreamPlaybackPlaylist> playback_playlist;
playback_playlist.instantiate();
playback_playlist->playlist = Ref<AudioStreamPlaylist>(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<AudioStream> 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<AudioStream> AudioStreamPlaylist::get_list_stream(int p_stream_index) const {
ERR_FAIL_INDEX_V(p_stream_index, MAX_STREAMS, Ref<AudioStream>());
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();
}
}
}

View file

@ -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<AudioStream> audio_streams[MAX_STREAMS];
HashSet<AudioStreamPlaybackPlaylist *> 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<AudioStream> p_stream);
Ref<AudioStream> get_list_stream(int p_stream_index) const;
virtual Ref<AudioStreamPlayback> 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<AudioStreamPlaylist> playlist;
Ref<AudioStreamPlayback> 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

View file

@ -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<AudioStreamPlayback> AudioStreamSynchronized::instantiate_playback() {
Ref<AudioStreamPlaybackSynchronized> playback_playlist;
playback_playlist.instantiate();
playback_playlist->stream = Ref<AudioStreamSynchronized>(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<AudioStream> 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<AudioStream> AudioStreamSynchronized::get_sync_stream(int p_stream_index) const {
ERR_FAIL_INDEX_V(p_stream_index, MAX_STREAMS, Ref<AudioStream>());
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();
}
}
}

View file

@ -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<AudioStream> audio_streams[MAX_STREAMS];
float audio_stream_volume_db[MAX_STREAMS] = {};
HashSet<AudioStreamPlaybackSynchronized *> 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<AudioStream> p_stream);
Ref<AudioStream> 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<AudioStreamPlayback> 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<AudioStreamSynchronized> stream;
Ref<AudioStreamPlayback> 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

View file

@ -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"

View file

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamInteractive" inherits="AudioStream" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
Audio stream that can playback music interactively, combining clips and a transition table.
</brief_description>
<description>
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.
</description>
<tutorials>
</tutorials>
<methods>
<method name="add_transition">
<return type="void" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<param index="2" name="from_time" type="int" enum="AudioStreamInteractive.TransitionFromTime" />
<param index="3" name="to_time" type="int" enum="AudioStreamInteractive.TransitionToTime" />
<param index="4" name="fade_mode" type="int" enum="AudioStreamInteractive.FadeMode" />
<param index="5" name="fade_beats" type="float" />
<param index="6" name="use_filler_clip" type="bool" default="false" />
<param index="7" name="filler_clip" type="int" default="-1" />
<param index="8" name="hold_previous" type="bool" default="false" />
<description>
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.
</description>
</method>
<method name="erase_transition">
<return type="void" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
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.
</description>
</method>
<method name="get_clip_auto_advance" qualifiers="const">
<return type="int" enum="AudioStreamInteractive.AutoAdvanceMode" />
<param index="0" name="clip_index" type="int" />
<description>
Return whether a clip has auto-advance enabled. See [method set_clip_auto_advance].
</description>
</method>
<method name="get_clip_auto_advance_next_clip" qualifiers="const">
<return type="int" />
<param index="0" name="clip_index" type="int" />
<description>
Return the clip towards which the clip referenced by [param clip_index] will auto-advance to.
</description>
</method>
<method name="get_clip_name" qualifiers="const">
<return type="StringName" />
<param index="0" name="clip_index" type="int" />
<description>
Return the name of a clip.
</description>
</method>
<method name="get_clip_stream" qualifiers="const">
<return type="AudioStream" />
<param index="0" name="clip_index" type="int" />
<description>
Return the [AudioStream] associated with a clip.
</description>
</method>
<method name="get_transition_fade_beats" qualifiers="const">
<return type="float" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return the time (in beats) for a transition (see [method add_transition]).
</description>
</method>
<method name="get_transition_fade_mode" qualifiers="const">
<return type="int" enum="AudioStreamInteractive.FadeMode" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return the mode for a transition (see [method add_transition]).
</description>
</method>
<method name="get_transition_filler_clip" qualifiers="const">
<return type="int" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return the filler clip for a transition (see [method add_transition]).
</description>
</method>
<method name="get_transition_from_time" qualifiers="const">
<return type="int" enum="AudioStreamInteractive.TransitionFromTime" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return the source time position for a transition (see [method add_transition]).
</description>
</method>
<method name="get_transition_list" qualifiers="const">
<return type="PackedInt32Array" />
<description>
Return the list of transitions (from, to interleaved).
</description>
</method>
<method name="get_transition_to_time" qualifiers="const">
<return type="int" enum="AudioStreamInteractive.TransitionToTime" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return the destination time position for a transition (see [method add_transition]).
</description>
</method>
<method name="has_transition" qualifiers="const">
<return type="bool" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return true if a given transition exists (was added via [method add_transition]).
</description>
</method>
<method name="is_transition_holding_previous" qualifiers="const">
<return type="bool" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return whether a transition uses the [i]hold previous[/i] functionality (see [method add_transition]).
</description>
</method>
<method name="is_transition_using_filler_clip" qualifiers="const">
<return type="bool" />
<param index="0" name="from_clip" type="int" />
<param index="1" name="to_clip" type="int" />
<description>
Return whether a transition uses the [i]filler clip[/i] functionality (see [method add_transition]).
</description>
</method>
<method name="set_clip_auto_advance">
<return type="void" />
<param index="0" name="clip_index" type="int" />
<param index="1" name="mode" type="int" enum="AudioStreamInteractive.AutoAdvanceMode" />
<description>
Set whether a clip will auto-advance by changing the auto-advance mode.
</description>
</method>
<method name="set_clip_auto_advance_next_clip">
<return type="void" />
<param index="0" name="clip_index" type="int" />
<param index="1" name="auto_advance_next_clip" type="int" />
<description>
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.
</description>
</method>
<method name="set_clip_name">
<return type="void" />
<param index="0" name="clip_index" type="int" />
<param index="1" name="name" type="StringName" />
<description>
Set the name of the current clip (for easier identification).
</description>
</method>
<method name="set_clip_stream">
<return type="void" />
<param index="0" name="clip_index" type="int" />
<param index="1" name="stream" type="AudioStream" />
<description>
Set the [AudioStream] associated with the current clip.
</description>
</method>
</methods>
<members>
<member name="clip_count" type="int" setter="set_clip_count" getter="get_clip_count" default="0">
Amount of clips contained in this interactive player.
</member>
<member name="initial_clip" type="int" setter="set_initial_clip" getter="get_initial_clip" default="0">
Index of the initial clip, which will be played first when this stream is played.
</member>
</members>
<constants>
<constant name="TRANSITION_FROM_TIME_IMMEDIATE" value="0" enum="TransitionFromTime">
Start transition as soon as possible, don't wait for any specific time position.
</constant>
<constant name="TRANSITION_FROM_TIME_NEXT_BEAT" value="1" enum="TransitionFromTime">
Transition when the clip playback position reaches the next beat.
</constant>
<constant name="TRANSITION_FROM_TIME_NEXT_BAR" value="2" enum="TransitionFromTime">
Transition when the clip playback position reaches the next bar.
</constant>
<constant name="TRANSITION_FROM_TIME_END" value="3" enum="TransitionFromTime">
Transition when the current clip finished playing.
</constant>
<constant name="TRANSITION_TO_TIME_SAME_POSITION" value="0" enum="TransitionToTime">
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.
</constant>
<constant name="TRANSITION_TO_TIME_START" value="1" enum="TransitionToTime">
Transition to the start of the destination clip.
</constant>
<constant name="FADE_DISABLED" value="0" enum="FadeMode">
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.
</constant>
<constant name="FADE_IN" value="1" enum="FadeMode">
Use a fade-in in the next clip, let the current clip finish.
</constant>
<constant name="FADE_OUT" value="2" enum="FadeMode">
Use a fade-out in the current clip, the next clip will start by itself.
</constant>
<constant name="FADE_CROSS" value="3" enum="FadeMode">
Use a cross-fade between clips.
</constant>
<constant name="FADE_AUTOMATIC" value="4" enum="FadeMode">
Use automatic fade logic depending on the transition from/to. It is recommended to use this by default.
</constant>
<constant name="AUTO_ADVANCE_DISABLED" value="0" enum="AutoAdvanceMode">
Disable auto-advance (default).
</constant>
<constant name="AUTO_ADVANCE_ENABLED" value="1" enum="AutoAdvanceMode">
Enable auto-advance, a clip must be specified.
</constant>
<constant name="AUTO_ADVANCE_RETURN_TO_HOLD" value="2" enum="AutoAdvanceMode">
Enable auto-advance, but instead of specifying a clip, the playback will return to hold (see [method add_transition]).
</constant>
<constant name="CLIP_ANY" value="-1">
This constant describes that any clip is valid for a specific transition as either source or destination.
</constant>
</constants>
</class>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamPlaybackInteractive" inherits="AudioStreamPlayback" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
Playback component of [AudioStreamInteractive].
</brief_description>
<description>
Playback component of [AudioStreamInteractive]. Contains functions to change the currently played clip.
</description>
<tutorials>
</tutorials>
<methods>
<method name="switch_to_clip">
<return type="void" />
<param index="0" name="clip_index" type="int" />
<description>
Switch to a clip (by index).
</description>
</method>
<method name="switch_to_clip_by_name">
<return type="void" />
<param index="0" name="clip_name" type="StringName" />
<description>
Switch to a clip (by name).
</description>
</method>
</methods>
</class>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamPlaybackPlaylist" inherits="AudioStreamPlayback" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
Playback class used for [AudioStreamPlaylist].
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
</class>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamPlaybackSynchronized" inherits="AudioStreamPlayback" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
</class>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamPlaylist" inherits="AudioStream" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
[AudioStream] that includes sub-streams and plays them back like a playslit.
</brief_description>
<description>
</description>
<tutorials>
</tutorials>
<methods>
<method name="get_bpm" qualifiers="const">
<return type="float" />
<description>
Return the bpm of the playlist, which can vary depending on the clip being played.
</description>
</method>
<method name="get_list_stream" qualifiers="const">
<return type="AudioStream" />
<param index="0" name="stream_index" type="int" />
<description>
Get the stream at playback position index.
</description>
</method>
<method name="set_list_stream">
<return type="void" />
<param index="0" name="stream_index" type="int" />
<param index="1" name="audio_stream" type="AudioStream" />
<description>
Set the stream at playback position index.
</description>
</method>
</methods>
<members>
<member name="fade_time" type="float" setter="set_fade_time" getter="get_fade_time" default="0.3">
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.
</member>
<member name="loop" type="bool" setter="set_loop" getter="has_loop" default="true">
If true, the playlist will loop, otherwise the playlist when end when the last stream is played.
</member>
<member name="shuffle" type="bool" setter="set_shuffle" getter="get_shuffle" default="false">
Shuffle the playlist. Streams are played in random order.
</member>
<member name="stream_count" type="int" setter="set_stream_count" getter="get_stream_count" default="0">
Amount of streams in the playlist.
</member>
</members>
<constants>
<constant name="MAX_STREAMS" value="64">
Maximum amount of streams supported in the playlist.
</constant>
</constants>
</class>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="AudioStreamSynchronized" inherits="AudioStream" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
Stream that can be fitted with sub-streams, which will be played in-sync.
</brief_description>
<description>
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.
</description>
<tutorials>
</tutorials>
<methods>
<method name="get_sync_stream" qualifiers="const">
<return type="AudioStream" />
<param index="0" name="stream_index" type="int" />
<description>
Get one of the synchronized streams, by index.
</description>
</method>
<method name="get_sync_stream_volume" qualifiers="const">
<return type="float" />
<param index="0" name="stream_index" type="int" />
<description>
Get the volume of one of the synchronized streams, by index.
</description>
</method>
<method name="set_sync_stream">
<return type="void" />
<param index="0" name="stream_index" type="int" />
<param index="1" name="audio_stream" type="AudioStream" />
<description>
Set one of the synchronized streams, by index.
</description>
</method>
<method name="set_sync_stream_volume">
<return type="void" />
<param index="0" name="stream_index" type="int" />
<param index="1" name="volume_db" type="float" />
<description>
Set the volume of one of the synchronized streams, by index.
</description>
</method>
</methods>
<members>
<member name="stream_count" type="int" setter="set_stream_count" getter="get_stream_count" default="0">
Set the total amount of streams that will be played back synchronized.
</member>
</members>
<constants>
<constant name="MAX_STREAMS" value="32">
Maximum amount of streams that can be synchrohized.
</constant>
</constants>
</class>

View file

@ -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<Texture> 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<Texture> 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<AudioStreamInteractive>(p_obj);
if (!audio_stream_interactive) {
return;
}
Ref<Font> 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<AudioStreamInteractive>(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<AudioStreamInteractive>(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<EditorInspectorPluginAudioStreamInteractive> inspector_plugin;
inspector_plugin.instantiate();
add_inspector_plugin(inspector_plugin);
}

View file

@ -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<TreeItem *> 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<Vector2i, int> selection_order;
Vector<Vector2i> 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

View file

@ -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<AudioStreamInteractiveEditorPlugin>();
}
#endif
}
void uninitialize_interactive_music_module(ModuleInitializationLevel p_level) {
// Nothing to do here.
}

View file

@ -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