From fd2c216396ae0ed6b71ec709e55a64850c219d25 Mon Sep 17 00:00:00 2001 From: Quentin Quadrat Date: Sun, 28 Jan 2024 21:34:59 +0100 Subject: [PATCH] Add routing CEF audio to Godot #42 #31 Update 2d with AudioStreamPlayer2D and 3d demos with AudioStreamPlayer3D --- AUTHORS | 1 + addons/gdcef/demos/2D/CEF.gd | 40 +++++++- addons/gdcef/demos/2D/CEF.tscn | 13 ++- addons/gdcef/demos/2D/default_bus_layout.tres | 8 ++ addons/gdcef/demos/3D/CEF.tscn | 4 +- addons/gdcef/demos/3D/GUI.gd | 20 +++- addons/gdcef/demos/3D/gui_in_3d.tscn | 13 ++- addons/gdcef/demos/3D/project.godot | 2 +- addons/gdcef/gdcef/src/gdbrowser.cpp | 36 +++++++- addons/gdcef/gdcef/src/gdbrowser.hpp | 92 ++++++++++++++++++- 10 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 addons/gdcef/demos/2D/default_bus_layout.tres diff --git a/AUTHORS b/AUTHORS index a1588b5..ebba5d7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,3 +8,4 @@ Contributors: Raphipod (docs, fixes, Windows) Daniel Sanche (javascript injection, fixes) BlayTheNinth (portage to Godot 4.2) +pixaline (Routing audio) diff --git a/addons/gdcef/demos/2D/CEF.gd b/addons/gdcef/demos/2D/CEF.gd index 8d27d81..40f4621 100644 --- a/addons/gdcef/demos/2D/CEF.gd +++ b/addons/gdcef/demos/2D/CEF.gd @@ -8,7 +8,8 @@ extends Control # Default pages const DEFAULT_PAGE = "user://default_page.html" -const RADIO_PAGE = "https://www.programmes-radio.com/fr/stream-e8BxeoRhsz9jY9mXXRiFTE/ecouter-KPJK" +const RADIO_PAGE = "http://streaming.radio.co/s9378c22ee/listen" +# "https://www.programmes-radio.com/fr/stream-e8BxeoRhsz9jY9mXXRiFTE/ecouter-KPJK" const HOME_PAGE = "https://github.com/Lecrapouille/gdcef" # The current browser as Godot node @@ -17,6 +18,8 @@ const HOME_PAGE = "https://github.com/Lecrapouille/gdcef" # Memorize if the mouse was pressed @onready var mouse_pressed : bool = false +@onready var playback = null + # ============================================================================== # Callback when a page has ended to load with success (200): we print a message # ============================================================================== @@ -33,6 +36,9 @@ func _on_page_loaded(node): # Display a load error message using a data: URI. # ============================================================================== func _on_page_failed_loading(aborted, msg_err, node): + # FIXME: I dunno why the radio page is considered as canceled by the user + if node.get_url() == RADIO_PAGE: + return var html = "

Failed to load URL " + node.get_url() if aborted: html = html + " aborted by the user!

