Implement BPM support

Based on #62896, only implements the BPM support part.

* Implements BPM support in the AudioStreamOGG/MP3 importers.
* Can select BPM/Bar Size and total beats in a song file, as well as edit looping points.
* Looping is now BPM aware
* Added a special importer UI for configuring this.
* Added a special preview showing the audio waveform as well as the playback position in the resource picker.
* Renamed `AudioStream::instance` to `instantiate` for correctness.
This commit is contained in:
reduz 2022-07-21 01:00:58 +02:00 committed by Juan Linietsky
parent 976cb7ea9f
commit d1ddee2258
41 changed files with 1548 additions and 438 deletions

View file

@ -275,6 +275,12 @@
Sets the volume of the bus at index [code]bus_idx[/code] to [code]volume_db[/code].
</description>
</method>
<method name="set_enable_tagging_used_audio_streams">
<return type="void" />
<argument index="0" name="enable" type="bool" />
<description>
</description>
</method>
<method name="swap_bus_effects">
<return type="void" />
<argument index="0" name="bus_idx" type="int" />

View file

@ -13,6 +13,16 @@
<link title="Audio Spectrum Demo">https://godotengine.org/asset-library/asset/528</link>
</tutorials>
<methods>
<method name="_get_beat_count" qualifiers="virtual const">
<return type="int" />
<description>
</description>
</method>
<method name="_get_bpm" qualifiers="virtual const">
<return type="float" />
<description>
</description>
</method>
<method name="_get_length" qualifiers="virtual const">
<return type="float" />
<description>
@ -23,7 +33,7 @@
<description>
</description>
</method>
<method name="_instance_playback" qualifiers="virtual const">
<method name="_instantiate_playback" qualifiers="virtual const">
<return type="AudioStreamPlayback" />
<description>
</description>
@ -39,10 +49,10 @@
Returns the length of the audio stream in seconds.
</description>
</method>
<method name="instance_playback">
<method name="instantiate_playback">
<return type="AudioStreamPlayback" />
<description>
Returns an AudioStreamPlayback. Useful for when you want to extend `_instance_playback` but call `instance_playback` from an internally held AudioStream subresource. An example of this can be found in the source files for `AudioStreamRandomPitch::instance_playback`.
Returns an AudioStreamPlayback. 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 files for [code]AudioStreamRandomPitch::instantiate_playback[/code].
</description>
</method>
<method name="is_monophonic" qualifiers="const">

View file

@ -50,5 +50,10 @@
<description>
</description>
</method>
<method name="_tag_used_streams" qualifiers="virtual">
<return type="void" />
<description>
</description>
</method>
</methods>
</class>

View file

