diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 10cdf5b2e720..768540a0fa1a 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1480,6 +1480,8 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF("animation/warnings/check_angle_interpolation_type_conflicting", true); GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/default_bus_layout", PROPERTY_HINT_FILE, "*.tres"), "res://default_bus_layout.tres"); + GLOBAL_DEF(PropertyInfo(Variant::INT, "audio/general/default_playback_type", PROPERTY_HINT_ENUM, "Stream,Sample"), 0); + GLOBAL_DEF(PropertyInfo(Variant::INT, "audio/general/default_playback_type.web", PROPERTY_HINT_ENUM, "Stream,Sample"), 1); GLOBAL_DEF_RST("audio/general/text_to_speech", false); GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/2d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f); GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/3d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f); diff --git a/doc/classes/AudioSample.xml b/doc/classes/AudioSample.xml new file mode 100644 index 000000000000..e3820c11a033 --- /dev/null +++ b/doc/classes/AudioSample.xml @@ -0,0 +1,11 @@ + + + + Base class for audio samples. + + + Base class for audio samples. + + + + diff --git a/doc/classes/AudioSamplePlayback.xml b/doc/classes/AudioSamplePlayback.xml new file mode 100644 index 000000000000..74a2d5e65650 --- /dev/null +++ b/doc/classes/AudioSamplePlayback.xml @@ -0,0 +1,11 @@ + + + + Meta class for playing back audio samples. + + + Meta class for playing back audio samples. + + + + diff --git a/doc/classes/AudioServer.xml b/doc/classes/AudioServer.xml index b3cf53367d1c..6830c632cf93 100644 --- a/doc/classes/AudioServer.xml +++ b/doc/classes/AudioServer.xml @@ -183,6 +183,14 @@ If [code]true[/code], the bus at index [param bus_idx] is in solo mode. + + + + + If [code]true[/code], the stream is registered as a sample. The engine will not have to register it before playing the sample. + If [code]false[/code], the stream will have to be registered before playing it. To prevent lag spikes, register the stream as sample with [method register_stream_as_sample]. + + @@ -198,6 +206,14 @@ Moves the bus from index [param index] to index [param to_index]. + + + + + Forces the registration of a stream as a sample. + [b]Note:[/b] Lag spikes may occur when calling this method, especially on single-threaded builds. It is suggested to call this method while loading assets, where the lag spike could be masked, instead of registering the sample right before it needs to be played. + + @@ -344,5 +360,19 @@ A 7.1 channel surround setup was detected. + + The playback will be considered of the type declared at [member ProjectSettings.audio/general/default_playback_type]. + + + Force the playback to be considered as a stream. + + + Force the playback to be considered as a sample. This can provide lower latency and more stable playback (with less risk of audio crackling), at the cost of having less flexibility. + [b]Note:[/b] Only currently supported on the web platform. + [b]Note:[/b] [AudioEffect]s are not supported when playback is considered as a sample. + + + Represents the size of the [enum PlaybackType] enum. + diff --git a/doc/classes/AudioStream.xml b/doc/classes/AudioStream.xml index 4abce3f1da01..44edff122e77 100644 --- a/doc/classes/AudioStream.xml +++ b/doc/classes/AudioStream.xml @@ -57,6 +57,18 @@ Override this method to customize the returned value of [method is_monophonic]. Should return [code]true[/code] if this audio stream only supports one channel. + + + + Returns if the current [AudioStream] can be used as a sample. Only static streams can be sampled. + + + + + + Generates an [AudioSample] based on the current stream. + + @@ -69,6 +81,12 @@ Returns a newly created [AudioStreamPlayback] intended to play this audio stream. Useful for when you want to extend [method _instantiate_playback] but call [method instantiate_playback] from an internally held AudioStream subresource. An example of this can be found in the source code for [code]AudioStreamRandomPitch::instantiate_playback[/code]. + + + + Returns [code]true[/code] if the stream is a collection of other streams, [code]false[/code] otherwise. + + diff --git a/doc/classes/AudioStreamPlayback.xml b/doc/classes/AudioStreamPlayback.xml index 9f87b76a2bfb..02f3407f79b5 100644 --- a/doc/classes/AudioStreamPlayback.xml +++ b/doc/classes/AudioStreamPlayback.xml @@ -79,5 +79,18 @@ Overridable method. Called whenever the audio stream is mixed if the playback is active and [method AudioServer.set_enable_tagging_used_audio_streams] has been set to [code]true[/code]. Editor plugins may use this method to "tag" the current position along the audio stream and display it in a preview. + + + + Returns the [AudioSamplePlayback] associated with this [AudioStreamPlayback] for playing back the audio sample of this stream. + + + + + + + Associates [AudioSamplePlayback] to this [AudioStreamPlayback] for playing back the audio sample of this stream. + + diff --git a/doc/classes/AudioStreamPlaybackPolyphonic.xml b/doc/classes/AudioStreamPlaybackPolyphonic.xml index 106f2a3dbb34..f71762d6a516 100644 --- a/doc/classes/AudioStreamPlaybackPolyphonic.xml +++ b/doc/classes/AudioStreamPlaybackPolyphonic.xml @@ -22,8 +22,10 @@ + + - Play an [AudioStream] at a given offset, volume and pitch scale. Playback starts immediately. + Play an [AudioStream] at a given offset, volume, pitch scale, playback type, and bus. Playback starts immediately. The return value is a unique integer ID that is associated to this playback stream and which can be used to control it. This ID becomes invalid when the stream ends (if it does not loop), when the [AudioStreamPlaybackPolyphonic] is stopped, or when [method stop_stream] is called. This function returns [constant INVALID_ID] if the amount of streams currently playing equals [member AudioStreamPolyphonic.polyphony]. If you need a higher amount of maximum polyphony, raise this value. diff --git a/doc/classes/AudioStreamPlayer.xml b/doc/classes/AudioStreamPlayer.xml index a7d0a1007372..7b4b7c289b0c 100644 --- a/doc/classes/AudioStreamPlayer.xml +++ b/doc/classes/AudioStreamPlayer.xml @@ -74,6 +74,9 @@ The audio's pitch and tempo, as a multiplier of the [member stream]'s sample rate. A value of [code]2.0[/code] doubles the audio's pitch, while a value of [code]0.5[/code] halves the pitch. + + The playback type of the stream player. If set other than to the default value, it will force that playback type. + If [code]true[/code], this node is playing sounds. Setting this property has the same effect as [method play] and [method stop]. diff --git a/doc/classes/AudioStreamPlayer2D.xml b/doc/classes/AudioStreamPlayer2D.xml index 8b818879769e..a3206ba1d6b1 100644 --- a/doc/classes/AudioStreamPlayer2D.xml +++ b/doc/classes/AudioStreamPlayer2D.xml @@ -78,6 +78,9 @@ The pitch and the tempo of the audio, as a multiplier of the audio sample's sample rate. + + The playback type of the stream player. If set other than to the default value, it will force that playback type. + If [code]true[/code], audio is playing or is queued to be played (see [method play]). diff --git a/doc/classes/AudioStreamPlayer3D.xml b/doc/classes/AudioStreamPlayer3D.xml index af92fd4a44c4..bf02caffb4cf 100644 --- a/doc/classes/AudioStreamPlayer3D.xml +++ b/doc/classes/AudioStreamPlayer3D.xml @@ -99,6 +99,9 @@ The pitch and the tempo of the audio, as a multiplier of the audio sample's sample rate. + + The playback type of the stream player. If set other than to the default value, it will force that playback type. + If [code]true[/code], audio is playing or is queued to be played (see [method play]). diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 84542b755c87..2e219652ec9a 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -393,7 +393,7 @@ Safer override for [member audio/driver/mix_rate] in the Web platform. Here [code]0[/code] means "let the browser choose" (since some browsers do not like forcing the mix rate). - Specifies the preferred output latency in milliseconds for audio. Lower values will result in lower audio latency at the cost of increased CPU usage. Low values may result in audible cracking on slower hardware. + Specifies the preferred output latency in milliseconds for audio. Lower values will result in lower audio latency at the cost of increased CPU usage. Low values may result in audible crackling on slower hardware. Audio output latency may be constrained by the host operating system and audio hardware drivers. If the host can not provide the specified audio output latency then Godot will attempt to use the nearest latency allowed by the host. As such you should always use [method AudioServer.get_output_latency] to determine the actual audio output latency. Audio output latency can be overridden using the [code]--audio-output-latency <ms>[/code] command line argument. [b]Note:[/b] This setting is ignored on Android, and on all versions of Windows prior to Windows 10. @@ -409,6 +409,15 @@ The base strength of the panning effect for all [AudioStreamPlayer3D] nodes. The panning strength can be further scaled on each Node using [member AudioStreamPlayer3D.panning_strength]. A value of [code]0.0[/code] disables stereo panning entirely, leaving only volume attenuation in place. A value of [code]1.0[/code] completely mutes one of the channels if the sound is located exactly to the left (or right) of the listener. The default value of [code]0.5[/code] is tuned for headphones. When using speakers, you may find lower values to sound better as speakers have a lower stereo separation compared to headphones. + + Specifies the default playback type of the platform. + The default value is set to [b]Stream[/b], as most platforms have no issues mixing streams. + + + Specifies the default playback type of the Web platform. + The default value is set to [b]Sample[/b] as the Web platform is not suited to mix audio streams outside of the Web Audio API, especially when exporting a single-threaded game. [b]Sample[/b] allows for lower latency on the web platform at the cost of flexibility ([AudioEffect]s are not supported). + [b]Warning:[/b] Forcing [b]Stream[/b] on the Web platform may cause high audio latency and crackling, especially when exporting a multi-threaded game. + Sets the [url=https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions/1616611-mixwithothers]mixWithOthers[/url] option for the AVAudioSession on iOS. This will override the mix behavior, if the category is set to [code]Play and Record[/code], [code]Playback[/code], or [code]Multi Route[/code]. [code]Ambient[/code] always has this set per default. diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index b8f244ca0237..7b93df70fa10 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -365,3 +365,10 @@ Validate extension JSON: Error: Field 'classes/Animation/methods/track_find_key/ Added optional arguments to avoid finding keys out of the animation range (GH-86661), and to handle backward seeking. Compatibility method registered. + + +GH-91382 +-------- +Validate extension JSON: Error: Field 'classes/AudioStreamPlaybackPolyphonic/methods/play_stream/arguments': size changed value in new API, from 4 to 6. + +Optional arguments added. Compatibility methods registered. diff --git a/modules/minimp3/audio_stream_mp3.cpp b/modules/minimp3/audio_stream_mp3.cpp index a46a1c93b52c..5720f844bb4d 100644 --- a/modules/minimp3/audio_stream_mp3.cpp +++ b/modules/minimp3/audio_stream_mp3.cpp @@ -145,6 +145,22 @@ void AudioStreamPlaybackMP3::tag_used_streams() { mp3_stream->tag_used(get_playback_position()); } +void AudioStreamPlaybackMP3::set_is_sample(bool p_is_sample) { + _is_sample = p_is_sample; +} + +bool AudioStreamPlaybackMP3::get_is_sample() const { + return _is_sample; +} + +Ref AudioStreamPlaybackMP3::get_sample_playback() const { + return sample_playback; +} + +void AudioStreamPlaybackMP3::set_sample_playback(const Ref &p_playback) { + sample_playback = p_playback; +} + void AudioStreamPlaybackMP3::set_parameter(const StringName &p_name, const Variant &p_value) { if (p_name == SNAME("looping")) { if (p_value == Variant()) { @@ -287,6 +303,18 @@ int AudioStreamMP3::get_bar_beats() const { return bar_beats; } +Ref AudioStreamMP3::generate_sample() const { + Ref sample; + sample.instantiate(); + sample->stream = this; + sample->loop_mode = loop + ? AudioSample::LoopMode::LOOP_FORWARD + : AudioSample::LoopMode::LOOP_DISABLED; + sample->loop_begin = loop_offset; + sample->loop_end = 0; + return sample; +} + void AudioStreamMP3::_bind_methods() { ClassDB::bind_method(D_METHOD("set_data", "data"), &AudioStreamMP3::set_data); ClassDB::bind_method(D_METHOD("get_data"), &AudioStreamMP3::get_data); diff --git a/modules/minimp3/audio_stream_mp3.h b/modules/minimp3/audio_stream_mp3.h index 7d85e0a3219d..81e8f8633c48 100644 --- a/modules/minimp3/audio_stream_mp3.h +++ b/modules/minimp3/audio_stream_mp3.h @@ -58,6 +58,9 @@ class AudioStreamPlaybackMP3 : public AudioStreamPlaybackResampled { Ref mp3_stream; + bool _is_sample = false; + Ref sample_playback; + protected: virtual int _mix_internal(AudioFrame *p_buffer, int p_frames) override; virtual float get_stream_sampling_rate() override; @@ -74,6 +77,11 @@ class AudioStreamPlaybackMP3 : public AudioStreamPlaybackResampled { virtual void tag_used_streams() override; + virtual void set_is_sample(bool p_is_sample) override; + virtual bool get_is_sample() const override; + virtual Ref get_sample_playback() const override; + virtual void set_sample_playback(const Ref &p_playback) override; + virtual void set_parameter(const StringName &p_name, const Variant &p_value) override; virtual Variant get_parameter(const StringName &p_name) const override; @@ -131,6 +139,11 @@ class AudioStreamMP3 : public AudioStream { virtual bool is_monophonic() const override; + virtual bool can_be_sampled() const override { + return true; + } + virtual Ref generate_sample() const override; + virtual void get_parameter_list(List *r_parameters) override; AudioStreamMP3(); diff --git a/modules/vorbis/audio_stream_ogg_vorbis.cpp b/modules/vorbis/audio_stream_ogg_vorbis.cpp index b235b6f96cb8..ff032c88c66f 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.cpp +++ b/modules/vorbis/audio_stream_ogg_vorbis.cpp @@ -376,6 +376,22 @@ void AudioStreamPlaybackOggVorbis::seek(double p_time) { } } +void AudioStreamPlaybackOggVorbis::set_is_sample(bool p_is_sample) { + _is_sample = p_is_sample; +} + +bool AudioStreamPlaybackOggVorbis::get_is_sample() const { + return _is_sample; +} + +Ref AudioStreamPlaybackOggVorbis::get_sample_playback() const { + return sample_playback; +} + +void AudioStreamPlaybackOggVorbis::set_sample_playback(const Ref &p_playback) { + sample_playback = p_playback; +} + AudioStreamPlaybackOggVorbis::~AudioStreamPlaybackOggVorbis() { if (block_is_allocated) { vorbis_block_clear(&block); @@ -517,6 +533,18 @@ void AudioStreamOggVorbis::get_parameter_list(List *r_parameters) { r_parameters->push_back(Parameter(PropertyInfo(Variant::BOOL, "looping", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE), Variant())); } +Ref AudioStreamOggVorbis::generate_sample() const { + Ref sample; + sample.instantiate(); + sample->stream = this; + sample->loop_mode = loop + ? AudioSample::LoopMode::LOOP_FORWARD + : AudioSample::LoopMode::LOOP_DISABLED; + sample->loop_begin = loop_offset; + sample->loop_end = 0; + return sample; +} + void AudioStreamOggVorbis::_bind_methods() { ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_buffer", "buffer"), &AudioStreamOggVorbis::load_from_buffer); ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_file", "path"), &AudioStreamOggVorbis::load_from_file); diff --git a/modules/vorbis/audio_stream_ogg_vorbis.h b/modules/vorbis/audio_stream_ogg_vorbis.h index 64a7815b578d..6293951f8d31 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.h +++ b/modules/vorbis/audio_stream_ogg_vorbis.h @@ -75,6 +75,9 @@ class AudioStreamPlaybackOggVorbis : public AudioStreamPlaybackResampled { Ref vorbis_data_playback; Ref vorbis_stream; + bool _is_sample = false; + Ref sample_playback; + int _mix_frames(AudioFrame *p_buffer, int p_frames); int _mix_frames_vorbis(AudioFrame *p_buffer, int p_frames); @@ -100,6 +103,11 @@ class AudioStreamPlaybackOggVorbis : public AudioStreamPlaybackResampled { virtual void set_parameter(const StringName &p_name, const Variant &p_value) override; virtual Variant get_parameter(const StringName &p_name) const override; + virtual void set_is_sample(bool p_is_sample) override; + virtual bool get_is_sample() const override; + virtual Ref get_sample_playback() const override; + virtual void set_sample_playback(const Ref &p_playback) override; + AudioStreamPlaybackOggVorbis() {} ~AudioStreamPlaybackOggVorbis(); }; @@ -159,6 +167,11 @@ class AudioStreamOggVorbis : public AudioStream { virtual void get_parameter_list(List *r_parameters) override; + virtual bool can_be_sampled() const override { + return true; + } + virtual Ref generate_sample() const override; + AudioStreamOggVorbis(); virtual ~AudioStreamOggVorbis(); }; diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index ec3c22bf7c5d..dd986e650cf1 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -33,6 +33,7 @@ #include "godot_audio.h" #include "core/config/project_settings.h" +#include "servers/audio/audio_stream.h" #include @@ -186,6 +187,181 @@ Error AudioDriverWeb::input_stop() { return OK; } +bool AudioDriverWeb::is_stream_registered_as_sample(const Ref &p_stream) const { + ERR_FAIL_COND_V_MSG(p_stream.is_null(), false, "Parameter p_stream is null."); + return godot_audio_sample_stream_is_registered(itos(p_stream->get_instance_id()).utf8().get_data()) != 0; +} + +void AudioDriverWeb::register_sample(const Ref &p_sample) { + ERR_FAIL_COND_MSG(p_sample.is_null(), "Parameter p_sample is null."); + ERR_FAIL_COND_MSG(p_sample->stream.is_null(), "Parameter p_sample->stream is null."); + + String loop_mode; + switch (p_sample->loop_mode) { + case AudioSample::LoopMode::LOOP_DISABLED: { + loop_mode = "disabled"; + } break; + + case AudioSample::LoopMode::LOOP_FORWARD: { + loop_mode = "forward"; + } break; + + case AudioSample::LoopMode::LOOP_PINGPONG: { + loop_mode = "pingpong"; + } break; + + case AudioSample::LoopMode::LOOP_BACKWARD: { + loop_mode = "backward"; + } break; + } + + double length = p_sample->stream->get_length(); + + Vector frames; + int frames_total = mix_rate * length; + { + Ref stream_playback = p_sample->stream->instantiate_playback(); + frames.resize(frames_total); + AudioFrame *frames_ptr = frames.ptrw(); + stream_playback->start(); + stream_playback->mix(frames_ptr, 1.0f, frames_total); + } + + PackedFloat32Array data; + data.resize(frames_total * 2); + float *data_ptrw = data.ptrw(); + for (int i = 0; i < frames_total; i++) { + data_ptrw[i] = frames[i].left; + data_ptrw[i + frames_total] = frames[i].right; + } + + godot_audio_sample_register_stream( + itos(p_sample->stream->get_instance_id()).utf8().get_data(), + data_ptrw, + frames_total, + loop_mode.utf8().get_data(), + p_sample->loop_begin, + p_sample->loop_end); +} + +void AudioDriverWeb::unregister_sample(const Ref &p_sample) { + ERR_FAIL_COND_MSG(p_sample.is_null(), "Parameter p_sample is null."); + ERR_FAIL_COND_MSG(p_sample->stream.is_null(), "Parameter p_sample->stream is null."); + + godot_audio_sample_unregister_stream(itos(p_sample->stream->get_instance_id()).utf8().get_data()); +} + +void AudioDriverWeb::start_sample_playback(const Ref &p_playback) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + ERR_FAIL_COND_MSG(p_playback->stream.is_null(), "Parameter p_playback->stream is null."); + + constexpr int real_max_channels = AudioServer::MAX_CHANNELS_PER_BUS * 2; + PackedFloat32Array volume; + volume.resize(real_max_channels); + float *volume_ptrw = volume.ptrw(); + for (int i = 0; i < real_max_channels; i += 2) { + if (p_playback->volume_vector.is_empty()) { + volume_ptrw[i] = 0; + volume_ptrw[i + 1] = 0; + } else { + const AudioFrame &frame = p_playback->volume_vector[i / 2]; + volume_ptrw[i] = frame.left; + volume_ptrw[i + 1] = frame.right; + } + } + godot_audio_sample_start( + itos(p_playback->get_instance_id()).utf8().get_data(), + itos(p_playback->stream->get_instance_id()).utf8().get_data(), + AudioServer::get_singleton()->get_bus_index(p_playback->bus), + p_playback->offset, + volume_ptrw); +} + +void AudioDriverWeb::stop_sample_playback(const Ref &p_playback) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + godot_audio_sample_stop(itos(p_playback->get_instance_id()).utf8().get_data()); +} + +void AudioDriverWeb::set_sample_playback_pause(const Ref &p_playback, bool p_paused) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + godot_audio_sample_set_pause(itos(p_playback->get_instance_id()).utf8().get_data(), p_paused); +} + +bool AudioDriverWeb::is_sample_playback_active(const Ref &p_playback) { + ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null."); + return godot_audio_sample_is_active(itos(p_playback->get_instance_id()).utf8().get_data()) != 0; +} + +void AudioDriverWeb::update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + godot_audio_sample_update_pitch_scale( + itos(p_playback->get_instance_id()).utf8().get_data(), + p_pitch_scale); +} + +void AudioDriverWeb::set_sample_playback_bus_volumes_linear(const Ref &p_playback, const HashMap> &p_bus_volumes) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + + constexpr int real_max_channels = AudioServer::MAX_CHANNELS_PER_BUS * 2; + + PackedInt32Array buses; + buses.resize(p_bus_volumes.size()); + int32_t *buses_ptrw = buses.ptrw(); + PackedFloat32Array values; + values.resize(p_bus_volumes.size() * AudioServer::MAX_CHANNELS_PER_BUS * 2); + float *values_ptrw = values.ptrw(); + int idx = 0; + for (KeyValue> pair : p_bus_volumes) { + int bus_index = AudioServer::get_singleton()->get_bus_index(pair.key); + buses_ptrw[idx] = bus_index; + ERR_FAIL_COND(pair.value.size() != AudioServer::MAX_CHANNELS_PER_BUS); + for (int i = 0; i < real_max_channels; i += 2) { + const AudioFrame &frame = pair.value[i / 2]; + values_ptrw[(idx * real_max_channels) + i] = frame.left; + values_ptrw[(idx * real_max_channels) + i + 1] = frame.right; + } + idx++; + } + godot_audio_sample_set_volumes_linear( + itos(p_playback->get_instance_id()).utf8().get_data(), + buses_ptrw, + buses.size(), + values_ptrw, + values.size()); +} + +void AudioDriverWeb::set_sample_bus_count(int p_count) { + godot_audio_sample_bus_set_count(p_count); +} + +void AudioDriverWeb::remove_sample_bus(int p_index) { + godot_audio_sample_bus_remove(p_index); +} + +void AudioDriverWeb::add_sample_bus(int p_at_pos) { + godot_audio_sample_bus_add(p_at_pos); +} + +void AudioDriverWeb::move_sample_bus(int p_bus, int p_to_pos) { + godot_audio_sample_bus_move(p_bus, p_to_pos); +} + +void AudioDriverWeb::set_sample_bus_send(int p_bus, const StringName &p_send) { + godot_audio_sample_bus_set_send(p_bus, AudioServer::get_singleton()->get_bus_index(p_send)); +} + +void AudioDriverWeb::set_sample_bus_volume_db(int p_bus, float p_volume_db) { + godot_audio_sample_bus_set_volume_db(p_bus, p_volume_db); +} + +void AudioDriverWeb::set_sample_bus_solo(int p_bus, bool p_enable) { + godot_audio_sample_bus_set_solo(p_bus, p_enable); +} + +void AudioDriverWeb::set_sample_bus_mute(int p_bus, bool p_enable) { + godot_audio_sample_bus_set_mute(p_bus, p_enable); +} + #ifdef THREADS_ENABLED /// AudioWorkletNode implementation (threads) diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h index df88d0a94ca7..298ad90fae1c 100644 --- a/platform/web/audio_driver_web.h +++ b/platform/web/audio_driver_web.h @@ -87,6 +87,26 @@ class AudioDriverWeb : public AudioDriver { static void resume(); + // Samples. + virtual bool is_stream_registered_as_sample(const Ref &p_stream) const override; + virtual void register_sample(const Ref &p_sample) override; + virtual void unregister_sample(const Ref &p_sample) override; + virtual void start_sample_playback(const Ref &p_playback) override; + virtual void stop_sample_playback(const Ref &p_playback) override; + virtual void set_sample_playback_pause(const Ref &p_playback, bool p_paused) override; + virtual bool is_sample_playback_active(const Ref &p_playback) override; + virtual void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f) override; + virtual void set_sample_playback_bus_volumes_linear(const Ref &p_playback, const HashMap> &p_bus_volumes) override; + + virtual void set_sample_bus_count(int p_count) override; + virtual void remove_sample_bus(int p_index) override; + virtual void add_sample_bus(int p_at_pos = -1) override; + virtual void move_sample_bus(int p_bus, int p_to_pos) override; + virtual void set_sample_bus_send(int p_bus, const StringName &p_send) override; + virtual void set_sample_bus_volume_db(int p_bus, float p_volume_db) override; + virtual void set_sample_bus_solo(int p_bus, bool p_enable) override; + virtual void set_sample_bus_mute(int p_bus, bool p_enable) override; + AudioDriverWeb() {} }; diff --git a/platform/web/eslint.config.cjs b/platform/web/eslint.config.cjs index 65ade096a7ff..8913a738f635 100644 --- a/platform/web/eslint.config.cjs +++ b/platform/web/eslint.config.cjs @@ -147,6 +147,7 @@ module.exports = [ 'GodotEventListeners': true, 'GodotFS': true, 'GodotOS': true, + 'GodotAudio': true, 'GodotRuntime': true, 'IDHandler': true, 'XRWebGLLayer': true, diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h index a32d5acd9768..8bebbcf7de1d 100644 --- a/platform/web/godot_audio.h +++ b/platform/web/godot_audio.h @@ -47,6 +47,26 @@ extern void godot_audio_resume(); extern int godot_audio_input_start(); extern void godot_audio_input_stop(); +// Samples +extern int godot_audio_sample_stream_is_registered(const char *p_stream_object_id); +extern void godot_audio_sample_register_stream(const char *p_stream_object_id, float *p_frames_buf, int p_frames_total, const char *p_loop_mode, int p_loop_begin, int p_loop_end); +extern void godot_audio_sample_unregister_stream(const char *p_stream_object_id); +extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float *p_volume_ptr); +extern void godot_audio_sample_stop(const char *p_playback_object_id); +extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause); +extern int godot_audio_sample_is_active(const char *p_playback_object_id); +extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale); +extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size); + +extern void godot_audio_sample_bus_set_count(int p_count); +extern void godot_audio_sample_bus_remove(int p_index); +extern void godot_audio_sample_bus_add(int p_at_pos = -1); +extern void godot_audio_sample_bus_move(int p_bus, int p_to_pos); +extern void godot_audio_sample_bus_set_send(int p_bus, int p_send_index); +extern void godot_audio_sample_bus_set_volume_db(int p_bus, float p_volume_db); +extern void godot_audio_sample_bus_set_solo(int p_bus, bool p_enable); +extern void godot_audio_sample_bus_set_mute(int p_bus, bool p_enable); + // Worklet typedef int32_t GodotAudioState[4]; extern int godot_audio_worklet_create(int p_channels); diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js index 601fa4a117b7..c366683dfaa8 100644 --- a/platform/web/js/engine/features.js +++ b/platform/web/js/engine/features.js @@ -76,7 +76,8 @@ const Features = { */ getMissingFeatures: function (supportedFeatures = {}) { const { - threads: supportsThreads = true, + // Quotes are needed for the Closure compiler. + 'threads': supportsThreads = true, } = supportedFeatures; const missing = []; diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js index b54c5cac855c..d7baece78161 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -28,18 +28,1048 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -const GodotAudio = { +/** + * @typedef { "disabled" | "forward" | "backward" | "pingpong" } LoopMode + */ + +/** + * @typedef {{ + * id: string + * audioBuffer: AudioBuffer + * }} SampleParams + * @typedef {{ + * numberOfChannels?: number + * sampleRate?: number + * loopMode?: LoopMode + * loopBegin?: number + * loopEnd?: number + * }} SampleOptions + */ + +/** + * Represents a sample, memory-wise. + * @class + */ +class Sample { + /** + * Returns a `Sample`. + * @param {string} id Id of the `Sample` to get. + * @returns {Sample} + * @throws {ReferenceError} When no `Sample` is found + */ + static getSample(id) { + if (!GodotAudio.samples.has(id)) { + throw new ReferenceError(`Could not find sample "${id}"`); + } + return GodotAudio.samples.get(id); + } + + /** + * Returns a `Sample` or `null`, if it doesn't exist. + * @param {string} id Id of the `Sample` to get. + * @returns {Sample?} + */ + static getSampleOrNull(id) { + return GodotAudio.samples.get(id) ?? null; + } + + /** + * Creates a `Sample` based on the params. Will register it to the + * `GodotAudio.samples` registry. + * @param {SampleParams} params Base params + * @param {SampleOptions} [options={}] Optional params + * @returns {Sample} + */ + static create(params, options = {}) { + const sample = new GodotAudio.Sample(params, options); + GodotAudio.samples.set(params.id, sample); + return sample; + } + + /** + * Deletes a `Sample` based on the id. + * @param {string} id `Sample` id to delete + * @returns {void} + */ + static delete(id) { + GodotAudio.samples.delete(id); + } + + /** + * `Sample` constructor. + * @param {SampleParams} params Base params + * @param {SampleOptions} [options={}] Optional params + * @constructor + */ + constructor(params, options = {}) { + /** @type {string} */ + this.id = params.id; + /** @type {AudioBuffer} */ + this._audioBuffer = null; + /** @type {number} */ + this.numberOfChannels = options.numberOfChannels ?? 2; + /** @type {number} */ + this.sampleRate = options.sampleRate ?? 44100; + /** @type {LoopMode} */ + this.loopMode = options.loopMode ?? 'disabled'; + /** @type {number} */ + this.loopBegin = options.loopBegin ?? 0; + /** @type {number} */ + this.loopEnd = options.loopEnd ?? 0; + + this.setAudioBuffer(params.audioBuffer); + } + + /** + * Gets the audio buffer of the sample. + * @returns {AudioBuffer} + */ + getAudioBuffer() { + return this._duplicateAudioBuffer(); + } + + /** + * Sets the audio buffer of the sample. + * @param {AudioBuffer} val The audio buffer to set. + * @returns {void} + */ + setAudioBuffer(val) { + this._audioBuffer = val; + } + + /** + * Clears the current sample. + * @returns {void} + */ + clear() { + this.audioBuffer = null; + GodotAudio.Sample.delete(this.id); + } + + /** + * Returns a duplicate of the stored audio buffer. + * @returns {AudioBuffer} + */ + _duplicateAudioBuffer() { + if (this._audioBuffer == null) { + throw new Error('couldn\'t duplicate a null audioBuffer'); + } + /** @type {Float32Array[]} */ + const channels = new Array(this._audioBuffer.numberOfChannels); + for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) { + const channel = new Float32Array(this._audioBuffer.getChannelData(i)); + channels[i] = channel; + } + const buffer = GodotAudio.ctx.createBuffer( + this.numberOfChannels, + this._audioBuffer.length, + this._audioBuffer.sampleRate + ); + for (let i = 0; i < channels.length; i++) { + buffer.copyToChannel(channels[i], i, 0); + } + return buffer; + } +} + +/** + * Represents a `SampleNode` linked to a `Bus`. + * @class + */ +class SampleNodeBus { + /** + * Creates a new `SampleNodeBus`. + * @param {Bus} bus The bus related to the new `SampleNodeBus`. + * @returns {SampleNodeBus} + */ + static create(bus) { + return new GodotAudio.SampleNodeBus(bus); + } + + /** + * `SampleNodeBus` constructor. + * @param {Bus} bus The bus related to the new `SampleNodeBus`. + * @constructor + */ + constructor(bus) { + const NUMBER_OF_WEB_CHANNELS = 6; + + /** @type {Bus} */ + this._bus = bus; + + /** @type {ChannelSplitterNode} */ + this._channelSplitter = GodotAudio.ctx.createChannelSplitter(NUMBER_OF_WEB_CHANNELS); + /** @type {GainNode} */ + this._l = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._r = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._sl = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._sr = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._c = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._lfe = GodotAudio.ctx.createGain(); + /** @type {ChannelMergerNode} */ + this._channelMerger = GodotAudio.ctx.createChannelMerger(NUMBER_OF_WEB_CHANNELS); + + this._channelSplitter + .connect(this._l, GodotAudio.WebChannel.CHANNEL_L) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_L + ); + this._channelSplitter + .connect(this._r, GodotAudio.WebChannel.CHANNEL_R) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_R + ); + this._channelSplitter + .connect(this._sl, GodotAudio.WebChannel.CHANNEL_SL) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_SL + ); + this._channelSplitter + .connect(this._sr, GodotAudio.WebChannel.CHANNEL_SR) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_SR + ); + this._channelSplitter + .connect(this._c, GodotAudio.WebChannel.CHANNEL_C) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_C + ); + this._channelSplitter + .connect(this._lfe, GodotAudio.WebChannel.CHANNEL_L) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_LFE + ); + + this._channelMerger.connect(this._bus.getInputNode()); + } + + /** + * Returns the input node. + * @returns {AudioNode} + */ + getInputNode() { + return this._channelSplitter; + } + + /** + * Returns the output node. + * @returns {AudioNode} + */ + getOutputNode() { + return this._channelMerger; + } + + /** + * Sets the volume for each (split) channel. + * @param {Float32Array} volume Volume array from the engine for each channel. + * @returns {void} + */ + setVolume(volume) { + if (volume.length !== GodotAudio.MAX_VOLUME_CHANNELS) { + throw new Error( + `Volume length isn't "${GodotAudio.MAX_VOLUME_CHANNELS}", is ${volume.length} instead` + ); + } + this._l.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_L] ?? 0; + this._r.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_R] ?? 0; + this._sl.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SL] ?? 0; + this._sr.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SR] ?? 0; + this._c.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_C] ?? 0; + this._lfe.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_LFE] ?? 0; + } + + /** + * Clears the current `SampleNodeBus` instance. + * @returns {void} + */ + clear() { + this._bus = null; + this._channelSplitter.disconnect(); + this._channelSplitter = null; + this._l.disconnect(); + this._l = null; + this._r.disconnect(); + this._r = null; + this._sl.disconnect(); + this._sl = null; + this._sr.disconnect(); + this._sr = null; + this._c.disconnect(); + this._c = null; + this._lfe.disconnect(); + this._lfe = null; + this._channelMerger.disconnect(); + this._channelMerger = null; + } +} + +/** + * @typedef {{ + * id: string + * streamObjectId: string + * busIndex: number + * }} SampleNodeParams + * @typedef {{ + * offset?: number + * playbackRate?: number + * startTime?: number + * loopMode?: LoopMode + * volume?: Float32Array + * }} SampleNodeOptions + */ + +/** + * Represents an `AudioNode` of a `Sample`. + * @class + */ +class SampleNode { + /** + * Returns a `SampleNode`. + * @param {string} id Id of the `SampleNode`. + * @returns {SampleNode} + * @throws {ReferenceError} When no `SampleNode` is not found + */ + static getSampleNode(id) { + if (!GodotAudio.sampleNodes.has(id)) { + throw new ReferenceError(`Could not find sample node "${id}"`); + } + return GodotAudio.sampleNodes.get(id); + } + + /** + * Returns a `SampleNode`, returns null if not found. + * @param {string} id Id of the SampleNode. + * @returns {SampleNode?} + */ + static getSampleNodeOrNull(id) { + return GodotAudio.sampleNodes.get(id) ?? null; + } + + /** + * Stops a `SampleNode` by id. + * @param {string} id Id of the `SampleNode` to stop. + * @returns {void} + */ + static stopSampleNode(id) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id); + if (sampleNode == null) { + return; + } + sampleNode.stop(); + } + + /** + * Pauses the `SampleNode` by id. + * @param {string} id Id of the `SampleNode` to pause. + * @param {boolean} enable State of the pause + * @returns {void} + */ + static pauseSampleNode(id, enable) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id); + if (sampleNode == null) { + return; + } + sampleNode.pause(enable); + } + + /** + * Creates a `SampleNode` based on the params. Will register the `SampleNode` to + * the `GodotAudio.sampleNodes` regisery. + * @param {SampleNodeParams} params Base params. + * @param {SampleNodeOptions} options Optional params. + * @returns {SampleNode} + */ + static create(params, options = {}) { + const sampleNode = new GodotAudio.SampleNode(params, options); + GodotAudio.sampleNodes.set(params.id, sampleNode); + return sampleNode; + } + + /** + * Deletes a `SampleNode` based on the id. + * @param {string} id Id of the `SampleNode` to delete. + * @returns {void} + */ + static delete(id) { + GodotAudio.sampleNodes.delete(id); + } + + /** + * @param {SampleNodeParams} params Base params + * @param {SampleNodeOptions} [options={}] Optional params + * @constructor + */ + constructor(params, options = {}) { + /** @type {string} */ + this.id = params.id; + /** @type {string} */ + this.streamObjectId = params.streamObjectId; + /** @type {number} */ + this.offset = options.offset ?? 0; + /** @type {LoopMode} */ + this.startTime = options.startTime ?? 0; + /** @type {number} */ + this.pauseTime = 0; + /** @type {number} */ + this._playbackRate = 44100; + /** @type {LoopMode} */ + this._loopMode = 'disabled'; + /** @type {number} */ + this._pitchScale = 1; + /** @type {Map} */ + this._sampleNodeBuses = new Map(); + /** @type {AudioBufferSourceNode} */ + this._source = GodotAudio.ctx.createBufferSource(); + + this.setPlaybackRate(options.playbackRate ?? 44100); + this.setLoopMode(options.loopMode ?? this.getSample().loopMode ?? 'disabled'); + this._source.buffer = this.getSample().getAudioBuffer(); + + /** @type {SampleNode} */ + // eslint-disable-next-line consistent-this + const self = this; + this._source.addEventListener('ended', (_) => { + switch (self.getSample().loopMode) { + case 'disabled': + GodotAudio.SampleNode.stopSampleNode(self.id); + break; + default: + // do nothing + } + }); + + const bus = GodotAudio.Bus.getBus(params.busIndex); + const sampleNodeBus = this.getSampleNodeBus(bus); + sampleNodeBus.setVolume(options.volume); + } + + /** + * Gets the loop mode of the current instance. + * @returns {LoopMode} + */ + getLoopMode() { + return this._loopMode; + } + + /** + * Sets the loop mode of the current instance. + * @param {LoopMode} val Value to set. + * @returns {void} + */ + setLoopMode(val) { + this._loopMode = val; + switch (val) { + case 'forward': + case 'backward': + this._source.loop = true; + break; + default: + this._source.loop = false; + } + } + + /** + * Gets the playback rate. + * @returns {number} + */ + getPlaybackRate() { + return this._playbackRate; + } + + /** + * Sets the playback rate. + * @param {number} val Value to set. + * @returns {void} + */ + setPlaybackRate(val) { + this._playbackRate = val; + this._syncPlaybackRate(); + } + + /** + * Gets the pitch scale. + * @returns {number} + */ + getPitchScale() { + return this._pitchScale; + } + + /** + * Sets the pitch scale. + * @param {number} val Value to set. + * @returns {void} + */ + setPitchScale(val) { + this._pitchScale = val; + this._syncPlaybackRate(); + } + + /** + * Returns the linked `Sample`. + * @returns {Sample} + */ + getSample() { + return GodotAudio.Sample.getSample(this.streamObjectId); + } + + /** + * Returns the output node. + * @returns {AudioNode} + */ + getOutputNode() { + return this._source; + } + + /** + * Starts the `SampleNode`. + * @returns {void} + */ + start() { + this._source.start(this.offset); + } + + /** + * Stops the `SampleNode`. + * @returns {void} + */ + stop() { + this._source.stop(); + this.clear(); + } + + /** + * Pauses the `SampleNode`. + * @param {boolean} [enable=true] State of the pause. + * @returns {void} + */ + pause(enable = true) { + if (enable) { + this.pauseTime = (GodotAudio.ctx.currentTime - this.startTime) / this.playbackRate; + this._source.stop(); + return; + } + + if (this.pauseTime === 0) { + return; + } + + this._source.disconnect(); + this._source = GodotAudio.ctx.createBufferSource(); + + this._source.buffer = this.getSample().getAudioBuffer(); + this._source.connect(this._gain); + this._source.start(this.offset + this.pauseTime); + } + + /** + * Connects an AudioNode to the output node of this `SampleNode`. + * @param {AudioNode} node AudioNode to connect. + * @returns {void} + */ + connect(node) { + return this.getOutputNode().connect(node); + } + + /** + * Sets the volumes of the `SampleNode` for each buses passed in parameters. + * @param {Bus[]} buses + * @param {Float32Array} volumes + */ + setVolumes(buses, volumes) { + for (let busIdx = 0; busIdx < buses.length; busIdx++) { + const sampleNodeBus = this.getSampleNodeBus(buses[busIdx]); + sampleNodeBus.setVolume( + volumes.slice( + busIdx * GodotAudio.MAX_VOLUME_CHANNELS, + (busIdx * GodotAudio.MAX_VOLUME_CHANNELS) + GodotAudio.MAX_VOLUME_CHANNELS + ) + ); + } + } + + /** + * Returns the SampleNodeBus based on the bus in parameters. + * @param {Bus} bus Bus to get the SampleNodeBus from. + * @returns {SampleNodeBus} + */ + getSampleNodeBus(bus) { + if (!this._sampleNodeBuses.has(bus)) { + const sampleNodeBus = GodotAudio.SampleNodeBus.create(bus); + this._sampleNodeBuses.set(bus, sampleNodeBus); + this._source.connect(sampleNodeBus.getInputNode()); + } + return this._sampleNodeBuses.get(bus); + } + + /** + * Clears the `SampleNode`. + * @returns {void} + */ + clear() { + this._source.stop(); + this._source.disconnect(); + this._source = null; + + for (const sampleNodeBus of this._sampleNodeBuses.values()) { + sampleNodeBus.clear(); + } + this._sampleNodeBuses.clear(); + this._sampleNodeBuses = null; + + GodotAudio.SampleNode.delete(this.id); + } + + /** + * Syncs the `AudioNode` playback rate based on the `SampleNode` playback rate and pitch scale. + * @returns {void} + */ + _syncPlaybackRate() { + this._source.playbackRate.value = this.getPlaybackRate() * this.getPitchScale(); + } +} + +/** + * Collection of nodes to represents a Godot Engine audio bus. + * @class + */ +class Bus { + /** + * Returns the number of registered buses. + * @returns {number} + */ + static getCount() { + return GodotAudio.buses.length; + } + + /** + * Sets the number of registered buses. + * Will delete buses if lower than the current number. + * @param {number} val Count of registered buses. + * @returns {void} + */ + static setCount(val) { + const buses = GodotAudio.buses; + if (val === buses.length) { + return; + } + + if (val < buses.length) { + // TODO: what to do with nodes connected to the deleted buses? + const deletedBuses = buses.slice(val); + for (let i = 0; i < deletedBuses.length; i++) { + const deletedBus = deletedBuses[i]; + deletedBus.clear(); + } + GodotAudio.buses = buses.slice(0, val); + return; + } + + for (let i = GodotAudio.buses.length; i < val; i++) { + GodotAudio.Bus.create(); + } + } + + /** + * Returns a `Bus` based on it's index number. + * @param {number} index + * @returns {Bus} + * @throws {ReferenceError} If the index value is outside the registry. + */ + static getBus(index) { + if (index < 0 || index >= GodotAudio.buses.length) { + throw new ReferenceError(`invalid bus index "${index}"`); + } + return GodotAudio.buses[index]; + } + + /** + * Returns a `Bus` based on it's index number. Returns null if it doesn't exist. + * @param {number} index + * @returns {Bus?} + */ + static getBusOrNull(index) { + if (index < 0 || index >= GodotAudio.buses.length) { + return null; + } + return GodotAudio.buses[index]; + } + + /** + * Move a bus from an index to another. + * @param {number} fromIndex From index + * @param {number} toIndex To index + * @returns {void} + */ + static move(fromIndex, toIndex) { + const movedBus = GodotAudio.Bus.getBus(fromIndex); + let buses = GodotAudio.buses; + buses = buses.filter((_, i) => i !== fromIndex); + // Inserts at index. + buses.splice(toIndex - 1, 0, movedBus); + GodotAudio.buses = buses; + } + + /** + * Adds a new bus at the specified index. + * @param {number} index Index to add a new bus. + * @returns {void} + */ + static addAt(index) { + const newBus = GodotAudio.Bus.create(); + if (index !== newBus.getId()) { + GodotAudio.Bus.move(newBus.getId(), index); + } + } + + /** + * Creates a `Bus` and registers it. + * @returns {Bus} + */ + static create() { + const newBus = new GodotAudio.Bus(); + const isFirstBus = GodotAudio.buses.length === 0; + GodotAudio.buses.push(newBus); + if (isFirstBus) { + newBus.setSend(null); + } else { + newBus.setSend(GodotAudio.Bus.getBus(0)); + } + return newBus; + } + + /** + * `Bus` constructor. + * @constructor + */ + constructor() { + /** @type {Set} */ + this._sampleNodes = new Set(); + /** @type {boolean} */ + this.isSolo = false; + /** @type {Bus?} */ + this._send = null; + + /** @type {GainNode} */ + this._gainNode = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._soloNode = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._muteNode = GodotAudio.ctx.createGain(); + + this._gainNode + .connect(this._soloNode) + .connect(this._muteNode); + } + + /** + * Returns the current id of the bus (its index). + * @returns {number} + */ + getId() { + return GodotAudio.buses.indexOf(this); + } + + /** + * Returns the bus volume db value. + * @returns {number} + */ + getVolumeDb() { + return GodotAudio.linear_to_db(this._gainNode.gain.value); + } + + /** + * Sets the bus volume db value. + * @param {number} val Value to set + * @returns {void} + */ + setVolumeDb(val) { + this._gainNode.gain.value = GodotAudio.db_to_linear(val); + } + + /** + * Returns the "send" bus. + * If null, this bus sends its contents directly to the output. + * If not null, this bus sends its contents to another bus. + * @returns {Bus?} + */ + getSend() { + return this._send; + } + + /** + * Sets the "send" bus. + * If null, this bus sends its contents directly to the output. + * If not null, this bus sends its contents to another bus. + * + * **Note:** if null, `getId()` must be equal to 0. Otherwise, it will throw. + * @param {Bus?} val + * @returns {void} + * @throws {Error} When val is `null` and `getId()` isn't equal to 0 + */ + setSend(val) { + this._send = val; + if (val == null) { + if (this.getId() == 0) { + this.getOutputNode().connect(GodotAudio.ctx.destination); + return; + } + throw new Error( + `Cannot send to "${val}" without the bus being at index 0 (current index: ${this.getId()})` + ); + } + this.connect(val); + } + + /** + * Returns the input node of the bus. + * @returns {AudioNode} + */ + getInputNode() { + return this._gainNode; + } + + /** + * Returns the output node of the bus. + * @returns {AudioNode} + */ + getOutputNode() { + return this._muteNode; + } + + /** + * Sets the mute status of the bus. + * @param {boolean} enable + */ + mute(enable) { + this._muteNode.gain.value = enable ? 0 : 1; + } + + /** + * Sets the solo status of the bus. + * @param {boolean} enable + */ + solo(enable) { + if (this.isSolo === enable) { + return; + } + + if (enable) { + if (GodotAudio.busSolo != null && GodotAudio.busSolo !== this) { + GodotAudio.busSolo._disableSolo(); + } + this._enableSolo(); + return; + } + + this._disableSolo(); + } + + /** + * Wrapper to simply add a sample node to the bus. + * @param {SampleNode} sampleNode `SampleNode` to remove + * @returns {void} + */ + addSampleNode(sampleNode) { + this._sampleNodes.add(sampleNode); + sampleNode.getOutputNode().connect(this.getInputNode()); + } + + /** + * Wrapper to simply remove a sample node from the bus. + * @param {SampleNode} sampleNode `SampleNode` to remove + * @returns {void} + */ + removeSampleNode(sampleNode) { + this._sampleNodes.delete(sampleNode); + sampleNode.getOutputNode().disconnect(); + } + + /** + * Wrapper to simply connect to another bus. + * @param {Bus} bus + * @returns {void} + */ + connect(bus) { + if (bus == null) { + throw new Error('cannot connect to null bus'); + } + this.getOutputNode().disconnect(); + this.getOutputNode().connect(bus.getInputNode()); + return bus; + } + + /** + * Clears the current bus. + * @returns {void} + */ + clear() { + GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this); + } + + /** @type {Bus["prototype"]["_syncSampleNodes"]} */ + _syncSampleNodes() { + const sampleNodes = Array.from(this._sampleNodes); + for (let i = 0; i < sampleNodes.length; i++) { + const sampleNode = sampleNodes[i]; + sampleNode.getOutputNode().disconnect(); + sampleNode.getOutputNode().connect(this.getInputNode()); + } + } + + /** + * Process to enable solo. + * @returns {void} + */ + _enableSolo() { + this.isSolo = true; + GodotAudio.busSolo = this; + this._soloNode.gain.value = 1; + const otherBuses = GodotAudio.buses.filter( + (otherBus) => otherBus !== this + ); + for (let i = 0; i < otherBuses.length; i++) { + const otherBus = otherBuses[i]; + otherBus._soloNode.gain.value = 0; + } + } + + /** + * Process to disable solo. + * @returns {void} + */ + _disableSolo() { + this.isSolo = false; + GodotAudio.busSolo = null; + this._soloNode.gain.value = 1; + const otherBuses = GodotAudio.buses.filter( + (otherBus) => otherBus !== this + ); + for (let i = 0; i < otherBuses.length; i++) { + const otherBus = otherBuses[i]; + otherBus._soloNode.gain.value = 1; + } + } +} + +const _GodotAudio = { $GodotAudio__deps: ['$GodotRuntime', '$GodotOS'], $GodotAudio: { + /** + * Max number of volume channels. + */ + MAX_VOLUME_CHANNELS: 8, + + /** + * Represents the index of each sound channel relative to the engine. + */ + GodotChannel: Object.freeze({ + CHANNEL_L: 0, + CHANNEL_R: 1, + CHANNEL_C: 3, + CHANNEL_LFE: 4, + CHANNEL_RL: 5, + CHANNEL_RR: 6, + CHANNEL_SL: 7, + CHANNEL_SR: 8, + }), + + /** + * Represents the index of each sound channel relative to the Web Audio API. + */ + WebChannel: Object.freeze({ + CHANNEL_L: 0, + CHANNEL_R: 1, + CHANNEL_SL: 2, + CHANNEL_SR: 3, + CHANNEL_C: 4, + CHANNEL_LFE: 5, + }), + + // `Sample` class + /** + * Registry of `Sample`s. + * @type {Map} + */ + samples: null, + Sample, + + // `SampleNodeBus` class + SampleNodeBus, + + // `SampleNode` class + /** + * Registry of `SampleNode`s. + * @type {Map} + */ + sampleNodes: null, + SampleNode, + + // `Bus` class + /** + * Registry of `Bus`es. + * @type {Bus[]} + */ + buses: null, + /** + * Reference to the current bus in solo mode. + * @type {Bus | null} + */ + busSolo: null, + Bus, + + /** @type {AudioContext} */ ctx: null, input: null, driver: null, interval: 0, + /** + * Converts linear volume to Db. + * @param {number} linear Linear value to convert. + * @returns {number} + */ + linear_to_db: function (linear) { + // eslint-disable-next-line no-loss-of-precision + return Math.log(linear) * 8.6858896380650365530225783783321; + }, + /** + * Converts Db volume to linear. + * @param {number} db Db value to convert. + * @returns {number} + */ + db_to_linear: function (db) { + // eslint-disable-next-line no-loss-of-precision + return Math.exp(db * 0.11512925464970228420089957273422); + }, + init: function (mix_rate, latency, onstatechange, onlatencyupdate) { + // Initialize classes static values. + GodotAudio.samples = new Map(); + GodotAudio.sampleNodes = new Map(); + GodotAudio.buses = []; + GodotAudio.busSolo = null; + const opts = {}; // If mix_rate is 0, let the browser choose. if (mix_rate) { + GodotAudio.sampleRate = mix_rate; opts['sampleRate'] = mix_rate; } // Do not specify, leave 'interactive' for good performance. @@ -58,8 +1088,8 @@ const GodotAudio = { case 'closed': state = 2; break; - - // no default + default: + // Do nothing. } onstatechange(state); }; @@ -148,6 +1178,163 @@ const GodotAudio = { resolve(); }); }, + + /** + * Triggered when a sample node needs to start. + * @param {string} playbackObjectId The unique id of the sample playback + * @param {string} streamObjectId The unique id of the stream + * @param {number} busIndex Index of the bus currently binded to the sample playback + * @param {SampleNodeOptions} startOptions Optional params + * @returns {void} + */ + start_sample: function ( + playbackObjectId, + streamObjectId, + busIndex, + startOptions + ) { + GodotAudio.SampleNode.stopSampleNode(playbackObjectId); + const sampleNode = GodotAudio.SampleNode.create( + { + busIndex, + id: playbackObjectId, + streamObjectId, + }, + startOptions + ); + sampleNode.start(); + }, + + /** + * Triggered when a sample node needs to be stopped. + * @param {string} playbackObjectId Id of the sample playback + * @returns {void} + */ + stop_sample: function (playbackObjectId) { + GodotAudio.SampleNode.stopSampleNode(playbackObjectId); + }, + + /** + * Triggered when a sample node needs to be paused or unpaused. + * @param {string} playbackObjectId Id of the sample playback + * @param {boolean} pause State of the pause + * @returns {void} + */ + sample_set_pause: function (playbackObjectId, pause) { + GodotAudio.SampleNode.pauseSampleNode(playbackObjectId, pause); + }, + + /** + * Triggered when a sample node needs its pitch scale to be updated. + * @param {string} playbackObjectId Id of the sample playback + * @param {number} pitchScale Pitch scale of the sample playback + * @returns {void} + */ + update_sample_pitch_scale: function (playbackObjectId, pitchScale) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId); + if (sampleNode == null) { + return; + } + sampleNode.setPitchScale(pitchScale); + }, + + /** + * Triggered when a sample node volumes need to be updated. + * @param {string} playbackObjectId Id of the sample playback + * @param {number[]} busIndexes Indexes of the buses that need to be updated + * @param {Float32Array} volumes Array of the volumes + * @returns {void} + */ + sample_set_volumes_linear: function (playbackObjectId, busIndexes, volumes) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId); + if (sampleNode == null) { + return; + } + const buses = busIndexes.map((busIndex) => GodotAudio.Bus.getBus(busIndex)); + sampleNode.setVolumes(buses, volumes); + }, + + /** + * Triggered when the bus count changes. + * @param {number} count Number of buses + * @returns {void} + */ + set_sample_bus_count: function (count) { + GodotAudio.Bus.setCount(count); + }, + + /** + * Triggered when a bus needs to be removed. + * @param {number} index Bus index + * @returns {void} + */ + remove_sample_bus: function (index) { + const bus = GodotAudio.Bus.getBus(index); + bus.clear(); + }, + + /** + * Triggered when a bus needs to be at the desired position. + * @param {number} atPos Position to add the bus + * @returns {void} + */ + add_sample_bus: function (atPos) { + GodotAudio.Bus.addAt(atPos); + }, + + /** + * Triggered when a bus needs to be moved. + * @param {number} busIndex Index of the bus to move + * @param {number} toPos Index of the new position of the bus + * @returns {void} + */ + move_sample_bus: function (busIndex, toPos) { + GodotAudio.Bus.move(busIndex, toPos); + }, + + /** + * Triggered when the "send" value of a bus changes. + * @param {number} busIndex Index of the bus to update the "send" value + * @param {number} sendIndex Index of the bus that is the new "send" + * @returns {void} + */ + set_sample_bus_send: function (busIndex, sendIndex) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.setSend(GodotAudio.Bus.getBus(sendIndex)); + }, + + /** + * Triggered when a bus needs its volume db to be updated. + * @param {number} busIndex Index of the bus to update its volume db + * @param {number} volumeDb Volume of the bus + * @returns {void} + */ + set_sample_bus_volume_db: function (busIndex, volumeDb) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.volumeDb = volumeDb; + }, + + /** + * Triggered when a bus needs to update its solo status + * @param {number} busIndex Index of the bus to update its solo status + * @param {boolean} enable Status of the solo + * @returns {void} + */ + set_sample_bus_solo: function (busIndex, enable) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.solo(enable); + }, + + /** + * Triggered when a bus needs to update its mute status + * @param {number} busIndex Index of the bus to update its mute status + * @param {boolean} enable Status of the mute + * @returns {void} + */ + set_sample_bus_mute: function (busIndex, enable) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.mute(enable); + }, }, godot_audio_is_available__sig: 'i', @@ -162,22 +1349,32 @@ const GodotAudio = { godot_audio_has_worklet__proxy: 'sync', godot_audio_has_worklet__sig: 'i', godot_audio_has_worklet: function () { - return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0; + return GodotAudio.ctx && GodotAudio.ctx.audioWorklet ? 1 : 0; }, godot_audio_has_script_processor__proxy: 'sync', godot_audio_has_script_processor__sig: 'i', godot_audio_has_script_processor: function () { - return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0; + return GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor ? 1 : 0; }, godot_audio_init__proxy: 'sync', godot_audio_init__sig: 'iiiii', - godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { + godot_audio_init: function ( + p_mix_rate, + p_latency, + p_state_change, + p_latency_update + ) { const statechange = GodotRuntime.get_func(p_state_change); const latencyupdate = GodotRuntime.get_func(p_latency_update); const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32'); - const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate); + const channels = GodotAudio.init( + mix_rate, + p_latency, + statechange, + latencyupdate + ); GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32'); return channels; }, @@ -210,10 +1407,311 @@ const GodotAudio = { GodotAudio.input = null; } }, + + godot_audio_sample_stream_is_registered__proxy: 'sync', + godot_audio_sample_stream_is_registered__sig: 'ii', + /** + * Returns if the sample stream is registered + * @param {number} streamObjectIdStrPtr Pointer of the streamObjectId + * @returns {number} + */ + godot_audio_sample_stream_is_registered: function (streamObjectIdStrPtr) { + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + return Number(GodotAudio.Sample.getSampleOrNull(streamObjectId) != null); + }, + + godot_audio_sample_register_stream__proxy: 'sync', + godot_audio_sample_register_stream__sig: 'viiiiiii', + /** + * Registers a stream. + * @param {number} streamObjectIdStrPtr StreamObjectId pointer + * @param {number} framesPtr Frames pointer + * @param {number} framesTotal Frames total value + * @param {number} loopModeStrPtr Loop mode pointer + * @param {number} loopBegin Loop begin value + * @param {number} loopEnd Loop end value + * @returns {void} + */ + godot_audio_sample_register_stream: function ( + streamObjectIdStrPtr, + framesPtr, + framesTotal, + loopModeStrPtr, + loopBegin, + loopEnd + ) { + const BYTES_PER_FLOAT32 = 4; + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + const loopMode = GodotRuntime.parseString(loopModeStrPtr); + const numberOfChannels = 2; + const sampleRate = GodotAudio.ctx.sampleRate; + + /** @type {Float32Array} */ + const subLeft = GodotRuntime.heapSub(HEAPF32, framesPtr, framesTotal); + /** @type {Float32Array} */ + const subRight = GodotRuntime.heapSub( + HEAPF32, + framesPtr + framesTotal * BYTES_PER_FLOAT32, + framesTotal + ); + + const audioBuffer = GodotAudio.ctx.createBuffer( + numberOfChannels, + framesTotal, + sampleRate + ); + audioBuffer.copyToChannel(new Float32Array(subLeft), 0, 0); + audioBuffer.copyToChannel(new Float32Array(subRight), 1, 0); + + GodotAudio.Sample.create( + { + id: streamObjectId, + audioBuffer, + }, + { + loopBegin, + loopEnd, + loopMode, + numberOfChannels, + sampleRate, + } + ); + }, + + godot_audio_sample_unregister_stream__proxy: 'sync', + godot_audio_sample_unregister_stream__sig: 'vi', + /** + * Unregisters a stream. + * @param {number} streamObjectIdStrPtr StreamObjectId pointer + * @returns {void} + */ + godot_audio_sample_unregister_stream: function (streamObjectIdStrPtr) { + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + const sample = GodotAudio.Sample.getSampleOrNull(streamObjectId); + if (sample != null) { + sample.clear(); + } + }, + + godot_audio_sample_start__proxy: 'sync', + godot_audio_sample_start__sig: 'viiiii', + /** + * Starts a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} streamObjectIdStrPtr Stream object id pointer + * @param {number} busIndex Bus index + * @param {number} offset Sample offset + * @param {number} volumePtr Volume pointer + * @returns {void} + */ + godot_audio_sample_start: function ( + playbackObjectIdStrPtr, + streamObjectIdStrPtr, + busIndex, + offset, + volumePtr + ) { + /** @type {string} */ + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + /** @type {string} */ + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + /** @type {Float32Array} */ + const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8); + /** @type {SampleNodeConstructorOptions} */ + const startOptions = { + offset, + volume, + playbackRate: 1, + }; + GodotAudio.start_sample( + playbackObjectId, + streamObjectId, + busIndex, + startOptions + ); + }, + + godot_audio_sample_stop__proxy: 'sync', + godot_audio_sample_stop__sig: 'vi', + /** + * Stops a sample from playing. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @returns {void} + */ + godot_audio_sample_stop: function (playbackObjectIdStrPtr) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.stop_sample(playbackObjectId); + }, + + godot_audio_sample_set_pause__proxy: 'sync', + godot_audio_sample_set_pause__sig: 'vii', + /** + * Sets the pause state of a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} pause Pause state + */ + godot_audio_sample_set_pause: function (playbackObjectIdStrPtr, pause) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.sample_set_pause(playbackObjectId, Boolean(pause)); + }, + + godot_audio_sample_is_active__proxy: 'sync', + godot_audio_sample_is_active__sig: 'ii', + /** + * Returns if the sample is active. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @returns {number} + */ + godot_audio_sample_is_active: function (playbackObjectIdStrPtr) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + return Number(GodotAudio.sampleNodes.has(playbackObjectId)); + }, + + godot_audio_sample_update_pitch_scale__proxy: 'sync', + godot_audio_sample_update_pitch_scale__sig: 'vii', + /** + * Updates the pitch scale of a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} pitchScale Pitch scale value + * @returns {void} + */ + godot_audio_sample_update_pitch_scale: function ( + playbackObjectIdStrPtr, + pitchScale + ) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.update_sample_pitch_scale(playbackObjectId, pitchScale); + }, + + godot_audio_sample_set_volumes_linear__proxy: 'sync', + godot_audio_sample_set_volumes_linear__sig: 'vii', + /** + * Sets the volumes linear of each mentioned bus for the sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} busesPtr Buses array pointer + * @param {number} busesSize Buses array size + * @param {number} volumesPtr Volumes array pointer + * @param {number} volumesSize Volumes array size + * @returns {void} + */ + godot_audio_sample_set_volumes_linear: function ( + playbackObjectIdStrPtr, + busesPtr, + busesSize, + volumesPtr, + volumesSize + ) { + /** @type {string} */ + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + + /** @type {Uint32Array} */ + const buses = GodotRuntime.heapSub(HEAP32, busesPtr, busesSize); + /** @type {Float32Array} */ + const volumes = GodotRuntime.heapSub(HEAPF32, volumesPtr, volumesSize); + + GodotAudio.sample_set_volumes_linear( + playbackObjectId, + Array.from(buses), + volumes + ); + }, + + godot_audio_sample_bus_set_count__proxy: 'sync', + godot_audio_sample_bus_set_count__sig: 'vi', + /** + * Sets the bus count. + * @param {number} count Bus count + * @returns {void} + */ + godot_audio_sample_bus_set_count: function (count) { + GodotAudio.set_sample_bus_count(count); + }, + + godot_audio_sample_bus_remove__proxy: 'sync', + godot_audio_sample_bus_remove__sig: 'vi', + /** + * Removes a bus. + * @param {number} index Index of the bus to remove + * @returns {void} + */ + godot_audio_sample_bus_remove: function (index) { + GodotAudio.remove_sample_bus(index); + }, + + godot_audio_sample_bus_add__proxy: 'sync', + godot_audio_sample_bus_add__sig: 'vi', + /** + * Adds a bus at the defined position. + * @param {number} atPos Position to add the bus + * @returns {void} + */ + godot_audio_sample_bus_add: function (atPos) { + GodotAudio.add_sample_bus(atPos); + }, + + godot_audio_sample_bus_move__proxy: 'sync', + godot_audio_sample_bus_move__sig: 'vii', + /** + * Moves the bus from a position to another. + * @param {number} fromPos Position of the bus to move + * @param {number} toPos Final position of the bus + * @returns {void} + */ + godot_audio_sample_bus_move: function (fromPos, toPos) { + GodotAudio.move_sample_bus(fromPos, toPos); + }, + + godot_audio_sample_bus_set_send__proxy: 'sync', + godot_audio_sample_bus_set_send__sig: 'vii', + /** + * Sets the "send" of a bus. + * @param {number} bus Position of the bus to set the send + * @param {number} sendIndex Position of the "send" bus + * @returns {void} + */ + godot_audio_sample_bus_set_send: function (bus, sendIndex) { + GodotAudio.set_sample_bus_send(bus, sendIndex); + }, + + godot_audio_sample_bus_set_volume_db__proxy: 'sync', + godot_audio_sample_bus_set_volume_db__sig: 'vii', + /** + * Sets the volume db of a bus. + * @param {number} bus Position of the bus to set the volume db + * @param {number} volumeDb Volume db to set + * @returns {void} + */ + godot_audio_sample_bus_set_volume_db: function (bus, volumeDb) { + GodotAudio.set_sample_bus_volume_db(bus, volumeDb); + }, + + godot_audio_sample_bus_set_solo__proxy: 'sync', + godot_audio_sample_bus_set_solo__sig: 'vii', + /** + * Sets the state of solo for a bus + * @param {number} bus Position of the bus to set the solo state + * @param {number} enable State of the solo + * @returns {void} + */ + godot_audio_sample_bus_set_solo: function (bus, enable) { + GodotAudio.set_sample_bus_solo(bus, Boolean(enable)); + }, + + godot_audio_sample_bus_set_mute__proxy: 'sync', + godot_audio_sample_bus_set_mute__sig: 'vii', + /** + * Sets the state of mute for a bus + * @param {number} bus Position of the bus to set the mute state + * @param {number} enable State of the mute + * @returns {void} + */ + godot_audio_sample_bus_set_mute: function (bus, enable) { + GodotAudio.set_sample_bus_mute(bus, Boolean(enable)); + }, }; -autoAddDeps(GodotAudio, '$GodotAudio'); -mergeInto(LibraryManager.library, GodotAudio); +autoAddDeps(_GodotAudio, '$GodotAudio'); +mergeInto(LibraryManager.library, _GodotAudio); /** * The AudioWorklet API driver, used when threads are available. @@ -227,16 +1725,18 @@ const GodotAudioWorklet = { create: function (channels) { const path = GodotConfig.locate_file('godot.audio.worklet.js'); - GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function () { - GodotAudioWorklet.worklet = new AudioWorkletNode( - GodotAudio.ctx, - 'godot-processor', - { - 'outputChannelCount': [channels], - } - ); - return Promise.resolve(); - }); + GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet + .addModule(path) + .then(function () { + GodotAudioWorklet.worklet = new AudioWorkletNode( + GodotAudio.ctx, + 'godot-processor', + { + outputChannelCount: [channels], + } + ); + return Promise.resolve(); + }); GodotAudio.driver = GodotAudioWorklet; }, @@ -254,7 +1754,14 @@ const GodotAudioWorklet = { }); }, - start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) { + start_no_threads: function ( + p_out_buf, + p_out_size, + out_callback, + p_in_buf, + p_in_size, + in_callback + ) { function RingBuffer() { let wpos = 0; let rpos = 0; @@ -276,7 +1783,10 @@ const GodotAudioWorklet = { wpos = 0; } if (pending_samples > 0) { - wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples); + wbuf.set( + buffer.subarray(wpos, wpos + pending_samples), + tot_sent - pending_samples + ); } port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) }); wpos += pending_samples; @@ -319,7 +1829,10 @@ const GodotAudioWorklet = { } if (event.data['cmd'] === 'read') { const read = event.data['data']; - GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port); + GodotAudioWorklet.ring_buffer.consumed( + read, + GodotAudioWorklet.worklet.port + ); } else if (event.data['cmd'] === 'input') { const buf = event.data['data']; if (buf.length > p_in_size) { @@ -376,7 +1889,13 @@ const GodotAudioWorklet = { godot_audio_worklet_start__proxy: 'sync', godot_audio_worklet_start__sig: 'viiiii', - godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { + godot_audio_worklet_start: function ( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + p_state + ) { const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); const state = GodotRuntime.heapSub(HEAP32, p_state, 4); @@ -385,14 +1904,33 @@ const GodotAudioWorklet = { godot_audio_worklet_start_no_threads__proxy: 'sync', godot_audio_worklet_start_no_threads__sig: 'viiiiii', - godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) { + godot_audio_worklet_start_no_threads: function ( + p_out_buf, + p_out_size, + p_out_callback, + p_in_buf, + p_in_size, + p_in_callback + ) { const out_callback = GodotRuntime.get_func(p_out_callback); const in_callback = GodotRuntime.get_func(p_in_callback); - GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback); + GodotAudioWorklet.start_no_threads( + p_out_buf, + p_out_size, + out_callback, + p_in_buf, + p_in_size, + in_callback + ); }, godot_audio_worklet_state_wait__sig: 'iiii', - godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { + godot_audio_worklet_state_wait: function ( + p_state, + p_idx, + p_expected, + p_timeout + ) { Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); return Atomics.load(HEAP32, (p_state >> 2) + p_idx); }, @@ -412,7 +1950,7 @@ autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet'); mergeInto(LibraryManager.library, GodotAudioWorklet); /* - * The deprecated ScriptProcessorNode API, used when threads are disabled. + * The ScriptProcessorNode API, used when threads are disabled. */ const GodotAudioScript = { $GodotAudioScript__deps: ['$GodotAudio'], @@ -420,7 +1958,11 @@ const GodotAudioScript = { script: null, create: function (buffer_length, channel_count) { - GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor( + buffer_length, + 2, + channel_count + ); GodotAudio.driver = GodotAudioScript; return GodotAudioScript.script.bufferSize; }, @@ -488,9 +2030,21 @@ const GodotAudioScript = { godot_audio_script_start__proxy: 'sync', godot_audio_script_start__sig: 'viiiii', - godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { + godot_audio_script_start: function ( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + p_cb + ) { const onprocess = GodotRuntime.get_func(p_cb); - GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); + GodotAudioScript.start( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + onprocess + ); }, }; diff --git a/scene/2d/audio_stream_player_2d.cpp b/scene/2d/audio_stream_player_2d.cpp index f88db0e3f43f..8e91dce425d2 100644 --- a/scene/2d/audio_stream_player_2d.cpp +++ b/scene/2d/audio_stream_player_2d.cpp @@ -128,6 +128,8 @@ void AudioStreamPlayer2D::_update_panning() { volume_vector.write[2] = AudioFrame(0, 0); volume_vector.write[3] = AudioFrame(0, 0); + StringName actual_bus = _get_actual_bus(); + for (Viewport *vp : viewports) { if (!vp->is_audio_listener_2d()) { continue; @@ -172,15 +174,20 @@ void AudioStreamPlayer2D::_update_panning() { const AudioFrame &prev_sample = volume_vector[0]; AudioFrame new_sample = AudioFrame(l, r) * multiplier; + volume_vector.write[0] = AudioFrame(MAX(prev_sample[0], new_sample[0]), MAX(prev_sample[1], new_sample[1])); } for (const Ref &playback : internal->stream_playbacks) { - AudioServer::get_singleton()->set_playback_bus_exclusive(playback, _get_actual_bus(), volume_vector); + AudioServer::get_singleton()->set_playback_bus_exclusive(playback, actual_bus, volume_vector); } - for (Ref &playback : internal->stream_playbacks) { + for (const Ref &playback : internal->stream_playbacks) { AudioServer::get_singleton()->set_playback_pitch_scale(playback, internal->pitch_scale); + if (playback->get_is_sample() && playback->get_sample_playback().is_valid()) { + Ref sample_playback = playback->get_sample_playback(); + AudioServer::get_singleton()->update_sample_playback_pitch_scale(sample_playback, internal->pitch_scale); + } } last_mix_count = AudioServer::get_singleton()->get_mix_count(); @@ -218,6 +225,15 @@ void AudioStreamPlayer2D::play(float p_from_pos) { } setplayback = stream_playback; setplay.set(p_from_pos); + + // Sample handling. + if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_valid()) { + Ref sample_playback = stream_playback->get_sample_playback(); + sample_playback->offset = p_from_pos; + sample_playback->bus = _get_actual_bus(); + + AudioServer::get_singleton()->start_sample_playback(sample_playback); + } } void AudioStreamPlayer2D::seek(float p_seconds) { @@ -326,6 +342,14 @@ float AudioStreamPlayer2D::get_panning_strength() const { return panning_strength; } +AudioServer::PlaybackType AudioStreamPlayer2D::get_playback_type() const { + return internal->get_playback_type(); +} + +void AudioStreamPlayer2D::set_playback_type(AudioServer::PlaybackType p_playback_type) { + internal->set_playback_type(p_playback_type); +} + bool AudioStreamPlayer2D::_set(const StringName &p_name, const Variant &p_value) { return internal->set(p_name, p_value); } @@ -385,6 +409,9 @@ void AudioStreamPlayer2D::_bind_methods() { ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer2D::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer2D::get_stream_playback); + ClassDB::bind_method(D_METHOD("set_playback_type", "playback_type"), &AudioStreamPlayer2D::set_playback_type); + ClassDB::bind_method(D_METHOD("get_playback_type"), &AudioStreamPlayer2D::get_playback_type); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,24,suffix:dB"), "set_volume_db", "get_volume_db"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_scale", PROPERTY_HINT_RANGE, "0.01,4,0.01,or_greater"), "set_pitch_scale", "get_pitch_scale"); @@ -397,6 +424,7 @@ void AudioStreamPlayer2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "panning_strength", PROPERTY_HINT_RANGE, "0,3,0.01,or_greater"), "set_panning_strength", "get_panning_strength"); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bus", PROPERTY_HINT_ENUM, ""), "set_bus", "get_bus"); ADD_PROPERTY(PropertyInfo(Variant::INT, "area_mask", PROPERTY_HINT_LAYERS_2D_PHYSICS), "set_area_mask", "get_area_mask"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "playback_type", PROPERTY_HINT_ENUM, "Default,Stream,Sample"), "set_playback_type", "get_playback_type"); ADD_SIGNAL(MethodInfo("finished")); } diff --git a/scene/2d/audio_stream_player_2d.h b/scene/2d/audio_stream_player_2d.h index 3a3e6510c811..309e206fe4ec 100644 --- a/scene/2d/audio_stream_player_2d.h +++ b/scene/2d/audio_stream_player_2d.h @@ -32,6 +32,7 @@ #define AUDIO_STREAM_PLAYER_2D_H #include "scene/2d/node_2d.h" +#include "servers/audio_server.h" struct AudioFrame; class AudioStream; @@ -64,6 +65,8 @@ class AudioStreamPlayer2D : public Node2D { uint64_t last_mix_count = -1; bool force_update_panning = false; + AudioServer::PlaybackType playback_type = AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT; + void _set_playing(bool p_enable); bool _is_active() const; @@ -137,6 +140,9 @@ class AudioStreamPlayer2D : public Node2D { bool has_stream_playback(); Ref get_stream_playback(); + AudioServer::PlaybackType get_playback_type() const; + void set_playback_type(AudioServer::PlaybackType p_playback_type); + AudioStreamPlayer2D(); ~AudioStreamPlayer2D(); }; diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp index 0cef56dbf249..6888462876e7 100644 --- a/scene/3d/audio_stream_player_3d.cpp +++ b/scene/3d/audio_stream_player_3d.cpp @@ -482,6 +482,12 @@ Vector AudioStreamPlayer3D::_update_panning() { } for (Ref &playback : internal->stream_playbacks) { AudioServer::get_singleton()->set_playback_pitch_scale(playback, actual_pitch_scale); + if (playback->get_is_sample()) { + Ref sample_playback = playback->get_sample_playback(); + if (sample_playback.is_valid()) { + AudioServer::get_singleton()->update_sample_playback_pitch_scale(sample_playback, actual_pitch_scale); + } + } } } return output_volume_vector; @@ -536,6 +542,15 @@ void AudioStreamPlayer3D::play(float p_from_pos) { } setplayback = stream_playback; setplay.set(p_from_pos); + + // Sample handling. + if (stream_playback->get_is_sample()) { + Ref sample_playback = stream_playback->get_sample_playback(); + sample_playback->offset = p_from_pos; + sample_playback->bus = _get_actual_bus(); + + AudioServer::get_singleton()->start_sample_playback(sample_playback); + } } void AudioStreamPlayer3D::seek(float p_seconds) { @@ -715,6 +730,14 @@ float AudioStreamPlayer3D::get_panning_strength() const { return panning_strength; } +AudioServer::PlaybackType AudioStreamPlayer3D::get_playback_type() const { + return internal->get_playback_type(); +} + +void AudioStreamPlayer3D::set_playback_type(AudioServer::PlaybackType p_playback_type) { + internal->set_playback_type(p_playback_type); +} + bool AudioStreamPlayer3D::_set(const StringName &p_name, const Variant &p_value) { return internal->set(p_name, p_value); } @@ -798,6 +821,9 @@ void AudioStreamPlayer3D::_bind_methods() { ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer3D::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer3D::get_stream_playback); + ClassDB::bind_method(D_METHOD("set_playback_type", "playback_type"), &AudioStreamPlayer3D::set_playback_type); + ClassDB::bind_method(D_METHOD("get_playback_type"), &AudioStreamPlayer3D::get_playback_type); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::INT, "attenuation_model", PROPERTY_HINT_ENUM, "Inverse,Inverse Square,Logarithmic,Disabled"), "set_attenuation_model", "get_attenuation_model"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,80,suffix:dB"), "set_volume_db", "get_volume_db"); @@ -812,6 +838,7 @@ void AudioStreamPlayer3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "panning_strength", PROPERTY_HINT_RANGE, "0,3,0.01,or_greater"), "set_panning_strength", "get_panning_strength"); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bus", PROPERTY_HINT_ENUM, ""), "set_bus", "get_bus"); ADD_PROPERTY(PropertyInfo(Variant::INT, "area_mask", PROPERTY_HINT_LAYERS_2D_PHYSICS), "set_area_mask", "get_area_mask"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "playback_type", PROPERTY_HINT_ENUM, "Default,Stream,Sample"), "set_playback_type", "get_playback_type"); ADD_GROUP("Emission Angle", "emission_angle"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emission_angle_enabled"), "set_emission_angle_enabled", "is_emission_angle_enabled"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_angle_degrees", PROPERTY_HINT_RANGE, "0.1,90,0.1,degrees"), "set_emission_angle", "get_emission_angle"); diff --git a/scene/3d/audio_stream_player_3d.h b/scene/3d/audio_stream_player_3d.h index 61005dc2496e..72356faad7fc 100644 --- a/scene/3d/audio_stream_player_3d.h +++ b/scene/3d/audio_stream_player_3d.h @@ -32,6 +32,7 @@ #define AUDIO_STREAM_PLAYER_3D_H #include "scene/3d/node_3d.h" +#include "servers/audio_server.h" class Area3D; struct AudioFrame; @@ -93,6 +94,8 @@ class AudioStreamPlayer3D : public Node3D { uint32_t area_mask = 1; + AudioServer::PlaybackType playback_type = AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT; + bool emission_angle_enabled = false; float emission_angle = 45.0; float emission_angle_filter_attenuation_db = -12.0; @@ -193,6 +196,9 @@ class AudioStreamPlayer3D : public Node3D { bool has_stream_playback(); Ref get_stream_playback(); + AudioServer::PlaybackType get_playback_type() const; + void set_playback_type(AudioServer::PlaybackType p_playback_type); + AudioStreamPlayer3D(); ~AudioStreamPlayer3D(); }; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index afd292e5528f..1c4a5ff20ea3 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -33,11 +33,15 @@ #include "core/config/engine.h" #include "core/config/project_settings.h" +#include "scene/2d/audio_stream_player_2d.h" #include "scene/animation/animation_player.h" +#include "scene/audio/audio_stream_player.h" #include "scene/resources/animation.h" #include "servers/audio/audio_stream.h" +#include "servers/audio_server.h" #ifndef _3D_DISABLED +#include "scene/3d/audio_stream_player_3d.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/3d/node_3d.h" #include "scene/3d/skeleton_3d.h" @@ -602,8 +606,8 @@ bool AnimationMixer::_update_caches() { root_motion_cache.rot = Quaternion(0, 0, 0, 1); root_motion_cache.scale = Vector3(1, 1, 1); - List sname; - get_animation_list(&sname); + List sname_list; + get_animation_list(&sname_list); bool check_path = GLOBAL_GET("animation/warnings/check_invalid_track_paths"); bool check_angle_interpolation = GLOBAL_GET("animation/warnings/check_angle_interpolation_type_conflicting"); @@ -632,7 +636,7 @@ bool AnimationMixer::_update_caches() { if (has_reset_anim) { reset_anim = get_animation(SceneStringName(RESET)); } - for (const StringName &E : sname) { + for (const StringName &E : sname_list) { Ref anim = get_animation(E); for (int i = 0; i < anim->get_track_count(); i++) { NodePath path = anim->track_get_path(i); @@ -833,6 +837,8 @@ bool AnimationMixer::_update_caches() { track_audio->object_id = child->get_instance_id(); track_audio->audio_stream.instantiate(); track_audio->audio_stream->set_polyphony(audio_max_polyphony); + track_audio->playback_type = (AudioServer::PlaybackType)(int)(child->call(SNAME("get_playback_type"))); + track_audio->bus = (StringName)(child->call(SNAME("get_bus"))); track = track_audio; @@ -1585,6 +1591,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (idx < 0) { continue; } + // Play stream. Ref stream = a->audio_track_get_key_stream(i, idx); if (stream.is_valid()) { @@ -1594,6 +1601,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (seeked) { start_ofs += time - a->track_get_key_time(i, idx); } + if (t_obj->call(SNAME("get_stream")) != t->audio_stream) { t_obj->call(SNAME("set_stream"), t->audio_stream); t->audio_stream_playback.unref(); @@ -1611,8 +1619,18 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (t->audio_stream_playback.is_null()) { t->audio_stream_playback = t_obj->call(SNAME("get_stream_playback")); } + + if (t_obj->call(SNAME("get_is_sample"))) { + Ref sample_playback; + sample_playback.instantiate(); + sample_playback->stream = stream; + t->audio_stream_playback->set_sample_playback(sample_playback); + AudioServer::get_singleton()->start_sample_playback(sample_playback); + continue; + } + PlayingAudioStreamInfo pasi; - pasi.index = t->audio_stream_playback->play_stream(stream, start_ofs); + pasi.index = t->audio_stream_playback->play_stream(stream, start_ofs, 0, 1.0, t->playback_type, t->bus); pasi.start = time; if (len && end_ofs > 0) { // Force an end at a time. pasi.len = len - start_ofs - end_ofs; @@ -2255,6 +2273,7 @@ void AnimationMixer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_callback_mode_discrete", "mode"), &AnimationMixer::set_callback_mode_discrete); ClassDB::bind_method(D_METHOD("get_callback_mode_discrete"), &AnimationMixer::get_callback_mode_discrete); + /* ---- Audio ---- */ ClassDB::bind_method(D_METHOD("set_audio_max_polyphony", "max_polyphony"), &AnimationMixer::set_audio_max_polyphony); ClassDB::bind_method(D_METHOD("get_audio_max_polyphony"), &AnimationMixer::get_audio_max_polyphony); diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h index 089a21019335..c029c68ae1c7 100644 --- a/scene/animation/animation_mixer.h +++ b/scene/animation/animation_mixer.h @@ -279,12 +279,15 @@ class AnimationMixer : public Node { Ref audio_stream; Ref audio_stream_playback; HashMap playing_streams; // Key is Animation resource ObjectID. + AudioServer::PlaybackType playback_type; + StringName bus; TrackCacheAudio(const TrackCacheAudio &p_other) : TrackCache(p_other), audio_stream(p_other.audio_stream), audio_stream_playback(p_other.audio_stream_playback), - playing_streams(p_other.playing_streams) {} + playing_streams(p_other.playing_streams), + playback_type(p_other.playback_type) {} TrackCacheAudio() { type = Animation::TYPE_AUDIO; @@ -313,6 +316,9 @@ class AnimationMixer : public Node { void _init_root_motion_cache(); bool _update_caches(); + /* ---- Audio ---- */ + AudioServer::PlaybackType playback_type; + /* ---- Blending processor ---- */ LocalVector animation_instances; HashMap track_map; @@ -425,6 +431,7 @@ class AnimationMixer : public Node { void set_callback_mode_discrete(AnimationCallbackModeDiscrete p_mode); AnimationCallbackModeDiscrete get_callback_mode_discrete() const; + /* ---- Audio ---- */ void set_audio_max_polyphony(int p_audio_max_polyphony); int get_audio_max_polyphony() const; diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp index 0c2bd64e845f..e90c1aa24516 100644 --- a/scene/audio/audio_stream_player.cpp +++ b/scene/audio/audio_stream_player.cpp @@ -95,6 +95,16 @@ void AudioStreamPlayer::play(float p_from_pos) { } AudioServer::get_singleton()->start_playback_stream(stream_playback, internal->bus, _get_volume_vector(), p_from_pos, internal->pitch_scale); internal->ensure_playback_limit(); + + // Sample handling. + if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_valid()) { + Ref sample_playback = stream_playback->get_sample_playback(); + sample_playback->offset = p_from_pos; + sample_playback->volume_vector = _get_volume_vector(); + sample_playback->bus = get_bus(); + + AudioServer::get_singleton()->start_sample_playback(sample_playback); + } } void AudioStreamPlayer::seek(float p_seconds) { @@ -205,6 +215,14 @@ Ref AudioStreamPlayer::get_stream_playback() { return internal->get_stream_playback(); } +AudioServer::PlaybackType AudioStreamPlayer::get_playback_type() const { + return internal->get_playback_type(); +} + +void AudioStreamPlayer::set_playback_type(AudioServer::PlaybackType p_playback_type) { + internal->set_playback_type(p_playback_type); +} + void AudioStreamPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_stream", "stream"), &AudioStreamPlayer::set_stream); ClassDB::bind_method(D_METHOD("get_stream"), &AudioStreamPlayer::get_stream); @@ -243,6 +261,9 @@ void AudioStreamPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer::get_stream_playback); + ClassDB::bind_method(D_METHOD("set_playback_type", "playback_type"), &AudioStreamPlayer::set_playback_type); + ClassDB::bind_method(D_METHOD("get_playback_type"), &AudioStreamPlayer::get_playback_type); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,24,suffix:dB"), "set_volume_db", "get_volume_db"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_scale", PROPERTY_HINT_RANGE, "0.01,4,0.01,or_greater"), "set_pitch_scale", "get_pitch_scale"); @@ -252,6 +273,7 @@ void AudioStreamPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "mix_target", PROPERTY_HINT_ENUM, "Stereo,Surround,Center"), "set_mix_target", "get_mix_target"); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_polyphony", PROPERTY_HINT_NONE, ""), "set_max_polyphony", "get_max_polyphony"); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bus", PROPERTY_HINT_ENUM, ""), "set_bus", "get_bus"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "playback_type", PROPERTY_HINT_ENUM, "Default,Stream,Sample"), "set_playback_type", "get_playback_type"); ADD_SIGNAL(MethodInfo("finished")); diff --git a/scene/audio/audio_stream_player.h b/scene/audio/audio_stream_player.h index ab3266f247ed..8eaf7f9c73aa 100644 --- a/scene/audio/audio_stream_player.h +++ b/scene/audio/audio_stream_player.h @@ -32,6 +32,7 @@ #define AUDIO_STREAM_PLAYER_H #include "scene/main/node.h" +#include "servers/audio_server.h" struct AudioFrame; class AudioStream; @@ -106,6 +107,9 @@ class AudioStreamPlayer : public Node { bool has_stream_playback(); Ref get_stream_playback(); + AudioServer::PlaybackType get_playback_type() const; + void set_playback_type(AudioServer::PlaybackType p_playback_type); + AudioStreamPlayer(); ~AudioStreamPlayer(); }; diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp index a7b8faaaae2b..853638e47fbc 100644 --- a/scene/audio/audio_stream_player_internal.cpp +++ b/scene/audio/audio_stream_player_internal.cpp @@ -141,6 +141,24 @@ Ref AudioStreamPlayerInternal::play_basic() { stream_playback->set_parameter(K.value.path, K.value.value); } + // Sample handling. + if (_is_sample()) { + if (stream->can_be_sampled()) { + stream_playback->set_is_sample(true); + if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_null()) { + if (!AudioServer::get_singleton()->is_stream_registered_as_sample(stream)) { + AudioServer::get_singleton()->register_stream_as_sample(stream); + } + Ref sample_playback; + sample_playback.instantiate(); + sample_playback->stream = stream; + stream_playback->set_sample_playback(sample_playback); + } + } else if (!stream->is_meta_stream()) { + WARN_PRINT(vformat(R"(%s is trying to play a sample from a stream that cannot be sampled.)", node->get_path())); + } + } + stream_playbacks.push_back(stream_playback); active.set(); _set_process(true); @@ -151,6 +169,9 @@ void AudioStreamPlayerInternal::set_stream_paused(bool p_pause) { // TODO this does not have perfect recall, fix that maybe? If there are zero playbacks registered with the AudioServer, this bool isn't persisted. for (Ref &playback : stream_playbacks) { AudioServer::get_singleton()->set_playback_paused(playback, p_pause); + if (_is_sample() && playback->get_sample_playback().is_valid()) { + AudioServer::get_singleton()->set_sample_playback_pause(playback->get_sample_playback(), p_pause); + } } } @@ -240,8 +261,12 @@ void AudioStreamPlayerInternal::seek(float p_seconds) { void AudioStreamPlayerInternal::stop() { for (Ref &playback : stream_playbacks) { AudioServer::get_singleton()->stop_playback_stream(playback); + if (_is_sample() && playback->get_sample_playback().is_valid()) { + AudioServer::get_singleton()->stop_sample_playback(playback->get_sample_playback()); + } } stream_playbacks.clear(); + active.clear(); _set_process(false); } @@ -251,6 +276,9 @@ bool AudioStreamPlayerInternal::is_playing() const { if (AudioServer::get_singleton()->is_playback_active(playback)) { return true; } + if (AudioServer::get_singleton()->is_sample_playback_active(playback)) { + return true; + } } return false; } @@ -299,6 +327,14 @@ Ref AudioStreamPlayerInternal::get_stream_playback() { return stream_playbacks[stream_playbacks.size() - 1]; } +void AudioStreamPlayerInternal::set_playback_type(AudioServer::PlaybackType p_playback_type) { + playback_type = p_playback_type; +} + +AudioServer::PlaybackType AudioStreamPlayerInternal::get_playback_type() const { + return playback_type; +} + StringName AudioStreamPlayerInternal::get_bus() const { const String bus_name = bus; for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) { diff --git a/scene/audio/audio_stream_player_internal.h b/scene/audio/audio_stream_player_internal.h index 366275244129..ec4489067e0f 100644 --- a/scene/audio/audio_stream_player_internal.h +++ b/scene/audio/audio_stream_player_internal.h @@ -33,14 +33,17 @@ #include "core/object/ref_counted.h" #include "core/templates/safe_refcount.h" +#include "servers/audio_server.h" class AudioStream; class AudioStreamPlayback; +class AudioSamplePlayback; class Node; class AudioStreamPlayerInternal : public Object { GDCLASS(AudioStreamPlayerInternal, Object); +private: struct ParameterData { StringName path; Variant value; @@ -51,12 +54,17 @@ class AudioStreamPlayerInternal : public Object { Node *node = nullptr; Callable play_callable; bool physical = false; + AudioServer::PlaybackType playback_type = AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT; HashMap playback_parameters; void _set_process(bool p_enabled); void _update_stream_parameters(); + _FORCE_INLINE_ bool _is_sample() { + return (AudioServer::get_singleton()->get_default_playback_type() == AudioServer::PlaybackType::PLAYBACK_TYPE_SAMPLE && get_playback_type() == AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT) || get_playback_type() == AudioServer::PlaybackType::PLAYBACK_TYPE_SAMPLE; + } + public: Vector> stream_playbacks; Ref stream; @@ -99,6 +107,9 @@ class AudioStreamPlayerInternal : public Object { bool has_stream_playback(); Ref get_stream_playback(); + void set_playback_type(AudioServer::PlaybackType p_playback_type); + AudioServer::PlaybackType get_playback_type() const; + AudioStreamPlayerInternal(Node *p_node, const Callable &p_play_callable, bool p_physical); }; diff --git a/scene/resources/audio_stream_polyphonic.compat.inc b/scene/resources/audio_stream_polyphonic.compat.inc new file mode 100644 index 000000000000..31074218e1b2 --- /dev/null +++ b/scene/resources/audio_stream_polyphonic.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* audio_stream_polyphonic.compat.inc */ +/**************************************************************************/ +/* 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 DISABLE_DEPRECATED + +AudioStreamPlaybackPolyphonic::ID AudioStreamPlaybackPolyphonic::_play_stream_bind_compat_91382(const Ref &p_stream, float p_from_offset, float p_volume_db, float p_pitch_scale) { + return play_stream(p_stream, p_from_offset, p_volume_db, p_pitch_scale, AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT, SceneStringName(Master)); +} + +void AudioStreamPlaybackPolyphonic::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("play_stream", "stream", "from_offset", "volume_db", "pitch_scale"), &AudioStreamPlaybackPolyphonic::_play_stream_bind_compat_91382, DEFVAL(0), DEFVAL(0), DEFVAL(1.0)); +} + +#endif diff --git a/scene/resources/audio_stream_polyphonic.cpp b/scene/resources/audio_stream_polyphonic.cpp index f7299b0789f4..45546d8dc7cd 100644 --- a/scene/resources/audio_stream_polyphonic.cpp +++ b/scene/resources/audio_stream_polyphonic.cpp @@ -29,7 +29,10 @@ /**************************************************************************/ #include "audio_stream_polyphonic.h" +#include "audio_stream_polyphonic.compat.inc" + #include "scene/main/scene_tree.h" +#include "servers/audio_server.h" Ref AudioStreamPolyphonic::instantiate_playback() { Ref playback; @@ -136,6 +139,10 @@ int AudioStreamPlaybackPolyphonic::mix(AudioFrame *p_buffer, float p_rate_scale, continue; } + if (s.stream_playback->get_is_sample()) { + continue; + } + float volume_db = s.volume_db; // Copy because it can be overridden at any time. float next_volume = Math::db_to_linear(volume_db); s.prev_volume_db = volume_db; @@ -195,8 +202,13 @@ int AudioStreamPlaybackPolyphonic::mix(AudioFrame *p_buffer, float p_rate_scale, return p_frames; } -AudioStreamPlaybackPolyphonic::ID AudioStreamPlaybackPolyphonic::play_stream(const Ref &p_stream, float p_from_offset, float p_volume_db, float p_pitch_scale) { +AudioStreamPlaybackPolyphonic::ID AudioStreamPlaybackPolyphonic::play_stream(const Ref &p_stream, float p_from_offset, float p_volume_db, float p_pitch_scale, AudioServer::PlaybackType p_playback_type, const StringName &p_bus) { ERR_FAIL_COND_V(p_stream.is_null(), INVALID_ID); + + AudioServer::PlaybackType playback_type = p_playback_type == AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT + ? AudioServer::get_singleton()->get_default_playback_type() + : p_playback_type; + for (uint32_t i = 0; i < streams.size(); i++) { if (!streams[i].active.is_set()) { // Can use this stream, as it's not active. @@ -210,6 +222,28 @@ AudioStreamPlaybackPolyphonic::ID AudioStreamPlaybackPolyphonic::play_stream(con streams[i].finish_request.clear(); streams[i].pending_play.set(); streams[i].active.set(); + + // Sample playback. + if (playback_type == AudioServer::PlaybackType::PLAYBACK_TYPE_SAMPLE && p_stream->can_be_sampled()) { + streams[i].stream_playback->set_is_sample(true); + if (!AudioServer::get_singleton()->is_stream_registered_as_sample(p_stream)) { + AudioServer::get_singleton()->register_stream_as_sample(p_stream); + } + float linear_volume = Math::db_to_linear(p_volume_db); + Ref sp; + sp.instantiate(); + sp->stream = streams[i].stream; + sp->offset = p_from_offset; + sp->volume_vector.resize(4); + sp->volume_vector.write[0] = AudioFrame(linear_volume, linear_volume); + sp->volume_vector.write[1] = AudioFrame(linear_volume, /* LFE= */ 1.0f); + sp->volume_vector.write[2] = AudioFrame(linear_volume, linear_volume); + sp->volume_vector.write[3] = AudioFrame(linear_volume, linear_volume); + sp->bus = p_bus; + streams[i].stream_playback->set_sample_playback(sp); + AudioServer::get_singleton()->start_sample_playback(sp); + } + return (ID(i) << INDEX_SHIFT) | ID(streams[i].id); } } @@ -260,8 +294,24 @@ void AudioStreamPlaybackPolyphonic::stop_stream(ID p_stream_id) { s->finish_request.set(); } +void AudioStreamPlaybackPolyphonic::set_is_sample(bool p_is_sample) { + _is_sample = p_is_sample; +} + +bool AudioStreamPlaybackPolyphonic::get_is_sample() const { + return _is_sample; +} + +Ref AudioStreamPlaybackPolyphonic::get_sample_playback() const { + return sample_playback; +} + +void AudioStreamPlaybackPolyphonic::set_sample_playback(const Ref &p_playback) { + sample_playback = p_playback; +} + void AudioStreamPlaybackPolyphonic::_bind_methods() { - ClassDB::bind_method(D_METHOD("play_stream", "stream", "from_offset", "volume_db", "pitch_scale"), &AudioStreamPlaybackPolyphonic::play_stream, DEFVAL(0), DEFVAL(0), DEFVAL(1.0)); + ClassDB::bind_method(D_METHOD("play_stream", "stream", "from_offset", "volume_db", "pitch_scale", "playback_type", "bus"), &AudioStreamPlaybackPolyphonic::play_stream, DEFVAL(0), DEFVAL(0), DEFVAL(1.0), DEFVAL(0), DEFVAL(SceneStringName(Master))); ClassDB::bind_method(D_METHOD("set_stream_volume", "stream", "volume_db"), &AudioStreamPlaybackPolyphonic::set_stream_volume); ClassDB::bind_method(D_METHOD("set_stream_pitch_scale", "stream", "pitch_scale"), &AudioStreamPlaybackPolyphonic::set_stream_pitch_scale); ClassDB::bind_method(D_METHOD("is_stream_playing", "stream"), &AudioStreamPlaybackPolyphonic::is_stream_playing); diff --git a/scene/resources/audio_stream_polyphonic.h b/scene/resources/audio_stream_polyphonic.h index e414401b6fae..01d0176c44ff 100644 --- a/scene/resources/audio_stream_polyphonic.h +++ b/scene/resources/audio_stream_polyphonic.h @@ -32,12 +32,16 @@ #define AUDIO_STREAM_POLYPHONIC_H #include "core/templates/local_vector.h" +#include "scene/scene_string_names.h" #include "servers/audio/audio_stream.h" +#include "servers/audio_server.h" class AudioStreamPolyphonic : public AudioStream { GDCLASS(AudioStreamPolyphonic, AudioStream) int polyphony = 32; + AudioServer::PlaybackType playback_type; + static void _bind_methods(); public: @@ -48,6 +52,8 @@ class AudioStreamPolyphonic : public AudioStream { void set_polyphony(int p_voices); int get_polyphony() const; + virtual bool is_meta_stream() const override { return true; } + AudioStreamPolyphonic(); }; @@ -81,6 +87,9 @@ class AudioStreamPlaybackPolyphonic : public AudioStreamPlayback { bool active = false; uint32_t id_counter = 1; + bool _is_sample = false; + Ref sample_playback; + _FORCE_INLINE_ Stream *_find_stream(int64_t p_id); friend class AudioStreamPolyphonic; @@ -107,12 +116,24 @@ class AudioStreamPlaybackPolyphonic : public AudioStreamPlayback { virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override; - ID play_stream(const Ref &p_stream, float p_from_offset = 0, float p_volume_db = 0, float p_pitch_scale = 1.0); + ID play_stream(const Ref &p_stream, float p_from_offset = 0, float p_volume_db = 0, float p_pitch_scale = 1.0, AudioServer::PlaybackType p_playback_type = AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT, const StringName &p_bus = SceneStringName(Master)); void set_stream_volume(ID p_stream_id, float p_volume_db); void set_stream_pitch_scale(ID p_stream_id, float p_pitch_scale); bool is_stream_playing(ID p_stream_id) const; void stop_stream(ID p_stream_id); + virtual void set_is_sample(bool p_is_sample) override; + virtual bool get_is_sample() const override; + virtual Ref get_sample_playback() const override; + virtual void set_sample_playback(const Ref &p_playback) override; + +private: +#ifndef DISABLE_DEPRECATED + ID _play_stream_bind_compat_91382(const Ref &p_stream, float p_from_offset = 0, float p_volume_db = 0, float p_pitch_scale = 1.0); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + +public: AudioStreamPlaybackPolyphonic(); }; diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index db2564af229f..e2ac0e6d2691 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -465,6 +465,22 @@ void AudioStreamPlaybackWAV::tag_used_streams() { base->tag_used(get_playback_position()); } +void AudioStreamPlaybackWAV::set_is_sample(bool p_is_sample) { + _is_sample = p_is_sample; +} + +bool AudioStreamPlaybackWAV::get_is_sample() const { + return _is_sample; +} + +Ref AudioStreamPlaybackWAV::get_sample_playback() const { + return sample_playback; +} + +void AudioStreamPlaybackWAV::set_sample_playback(const Ref &p_playback) { + sample_playback = p_playback; +} + AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {} AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() { @@ -696,6 +712,33 @@ String AudioStreamWAV::get_stream_name() const { return ""; } +Ref AudioStreamWAV::generate_sample() const { + Ref sample; + sample.instantiate(); + sample->stream = this; + switch (loop_mode) { + case AudioStreamWAV::LoopMode::LOOP_DISABLED: { + sample->loop_mode = AudioSample::LoopMode::LOOP_DISABLED; + } break; + + case AudioStreamWAV::LoopMode::LOOP_FORWARD: { + sample->loop_mode = AudioSample::LoopMode::LOOP_FORWARD; + } break; + + case AudioStreamWAV::LoopMode::LOOP_PINGPONG: { + sample->loop_mode = AudioSample::LoopMode::LOOP_PINGPONG; + } break; + + case AudioStreamWAV::LoopMode::LOOP_BACKWARD: { + sample->loop_mode = AudioSample::LoopMode::LOOP_BACKWARD; + } break; + } + sample->loop_begin = loop_begin; + sample->loop_end = loop_end; + sample->sample_rate = mix_rate; + return sample; +} + void AudioStreamWAV::_bind_methods() { ClassDB::bind_method(D_METHOD("set_data", "data"), &AudioStreamWAV::set_data); ClassDB::bind_method(D_METHOD("get_data"), &AudioStreamWAV::get_data); diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 146142d8a429..806db675b6ca 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -78,6 +78,9 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { template void do_resample(const Depth *p_src, AudioFrame *p_dst, int64_t &p_offset, int32_t &p_increment, uint32_t p_amount, IMA_ADPCM_State *p_ima_adpcm, QOA_State *p_qoa); + bool _is_sample = false; + Ref sample_playback; + public: virtual void start(double p_from_pos = 0.0) override; virtual void stop() override; @@ -92,6 +95,11 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { virtual void tag_used_streams() override; + virtual void set_is_sample(bool p_is_sample) override; + virtual bool get_is_sample() const override; + virtual Ref get_sample_playback() const override; + virtual void set_sample_playback(const Ref &p_playback) override; + AudioStreamPlaybackWAV(); ~AudioStreamPlaybackWAV(); }; @@ -166,6 +174,11 @@ class AudioStreamWAV : public AudioStream { virtual Ref instantiate_playback() override; virtual String get_stream_name() const override; + virtual bool can_be_sampled() const override { + return true; + } + virtual Ref generate_sample() const override; + AudioStreamWAV(); ~AudioStreamWAV(); }; diff --git a/servers/audio/audio_stream.cpp b/servers/audio/audio_stream.cpp index dc54e01ec8df..0dc6d160509d 100644 --- a/servers/audio/audio_stream.cpp +++ b/servers/audio/audio_stream.cpp @@ -90,6 +90,10 @@ Variant AudioStreamPlayback::get_parameter(const StringName &p_name) const { return ret; } +Ref AudioStreamPlayback::get_sample_playback() const { + return nullptr; +} + void AudioStreamPlayback::_bind_methods() { GDVIRTUAL_BIND(_start, "from_pos") GDVIRTUAL_BIND(_stop) @@ -101,6 +105,17 @@ void AudioStreamPlayback::_bind_methods() { GDVIRTUAL_BIND(_tag_used_streams); GDVIRTUAL_BIND(_set_parameter, "name", "value"); GDVIRTUAL_BIND(_get_parameter, "name"); + + ClassDB::bind_method(D_METHOD("set_sample_playback", "playback_sample"), &AudioStreamPlayback::set_sample_playback); + ClassDB::bind_method(D_METHOD("get_sample_playback"), &AudioStreamPlayback::get_sample_playback); +} + +AudioStreamPlayback::AudioStreamPlayback() {} + +AudioStreamPlayback::~AudioStreamPlayback() { + if (get_sample_playback().is_valid() && likely(AudioServer::get_singleton() != nullptr)) { + AudioServer::get_singleton()->stop_sample_playback(get_sample_playback()); + } } ////////////////////////////// @@ -271,10 +286,22 @@ void AudioStream::get_parameter_list(List *r_parameters) { } } +Ref AudioStream::generate_sample() const { + ERR_FAIL_COND_V_MSG(!can_be_sampled(), nullptr, "Cannot generate a sample for a stream that cannot be sampled."); + Ref sample; + sample.instantiate(); + sample->stream = this; + return sample; +} + void AudioStream::_bind_methods() { ClassDB::bind_method(D_METHOD("get_length"), &AudioStream::get_length); ClassDB::bind_method(D_METHOD("is_monophonic"), &AudioStream::is_monophonic); ClassDB::bind_method(D_METHOD("instantiate_playback"), &AudioStream::instantiate_playback); + ClassDB::bind_method(D_METHOD("can_be_sampled"), &AudioStream::can_be_sampled); + ClassDB::bind_method(D_METHOD("generate_sample"), &AudioStream::generate_sample); + ClassDB::bind_method(D_METHOD("is_meta_stream"), &AudioStream::is_meta_stream); + GDVIRTUAL_BIND(_instantiate_playback); GDVIRTUAL_BIND(_get_stream_name); GDVIRTUAL_BIND(_get_length); diff --git a/servers/audio/audio_stream.h b/servers/audio/audio_stream.h index aa1ad4cc3ad3..0ca4777d5c40 100644 --- a/servers/audio/audio_stream.h +++ b/servers/audio/audio_stream.h @@ -43,6 +43,39 @@ class AudioStream; +class AudioSamplePlayback : public RefCounted { + GDCLASS(AudioSamplePlayback, RefCounted); + +public: + Ref stream; + + float offset = 0.0f; + Vector volume_vector; + StringName bus; +}; + +class AudioSample : public RefCounted { + GDCLASS(AudioSample, RefCounted) + +public: + enum LoopMode { + LOOP_DISABLED, + LOOP_FORWARD, + LOOP_PINGPONG, + LOOP_BACKWARD, + }; + + Ref stream; + Vector data; + int num_channels = 1; + int sample_rate = 44100; + LoopMode loop_mode = LOOP_DISABLED; + int loop_begin = 0; + int loop_end = 0; +}; + +/////////// + class AudioStreamPlayback : public RefCounted { GDCLASS(AudioStreamPlayback, RefCounted); @@ -75,6 +108,14 @@ class AudioStreamPlayback : public RefCounted { virtual Variant get_parameter(const StringName &p_name) const; virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames); + + virtual void set_is_sample(bool p_is_sample) {} + virtual bool get_is_sample() const { return false; } + virtual Ref get_sample_playback() const; + virtual void set_sample_playback(const Ref &p_playback) {} + + AudioStreamPlayback(); + ~AudioStreamPlayback(); }; class AudioStreamPlaybackResampled : public AudioStreamPlayback { @@ -161,6 +202,11 @@ class AudioStream : public Resource { }; virtual void get_parameter_list(List *r_parameters); + + virtual bool can_be_sampled() const { return false; } + virtual Ref generate_sample() const; + + virtual bool is_meta_stream() const { return false; } }; // Microphone @@ -292,6 +338,8 @@ class AudioStreamRandomizer : public AudioStream { virtual double get_length() const override; //if supported, otherwise return 0 virtual bool is_monophonic() const override; + virtual bool is_meta_stream() const override { return true; } + AudioStreamRandomizer(); }; diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index d37836ed9609..fefb8bfd413d 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -120,16 +120,18 @@ int AudioDriver::_get_configured_mix_rate() { StringName audio_driver_setting = "audio/driver/mix_rate"; int mix_rate = GLOBAL_GET(audio_driver_setting); +#ifdef WEB_ENABLED + // `0` is an acceptable value (resorts to the browser's default). + return MAX(0, mix_rate); +#else // !WEB_ENABLED // In the case of invalid mix rate, let's default to a sensible value.. if (mix_rate <= 0) { -#ifndef WEB_ENABLED WARN_PRINT(vformat("Invalid mix rate of %d, consider reassigning setting \'%s\'. \nDefaulting mix rate to value %d.", mix_rate, audio_driver_setting, AudioDriverManager::DEFAULT_MIX_RATE)); -#endif mix_rate = AudioDriverManager::DEFAULT_MIX_RATE; } - return mix_rate; +#endif } AudioDriver::SpeakerMode AudioDriver::get_speaker_mode_by_total_channels(int p_channels) const { @@ -181,6 +183,18 @@ PackedStringArray AudioDriver::get_input_device_list() { return list; } +void AudioDriver::start_sample_playback(const Ref &p_playback) { + if (p_playback.is_valid()) { + if (p_playback->stream.is_valid()) { + WARN_PRINT_ED(vformat(R"(Trying to play stream (%s) as a sample (%s), but the driver doesn't support sample playback.)", p_playback->get_instance_id(), p_playback->stream->get_instance_id())); + } else { + WARN_PRINT_ED(vformat(R"(Trying to play stream (%s) as a null sample, but the driver doesn't support sample playback.)", p_playback->get_instance_id())); + } + } else { + WARN_PRINT_ED("Trying to play a null sample playback from a driver that don't support sample playback."); + } +} + AudioDriverDummy AudioDriverManager::dummy_driver; AudioDriver *AudioDriverManager::drivers[MAX_DRIVERS] = { &AudioDriverManager::dummy_driver, @@ -367,6 +381,10 @@ void AudioServer::_mix_step() { continue; } + if (playback->stream_playback->get_is_sample()) { + continue; + } + bool fading_out = playback->state.load() == AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION || playback->state.load() == AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE; AudioFrame *buf = mix_buffer.ptrw(); @@ -770,6 +788,8 @@ void AudioServer::set_bus_count(int p_count) { unlock(); + AudioDriver::get_singleton()->set_sample_bus_count(p_count); + emit_signal(SNAME("bus_layout_changed")); } @@ -785,6 +805,8 @@ void AudioServer::remove_bus(int p_index) { buses.remove_at(p_index); unlock(); + AudioDriver::get_singleton()->remove_sample_bus(p_index); + emit_signal(SNAME("bus_layout_changed")); } @@ -839,6 +861,8 @@ void AudioServer::add_bus(int p_at_pos) { buses.insert(p_at_pos, bus); } + AudioDriver::get_singleton()->add_sample_bus(p_at_pos); + emit_signal(SNAME("bus_layout_changed")); } @@ -863,6 +887,8 @@ void AudioServer::move_bus(int p_bus, int p_to_pos) { buses.insert(p_to_pos - 1, bus); } + AudioDriver::get_singleton()->move_sample_bus(p_bus, p_to_pos); + emit_signal(SNAME("bus_layout_changed")); } @@ -934,6 +960,8 @@ void AudioServer::set_bus_volume_db(int p_bus, float p_volume_db) { MARK_EDITED buses[p_bus]->volume_db = p_volume_db; + + AudioDriver::get_singleton()->set_sample_bus_volume_db(p_bus, p_volume_db); } float AudioServer::get_bus_volume_db(int p_bus) const { @@ -952,6 +980,8 @@ void AudioServer::set_bus_send(int p_bus, const StringName &p_send) { MARK_EDITED buses[p_bus]->send = p_send; + + AudioDriver::get_singleton()->set_sample_bus_send(p_bus, p_send); } StringName AudioServer::get_bus_send(int p_bus) const { @@ -965,6 +995,8 @@ void AudioServer::set_bus_solo(int p_bus, bool p_enable) { MARK_EDITED buses[p_bus]->solo = p_enable; + + AudioDriver::get_singleton()->set_sample_bus_solo(p_bus, p_enable); } bool AudioServer::is_bus_solo(int p_bus) const { @@ -979,6 +1011,8 @@ void AudioServer::set_bus_mute(int p_bus, bool p_enable) { MARK_EDITED buses[p_bus]->mute = p_enable; + + AudioDriver::get_singleton()->set_sample_bus_mute(p_bus, p_enable); } bool AudioServer::is_bus_mute(int p_bus) const { @@ -1214,6 +1248,13 @@ void AudioServer::set_playback_bus_exclusive(Ref p_playback void AudioServer::set_playback_bus_volumes_linear(Ref p_playback, const HashMap> &p_bus_volumes) { ERR_FAIL_COND(p_bus_volumes.size() > MAX_BUSES_PER_PLAYBACK); + // Samples. + if (p_playback->get_is_sample() && p_playback->get_sample_playback().is_valid()) { + Ref sample_playback = p_playback->get_sample_playback(); + AudioDriver::get_singleton()->set_sample_playback_bus_volumes_linear(sample_playback, p_bus_volumes); + return; + } + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); if (!playback_node) { return; @@ -1265,6 +1306,13 @@ void AudioServer::set_playback_all_bus_volumes_linear(Ref p void AudioServer::set_playback_pitch_scale(Ref p_playback, float p_pitch_scale) { ERR_FAIL_COND(p_playback.is_null()); + // Samples. + if (p_playback->get_is_sample() && p_playback->get_sample_playback().is_valid()) { + Ref sample_playback = p_playback->get_sample_playback(); + AudioServer::get_singleton()->update_sample_playback_pitch_scale(sample_playback, p_pitch_scale); + return; + } + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); if (!playback_node) { return; @@ -1385,6 +1433,7 @@ void AudioServer::init() { if (AudioDriver::get_singleton()) { AudioDriver::get_singleton()->start(); + AudioDriver::get_singleton()->set_sample_bus_count(1); } #ifdef TOOLS_ENABLED @@ -1597,6 +1646,9 @@ void AudioServer::set_bus_layout(const Ref &p_bus_layout) { } buses.resize(p_bus_layout->buses.size()); bus_map.clear(); + + AudioDriver::get_singleton()->set_sample_bus_count(buses.size()); + for (int i = 0; i < p_bus_layout->buses.size(); i++) { Bus *bus = memnew(Bus); if (i == 0) { @@ -1604,6 +1656,7 @@ void AudioServer::set_bus_layout(const Ref &p_bus_layout) { } else { bus->name = p_bus_layout->buses[i].name; bus->send = p_bus_layout->buses[i].send; + AudioDriver::get_singleton()->set_sample_bus_send(i, bus->send); } bus->solo = p_bus_layout->buses[i].solo; @@ -1611,6 +1664,10 @@ void AudioServer::set_bus_layout(const Ref &p_bus_layout) { bus->bypass = p_bus_layout->buses[i].bypass; bus->volume_db = p_bus_layout->buses[i].volume_db; + AudioDriver::get_singleton()->set_sample_bus_solo(i, bus->solo); + AudioDriver::get_singleton()->set_sample_bus_mute(i, bus->mute); + AudioDriver::get_singleton()->set_sample_bus_volume_db(i, bus->volume_db); + for (int j = 0; j < p_bus_layout->buses[i].effects.size(); j++) { Ref fx = p_bus_layout->buses[i].effects[j].effect; @@ -1638,6 +1695,8 @@ void AudioServer::set_bus_layout(const Ref &p_bus_layout) { set_edited(false); #endif unlock(); + + // Samples bus sync. } Ref AudioServer::generate_bus_layout() const { @@ -1705,6 +1764,82 @@ void AudioServer::get_argument_options(const StringName &p_function, int p_idx, } #endif +AudioServer::PlaybackType AudioServer::get_default_playback_type() const { + int playback_type = GLOBAL_GET("audio/general/default_playback_type"); + ERR_FAIL_COND_V_MSG( + playback_type < 0 || playback_type >= PlaybackType::PLAYBACK_TYPE_MAX, + PlaybackType::PLAYBACK_TYPE_STREAM, + vformat(R"(Project settings value (%s) for "audio/general/default_playback_type" is not supported)", playback_type)); + + switch (playback_type) { + case 1: { + return PlaybackType::PLAYBACK_TYPE_SAMPLE; + } break; + + case 0: + default: { + return PlaybackType::PLAYBACK_TYPE_STREAM; + } break; + } +} + +bool AudioServer::is_stream_registered_as_sample(const Ref &p_stream) { + ERR_FAIL_COND_V_MSG(p_stream.is_null(), false, "Parameter p_stream is null."); + return AudioDriver::get_singleton()->is_stream_registered_as_sample(p_stream); +} + +void AudioServer::register_stream_as_sample(const Ref &p_stream) { + ERR_FAIL_COND_MSG(p_stream.is_null(), "Parameter p_stream is null."); + ERR_FAIL_COND_MSG(!(p_stream->can_be_sampled()), "Parameter p_stream cannot be sampled."); + Ref sample = p_stream->generate_sample(); + register_sample(sample); +} + +void AudioServer::unregister_stream_as_sample(const Ref &p_stream) { + ERR_FAIL_COND_MSG(p_stream.is_null(), "Parameter p_stream is null."); + ERR_FAIL_COND_MSG(!(p_stream->can_be_sampled()), "Parameter p_stream cannot be sampled."); + Ref sample = p_stream->generate_sample(); + unregister_sample(sample); +} + +void AudioServer::register_sample(const Ref &p_sample) { + ERR_FAIL_COND_MSG(p_sample.is_null(), "Parameter p_sample is null."); + ERR_FAIL_COND_MSG(p_sample->stream.is_null(), "Parameter p_sample->stream is null."); + ERR_FAIL_COND_MSG(!(p_sample->stream->can_be_sampled()), "Parameter p_stream cannot be sampled."); + AudioDriver::get_singleton()->register_sample(p_sample); +} + +void AudioServer::unregister_sample(const Ref &p_sample) { + ERR_FAIL_COND_MSG(p_sample.is_null(), "Parameter p_sample is null."); + ERR_FAIL_COND_MSG(p_sample->stream.is_null(), "Parameter p_sample->stream is null."); + AudioDriver::get_singleton()->unregister_sample(p_sample); +} + +void AudioServer::start_sample_playback(const Ref &p_playback) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + AudioDriver::get_singleton()->start_sample_playback(p_playback); +} + +void AudioServer::stop_sample_playback(const Ref &p_playback) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + AudioDriver::get_singleton()->stop_sample_playback(p_playback); +} + +void AudioServer::set_sample_playback_pause(const Ref &p_playback, bool p_paused) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + AudioDriver::get_singleton()->set_sample_playback_pause(p_playback, p_paused); +} + +bool AudioServer::is_sample_playback_active(const Ref &p_playback) { + ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null."); + return AudioDriver::get_singleton()->is_sample_playback_active(p_playback); +} + +void AudioServer::update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale) { + ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); + return AudioDriver::get_singleton()->update_sample_playback_pitch_scale(p_playback, p_pitch_scale); +} + void AudioServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_bus_count", "amount"), &AudioServer::set_bus_count); ClassDB::bind_method(D_METHOD("get_bus_count"), &AudioServer::get_bus_count); @@ -1774,6 +1909,9 @@ void AudioServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_enable_tagging_used_audio_streams", "enable"), &AudioServer::set_enable_tagging_used_audio_streams); + ClassDB::bind_method(D_METHOD("is_stream_registered_as_sample", "stream"), &AudioServer::is_stream_registered_as_sample); + ClassDB::bind_method(D_METHOD("register_stream_as_sample", "stream"), &AudioServer::register_stream_as_sample); + ADD_PROPERTY(PropertyInfo(Variant::INT, "bus_count"), "set_bus_count", "get_bus_count"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "output_device"), "set_output_device", "get_output_device"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "input_device"), "set_input_device", "get_input_device"); @@ -1789,6 +1927,11 @@ void AudioServer::_bind_methods() { BIND_ENUM_CONSTANT(SPEAKER_SURROUND_31); BIND_ENUM_CONSTANT(SPEAKER_SURROUND_51); BIND_ENUM_CONSTANT(SPEAKER_SURROUND_71); + + BIND_ENUM_CONSTANT(PLAYBACK_TYPE_DEFAULT); + BIND_ENUM_CONSTANT(PLAYBACK_TYPE_STREAM); + BIND_ENUM_CONSTANT(PLAYBACK_TYPE_SAMPLE); + BIND_ENUM_CONSTANT(PLAYBACK_TYPE_MAX); } AudioServer::AudioServer() { diff --git a/servers/audio_server.h b/servers/audio_server.h index 7dd81beabe04..4825e2433643 100644 --- a/servers/audio_server.h +++ b/servers/audio_server.h @@ -42,9 +42,11 @@ #include class AudioDriverDummy; +class AudioSample; class AudioStream; class AudioStreamWAV; class AudioStreamPlayback; +class AudioSamplePlayback; class AudioDriver { static AudioDriver *singleton; @@ -129,6 +131,28 @@ class AudioDriver { void reset_profiling_time() { prof_time.set(0); } #endif + // Samples handling. + virtual bool is_stream_registered_as_sample(const Ref &p_stream) const { + return false; + } + virtual void register_sample(const Ref &p_sample) {} + virtual void unregister_sample(const Ref &p_sample) {} + virtual void start_sample_playback(const Ref &p_playback); + virtual void stop_sample_playback(const Ref &p_playback) {} + virtual void set_sample_playback_pause(const Ref &p_playback, bool p_paused) {} + virtual bool is_sample_playback_active(const Ref &p_playback) { return false; } + virtual void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f) {} + virtual void set_sample_playback_bus_volumes_linear(const Ref &p_playback, const HashMap> &p_bus_volumes) {} + + virtual void set_sample_bus_count(int p_count) {} + virtual void remove_sample_bus(int p_bus) {} + virtual void add_sample_bus(int p_at_pos = -1) {} + virtual void move_sample_bus(int p_bus, int p_to_pos) {} + virtual void set_sample_bus_send(int p_bus, const StringName &p_send) {} + virtual void set_sample_bus_volume_db(int p_bus, float p_volume_db) {} + virtual void set_sample_bus_solo(int p_bus, bool p_enable) {} + virtual void set_sample_bus_mute(int p_bus, bool p_enable) {} + AudioDriver() {} virtual ~AudioDriver() {} }; @@ -166,6 +190,13 @@ class AudioServer : public Object { SPEAKER_SURROUND_71, }; + enum PlaybackType { + PLAYBACK_TYPE_DEFAULT, + PLAYBACK_TYPE_STREAM, + PLAYBACK_TYPE_SAMPLE, + PLAYBACK_TYPE_MAX + }; + enum { AUDIO_DATA_INVALID_ID = -1, MAX_CHANNELS_PER_BUS = 4, @@ -440,11 +471,25 @@ class AudioServer : public Object { virtual void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const override; #endif + PlaybackType get_default_playback_type() const; + + bool is_stream_registered_as_sample(const Ref &p_stream); + void register_stream_as_sample(const Ref &p_stream); + void unregister_stream_as_sample(const Ref &p_stream); + void register_sample(const Ref &p_sample); + void unregister_sample(const Ref &p_sample); + void start_sample_playback(const Ref &p_playback); + void stop_sample_playback(const Ref &p_playback); + void set_sample_playback_pause(const Ref &p_playback, bool p_paused); + bool is_sample_playback_active(const Ref &p_playback); + void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f); + AudioServer(); virtual ~AudioServer(); }; VARIANT_ENUM_CAST(AudioServer::SpeakerMode) +VARIANT_ENUM_CAST(AudioServer::PlaybackType) class AudioBusLayout : public Resource { GDCLASS(AudioBusLayout, Resource); diff --git a/servers/register_server_types.cpp b/servers/register_server_types.cpp index 7a15d202a330..fc2aae10c7ab 100644 --- a/servers/register_server_types.cpp +++ b/servers/register_server_types.cpp @@ -176,6 +176,8 @@ void register_server_types() { GDREGISTER_VIRTUAL_CLASS(AudioStreamPlaybackResampled); GDREGISTER_CLASS(AudioStreamMicrophone); GDREGISTER_CLASS(AudioStreamRandomizer); + GDREGISTER_CLASS(AudioSample); + GDREGISTER_CLASS(AudioSamplePlayback); GDREGISTER_VIRTUAL_CLASS(AudioEffect); GDREGISTER_VIRTUAL_CLASS(AudioEffectInstance); GDREGISTER_CLASS(AudioEffectEQ);