" @@ -183,9 +189,11 @@ func _on_radio_pressed(): # ============================================================================== # Mute/unmute the sound # ============================================================================== -func _on_Mute_pressed(): - if current_browser != null: - current_browser.set_muted(not current_browser.is_muted()) +func _on_mute_pressed(): + if current_browser == null: + return + current_browser.set_muted($Panel/VBox/HBox2/Mute.button_pressed) + $AudioStreamPlayer2D.stream_paused = $Panel/VBox/HBox2/Mute.button_pressed pass #### @@ -285,6 +293,7 @@ func _ready(): push_error($CEF.get_error()) return print("CEF version: " + $CEF.get_full_version()) + print("You are listening CEF native audio") # Wait one frame for the texture rect to get its size current_browser = await create_browser(HOME_PAGE) @@ -295,3 +304,26 @@ func _ready(): # ============================================================================== func _process(_delta): pass + +# ============================================================================== +# CEF audio will be routed to this Godot stream object. +# ============================================================================== +func _on_routing_audio_pressed(): + if current_browser == null: + return + if $Panel/VBox/HBox2/RoutingAudio.button_pressed: + print("You are listening CEF audio routed to Godot and filtered with reverberation effect") + $AudioStreamPlayer2D.stream = AudioStreamGenerator.new() + $AudioStreamPlayer2D.stream.set_buffer_length(1) + $AudioStreamPlayer2D.playing = true + current_browser.audio_stream = $AudioStreamPlayer2D.get_stream_playback() + else: + print("You are listening CEF native audio") + current_browser.audio_stream = null + current_browser.set_muted(false) + $Panel/VBox/HBox2/Mute.button_pressed = false + # Not necessary, but, I do not know what, to apply the new mode, the user + # shall click on the html halt button and click on the html button. To avoid + # this, we reload the page. + current_browser.reload() + pass diff --git a/addons/gdcef/demos/2D/CEF.tscn b/addons/gdcef/demos/2D/CEF.tscn index 1c798a9..37584e2 100644 --- a/addons/gdcef/demos/2D/CEF.tscn +++ b/addons/gdcef/demos/2D/CEF.tscn @@ -101,9 +101,13 @@ layout_mode = 2 tooltip_text = "Load a page with sound" text = "Radio" -[node name="Mute" type="Button" parent="Panel/VBox/HBox2"] +[node name="RoutingAudio" type="CheckBox" parent="Panel/VBox/HBox2"] +layout_mode = 2 +tooltip_text = "Route CEF audio to Godot" +text = " Audio routing" + +[node name="Mute" type="CheckBox" parent="Panel/VBox/HBox2"] layout_mode = 2 -tooltip_text = "Mute/unmute the sound" text = "Mute sound" [node name="Info2" type="Label" parent="Panel/VBox/HBox2"] @@ -127,6 +131,8 @@ anchor_bottom = 1.0 edit_alpha = false presets_visible = false +[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="."] + [connection signal="pressed" from="Panel/VBox/HBox/New" to="." method="_on_Add_pressed"] [connection signal="pressed" from="Panel/VBox/HBox/Home" to="." method="_on_Home_pressed"] [connection signal="pressed" from="Panel/VBox/HBox/Go" to="." method="_on_go_pressed"] @@ -139,5 +145,6 @@ presets_visible = false [connection signal="resized" from="Panel/VBox/TextureRect" to="." method="_on_texture_rect_resized"] [connection signal="pressed" from="Panel/VBox/HBox2/BGColor" to="." method="_on_BGColor_pressed"] [connection signal="pressed" from="Panel/VBox/HBox2/Radio" to="." method="_on_radio_pressed"] -[connection signal="pressed" from="Panel/VBox/HBox2/Mute" to="." method="_on_Mute_pressed"] +[connection signal="pressed" from="Panel/VBox/HBox2/RoutingAudio" to="." method="_on_routing_audio_pressed"] +[connection signal="pressed" from="Panel/VBox/HBox2/Mute" to="." method="_on_mute_pressed"] [connection signal="color_changed" from="ColorPopup/ColorPicker" to="." method="_on_ColorPicker_color_changed"] diff --git a/addons/gdcef/demos/2D/default_bus_layout.tres b/addons/gdcef/demos/2D/default_bus_layout.tres new file mode 100644 index 0000000..131f49e --- /dev/null +++ b/addons/gdcef/demos/2D/default_bus_layout.tres @@ -0,0 +1,8 @@ +[gd_resource type="AudioBusLayout" load_steps=2 format=3 uid="uid://bebfjoqlbtwr1"] + +[sub_resource type="AudioEffectReverb" id="AudioEffectReverb_ox0vc"] +resource_name = "Reverb" + +[resource] +bus/0/effect/0/effect = SubResource("AudioEffectReverb_ox0vc") +bus/0/effect/0/enabled = true diff --git a/addons/gdcef/demos/3D/CEF.tscn b/addons/gdcef/demos/3D/CEF.tscn index 754dab7..3b29025 100644 --- a/addons/gdcef/demos/3D/CEF.tscn +++ b/addons/gdcef/demos/3D/CEF.tscn @@ -35,7 +35,7 @@ offset_top = 155.0 offset_right = 95.0 offset_bottom = 178.0 mouse_filter = 0 -text = "Hello world!" +text = "Play the radio stream!" [node name="Label2" type="Label" parent="Panel"] layout_mode = 0 @@ -80,6 +80,6 @@ offset_bottom = 26.0 [connection signal="gui_input" from="Panel/TextureRect" to="." method="_on_TextureRect_gui_input"] [connection signal="pressed" from="Panel/Home" to="." method="_on_Home_pressed"] [connection signal="pressed" from="Panel/Prev" to="." method="_on_Prev_pressed"] -[connection signal="pressed" from="Panel/Next" to="." method="_on_Prev_pressed"] [connection signal="pressed" from="Panel/Next" to="." method="_on_Next_pressed"] +[connection signal="pressed" from="Panel/Next" to="." method="_on_Prev_pressed"] [connection signal="text_changed" from="Panel/TextEdit" to="." method="_on_TextEdit_text_changed"] diff --git a/addons/gdcef/demos/3D/GUI.gd b/addons/gdcef/demos/3D/GUI.gd index 7f9d564..dc61440 100644 --- a/addons/gdcef/demos/3D/GUI.gd +++ b/addons/gdcef/demos/3D/GUI.gd @@ -7,7 +7,12 @@ extends Control # Name of the browser -const browser_name = "browser1" +const browser_name = "browser" + +# Page with sound +# "https://www.programmes-radio.com/fr/stream-e8BxeoRhsz9jY9mXXRiFTE/ecouter-KPJK" +const RADIO_URL = "http://streaming.radio.co/s9378c22ee/listen" +const HOME_URL = "https://github.com/Lecrapouille/gdcef" # Memorize if the mouse was pressed @onready var mouse_pressed : bool = false @@ -20,7 +25,7 @@ func _on_Home_pressed(): if browser == null: $Panel/Label.set_text("Failed getting Godot node " + browser_name) return - browser.load_url("https://bitbucket.org/chromiumembedded/cef/wiki/Home") + browser.load_url(HOME_URL) pass # ============================================================================== @@ -161,10 +166,19 @@ func _ready(): # {"image_loading", true} # {"databases", true} # {"webgl", true} - var browser = $CEF.create_browser("https://github.com/Lecrapouille/gdcef", $Panel/TextureRect, {"javascript":true}) + var browser = $CEF.create_browser(RADIO_URL, $Panel/TextureRect, {"javascript":true}) browser.name = browser_name browser.connect("on_page_loaded", _on_page_loaded) browser.connect("on_page_failed_loading", _on_page_failed_loading) + browser.set_zoom_level(0.05) + + # 3D sound + get_tree().get_root().print_tree_pretty() + var player = get_node("/root/GUIin3D/Background/Cube2/AudioStreamPlayer3D") + player.stream = AudioStreamGenerator.new() + player.stream.set_buffer_length(1) + player.playing = true + browser.audio_stream = player.get_stream_playback() pass # ============================================================================== diff --git a/addons/gdcef/demos/3D/gui_in_3d.tscn b/addons/gdcef/demos/3D/gui_in_3d.tscn index 277c39d..f3e12ad 100644 --- a/addons/gdcef/demos/3D/gui_in_3d.tscn +++ b/addons/gdcef/demos/3D/gui_in_3d.tscn @@ -52,7 +52,7 @@ environment = SubResource("Environment_niyks") [node name="GUIPanel3D" parent="." instance=ExtResource("1")] [node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 0.999999, 0, 0, 3) +transform = Transform3D(0.997487, 0, 0.0708434, 0, 1, 0, -0.0708434, 0, 0.997487, 0.372951, 0, 1.79731) fov = 74.0 near = 0.1 @@ -99,3 +99,14 @@ surface_material_override/0 = SubResource("4") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.88761, 2.01326, 0.374871) mesh = SubResource("3") surface_material_override/0 = SubResource("4") + +[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Background/Cube2"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6.09059, 0, 8.36119) +volume_db = 20.0 +emission_angle_enabled = true +emission_angle_degrees = 17.0 + +[node name="Cube3" type="MeshInstance3D" parent="Background"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.62461, 1.46641, -0.69153) +mesh = SubResource("3") +surface_material_override/0 = SubResource("4") diff --git a/addons/gdcef/demos/3D/project.godot b/addons/gdcef/demos/3D/project.godot index 4347b82..17db0b0 100644 --- a/addons/gdcef/demos/3D/project.godot +++ b/addons/gdcef/demos/3D/project.godot @@ -10,7 +10,7 @@ config_version=5 [application] -config/name="GUI in 3D" +config/name="CEF in 3D" config/description="A demo showing a GUI instanced within a 3D scene using viewports, as well as forwarding mouse and keyboard input to the GUI." run/main_scene="res://gui_in_3d.tscn" diff --git a/addons/gdcef/gdcef/src/gdbrowser.cpp b/addons/gdcef/gdcef/src/gdbrowser.cpp index 854a7db..de46ed0 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.cpp +++ b/addons/gdcef/gdcef/src/gdbrowser.cpp @@ -78,6 +78,10 @@ void GDBrowserView::_bind_methods() ClassDB::bind_method(D_METHOD("set_mouse_wheel_horizontal"), &GDBrowserView::mouseWheelHorizontal); ClassDB::bind_method(D_METHOD("set_muted"), &GDBrowserView::mute); ClassDB::bind_method(D_METHOD("is_muted"), &GDBrowserView::muted); + ClassDB::bind_method(D_METHOD("set_audio_stream", "audio"), &GDBrowserView::setAudioStreamer); + ClassDB::bind_method(D_METHOD("get_audio_stream"), &GDBrowserView::getAudioStreamer); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "audio_stream", PROPERTY_HINT_NODE_TYPE, + "AudioStreamGeneratorPlayback"), "set_audio_stream", "get_audio_stream"); ADD_SIGNAL(MethodInfo("on_page_loaded", PropertyInfo(Variant::OBJECT, "node"))); ADD_SIGNAL(MethodInfo("on_page_failed_loading", PropertyInfo(Variant::BOOL, "aborted"), @@ -138,6 +142,7 @@ GDBrowserView::GDBrowserView() BROWSER_DEBUG_VAL("Create Godot texture"); m_impl = new GDBrowserView::Impl(*this); + assert((m_impl != nullptr) && "Failed allocating GDBrowserView"); m_image.instantiate(); m_texture.instantiate(); } @@ -303,7 +308,6 @@ void GDBrowserView::stopLoading() //------------------------------------------------------------------------------ void GDBrowserView::executeJavaScript(godot::String javascript) { - if (m_browser && m_browser->GetMainFrame()) { CefString codeStr; @@ -462,3 +466,33 @@ bool GDBrowserView::muted() return m_browser->GetHost()->IsAudioMuted(); } + +//------------------------------------------------------------------------------ +void GDBrowserView::onAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) +{ + m_impl->m_audio.channels = int(params.channel_layout); +} + +//------------------------------------------------------------------------------ +void GDBrowserView::onAudioStreamPacket(CefRefPtr browser, + const float** data, int frames, int64_t pts) +{ + if ((m_impl == nullptr) || (m_impl->m_audio.streamer == nullptr)) + { + return ; + } + + if ((data == nullptr) || (frames <= 0) || (m_impl->m_audio.channels == -1)) + return; + + auto& streamer = *(m_impl->m_audio.streamer.ptr()); + if (streamer.can_push_buffer(frames)) + { + for (int i = 0; i < frames; i++) + { + streamer.push_frame(godot::Vector2(data[0][i], data[0][i])); + } + } +} diff --git a/addons/gdcef/gdcef/src/gdbrowser.hpp b/addons/gdcef/gdcef/src/gdbrowser.hpp index 35951f8..4e6e755 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.hpp +++ b/addons/gdcef/gdcef/src/gdbrowser.hpp @@ -60,6 +60,7 @@ # include "gd_script.hpp" # include "node.hpp" # include "image_texture.hpp" +# include "audio_stream_generator_playback.hpp" # include "global_constants.hpp" // Chromium Embedded Framework @@ -98,17 +99,33 @@ class GDBrowserView : public godot::Node private: // CEF interfaces + // ************************************************************************* + //! \brief Routing CEF audio to Godot streamer node. + // ************************************************************************* + struct RoutingAudio + { + //! \brief Godot audio streamer + godot::Ref streamer = nullptr; + //! \brief Audio received from CEF + godot::PackedVector2Array buffer; + //! \brief Number of audio channels + int channels = -1; + }; + // ************************************************************************* //! \brief Mandatory since Godot ref counter is conflicting with CEF ref //! counting and therefore we reach with pure virtual destructor called. //! To avoid this we have to create this intermediate class. // ************************************************************************* - class Impl: public CefRenderHandler, + class Impl: public CefClient, + public CefRenderHandler, public CefLoadHandler, - public CefClient + public CefAudioHandler { public: + friend GDBrowserView; + // --------------------------------------------------------------------- //! \brief Pass the owner instance. // --------------------------------------------------------------------- @@ -146,6 +163,16 @@ class GDBrowserView : public godot::Node return this; } + // --------------------------------------------------------------------- + //! \brief Return the handler for audio rendering events. + // --------------------------------------------------------------------- + virtual CefRefPtr GetAudioHandler() override + { + // FIXME this is called once, so we cannot swap modes :( How to do that ? + std::cout << (m_audio.streamer == nullptr ? "GetAudioHandler CEF Audio" : "GetAudioHandler Godot audio") << "\n"; + return m_audio.streamer != nullptr ? this : nullptr; + } + private: // CefRenderHandler interfaces // --------------------------------------------------------------------- @@ -207,9 +234,32 @@ class GDBrowserView : public godot::Node m_owner.onLoadError(browser, frame, errorCode == ERR_ABORTED, errorText); } + private: // CefAudioHandler interfaces + + virtual void OnAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, + int channels) override + { + m_owner.onAudioStreamStarted(browser, params, channels); + } + + virtual void OnAudioStreamPacket(CefRefPtr browser, + const float** data, int frames, + int64_t pts) override + { + m_owner.onAudioStreamPacket(browser, data, frames, pts); + } + + virtual void OnAudioStreamStopped(CefRefPtr browser) override + {} + + virtual void OnAudioStreamError(CefRefPtr browser, const CefString& message) override + {} + private: GDBrowserView& m_owner; + RoutingAudio m_audio; }; public: @@ -445,6 +495,21 @@ class GDBrowserView : public godot::Node //-------------------------------------------------------------------------- bool muted(); + void setAudioStreamer(godot::Ref streamer) + { + if (m_impl != nullptr) + { + m_impl->m_audio.streamer = streamer; + } + } + + godot::Ref getAudioStreamer() + { + if (m_impl == nullptr) + return nullptr; + return m_impl->m_audio.streamer; + } + private: void resize_(int width, int height); @@ -482,6 +547,29 @@ class GDBrowserView : public godot::Node void onLoadError(CefRefPtr browser, CefRefPtr frame, const bool aborted, const CefString& errorText); + // ------------------------------------------------------------------------- + //! \brief Called on a browser audio capture thread when the browser starts + //! streaming audio. OnAudioStreamStopped will always be called after + //! OnAudioStreamStarted; both methods may be called multiple times + //! for the same browser. |params| contains the audio parameters like + //! sample rate and channel layout. |channels| is the number of channels. + // ------------------------------------------------------------------------- + void onAudioStreamStarted(CefRefPtr browser, + const CefAudioParameters& params, int channels); + + // ------------------------------------------------------------------------- + //! \brief Called on the audio stream thread when a PCM packet is received for the + //! stream. |data| is an array representing the raw PCM data as a floating + //! point type, i.e. 4-byte value(s). |frames| is the number of frames in the + //! PCM packet. |pts| is the presentation timestamp (in milliseconds since the + //! Unix Epoch) and represents the time at which the decompressed packet + //! should be presented to the user. Based on |frames| and the + //! |channel_layout| value passed to OnAudioStreamStarted you can calculate + //! the size of the |data| array in bytes. + // ------------------------------------------------------------------------- + void onAudioStreamPacket(CefRefPtr browser, const float** data, + int frames, int64_t pts); + private: //! \brief CEF interface implementation