@ -153,6 +153,8 @@ void AudioStreamPreviewGenerator::_preview_thread(void *p_preview) {
singleton->call_deferred(SNAME("_update_emit"), preview->id);
}
preview->preview->version++;
preview->playback->stop();
preview->generating.clear();
@ -171,7 +173,7 @@ Ref<AudioStreamPreview> AudioStreamPreviewGenerator::generate_preview(const Ref<
Preview *preview = &previews[p_stream->get_instance_id()];
preview->base_stream = p_stream;
preview->playback = preview->base_stream->instance_playback();
preview->playback = preview->base_stream->instantiate_playback();
preview->generating.set();
preview->id = p_stream->get_instance_id();

View file

@ -43,8 +43,10 @@ class AudioStreamPreview : public RefCounted {
float length;
friend class AudioStreamPreviewGenerator;
uint64_t version = 1;
public:
uint64_t get_version() const { return version; }
float get_length() const;
float get_max(float p_time, float p_time_next) const;
float get_min(float p_time, float p_time_next) const;

View file

@ -104,6 +104,7 @@
#include "editor/editor_translation_parser.h"
#include "editor/export_template_manager.h"
#include "editor/filesystem_dock.h"
#include "editor/import/audio_stream_import_settings.h"
#include "editor/import/dynamic_font_import_settings.h"
#include "editor/import/editor_import_collada.h"
#include "editor/import/resource_importer_bitmask.h"
@ -131,7 +132,6 @@
#include "editor/plugins/animation_state_machine_editor.h"
#include "editor/plugins/animation_tree_editor_plugin.h"
#include "editor/plugins/asset_library_editor_plugin.h"
#include "editor/plugins/audio_stream_editor_plugin.h"
#include "editor/plugins/audio_stream_randomizer_editor_plugin.h"
#include "editor/plugins/bit_map_editor_plugin.h"
#include "editor/plugins/bone_map_editor_plugin.h"
@ -5899,6 +5899,8 @@ EditorNode::EditorNode() {
RenderingServer::get_singleton()->set_debug_generate_wireframes(true);
AudioServer::get_singleton()->set_enable_tagging_used_audio_streams(true);
// No navigation server by default if in editor.
NavigationServer3D::get_singleton()->set_active(false);
@ -6443,6 +6445,9 @@ EditorNode::EditorNode() {
scene_import_settings = memnew(SceneImportSettings);
gui_base->add_child(scene_import_settings);
audio_stream_import_settings = memnew(AudioStreamImportSettings);
gui_base->add_child(audio_stream_import_settings);
fontdata_import_settings = memnew(DynamicFontImportSettings);
gui_base->add_child(fontdata_import_settings);
@ -7079,7 +7084,6 @@ EditorNode::EditorNode() {
// This list is alphabetized, and plugins that depend on Node2D are in their own section below.
add_editor_plugin(memnew(AnimationTreeEditorPlugin));
add_editor_plugin(memnew(AudioBusesEditorPlugin(audio_bus_editor)));
add_editor_plugin(memnew(AudioStreamEditorPlugin));
add_editor_plugin(memnew(AudioStreamRandomizerEditorPlugin));
add_editor_plugin(memnew(BitMapEditorPlugin));
add_editor_plugin(memnew(BoneMapEditorPlugin));

View file

@ -88,6 +88,7 @@ class ProjectExportDialog;
class ProjectSettingsEditor;
class RunSettingsDialog;
class SceneImportSettings;
class AudioStreamImportSettings;
class ScriptCreateDialog;
class SubViewport;
class TabBar;
@ -468,6 +469,7 @@ private:
DynamicFontImportSettings *fontdata_import_settings = nullptr;
SceneImportSettings *scene_import_settings = nullptr;
AudioStreamImportSettings *audio_stream_import_settings = nullptr;
String import_reload_fn;

View file

@ -3517,6 +3517,9 @@ void EditorPropertyResource::setup(Object *p_object, const String &p_path, const
shader_picker->set_edited_material(Object::cast_to<ShaderMaterial>(p_object));
resource_picker = shader_picker;
connect(SNAME("ready"), callable_mp(this, &EditorPropertyResource::_update_preferred_shader));
} else if (p_base_type == "AudioStream") {
EditorAudioStreamPicker *astream_picker = memnew(EditorAudioStreamPicker);
resource_picker = astream_picker;
} else {
resource_picker = memnew(EditorResourcePicker);
}

View file

@ -30,6 +30,7 @@
#include "editor_resource_picker.h"
#include "editor/audio_stream_preview.h"
#include "editor/editor_file_dialog.h"
#include "editor/editor_node.h"
#include "editor/editor_resource_preview.h"
@ -47,32 +48,37 @@ void EditorResourcePicker::clear_caches() {
}
void EditorResourcePicker::_update_resource() {
preview_rect->set_texture(Ref<Texture2D>());
assign_button->set_custom_minimum_size(Size2(1, 1));
String resource_path;
if (edited_resource.is_valid() && edited_resource->get_path().is_resource_file()) {
resource_path = edited_resource->get_path() + "\n";
}
if (edited_resource == Ref<Resource>()) {
assign_button->set_icon(Ref<Texture2D>());
assign_button->set_text(TTR("[empty]"));
assign_button->set_tooltip("");
} else {
assign_button->set_icon(EditorNode::get_singleton()->get_object_icon(edited_resource.operator->(), "Object"));
if (preview_rect) {
preview_rect->set_texture(Ref<Texture2D>());
if (!edited_resource->get_name().is_empty()) {
assign_button->set_text(edited_resource->get_name());
} else if (edited_resource->get_path().is_resource_file()) {
assign_button->set_text(edited_resource->get_path().get_file());
assign_button->set_custom_minimum_size(assign_button_min_size);
if (edited_resource == Ref<Resource>()) {
assign_button->set_icon(Ref<Texture2D>());
assign_button->set_text(TTR("[empty]"));
assign_button->set_tooltip("");
} else {
assign_button->set_text(edited_resource->get_class());
}
assign_button->set_icon(EditorNode::get_singleton()->get_object_icon(edited_resource.operator->(), "Object"));
String resource_path;
if (edited_resource->get_path().is_resource_file()) {
resource_path = edited_resource->get_path() + "\n";
if (!edited_resource->get_name().is_empty()) {
assign_button->set_text(edited_resource->get_name());
} else if (edited_resource->get_path().is_resource_file()) {
assign_button->set_text(edited_resource->get_path().get_file());
} else {
assign_button->set_text(edited_resource->get_class());
}
assign_button->set_tooltip(resource_path + TTR("Type:") + " " + edited_resource->get_class());
// Preview will override the above, so called at the end.
EditorResourcePreview::get_singleton()->queue_edited_resource_preview(edited_resource, this, "_update_resource_preview", edited_resource->get_instance_id());
}
} else if (edited_resource.is_valid()) {
assign_button->set_tooltip(resource_path + TTR("Type:") + " " + edited_resource->get_class());
// Preview will override the above, so called at the end.
EditorResourcePreview::get_singleton()->queue_edited_resource_preview(edited_resource, this, "_update_resource_preview", edited_resource->get_instance_id());
}
}
@ -81,28 +87,30 @@ void EditorResourcePicker::_update_resource_preview(const String &p_path, const
return;
}
Ref<Script> script = edited_resource;
if (script.is_valid()) {
assign_button->set_text(script->get_path().get_file());
return;
}
if (p_preview.is_valid()) {
preview_rect->set_offset(SIDE_LEFT, assign_button->get_icon()->get_width() + assign_button->get_theme_stylebox(SNAME("normal"))->get_default_margin(SIDE_LEFT) + get_theme_constant(SNAME("h_separation"), SNAME("Button")));
// Resource-specific stretching.
if (Ref<GradientTexture1D>(edited_resource).is_valid() || Ref<Gradient>(edited_resource).is_valid()) {
preview_rect->set_stretch_mode(TextureRect::STRETCH_SCALE);
assign_button->set_custom_minimum_size(Size2(1, 1));
} else {
preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size");
thumbnail_size *= EDSCALE;
assign_button->set_custom_minimum_size(Size2(1, thumbnail_size));
if (preview_rect) {
Ref<Script> script = edited_resource;
if (script.is_valid()) {
assign_button->set_text(script->get_path().get_file());
return;
}
preview_rect->set_texture(p_preview);
assign_button->set_text("");
if (p_preview.is_valid()) {
preview_rect->set_offset(SIDE_LEFT, assign_button->get_icon()->get_width() + assign_button->get_theme_stylebox(SNAME("normal"))->get_default_margin(SIDE_LEFT) + get_theme_constant(SNAME("h_separation"), SNAME("Button")));
// Resource-specific stretching.
if (Ref<GradientTexture1D>(edited_resource).is_valid() || Ref<Gradient>(edited_resource).is_valid()) {
preview_rect->set_stretch_mode(TextureRect::STRETCH_SCALE);
assign_button->set_custom_minimum_size(assign_button_min_size);
} else {
preview_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size");
thumbnail_size *= EDSCALE;
assign_button->set_custom_minimum_size(Size2(MIN(1, assign_button_min_size.x), MIN(thumbnail_size, assign_button_min_size.y)));
}
preview_rect->set_texture(p_preview);
assign_button->set_text("");
}
}
}
@ -866,7 +874,7 @@ void EditorResourcePicker::_ensure_resource_menu() {
edit_menu->connect("popup_hide", callable_mp((BaseButton *)edit_button, &BaseButton::set_pressed), varray(false));
}
EditorResourcePicker::EditorResourcePicker() {
EditorResourcePicker::EditorResourcePicker(bool p_hide_assign_button_controls) {
assign_button = memnew(Button);
assign_button->set_flat(true);
assign_button->set_h_size_flags(SIZE_EXPAND_FILL);
@ -877,13 +885,15 @@ EditorResourcePicker::EditorResourcePicker() {
assign_button->connect("draw", callable_mp(this, &EditorResourcePicker::_button_draw));
assign_button->connect("gui_input", callable_mp(this, &EditorResourcePicker::_button_input));
preview_rect = memnew(TextureRect);
preview_rect->set_ignore_texture_size(true);
preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
preview_rect->set_offset(SIDE_TOP, 1);
preview_rect->set_offset(SIDE_BOTTOM, -1);
preview_rect->set_offset(SIDE_RIGHT, -1);
assign_button->add_child(preview_rect);
if (!p_hide_assign_button_controls) {
preview_rect = memnew(TextureRect);
preview_rect->set_ignore_texture_size(true);
preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
preview_rect->set_offset(SIDE_TOP, 1);
preview_rect->set_offset(SIDE_BOTTOM, -1);
preview_rect->set_offset(SIDE_RIGHT, -1);
assign_button->add_child(preview_rect);
}
edit_button = memnew(Button);
edit_button->set_flat(true);
@ -993,3 +1003,176 @@ void EditorShaderPicker::set_preferred_mode(int p_mode) {
EditorShaderPicker::EditorShaderPicker() {
}
//////////////
void EditorAudioStreamPicker::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY:
case NOTIFICATION_THEME_CHANGED: {
_update_resource();
} break;
case NOTIFICATION_INTERNAL_PROCESS: {
Ref<AudioStream> audio_stream = get_edited_resource();
if (audio_stream.is_valid()) {
if (audio_stream->get_length() > 0) {
Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream);
if (preview.is_valid()) {
if (preview->get_version() != last_preview_version) {
stream_preview_rect->update();
last_preview_version = preview->get_version();
}
}
}
uint64_t tagged_frame = audio_stream->get_tagged_frame();
uint64_t diff_frames = AudioServer::get_singleton()->get_mixed_frames() - tagged_frame;
uint64_t diff_msec = diff_frames * 1000 / AudioServer::get_singleton()->get_mix_rate();
if (diff_msec < 300) {
uint32_t count = audio_stream->get_tagged_frame_count();
bool differ = false;
if (count != tagged_frame_offset_count) {
differ = true;
}
float offsets[MAX_TAGGED_FRAMES];
for (uint32_t i = 0; i < MIN(count, uint32_t(MAX_TAGGED_FRAMES)); i++) {
offsets[i] = audio_stream->get_tagged_frame_offset(i);
if (offsets[i] != tagged_frame_offsets[i]) {
differ = true;
}
}
if (differ) {
tagged_frame_offset_count = count;
for (uint32_t i = 0; i < count; i++) {
tagged_frame_offsets[i] = offsets[i];
}
}
stream_preview_rect->update();
} else {
if (tagged_frame_offset_count != 0) {
stream_preview_rect->update();
}
tagged_frame_offset_count = 0;
}
}
} break;
}
}
void EditorAudioStreamPicker::_update_resource() {
EditorResourcePicker::_update_resource();
Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label"));
Ref<AudioStream> audio_stream = get_edited_resource();
if (audio_stream.is_valid() && audio_stream->get_length() > 0.0) {
set_assign_button_min_size(Size2(1, font->get_height(font_size) * 3));
} else {
set_assign_button_min_size(Size2(1, font->get_height(font_size) * 1.5));
}
stream_preview_rect->update();
}
void EditorAudioStreamPicker::_preview_draw() {
Ref<AudioStream> audio_stream = get_edited_resource();
if (!audio_stream.is_valid()) {
get_assign_button()->set_text(TTR("[empty]"));
return;
}
int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label"));
get_assign_button()->set_text("");
Size2i size = stream_preview_rect->get_size();
Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
Rect2 rect(Point2(), size);
if (audio_stream->get_length() > 0) {
rect.size.height *= 0.5;
stream_preview_rect->draw_rect(rect, Color(0, 0, 0, 1));
Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(audio_stream);
float preview_len = preview->get_length();
Vector<Vector2> lines;
lines.resize(size.width * 2);
for (int i = 0; i < size.width; i++) {
float ofs = i * preview_len / size.width;
float ofs_n = (i + 1) * preview_len / size.width;
float max = preview->get_max(ofs, ofs_n) * 0.5 + 0.5;
float min = preview->get_min(ofs, ofs_n) * 0.5 + 0.5;
int idx = i;
lines.write[idx * 2 + 0] = Vector2(i + 1, rect.position.y + min * rect.size.y);
lines.write[idx * 2 + 1] = Vector2(i + 1, rect.position.y + max * rect.size.y);
}
Vector<Color> color;
color.push_back(get_theme_color(SNAME("contrast_color_2"), SNAME("Editor")));
RS::get_singleton()->canvas_item_add_multiline(stream_preview_rect->get_canvas_item(), lines, color);
if (tagged_frame_offset_count) {
Color accent = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
for (uint32_t i = 0; i < tagged_frame_offset_count; i++) {
int x = CLAMP(tagged_frame_offsets[i] * size.width / preview_len, 0, size.width);
if (x == 0) {
continue; // Because some may always return 0, ignore offset 0.
}
stream_preview_rect->draw_rect(Rect2i(x, 0, 2, rect.size.height), accent);
}
}
rect.position.y += rect.size.height;
}
Ref<Texture2D> icon;
Color icon_modulate(1, 1, 1, 1);
if (tagged_frame_offset_count > 0) {
icon = get_theme_icon(SNAME("Play"), SNAME("EditorIcons"));
if ((OS::get_singleton()->get_ticks_msec() % 500) > 250) {
icon_modulate = Color(1, 0.5, 0.5, 1); // get_theme_color(SNAME("accent_color"), SNAME("Editor"));
}
} else {
icon = EditorNode::get_singleton()->get_object_icon(audio_stream.operator->(), "Object");
}
String text;
if (!audio_stream->get_name().is_empty()) {
text = audio_stream->get_name();
} else if (audio_stream->get_path().is_resource_file()) {
text = audio_stream->get_path().get_file();
} else {
text = audio_stream->get_class().replace_first("AudioStream", "");
}
stream_preview_rect->draw_texture(icon, Point2i(EDSCALE * 2, rect.position.y + (rect.size.height - icon->get_height()) / 2), icon_modulate);
stream_preview_rect->draw_string(font, Point2i(EDSCALE * 2 + icon->get_width(), rect.position.y + font->get_ascent(font_size) + (rect.size.height - font->get_height(font_size)) / 2), text, HORIZONTAL_ALIGNMENT_CENTER, size.width - 4 * EDSCALE - icon->get_width());
}
EditorAudioStreamPicker::EditorAudioStreamPicker() :
EditorResourcePicker(true) {
stream_preview_rect = memnew(Control);
stream_preview_rect->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
stream_preview_rect->set_offset(SIDE_TOP, 1);
stream_preview_rect->set_offset(SIDE_BOTTOM, -1);
stream_preview_rect->set_offset(SIDE_RIGHT, -1);
stream_preview_rect->set_mouse_filter(MOUSE_FILTER_IGNORE);
stream_preview_rect->connect("draw", callable_mp(this, &EditorAudioStreamPicker::_preview_draw));
get_assign_button()->add_child(stream_preview_rect);
get_assign_button()->move_child(stream_preview_rect, 0);
set_process_internal(true);
}

View file

@ -58,6 +58,8 @@ class EditorResourcePicker : public HBoxContainer {
EditorFileDialog *file_dialog = nullptr;
EditorQuickOpen *quick_open = nullptr;
Size2i assign_button_min_size = Size2i(1, 1);
enum MenuOption {
OBJ_MENU_LOAD,
OBJ_MENU_QUICKLOAD,
@ -75,7 +77,6 @@ class EditorResourcePicker : public HBoxContainer {
PopupMenu *edit_menu = nullptr;
void _update_resource();
void _update_resource_preview(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, ObjectID p_obj);
void _resource_selected();
@ -100,9 +101,17 @@ class EditorResourcePicker : public HBoxContainer {
void _ensure_resource_menu();
protected:
virtual void _update_resource();
Button *get_assign_button() { return assign_button; }
static void _bind_methods();
void _notification(int p_what);
void set_assign_button_min_size(const Size2i &p_size) {
assign_button_min_size = p_size;
assign_button->set_custom_minimum_size(assign_button_min_size);
}
GDVIRTUAL1(_set_create_options, Object *)
GDVIRTUAL1R(bool, _handle_menu_selected, int)
@ -126,7 +135,7 @@ public:
virtual void set_create_options(Object *p_menu_node);
virtual bool handle_menu_selected(int p_which);
EditorResourcePicker();
EditorResourcePicker(bool p_hide_assign_button_controls = false);
};
class EditorScriptPicker : public EditorResourcePicker {
@ -173,4 +182,26 @@ public:
EditorShaderPicker();
};
class EditorAudioStreamPicker : public EditorResourcePicker {
GDCLASS(EditorAudioStreamPicker, EditorResourcePicker);
uint64_t last_preview_version = 0;
Control *stream_preview_rect = nullptr;
enum {
MAX_TAGGED_FRAMES = 8
};
float tagged_frame_offsets[MAX_TAGGED_FRAMES];
uint32_t tagged_frame_offset_count = 0;
void _preview_draw();
virtual void _update_resource() override;
protected:
void _notification(int p_what);
public:
EditorAudioStreamPicker();
};
#endif // EDITOR_RESOURCE_PICKER_H

View file

@ -985,7 +985,9 @@ void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorit
}
}
if (ResourceLoader::get_resource_type(fpath) == "PackedScene") {
String resource_type = ResourceLoader::get_resource_type(fpath);
if (resource_type == "PackedScene") {
bool is_imported = false;
{
@ -1005,7 +1007,7 @@ void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorit
} else {
EditorNode::get_singleton()->open_request(fpath);
}
} else if (ResourceLoader::get_resource_type(fpath) == "AnimationLibrary") {
} else if (resource_type == "AnimationLibrary") {
bool is_imported = false;
{
@ -1025,6 +1027,25 @@ void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorit
} else {
EditorNode::get_singleton()->open_request(fpath);
}
} else if (ResourceLoader::is_imported(fpath)) {
// If the importer has advanced settings, show them.
int order;
bool can_threads;
String name;
Error err = ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(fpath, order, can_threads, name);
bool used_advanced_settings = false;
if (err == OK) {
Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(name);
if (importer.is_valid() && importer->has_advanced_options()) {
importer->show_advanced_options(fpath);
used_advanced_settings = true;
}
}
if (!used_advanced_settings) {
EditorNode::get_singleton()->load_resource(fpath);
}
} else {
EditorNode::get_singleton()->load_resource(fpath);
}

View file

@ -0,0 +1,650 @@
/*************************************************************************/
/* audio_stream_import_settings.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "audio_stream_import_settings.h"
#include "editor/audio_stream_preview.h"
#include "editor/editor_file_system.h"
#include "editor/editor_scale.h"
AudioStreamImportSettings *AudioStreamImportSettings::singleton = nullptr;
void AudioStreamImportSettings::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
AudioStreamPreviewGenerator::get_singleton()->connect("preview_updated", callable_mp(this, &AudioStreamImportSettings::_preview_changed));
connect("confirmed", callable_mp(this, &AudioStreamImportSettings::_reimport));
} break;
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_ENTER_TREE: {
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
_stop_button->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons")));
_preview->set_color(get_theme_color(SNAME("dark_color_2"), SNAME("Editor")));
color_rect->set_color(get_theme_color(SNAME("dark_color_1"), SNAME("Editor")));
_current_label->add_theme_font_override("font", get_theme_font(SNAME("status_source"), SNAME("EditorFonts")));
_current_label->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("status_source_size"), SNAME("EditorFonts")));
_duration_label->add_theme_font_override("font", get_theme_font(SNAME("status_source"), SNAME("EditorFonts")));
_duration_label->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("status_source_size"), SNAME("EditorFonts")));
zoom_in->set_icon(get_theme_icon(SNAME("ZoomMore"), SNAME("EditorIcons")));
zoom_out->set_icon(get_theme_icon(SNAME("ZoomLess"), SNAME("EditorIcons")));
zoom_reset->set_icon(get_theme_icon(SNAME("ZoomReset"), SNAME("EditorIcons")));
_indicator->update();
_preview->update();
} break;
case NOTIFICATION_PROCESS: {
_current = _player->get_playback_position();
_indicator->update();
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
if (!is_visible()) {
_stop();
}
} break;
}
}
void AudioStreamImportSettings::_draw_preview() {
Rect2 rect = _preview->get_rect();
Size2 size = rect.size;
Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(stream);
float preview_offset = zoom_bar->get_value();
float preview_len = zoom_bar->get_page();
Ref<Font> beat_font = get_theme_font(SNAME("main"), SNAME("EditorFonts"));
int main_size = get_theme_font_size(SNAME("main_size"), SNAME("EditorFonts"));
Vector<Vector2> lines;
lines.resize(size.width * 2);
Color color_active = get_theme_color(SNAME("contrast_color_2"), SNAME("Editor"));
Color color_inactive = color_active;
color_inactive.a *= 0.5;
Vector<Color> color;
color.resize(lines.size());
float inactive_from = 1e20;
float beat_size = 0;
int last_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;
rect.position.y += y_ofs;
rect.size.y -= y_ofs;
if (stream->get_beat_count() > 0) {
last_beat = stream->get_beat_count();
inactive_from = last_beat * beat_size;
}
}
for (int i = 0; i < size.width; i++) {
float ofs = preview_offset + i * preview_len / size.width;
float ofs_n = preview_offset + (i + 1) * preview_len / size.width;
float max = preview->get_max(ofs, ofs_n) * 0.5 + 0.5;
float min = preview->get_min(ofs, ofs_n) * 0.5 + 0.5;
int idx = i;
lines.write[idx * 2 + 0] = Vector2(i + 1, rect.position.y + min * rect.size.y);
lines.write[idx * 2 + 1] = Vector2(i + 1, rect.position.y + max * rect.size.y);
Color c = ofs > inactive_from ? color_inactive : color_active;
color.write[idx * 2 + 0] = c;
color.write[idx * 2 + 1] = c;
}
RS::get_singleton()->canvas_item_add_multiline(_preview->get_canvas_item(), lines, color);
if (beat_size) {
Color beat_color = Color(1, 1, 1, 1);
Color final_beat_color = beat_color;
Color bar_color = beat_color;
beat_color.a *= 0.4;
bar_color.a *= 0.6;
int prev_beat = 0; // Do not draw beat zero
Color color_bg = color_active;
color_bg.a *= 0.2;
_preview->draw_rect(Rect2(0, 0, rect.size.width, rect.position.y), color_bg);
int bar_beats = stream->get_bar_beats();
int last_text_end_x = 0;
for (int i = 0; i < size.width; i++) {
float ofs = preview_offset + i * preview_len / size.width;
int beat = int(ofs / beat_size);
if (beat != prev_beat) {
String text = itos(beat);
int text_w = beat_font->get_string_size(text).width;
if (i - text_w / 2 > last_text_end_x + 2 * EDSCALE) {
int x_ofs = i - text_w / 2;
_preview->draw_string(beat_font, Point2(x_ofs, 2 * EDSCALE + beat_font->get_ascent(main_size)), text, HORIZONTAL_ALIGNMENT_LEFT, rect.size.width - x_ofs, Font::DEFAULT_FONT_SIZE, color_active);
last_text_end_x = i + text_w / 2;
}
if (beat == last_beat) {
_preview->draw_rect(Rect2i(i, rect.position.y, 2, rect.size.height), final_beat_color);
// Darken subsequent beats
beat_color.a *= 0.3;
color_active.a *= 0.3;
} else {
_preview->draw_rect(Rect2i(i, rect.position.y, 1, rect.size.height), (beat % bar_beats) == 0 ? bar_color : beat_color);
}
prev_beat = beat;
}
}
}
}
void AudioStreamImportSettings::_preview_changed(ObjectID p_which) {
if (stream.is_valid() && stream->get_instance_id() == p_which) {
_preview->update();
}
}
void AudioStreamImportSettings::_preview_zoom_in() {
if (!stream.is_valid()) {
return;
}
float page_size = zoom_bar->get_page();
zoom_bar->set_page(page_size * 0.5);
zoom_bar->set_value(zoom_bar->get_value() + page_size * 0.25);
_preview->update();
_indicator->update();
}
void AudioStreamImportSettings::_preview_zoom_out() {
if (!stream.is_valid()) {
return;
}
float page_size = zoom_bar->get_page();
zoom_bar->set_page(MIN(zoom_bar->get_max(), page_size * 2.0));
zoom_bar->set_value(zoom_bar->get_value() - page_size * 0.5);
_preview->update();
_indicator->update();
}
void AudioStreamImportSettings::_preview_zoom_reset() {
if (!stream.is_valid()) {
return;
}
zoom_bar->set_max(stream->get_length());
zoom_bar->set_page(zoom_bar->get_max());
zoom_bar->set_value(0);
_preview->update();
_indicator->update();
}
void AudioStreamImportSettings::_preview_zoom_offset_changed(double) {
_preview->update();
_indicator->update();
}
void AudioStreamImportSettings::_audio_changed() {
if (!is_visible()) {
return;
}
_preview->update();
_indicator->update();
color_rect->update();
}
void AudioStreamImportSettings::_play() {
if (_player->is_playing()) {
// '_pausing' variable indicates that we want to pause the audio player, not stop it. See '_on_finished()'.
_pausing = true;
_player->stop();
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
set_process(false);
} else {
_player->play(_current);
_play_button->set_icon(get_theme_icon(SNAME("Pause"), SNAME("EditorIcons")));
set_process(true);
}
}
void AudioStreamImportSettings::_stop() {
_player->stop();
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
_current = 0;
_indicator->update();
set_process(false);
}
void AudioStreamImportSettings::_on_finished() {
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
if (!_pausing) {
_current = 0;
_indicator->update();
} else {
_pausing = false;
}
set_process(false);
}
void AudioStreamImportSettings::_draw_indicator() {
if (!stream.is_valid()) {
return;
}
Rect2 rect = _preview->get_rect();
Ref<Font> beat_font = get_theme_font(SNAME("main"), SNAME("EditorFonts"));
int main_size = get_theme_font_size(SNAME("main_size"), SNAME("EditorFonts"));
if (stream->get_bpm() > 0) {
int y_ofs = beat_font->get_height(main_size) + 4 * EDSCALE;
rect.position.y += y_ofs;
rect.size.height -= y_ofs;
}
float ofs_x = (_current - zoom_bar->get_value()) * rect.size.width / zoom_bar->get_page();
if (ofs_x < 0 || ofs_x >= rect.size.width) {
return;
}
const Color color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
_indicator->draw_line(Point2(ofs_x, rect.position.y), Point2(ofs_x, rect.position.y + rect.size.height), color, Math::round(2 * EDSCALE));
_indicator->draw_texture(
get_theme_icon(SNAME("TimelineIndicator"), SNAME("EditorIcons")),
Point2(ofs_x - get_theme_icon(SNAME("TimelineIndicator"), SNAME("EditorIcons"))->get_width() * 0.5, rect.position.y),
color);
if (stream->get_bpm() > 0 && _hovering_beat != -1) {
// Draw hovered beat.
float preview_offset = zoom_bar->get_value();
float preview_len = zoom_bar->get_page();
float beat_size = 60 / float(stream->get_bpm());
int prev_beat = 0;
int last_text_end_x = 0;
for (int i = 0; i < rect.size.width; i++) {
float ofs = preview_offset + i * preview_len / rect.size.width;
int beat = int(ofs / beat_size);
if (beat != prev_beat) {
String text = itos(beat);
int text_w = beat_font->get_string_size(text).width;
if (i - text_w / 2 > last_text_end_x + 2 * EDSCALE && beat == _hovering_beat) {
int x_ofs = i - text_w / 2;
_indicator->draw_string(beat_font, Point2(x_ofs, 2 * EDSCALE + beat_font->get_ascent(main_size)), text, HORIZONTAL_ALIGNMENT_LEFT, rect.size.width - x_ofs, Font::DEFAULT_FONT_SIZE, color);
last_text_end_x = i + text_w / 2;
break;
}
prev_beat = beat;
}
}
}
_current_label->set_text(String::num(_current, 2).pad_decimals(2) + " /");
}
void AudioStreamImportSettings::_on_indicator_mouse_exited() {
_hovering_beat = -1;
_indicator->update();
}
void AudioStreamImportSettings::_on_input_indicator(Ref<InputEvent> p_event) {
const Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
if (stream->get_bpm() > 0) {
int main_size = get_theme_font_size(SNAME("main_size"), SNAME("EditorFonts"));
Ref<Font> beat_font = get_theme_font(SNAME("main"), SNAME("EditorFonts"));
int y_ofs = beat_font->get_height(main_size) + 4 * EDSCALE;
if ((!_dragging && mb->get_position().y < y_ofs) || _beat_len_dragging) {
if (mb->is_pressed()) {
_set_beat_len_to(mb->get_position().x);
_beat_len_dragging = true;
} else {
_beat_len_dragging = false;
}
return;
}
}
if (mb->is_pressed()) {
_seek_to(mb->get_position().x);
}
_dragging = mb->is_pressed();
}
const Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid()) {
if (_dragging) {
_seek_to(mm->get_position().x);
}
if (_beat_len_dragging) {
_set_beat_len_to(mm->get_position().x);
}
if (stream->get_bpm() > 0) {
int main_size = get_theme_font_size(SNAME("main_size"), SNAME("EditorFonts"));
Ref<Font> beat_font = get_theme_font(SNAME("main"), SNAME("EditorFonts"));
int y_ofs = beat_font->get_height(main_size) + 4 * EDSCALE;
if (mm->get_position().y < y_ofs) {
int new_hovering_beat = _get_beat_at_pos(mm->get_position().x);
if (new_hovering_beat != _hovering_beat) {
_hovering_beat = new_hovering_beat;
_indicator->update();
}
} else if (_hovering_beat != -1) {
_hovering_beat = -1;
_indicator->update();
}
}
}
}
int AudioStreamImportSettings::_get_beat_at_pos(real_t p_x) {
float ofs_sec = zoom_bar->get_value() + p_x * zoom_bar->get_page() / _preview->get_size().width;
ofs_sec = CLAMP(ofs_sec, 0, stream->get_length());
float beat_size = 60 / float(stream->get_bpm());
int beat = int(ofs_sec / beat_size + 0.5);
if (beat * beat_size > stream->get_length() + 0.001) { // Stream may end few audio frames before but may still want to use full loop.
beat--;
}
return beat;
}
void AudioStreamImportSettings::_set_beat_len_to(real_t p_x) {
int beat = _get_beat_at_pos(p_x);
if (beat < 1) {
beat = 1; // Because 0 is disable.
}
updating_settings = true;
beats_enabled->set_pressed(true);
beats_edit->set_value(beat);
updating_settings = false;
_settings_changed();
}
void AudioStreamImportSettings::_seek_to(real_t p_x) {
_current = zoom_bar->get_value() + p_x / _preview->get_rect().size.x * zoom_bar->get_page();
_current = CLAMP(_current, 0, stream->get_length());
_player->seek(_current);
_indicator->update();
}
void AudioStreamImportSettings::edit(const String &p_path, const String &p_importer, const Ref<AudioStream> &p_stream) {
if (!stream.is_null()) {
stream->disconnect("changed", callable_mp(this, &AudioStreamImportSettings::_audio_changed));
}
importer = p_importer;
path = p_path;
stream = p_stream;
_player->set_stream(stream);
_current = 0;
String text = String::num(stream->get_length(), 2).pad_decimals(2) + "s";
_duration_label->set_text(text);
if (!stream.is_null()) {
stream->connect("changed", callable_mp(this, &AudioStreamImportSettings::_audio_changed));
_preview->update();
_indicator->update();
color_rect->update();
} else {
hide();
}
params.clear();
if (stream.is_valid()) {
Ref<ConfigFile> config_file;
config_file.instantiate();
Error err = config_file->load(p_path + ".import");
updating_settings = true;
if (err == OK) {
double bpm = config_file->get_value("params", "bpm", 0);
int beats = config_file->get_value("params", "beat_count", 0);
bpm_edit->set_value(bpm > 0 ? bpm : 120);
bpm_enabled->set_pressed(bpm > 0);
beats_edit->set_value(beats);
beats_enabled->set_pressed(beats > 0);
loop->set_pressed(config_file->get_value("params", "loop", false));
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));
List<String> keys;
config_file->get_section_keys("params", &keys);
for (const String &K : keys) {
params[K] = config_file->get_value("params", K);
}
} else {
bpm_edit->set_value(false);
bpm_enabled->set_pressed(false);
beats_edit->set_value(0);
beats_enabled->set_pressed(false);
bar_beats_edit->set_value(4);
loop->set_pressed(false);
loop_offset->set_value(0);
}
_preview_zoom_reset();
updating_settings = false;
_settings_changed();
set_title(vformat(TTR("Audio Stream Importer: %s"), p_path.get_file()));
popup_centered();
}
}
void AudioStreamImportSettings::_settings_changed() {
if (updating_settings) {
return;
}
updating_settings = true;
stream->call("set_loop", loop->is_pressed());
stream->call("set_loop_offset", loop_offset->get_value());
if (bpm_enabled->is_pressed()) {
stream->call("set_bpm", bpm_edit->get_value());
beats_enabled->show();
beats_edit->show();
bar_beats_label->show();
bar_beats_edit->show();
double bpm = bpm_edit->get_value();
if (bpm > 0) {
float beat_size = 60 / float(bpm);
int beat_max = int((stream->get_length() + 0.001) / beat_size);
int current_beat = beats_edit->get_value();
beats_edit->set_max(beat_max);
if (current_beat > beat_max) {
beats_edit->set_value(beat_max);
stream->call("set_beat_count", beat_max);
}
}
stream->call("set_bar_beats", bar_beats_edit->get_value());
} else {
stream->call("set_bpm", 0);
stream->call("set_bar_beats", 4);
beats_enabled->hide();
beats_edit->hide();
bar_beats_label->hide();
bar_beats_edit->hide();
}
if (bpm_enabled->is_pressed() && beats_enabled->is_pressed()) {
stream->call("set_beat_count", beats_edit->get_value());
} else {
stream->call("set_beat_count", 0);
}
updating_settings = false;
_preview->update();
_indicator->update();
color_rect->update();
}
void AudioStreamImportSettings::_reimport() {
params["loop"] = loop->is_pressed();
params["loop_offset"] = loop_offset->get_value();
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);
EditorFileSystem::get_singleton()->reimport_file_with_custom_parameters(path, importer, params);
}
AudioStreamImportSettings::AudioStreamImportSettings() {
get_ok_button()->set_text(TTR("Reimport"));
get_cancel_button()->set_text(TTR("Close"));
VBoxContainer *main_vbox = memnew(VBoxContainer);
add_child(main_vbox);
HBoxContainer *loop_hb = memnew(HBoxContainer);
loop_hb->add_theme_constant_override("separation", 4 * EDSCALE);
loop = memnew(CheckBox);
loop->set_text(TTR("Enable"));
loop->set_tooltip(TTR("Enable looping."));
loop->connect("toggled", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
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(TTR("Loop offset (from beginning). Note that if BPM is set, this setting will be ignored."));
loop_offset->connect("value_changed", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
loop_hb->add_child(loop_offset);
main_vbox->add_margin_child(TTR("Loop:"), loop_hb);
HBoxContainer *interactive_hb = memnew(HBoxContainer);
interactive_hb->add_theme_constant_override("separation", 4 * EDSCALE);
bpm_enabled = memnew(CheckBox);
bpm_enabled->set_text((TTR("BPM:")));
bpm_enabled->connect("toggled", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
interactive_hb->add_child(bpm_enabled);
bpm_edit = memnew(SpinBox);
bpm_edit->set_max(400);
bpm_edit->set_step(0.01);
bpm_edit->set_tooltip(TTR("Configure the Beats Per Measure (tempo) used for the interactive streams.\nThis is required in order to configure beat information."));
bpm_edit->connect("value_changed", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
interactive_hb->add_child(bpm_edit);
interactive_hb->add_spacer();
bar_beats_label = memnew(Label(TTR("Beats/Bar:")));
interactive_hb->add_child(bar_beats_label);
bar_beats_edit = memnew(SpinBox);
bar_beats_edit->set_tooltip(TTR("Configure the Beats Per Bar. This used for music-aware transitions between AudioStreams."));
bar_beats_edit->set_min(2);
bar_beats_edit->set_max(32);
bar_beats_edit->connect("value_changed", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
interactive_hb->add_child(bar_beats_edit);
interactive_hb->add_spacer();
beats_enabled = memnew(CheckBox);
beats_enabled->set_text(TTR("Length (in beats):"));
beats_enabled->connect("toggled", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
interactive_hb->add_child(beats_enabled);
beats_edit = memnew(SpinBox);
beats_edit->set_tooltip(TTR("Configure the amount of Beats used for music-aware looping. If zero, it will be autodetected from the length.\nIt is recommended to set this value (either manually or by clicking on a beat number in the preview) to ensure looping works properly."));
beats_edit->set_max(99999);
beats_edit->connect("value_changed", callable_mp(this, &AudioStreamImportSettings::_settings_changed).unbind(1));
interactive_hb->add_child(beats_edit);
main_vbox->add_margin_child(TTR("Music Playback:"), interactive_hb);
color_rect = memnew(ColorRect);
main_vbox->add_margin_child(TTR("Preview:"), color_rect);
color_rect->set_custom_minimum_size(Size2(600, 200) * EDSCALE);
color_rect->set_v_size_flags(Control::SIZE_EXPAND_FILL);
_player = memnew(AudioStreamPlayer);
_player->connect("finished", callable_mp(this, &AudioStreamImportSettings::_on_finished));
color_rect->add_child(_player);
VBoxContainer *vbox = memnew(VBoxContainer);
vbox->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT, Control::PRESET_MODE_MINSIZE, 0);
color_rect->add_child(vbox);
vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL);
_preview = memnew(ColorRect);
_preview->set_v_size_flags(Control::SIZE_EXPAND_FILL);
_preview->connect("draw", callable_mp(this, &AudioStreamImportSettings::_draw_preview));
_preview->set_v_size_flags(Control::SIZE_EXPAND_FILL);
vbox->add_child(_preview);
HBoxContainer *zoom_hbox = memnew(HBoxContainer);
zoom_bar = memnew(HScrollBar);
zoom_in = memnew(Button);
zoom_in->set_flat(true);
zoom_reset = memnew(Button);
zoom_reset->set_flat(true);
zoom_out = memnew(Button);
zoom_out->set_flat(true);
zoom_hbox->add_child(zoom_bar);
zoom_bar->set_h_size_flags(Control::SIZE_EXPAND_FILL);
zoom_bar->set_v_size_flags(Control::SIZE_EXPAND_FILL);
zoom_hbox->add_child(zoom_out);
zoom_hbox->add_child(zoom_reset);
zoom_hbox->add_child(zoom_in);
zoom_in->connect("pressed", callable_mp(this, &AudioStreamImportSettings::_preview_zoom_in));
zoom_reset->connect("pressed", callable_mp(this, &AudioStreamImportSettings::_preview_zoom_reset));
zoom_out->connect("pressed", callable_mp(this, &AudioStreamImportSettings::_preview_zoom_out));
zoom_bar->connect("value_changed", callable_mp(this, &AudioStreamImportSettings::_preview_zoom_offset_changed));
vbox->add_child(zoom_hbox);
_indicator = memnew(Control);
_indicator->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
_indicator->connect("draw", callable_mp(this, &AudioStreamImportSettings::_draw_indicator));
_indicator->connect("gui_input", callable_mp(this, &AudioStreamImportSettings::_on_input_indicator));
_indicator->connect("mouse_exited", callable_mp(this, &AudioStreamImportSettings::_on_indicator_mouse_exited));
_preview->add_child(_indicator);
HBoxContainer *hbox = memnew(HBoxContainer);
hbox->add_theme_constant_override("separation", 0);
vbox->add_child(hbox);
_play_button = memnew(Button);
_play_button->set_flat(true);
hbox->add_child(_play_button);
_play_button->set_focus_mode(Control::FOCUS_NONE);
_play_button->connect("pressed", callable_mp(this, &AudioStreamImportSettings::_play));
_stop_button = memnew(Button);
_stop_button->set_flat(true);
hbox->add_child(_stop_button);
_stop_button->set_focus_mode(Control::FOCUS_NONE);
_stop_button->connect("pressed", callable_mp(this, &AudioStreamImportSettings::_stop));
_current_label = memnew(Label);
_current_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
_current_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
_current_label->set_modulate(Color(1, 1, 1, 0.5));
hbox->add_child(_current_label);
_duration_label = memnew(Label);
hbox->add_child(_duration_label);
singleton = this;
}

View file

@ -1,5 +1,5 @@
/*************************************************************************/
/* audio_stream_editor_plugin.h */
/* audio_stream_import_settings.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@ -28,17 +28,27 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef AUDIO_STREAM_EDITOR_PLUGIN_H
#define AUDIO_STREAM_EDITOR_PLUGIN_H
#ifndef AUDIO_STREAM_IMPORT_SETTINGS_H
#define AUDIO_STREAM_IMPORT_SETTINGS_H
#include "editor/editor_plugin.h"
#include "scene/audio/audio_stream_player.h"
#include "scene/gui/color_rect.h"
#include "scene/gui/spin_box.h"
#include "scene/resources/texture.h"
class AudioStreamEditor : public ColorRect {
GDCLASS(AudioStreamEditor, ColorRect);
class AudioStreamImportSettings : public ConfirmationDialog {
GDCLASS(AudioStreamImportSettings, ConfirmationDialog);
CheckBox *bpm_enabled = nullptr;
SpinBox *bpm_edit = nullptr;
CheckBox *beats_enabled = nullptr;
SpinBox *beats_edit = nullptr;
Label *bar_beats_label = nullptr;
SpinBox *bar_beats_edit = nullptr;
CheckBox *loop = nullptr;
SpinBox *loop_offset = nullptr;
ColorRect *color_rect = nullptr;
Ref<AudioStream> stream;
AudioStreamPlayer *_player = nullptr;
ColorRect *_preview = nullptr;
@ -46,18 +56,42 @@ class AudioStreamEditor : public ColorRect {
Label *_current_label = nullptr;
Label *_duration_label = nullptr;
HScrollBar *zoom_bar = nullptr;
Button *zoom_in = nullptr;
Button *zoom_reset = nullptr;
Button *zoom_out = nullptr;
Button *_play_button = nullptr;
Button *_stop_button = nullptr;
bool updating_settings = false;
float _current = 0;
bool _dragging = false;
bool _beat_len_dragging = false;
bool _pausing = false;
int _hovering_beat = -1;
HashMap<StringName, Variant> params;
String importer;
String path;
void _audio_changed();
static AudioStreamImportSettings *singleton;
void _settings_changed();
void _reimport();
protected:
void _notification(int p_what);
void _preview_changed(ObjectID p_which);
void _preview_zoom_in();
void _preview_zoom_out();
void _preview_zoom_reset();
void _preview_zoom_offset_changed(double);
void _play();
void _stop();
void _on_finished();
@ -65,27 +99,16 @@ protected:
void _draw_indicator();
void _on_input_indicator(Ref<InputEvent> p_event);
void _seek_to(real_t p_x);
static void _bind_methods();
void _set_beat_len_to(real_t p_x);
void _on_indicator_mouse_exited();
int _get_beat_at_pos(real_t p_x);
public:
void edit(Ref<AudioStream> p_stream);
AudioStreamEditor();
void edit(const String &p_path, const String &p_importer, const Ref<AudioStream> &p_stream);
static AudioStreamImportSettings *get_singleton() { return singleton; }
AudioStreamImportSettings();
};
class AudioStreamEditorPlugin : public EditorPlugin {
GDCLASS(AudioStreamEditorPlugin, EditorPlugin);
AudioStreamEditor *audio_editor = nullptr;
public:
virtual String get_name() const override { return "Audio"; }
bool has_main_screen() const override { return false; }
virtual void edit(Object *p_object) override;
virtual bool handles(Object *p_object) const override;
virtual void make_visible(bool p_visible) override;
AudioStreamEditorPlugin();
~AudioStreamEditorPlugin();
};
#endif // AUDIO_STREAM_EDITOR_PLUGIN_H
#endif // AUDIO_STREAM_IMPORT_SETTINGS_H

View file

@ -1,285 +0,0 @@
/*************************************************************************/
/* audio_stream_editor_plugin.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "audio_stream_editor_plugin.h"
#include "core/config/project_settings.h"
#include "core/io/resource_loader.h"
#include "core/os/keyboard.h"
#include "editor/audio_stream_preview.h"
#include "editor/editor_node.h"
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
void AudioStreamEditor::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
AudioStreamPreviewGenerator::get_singleton()->connect("preview_updated", callable_mp(this, &AudioStreamEditor::_preview_changed));
} break;
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_ENTER_TREE: {
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
_stop_button->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons")));
_preview->set_color(get_theme_color(SNAME("dark_color_2"), SNAME("Editor")));
set_color(get_theme_color(SNAME("dark_color_1"), SNAME("Editor")));
_indicator->update();
_preview->update();
} break;
case NOTIFICATION_PROCESS: {
_current = _player->get_playback_position();
_indicator->update();
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
if (!is_visible_in_tree()) {
_stop();
}
} break;
}
}
void AudioStreamEditor::_draw_preview() {
Rect2 rect = _preview->get_rect();
Size2 size = get_size();
Ref<AudioStreamPreview> preview = AudioStreamPreviewGenerator::get_singleton()->generate_preview(stream);
float preview_len = preview->get_length();
Vector<Vector2> lines;
lines.resize(size.width * 2);
for (int i = 0; i < size.width; i++) {
float ofs = i * preview_len / size.width;
float ofs_n = (i + 1) * preview_len / size.width;
float max = preview->get_max(ofs, ofs_n) * 0.5 + 0.5;
float min = preview->get_min(ofs, ofs_n) * 0.5 + 0.5;
int idx = i;
lines.write[idx * 2 + 0] = Vector2(i + 1, rect.position.y + min * rect.size.y);
lines.write[idx * 2 + 1] = Vector2(i + 1, rect.position.y + max * rect.size.y);
}
Vector<Color> color;
color.push_back(get_theme_color(SNAME("contrast_color_2"), SNAME("Editor")));
RS::get_singleton()->canvas_item_add_multiline(_preview->get_canvas_item(), lines, color);
}
void AudioStreamEditor::_preview_changed(ObjectID p_which) {
if (stream.is_valid() && stream->get_instance_id() == p_which) {
_preview->update();
}
}
void AudioStreamEditor::_audio_changed() {
if (!is_visible()) {
return;
}
update();
}
void AudioStreamEditor::_play() {
if (_player->is_playing()) {
// '_pausing' variable indicates that we want to pause the audio player, not stop it. See '_on_finished()'.
_pausing = true;
_player->stop();
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
set_process(false);
} else {
_player->play(_current);
_play_button->set_icon(get_theme_icon(SNAME("Pause"), SNAME("EditorIcons")));
set_process(true);
}
}
void AudioStreamEditor::_stop() {
_player->stop();
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
_current = 0;
_indicator->update();
set_process(false);
}
void AudioStreamEditor::_on_finished() {
_play_button->set_icon(get_theme_icon(SNAME("MainPlay"), SNAME("EditorIcons")));
if (!_pausing) {
_current = 0;
_indicator->update();
} else {
_pausing = false;
}
set_process(false);
}
void AudioStreamEditor::_draw_indicator() {
if (!stream.is_valid()) {
return;
}
Rect2 rect = _preview->get_rect();
float len = stream->get_length();
float ofs_x = _current / len * rect.size.width;
const Color color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
_indicator->draw_line(Point2(ofs_x, 0), Point2(ofs_x, rect.size.height), color, Math::round(2 * EDSCALE));
_indicator->draw_texture(
get_theme_icon(SNAME("TimelineIndicator"), SNAME("EditorIcons")),
Point2(ofs_x - get_theme_icon(SNAME("TimelineIndicator"), SNAME("EditorIcons"))->get_width() * 0.5, 0),
color);
_current_label->set_text(String::num(_current, 2).pad_decimals(2) + " /");
}
void AudioStreamEditor::_on_input_indicator(Ref<InputEvent> p_event) {
const Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
if (mb->is_pressed()) {
_seek_to(mb->get_position().x);
}
_dragging = mb->is_pressed();
}
const Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid()) {
if (_dragging) {
_seek_to(mm->get_position().x);
}
}
}
void AudioStreamEditor::_seek_to(real_t p_x) {
_current = p_x / _preview->get_rect().size.x * stream->get_length();
_current = CLAMP(_current, 0, stream->get_length());
_player->seek(_current);
_indicator->update();
}
void AudioStreamEditor::edit(Ref<AudioStream> p_stream) {
if (!stream.is_null()) {
stream->disconnect("changed", callable_mp(this, &AudioStreamEditor::_audio_changed));
}
stream = p_stream;
_player->set_stream(stream);
_current = 0;
String text = String::num(stream->get_length(), 2).pad_decimals(2) + "s";
_duration_label->set_text(text);
if (!stream.is_null()) {
stream->connect("changed", callable_mp(this, &AudioStreamEditor::_audio_changed));
update();
} else {
hide();
}
}
void AudioStreamEditor::_bind_methods() {
}
AudioStreamEditor::AudioStreamEditor() {
set_custom_minimum_size(Size2(1, 100) * EDSCALE);
_player = memnew(AudioStreamPlayer);
_player->connect("finished", callable_mp(this, &AudioStreamEditor::_on_finished));
add_child(_player);
VBoxContainer *vbox = memnew(VBoxContainer);
vbox->set_anchors_and_offsets_preset(PRESET_FULL_RECT, PRESET_MODE_MINSIZE, 0);
add_child(vbox);
_preview = memnew(ColorRect);
_preview->set_v_size_flags(SIZE_EXPAND_FILL);
_preview->connect("draw", callable_mp(this, &AudioStreamEditor::_draw_preview));
vbox->add_child(_preview);
_indicator = memnew(Control);
_indicator->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
_indicator->connect("draw", callable_mp(this, &AudioStreamEditor::_draw_indicator));
_indicator->connect("gui_input", callable_mp(this, &AudioStreamEditor::_on_input_indicator));
_preview->add_child(_indicator);
HBoxContainer *hbox = memnew(HBoxContainer);
hbox->add_theme_constant_override("separation", 0);
vbox->add_child(hbox);
_play_button = memnew(Button);
_play_button->set_flat(true);
hbox->add_child(_play_button);
_play_button->set_focus_mode(Control::FOCUS_NONE);
_play_button->connect("pressed", callable_mp(this, &AudioStreamEditor::_play));
_play_button->set_shortcut(ED_SHORTCUT("inspector/audio_preview_play_pause", TTR("Audio Preview Play/Pause"), Key::SPACE));
_stop_button = memnew(Button);
_stop_button->set_flat(true);
hbox->add_child(_stop_button);
_stop_button->set_focus_mode(Control::FOCUS_NONE);
_stop_button->connect("pressed", callable_mp(this, &AudioStreamEditor::_stop));
_current_label = memnew(Label);
_current_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
_current_label->set_h_size_flags(SIZE_EXPAND_FILL);
_current_label->add_theme_font_override("font", EditorNode::get_singleton()->get_gui_base()->get_theme_font(SNAME("status_source"), SNAME("EditorFonts")));
_current_label->add_theme_font_size_override("font_size", EditorNode::get_singleton()->get_gui_base()->get_theme_font_size(SNAME("status_source_size"), SNAME("EditorFonts")));
_current_label->set_modulate(Color(1, 1, 1, 0.5));
hbox->add_child(_current_label);
_duration_label = memnew(Label);
_duration_label->add_theme_font_override("font", EditorNode::get_singleton()->get_gui_base()->get_theme_font(SNAME("status_source"), SNAME("EditorFonts")));
_duration_label->add_theme_font_size_override("font_size", EditorNode::get_singleton()->get_gui_base()->get_theme_font_size(SNAME("status_source_size"), SNAME("EditorFonts")));
hbox->add_child(_duration_label);
}
void AudioStreamEditorPlugin::edit(Object *p_object) {
AudioStream *s = Object::cast_to<AudioStream>(p_object);
if (!s) {
return;
}
audio_editor->edit(Ref<AudioStream>(s));
}
bool AudioStreamEditorPlugin::handles(Object *p_object) const {
return p_object->is_class("AudioStream");
}
void AudioStreamEditorPlugin::make_visible(bool p_visible) {
audio_editor->set_visible(p_visible);
}
AudioStreamEditorPlugin::AudioStreamEditorPlugin() {
audio_editor = memnew(AudioStreamEditor);
add_control_to_container(CONTAINER_PROPERTY_EDITOR_BOTTOM, audio_editor);
audio_editor->hide();
}
AudioStreamEditorPlugin::~AudioStreamEditorPlugin() {
}

View file

@ -599,7 +599,7 @@ Ref<Texture2D> EditorAudioStreamPreviewPlugin::generate(const Ref<Resource> &p_f
uint8_t *imgdata = img.ptrw();
uint8_t *imgw = imgdata;
Ref<AudioStreamPlayback> playback = stream->instance_playback();
Ref<AudioStreamPlayback> playback = stream->instantiate_playback();
ERR_FAIL_COND_V(playback.is_null(), Ref<Texture2D>());
real_t len_s = stream->get_length();

View file

@ -46,6 +46,12 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) {
int frames_mixed_this_step = p_frames;
int beat_length_frames = -1;
bool beat_loop = mp3_stream->has_loop() && mp3_stream->get_bpm() > 0 && mp3_stream->get_beat_count() > 0;
if (beat_loop) {
beat_length_frames = mp3_stream->get_beat_count() * mp3_stream->sample_rate * 60 / mp3_stream->get_bpm();
}
while (todo && active) {
mp3dec_frame_info_t frame_info;
mp3d_sample_t *buf_frame = nullptr;
@ -54,8 +60,25 @@ 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));
loop_fade_remaining++;
}
--todo;
++frames_mixed;
if (beat_loop && (int)frames_mixed >= beat_length_frames) {
for (int i = 0; i < FADE_SIZE; i++) {
samples_mixed = mp3dec_ex_read_frame(mp3d, &buf_frame, &frame_info, mp3_stream->channels);
loop_fade[i] = AudioFrame(buf_frame[0], buf_frame[samples_mixed - 1]);
if (!samples_mixed) {
break;
}
}
loop_fade_remaining = 0;
seek(mp3_stream->loop_offset);
loops++;
}
}
else {
@ -117,6 +140,10 @@ void AudioStreamPlaybackMP3::seek(float p_time) {
mp3dec_ex_seek(mp3d, (uint64_t)frames_mixed * mp3_stream->channels);
}
void AudioStreamPlaybackMP3::tag_used_streams() {
mp3_stream->tag_used(get_playback_position());
}
AudioStreamPlaybackMP3::~AudioStreamPlaybackMP3() {
if (mp3d) {
mp3dec_ex_close(mp3d);
@ -124,7 +151,7 @@ AudioStreamPlaybackMP3::~AudioStreamPlaybackMP3() {
}
}
Ref<AudioStreamPlayback> AudioStreamMP3::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamMP3::instantiate_playback() {
Ref<AudioStreamPlaybackMP3> mp3s;
ERR_FAIL_COND_V_MSG(data.is_empty(), mp3s,
@ -206,6 +233,36 @@ bool AudioStreamMP3::is_monophonic() const {
return false;
}
void AudioStreamMP3::set_bpm(double p_bpm) {
ERR_FAIL_COND(p_bpm < 0);
bpm = p_bpm;
emit_changed();
}
double AudioStreamMP3::get_bpm() const {
return bpm;
}
void AudioStreamMP3::set_beat_count(int p_beat_count) {
ERR_FAIL_COND(p_beat_count < 0);
beat_count = p_beat_count;
emit_changed();
}
int AudioStreamMP3::get_beat_count() const {
return beat_count;
}
void AudioStreamMP3::set_bar_beats(int p_bar_beats) {
ERR_FAIL_COND(p_bar_beats < 0);
bar_beats = p_bar_beats;
emit_changed();
}
int AudioStreamMP3::get_bar_beats() const {
return bar_beats;
}
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);
@ -216,7 +273,19 @@ void AudioStreamMP3::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_loop_offset", "seconds"), &AudioStreamMP3::set_loop_offset);
ClassDB::bind_method(D_METHOD("get_loop_offset"), &AudioStreamMP3::get_loop_offset);
ClassDB::bind_method(D_METHOD("set_bpm", "bpm"), &AudioStreamMP3::set_bpm);
ClassDB::bind_method(D_METHOD("get_bpm"), &AudioStreamMP3::get_bpm);
ClassDB::bind_method(D_METHOD("set_beat_count", "count"), &AudioStreamMP3::set_beat_count);
ClassDB::bind_method(D_METHOD("get_beat_count"), &AudioStreamMP3::get_beat_count);
ClassDB::bind_method(D_METHOD("set_bar_beats", "count"), &AudioStreamMP3::set_bar_beats);
ClassDB::bind_method(D_METHOD("get_bar_beats"), &AudioStreamMP3::get_bar_beats);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), "set_bpm", "get_bpm");
ADD_PROPERTY(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,1,or_greater"), "set_beat_count", "get_beat_count");
ADD_PROPERTY(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,1,or_greater"), "set_bar_beats", "get_bar_beats");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "loop_offset"), "set_loop_offset", "get_loop_offset");
}

View file

@ -41,6 +41,12 @@ class AudioStreamMP3;
class AudioStreamPlaybackMP3 : public AudioStreamPlaybackResampled {
GDCLASS(AudioStreamPlaybackMP3, AudioStreamPlaybackResampled);
enum {
FADE_SIZE = 256
};
AudioFrame loop_fade[FADE_SIZE];
int loop_fade_remaining = FADE_SIZE;
mp3dec_ex_t *mp3d = nullptr;
uint32_t frames_mixed = 0;
bool active = false;
@ -64,6 +70,8 @@ public:
virtual float get_playback_position() const override;
virtual void seek(float p_time) override;
virtual void tag_used_streams() override;
AudioStreamPlaybackMP3() {}
~AudioStreamPlaybackMP3();
};
@ -85,17 +93,30 @@ class AudioStreamMP3 : public AudioStream {
float loop_offset = 0.0;
void clear_data();
double bpm = 0;
int beat_count = 0;
int bar_beats = 4;
protected:
static void _bind_methods();
public:
void set_loop(bool p_enable);
bool has_loop() const;
virtual bool has_loop() const override;
void set_loop_offset(float p_seconds);
float get_loop_offset() const;
virtual Ref<AudioStreamPlayback> instance_playback() override;
void set_bpm(double p_bpm);
virtual double get_bpm() const override;
void set_beat_count(int p_beat_count);
virtual int get_beat_count() const override;
void set_bar_beats(int p_bar_beats);
virtual int get_bar_beats() const override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
void set_data(const Vector<uint8_t> &p_data);

View file

@ -9,6 +9,12 @@
<tutorials>
</tutorials>
<members>
<member name="bar_beats" type="int" setter="set_bar_beats" getter="get_bar_beats" default="4">
</member>
<member name="beat_count" type="int" setter="set_beat_count" getter="get_beat_count" default="0">
</member>
<member name="bpm" type="float" setter="set_bpm" getter="get_bpm" default="0.0">
</member>
<member name="data" type="PackedByteArray" setter="set_data" getter="get_data" default="PackedByteArray()">
Contains the audio data in bytes.
You can load a file without having to import it beforehand using the code snippet below. Keep in mind that this snippet loads the whole file into memory and may not be ideal for huge files (hundreds of megabytes or more).

View file

@ -34,6 +34,10 @@
#include "core/io/resource_saver.h"
#include "scene/resources/texture.h"
#ifdef TOOLS_ENABLED
#include "editor/import/audio_stream_import_settings.h"
#endif
String ResourceImporterMP3::get_importer_name() const {
return "mp3";
}
@ -69,14 +73,26 @@ String ResourceImporterMP3::get_preset_name(int p_idx) const {
void ResourceImporterMP3::get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset) const {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "loop"), true));
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));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,or_greater"), 4));
}
Error ResourceImporterMP3::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
bool loop = p_options["loop"];
float loop_offset = p_options["loop_offset"];
#ifdef TOOLS_ENABLED
bool ResourceImporterMP3::has_advanced_options() const {
return true;
}
void ResourceImporterMP3::show_advanced_options(const String &p_path) {
Ref<AudioStreamMP3> mp3_stream = import_mp3(p_path);
if (mp3_stream.is_valid()) {
AudioStreamImportSettings::get_singleton()->edit(p_path, "mp3", mp3_stream);
}
}
#endif
Ref<FileAccess> f = FileAccess::open(p_source_file, FileAccess::READ);
ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
Ref<AudioStreamMP3> ResourceImporterMP3::import_mp3(const String &p_path) {
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V(f.is_null(), Ref<AudioStreamMP3>());
uint64_t len = f->get_length();
@ -90,9 +106,27 @@ Error ResourceImporterMP3::import(const String &p_source_file, const String &p_s
mp3_stream.instantiate();
mp3_stream->set_data(data);
ERR_FAIL_COND_V(!mp3_stream->get_data().size(), ERR_FILE_CORRUPT);
ERR_FAIL_COND_V(!mp3_stream->get_data().size(), Ref<AudioStreamMP3>());
return mp3_stream;
}
Error ResourceImporterMP3::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
bool loop = p_options["loop"];
float loop_offset = p_options["loop_offset"];
double bpm = p_options["bpm"];
float beat_count = p_options["beat_count"];
float bar_beats = p_options["bar_beats"];
Ref<AudioStreamMP3> mp3_stream = import_mp3(p_source_file);
if (mp3_stream.is_null()) {
return ERR_CANT_OPEN;
}
mp3_stream->set_loop(loop);
mp3_stream->set_loop_offset(loop_offset);
mp3_stream->set_bpm(bpm);
mp3_stream->set_beat_count(beat_count);
mp3_stream->set_bar_beats(bar_beats);
return ResourceSaver::save(p_save_path + ".mp3str", mp3_stream);
}

View file

@ -50,6 +50,12 @@ public:
virtual void get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset = 0) const override;
virtual bool get_option_visibility(const String &p_path, const String &p_option, const HashMap<StringName, Variant> &p_options) const override;
#ifdef TOOLS_ENABLED
virtual bool has_advanced_options() const override;
virtual void show_advanced_options(const String &p_path) override;
#endif
static Ref<AudioStreamMP3> import_mp3(const String &p_path);
virtual Error import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files = nullptr, Variant *r_metadata = nullptr) override;
ResourceImporterMP3();

View file

@ -106,7 +106,7 @@ float OGGPacketSequence::get_length() const {
return granule_pos / sampling_rate;
}
Ref<OGGPacketSequencePlayback> OGGPacketSequence::instance_playback() {
Ref<OGGPacketSequencePlayback> OGGPacketSequence::instantiate_playback() {
Ref<OGGPacketSequencePlayback> playback;
playback.instantiate();
playback->ogg_packet_sequence = Ref<OGGPacketSequence>(this);

View file

@ -86,7 +86,7 @@ public:
// Returns the granule position of the last page in this sequence.
int64_t get_final_granule_pos() const;
Ref<OGGPacketSequencePlayback> instance_playback();
Ref<OGGPacketSequencePlayback> instantiate_playback();
OGGPacketSequence() {}
virtual ~OGGPacketSequence() {}

View file

@ -170,7 +170,7 @@ protected:
static void _bind_methods();
public:
Ref<VideoStreamPlayback> instance_playback() override {
Ref<VideoStreamPlayback> instantiate_playback() override {
Ref<VideoStreamPlaybackTheora> pb = memnew(VideoStreamPlaybackTheora);
pb->set_audio_track(audio_track);
pb->set_file(file);

View file

@ -43,28 +43,93 @@ int AudioStreamPlaybackOGGVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram
int todo = p_frames;
int start_buffer = 0;
int beat_length_frames = -1;
bool beat_loop = vorbis_stream->has_loop();
if (beat_loop && vorbis_stream->get_bpm() > 0 && vorbis_stream->get_beat_count() > 0) {
beat_length_frames = vorbis_stream->get_beat_count() * vorbis_data->get_sampling_rate() * 60 / vorbis_stream->get_bpm();
}
while (todo > 0 && active) {
AudioFrame *buffer = p_buffer;
if (start_buffer > 0) {
buffer = buffer + start_buffer;
buffer += p_frames - todo;
int to_mix = todo;
if (beat_length_frames >= 0 && (beat_length_frames - (int)frames_mixed) < to_mix) {
to_mix = MAX(0, beat_length_frames - (int)frames_mixed);
}
int mixed = _mix_frames_vorbis(buffer, todo);
int mixed = _mix_frames_vorbis(buffer, to_mix);
ERR_FAIL_COND_V(mixed < 0, 0);
todo -= mixed;
frames_mixed += mixed;
start_buffer += mixed;
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));
}
loop_fade_remaining = to_fade;
}
if (beat_length_frames >= 0) {
/**
* Length determined by beat length
* This code is commented out because, in practice, it is prefered that the fade
* is done by the transitioner and this stream just goes on until it ends while fading out.
*
* End fade implementation is left here for reference in case at some point this feature
* is desired.
if (!beat_loop && (int)frames_mixed > beat_length_frames - FADE_SIZE) {
print_line("beat length fade/after mix?");
//No loop, just fade and finish
for (int i = 0; i < mixed; i++) {
int idx = frames_mixed + i - mixed;
buffer[i] *= 1.0 - float(MAX(0, (idx - (beat_length_frames - FADE_SIZE)))) / float(FADE_SIZE);
}
if ((int)frames_mixed == beat_length_frames) {
for (int i = p_frames - todo; i < p_frames; i++) {
p_buffer[i] = AudioFrame(0, 0);
}
active = false;
break;
}
} else
**/
if (beat_loop && beat_length_frames <= (int)frames_mixed) {
// End of file when doing beat-based looping. <= used instead of == because importer editing
if (!have_packets_left && !have_samples_left) {
//Nothing remaining, so do nothing.
loop_fade_remaining = FADE_SIZE;
} else {
// Add some loop fade;
int faded_mix = _mix_frames_vorbis(loop_fade, FADE_SIZE);
for (int i = faded_mix; i < FADE_SIZE; i++) {
// In case lesss was mixed, pad with zeros
loop_fade[i] = AudioFrame(0, 0);
}
loop_fade_remaining = 0;
}
seek(vorbis_stream->loop_offset);
loops++;
// We still have buffer to fill, start from this element in the next iteration.
continue;
}
}
if (!have_packets_left && !have_samples_left) {
//end of file!
// Actual end of file!
bool is_not_empty = mixed > 0 || vorbis_stream->get_length() > 0;
if (vorbis_stream->loop && is_not_empty) {
//loop
seek(vorbis_stream->loop_offset);
loops++;
// we still have buffer to fill, start from this element in the next iteration.
start_buffer = p_frames - todo;
// We still have buffer to fill, start from this element in the next iteration.
} else {
for (int i = p_frames - todo; i < p_frames; i++) {
p_buffer[i] = AudioFrame(0, 0);
@ -130,7 +195,7 @@ bool AudioStreamPlaybackOGGVorbis::_alloc_vorbis() {
comment_is_allocated = true;
ERR_FAIL_COND_V(vorbis_data.is_null(), false);
vorbis_data_playback = vorbis_data->instance_playback();
vorbis_data_playback = vorbis_data->instantiate_playback();
ogg_packet *packet;
int err;
@ -160,6 +225,7 @@ bool AudioStreamPlaybackOGGVorbis::_alloc_vorbis() {
void AudioStreamPlaybackOGGVorbis::start(float p_from_pos) {
ERR_FAIL_COND(!ready);
loop_fade_remaining = FADE_SIZE;
active = true;
seek(p_from_pos);
loops = 0;
@ -182,6 +248,10 @@ float AudioStreamPlaybackOGGVorbis::get_playback_position() const {
return float(frames_mixed) / vorbis_data->get_sampling_rate();
}
void AudioStreamPlaybackOGGVorbis::tag_used_streams() {
vorbis_stream->tag_used(get_playback_position());
}
void AudioStreamPlaybackOGGVorbis::seek(float p_time) {
ERR_FAIL_COND(!ready);
ERR_FAIL_COND(vorbis_stream.is_null());
@ -315,7 +385,7 @@ AudioStreamPlaybackOGGVorbis::~AudioStreamPlaybackOGGVorbis() {
}
}
Ref<AudioStreamPlayback> AudioStreamOGGVorbis::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamOGGVorbis::instantiate_playback() {
Ref<AudioStreamPlaybackOGGVorbis> ovs;
ERR_FAIL_COND_V(packet_sequence.is_null(), nullptr);
@ -347,7 +417,7 @@ void AudioStreamOGGVorbis::maybe_update_info() {
vorbis_info_init(&info);
vorbis_comment_init(&comment);
Ref<OGGPacketSequencePlayback> packet_sequence_playback = packet_sequence->instance_playback();
Ref<OGGPacketSequencePlayback> packet_sequence_playback = packet_sequence->instantiate_playback();
for (int i = 0; i < 3; i++) {
ogg_packet *packet;
@ -405,6 +475,36 @@ float AudioStreamOGGVorbis::get_length() const {
return packet_sequence->get_length();
}
void AudioStreamOGGVorbis::set_bpm(double p_bpm) {
ERR_FAIL_COND(p_bpm < 0);
bpm = p_bpm;
emit_changed();
}
double AudioStreamOGGVorbis::get_bpm() const {
return bpm;
}
void AudioStreamOGGVorbis::set_beat_count(int p_beat_count) {
ERR_FAIL_COND(p_beat_count < 0);
beat_count = p_beat_count;
emit_changed();
}
int AudioStreamOGGVorbis::get_beat_count() const {
return beat_count;
}
void AudioStreamOGGVorbis::set_bar_beats(int p_bar_beats) {
ERR_FAIL_COND(p_bar_beats < 2);
bar_beats = p_bar_beats;
emit_changed();
}
int AudioStreamOGGVorbis::get_bar_beats() const {
return bar_beats;
}
bool AudioStreamOGGVorbis::is_monophonic() const {
return false;
}
@ -419,7 +519,19 @@ void AudioStreamOGGVorbis::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_loop_offset", "seconds"), &AudioStreamOGGVorbis::set_loop_offset);
ClassDB::bind_method(D_METHOD("get_loop_offset"), &AudioStreamOGGVorbis::get_loop_offset);
ClassDB::bind_method(D_METHOD("set_bpm", "bpm"), &AudioStreamOGGVorbis::set_bpm);
ClassDB::bind_method(D_METHOD("get_bpm"), &AudioStreamOGGVorbis::get_bpm);
ClassDB::bind_method(D_METHOD("set_beat_count", "count"), &AudioStreamOGGVorbis::set_beat_count);
ClassDB::bind_method(D_METHOD("get_beat_count"), &AudioStreamOGGVorbis::get_beat_count);
ClassDB::bind_method(D_METHOD("set_bar_beats", "count"), &AudioStreamOGGVorbis::set_bar_beats);
ClassDB::bind_method(D_METHOD("get_bar_beats"), &AudioStreamOGGVorbis::get_bar_beats);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "packet_sequence", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_packet_sequence", "get_packet_sequence");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bpm", PROPERTY_HINT_RANGE, "0,400,0.01,or_greater"), "set_bpm", "get_bpm");
ADD_PROPERTY(PropertyInfo(Variant::INT, "beat_count", PROPERTY_HINT_RANGE, "0,512,1,or_greater"), "set_beat_count", "get_beat_count");
ADD_PROPERTY(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,1,or_greater"), "set_bar_beats", "get_bar_beats");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "loop_offset"), "set_loop_offset", "get_loop_offset");
}

View file

@ -45,6 +45,12 @@ class AudioStreamPlaybackOGGVorbis : public AudioStreamPlaybackResampled {
bool active = false;
int loops = 0;
enum {
FADE_SIZE = 256
};
AudioFrame loop_fade[FADE_SIZE];
int loop_fade_remaining = FADE_SIZE;
vorbis_info info;
vorbis_comment comment;
vorbis_dsp_state dsp_state;
@ -66,6 +72,7 @@ class AudioStreamPlaybackOGGVorbis : public AudioStreamPlaybackResampled {
Ref<OGGPacketSequencePlayback> vorbis_data_playback;
Ref<AudioStreamOGGVorbis> vorbis_stream;
int _mix_frames(AudioFrame *p_buffer, int p_frames);
int _mix_frames_vorbis(AudioFrame *p_buffer, int p_frames);
// Allocates vorbis data structures. Returns true upon success, false on failure.
@ -85,6 +92,8 @@ public:
virtual float get_playback_position() const override;
virtual void seek(float p_time) override;
virtual void tag_used_streams() override;
AudioStreamPlaybackOGGVorbis() {}
~AudioStreamPlaybackOGGVorbis();
};
@ -107,17 +116,30 @@ class AudioStreamOGGVorbis : public AudioStream {
Ref<OGGPacketSequence> packet_sequence;
double bpm = 0;
int beat_count = 0;
int bar_beats = 4;
protected:
static void _bind_methods();
public:
void set_loop(bool p_enable);
bool has_loop() const;
virtual bool has_loop() const override;
void set_loop_offset(float p_seconds);
float get_loop_offset() const;
virtual Ref<AudioStreamPlayback> instance_playback() override;
void set_bpm(double p_bpm);
virtual double get_bpm() const override;
void set_beat_count(int p_beat_count);
virtual int get_beat_count() const override;
void set_bar_beats(int p_bar_beats);
virtual int get_bar_beats() const override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
void set_packet_sequence(Ref<OGGPacketSequence> p_packet_sequence);

View file

@ -7,6 +7,12 @@
<tutorials>
</tutorials>
<members>
<member name="bar_beats" type="int" setter="set_bar_beats" getter="get_bar_beats" default="4">
</member>
<member name="beat_count" type="int" setter="set_beat_count" getter="get_beat_count" default="0">
</member>
<member name="bpm" type="float" setter="set_bpm" getter="get_bpm" default="0.0">
</member>
<member name="loop" type="bool" setter="set_loop" getter="has_loop" default="false">
If [code]true[/code], the stream will automatically loop when it reaches the end.
</member>

View file

@ -30,13 +30,16 @@
#include "resource_importer_ogg_vorbis.h"
#include "audio_stream_ogg_vorbis.h"
#include "core/io/file_access.h"
#include "core/io/resource_saver.h"
#include "scene/resources/texture.h"
#include "thirdparty/libogg/ogg/ogg.h"
#include "thirdparty/libvorbis/vorbis/codec.h"
#ifdef TOOLS_ENABLED
#include "editor/import/audio_stream_import_settings.h"
#endif
String ResourceImporterOGGVorbis::get_importer_name() const {
return "oggvorbisstr";
}
@ -72,14 +75,14 @@ String ResourceImporterOGGVorbis::get_preset_name(int p_idx) const {
void ResourceImporterOGGVorbis::get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset) const {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "loop"), true));
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));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "bar_beats", PROPERTY_HINT_RANGE, "2,32,or_greater"), 4));
}
Error ResourceImporterOGGVorbis::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
bool loop = p_options["loop"];
float loop_offset = p_options["loop_offset"];
Ref<FileAccess> f = FileAccess::open(p_source_file, FileAccess::READ);
ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_OPEN, "Cannot open file '" + p_source_file + "'.");
Ref<AudioStreamOGGVorbis> ResourceImporterOGGVorbis::import_ogg_vorbis(const String &p_path) {
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(f.is_null(), Ref<AudioStreamOGGVorbis>(), "Cannot open file '" + p_path + "'.");
uint64_t len = f->get_length();
@ -107,16 +110,16 @@ Error ResourceImporterOGGVorbis::import(const String &p_source_file, const Strin
size_t packet_count = 0;
bool done = false;
while (!done) {
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Error::ERR_INVALID_DATA, "Ogg sync error " + itos(err));
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Ref<AudioStreamOGGVorbis>(), "Ogg sync error " + itos(err));
while (ogg_sync_pageout(&sync_state, &page) != 1) {
if (cursor >= len) {
done = true;
break;
}
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Error::ERR_INVALID_DATA, "Ogg sync error " + itos(err));
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Ref<AudioStreamOGGVorbis>(), "Ogg sync error " + itos(err));
char *sync_buf = ogg_sync_buffer(&sync_state, OGG_SYNC_BUFFER_SIZE);
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Error::ERR_INVALID_DATA, "Ogg sync error " + itos(err));
ERR_FAIL_COND_V(cursor > len, Error::ERR_INVALID_DATA);
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Ref<AudioStreamOGGVorbis>(), "Ogg sync error " + itos(err));
ERR_FAIL_COND_V(cursor > len, Ref<AudioStreamOGGVorbis>());
size_t copy_size = len - cursor;
if (copy_size > OGG_SYNC_BUFFER_SIZE) {
copy_size = OGG_SYNC_BUFFER_SIZE;
@ -124,22 +127,22 @@ Error ResourceImporterOGGVorbis::import(const String &p_source_file, const Strin
memcpy(sync_buf, &file_data[cursor], copy_size);
ogg_sync_wrote(&sync_state, copy_size);
cursor += copy_size;
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Error::ERR_INVALID_DATA, "Ogg sync error " + itos(err));
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Ref<AudioStreamOGGVorbis>(), "Ogg sync error " + itos(err));
}
if (done) {
break;
}
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Error::ERR_INVALID_DATA, "Ogg sync error " + itos(err));
ERR_FAIL_COND_V_MSG((err = ogg_sync_check(&sync_state)), Ref<AudioStreamOGGVorbis>(), "Ogg sync error " + itos(err));
// Have a page now.
if (!initialized_stream) {
if (ogg_stream_init(&stream_state, ogg_page_serialno(&page))) {
ERR_FAIL_V_MSG(Error::ERR_OUT_OF_MEMORY, "Failed allocating memory for OGG Vorbis stream.");
ERR_FAIL_V_MSG(Ref<AudioStreamOGGVorbis>(), "Failed allocating memory for OGG Vorbis stream.");
}
initialized_stream = true;
}
ogg_stream_pagein(&stream_state, &page);
ERR_FAIL_COND_V_MSG((err = ogg_stream_check(&stream_state)), Error::ERR_INVALID_DATA, "Ogg stream error " + itos(err));
ERR_FAIL_COND_V_MSG((err = ogg_stream_check(&stream_state)), Ref<AudioStreamOGGVorbis>(), "Ogg stream error " + itos(err));
int desync_iters = 0;
Vector<Vector<uint8_t>> packet_data;
@ -150,7 +153,7 @@ Error ResourceImporterOGGVorbis::import(const String &p_source_file, const Strin
if (err == -1) {
// According to the docs this is usually recoverable, but don't sit here spinning forever.
desync_iters++;
ERR_FAIL_COND_V_MSG(desync_iters > 100, Error::ERR_INVALID_DATA, "Packet sync issue during ogg import");
ERR_FAIL_COND_V_MSG(desync_iters > 100, Ref<AudioStreamOGGVorbis>(), "Packet sync issue during ogg import");
continue;
} else if (err == 0) {
// Not enough data to fully reconstruct a packet. Go on to the next page.
@ -183,12 +186,45 @@ Error ResourceImporterOGGVorbis::import(const String &p_source_file, const Strin
ogg_sync_clear(&sync_state);
if (ogg_packet_sequence->get_packet_granule_positions().is_empty()) {
ERR_FAIL_V_MSG(Error::ERR_FILE_CORRUPT, "OGG Vorbis decoding failed. Check that your data is a valid OGG Vorbis audio stream.");
ERR_FAIL_V_MSG(Ref<AudioStreamOGGVorbis>(), "OGG Vorbis decoding failed. Check that your data is a valid OGG Vorbis audio stream.");
}
ogg_vorbis_stream->set_packet_sequence(ogg_packet_sequence);
return ogg_vorbis_stream;
}
#ifdef TOOLS_ENABLED
bool ResourceImporterOGGVorbis::has_advanced_options() const {
return true;
}
void ResourceImporterOGGVorbis::show_advanced_options(const String &p_path) {
Ref<AudioStreamOGGVorbis> ogg_stream = import_ogg_vorbis(p_path);
if (ogg_stream.is_valid()) {
AudioStreamImportSettings::get_singleton()->edit(p_path, "oggvorbisstr", ogg_stream);
}
}
#endif
Error ResourceImporterOGGVorbis::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
bool loop = p_options["loop"];
float loop_offset = p_options["loop_offset"];
double bpm = p_options["bpm"];
int beat_count = p_options["beat_count"];
int bar_beats = p_options["bar_beats"];
Ref<AudioStreamOGGVorbis> ogg_vorbis_stream = import_ogg_vorbis(p_source_file);
if (ogg_vorbis_stream.is_null()) {
return ERR_CANT_OPEN;
}
ogg_vorbis_stream->set_loop(loop);
ogg_vorbis_stream->set_loop_offset(loop_offset);
ogg_vorbis_stream->set_bpm(bpm);
ogg_vorbis_stream->set_beat_count(beat_count);
ogg_vorbis_stream->set_bar_beats(bar_beats);
return ResourceSaver::save(p_save_path + ".oggvorbisstr", ogg_vorbis_stream);
}

View file

@ -31,6 +31,7 @@
#ifndef RESOURCE_IMPORTER_OGG_VORBIS_H
#define RESOURCE_IMPORTER_OGG_VORBIS_H
#include "audio_stream_ogg_vorbis.h"
#include "core/io/resource_importer.h"
class ResourceImporterOGGVorbis : public ResourceImporter {
@ -43,7 +44,13 @@ class ResourceImporterOGGVorbis : public ResourceImporter {
private:
// virtual int get_samples_in_packet(Vector<uint8_t> p_packet) = 0;
static Ref<AudioStreamOGGVorbis> import_ogg_vorbis(const String &p_path);
public:
#ifdef TOOLS_ENABLED
virtual bool has_advanced_options() const override;
virtual void show_advanced_options(const String &p_path) override;
#endif
virtual void get_recognized_extensions(List<String> *p_extensions) const override;
virtual String get_save_extension() const override;
virtual String get_resource_type() const override;

View file

@ -69,7 +69,7 @@ void AudioStreamPlayer2D::_notification(int p_what) {
if (setplay.get() >= 0 && stream.is_valid()) {
active.set();
Ref<AudioStreamPlayback> new_playback = stream->instance_playback();
Ref<AudioStreamPlayback> new_playback = stream->instantiate_playback();
ERR_FAIL_COND_MSG(new_playback.is_null(), "Failed to instantiate playback.");
AudioServer::get_singleton()->start_playback_stream(new_playback, _get_actual_bus(), volume_vector, setplay.get(), pitch_scale);
stream_playbacks.push_back(new_playback);

View file

@ -281,7 +281,7 @@ void AudioStreamPlayer3D::_notification(int p_what) {
if (setplay.get() >= 0 && stream.is_valid()) {
active.set();
Ref<AudioStreamPlayback> new_playback = stream->instance_playback();
Ref<AudioStreamPlayback> new_playback = stream->instantiate_playback();
ERR_FAIL_COND_MSG(new_playback.is_null(), "Failed to instantiate playback.");
HashMap<StringName, Vector<AudioFrame>> bus_map;
bus_map[_get_actual_bus()] = volume_vector;

View file

@ -136,7 +136,7 @@ void AudioStreamPlayer::play(float p_from_pos) {
if (stream->is_monophonic() && is_playing()) {
stop();
}
Ref<AudioStreamPlayback> stream_playback = stream->instance_playback();
Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback();
ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback.");
AudioServer::get_singleton()->start_playback_stream(stream_playback, bus, _get_volume_vector(), p_from_pos, pitch_scale);

View file

@ -225,7 +225,7 @@ void VideoStreamPlayer::set_stream(const Ref<VideoStream> &p_stream) {
stream = p_stream;
if (stream.is_valid()) {
stream->set_audio_track(audio_track);
playback = stream->instance_playback();
playback = stream->instantiate_playback();
} else {
playback = Ref<VideoStreamPlayback>();
}

View file

@ -406,6 +406,10 @@ int AudioStreamPlaybackSample::mix(AudioFrame *p_buffer, float p_rate_scale, int
return p_frames;
}
void AudioStreamPlaybackSample::tag_used_streams() {
base->tag_used(get_playback_position());
}
AudioStreamPlaybackSample::AudioStreamPlaybackSample() {}
/////////////////////
@ -599,7 +603,7 @@ Error AudioStreamSample::save_to_wav(const String &p_path) {
return OK;
}
Ref<AudioStreamPlayback> AudioStreamSample::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamSample::instantiate_playback() {
Ref<AudioStreamPlaybackSample> sample;
sample.instantiate();
sample->base = Ref<AudioStreamSample>(this);

View file

@ -75,6 +75,8 @@ public:
virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
virtual void tag_used_streams() override;
AudioStreamPlaybackSample();
};
@ -144,7 +146,7 @@ public:
Error save_to_wav(const String &p_path);
virtual Ref<AudioStreamPlayback> instance_playback() override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
AudioStreamSample();

View file

@ -72,7 +72,7 @@ class VideoStream : public Resource {
public:
virtual void set_audio_track(int p_track) = 0;
virtual Ref<VideoStreamPlayback> instance_playback() = 0;
virtual Ref<VideoStreamPlayback> instantiate_playback() = 0;
};
#endif

View file

@ -83,6 +83,10 @@ int AudioStreamPlayback::mix(AudioFrame *p_buffer, float p_rate_scale, int p_fra
return 0;
}
void AudioStreamPlayback::tag_used_streams() {
GDVIRTUAL_CALL(_tag_used_streams);
}
void AudioStreamPlayback::_bind_methods() {
GDVIRTUAL_BIND(_start, "from_pos")
GDVIRTUAL_BIND(_stop)
@ -91,6 +95,7 @@ void AudioStreamPlayback::_bind_methods() {
GDVIRTUAL_BIND(_get_playback_position)
GDVIRTUAL_BIND(_seek, "position")
GDVIRTUAL_BIND(_mix, "buffer", "rate_scale", "frames");
GDVIRTUAL_BIND(_tag_used_streams);
}
//////////////////////////////
@ -187,9 +192,9 @@ int AudioStreamPlaybackResampled::mix(AudioFrame *p_buffer, float p_rate_scale,
////////////////////////////////
Ref<AudioStreamPlayback> AudioStream::instance_playback() {
Ref<AudioStreamPlayback> AudioStream::instantiate_playback() {
Ref<AudioStreamPlayback> ret;
if (GDVIRTUAL_CALL(_instance_playback, ret)) {
if (GDVIRTUAL_CALL(_instantiate_playback, ret)) {
return ret;
}
ERR_FAIL_V_MSG(Ref<AudioStreamPlayback>(), "Method must be implemented!");
@ -218,19 +223,74 @@ bool AudioStream::is_monophonic() const {
return true;
}
double AudioStream::get_bpm() const {
double ret = 0;
if (GDVIRTUAL_CALL(_get_bpm, ret)) {
return ret;
}
return 0;
}
bool AudioStream::has_loop() const {
bool ret = 0;
if (GDVIRTUAL_CALL(_has_loop, ret)) {
return ret;
}
return 0;
}
int AudioStream::get_bar_beats() const {
int ret = 0;
if (GDVIRTUAL_CALL(_get_bar_beats, ret)) {
return ret;
}
return 0;
}
int AudioStream::get_beat_count() const {
int ret = 0;
if (GDVIRTUAL_CALL(_get_beat_count, ret)) {
return ret;
}
return 0;
}
void AudioStream::tag_used(float p_offset) {
if (tagged_frame != AudioServer::get_singleton()->get_mixed_frames()) {
offset_count = 0;
tagged_frame = AudioServer::get_singleton()->get_mixed_frames();
}
if (offset_count < MAX_TAGGED_OFFSETS) {
tagged_offsets[offset_count++] = p_offset;
}
}
uint64_t AudioStream::get_tagged_frame() const {
return tagged_frame;
}
uint32_t AudioStream::get_tagged_frame_count() const {
return offset_count;
}
float AudioStream::get_tagged_frame_offset(int p_index) const {
ERR_FAIL_INDEX_V(p_index, MAX_TAGGED_OFFSETS, 0);
return tagged_offsets[p_index];
}
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("instance_playback"), &AudioStream::instance_playback);
GDVIRTUAL_BIND(_instance_playback);
ClassDB::bind_method(D_METHOD("instantiate_playback"), &AudioStream::instantiate_playback);
GDVIRTUAL_BIND(_instantiate_playback);
GDVIRTUAL_BIND(_get_stream_name);
GDVIRTUAL_BIND(_get_length);
GDVIRTUAL_BIND(_is_monophonic);
GDVIRTUAL_BIND(_get_bpm)
GDVIRTUAL_BIND(_get_beat_count)
}
////////////////////////////////
Ref<AudioStreamPlayback> AudioStreamMicrophone::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamMicrophone::instantiate_playback() {
Ref<AudioStreamPlaybackMicrophone> playback;
playback.instantiate();
@ -363,6 +423,10 @@ void AudioStreamPlaybackMicrophone::seek(float p_time) {
// Can't seek a microphone input
}
void AudioStreamPlaybackMicrophone::tag_used_streams() {
microphone->tag_used(0);
}
AudioStreamPlaybackMicrophone::~AudioStreamPlaybackMicrophone() {
microphone->playbacks.erase(this);
stop();
@ -490,7 +554,7 @@ Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback_random() {
for (PoolEntry &entry : local_pool) {
cumulative_weight += entry.weight;
if (cumulative_weight > chosen_cumulative_weight) {
playback->playback = entry.stream->instance_playback();
playback->playback = entry.stream->instantiate_playback();
last_playback = entry.stream;
break;
}
@ -498,7 +562,7 @@ Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback_random() {
if (playback->playback.is_null()) {
// This indicates a floating point error. Take the last element.
last_playback = local_pool[local_pool.size() - 1].stream;
playback->playback = local_pool.write[local_pool.size() - 1].stream->instance_playback();
playback->playback = local_pool.write[local_pool.size() - 1].stream->instantiate_playback();
}
return playback;
}
@ -532,14 +596,14 @@ Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback_no_repeats() {
cumulative_weight += entry.weight;
if (cumulative_weight > chosen_cumulative_weight) {
last_playback = entry.stream;
playback->playback = entry.stream->instance_playback();
playback->playback = entry.stream->instantiate_playback();
break;
}
}
if (playback->playback.is_null()) {
// This indicates a floating point error. Take the last element.
last_playback = local_pool[local_pool.size() - 1].stream;
playback->playback = local_pool.write[local_pool.size() - 1].stream->instance_playback();
playback->playback = local_pool.write[local_pool.size() - 1].stream->instantiate_playback();
}
return playback;
}
@ -568,7 +632,7 @@ Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback_sequential() {
for (Ref<AudioStream> &entry : local_pool) {
if (found_last_stream) {
last_playback = entry;
playback->playback = entry->instance_playback();
playback->playback = entry->instantiate_playback();
break;
}
if (entry == last_playback) {
@ -578,12 +642,12 @@ Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback_sequential() {
if (playback->playback.is_null()) {
// Wrap around
last_playback = local_pool[0];
playback->playback = local_pool.write[0]->instance_playback();
playback->playback = local_pool.write[0]->instantiate_playback();
}
return playback;
}
Ref<AudioStreamPlayback> AudioStreamRandomizer::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamRandomizer::instantiate_playback() {
switch (playback_mode) {
case PLAYBACK_RANDOM:
return instance_playback_random();
@ -762,6 +826,14 @@ void AudioStreamPlaybackRandomizer::seek(float p_time) {
}
}
void AudioStreamPlaybackRandomizer::tag_used_streams() {
Ref<AudioStreamPlayback> p = playing; // Thread safety
if (p.is_valid()) {
p->tag_used_streams();
}
randomizer->tag_used(0);
}
int AudioStreamPlaybackRandomizer::mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) {
if (playing.is_valid()) {
return playing->mix(p_buffer, p_rate_scale * pitch_scale, p_frames);

View file

@ -40,6 +40,8 @@
#include "core/object/script_language.h"
#include "core/variant/native_ptr.h"
class AudioStream;
class AudioStreamPlayback : public RefCounted {
GDCLASS(AudioStreamPlayback, RefCounted);
@ -52,6 +54,7 @@ protected:
GDVIRTUAL0RC(float, _get_playback_position)
GDVIRTUAL1(_seek, float)
GDVIRTUAL3R(int, _mix, GDNativePtr<AudioFrame>, float, int)
GDVIRTUAL0(_tag_used_streams)
public:
virtual void start(float p_from_pos = 0.0);
virtual void stop();
@ -62,6 +65,8 @@ public:
virtual float get_playback_position() const;
virtual void seek(float p_time);
virtual void tag_used_streams();
virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames);
};
@ -72,7 +77,7 @@ class AudioStreamPlaybackResampled : public AudioStreamPlayback {
FP_BITS = 16, //fixed point used for resampling
FP_LEN = (1 << FP_BITS),
FP_MASK = FP_LEN - 1,
INTERNAL_BUFFER_LEN = 256,
INTERNAL_BUFFER_LEN = 128, // 128 warrants 3ms positional jitter at much at 44100hz
CUBIC_INTERP_HISTORY = 4
};
@ -101,20 +106,42 @@ class AudioStream : public Resource {
GDCLASS(AudioStream, Resource);
OBJ_SAVE_TYPE(AudioStream); // Saves derived classes with common type so they can be interchanged.
enum {
MAX_TAGGED_OFFSETS = 8
};
uint64_t tagged_frame = 0;
uint64_t offset_count = 0;
float tagged_offsets[MAX_TAGGED_OFFSETS];
protected:
static void _bind_methods();
GDVIRTUAL0RC(Ref<AudioStreamPlayback>, _instance_playback)
GDVIRTUAL0RC(Ref<AudioStreamPlayback>, _instantiate_playback)
GDVIRTUAL0RC(String, _get_stream_name)
GDVIRTUAL0RC(float, _get_length)
GDVIRTUAL0RC(bool, _is_monophonic)
GDVIRTUAL0RC(double, _get_bpm)
GDVIRTUAL0RC(bool, _has_loop)
GDVIRTUAL0RC(int, _get_bar_beats)
GDVIRTUAL0RC(int, _get_beat_count)
public:
virtual Ref<AudioStreamPlayback> instance_playback();
virtual Ref<AudioStreamPlayback> instantiate_playback();
virtual String get_stream_name() const;
virtual double get_bpm() const;
virtual bool has_loop() const;
virtual int get_bar_beats() const;
virtual int get_beat_count() const;
virtual float get_length() const;
virtual bool is_monophonic() const;
void tag_used(float p_offset);
uint64_t get_tagged_frame() const;
uint32_t get_tagged_frame_count() const;
float get_tagged_frame_offset(int p_index) const;
};
// Microphone
@ -131,7 +158,7 @@ protected:
static void _bind_methods();
public:
virtual Ref<AudioStreamPlayback> instance_playback() override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
virtual float get_length() const override; //if supported, otherwise return 0
@ -153,6 +180,7 @@ class AudioStreamPlaybackMicrophone : public AudioStreamPlaybackResampled {
protected:
virtual int _mix_internal(AudioFrame *p_buffer, int p_frames) override;
virtual float get_stream_sampling_rate() override;
virtual float get_playback_position() const override;
public:
virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
@ -163,9 +191,10 @@ public:
virtual int get_loop_count() const override; //times it looped
virtual float get_playback_position() const override;
virtual void seek(float p_time) override;
virtual void tag_used_streams() override;
~AudioStreamPlaybackMicrophone();
AudioStreamPlaybackMicrophone();
};
@ -233,7 +262,7 @@ public:
void set_playback_mode(PlaybackMode p_playback_mode);
PlaybackMode get_playback_mode() const;
virtual Ref<AudioStreamPlayback> instance_playback() override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
virtual float get_length() const override; //if supported, otherwise return 0
@ -265,6 +294,8 @@ public:
virtual int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) override;
virtual void tag_used_streams() override;
~AudioStreamPlaybackRandomizer();
};

View file

@ -46,7 +46,7 @@ float AudioStreamGenerator::get_buffer_length() const {
return buffer_len;
}
Ref<AudioStreamPlayback> AudioStreamGenerator::instance_playback() {
Ref<AudioStreamPlayback> AudioStreamGenerator::instantiate_playback() {
Ref<AudioStreamGeneratorPlayback> playback;
playback.instantiate();
playback->generator = this;
@ -196,6 +196,10 @@ void AudioStreamGeneratorPlayback::seek(float p_time) {
//no seek possible
}
void AudioStreamGeneratorPlayback::tag_used_streams() {
generator->tag_used(0);
}
void AudioStreamGeneratorPlayback::_bind_methods() {
ClassDB::bind_method(D_METHOD("push_frame", "frame"), &AudioStreamGeneratorPlayback::push_frame);
ClassDB::bind_method(D_METHOD("can_push_buffer", "amount"), &AudioStreamGeneratorPlayback::can_push_buffer);

View file

@ -50,7 +50,7 @@ public:
void set_buffer_length(float p_seconds);
float get_buffer_length() const;
virtual Ref<AudioStreamPlayback> instance_playback() override;
virtual Ref<AudioStreamPlayback> instantiate_playback() override;
virtual String get_stream_name() const override;
virtual float get_length() const override;
@ -89,6 +89,8 @@ public:
int get_frames_available() const;
int get_skips() const;
virtual void tag_used_streams() override;
void clear_buffer();
AudioStreamGeneratorPlayback();

View file

@ -350,6 +350,10 @@ void AudioServer::_mix_step() {
// Mix the audio stream
unsigned int mixed_frames = playback->stream_playback->mix(&buf[LOOKAHEAD_BUFFER_SIZE], playback->pitch_scale.get(), buffer_size);
if (tag_used_audio_streams && playback->stream_playback->is_playing()) {
playback->stream_playback->tag_used_streams();
}
if (mixed_frames != buffer_size) {
// We know we have at least the size of our lookahead buffer for fade-out purposes.
@ -1312,6 +1316,10 @@ uint64_t AudioServer::get_mix_count() const {
return mix_count;
}
uint64_t AudioServer::get_mixed_frames() const {
return mix_frames;
}
void AudioServer::notify_listener_changed() {
for (CallbackItem *ci : listener_changed_callback_list) {
ci->callback(ci->userdata);
@ -1653,6 +1661,10 @@ void AudioServer::capture_set_device(const String &p_name) {
AudioDriver::get_singleton()->capture_set_device(p_name);
}
void AudioServer::set_enable_tagging_used_audio_streams(bool p_enable) {
tag_used_audio_streams = p_enable;
}
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);
@ -1719,6 +1731,8 @@ void AudioServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_bus_layout", "bus_layout"), &AudioServer::set_bus_layout);
ClassDB::bind_method(D_METHOD("generate_bus_layout"), &AudioServer::generate_bus_layout);
ClassDB::bind_method(D_METHOD("set_enable_tagging_used_audio_streams", "enable"), &AudioServer::set_enable_tagging_used_audio_streams);
ADD_PROPERTY(PropertyInfo(Variant::INT, "bus_count"), "set_bus_count", "get_bus_count");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "device"), "set_device", "get_device");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "capture_device"), "capture_set_device", "capture_get_device");

View file

@ -187,6 +187,8 @@ private:
float playback_speed_scale = 1.0f;
bool tag_used_audio_streams = false;
struct Bus {
StringName name;
bool solo = false;
@ -380,6 +382,7 @@ public:
bool is_playback_paused(Ref<AudioStreamPlayback> p_playback);
uint64_t get_mix_count() const;
uint64_t get_mixed_frames() const;
void notify_listener_changed();
@ -424,6 +427,8 @@ public:
String capture_get_device();
void capture_set_device(const String &p_name);
void set_enable_tagging_used_audio_streams(bool p_enable);
AudioServer();
virtual ~AudioServer();
};