From 89b20b896855716507060b75e45a12b1db8ab216 Mon Sep 17 00:00:00 2001 From: Juan Linietsky Date: Tue, 26 Dec 2023 10:43:21 +0100 Subject: [PATCH] Improve AudioStream looping * AudioStream (OGG/MP3) import dialog now allows to optionally set loop in beats. * Loop points other than zero now properly fade-in to avoid clicks. Keep in mind if you have strong transients in the loop they may be smoothed due to the fade, so consider eventually separating in two clips and use upcoming AudioStreamPlaybackPlaylist or AudioStreamPlaybackInteractive. * Stream import dialog now indicates the beat where loop occurs. * Cleaned up the import dialog a bit. --- .../import/audio_stream_import_settings.cpp | 53 ++++++++++++++++--- editor/import/audio_stream_import_settings.h | 4 ++ modules/minimp3/audio_stream_mp3.cpp | 7 ++- modules/minimp3/resource_importer_mp3.cpp | 6 +++ modules/vorbis/audio_stream_ogg_vorbis.cpp | 7 ++- .../vorbis/resource_importer_ogg_vorbis.cpp | 6 +++ 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/editor/import/audio_stream_import_settings.cpp b/editor/import/audio_stream_import_settings.cpp index 5414c6e74b14..1e64d4ed3d33 100644 --- a/editor/import/audio_stream_import_settings.cpp +++ b/editor/import/audio_stream_import_settings.cpp @@ -87,7 +87,7 @@ void AudioStreamImportSettingsDialog::_draw_preview() { Size2 rect_size = rect.size; int width = rect_size.width; - Ref preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(stream); + Ref preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(stream_no_loop); float preview_offset = zoom_bar->get_value(); float preview_len = zoom_bar->get_page(); @@ -104,6 +104,7 @@ void AudioStreamImportSettingsDialog::_draw_preview() { float inactive_from = 1e20; float beat_size = 0; int last_beat = 0; + int loop_beat = 0; if (stream->get_bpm() > 0) { beat_size = 60 / float(stream->get_bpm()); int y_ofs = beat_font->get_height(main_size) + 4 * EDSCALE; @@ -114,6 +115,10 @@ void AudioStreamImportSettingsDialog::_draw_preview() { last_beat = stream->get_beat_count(); inactive_from = last_beat * beat_size; } + + if (loop->is_pressed() && loop_offset_type->get_selected() == 1) { + loop_beat = loop_offset->get_value(); + } } for (int i = 0; i < width; i++) { @@ -136,6 +141,7 @@ void AudioStreamImportSettingsDialog::_draw_preview() { if (beat_size) { Color beat_color = Color(1, 1, 1, 1); Color final_beat_color = beat_color; + Color loop_beat_color = beat_color * Color(1, 0.5, 0.5, 1.0); Color bar_color = beat_color; beat_color.a *= 0.4; bar_color.a *= 0.6; @@ -164,6 +170,8 @@ void AudioStreamImportSettingsDialog::_draw_preview() { // Darken subsequent beats beat_color.a *= 0.3; color_active.a *= 0.3; + } else if (beat == loop_beat && loop_beat != 0) { + _preview->draw_rect(Rect2i(i, rect.position.y, 2, rect.size.height), loop_beat_color); } else { _preview->draw_rect(Rect2i(i, rect.position.y, 1, rect.size.height), (beat % bar_beats) == 0 ? bar_color : beat_color); } @@ -174,7 +182,7 @@ void AudioStreamImportSettingsDialog::_draw_preview() { } void AudioStreamImportSettingsDialog::_preview_changed(ObjectID p_which) { - if (stream.is_valid() && stream->get_instance_id() == p_which) { + if (stream_no_loop.is_valid() && stream_no_loop->get_instance_id() == p_which) { _preview->queue_redraw(); } } @@ -400,6 +408,14 @@ void AudioStreamImportSettingsDialog::_seek_to(real_t p_x) { _indicator->queue_redraw(); } +void AudioStreamImportSettingsDialog::_update_loop_offset_type() { + if (loop_offset_type->get_selected() == 0) { + loop_offset->set_step(0.001); + } else { + loop_offset->set_step(1); + } +} + void AudioStreamImportSettingsDialog::edit(const String &p_path, const String &p_importer, const Ref &p_stream) { if (!stream.is_null()) { stream->disconnect_changed(callable_mp(this, &AudioStreamImportSettingsDialog::_audio_changed)); @@ -410,6 +426,9 @@ void AudioStreamImportSettingsDialog::edit(const String &p_path, const String &p stream = p_stream; _player->set_stream(stream); + stream_no_loop = stream->duplicate(true); + stream_no_loop->call("set_loop", false); + _current = 0; String text = String::num(stream->get_length(), 2).pad_decimals(2) + "s"; _duration_label->set_text(text); @@ -437,6 +456,13 @@ void AudioStreamImportSettingsDialog::edit(const String &p_path, const String &p beats_edit->set_value(beats); beats_enabled->set_pressed(beats > 0); loop->set_pressed(config_file->get_value("params", "loop", false)); + + if (config_file->has_section_key("params", "loop_offset_type")) { + loop_offset_type->select(config_file->get_value("params", "loop_offset_type", 0)); + } else { + loop_offset_type->select(0); + } + _update_loop_offset_type(); loop_offset->set_value(config_file->get_value("params", "loop_offset", 0)); bar_beats_edit->set_value(config_file->get_value("params", "bar_beats", 4)); @@ -471,11 +497,18 @@ void AudioStreamImportSettingsDialog::_settings_changed() { updating_settings = true; stream->call("set_loop", loop->is_pressed()); - stream->call("set_loop_offset", loop_offset->get_value()); + float loop_sec = loop_offset->get_value(); + if (loop_offset_type->get_selected() == 1) { + loop_sec *= (60.0 / bpm_edit->get_value()); + } + + stream->call("set_loop_offset", loop_sec); if (loop->is_pressed()) { loop_offset->set_editable(true); + loop_offset_type->set_disabled(false); } else { loop_offset->set_editable(false); + loop_offset_type->set_disabled(true); } if (bpm_enabled->is_pressed()) { @@ -518,6 +551,7 @@ void AudioStreamImportSettingsDialog::_settings_changed() { void AudioStreamImportSettingsDialog::_reimport() { params["loop"] = loop->is_pressed(); params["loop_offset"] = loop_offset->get_value(); + params["loop_offset_type"] = loop_offset_type->get_selected(); params["bpm"] = bpm_enabled->is_pressed() ? double(bpm_edit->get_value()) : double(0); params["beat_count"] = (bpm_enabled->is_pressed() && beats_enabled->is_pressed()) ? int(beats_edit->get_value()) : int(0); params["bar_beats"] = (bpm_enabled->is_pressed()) ? int(bar_beats_edit->get_value()) : int(4); @@ -541,13 +575,19 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { loop_hb->add_child(loop); loop_hb->add_spacer(); loop_hb->add_child(memnew(Label(TTR("Offset:")))); + loop_offset = memnew(SpinBox); loop_offset->set_max(10000); loop_offset->set_step(0.001); - loop_offset->set_suffix("sec"); loop_offset->set_tooltip_text(TTR("Loop offset (from beginning). Note that if BPM is set, this setting will be ignored.")); loop_offset->connect("value_changed", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); loop_hb->add_child(loop_offset); + loop_offset_type = memnew(OptionButton); + loop_offset_type->add_item(TTR("Sec")); + loop_offset_type->add_item(TTR("Beats")); + loop_offset_type->connect("item_selected", callable_mp(this, &AudioStreamImportSettingsDialog::_update_loop_offset_type).unbind(1)); + loop_offset_type->connect("item_selected", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); + loop_hb->add_child(loop_offset_type); main_vbox->add_margin_child(TTR("Loop:"), loop_hb); HBoxContainer *interactive_hb = memnew(HBoxContainer); @@ -564,7 +604,7 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { interactive_hb->add_child(bpm_edit); interactive_hb->add_spacer(); beats_enabled = memnew(CheckBox); - beats_enabled->set_text(TTR("Beat Count:")); + beats_enabled->set_text(TTR("Length (beats):")); beats_enabled->connect("toggled", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); interactive_hb->add_child(beats_enabled); beats_edit = memnew(SpinBox); @@ -572,7 +612,8 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { beats_edit->set_max(99999); beats_edit->connect("value_changed", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); interactive_hb->add_child(beats_edit); - bar_beats_label = memnew(Label(TTR("Bar Beats:"))); + bar_beats_label = memnew(Label(TTR("Bar (beats):"))); + interactive_hb->add_spacer(); interactive_hb->add_child(bar_beats_label); bar_beats_edit = memnew(SpinBox); bar_beats_edit->set_tooltip_text(TTR("Configure the Beats Per Bar. This used for music-aware transitions between AudioStreams.")); diff --git a/editor/import/audio_stream_import_settings.h b/editor/import/audio_stream_import_settings.h index da6344adb9b1..4047f7ce33b8 100644 --- a/editor/import/audio_stream_import_settings.h +++ b/editor/import/audio_stream_import_settings.h @@ -35,6 +35,7 @@ #include "scene/audio/audio_stream_player.h" #include "scene/gui/color_rect.h" #include "scene/gui/dialogs.h" +#include "scene/gui/option_button.h" #include "scene/gui/spin_box.h" #include "scene/resources/texture.h" @@ -50,9 +51,11 @@ class AudioStreamImportSettingsDialog : public ConfirmationDialog { Label *bar_beats_label = nullptr; SpinBox *bar_beats_edit = nullptr; CheckBox *loop = nullptr; + OptionButton *loop_offset_type = nullptr; SpinBox *loop_offset = nullptr; ColorRect *color_rect = nullptr; Ref stream; + Ref stream_no_loop; AudioStreamPlayer *_player = nullptr; ColorRect *_preview = nullptr; Control *_indicator = nullptr; @@ -86,6 +89,7 @@ class AudioStreamImportSettingsDialog : public ConfirmationDialog { void _settings_changed(); void _reimport(); + void _update_loop_offset_type(); protected: void _notification(int p_what); diff --git a/modules/minimp3/audio_stream_mp3.cpp b/modules/minimp3/audio_stream_mp3.cpp index 4efa4d329ea0..b494e8943243 100644 --- a/modules/minimp3/audio_stream_mp3.cpp +++ b/modules/minimp3/audio_stream_mp3.cpp @@ -60,7 +60,12 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) { if (samples_mixed) { p_buffer[p_frames - todo] = AudioFrame(buf_frame[0], buf_frame[samples_mixed - 1]); if (loop_fade_remaining < FADE_SIZE) { - p_buffer[p_frames - todo] += loop_fade[loop_fade_remaining] * (float(FADE_SIZE - loop_fade_remaining) / float(FADE_SIZE)); + float c = (float(FADE_SIZE - loop_fade_remaining) / float(FADE_SIZE)); + if (mp3_stream->loop_offset > 0) { + // Only fade-in if loop-offset > 0 + p_buffer[p_frames - todo] *= 1.0 - c; + } + p_buffer[p_frames - todo] += loop_fade[loop_fade_remaining] * c; loop_fade_remaining++; } --todo; diff --git a/modules/minimp3/resource_importer_mp3.cpp b/modules/minimp3/resource_importer_mp3.cpp index 33c926689ab4..619d1fef3430 100644 --- a/modules/minimp3/resource_importer_mp3.cpp +++ b/modules/minimp3/resource_importer_mp3.cpp @@ -76,6 +76,7 @@ String ResourceImporterMP3::get_preset_name(int p_idx) const { void ResourceImporterMP3::get_import_options(const String &p_path, List *r_options, int p_preset) const { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "loop"), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "loop_offset_type", PROPERTY_HINT_ENUM, "Seconds,Beats"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "loop_offset"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,or_greater"), 0)); @@ -118,10 +119,15 @@ Ref ResourceImporterMP3::import_mp3(const String &p_path) { Error ResourceImporterMP3::import(const String &p_source_file, const String &p_save_path, const HashMap &p_options, List *r_platform_variants, List *r_gen_files, Variant *r_metadata) { bool loop = p_options["loop"]; float loop_offset = p_options["loop_offset"]; + int loop_offset_type = p_options["loop_offset_type"]; double bpm = p_options["bpm"]; float beat_count = p_options["beat_count"]; float bar_beats = p_options["bar_beats"]; + if (loop_offset_type == 1) { + loop_offset *= (60.0 / bpm); + } + Ref mp3_stream = import_mp3(p_source_file); if (mp3_stream.is_null()) { return ERR_CANT_OPEN; diff --git a/modules/vorbis/audio_stream_ogg_vorbis.cpp b/modules/vorbis/audio_stream_ogg_vorbis.cpp index 7ec0b697bf7c..6183ce9b18e0 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.cpp +++ b/modules/vorbis/audio_stream_ogg_vorbis.cpp @@ -68,7 +68,12 @@ int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram if (loop_fade_remaining < FADE_SIZE) { int to_fade = loop_fade_remaining + MIN(FADE_SIZE - loop_fade_remaining, mixed); for (int i = loop_fade_remaining; i < to_fade; i++) { - buffer[i - loop_fade_remaining] += loop_fade[i] * (float(FADE_SIZE - i) / float(FADE_SIZE)); + float c = (float(FADE_SIZE - i) / float(FADE_SIZE)); + if (vorbis_stream->loop_offset > 0) { + // Only fade-in if loop-offset > 0 + buffer[i - loop_fade_remaining] *= 1.0 - c; + } + buffer[i - loop_fade_remaining] += loop_fade[i] * c; } loop_fade_remaining = to_fade; } diff --git a/modules/vorbis/resource_importer_ogg_vorbis.cpp b/modules/vorbis/resource_importer_ogg_vorbis.cpp index bf5d964d39f5..1dd136f8176e 100644 --- a/modules/vorbis/resource_importer_ogg_vorbis.cpp +++ b/modules/vorbis/resource_importer_ogg_vorbis.cpp @@ -75,6 +75,7 @@ String ResourceImporterOggVorbis::get_preset_name(int p_idx) const { void ResourceImporterOggVorbis::get_import_options(const String &p_path, List *r_options, int p_preset) const { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "loop"), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "loop_offset_type", PROPERTY_HINT_ENUM, "Seconds,Beats"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "loop_offset"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,or_greater"), 0)); @@ -98,10 +99,15 @@ void ResourceImporterOggVorbis::show_advanced_options(const String &p_path) { Error ResourceImporterOggVorbis::import(const String &p_source_file, const String &p_save_path, const HashMap &p_options, List *r_platform_variants, List *r_gen_files, Variant *r_metadata) { bool loop = p_options["loop"]; double loop_offset = p_options["loop_offset"]; + int loop_offset_type = p_options["loop_offset_type"]; double bpm = p_options["bpm"]; int beat_count = p_options["beat_count"]; int bar_beats = p_options["bar_beats"]; + if (loop_offset_type == 1) { + loop_offset *= (60.0 / bpm); + } + Ref ogg_vorbis_stream = load_from_file(p_source_file); if (ogg_vorbis_stream.is_null()) { return ERR_CANT_OPEN;