Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interaction sounds to BaseButton (customizable via theme) #1472

Open
SoloCarryGuy opened this issue Sep 7, 2020 · 24 comments
Open

Add interaction sounds to BaseButton (customizable via theme) #1472

SoloCarryGuy opened this issue Sep 7, 2020 · 24 comments

Comments

@SoloCarryGuy
Copy link

SoloCarryGuy commented Sep 7, 2020

Describe the project you are working on:
I am working on a match 3 game

Describe the problem or limitation you are having in your project:
For all the button presses I have to manually code a sound to be played by creating a global sound manager whenever a button is pressed

Describe the feature / enhancement and how it helps to overcome the problem or limitation:
Add sound to be played on button pressed

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

If this enhancement will not be used often, can it be worked around with a few lines of script?:
I think this is used for every button in every game made.

Is there a reason why this should be core and not an add-on in the asset library?:
Again, because there are always sounds on button presses

@Jummit
Copy link

Jummit commented Sep 7, 2020

Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

https://gamedev.stackexchange.com/questions/184354/add-a-sound-to-all-the-buttons-in-a-project/184363#184363

@Calinou
Copy link
Member

Calinou commented Sep 7, 2020

See also godotengine/godot#3608.

@JOELwindows7
Copy link

JOELwindows7 commented Dec 28, 2020

Sorry, I am a beginner in godot and game development, I don't know how to impplement it.

https://gamedev.stackexchange.com/questions/184354/add-a-sound-to-all-the-buttons-in-a-project/184363#184363

Thancc. cool and good. However though, there is a disadvantage you have to beware:

  • Godot projects workflow are made in various ways. The problem using such method comes at which workflow where nodes will be instantiated and freed.
  • As you can see, everytime a button node appears, each button signal of pressed will be connected to that ButtonSound node on that audio related Singleton as a child.
  • connecting alot of signals of button pressed will gradually kill performance.
  • I think signal connection is automatically removed when the node is going to be freed, idk
  • well let's say we will have 1000 more buttons here.
  • the frame drops severely, because there are 1000 more signal connections. not just a frame drop though, it's really lagging now.
  • once it happened, you have to restart this Godot process all over again to clear application instance and its allocations.

well, I guess, maybe an AudioStreamPlayer in each Button node might be sufficient way for now.
anybody can solve the eventual lag everytime new button node appears for particularly using automatic signal connection method like @Jummit above?

@Jummit
Copy link

Jummit commented Dec 28, 2020

As long as you don't have thousands of buttons instantiated at the same time, there shouldn't be a problem, right?

@JOELwindows7
Copy link

JOELwindows7 commented Dec 28, 2020

As long as you don't have thousands of buttons instantiated at the same time, there shouldn't be a problem, right?

at least it works really well. If the workflow is simply at only one way form of a UI in this node or anything simple and not complicated, this should not be a problem.

but yeah. my game workflow is like above, New nodes that will instantiated & freed. And I have alot.

perhaps by having _on_node_freed method which contains disconnect signal would help.

@Calinou
Copy link
Member

Calinou commented Sep 19, 2021

I worked around this in godot-mdk by adding a custom class that extends Button and instantiating it in my scenes using the editor instead of using Button. Here's a more generic example:

# Save this script, then add MyButton nodes instead of Button using the Create New Node dialog.
extends Button
class_name MyButton


## Called when the button is pressed.
func _pressed():
	# This assumes you have a Sound singleton designed to play sounds with polyphony:
	# https://github.com/Calinou/godot-mdk/blob/master/autoload/sound.gd
	Sound.play(self, preload("res://button_click.wav"))

The _pressed() function is called automatically by Godot when you add it to a node that extends BaseButton. No need to connect it to a signal.

@YuriSizov
Copy link
Contributor

As the theme guy on the team I fully support that controls should have an inherent ability to produce focus and interaction sounds, and that it should be customizable with a theme. It's not too hard to work around with nodes, but it's an unnecessary complication for getting your game that polished feel.

I will gladly work on this.

@YuriSizov YuriSizov changed the title Add a sound property to buttons Add interaction sounds to Button customizable via theme Sep 19, 2021
@Calinou Calinou changed the title Add interaction sounds to Button customizable via theme Add interaction sounds to BaseButton (customizable via theme) Sep 19, 2021
@dalexeev
Copy link
Member

One more option (for 4.0-dev):

extends Button
class_name MyButton

var _allow_focus_sfx := true

func _init() -> void:
    focus_entered.connect(_on_focus_entered)

func _input(event: InputEvent) -> void:
    if !is_visible_in_tree() || !has_focus():
        return

    if event.is_action_pressed('ui_accept'):
        if disabled:
            SFX.play('gui_error.wav')
        else:
            SFX.play('gui_accept.wav')

func grab_focus_no_sfx() -> void:
    _allow_focus_sfx = false
    grab_focus()
    _allow_focus_sfx = true

func _on_focus_entered() -> void:
    if _allow_focus_sfx:
        SFX.play('gui_focus.wav')

@Shadowblitz16
Copy link

Shadowblitz16 commented Apr 18, 2023

@dalexeev that doesn't work for mouse right clicks
infact that doesn't seem to work for alot of things

@Calinou
Copy link
Member

Calinou commented Apr 19, 2023

Note that this will probably require something analogous to SceneTreeTimer/SceneTreeTween to be implemented. A SceneTreeAudioStreamPlayer should allow polyphonic audio playback without creating any nodes, so that using things like get_tree().change_scene_to_packed() does not stop sound playback.

(This is also useful in game logic if you want to avoid the issue where freeing a node stops its audio playback. However, we'd need to add positional variants in this case as well, and you would most likely not be able to change their position after creating them.)

@dalexeev that doesn't work for mouse right clicks infact that doesn't seem to work for alot of things

It should work with right-clicks if the button has the appropriate click mask. Buttons should not play a sound if clicked with a button that doesn't actually press them, so it's expected that by default, right-clicking shouldn't play a sound.

@Shadowblitz16
Copy link

Shadowblitz16 commented Apr 19, 2023

@Calinou it doesn't.
it seems like it has to do with the mouse clicks that don't have a mask that happen after it's already focused.

it only tends to play the focus sound with the mouse.
I also tried changing action mode to press it doesn't change anything

try it

extends Button
class_name MyButton

var _allow_focus_sfx := true

@export var sound_error  : AudioStream
@export var sound_focus  : AudioStream
@export var sound_accept : AudioStream

func _init() -> void:
	focus_entered.connect(_on_focus_entered)

func _input(event: InputEvent) -> void:
	if !is_visible_in_tree() || !has_focus():
		return

	if event.is_action_pressed('ui_accept'):
		if    disabled && sound_error : Sound.play(self, sound_error)
		elif !disabled && sound_accept: Sound.play(self, sound_accept)

func grab_focus_no_sfx() -> void:
	_allow_focus_sfx = false
	grab_focus()
	_allow_focus_sfx = true

func _on_focus_entered() -> void:
	if _allow_focus_sfx && sound_focus:
		Sound.play(self, sound_focus)

@dalexeev
Copy link
Member

@Shadowblitz16 Sorry, I forgot to mention that this class was only implemented for keyboard control. Mouse support requires other changes. For example like this:

Code
extends Button
class_name MyButton

var _allow_focus_sfx := true

func _init() -> void:
    focus_entered.connect(_on_focus_entered)
    button_down.connect(_on_button_down)
    pressed.connect(_on_pressed)

func _gui_input(event: InputEvent) -> void:
    if disabled:
        if event.is_action_pressed("ui_accept") or (
                event is InputEventMouseButton
                        and event.button_index == MOUSE_BUTTON_LEFT
                        and event.is_pressed()):
            SFX.play("gui_error.wav")

func grab_focus_no_sfx() -> void:
    _allow_focus_sfx = false
    grab_focus()
    _allow_focus_sfx = true

func _on_focus_entered() -> void:
    if _allow_focus_sfx and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
        SFX.play("gui_focus.wav")

func _on_button_down() -> void:
    SFX.play("gui_focus.wav")

func _on_pressed() -> void:
    if toggle_mode:
        if button_pressed:
            SFX.play("gui_enabled.wav")
        else:
            SFX.play("gui_disabled.wav")
    else:
        SFX.play("gui_accept.wav")

@Calinou
Copy link
Member

Calinou commented Jul 23, 2023

@YuriSizov How do you envision the audio theme items in terms of structure? I'm asking because I wonder how we could expose a way to set generic "hover", "focus", "pressed" sounds that work across the entire theme, without requiring users to override them on individual classes. However, if users wish to override the interaction sounds on a specific control, they should be able to do so.

Could this work in a manner similar to the Default Font top-level Theme property?

@YuriSizov
Copy link
Contributor

@Calinou I'm not a fan of this idea. The default font works because we can decide to use it as a fallback based on the resource type. What you propose is that we add special resolution logic based on the resource type AND the name of a theme item. We don't really have a very consistent theme item naming convention and it would make the whole thing string-reliant.

So for now I'd focus on implementing the core system, having audio as a theme item type, making it work, figuring out how it must work. The hard part is figuring out how the audio stuff is going to be configured. Should we allow for positional audio or should it be position-less? How do we configure the bus? Would it be a part of some theme audio resource or a setting on the theme resource? Or a global setting per project?

Having some kind of fallback system can be added later, if we truly need it. Way after we ship the initial feature.

@Calinou
Copy link
Member

Calinou commented Jul 25, 2023

7.5 years after originally proposing this feature, I have a working proof of concept: https://github.com/Calinou/godot/tree/add-theme-audio-items
See commit message for technical details.

Testing project: control_gallery.zip
You need to compile the above branch for that project to play audio. Audio might be quite loud by default depending on your setup, so consider turning down your volume before interacting with the UI.

I couldn't figure out how to play audio without creating nodes1, so I added a helper method to SceneTree that creates a temporary top-level internal node (similar to what godotengine/godot#79599 does). The node is automatically freed once audio playback is finished. Multiple nodes may be created at once, which implicitly allows for polyphony (including with different, unrelated AudioStreams).

Using an AudioStreamRandomizer resource should work too, if you want to add some variation to UI sounds. From a performance standpoint, creating nodes is probably not too bad since you're unlikely to have more than 5 active UI audio sources at a given time. (This is especially the case if you stick to WAV, as recommended for short audio files.)

Footnotes

  1. Some commented out code remains in the branch if you'd like to take a look.

@YuriSizov
Copy link
Contributor

Amazing work!

I couldn't figure out how to play audio without creating nodes

Well, that's a bummer. I think this should be resolved first, we need to be able to do the playback directly with the server, at least for the non-positional audio (which is what the UI sfx would likely be). Maybe @ellenhp can help us figure out how feasible it would be to implement?

@Calinou
Copy link
Member

Calinou commented Aug 7, 2023

I made further progress in my branch: https://github.com/Calinou/godot/tree/add-theme-audio-items

August 2023 patch
From dd1e89856eb30261bd9552a5746f8b16dd505e56 Mon Sep 17 00:00:00 2001
From: Hugo Locurcio <[email protected]>
Date: Tue, 25 Jul 2023 03:13:07 +0200
Subject: [PATCH] Add theme audio items

This allows themes to play sounds when hovering/pressing buttons.
A new "audio" theme item type is added to allow custom controls
to define their own audio streams, as well as benefit from the theme
override system.

Playback is handled by adding internal nodes at the top of the SceneTree,
and is not affected by pause or scene changes. Nodes are automatically freed
once playback is done. Polyphony is implicitly supported by creating a node
for each audio playback event.

`get_tree().create_audio_stream_player(stream: AudioStream, bus: StringName, volume_db: float)`
can now be used to play a sound non-positionally, without having to worry
about a parent node being freed and having the sound be cut off early.
This is useful to play one-off sounds such as announcer audio.

TODO:

- Reimplement `focus` audio playback to work with all Control-derived nodes.
  Perhaps also use different theme items for mouse/touch and keyboard/gamepad-induced focus
  (to prevent playing audio twice when clicking a button with mouse/touch).
- Fix BaseButton's `pressed_disabled` never being played as this isn't called
  on disabled buttons. SpinBox also has the same issue.
- Return early in `SceneTree::create_audio_stream_player()` if using a bare
  AudioStream resource (useful to mute specific sounds with theme overrides
  without spamming errors).
- Fix audio theme overrides not appearing in the inspector for Control-derived nodes.
- Make `--doctool` able to see audio theme items.
---
 core/config/project_settings.cpp              |   1 +
 doc/classes/AudioStreamPlayer.xml             |   1 +
 doc/classes/Control.xml                       |  42 ++++
 doc/classes/ProjectSettings.xml               |   4 +
 doc/classes/SceneTree.xml                     |  18 ++
 doc/classes/Theme.xml                         |  64 +++++-
 doc/classes/ThemeDB.xml                       |   3 +
 doc/classes/Window.xml                        |  52 ++++-
 editor/editor_help.cpp                        |   1 +
 editor/plugins/theme_editor_plugin.cpp        | 203 +++++++++++++++-
 editor/plugins/theme_editor_plugin.h          |  11 +
 scene/gui/base_button.cpp                     |   5 +
 scene/gui/control.cpp                         |  90 ++++++++
 scene/gui/control.h                           |   7 +
 scene/gui/item_list.cpp                       |   3 +
 scene/gui/line_edit.cpp                       |   6 +
 scene/gui/popup_menu.cpp                      |   8 +-
 scene/gui/slider.cpp                          |  10 +
 scene/gui/spin_box.cpp                        |   5 +
 scene/gui/tab_bar.cpp                         |  21 ++
 scene/gui/text_edit.cpp                       |   1 +
 scene/gui/tree.cpp                            |   5 +
 scene/main/scene_tree.cpp                     | 148 ++++++++++++
 scene/main/scene_tree.h                       |  28 +++
 scene/main/window.cpp                         |  88 +++++++
 scene/main/window.h                           |   7 +
 .../resources/default_theme/default_theme.cpp |  37 +++
 scene/resources/theme.cpp                     | 217 ++++++++++++++++++
 scene/resources/theme.h                       |  17 ++
 scene/theme/theme_db.cpp                      |  17 ++
 scene/theme/theme_db.h                        |   5 +
 31 files changed, 1117 insertions(+), 8 deletions(-)

diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index 973162a0664..a144d5b748b 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -1302,6 +1302,7 @@ ProjectSettings::ProjectSettings() {
 	GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor", false);
 
 	GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/default_bus_layout", PROPERTY_HINT_FILE, "*.tres"), "res://default_bus_layout.tres");
+	GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "audio/buses/gui_theme_bus"), "Master");
 	GLOBAL_DEF_RST("audio/general/text_to_speech", false);
 	GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/2d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f);
 	GLOBAL_DEF_RST(PropertyInfo(Variant::FLOAT, "audio/general/3d_panning_strength", PROPERTY_HINT_RANGE, "0,2,0.01"), 0.5f);
diff --git a/doc/classes/AudioStreamPlayer.xml b/doc/classes/AudioStreamPlayer.xml
index c48c7f43008..ee1dcc0b957 100644
--- a/doc/classes/AudioStreamPlayer.xml
+++ b/doc/classes/AudioStreamPlayer.xml
@@ -6,6 +6,7 @@
 	<description>
 		Plays an audio stream non-positionally.
 		To play audio positionally, use [AudioStreamPlayer2D] or [AudioStreamPlayer3D] instead of [AudioStreamPlayer].
+		To keep playing audio after freeing the node using the [AudioStreamPlayer] or after changing scenes, use [method SceneTree.create_audio_stream_player] instead.
 	</description>
 	<tutorials>
 		<link title="Audio streams">$DOCS_URL/tutorials/audio/audio_streams.html</link>
diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml
index ee790b69686..03a865750b2 100644
--- a/doc/classes/Control.xml
+++ b/doc/classes/Control.xml
@@ -220,6 +220,15 @@
 				[b]Note:[/b] This does not affect the methods in [Input], only the way events are propagated.
 			</description>
 		</method>
+		<method name="add_theme_audio_override">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="audio" type="AudioStream" />
+			<description>
+				Creates a local override for a theme audio with the specified [param name]. Local overrides always take precedence when fetching theme items for the control. An override can be removed with [method remove_theme_audio_override].
+				See also [method get_theme_audio].
+			</description>
+		</method>
 		<method name="add_theme_color_override">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
@@ -444,6 +453,15 @@
 				[/codeblock]
 			</description>
 		</method>
+		<method name="get_theme_audio" qualifiers="const">
+			<return type="AudioStream" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
+			<description>
+				Returns an audio from the first matching [Theme] in the tree if that [Theme] has an audio item with the specified [param name] and [param theme_type].
+				See [method get_theme_color] for details.
+			</description>
+		</method>
 		<method name="get_theme_color" qualifiers="const">
 			<return type="Color" />
 			<param index="0" name="name" type="StringName" />
@@ -577,6 +595,23 @@
 				Returns [code]true[/code] if this is the current focused control. See [member focus_mode].
 			</description>
 		</method>
+		<method name="has_theme_audio" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
+			<description>
+				Returns [code]true[/code] if there is a matching [Theme] in the tree that has an audio item with the specified [param name] and [param theme_type].
+				See [method get_theme_color] for details.
+			</description>
+		</method>
+		<method name="has_theme_audio_override" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="name" type="StringName" />
+			<description>
+				Returns [code]true[/code] if there is a local override for a theme audio with the specified [param name] in this [Control] node.
+				See [method add_theme_audio_override].
+			</description>
+		</method>
 		<method name="has_theme_color" qualifiers="const">
 			<return type="bool" />
 			<param index="0" name="name" type="StringName" />
@@ -698,6 +733,13 @@
 				Give up the focus. No other control will be able to receive input.
 			</description>
 		</method>
+		<method name="remove_theme_audio_override">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<description>
+				Removes a local override for a theme audio with the specified [param name] previously added by [method add_theme_audio_override] or via the Inspector dock.
+			</description>
+		</method>
 		<method name="remove_theme_color_override">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 3c4b8837f81..f86f2dcebdb 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -359,6 +359,10 @@
 		<member name="audio/buses/default_bus_layout" type="String" setter="" getter="" default="&quot;res://default_bus_layout.tres&quot;">
 			Default [AudioBusLayout] resource file to use in the project, unless overridden by the scene.
 		</member>
+		<member name="audio/buses/gui_theme_bus" type="String" setter="" getter="" default="&quot;Master&quot;">
+			The name of the audio bus to play GUI theme audio in (case-sensitive). All sounds played using [method SceneTree.play_theme_audio] will go through the bus specified in [member audio/buses/gui_theme_bus]. This can be used to put UI sounds in a dedicated audio bus, which allows for adjusting UI volume independently from other sounds in the project.
+			If the specified audio bus doesn't exist, audio will play on the [code]Master[/code] bus instead.
+		</member>
 		<member name="audio/driver/driver" type="String" setter="" getter="">
 			Specifies the audio driver to use. This setting is platform-dependent as each platform supports different audio drivers. If left empty, the default audio driver will be used.
 			The [code]Dummy[/code] audio driver disables all audio playback and recording, which is useful for non-game applications as it reduces CPU usage. It also prevents the engine from appearing as an application playing audio in the OS' audio mixer.
diff --git a/doc/classes/SceneTree.xml b/doc/classes/SceneTree.xml
index c0d98ef921d..80838ef0586 100644
--- a/doc/classes/SceneTree.xml
+++ b/doc/classes/SceneTree.xml
@@ -54,6 +54,16 @@
 				[b]Note:[/b] The new scene node is added to the tree at the end of the frame. You won't be able to access it immediately after the [method change_scene_to_packed] call.
 			</description>
 		</method>
+		<method name="create_audio_stream_player">
+			<return type="AudioStreamPlayer" />
+			<param index="0" name="stream" type="AudioStream" />
+			<param index="1" name="bus" type="StringName" default="&amp;&quot;Master&quot;" />
+			<param index="2" name="volume_db" type="float" default="0.0" />
+			<description>
+				Plays the specified [param stream] on [param bus] with volume offset by [param volume_db] and returns the associated [AudioStreamPlayer] instance, which is automatically freed once the audio is done playing. Audio is played in a non-positional manner. Unlike when using [AudioStreamPlayer] within another node, the audio will not stop if the node that called [method create_audio_stream_player] is freed or if the scene is changed using [method change_scene_to_file] or [method change_scene_to_packed].
+				To play UI sounds in custom [Control]s, use [method play_theme_audio] instead.
+			</description>
+		</method>
 		<method name="create_timer">
 			<return type="SceneTreeTimer" />
 			<param index="0" name="time_sec" type="float" />
@@ -158,6 +168,14 @@
 				[b]Note:[/b] Group call flags are used to control the notification sending behavior. By default, notifications will be sent immediately in a way similar to [method notify_group]. However, if the [constant GROUP_CALL_DEFERRED] flag is present in the [param call_flags] argument, notifications will be sent at the end of the current frame in a way similar to using [code]Object.call_deferred("notification", ...)[/code].
 			</description>
 		</method>
+		<method name="play_theme_audio">
+			<return type="AudioStreamPlayer" />
+			<param index="0" name="stream" type="AudioStream" />
+			<description>
+				Plays the specified [param stream] and returns the [AudioStreamPlayer] instance, which is automatically freed once the audio is done playing. [method play_theme_audio] is intended to be used by custom [Control]s to play sounds on user interaction based on theme items. The bus used for theme audio can be configured using [member ProjectSettings.audio/buses/gui_theme_bus].
+				To play generic audio in a non-positional manner without having to create an [AudioStreamPlayer] node, use [method create_audio_stream_player] instead.
+			</description>
+		</method>
 		<method name="queue_delete">
 			<return type="void" />
 			<param index="0" name="obj" type="Object" />
diff --git a/doc/classes/Theme.xml b/doc/classes/Theme.xml
index eb3c1705835..a3ca4a3f145 100644
--- a/doc/classes/Theme.xml
+++ b/doc/classes/Theme.xml
@@ -27,6 +27,15 @@
 				Removes all the theme properties defined on the theme resource.
 			</description>
 		</method>
+		<method name="clear_audio">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" />
+			<description>
+				Removes the audio property defined by [param name] and [param theme_type], if it exists.
+				Fails if it doesn't exist. Use [method has_audio] to check for existence.
+			</description>
+		</method>
 		<method name="clear_color">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
@@ -99,6 +108,28 @@
 				Unmarks [param theme_type] as being a variation of another theme type. See [method set_type_variation].
 			</description>
 		</method>
+		<method name="get_audio" qualifiers="const">
+			<return type="AudioStream" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" />
+			<description>
+				Returns the audio property defined by [param name] and [param theme_type], if it exists.
+				Returns the engine fallback audio value if the property doesn't exist (see [member ThemeDB.fallback_audio]). Use [method has_audio] to check for existence.
+			</description>
+		</method>
+		<method name="get_audio_list" qualifiers="const">
+			<return type="PackedStringArray" />
+			<param index="0" name="theme_type" type="String" />
+			<description>
+				Returns a list of names for audio properties defined with [param theme_type]. Use [method get_audio_type_list] to get a list of possible theme type names.
+			</description>
+		</method>
+		<method name="get_audio_type_list" qualifiers="const">
+			<return type="PackedStringArray" />
+			<description>
+				Returns a list of all unique theme type names for audio properties. Use [method get_type_list] to get a list of all unique theme types.
+			</description>
+		</method>
 		<method name="get_color" qualifiers="const">
 			<return type="Color" />
 			<param index="0" name="name" type="StringName" />
@@ -281,6 +312,15 @@
 				Returns a list of all type variations for the given [param base_type].
 			</description>
 		</method>
+		<method name="has_audio" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" />
+			<description>
+				Returns [code]true[/code] if the audio property defined by [param name] and [param theme_type] exists.
+				Returns [code]false[/code] if it doesn't exist. Use [method set_audio] to define it.
+			</description>
+		</method>
 		<method name="has_color" qualifiers="const">
 			<return type="bool" />
 			<param index="0" name="name" type="StringName" />
@@ -390,6 +430,16 @@
 				Removes the theme type, gracefully discarding defined theme items. If the type is a variation, this information is also erased. If the type is a base for type variations, those variations lose their base.
 			</description>
 		</method>
+		<method name="rename_audio">
+			<return type="void" />
+			<param index="0" name="old_name" type="StringName" />
+			<param index="1" name="name" type="StringName" />
+			<param index="2" name="theme_type" type="StringName" />
+			<description>
+				Renames the audio property defined by [param old_name] and [param theme_type] to [param name], if it exists.
+				Fails if it doesn't exist, or if a similar property with the new name already exists. Use [method has_audio] to check for existence, and [method clear_audio] to remove the existing property.
+			</description>
+		</method>
 		<method name="rename_color">
 			<return type="void" />
 			<param index="0" name="old_name" type="StringName" />
@@ -462,6 +512,15 @@
 				[b]Note:[/b] This method is analogous to calling the corresponding data type specific method, but can be used for more generalized logic.
 			</description>
 		</method>
+		<method name="set_audio">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" />
+			<param index="2" name="audio" type="AudioStream" />
+			<description>
+				Creates or changes the value of the audio property defined by [param name] and [param theme_type]. Use [method clear_audio] to remove the property.
+			</description>
+		</method>
 		<method name="set_color">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
@@ -573,7 +632,10 @@
 		<constant name="DATA_TYPE_STYLEBOX" value="5" enum="DataType">
 			Theme's [StyleBox] item type.
 		</constant>
-		<constant name="DATA_TYPE_MAX" value="6" enum="DataType">
+		<constant name="DATA_TYPE_AUDIOSTREAM" value="6" enum="DataType">
+			Theme's [AudioStream] item type.
+		</constant>
+		<constant name="DATA_TYPE_MAX" value="7" enum="DataType">
 			Maximum value for the DataType enum.
 		</constant>
 	</constants>
diff --git a/doc/classes/ThemeDB.xml b/doc/classes/ThemeDB.xml
index 106d011c434..45c511eeaed 100644
--- a/doc/classes/ThemeDB.xml
+++ b/doc/classes/ThemeDB.xml
@@ -25,6 +25,9 @@
 		</method>
 	</methods>
 	<members>
+		<member name="fallback_audio" type="AudioStream" setter="set_fallback_audio" getter="get_fallback_audio">
+			The fallback audio of every [Control] node and [Theme] resource. Used when no other value is available to the control.
+		</member>
 		<member name="fallback_base_scale" type="float" setter="set_fallback_base_scale" getter="get_fallback_base_scale" default="1.0">
 			The fallback base scale factor of every [Control] node and [Theme] resource. Used when no other value is available to the control.
 			See also [member Theme.default_base_scale].
diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml
index 82498b9ba47..7ce50ebb556 100644
--- a/doc/classes/Window.xml
+++ b/doc/classes/Window.xml
@@ -16,6 +16,15 @@
 				Virtual method to be implemented by the user. Overrides the value returned by [method get_contents_minimum_size].
 			</description>
 		</method>
+		<method name="add_theme_audio_override">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="audio" type="AudioStream" />
+			<description>
+				Creates a local override for a theme audio with the specified [param name]. Local overrides always take precedence when fetching theme items for the control. An override can be removed with [method remove_theme_audio_override].
+				See also [method get_theme_audio].
+			</description>
+		</method>
 		<method name="add_theme_color_override">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
@@ -126,6 +135,15 @@
 				Returns the window's size including its border.
 			</description>
 		</method>
+		<method name="get_theme_audio" qualifiers="const">
+			<return type="AudioStream" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
+			<description>
+				Returns an audio from the first matching [Theme] in the tree if that [Theme] has an audio item with the specified [param name] and [param theme_type].
+				See [method Control.get_theme_color] for details.
+			</description>
+		</method>
 		<method name="get_theme_color" qualifiers="const">
 			<return type="Color" />
 			<param index="0" name="name" type="StringName" />
@@ -219,6 +237,23 @@
 				Returns [code]true[/code] if the window is focused.
 			</description>
 		</method>
+		<method name="has_theme_audio" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="name" type="StringName" />
+			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
+			<description>
+				Returns [code]true[/code] if there is a matching [Theme] in the tree that has an audio item with the specified [param name] and [param theme_type].
+				See [method Control.get_theme_audio] for details.
+			</description>
+		</method>
+		<method name="has_theme_audio_override" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="name" type="StringName" />
+			<description>
+				Returns [code]true[/code] if there is a local override for a theme audio with the specified [param name] in this [Control] node.
+				See [method add_theme_audio_override].
+			</description>
+		</method>
 		<method name="has_theme_color" qualifiers="const">
 			<return type="bool" />
 			<param index="0" name="name" type="StringName" />
@@ -242,7 +277,7 @@
 			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
 			<description>
 				Returns [code]true[/code] if there is a matching [Theme] in the tree that has a constant item with the specified [param name] and [param theme_type].
-				See [method Control.get_theme_color] for details.
+				See [method Control.get_theme_constant] for details.
 			</description>
 		</method>
 		<method name="has_theme_constant_override" qualifiers="const">
@@ -259,7 +294,7 @@
 			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
 			<description>
 				Returns [code]true[/code] if there is a matching [Theme] in the tree that has a font item with the specified [param name] and [param theme_type].
-				See [method Control.get_theme_color] for details.
+				See [method Control.get_theme_font] for details.
 			</description>
 		</method>
 		<method name="has_theme_font_override" qualifiers="const">
@@ -276,7 +311,7 @@
 			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
 			<description>
 				Returns [code]true[/code] if there is a matching [Theme] in the tree that has a font size item with the specified [param name] and [param theme_type].
-				See [method Control.get_theme_color] for details.
+				See [method Control.get_theme_font_size] for details.
 			</description>
 		</method>
 		<method name="has_theme_font_size_override" qualifiers="const">
@@ -293,7 +328,7 @@
 			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
 			<description>
 				Returns [code]true[/code] if there is a matching [Theme] in the tree that has an icon item with the specified [param name] and [param theme_type].
-				See [method Control.get_theme_color] for details.
+				See [method Control.get_theme_icon] for details.
 			</description>
 		</method>
 		<method name="has_theme_icon_override" qualifiers="const">
@@ -310,7 +345,7 @@
 			<param index="1" name="theme_type" type="StringName" default="&quot;&quot;" />
 			<description>
 				Returns [code]true[/code] if there is a matching [Theme] in the tree that has a stylebox item with the specified [param name] and [param theme_type].
-				See [method Control.get_theme_color] for details.
+				See [method Control.get_theme_stylebox] for details.
 			</description>
 		</method>
 		<method name="has_theme_stylebox_override" qualifiers="const">
@@ -442,6 +477,13 @@
 				Popups the [Window] with a position shifted by parent [Window]'s position. If the [Window] is embedded, has the same effect as [method popup].
 			</description>
 		</method>
+		<method name="remove_theme_audio_override">
+			<return type="void" />
+			<param index="0" name="name" type="StringName" />
+			<description>
+				Removes a local override for a theme audio with the specified [param name] previously added by [method add_theme_audio_override] or via the Inspector dock.
+			</description>
+		</method>
 		<method name="remove_theme_color_override">
 			<return type="void" />
 			<param index="0" name="name" type="StringName" />
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 7573fcd21e9..63cffce8244 100644
--- a/editor/editor_help.cpp
+++ b/editor/editor_help.cpp
@@ -1186,6 +1186,7 @@ void EditorHelp::_update_doc() {
 		data_type_names["font_size"] = TTR("Font Sizes");
 		data_type_names["icon"] = TTR("Icons");
 		data_type_names["style"] = TTR("Styles");
+		data_type_names["audio"] = TTR("Audios");
 
 		for (int i = 0; i < cd.theme_properties.size(); i++) {
 			theme_property_line[cd.theme_properties[i].name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description.
diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp
index a1ddfc4b850..a90097ac42b 100644
--- a/editor/plugins/theme_editor_plugin.cpp
+++ b/editor/plugins/theme_editor_plugin.cpp
@@ -71,6 +71,7 @@ void ThemeItemImportTree::_update_items_tree() {
 	int font_size_amount = 0;
 	int icon_amount = 0;
 	int stylebox_amount = 0;
+	int audio_amount = 0;
 
 	tree_color_items.clear();
 	tree_constant_items.clear();
@@ -78,6 +79,7 @@ void ThemeItemImportTree::_update_items_tree() {
 	tree_font_size_items.clear();
 	tree_icon_items.clear();
 	tree_stylebox_items.clear();
+	tree_audio_items.clear();
 
 	for (const StringName &E : types) {
 		String type_name = (String)E;
@@ -194,6 +196,14 @@ void ThemeItemImportTree::_update_items_tree() {
 					stylebox_amount += filtered_names.size();
 					break;
 
+				case Theme::DATA_TYPE_AUDIOSTREAM:
+					data_type_node->set_icon(0, get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons")));
+					data_type_node->set_text(0, TTR("Audios"));
+
+					item_list = &tree_audio_items;
+					audio_amount += filtered_names.size();
+					break;
+
 				case Theme::DATA_TYPE_MAX:
 					break; // Can't happen, but silences warning.
 			}
@@ -316,6 +326,20 @@ void ThemeItemImportTree::_update_items_tree() {
 		select_full_styleboxes_button->set_visible(false);
 		deselect_all_styleboxes_button->set_visible(false);
 	}
+
+	if (audio_amount > 0) {
+		Array arr;
+		arr.push_back(audio_amount);
+		select_audios_label->set_text(TTRN("1 audio", "{num} audios", audio_amount).format(arr, "{num}"));
+		select_all_audios_button->set_visible(true);
+		select_full_audios_button->set_visible(true);
+		deselect_all_audios_button->set_visible(true);
+	} else {
+		select_audios_label->set_text(TTR("No audios found."));
+		select_all_audios_button->set_visible(false);
+		select_full_audios_button->set_visible(false);
+		deselect_all_audios_button->set_visible(false);
+	}
 }
 
 void ThemeItemImportTree::_toggle_type_items(bool p_collapse) {
@@ -433,6 +457,10 @@ void ThemeItemImportTree::_update_total_selected(Theme::DataType p_data_type) {
 			total_selected_items_label = total_selected_styleboxes_label;
 			break;
 
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			total_selected_items_label = total_selected_audios_label;
+			break;
+
 		case Theme::DATA_TYPE_MAX:
 			return; // Can't happen, but silences warning.
 	}
@@ -598,6 +626,10 @@ void ThemeItemImportTree::_select_all_data_type_pressed(int p_data_type) {
 			item_list = &tree_stylebox_items;
 			break;
 
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			item_list = &tree_audio_items;
+			break;
+
 		case Theme::DATA_TYPE_MAX:
 			return; // Can't happen, but silences warning.
 	}
@@ -653,6 +685,10 @@ void ThemeItemImportTree::_select_full_data_type_pressed(int p_data_type) {
 			item_list = &tree_stylebox_items;
 			break;
 
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			item_list = &tree_audio_items;
+			break;
+
 		case Theme::DATA_TYPE_MAX:
 			return; // Can't happen, but silences warning.
 	}
@@ -710,6 +746,10 @@ void ThemeItemImportTree::_deselect_all_data_type_pressed(int p_data_type) {
 			item_list = &tree_stylebox_items;
 			break;
 
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			item_list = &tree_audio_items;
+			break;
+
 		case Theme::DATA_TYPE_MAX:
 			return; // Can't happen, but silences warning.
 	}
@@ -788,6 +828,10 @@ void ThemeItemImportTree::_import_selected() {
 						item_value = Ref<StyleBox>();
 						break;
 
+					case Theme::DATA_TYPE_AUDIOSTREAM:
+						item_value = Ref<AudioStream>();
+						break;
+
 					case Theme::DATA_TYPE_MAX:
 						break; // Can't happen, but silences warning.
 				}
@@ -839,6 +883,7 @@ void ThemeItemImportTree::reset_item_tree() {
 	total_selected_font_sizes_label->hide();
 	total_selected_icons_label->hide();
 	total_selected_styleboxes_label->hide();
+	total_selected_audios_label->hide();
 
 	_update_items_tree();
 }
@@ -894,6 +939,11 @@ void ThemeItemImportTree::_notification(int p_what) {
 			deselect_all_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeDeselectAll"), SNAME("EditorIcons")));
 			select_all_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeSelectAll"), SNAME("EditorIcons")));
 			select_full_styleboxes_button->set_icon(get_theme_icon(SNAME("ThemeSelectFull"), SNAME("EditorIcons")));
+
+			select_audios_icon->set_texture(get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons")));
+			deselect_all_audios_button->set_icon(get_theme_icon(SNAME("ThemeDeselectAll"), SNAME("EditorIcons")));
+			select_all_audios_button->set_icon(get_theme_icon(SNAME("ThemeSelectAll"), SNAME("EditorIcons")));
+			select_full_audios_button->set_icon(get_theme_icon(SNAME("ThemeSelectFull"), SNAME("EditorIcons")));
 		} break;
 	}
 }
@@ -988,6 +1038,13 @@ ThemeItemImportTree::ThemeItemImportTree() {
 	select_full_styleboxes_button = memnew(Button);
 	total_selected_styleboxes_label = memnew(Label);
 
+	select_audios_icon = memnew(TextureRect);
+	select_audios_label = memnew(Label);
+	deselect_all_audios_button = memnew(Button);
+	select_all_audios_button = memnew(Button);
+	select_full_audios_button = memnew(Button);
+	total_selected_audios_label = memnew(Label);
+
 	for (int i = 0; i < Theme::DATA_TYPE_MAX; i++) {
 		Theme::DataType dt = (Theme::DataType)i;
 
@@ -1088,6 +1145,20 @@ ThemeItemImportTree::ThemeItemImportTree() {
 				deselect_all_items_tooltip = TTR("Deselect all visible stylebox items.");
 				break;
 
+			case Theme::DATA_TYPE_AUDIOSTREAM:
+				select_items_icon = select_audios_icon;
+				select_items_label = select_audios_label;
+				deselect_all_items_button = deselect_all_audios_button;
+				select_all_items_button = select_all_audios_button;
+				select_full_items_button = select_full_audios_button;
+				total_selected_items_label = total_selected_audios_label;
+
+				items_title = TTR("Audios");
+				select_all_items_tooltip = TTR("Select all visible audio items.");
+				select_full_items_tooltip = TTR("Select all visible audio items and their data.");
+				deselect_all_items_tooltip = TTR("Deselect all visible audio items.");
+				break;
+
 			case Theme::DATA_TYPE_MAX:
 				continue; // Can't happen, but silences warning.
 		}
@@ -1274,6 +1345,7 @@ void ThemeItemEditorDialog::_update_edit_types() {
 		edit_items_add_font_size->set_disabled(false);
 		edit_items_add_icon->set_disabled(false);
 		edit_items_add_stylebox->set_disabled(false);
+		edit_items_add_audio->set_disabled(false);
 
 		edit_items_remove_class->set_disabled(false);
 		edit_items_remove_custom->set_disabled(false);
@@ -1288,6 +1360,7 @@ void ThemeItemEditorDialog::_update_edit_types() {
 		edit_items_add_font_size->set_disabled(true);
 		edit_items_add_icon->set_disabled(true);
 		edit_items_add_stylebox->set_disabled(true);
+		edit_items_add_audio->set_disabled(true);
 
 		edit_items_remove_class->set_disabled(true);
 		edit_items_remove_custom->set_disabled(true);
@@ -1471,6 +1544,29 @@ void ThemeItemEditorDialog::_update_edit_item_tree(String p_item_type) {
 		}
 	}
 
+	{ // Audios.
+		names.clear();
+		edited_theme->get_audio_list(p_item_type, &names);
+
+		if (names.size() > 0) {
+			TreeItem *audio_root = edit_items_tree->create_item(root);
+			audio_root->set_metadata(0, Theme::DATA_TYPE_AUDIOSTREAM);
+			audio_root->set_icon(0, get_theme_audio(SNAME("AudioStream"), SNAME("EditorIcons")));
+			audio_root->set_text(0, TTR("Audios"));
+			audio_root->add_button(0, get_theme_audio(SNAME("Clear"), SNAME("EditorIcons")), ITEMS_TREE_REMOVE_DATA_TYPE, false, TTR("Remove All audio Items"));
+
+			names.sort_custom<StringName::AlphCompare>();
+			for (const StringName &E : names) {
+				TreeItem *item = edit_items_tree->create_item(audio_root);
+				item->set_text(0, E);
+				item->add_button(0, get_theme_audio(SNAME("Edit"), SNAME("EditorIcons")), ITEMS_TREE_RENAME_ITEM, false, TTR("Rename Item"));
+				item->add_button(0, get_theme_audio(SNAME("Remove"), SNAME("EditorIcons")), ITEMS_TREE_REMOVE_ITEM, false, TTR("Remove Item"));
+			}
+
+			has_any_items = true;
+		}
+	}
+
 	// If some type is selected, but it doesn't seem to have any items, show a guiding message.
 	TreeItem *selected_item = edit_type_list->get_selected();
 	if (selected_item) {
@@ -1568,6 +1664,10 @@ void ThemeItemEditorDialog::_add_theme_item(Theme::DataType p_data_type, String
 			ur->add_do_method(*edited_theme, "set_constant", p_item_name, p_item_type, 0);
 			ur->add_undo_method(*edited_theme, "clear_constant", p_item_name, p_item_type);
 			break;
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			ur->add_do_method(*edited_theme, "set_audio", p_item_name, p_item_type, 0);
+			ur->add_undo_method(*edited_theme, "clear_audio", p_item_name, p_item_type);
+			break;
 		case Theme::DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -1758,6 +1858,9 @@ void ThemeItemEditorDialog::_open_add_theme_item_dialog(int p_data_type) {
 		case Theme::DATA_TYPE_STYLEBOX:
 			edit_theme_item_dialog->set_title(TTR("Add Stylebox Item"));
 			break;
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			edit_theme_item_dialog->set_title(TTR("Add Audio Item"));
+			break;
 		case Theme::DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -1794,6 +1897,9 @@ void ThemeItemEditorDialog::_open_rename_theme_item_dialog(Theme::DataType p_dat
 		case Theme::DATA_TYPE_STYLEBOX:
 			edit_theme_item_dialog->set_title(TTR("Rename Stylebox Item"));
 			break;
+		case Theme::DATA_TYPE_AUDIOSTREAM:
+			edit_theme_item_dialog->set_title(TTR("Rename Audio Item"));
+			break;
 		case Theme::DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -1878,6 +1984,7 @@ void ThemeItemEditorDialog::_notification(int p_what) {
 			edit_items_add_font_size->set_icon(get_theme_icon(SNAME("FontSize"), SNAME("EditorIcons")));
 			edit_items_add_icon->set_icon(get_theme_icon(SNAME("ImageTexture"), SNAME("EditorIcons")));
 			edit_items_add_stylebox->set_icon(get_theme_icon(SNAME("StyleBoxFlat"), SNAME("EditorIcons")));
+			edit_items_add_audio->set_icon(get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons")));
 
 			edit_items_remove_class->set_icon(get_theme_icon(SNAME("Control"), SNAME("EditorIcons")));
 			edit_items_remove_custom->set_icon(get_theme_icon(SNAME("ThemeRemoveCustomItems"), SNAME("EditorIcons")));
@@ -1999,6 +2106,13 @@ ThemeItemEditorDialog::ThemeItemEditorDialog(ThemeTypeEditor *p_theme_type_edito
 	edit_items_toolbar->add_child(edit_items_add_stylebox);
 	edit_items_add_stylebox->connect("pressed", callable_mp(this, &ThemeItemEditorDialog::_open_add_theme_item_dialog).bind(Theme::DATA_TYPE_STYLEBOX));
 
+	edit_items_add_audio = memnew(Button);
+	edit_items_add_audio->set_tooltip_text(TTR("Add Audio Item"));
+	edit_items_add_audio->set_flat(true);
+	edit_items_add_audio->set_disabled(true);
+	edit_items_toolbar->add_child(edit_items_add_audio);
+	edit_items_add_audio->connect("pressed", callable_mp(this, &ThemeItemEditorDialog::_open_add_theme_item_dialog).bind(Theme::DATA_TYPE_AUDIOSTREAM));
+
 	edit_items_toolbar->add_child(memnew(VSeparator));
 
 	Label *edit_items_toolbar_remove_label = memnew(Label);
@@ -2732,6 +2846,44 @@ void ThemeTypeEditor::_update_type_items() {
 		}
 	}
 
+	// Audios.
+	{
+		for (int i = audio_items_list->get_child_count() - 1; i >= 0; i--) {
+			Node *node = audio_items_list->get_child(i);
+			node->queue_free();
+			audio_items_list->remove_child(node);
+		}
+
+		HashMap<StringName, bool> audio_items = _get_type_items(edited_type, &Theme::get_audio_list, show_default);
+		for (const KeyValue<StringName, bool> &E : audio_items) {
+			HBoxContainer *item_control = _create_property_control(Theme::DATA_TYPE_AUDIOSTREAM, E.key, E.value);
+			EditorResourcePicker *item_editor = memnew(EditorResourcePicker);
+			item_editor->set_h_size_flags(SIZE_EXPAND_FILL);
+			item_editor->set_base_type("AudioStream");
+			item_control->add_child(item_editor);
+
+			if (E.value) {
+				if (edited_theme->has_audio(E.key, edited_type)) {
+					item_editor->set_edited_resource(edited_theme->get_audio(E.key, edited_type));
+				} else {
+					item_editor->set_edited_resource(Ref<Resource>());
+				}
+				item_editor->connect("resource_selected", callable_mp(this, &ThemeTypeEditor::_edit_resource_item));
+				item_editor->connect("resource_changed", callable_mp(this, &ThemeTypeEditor::_audio_item_changed).bind(E.key));
+			} else {
+				if (ThemeDB::get_singleton()->get_default_theme()->has_audio(E.key, edited_type)) {
+					item_editor->set_edited_resource(ThemeDB::get_singleton()->get_default_theme()->get_audio(E.key, edited_type));
+				} else {
+					item_editor->set_edited_resource(Ref<Resource>());
+				}
+				item_editor->set_editable(false);
+			}
+
+			_add_focusable(item_editor);
+			audio_items_list->add_child(item_control);
+		}
+	}
+
 	// Various type settings.
 	if (edited_type.is_empty() || ClassDB::class_exists(edited_type)) {
 		type_variation_edit->set_editable(false);
@@ -2826,6 +2978,15 @@ void ThemeTypeEditor::_add_default_type_items() {
 			}
 		}
 	}
+	{
+		names.clear();
+		ThemeDB::get_singleton()->get_default_theme()->get_audio_list(default_type, &names);
+		for (const StringName &E : names) {
+			if (!new_snapshot->has_audio(E, edited_type)) {
+				new_snapshot->set_audio(E, edited_type, ThemeDB::get_singleton()->get_default_theme()->get_audio(E, edited_type));
+			}
+		}
+	}
 
 	updating = false;
 
@@ -2882,6 +3043,10 @@ void ThemeTypeEditor::_item_add_cbk(int p_data_type, Control *p_control) {
 				ur->add_undo_method(this, "_unpin_leading_stylebox");
 			}
 		} break;
+		case Theme::DATA_TYPE_AUDIOSTREAM: {
+			ur->add_do_method(*edited_theme, "set_audio", item_name, edited_type, Ref<Texture2D>());
+			ur->add_undo_method(*edited_theme, "clear_audio", item_name, edited_type);
+		} break;
 	}
 
 	ur->commit_action();
@@ -2927,6 +3092,10 @@ void ThemeTypeEditor::_item_override_cbk(int p_data_type, String p_item_name) {
 				ur->add_undo_method(this, "_unpin_leading_stylebox");
 			}
 		} break;
+		case Theme::DATA_TYPE_AUDIOSTREAM: {
+			ur->add_do_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref<Texture2D>());
+			ur->add_undo_method(*edited_theme, "clear_audio", p_item_name, edited_type);
+		} break;
 	}
 
 	ur->commit_action();
@@ -2979,6 +3148,14 @@ void ThemeTypeEditor::_item_remove_cbk(int p_data_type, String p_item_name) {
 				ur->add_undo_method(this, "_pin_leading_stylebox", p_item_name, sb);
 			}
 		} break;
+		case Theme::DATA_TYPE_AUDIOSTREAM: {
+			ur->add_do_method(*edited_theme, "clear_audio", p_item_name, edited_type);
+			if (edited_theme->has_audio(p_item_name, edited_type)) {
+				ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, edited_theme->get_audio(p_item_name, edited_type));
+			} else {
+				ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref<Texture2D>());
+			}
+		} break;
 	}
 
 	ur->commit_action();
@@ -3043,6 +3220,10 @@ void ThemeTypeEditor::_item_rename_confirmed(int p_data_type, String p_item_name
 				leading_stylebox.item_name = new_name;
 			}
 		} break;
+		case Theme::DATA_TYPE_AUDIOSTREAM: {
+			ur->add_do_method(*edited_theme, "rename_audio", p_item_name, new_name, edited_type);
+			ur->add_undo_method(*edited_theme, "rename_audio", new_name, p_item_name, edited_type);
+		} break;
 	}
 
 	ur->commit_action();
@@ -3148,6 +3329,23 @@ void ThemeTypeEditor::_stylebox_item_changed(Ref<StyleBox> p_value, String p_ite
 	ur->commit_action();
 }
 
+void ThemeTypeEditor::_audio_item_changed(Ref<AudioStream> p_value, String p_item_name) {
+	EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
+	ur->create_action(TTR("Set Audio Item in Theme"));
+
+	ur->add_do_method(*edited_theme, "set_audio", p_item_name, edited_type, p_value.is_valid() ? p_value : Ref<AudioStream>());
+	if (edited_theme->has_audio(p_item_name, edited_type)) {
+		ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, edited_theme->get_audio(p_item_name, edited_type));
+	} else {
+		ur->add_undo_method(*edited_theme, "set_audio", p_item_name, edited_type, Ref<Texture2D>());
+	}
+
+	ur->add_do_method(this, "call_deferred", "_update_type_items");
+	ur->add_undo_method(this, "call_deferred", "_update_type_items");
+
+	ur->commit_action();
+}
+
 void ThemeTypeEditor::_change_pinned_stylebox() {
 	if (leading_stylebox.pinned) {
 		if (leading_stylebox.stylebox.is_valid()) {
@@ -3321,7 +3519,8 @@ void ThemeTypeEditor::_notification(int p_what) {
 			data_type_tabs->set_tab_icon(3, get_theme_icon(SNAME("FontSize"), SNAME("EditorIcons")));
 			data_type_tabs->set_tab_icon(4, get_theme_icon(SNAME("ImageTexture"), SNAME("EditorIcons")));
 			data_type_tabs->set_tab_icon(5, get_theme_icon(SNAME("StyleBoxFlat"), SNAME("EditorIcons")));
-			data_type_tabs->set_tab_icon(6, get_theme_icon(SNAME("Tools"), SNAME("EditorIcons")));
+			data_type_tabs->set_tab_icon(6, get_theme_icon(SNAME("AudioStream"), SNAME("EditorIcons")));
+			data_type_tabs->set_tab_icon(7, get_theme_icon(SNAME("Tools"), SNAME("EditorIcons")));
 
 			type_variation_button->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
 		} break;
@@ -3371,6 +3570,7 @@ void ThemeTypeEditor::select_type(String p_type_name) {
 		edited_theme->add_font_size_type(edited_type);
 		edited_theme->add_color_type(edited_type);
 		edited_theme->add_constant_type(edited_type);
+		edited_theme->add_audio_type(edited_type);
 
 		_update_type_list();
 	}
@@ -3433,6 +3633,7 @@ ThemeTypeEditor::ThemeTypeEditor() {
 	font_size_items_list = _create_item_list(Theme::DATA_TYPE_FONT_SIZE);
 	icon_items_list = _create_item_list(Theme::DATA_TYPE_ICON);
 	stylebox_items_list = _create_item_list(Theme::DATA_TYPE_STYLEBOX);
+	audio_items_list = _create_item_list(Theme::DATA_TYPE_AUDIOSTREAM);
 
 	VBoxContainer *type_settings_tab = memnew(VBoxContainer);
 	type_settings_tab->set_custom_minimum_size(Size2(0, 160) * EDSCALE);
diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h
index 077ce8e8f75..dc04f483187 100644
--- a/editor/plugins/theme_editor_plugin.h
+++ b/editor/plugins/theme_editor_plugin.h
@@ -87,6 +87,7 @@ class ThemeItemImportTree : public VBoxContainer {
 	List<TreeItem *> tree_font_size_items;
 	List<TreeItem *> tree_icon_items;
 	List<TreeItem *> tree_stylebox_items;
+	List<TreeItem *> tree_audio_items;
 
 	bool updating_tree = false;
 
@@ -137,6 +138,13 @@ class ThemeItemImportTree : public VBoxContainer {
 	Button *deselect_all_styleboxes_button = nullptr;
 	Label *total_selected_styleboxes_label = nullptr;
 
+	TextureRect *select_audios_icon = nullptr;
+	Label *select_audios_label = nullptr;
+	Button *select_all_audios_button = nullptr;
+	Button *select_full_audios_button = nullptr;
+	Button *deselect_all_audios_button = nullptr;
+	Label *total_selected_audios_label = nullptr;
+
 	HBoxContainer *select_icons_warning_hb = nullptr;
 	TextureRect *select_icons_warning_icon = nullptr;
 	Label *select_icons_warning = nullptr;
@@ -210,6 +218,7 @@ class ThemeItemEditorDialog : public AcceptDialog {
 	Button *edit_items_add_font_size = nullptr;
 	Button *edit_items_add_icon = nullptr;
 	Button *edit_items_add_stylebox = nullptr;
+	Button *edit_items_add_audio = nullptr;
 	Button *edit_items_remove_class = nullptr;
 	Button *edit_items_remove_custom = nullptr;
 	Button *edit_items_remove_all = nullptr;
@@ -348,6 +357,7 @@ class ThemeTypeEditor : public MarginContainer {
 	VBoxContainer *font_size_items_list = nullptr;
 	VBoxContainer *icon_items_list = nullptr;
 	VBoxContainer *stylebox_items_list = nullptr;
+	VBoxContainer *audio_items_list = nullptr;
 
 	LineEdit *type_variation_edit = nullptr;
 	Button *type_variation_button = nullptr;
@@ -392,6 +402,7 @@ class ThemeTypeEditor : public MarginContainer {
 	void _font_item_changed(Ref<Font> p_value, String p_item_name);
 	void _icon_item_changed(Ref<Texture2D> p_value, String p_item_name);
 	void _stylebox_item_changed(Ref<StyleBox> p_value, String p_item_name);
+	void _audio_item_changed(Ref<AudioStream> p_value, String p_item_name);
 	void _change_pinned_stylebox();
 	void _on_pin_leader_button_pressed(Control *p_editor, String p_item_name);
 	void _pin_leading_stylebox(String p_item_name, Ref<StyleBox> p_stylebox);
diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp
index f57afb66b30..6eb7ef7bb99 100644
--- a/scene/gui/base_button.cpp
+++ b/scene/gui/base_button.cpp
@@ -88,6 +88,9 @@ void BaseButton::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_MOUSE_ENTER: {
 			status.hovering = true;
+			if (!status.disabled) {
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("hover")));
+			}
 			queue_redraw();
 		} break;
 
@@ -134,6 +137,8 @@ void BaseButton::_notification(int p_what) {
 
 void BaseButton::_pressed() {
 	GDVIRTUAL_CALL(_pressed);
+	// TODO: `pressed_disabled` never occurs as this isn't called on disabled buttons.
+	get_tree()->play_theme_audio(get_theme_audio(is_disabled() ? SNAME("pressed_disabled") : SNAME("pressed")));
 	pressed();
 	emit_signal(SNAME("pressed"));
 }
diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp
index 0ca04faf9b1..efaa92440a0 100644
--- a/scene/gui/control.cpp
+++ b/scene/gui/control.cpp
@@ -214,6 +214,8 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List
 			ThemeDB::get_singleton()->get_default_theme()->get_font_size_list(get_class(), &sn);
 		} else if (pf == "add_theme_constant_override" || pf == "has_theme_constant" || pf == "has_theme_constant_override" || pf == "get_theme_constant") {
 			ThemeDB::get_singleton()->get_default_theme()->get_constant_list(get_class(), &sn);
+		} else if (pf == "add_theme_audio_override" || pf == "has_theme_audio" || pf == "has_theme_audio_override" || pf == "get_theme_audio") {
+			ThemeDB::get_singleton()->get_default_theme()->get_audio_list(get_class(), &sn);
 		}
 
 		sn.sort_custom<StringName::AlphCompare>();
@@ -301,6 +303,10 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) {
 			String dname = name.get_slicec('/', 1);
 			data.theme_constant_override.erase(dname);
 			_notify_theme_override_changed();
+		} else if (name.begins_with("theme_override_audios/")) {
+			String dname = name.get_slicec('/', 1);
+			data.theme_audio_override.erase(dname);
+			_notify_theme_override_changed();
 		} else {
 			return false;
 		}
@@ -324,6 +330,9 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) {
 		} else if (name.begins_with("theme_override_constants/")) {
 			String dname = name.get_slicec('/', 1);
 			add_theme_constant_override(dname, p_value);
+		} else if (name.begins_with("theme_override_audios/")) {
+			String dname = name.get_slicec('/', 1);
+			add_theme_audio_override(dname, p_value);
 		} else {
 			return false;
 		}
@@ -356,6 +365,9 @@ bool Control::_get(const StringName &p_name, Variant &r_ret) const {
 	} else if (sname.begins_with("theme_override_constants/")) {
 		String name = sname.get_slicec('/', 1);
 		r_ret = data.theme_constant_override.has(name) ? Variant(data.theme_constant_override[name]) : Variant();
+	} else if (sname.begins_with("theme_override_audios/")) {
+		String name = sname.get_slicec('/', 1);
+		r_ret = data.theme_audio_override.has(name) ? Variant(data.theme_audio_override[name]) : Variant();
 	} else {
 		return false;
 	}
@@ -441,6 +453,18 @@ void Control::_get_property_list(List<PropertyInfo> *p_list) const {
 			p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_styles") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage));
 		}
 	}
+	{
+		List<StringName> names;
+		default_theme->get_audio_list(get_class_name(), &names);
+		for (const StringName &E : names) {
+			uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE;
+			if (data.theme_audio_override.has(E)) {
+				usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED;
+			}
+
+			p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_audios") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", usage));
+		}
+	}
 }
 
 void Control::_validate_property(PropertyInfo &p_property) const {
@@ -2441,6 +2465,7 @@ void Control::_invalidate_theme_cache() {
 	data.theme_font_size_cache.clear();
 	data.theme_color_cache.clear();
 	data.theme_constant_cache.clear();
+	data.theme_audio_cache.clear();
 }
 
 void Control::_update_theme_item_cache() {
@@ -2660,6 +2685,30 @@ int Control::get_theme_constant(const StringName &p_name, const StringName &p_th
 	return constant;
 }
 
+Ref<AudioStream> Control::get_theme_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	ERR_READ_THREAD_GUARD_V(Ref<AudioStream>());
+	if (!data.initialized) {
+		WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED");
+	}
+
+	if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) {
+		const Ref<AudioStream> *audio = data.theme_audio_override.getptr(p_name);
+		if (audio) {
+			return *audio;
+		}
+	}
+
+	if (data.theme_audio_cache.has(p_theme_type) && data.theme_audio_cache[p_theme_type].has(p_name)) {
+		return data.theme_audio_cache[p_theme_type][p_name];
+	}
+
+	List<StringName> theme_types;
+	data.theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types);
+	Ref<AudioStream> audio = data.theme_owner->get_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types);
+	data.theme_audio_cache[p_theme_type][p_name] = audio;
+	return audio;
+}
+
 bool Control::has_theme_icon(const StringName &p_name, const StringName &p_theme_type) const {
 	ERR_READ_THREAD_GUARD_V(false);
 	if (!data.initialized) {
@@ -2762,6 +2811,23 @@ bool Control::has_theme_constant(const StringName &p_name, const StringName &p_t
 	return data.theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_CONSTANT, p_name, theme_types);
 }
 
+bool Control::has_theme_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	ERR_READ_THREAD_GUARD_V(false);
+	if (!data.initialized) {
+		WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED");
+	}
+
+	if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) {
+		if (has_theme_audio_override(p_name)) {
+			return true;
+		}
+	}
+
+	List<StringName> theme_types;
+	data.theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types);
+	return data.theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types);
+}
+
 /// Local property overrides.
 
 void Control::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) {
@@ -2821,6 +2887,12 @@ void Control::add_theme_constant_override(const StringName &p_name, int p_consta
 	_notify_theme_override_changed();
 }
 
+void Control::add_theme_audio_override(const StringName &p_name, const Ref<AudioStream> &p_audio) {
+	ERR_MAIN_THREAD_GUARD;
+	data.theme_audio_override[p_name] = p_audio;
+	_notify_theme_override_changed();
+}
+
 void Control::remove_theme_icon_override(const StringName &p_name) {
 	ERR_MAIN_THREAD_GUARD;
 	if (data.theme_icon_override.has(p_name)) {
@@ -2869,6 +2941,12 @@ void Control::remove_theme_constant_override(const StringName &p_name) {
 	_notify_theme_override_changed();
 }
 
+void Control::remove_theme_audio_override(const StringName &p_name) {
+	ERR_MAIN_THREAD_GUARD;
+	data.theme_audio_override.erase(p_name);
+	_notify_theme_override_changed();
+}
+
 bool Control::has_theme_icon_override(const StringName &p_name) const {
 	ERR_READ_THREAD_GUARD_V(false);
 	const Ref<Texture2D> *tex = data.theme_icon_override.getptr(p_name);
@@ -2905,6 +2983,12 @@ bool Control::has_theme_constant_override(const StringName &p_name) const {
 	return constant != nullptr;
 }
 
+bool Control::has_theme_audio_override(const StringName &p_name) const {
+	ERR_READ_THREAD_GUARD_V(false);
+	const Ref<AudioStream> *audio = data.theme_audio_override.getptr(p_name);
+	return audio != nullptr;
+}
+
 /// Default theme properties.
 
 float Control::get_theme_default_base_scale() const {
@@ -3351,6 +3435,7 @@ void Control::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Control::add_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Control::add_theme_color_override);
 	ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Control::add_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("add_theme_audio_override", "name", "audio"), &Control::add_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("remove_theme_icon_override", "name"), &Control::remove_theme_icon_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Control::remove_theme_style_override);
@@ -3358,6 +3443,7 @@ void Control::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Control::remove_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Control::remove_theme_color_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Control::remove_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("remove_theme_audio_override", "name"), &Control::remove_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "theme_type"), &Control::get_theme_icon, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "theme_type"), &Control::get_theme_stylebox, DEFVAL(""));
@@ -3365,6 +3451,7 @@ void Control::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "theme_type"), &Control::get_theme_font_size, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_color", "name", "theme_type"), &Control::get_theme_color, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "theme_type"), &Control::get_theme_constant, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("get_theme_audio", "name", "theme_type"), &Control::get_theme_audio, DEFVAL(""));
 
 	ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Control::has_theme_icon_override);
 	ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Control::has_theme_stylebox_override);
@@ -3372,6 +3459,7 @@ void Control::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Control::has_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Control::has_theme_color_override);
 	ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Control::has_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("has_theme_audio_override", "name"), &Control::has_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "theme_type"), &Control::has_theme_icon, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "theme_type"), &Control::has_theme_stylebox, DEFVAL(""));
@@ -3379,6 +3467,7 @@ void Control::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "theme_type"), &Control::has_theme_font_size, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_color", "name", "theme_type"), &Control::has_theme_color, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "theme_type"), &Control::has_theme_constant, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("has_theme_audio", "name", "theme_type"), &Control::has_theme_audio, DEFVAL(""));
 
 	ClassDB::bind_method(D_METHOD("get_theme_default_base_scale"), &Control::get_theme_default_base_scale);
 	ClassDB::bind_method(D_METHOD("get_theme_default_font"), &Control::get_theme_default_font);
@@ -3646,4 +3735,5 @@ Control::~Control() {
 	data.theme_font_size_override.clear();
 	data.theme_color_override.clear();
 	data.theme_constant_override.clear();
+	data.theme_audio_override.clear();
 }
diff --git a/scene/gui/control.h b/scene/gui/control.h
index 7cb8fc5bf6a..44a0b1934bd 100644
--- a/scene/gui/control.h
+++ b/scene/gui/control.h
@@ -238,6 +238,7 @@ class Control : public CanvasItem {
 		Theme::ThemeFontSizeMap theme_font_size_override;
 		Theme::ThemeColorMap theme_color_override;
 		Theme::ThemeConstantMap theme_constant_override;
+		Theme::ThemeAudioMap theme_audio_override;
 
 		mutable HashMap<StringName, Theme::ThemeIconMap> theme_icon_cache;
 		mutable HashMap<StringName, Theme::ThemeStyleMap> theme_style_cache;
@@ -245,6 +246,7 @@ class Control : public CanvasItem {
 		mutable HashMap<StringName, Theme::ThemeFontSizeMap> theme_font_size_cache;
 		mutable HashMap<StringName, Theme::ThemeColorMap> theme_color_cache;
 		mutable HashMap<StringName, Theme::ThemeConstantMap> theme_constant_cache;
+		mutable HashMap<StringName, Theme::ThemeAudioMap> theme_audio_cache;
 
 		// Internationalization.
 
@@ -568,6 +570,7 @@ class Control : public CanvasItem {
 	void add_theme_font_size_override(const StringName &p_name, int p_font_size);
 	void add_theme_color_override(const StringName &p_name, const Color &p_color);
 	void add_theme_constant_override(const StringName &p_name, int p_constant);
+	void add_theme_audio_override(const StringName &p_name, const Ref<AudioStream> &p_audio);
 
 	void remove_theme_icon_override(const StringName &p_name);
 	void remove_theme_style_override(const StringName &p_name);
@@ -575,6 +578,7 @@ class Control : public CanvasItem {
 	void remove_theme_font_size_override(const StringName &p_name);
 	void remove_theme_color_override(const StringName &p_name);
 	void remove_theme_constant_override(const StringName &p_name);
+	void remove_theme_audio_override(const StringName &p_name);
 
 	Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
@@ -582,6 +586,7 @@ class Control : public CanvasItem {
 	int get_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	Color get_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	int get_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
+	Ref<AudioStream> get_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 
 	bool has_theme_icon_override(const StringName &p_name) const;
 	bool has_theme_stylebox_override(const StringName &p_name) const;
@@ -589,6 +594,7 @@ class Control : public CanvasItem {
 	bool has_theme_font_size_override(const StringName &p_name) const;
 	bool has_theme_color_override(const StringName &p_name) const;
 	bool has_theme_constant_override(const StringName &p_name) const;
+	bool has_theme_audio_override(const StringName &p_name) const;
 
 	bool has_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
@@ -596,6 +602,7 @@ class Control : public CanvasItem {
 	bool has_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
+	bool has_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 
 	float get_theme_default_base_scale() const;
 	Ref<Font> get_theme_default_font() const;
diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp
index b273f709f2b..7958043212f 100644
--- a/scene/gui/item_list.cpp
+++ b/scene/gui/item_list.cpp
@@ -695,6 +695,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 						emit_signal(SNAME("multi_selected"), j, true);
 					}
 				}
+				get_tree()->play_theme_audio(get_theme_audio(items[i].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected")));
 				emit_signal(SNAME("item_clicked"), i, get_local_mouse_position(), mb->get_button_index());
 
 			} else {
@@ -705,6 +706,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 
 				if (!items[i].selected || allow_reselect) {
 					select(i, select_mode == SELECT_SINGLE || !mb->is_command_or_control_pressed());
+					get_tree()->play_theme_audio(get_theme_audio(items[i].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected")));
 
 					if (select_mode == SELECT_SINGLE) {
 						emit_signal(SNAME("item_selected"), i);
@@ -722,6 +724,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) {
 
 			return;
 		} else if (closest != -1) {
+			get_tree()->play_theme_audio(get_theme_audio(items[closest].disabled ? SNAME("item_disabled_selected") : SNAME("item_selected")));
 			emit_signal(SNAME("item_clicked"), closest, get_local_mouse_position(), mb->get_button_index());
 		} else {
 			// Since closest is null, more likely we clicked on empty space, so send signal to interested controls. Allows, for example, implement items deselecting.
diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp
index 42ee6cb0bcd..4b12751df9f 100644
--- a/scene/gui/line_edit.cpp
+++ b/scene/gui/line_edit.cpp
@@ -474,6 +474,7 @@ void LineEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 		// Default is ENTER and KP_ENTER. Cannot use ui_accept as default includes SPACE.
 		if (k->is_action("ui_text_submit", false)) {
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("text_submitted")));
 			emit_signal(SNAME("text_submitted"), text);
 			if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) {
 				DisplayServer::get_singleton()->virtual_keyboard_hide();
@@ -1752,6 +1753,7 @@ void LineEdit::insert_text_at_caret(String p_text) {
 		// Truncate text to append to fit in max_length, if needed.
 		int available_chars = max_length - text.length();
 		if (p_text.length() > available_chars) {
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("text_change_rejected")));
 			emit_signal(SNAME("text_change_rejected"), p_text.substr(available_chars));
 			p_text = p_text.substr(0, available_chars);
 		}
@@ -2277,6 +2279,10 @@ void LineEdit::_text_changed() {
 }
 
 void LineEdit::_emit_text_change() {
+	if (get_tree()) {
+		get_tree()->play_theme_audio(get_theme_audio(SNAME("text_changed")));
+	}
+
 	emit_signal(SNAME("text_changed"), text);
 	text_changed_dirty = false;
 }
diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp
index 40db8deaace..20506f32241 100644
--- a/scene/gui/popup_menu.cpp
+++ b/scene/gui/popup_menu.cpp
@@ -459,7 +459,13 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) {
 					return;
 				}
 
-				if (items[over].separator || items[over].disabled) {
+				if (items[over].separator) {
+					return;
+				}
+
+				get_tree()->play_theme_audio(get_theme_audio(items[over].disabled ? SNAME("item_disabled_activated") : SNAME("item_activated")));
+
+				if (items[over].disabled) {
 					return;
 				}
 
diff --git a/scene/gui/slider.cpp b/scene/gui/slider.cpp
index 398f637e856..0083f8c1e9f 100644
--- a/scene/gui/slider.cpp
+++ b/scene/gui/slider.cpp
@@ -75,19 +75,23 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) {
 				grab.active = true;
 				grab.uvalue = get_as_ratio();
 
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("drag_started")));
 				emit_signal(SNAME("drag_started"));
 			} else {
 				grab.active = false;
 
 				const bool value_changed = !Math::is_equal_approx((double)grab.uvalue, get_as_ratio());
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("drag_ended")));
 				emit_signal(SNAME("drag_ended"), value_changed);
 			}
 		} else if (scrollable) {
 			if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) {
 				grab_focus();
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 				set_value(get_value() + get_step());
 			} else if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN) {
 				grab_focus();
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 				set_value(get_value() - get_step());
 			}
 		}
@@ -128,6 +132,7 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) {
 				}
 				set_process_internal(true);
 			}
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_value() - (custom_step >= 0 ? custom_step : get_step()));
 			accept_event();
 		} else if (p_event->is_action_pressed("ui_right", true)) {
@@ -140,6 +145,7 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) {
 				}
 				set_process_internal(true);
 			}
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_value() + (custom_step >= 0 ? custom_step : get_step()));
 			accept_event();
 		} else if (p_event->is_action_pressed("ui_up", true)) {
@@ -152,6 +158,7 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) {
 				}
 				set_process_internal(true);
 			}
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_value() + (custom_step >= 0 ? custom_step : get_step()));
 			accept_event();
 		} else if (p_event->is_action_pressed("ui_down", true)) {
@@ -164,12 +171,15 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) {
 				}
 				set_process_internal(true);
 			}
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_value() - (custom_step >= 0 ? custom_step : get_step()));
 			accept_event();
 		} else if (p_event->is_action("ui_home", true) && p_event->is_pressed()) {
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_min());
 			accept_event();
 		} else if (p_event->is_action("ui_end", true) && p_event->is_pressed()) {
+			get_tree()->play_theme_audio(get_theme_audio(SNAME("value_changed")));
 			set_value(get_max());
 			accept_event();
 		}
diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp
index 7cb54f24ea9..9957e374cd7 100644
--- a/scene/gui/spin_box.cpp
+++ b/scene/gui/spin_box.cpp
@@ -96,6 +96,7 @@ void SpinBox::_range_click_timeout() {
 		bool up = get_local_mouse_position().y < (get_size().height / 2);
 		double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step();
 		set_value(get_value() + (up ? step : -step));
+		get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 
 		if (range_click_timer->is_one_shot()) {
 			range_click_timer->set_wait_time(0.075);
@@ -136,6 +137,7 @@ void SpinBox::gui_input(const Ref<InputEvent> &p_event) {
 				line_edit->grab_focus();
 
 				set_value(get_value() + (up ? step : -step));
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 
 				range_click_timer->set_wait_time(0.6);
 				range_click_timer->set_one_shot(true);
@@ -147,16 +149,19 @@ void SpinBox::gui_input(const Ref<InputEvent> &p_event) {
 			case MouseButton::RIGHT: {
 				line_edit->grab_focus();
 				set_value((up ? get_max() : get_min()));
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 			} break;
 			case MouseButton::WHEEL_UP: {
 				if (line_edit->has_focus()) {
 					set_value(get_value() + step * mb->get_factor());
+					get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 					accept_event();
 				}
 			} break;
 			case MouseButton::WHEEL_DOWN: {
 				if (line_edit->has_focus()) {
 					set_value(get_value() - step * mb->get_factor());
+					get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 					accept_event();
 				}
 			} break;
diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp
index 959a51eff91..bae8dc88a41 100644
--- a/scene/gui/tab_bar.cpp
+++ b/scene/gui/tab_bar.cpp
@@ -188,6 +188,7 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
 
 		if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
 			if (rb_hover != -1) {
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 				emit_signal(SNAME("tab_button_pressed"), rb_hover);
 			}
 
@@ -197,6 +198,7 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
 
 		if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
 			if (cb_hover != -1) {
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 				emit_signal(SNAME("tab_close_pressed"), cb_hover);
 			}
 
@@ -212,15 +214,21 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
 					if (pos.x < theme_cache.decrement_icon->get_width()) {
 						if (missing_right) {
 							offset++;
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 							_update_cache();
 							queue_redraw();
+						} else {
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled")));
 						}
 						return;
 					} else if (pos.x < theme_cache.increment_icon->get_width() + theme_cache.decrement_icon->get_width()) {
 						if (offset > 0) {
 							offset--;
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 							_update_cache();
 							queue_redraw();
+						} else {
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled")));
 						}
 						return;
 					}
@@ -229,15 +237,21 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
 					if (pos.x > limit + theme_cache.decrement_icon->get_width()) {
 						if (missing_right) {
 							offset++;
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 							_update_cache();
 							queue_redraw();
+						} else {
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled")));
 						}
 						return;
 					} else if (pos.x > limit) {
 						if (offset > 0) {
 							offset--;
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
 							_update_cache();
 							queue_redraw();
+						} else {
+							get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed_disabled")));
 						}
 						return;
 					}
@@ -278,6 +292,10 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
 			}
 
 			if (found != -1) {
+				if (current != found) {
+					get_tree()->play_theme_audio(get_theme_audio(SNAME("pressed")));
+				}
+
 				set_current_tab(found);
 
 				if (mb->get_button_index() == MouseButton::RIGHT) {
@@ -879,6 +897,9 @@ void TabBar::_update_hover() {
 		hover = hover_now;
 
 		if (hover != -1) {
+			if (!is_tab_disabled(hover)) {
+				get_tree()->play_theme_audio(get_theme_audio(SNAME("hover")));
+			}
 			emit_signal(SNAME("tab_hovered"), hover);
 		}
 
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index 3b2013f7ecf..946b7502caf 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -7611,6 +7611,7 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) {
 /*** Super internal Core API. Everything builds on it. ***/
 
 void TextEdit::_text_changed_emit() {
+	get_tree()->play_theme_audio(get_theme_audio(SNAME("text_changed")));
 	emit_signal(SNAME("text_changed"));
 	text_changed_dirty = false;
 }
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index 433ae656ba3..c1d009ccc7a 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -2626,6 +2626,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c
 				selected_item = p_selected;
 				selected_col = 0;
 				if (!emitted_row) {
+					get_tree()->play_theme_audio(get_theme_audio(SNAME("item_selected")));
 					emit_signal(SNAME("item_selected"));
 					emitted_row = true;
 				}
@@ -2643,6 +2644,10 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c
 					selected_item = p_selected;
 					selected_col = i;
 
+					if (get_tree()) {
+						get_tree()->play_theme_audio(get_theme_audio(SNAME("item_selected")));
+					}
+
 					emit_signal(SNAME("cell_selected"));
 					if (select_mode == SELECT_MULTI) {
 						emit_signal(SNAME("multi_selected"), p_current, i, true);
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index db9c1efa68e..cb2f2291d6c 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -44,6 +44,7 @@
 #include "core/string/print_string.h"
 #include "node.h"
 #include "scene/animation/tween.h"
+#include "scene/audio/audio_stream_player.h"
 #include "scene/debugger/scene_debugger.h"
 #include "scene/gui/control.h"
 #include "scene/main/multiplayer_api.h"
@@ -117,6 +118,79 @@ void SceneTreeTimer::release_connections() {
 
 SceneTreeTimer::SceneTreeTimer() {}
 
+// void SceneTreeAudioStreamPlayer::_bind_methods() {
+// 	ClassDB::bind_method(D_METHOD("set_stream", "stream"), &SceneTreeAudioStreamPlayer::set_stream);
+// 	ClassDB::bind_method(D_METHOD("get_stream"), &SceneTreeAudioStreamPlayer::get_stream);
+
+// 	ClassDB::bind_method(D_METHOD("get_stream_playback"), &SceneTreeAudioStreamPlayer::get_stream_playback);
+
+// 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream");
+
+// 	ADD_SIGNAL(MethodInfo("finished"));
+// }
+
+// void SceneTreeAudioStreamPlayer::set_stream(Ref<AudioStream> p_stream) {
+// 	stream = p_stream;
+
+// 	if (stream.is_valid()) {
+// 		stream_playback = stream->instantiate_playback();
+// 	}
+
+// 	if (stream_playback.is_null()) {
+// 		return;
+// 	}
+
+// 	// TODO: Add a parameter to control output bus.
+
+// 	Vector<AudioFrame> volume_vector;
+// 	// We need at most four stereo pairs (for 7.1 systems).
+// 	volume_vector.resize(4);
+
+// 	// Initialize the volume vector to zero.
+// 	for (AudioFrame &channel_volume_db : volume_vector) {
+// 		channel_volume_db = AudioFrame(0, 0);
+// 	}
+
+// 	// TODO: Add option to change volume dB.
+// 	float volume_linear = Math::db_to_linear(0.0);
+
+// 	// Set the volume vector up according to the speaker mode and mix target.
+// 	// TODO do we need to scale the volume down when we output to more channels?
+// 	if (AudioServer::get_singleton()->get_speaker_mode() == AudioServer::SPEAKER_MODE_STEREO) {
+// 		volume_vector.write[0] = AudioFrame(volume_linear, volume_linear);
+// 	} else {
+// 		switch (1) {
+// 			case 1: {
+// 				volume_vector.write[0] = AudioFrame(volume_linear, volume_linear);
+// 			} break;
+// 			case 2: {
+// 				// TODO Make sure this is right.
+// 				volume_vector.write[0] = AudioFrame(volume_linear, volume_linear);
+// 				volume_vector.write[1] = AudioFrame(volume_linear, /* LFE= */ 1.0f);
+// 				volume_vector.write[2] = AudioFrame(volume_linear, volume_linear);
+// 				volume_vector.write[3] = AudioFrame(volume_linear, volume_linear);
+// 			} break;
+// 			case 3: {
+// 				// TODO Make sure this is right.
+// 				volume_vector.write[1] = AudioFrame(volume_linear, /* LFE= */ 1.0f);
+// 			} break;
+// 		}
+// 	}
+
+// 	AudioServer::get_singleton()->start_playback_stream(stream_playback, SNAME("Master"), volume_vector);
+// 	//active.set();
+// }
+
+// Ref<AudioStream> SceneTreeAudioStreamPlayer::get_stream() const {
+// 	return stream;
+// }
+
+// Ref<AudioStreamPlayback> SceneTreeAudioStreamPlayer::get_stream_playback() const {
+// 	return stream_playback;
+// }
+
+// SceneTreeAudioStreamPlayer::SceneTreeAudioStreamPlayer() {}
+
 void SceneTree::tree_changed() {
 	tree_version++;
 	emit_signal(tree_changed_name);
@@ -519,6 +593,8 @@ bool SceneTree::process(double p_time) {
 
 	process_tweens(p_time, false);
 
+	// process_audio_stream_players(p_time);
+
 	flush_transform_notifications(); //additional transforms after timers update
 
 	_call_idle_callbacks();
@@ -617,6 +693,40 @@ void SceneTree::process_tweens(double p_delta, bool p_physics) {
 	}
 }
 
+// void SceneTree::process_audio_stream_players(double p_delta) {
+// 	_THREAD_SAFE_METHOD_
+// 	// This method works similarly to how SceneTreeTimers are handled.
+// 	List<Ref<SceneTreeAudioStreamPlayer>>::Element *L = audio_stream_players.back();
+
+// 	for (List<Ref<SceneTreeAudioStreamPlayer>>::Element *E = audio_stream_players.front(); E;) {
+// 		List<Ref<SceneTreeAudioStreamPlayer>>::Element *N = E->next();
+// 		Vector<Ref<AudioStreamPlayback>> playbacks_to_remove;
+// 		// for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+// 		// 	if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) {
+// 		// 		playbacks_to_remove.push_back(playback);
+// 		// 	}
+// 		// }
+// 		// // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble.
+// 		// for (Ref<AudioStreamPlayback> &playback : playbacks_to_remove) {
+// 		// 	stream_playbacks.erase(playback);
+// 		// }
+// 		if (E->get()->get_stream_playback().is_valid() && AudioServer::get_singleton()->is_playback_active(E->get()->get_stream_playback())) {
+// 			print_line("Freeing audio");
+// 			// This node is no longer actively playing audio.
+// 			//active.clear();
+// 			//set_process_internal(false);
+// 			E->get()->emit_signal(SNAME("finished"));
+// 			AudioServer::get_singleton()->stop_playback_stream(E->get()->get_stream_playback());
+// 			audio_stream_players.erase(E);
+// 		}
+// 		if (E == L) {
+// 			// Break on last, so if new timers were added during list traversal, ignore them.
+// 			break;
+// 		}
+// 		E = N;
+// 	}
+// }
+
 void SceneTree::finalize() {
 	_flush_delete_queue();
 
@@ -1467,6 +1577,41 @@ Ref<Tween> SceneTree::create_tween() {
 	return tween;
 }
 
+// Ref<SceneTreeAudioStreamPlayer> SceneTree::create_audio_stream_player(Ref<AudioStream> p_stream) {
+// 	_THREAD_SAFE_METHOD_
+// 	Ref<SceneTreeAudioStreamPlayer> stasp;
+// 	stasp.instantiate();
+// 	stasp->set_stream(p_stream);
+// 	audio_stream_players.push_back(stasp);
+// 	return stasp;
+// }
+
+// Helper method that automatically sets the bus based on the project setting.
+AudioStreamPlayer *SceneTree::play_theme_audio(const Ref<AudioStream> &p_stream) {
+	return create_audio_stream_player(p_stream, gui_theme_bus);
+}
+
+AudioStreamPlayer *SceneTree::create_audio_stream_player(const Ref<AudioStream> &p_stream, const StringName &p_bus, float p_volume_db) {
+	_THREAD_SAFE_METHOD_
+	// TODO: Early return if using a bare AudioStream resource
+	// (useful to mute specific sounds with theme overrides without spamming errors).
+	if (p_stream.is_null()) {
+		return nullptr;
+	}
+
+	AudioStreamPlayer *asp = memnew(AudioStreamPlayer);
+	asp->connect(SceneStringNames::get_singleton()->finished, callable_mp(this, &SceneTree::_on_audio_finished).bind(asp));
+	asp->set_bus(p_bus);
+	asp->set_stream(p_stream);
+	asp->set_autoplay(true);
+	root->add_child(asp, false, Node::INTERNAL_MODE_BACK);
+	return asp;
+}
+
+void SceneTree::_on_audio_finished(AudioStreamPlayer *p_player) {
+	p_player->queue_free();
+}
+
 TypedArray<Tween> SceneTree::get_processed_tweens() {
 	_THREAD_SAFE_METHOD_
 	TypedArray<Tween> ret;
@@ -1561,6 +1706,8 @@ void SceneTree::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("create_timer", "time_sec", "process_always", "process_in_physics", "ignore_time_scale"), &SceneTree::create_timer, DEFVAL(true), DEFVAL(false), DEFVAL(false));
 	ClassDB::bind_method(D_METHOD("create_tween"), &SceneTree::create_tween);
 	ClassDB::bind_method(D_METHOD("get_processed_tweens"), &SceneTree::get_processed_tweens);
+	ClassDB::bind_method(D_METHOD("create_audio_stream_player", "stream", "bus", "volume_db"), &SceneTree::create_audio_stream_player, DEFVAL(SNAME("Master")), DEFVAL(0.0f));
+	ClassDB::bind_method(D_METHOD("play_theme_audio", "stream"), &SceneTree::play_theme_audio);
 
 	ClassDB::bind_method(D_METHOD("get_node_count"), &SceneTree::get_node_count);
 	ClassDB::bind_method(D_METHOD("get_frame"), &SceneTree::get_frame);
@@ -1693,6 +1840,7 @@ SceneTree::SceneTree() {
 	debug_paths_color = GLOBAL_DEF("debug/shapes/paths/geometry_color", Color(0.1, 1.0, 0.7, 0.4));
 	debug_paths_width = GLOBAL_DEF("debug/shapes/paths/geometry_width", 2.0);
 	collision_debug_contacts = GLOBAL_DEF(PropertyInfo(Variant::INT, "debug/shapes/collision/max_contacts_displayed", PROPERTY_HINT_RANGE, "0,20000,1"), 10000);
+	gui_theme_bus = GLOBAL_GET("audio/buses/gui_theme_bus");
 
 	GLOBAL_DEF("debug/shapes/collision/draw_2d_outlines", true);
 
diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h
index e1597d3890a..520b251d2c9 100644
--- a/scene/main/scene_tree.h
+++ b/scene/main/scene_tree.h
@@ -41,6 +41,9 @@
 
 class PackedScene;
 class Node;
+class AudioStream;
+class AudioStreamPlayback;
+class AudioStreamPlayer;
 class Window;
 class Material;
 class Mesh;
@@ -78,6 +81,24 @@ class SceneTreeTimer : public RefCounted {
 	SceneTreeTimer();
 };
 
+// class SceneTreeAudioStreamPlayer : public RefCounted {
+// 	GDCLASS(SceneTreeAudioStreamPlayer, RefCounted);
+
+// 	Ref<AudioStream> stream;
+// 	Ref<AudioStreamPlayback> stream_playback;
+
+// protected:
+// 	static void _bind_methods();
+
+// public:
+// 	void set_stream(Ref<AudioStream> p_stream);
+// 	Ref<AudioStream> get_stream() const;
+
+// 	Ref<AudioStreamPlayback> get_stream_playback() const;
+
+// 	SceneTreeAudioStreamPlayer();
+// };
+
 class SceneTree : public MainLoop {
 	_THREAD_SAFE_CLASS_
 
@@ -189,11 +210,13 @@ class SceneTree : public MainLoop {
 	Ref<Material> debug_paths_material;
 	Ref<Material> collision_material;
 	int collision_debug_contacts;
+	StringName gui_theme_bus;
 
 	void _flush_scene_change();
 
 	List<Ref<SceneTreeTimer>> timers;
 	List<Ref<Tween>> tweens;
+	// List<Ref<SceneTreeAudioStreamPlayer>> audio_stream_players;
 
 	///network///
 
@@ -210,6 +233,7 @@ class SceneTree : public MainLoop {
 	void node_renamed(Node *p_node);
 	void process_timers(double p_delta, bool p_physics_frame);
 	void process_tweens(double p_delta, bool p_physics_frame);
+	// void process_audio_stream_players(double p_delta);
 
 	Group *add_to_group(const StringName &p_group, Node *p_node);
 	void remove_from_group(const StringName &p_group, Node *p_node);
@@ -401,6 +425,10 @@ class SceneTree : public MainLoop {
 
 	Ref<SceneTreeTimer> create_timer(double p_delay_sec, bool p_process_always = true, bool p_process_in_physics = false, bool p_ignore_time_scale = false);
 	Ref<Tween> create_tween();
+	// Ref<SceneTreeAudioStreamPlayer> create_audio_stream_player(Ref<AudioStream> p_stream);
+	AudioStreamPlayer *play_theme_audio(const Ref<AudioStream> &p_stream);
+	AudioStreamPlayer *create_audio_stream_player(const Ref<AudioStream> &p_stream, const StringName &p_bus = SNAME("Master"), float p_volume_db = 0.0f);
+	void _on_audio_finished(AudioStreamPlayer *p_player);
 	TypedArray<Tween> get_processed_tweens();
 
 	//used by Main::start, don't use otherwise
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index d0658c489d9..1ac74b40d09 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -84,6 +84,10 @@ bool Window::_set(const StringName &p_name, const Variant &p_value) {
 			String dname = name.get_slicec('/', 1);
 			theme_constant_override.erase(dname);
 			_notify_theme_override_changed();
+		} else if (name.begins_with("theme_override_audios/")) {
+			String dname = name.get_slicec('/', 1);
+			theme_audio_override.erase(dname);
+			_notify_theme_override_changed();
 		} else {
 			return false;
 		}
@@ -107,6 +111,9 @@ bool Window::_set(const StringName &p_name, const Variant &p_value) {
 		} else if (name.begins_with("theme_override_constants/")) {
 			String dname = name.get_slicec('/', 1);
 			add_theme_constant_override(dname, p_value);
+		} else if (name.begins_with("theme_override_audios/")) {
+			String dname = name.get_slicec('/', 1);
+			add_theme_audio_override(dname, p_value);
 		} else {
 			return false;
 		}
@@ -140,6 +147,9 @@ bool Window::_get(const StringName &p_name, Variant &r_ret) const {
 	} else if (sname.begins_with("theme_override_constants/")) {
 		String name = sname.get_slicec('/', 1);
 		r_ret = theme_constant_override.has(name) ? Variant(theme_constant_override[name]) : Variant();
+	} else if (sname.begins_with("theme_override_audios/")) {
+		String name = sname.get_slicec('/', 1);
+		r_ret = theme_audio_override.has(name) ? Variant(theme_audio_override[name]) : Variant();
 	} else {
 		return false;
 	}
@@ -226,6 +236,18 @@ void Window::_get_property_list(List<PropertyInfo> *p_list) const {
 			p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_styles") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage));
 		}
 	}
+	{
+		List<StringName> names;
+		default_theme->get_stylebox_list(get_class_name(), &names);
+		for (const StringName &E : names) {
+			uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE;
+			if (theme_style_override.has(E)) {
+				usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED;
+			}
+
+			p_list->push_back(PropertyInfo(Variant::OBJECT, PNAME("theme_override_audios") + String("/") + E, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", usage));
+		}
+	}
 }
 
 void Window::_validate_property(PropertyInfo &p_property) const {
@@ -1882,6 +1904,7 @@ void Window::_invalidate_theme_cache() {
 	theme_font_size_cache.clear();
 	theme_color_cache.clear();
 	theme_constant_cache.clear();
+	theme_audio_cache.clear();
 }
 
 void Window::_update_theme_item_cache() {
@@ -2066,6 +2089,30 @@ int Window::get_theme_constant(const StringName &p_name, const StringName &p_the
 	return constant;
 }
 
+Ref<AudioStream> Window::get_theme_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	ERR_READ_THREAD_GUARD_V(Ref<AudioStream>());
+	if (!initialized) {
+		WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED");
+	}
+
+	if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == theme_type_variation) {
+		const Ref<AudioStream> *audio = theme_audio_override.getptr(p_name);
+		if (audio) {
+			return *audio;
+		}
+	}
+
+	if (theme_audio_cache.has(p_theme_type) && theme_audio_cache[p_theme_type].has(p_name)) {
+		return theme_audio_cache[p_theme_type][p_name];
+	}
+
+	List<StringName> theme_types;
+	theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types);
+	Ref<AudioStream> audio = theme_owner->get_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types);
+	theme_audio_cache[p_theme_type][p_name] = audio;
+	return audio;
+}
+
 bool Window::has_theme_icon(const StringName &p_name, const StringName &p_theme_type) const {
 	ERR_READ_THREAD_GUARD_V(false);
 	if (!initialized) {
@@ -2168,6 +2215,23 @@ bool Window::has_theme_constant(const StringName &p_name, const StringName &p_th
 	return theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_CONSTANT, p_name, theme_types);
 }
 
+bool Window::has_theme_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	ERR_READ_THREAD_GUARD_V(false);
+	if (!initialized) {
+		WARN_PRINT_ONCE("Attempting to access theme items too early; prefer NOTIFICATION_POSTINITIALIZE and NOTIFICATION_THEME_CHANGED");
+	}
+
+	if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == theme_type_variation) {
+		if (has_theme_audio_override(p_name)) {
+			return true;
+		}
+	}
+
+	List<StringName> theme_types;
+	theme_owner->get_theme_type_dependencies(this, p_theme_type, &theme_types);
+	return theme_owner->has_theme_item_in_types(Theme::DATA_TYPE_AUDIOSTREAM, p_name, theme_types);
+}
+
 /// Local property overrides.
 
 void Window::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) {
@@ -2227,6 +2291,12 @@ void Window::add_theme_constant_override(const StringName &p_name, int p_constan
 	_notify_theme_override_changed();
 }
 
+void Window::add_theme_audio_override(const StringName &p_name, const Ref<AudioStream> &p_audio) {
+	ERR_MAIN_THREAD_GUARD;
+	theme_audio_override[p_name] = p_audio;
+	_notify_theme_override_changed();
+}
+
 void Window::remove_theme_icon_override(const StringName &p_name) {
 	ERR_MAIN_THREAD_GUARD;
 	if (theme_icon_override.has(p_name)) {
@@ -2275,6 +2345,12 @@ void Window::remove_theme_constant_override(const StringName &p_name) {
 	_notify_theme_override_changed();
 }
 
+void Window::remove_theme_audio_override(const StringName &p_name) {
+	ERR_MAIN_THREAD_GUARD;
+	theme_audio_override.erase(p_name);
+	_notify_theme_override_changed();
+}
+
 bool Window::has_theme_icon_override(const StringName &p_name) const {
 	ERR_READ_THREAD_GUARD_V(false);
 	const Ref<Texture2D> *tex = theme_icon_override.getptr(p_name);
@@ -2311,6 +2387,12 @@ bool Window::has_theme_constant_override(const StringName &p_name) const {
 	return constant != nullptr;
 }
 
+bool Window::has_theme_audio_override(const StringName &p_name) const {
+	ERR_READ_THREAD_GUARD_V(false);
+	const Ref<AudioStream> *audio = theme_audio_override.getptr(p_name);
+	return audio != nullptr;
+}
+
 /// Default theme properties.
 
 float Window::get_theme_default_base_scale() const {
@@ -2614,6 +2696,7 @@ void Window::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Window::add_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Window::add_theme_color_override);
 	ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Window::add_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("add_theme_audio_override", "name", "audio"), &Window::add_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("remove_theme_icon_override", "name"), &Window::remove_theme_icon_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Window::remove_theme_style_override);
@@ -2621,6 +2704,7 @@ void Window::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Window::remove_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Window::remove_theme_color_override);
 	ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Window::remove_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("remove_theme_audio_override", "name"), &Window::remove_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "theme_type"), &Window::get_theme_icon, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "theme_type"), &Window::get_theme_stylebox, DEFVAL(""));
@@ -2628,6 +2712,7 @@ void Window::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "theme_type"), &Window::get_theme_font_size, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_color", "name", "theme_type"), &Window::get_theme_color, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "theme_type"), &Window::get_theme_constant, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("get_theme_audio", "name", "theme_type"), &Window::get_theme_audio, DEFVAL(""));
 
 	ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Window::has_theme_icon_override);
 	ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Window::has_theme_stylebox_override);
@@ -2635,6 +2720,7 @@ void Window::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Window::has_theme_font_size_override);
 	ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Window::has_theme_color_override);
 	ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Window::has_theme_constant_override);
+	ClassDB::bind_method(D_METHOD("has_theme_audio_override", "name"), &Window::has_theme_audio_override);
 
 	ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "theme_type"), &Window::has_theme_icon, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "theme_type"), &Window::has_theme_stylebox, DEFVAL(""));
@@ -2642,6 +2728,7 @@ void Window::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "theme_type"), &Window::has_theme_font_size, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_color", "name", "theme_type"), &Window::has_theme_color, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "theme_type"), &Window::has_theme_constant, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("has_theme_audio", "name", "theme_type"), &Window::has_theme_audio, DEFVAL(""));
 
 	ClassDB::bind_method(D_METHOD("get_theme_default_base_scale"), &Window::get_theme_default_base_scale);
 	ClassDB::bind_method(D_METHOD("get_theme_default_font"), &Window::get_theme_default_font);
@@ -2800,4 +2887,5 @@ Window::~Window() {
 	theme_font_size_override.clear();
 	theme_color_override.clear();
 	theme_constant_override.clear();
+	theme_audio_override.clear();
 }
diff --git a/scene/main/window.h b/scene/main/window.h
index 18ddd896625..594d7b40fcd 100644
--- a/scene/main/window.h
+++ b/scene/main/window.h
@@ -172,6 +172,7 @@ class Window : public Viewport {
 	Theme::ThemeFontSizeMap theme_font_size_override;
 	Theme::ThemeColorMap theme_color_override;
 	Theme::ThemeConstantMap theme_constant_override;
+	Theme::ThemeAudioMap theme_audio_override;
 
 	mutable HashMap<StringName, Theme::ThemeIconMap> theme_icon_cache;
 	mutable HashMap<StringName, Theme::ThemeStyleMap> theme_style_cache;
@@ -179,6 +180,7 @@ class Window : public Viewport {
 	mutable HashMap<StringName, Theme::ThemeFontSizeMap> theme_font_size_cache;
 	mutable HashMap<StringName, Theme::ThemeColorMap> theme_color_cache;
 	mutable HashMap<StringName, Theme::ThemeConstantMap> theme_constant_cache;
+	mutable HashMap<StringName, Theme::ThemeAudioMap> theme_audio_cache;
 
 	void _theme_changed();
 	void _notify_theme_override_changed();
@@ -366,6 +368,7 @@ class Window : public Viewport {
 	void add_theme_font_size_override(const StringName &p_name, int p_font_size);
 	void add_theme_color_override(const StringName &p_name, const Color &p_color);
 	void add_theme_constant_override(const StringName &p_name, int p_constant);
+	void add_theme_audio_override(const StringName &p_name, const Ref<AudioStream> &p_audio);
 
 	void remove_theme_icon_override(const StringName &p_name);
 	void remove_theme_style_override(const StringName &p_name);
@@ -373,6 +376,7 @@ class Window : public Viewport {
 	void remove_theme_font_size_override(const StringName &p_name);
 	void remove_theme_color_override(const StringName &p_name);
 	void remove_theme_constant_override(const StringName &p_name);
+	void remove_theme_audio_override(const StringName &p_name);
 
 	Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
@@ -380,6 +384,7 @@ class Window : public Viewport {
 	int get_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	Color get_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	int get_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
+	Ref<AudioStream> get_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 
 	bool has_theme_icon_override(const StringName &p_name) const;
 	bool has_theme_stylebox_override(const StringName &p_name) const;
@@ -387,6 +392,7 @@ class Window : public Viewport {
 	bool has_theme_font_size_override(const StringName &p_name) const;
 	bool has_theme_color_override(const StringName &p_name) const;
 	bool has_theme_constant_override(const StringName &p_name) const;
+	bool has_theme_audio_override(const StringName &p_name) const;
 
 	bool has_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
@@ -394,6 +400,7 @@ class Window : public Viewport {
 	bool has_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 	bool has_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
+	bool has_theme_audio(const StringName &p_name, const StringName &p_theme_type = StringName()) const;
 
 	float get_theme_default_base_scale() const;
 	Ref<Font> get_theme_default_font() const;
diff --git a/scene/resources/default_theme/default_theme.cpp b/scene/resources/default_theme/default_theme.cpp
index b6a1737acb4..a474a985a27 100644
--- a/scene/resources/default_theme/default_theme.cpp
+++ b/scene/resources/default_theme/default_theme.cpp
@@ -145,9 +145,20 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 		icons[default_theme_icons_names[i]] = generate_icon(i);
 	}
 
+	// Control
+
+	theme->set_audio("focus", "Control", Ref<AudioStream>());
+
 	// Panel
+
 	theme->set_stylebox("panel", "Panel", make_flat_stylebox(style_normal_color, 0, 0, 0, 0));
 
+	// BaseButton
+
+	theme->set_audio("hover", "BaseButton", Ref<AudioStream>());
+	theme->set_audio("pressed", "BaseButton", Ref<AudioStream>());
+	theme->set_audio("pressed_disabled", "BaseButton", Ref<AudioStream>());
+
 	// Button
 
 	const Ref<StyleBoxFlat> button_normal = make_flat_stylebox(style_normal_color);
@@ -420,6 +431,10 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	theme->set_icon("clear", "LineEdit", icons["line_edit_clear"]);
 
+	theme->set_audio("text_changed", "LineEdit", Ref<AudioStream>());
+	theme->set_audio("text_submitted", "LineEdit", Ref<AudioStream>());
+	theme->set_audio("text_change_rejected", "LineEdit", Ref<AudioStream>());
+
 	// ProgressBar
 
 	theme->set_stylebox("background", "ProgressBar", make_flat_stylebox(style_disabled_color, 2, 2, 2, 2, 6));
@@ -464,6 +479,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("outline_size", "TextEdit", 0);
 	theme->set_constant("caret_width", "TextEdit", 1);
 
+	theme->set_audio("text_changed", "TextEdit", Ref<AudioStream>());
+
 	// CodeEdit
 
 	theme->set_stylebox("normal", "CodeEdit", style_line_edit);
@@ -558,6 +575,12 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	const Ref<StyleBoxFlat> style_slider_grabber = make_flat_stylebox(style_progress_color, 4, 4, 4, 4, 4);
 	const Ref<StyleBoxFlat> style_slider_grabber_highlight = make_flat_stylebox(style_focus_color, 4, 4, 4, 4, 4);
 
+	// Slider
+
+	theme->set_audio("drag_started", "Slider", Ref<AudioStream>());
+	theme->set_audio("drag_ended", "Slider", Ref<AudioStream>());
+	theme->set_audio("value_changed", "Slider", Ref<AudioStream>());
+
 	// HSlider
 
 	theme->set_stylebox("slider", "HSlider", style_slider);
@@ -590,6 +613,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	theme->set_icon("updown", "SpinBox", icons["updown"]);
 
+	theme->set_audio("pressed", "SpinBox", Ref<AudioStream>());
+
 	// ScrollContainer
 
 	Ref<StyleBoxEmpty> empty;
@@ -698,6 +723,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("item_end_padding", "PopupMenu", Math::round(2 * scale));
 	theme->set_constant("icon_max_width", "PopupMenu", 0);
 
+	theme->set_audio("item_activated", "PopupMenu", Ref<AudioStream>());
+	theme->set_audio("item_disabled_activated", "PopupMenu", Ref<AudioStream>());
+
 	// GraphNode
 	Ref<StyleBoxFlat> graphnode_normal = make_flat_stylebox(style_normal_color, 18, 42, 18, 12);
 	graphnode_normal->set_border_width(SIDE_TOP, 30);
@@ -799,6 +827,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("scrollbar_h_separation", "Tree", Math::round(4 * scale));
 	theme->set_constant("scrollbar_v_separation", "Tree", Math::round(4 * scale));
 
+	theme->set_audio("item_selected", "Tree", Ref<AudioStream>());
+
 	// ItemList
 
 	theme->set_stylebox("panel", "ItemList", make_flat_stylebox(style_normal_color));
@@ -824,6 +854,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 
 	theme->set_constant("outline_size", "ItemList", 0);
 
+	theme->set_audio("item_selected", "ItemList", Ref<AudioStream>());
+	theme->set_audio("item_disabled_selected", "ItemList", Ref<AudioStream>());
+
 	// TabContainer
 
 	Ref<StyleBoxFlat> style_tab_selected = make_flat_stylebox(style_normal_color, 10, 4, 10, 4, 0);
@@ -899,6 +932,10 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_constant("icon_max_width", "TabBar", 0);
 	theme->set_constant("outline_size", "TabBar", 0);
 
+	theme->set_audio("hover", "TabBar", Ref<AudioStream>());
+	theme->set_audio("pressed", "TabBar", Ref<AudioStream>());
+	theme->set_audio("pressed_disabled", "TabBar", Ref<AudioStream>());
+
 	// Separators
 
 	theme->set_stylebox("separator", "HSeparator", separator_horizontal);
diff --git a/scene/resources/theme.cpp b/scene/resources/theme.cpp
index 799a8471b90..fc6e448b187 100644
--- a/scene/resources/theme.cpp
+++ b/scene/resources/theme.cpp
@@ -54,6 +54,8 @@ bool Theme::_set(const StringName &p_name, const Variant &p_value) {
 			set_color(prop_name, theme_type, p_value);
 		} else if (type == "constants") {
 			set_constant(prop_name, theme_type, p_value);
+		} else if (type == "audios") {
+			set_audio(prop_name, theme_type, p_value);
 		} else if (type == "base_type") {
 			set_type_variation(theme_type, p_value);
 		} else {
@@ -98,6 +100,12 @@ bool Theme::_get(const StringName &p_name, Variant &r_ret) const {
 			r_ret = get_color(prop_name, theme_type);
 		} else if (type == "constants") {
 			r_ret = get_constant(prop_name, theme_type);
+		} else if (type == "audios") {
+			if (!has_audio(prop_name, theme_type)) {
+				r_ret = Ref<AudioStream>();
+			} else {
+				r_ret = get_audio(prop_name, theme_type);
+			}
 		} else if (type == "base_type") {
 			r_ret = get_type_variation_base(theme_type);
 		} else {
@@ -160,6 +168,13 @@ void Theme::_get_property_list(List<PropertyInfo> *p_list) const {
 		}
 	}
 
+	// Audios.
+	for (const KeyValue<StringName, ThemeAudioMap> &E : audio_map) {
+		for (const KeyValue<StringName, Ref<AudioStream>> &F : E.value) {
+			list.push_back(PropertyInfo(Variant::OBJECT, String() + E.key + "/audios/" + F.key, PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL));
+		}
+	}
+
 	// Sort and store properties.
 	list.sort();
 	String prev_type;
@@ -852,6 +867,115 @@ void Theme::get_constant_type_list(List<StringName> *p_list) const {
 	}
 }
 
+void Theme::set_audio(const StringName &p_name, const StringName &p_theme_type, const Ref<AudioStream> &p_audio) {
+	ERR_FAIL_COND_MSG(!is_valid_item_name(p_name), vformat("Invalid item name: '%s'", p_name));
+	ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type));
+
+	bool existing = false;
+	if (audio_map[p_theme_type][p_name].is_valid()) {
+		existing = true;
+		audio_map[p_theme_type][p_name]->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed));
+	}
+
+	audio_map[p_theme_type][p_name] = p_audio;
+
+	if (p_audio.is_valid()) {
+		audio_map[p_theme_type][p_name]->connect_changed(callable_mp(this, &Theme::_emit_theme_changed).bind(false), CONNECT_REFERENCE_COUNTED);
+	}
+
+	_emit_theme_changed(!existing);
+}
+
+Ref<AudioStream> Theme::get_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	if (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name) && audio_map[p_theme_type][p_name].is_valid()) {
+		return audio_map[p_theme_type][p_name];
+	} else {
+		return ThemeDB::get_singleton()->get_fallback_audio();
+	}
+}
+
+bool Theme::has_audio(const StringName &p_name, const StringName &p_theme_type) const {
+	return (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name) && audio_map[p_theme_type][p_name].is_valid());
+}
+
+bool Theme::has_audio_nocheck(const StringName &p_name, const StringName &p_theme_type) const {
+	return (audio_map.has(p_theme_type) && audio_map[p_theme_type].has(p_name));
+}
+
+void Theme::rename_audio(const StringName &p_old_name, const StringName &p_name, const StringName &p_theme_type) {
+	ERR_FAIL_COND_MSG(!is_valid_item_name(p_name), vformat("Invalid item name: '%s'", p_name));
+	ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type));
+	ERR_FAIL_COND_MSG(!audio_map.has(p_theme_type), "Cannot rename the audio '" + String(p_old_name) + "' because the node type '" + String(p_theme_type) + "' does not exist.");
+	ERR_FAIL_COND_MSG(audio_map[p_theme_type].has(p_name), "Cannot rename the audio '" + String(p_old_name) + "' because the new name '" + String(p_name) + "' already exists.");
+	ERR_FAIL_COND_MSG(!audio_map[p_theme_type].has(p_old_name), "Cannot rename the audio '" + String(p_old_name) + "' because it does not exist.");
+
+	audio_map[p_theme_type][p_name] = audio_map[p_theme_type][p_old_name];
+	audio_map[p_theme_type].erase(p_old_name);
+
+	_emit_theme_changed(true);
+}
+
+void Theme::clear_audio(const StringName &p_name, const StringName &p_theme_type) {
+	ERR_FAIL_COND_MSG(!audio_map.has(p_theme_type), "Cannot clear the audio '" + String(p_name) + "' because the node type '" + String(p_theme_type) + "' does not exist.");
+	ERR_FAIL_COND_MSG(!audio_map[p_theme_type].has(p_name), "Cannot clear the audio '" + String(p_name) + "' because it does not exist.");
+
+	if (audio_map[p_theme_type][p_name].is_valid()) {
+		audio_map[p_theme_type][p_name]->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed));
+	}
+
+	audio_map[p_theme_type].erase(p_name);
+
+	_emit_theme_changed(true);
+}
+
+void Theme::get_audio_list(StringName p_theme_type, List<StringName> *p_list) const {
+	ERR_FAIL_NULL(p_list);
+
+	if (!audio_map.has(p_theme_type)) {
+		return;
+	}
+
+	for (const KeyValue<StringName, Ref<AudioStream>> &E : audio_map[p_theme_type]) {
+		p_list->push_back(E.key);
+	}
+}
+
+void Theme::add_audio_type(const StringName &p_theme_type) {
+	ERR_FAIL_COND_MSG(!is_valid_type_name(p_theme_type), vformat("Invalid type name: '%s'", p_theme_type));
+
+	if (audio_map.has(p_theme_type)) {
+		return;
+	}
+	audio_map[p_theme_type] = ThemeAudioMap();
+}
+
+void Theme::remove_audio_type(const StringName &p_theme_type) {
+	if (!audio_map.has(p_theme_type)) {
+		return;
+	}
+
+	_freeze_change_propagation();
+
+	for (const KeyValue<StringName, Ref<AudioStream>> &E : audio_map[p_theme_type]) {
+		Ref<AudioStream> audio = E.value;
+		if (audio.is_valid()) {
+			audio->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed));
+		}
+	}
+
+	audio_map.erase(p_theme_type);
+
+	_unfreeze_and_propagate_changes();
+}
+
+void Theme::get_audio_type_list(List<StringName> *p_list) const {
+	ERR_FAIL_NULL(p_list);
+
+	for (const KeyValue<StringName, ThemeAudioMap> &E : audio_map) {
+		p_list->push_back(E.key);
+	}
+}
+
 // Generic methods for managing theme items.
 void Theme::set_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type, const Variant &p_value) {
 	switch (p_data_type) {
@@ -891,6 +1015,12 @@ void Theme::set_theme_item(DataType p_data_type, const StringName &p_name, const
 			Ref<StyleBox> stylebox_value = Object::cast_to<StyleBox>(p_value.get_validated_object());
 			set_stylebox(p_name, p_theme_type, stylebox_value);
 		} break;
+		case DATA_TYPE_AUDIOSTREAM: {
+			ERR_FAIL_COND_MSG(p_value.get_type() != Variant::OBJECT, "Theme item's data type (Object) does not match Variant's type (" + Variant::get_type_name(p_value.get_type()) + ").");
+
+			Ref<AudioStream> audio_value = Object::cast_to<AudioStream>(p_value.get_validated_object());
+			set_audio(p_name, p_theme_type, audio_value);
+		} break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -910,6 +1040,8 @@ Variant Theme::get_theme_item(DataType p_data_type, const StringName &p_name, co
 			return get_icon(p_name, p_theme_type);
 		case DATA_TYPE_STYLEBOX:
 			return get_stylebox(p_name, p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			return get_audio(p_name, p_theme_type);
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -931,6 +1063,8 @@ bool Theme::has_theme_item(DataType p_data_type, const StringName &p_name, const
 			return has_icon(p_name, p_theme_type);
 		case DATA_TYPE_STYLEBOX:
 			return has_stylebox(p_name, p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			return has_audio(p_name, p_theme_type);
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -952,6 +1086,8 @@ bool Theme::has_theme_item_nocheck(DataType p_data_type, const StringName &p_nam
 			return has_icon_nocheck(p_name, p_theme_type);
 		case DATA_TYPE_STYLEBOX:
 			return has_stylebox_nocheck(p_name, p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			return has_audio_nocheck(p_name, p_theme_type);
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -978,6 +1114,8 @@ void Theme::rename_theme_item(DataType p_data_type, const StringName &p_old_name
 			break;
 		case DATA_TYPE_STYLEBOX:
 			rename_stylebox(p_old_name, p_name, p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			rename_audio(p_old_name, p_name, p_theme_type);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1003,6 +1141,8 @@ void Theme::clear_theme_item(DataType p_data_type, const StringName &p_name, con
 			break;
 		case DATA_TYPE_STYLEBOX:
 			clear_stylebox(p_name, p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			clear_audio(p_name, p_theme_type);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1028,6 +1168,8 @@ void Theme::get_theme_item_list(DataType p_data_type, StringName p_theme_type, L
 			break;
 		case DATA_TYPE_STYLEBOX:
 			get_stylebox_list(p_theme_type, p_list);
+		case DATA_TYPE_AUDIOSTREAM:
+			get_audio_list(p_theme_type, p_list);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1053,6 +1195,8 @@ void Theme::add_theme_item_type(DataType p_data_type, const StringName &p_theme_
 			break;
 		case DATA_TYPE_STYLEBOX:
 			add_stylebox_type(p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			add_audio_type(p_theme_type);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1078,6 +1222,8 @@ void Theme::remove_theme_item_type(DataType p_data_type, const StringName &p_the
 			break;
 		case DATA_TYPE_STYLEBOX:
 			remove_stylebox_type(p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			remove_audio_type(p_theme_type);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1103,6 +1249,8 @@ void Theme::get_theme_item_type_list(DataType p_data_type, List<StringName> *p_l
 			break;
 		case DATA_TYPE_STYLEBOX:
 			get_stylebox_type_list(p_list);
+		case DATA_TYPE_AUDIOSTREAM:
+			get_audio_type_list(p_list);
 			break;
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
@@ -1240,6 +1388,11 @@ void Theme::get_type_list(List<StringName> *p_list) const {
 		types.insert(E.key);
 	}
 
+	// Audios.
+	for (const KeyValue<StringName, ThemeAudioMap> &E : audio_map) {
+		types.insert(E.key);
+	}
+
 	for (const StringName &E : types) {
 		p_list->push_back(E);
 	}
@@ -1451,6 +1604,36 @@ Vector<String> Theme::_get_constant_type_list() const {
 	return ilret;
 }
 
+Vector<String> Theme::_get_audio_list(const String &p_theme_type) const {
+	Vector<String> ilret;
+	List<StringName> il;
+
+	get_audio_list(p_theme_type, &il);
+	ilret.resize(il.size());
+
+	int i = 0;
+	String *w = ilret.ptrw();
+	for (List<StringName>::Element *E = il.front(); E; E = E->next(), i++) {
+		w[i] = E->get();
+	}
+	return ilret;
+}
+
+Vector<String> Theme::_get_audio_type_list() const {
+	Vector<String> ilret;
+	List<StringName> il;
+
+	get_audio_type_list(&il);
+	ilret.resize(il.size());
+
+	int i = 0;
+	String *w = ilret.ptrw();
+	for (List<StringName>::Element *E = il.front(); E; E = E->next(), i++) {
+		w[i] = E->get();
+	}
+	return ilret;
+}
+
 Vector<String> Theme::_get_theme_item_list(DataType p_data_type, const String &p_theme_type) const {
 	switch (p_data_type) {
 		case DATA_TYPE_COLOR:
@@ -1465,6 +1648,8 @@ Vector<String> Theme::_get_theme_item_list(DataType p_data_type, const String &p
 			return _get_icon_list(p_theme_type);
 		case DATA_TYPE_STYLEBOX:
 			return _get_stylebox_list(p_theme_type);
+		case DATA_TYPE_AUDIOSTREAM:
+			return _get_audio_list(p_theme_type);
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -1486,6 +1671,8 @@ Vector<String> Theme::_get_theme_item_type_list(DataType p_data_type) const {
 			return _get_icon_type_list();
 		case DATA_TYPE_STYLEBOX:
 			return _get_stylebox_type_list();
+		case DATA_TYPE_AUDIOSTREAM:
+			return _get_audio_type_list();
 		case DATA_TYPE_MAX:
 			break; // Can't happen, but silences warning.
 	}
@@ -1605,6 +1792,15 @@ void Theme::merge_with(const Ref<Theme> &p_other) {
 		}
 	}
 
+	// Audios.
+	{
+		for (const KeyValue<StringName, ThemeAudioMap> &E : p_other->audio_map) {
+			for (const KeyValue<StringName, Ref<AudioStream>> &F : E.value) {
+				set_audio(F.key, E.key, F.value);
+			}
+		}
+	}
+
 	// Type variations.
 	{
 		for (const KeyValue<StringName, StringName> &E : p_other->variation_map) {
@@ -1650,12 +1846,24 @@ void Theme::clear() {
 		}
 	}
 
+	{
+		for (const KeyValue<StringName, ThemeAudioMap> &E : audio_map) {
+			for (const KeyValue<StringName, Ref<AudioStream>> &F : E.value) {
+				if (F.value.is_valid()) {
+					Ref<AudioStream> audio = F.value;
+					audio->disconnect_changed(callable_mp(this, &Theme::_emit_theme_changed));
+				}
+			}
+		}
+	}
+
 	icon_map.clear();
 	style_map.clear();
 	font_map.clear();
 	font_size_map.clear();
 	color_map.clear();
 	constant_map.clear();
+	audio_map.clear();
 
 	variation_map.clear();
 	variation_base_map.clear();
@@ -1716,6 +1924,14 @@ void Theme::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_constant_list", "theme_type"), &Theme::_get_constant_list);
 	ClassDB::bind_method(D_METHOD("get_constant_type_list"), &Theme::_get_constant_type_list);
 
+	ClassDB::bind_method(D_METHOD("set_audio", "name", "theme_type", "audio"), &Theme::set_audio);
+	ClassDB::bind_method(D_METHOD("get_audio", "name", "theme_type"), &Theme::get_audio);
+	ClassDB::bind_method(D_METHOD("has_audio", "name", "theme_type"), &Theme::has_audio);
+	ClassDB::bind_method(D_METHOD("rename_audio", "old_name", "name", "theme_type"), &Theme::rename_audio);
+	ClassDB::bind_method(D_METHOD("clear_audio", "name", "theme_type"), &Theme::clear_audio);
+	ClassDB::bind_method(D_METHOD("get_audio_list", "theme_type"), &Theme::_get_audio_list);
+	ClassDB::bind_method(D_METHOD("get_audio_type_list"), &Theme::_get_font_type_list);
+
 	ClassDB::bind_method(D_METHOD("set_default_base_scale", "base_scale"), &Theme::set_default_base_scale);
 	ClassDB::bind_method(D_METHOD("get_default_base_scale"), &Theme::get_default_base_scale);
 	ClassDB::bind_method(D_METHOD("has_default_base_scale"), &Theme::has_default_base_scale);
@@ -1759,6 +1975,7 @@ void Theme::_bind_methods() {
 	BIND_ENUM_CONSTANT(DATA_TYPE_FONT_SIZE);
 	BIND_ENUM_CONSTANT(DATA_TYPE_ICON);
 	BIND_ENUM_CONSTANT(DATA_TYPE_STYLEBOX);
+	BIND_ENUM_CONSTANT(DATA_TYPE_AUDIOSTREAM);
 	BIND_ENUM_CONSTANT(DATA_TYPE_MAX);
 }
 
diff --git a/scene/resources/theme.h b/scene/resources/theme.h
index c7a76f4c5ec..53d14f5982b 100644
--- a/scene/resources/theme.h
+++ b/scene/resources/theme.h
@@ -35,6 +35,7 @@
 #include "scene/resources/font.h"
 #include "scene/resources/style_box.h"
 #include "scene/resources/texture.h"
+#include "servers/audio/audio_stream.h"
 
 class Theme : public Resource {
 	GDCLASS(Theme, Resource);
@@ -53,6 +54,7 @@ class Theme : public Resource {
 	using ThemeFontSizeMap = HashMap<StringName, int>;
 	using ThemeColorMap = HashMap<StringName, Color>;
 	using ThemeConstantMap = HashMap<StringName, int>;
+	using ThemeAudioMap = HashMap<StringName, Ref<AudioStream>>;
 
 	enum DataType {
 		DATA_TYPE_COLOR,
@@ -61,6 +63,7 @@ class Theme : public Resource {
 		DATA_TYPE_FONT_SIZE,
 		DATA_TYPE_ICON,
 		DATA_TYPE_STYLEBOX,
+		DATA_TYPE_AUDIOSTREAM,
 		DATA_TYPE_MAX
 	};
 
@@ -75,6 +78,7 @@ class Theme : public Resource {
 	HashMap<StringName, ThemeFontSizeMap> font_size_map;
 	HashMap<StringName, ThemeColorMap> color_map;
 	HashMap<StringName, ThemeConstantMap> constant_map;
+	HashMap<StringName, ThemeAudioMap> audio_map;
 	HashMap<StringName, StringName> variation_map;
 	HashMap<StringName, List<StringName>> variation_base_map;
 
@@ -90,6 +94,8 @@ class Theme : public Resource {
 	Vector<String> _get_color_type_list() const;
 	Vector<String> _get_constant_list(const String &p_theme_type) const;
 	Vector<String> _get_constant_type_list() const;
+	Vector<String> _get_audio_list(const String &p_theme_type) const;
+	Vector<String> _get_audio_type_list() const;
 
 	Vector<String> _get_theme_item_list(DataType p_data_type, const String &p_theme_type) const;
 	Vector<String> _get_theme_item_type_list(DataType p_data_type) const;
@@ -196,6 +202,17 @@ class Theme : public Resource {
 	void remove_constant_type(const StringName &p_theme_type);
 	void get_constant_type_list(List<StringName> *p_list) const;
 
+	void set_audio(const StringName &p_name, const StringName &p_theme_type, const Ref<AudioStream> &p_audio);
+	Ref<AudioStream> get_audio(const StringName &p_name, const StringName &p_theme_type) const;
+	bool has_audio(const StringName &p_name, const StringName &p_theme_type) const;
+	bool has_audio_nocheck(const StringName &p_name, const StringName &p_theme_type) const;
+	void rename_audio(const StringName &p_old_name, const StringName &p_name, const StringName &p_theme_type);
+	void clear_audio(const StringName &p_name, const StringName &p_theme_type);
+	void get_audio_list(StringName p_theme_type, List<StringName> *p_list) const;
+	void add_audio_type(const StringName &p_theme_type);
+	void remove_audio_type(const StringName &p_theme_type);
+	void get_audio_type_list(List<StringName> *p_list) const;
+
 	void set_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type, const Variant &p_value);
 	Variant get_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type) const;
 	bool has_theme_item(DataType p_data_type, const StringName &p_name, const StringName &p_theme_type) const;
diff --git a/scene/theme/theme_db.cpp b/scene/theme/theme_db.cpp
index 9b85a62c6ec..87f6148fc15 100644
--- a/scene/theme/theme_db.cpp
+++ b/scene/theme/theme_db.cpp
@@ -176,6 +176,19 @@ Ref<StyleBox> ThemeDB::get_fallback_stylebox() {
 	return fallback_stylebox;
 }
 
+void ThemeDB::set_fallback_audio(const Ref<AudioStream> &p_audio) {
+	if (fallback_audio == p_audio) {
+		return;
+	}
+
+	fallback_audio = p_audio;
+	emit_signal(SNAME("fallback_changed"));
+}
+
+Ref<AudioStream> ThemeDB::get_fallback_audio() {
+	return fallback_audio;
+}
+
 // Object methods.
 void ThemeDB::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_default_theme"), &ThemeDB::get_default_theme);
@@ -191,6 +204,8 @@ void ThemeDB::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_fallback_icon"), &ThemeDB::get_fallback_icon);
 	ClassDB::bind_method(D_METHOD("set_fallback_stylebox", "stylebox"), &ThemeDB::set_fallback_stylebox);
 	ClassDB::bind_method(D_METHOD("get_fallback_stylebox"), &ThemeDB::get_fallback_stylebox);
+	ClassDB::bind_method(D_METHOD("set_fallback_audio", "audio"), &ThemeDB::set_fallback_audio);
+	ClassDB::bind_method(D_METHOD("get_fallback_audio"), &ThemeDB::get_fallback_audio);
 
 	ADD_GROUP("Fallback values", "fallback_");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fallback_base_scale", PROPERTY_HINT_RANGE, "0.0,2.0,0.01,or_greater"), "set_fallback_base_scale", "get_fallback_base_scale");
@@ -198,6 +213,7 @@ void ThemeDB::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "fallback_font_size", PROPERTY_HINT_RANGE, "0,256,1,or_greater,suffix:px"), "set_fallback_font_size", "get_fallback_font_size");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_NONE), "set_fallback_icon", "get_fallback_icon");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_stylebox", PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", PROPERTY_USAGE_NONE), "set_fallback_stylebox", "get_fallback_stylebox");
+	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "fallback_audio", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream", PROPERTY_USAGE_NONE), "set_fallback_audio", "get_fallback_audio");
 
 	ADD_SIGNAL(MethodInfo("fallback_changed"));
 }
@@ -224,6 +240,7 @@ ThemeDB::~ThemeDB() {
 	fallback_font.unref();
 	fallback_icon.unref();
 	fallback_stylebox.unref();
+	fallback_audio.unref();
 
 	singleton = nullptr;
 }
diff --git a/scene/theme/theme_db.h b/scene/theme/theme_db.h
index f65899f5ea7..916d8fbfc63 100644
--- a/scene/theme/theme_db.h
+++ b/scene/theme/theme_db.h
@@ -35,6 +35,7 @@
 #include "core/object/ref_counted.h"
 
 class Font;
+class AudioStream;
 class StyleBox;
 class Texture2D;
 class Theme;
@@ -54,6 +55,7 @@ class ThemeDB : public Object {
 	int fallback_font_size;
 	Ref<Texture2D> fallback_icon;
 	Ref<StyleBox> fallback_stylebox;
+	Ref<AudioStream> fallback_audio;
 
 protected:
 	static void _bind_methods();
@@ -87,6 +89,9 @@ class ThemeDB : public Object {
 	void set_fallback_stylebox(const Ref<StyleBox> &p_stylebox);
 	Ref<StyleBox> get_fallback_stylebox();
 
+	void set_fallback_audio(const Ref<AudioStream> &p_audio);
+	Ref<AudioStream> get_fallback_audio();
+
 	static ThemeDB *get_singleton();
 	ThemeDB();
 	~ThemeDB();
simplescreenrecorder-2023-08-07_11.34.57.mp4

Sounds from https://github.com/redeclipse/sounds.

There are still some things to iron out (specifically regarding the inspector and --doctool not "seeing" audio theme items for some reason), but it's getting there.

@conde2
Copy link

conde2 commented Feb 26, 2024

Is there a pull request for this in godot master? This is a must feature.
@Calinou amazing work!

@Calinou
Copy link
Member

Calinou commented Feb 26, 2024

Is there a pull request for this in godot master? This is a must feature.

Not yet, as my branch still has some work left to do (check the TODO part of the commit message). This is too late for 4.3 either way, which is entering feature freeze soon.

@conde2
Copy link

conde2 commented Feb 27, 2024

That's sad to hear. Maybe this comes to the 4.4 then? For the video it was looking very promising, keep the good work.

I will leave it out here as I don't have much to contribute to the proposal, but imo audio theme UI is brilliant suggestion that I strongly support.

@TruFelix
Copy link

TruFelix commented Aug 9, 2024

I am too waiting for this feature! But I also need to add sounds to disabled buttons as well.

@Calinou
Copy link
Member

Calinou commented Aug 10, 2024

But I also need to add sounds to disabled buttons as well.

The branch I linked above already handles this for some controls, although Button doesn't play its disabled sounds when pressed yet. This will require some refactoring of the GUI input code in BaseButton so that some of its code is run even when the button is disabled.

@ch0m5
Copy link

ch0m5 commented Oct 29, 2024

Is there an ETA for this feature or someone actively working on it? I've been following this thread for a while but I don't know how far has the feature progressed.

I'm working the pre-production of a UI-heavy project of considerable size and I'm unsure if I should account for this being in the engine sometime in the near future regarding the game's UI, and currently working around it requires to define a specific behavior for most if not all Buttons in a game.

Progress so far looks great!

@Calinou
Copy link
Member

Calinou commented Oct 29, 2024

Is there an ETA for this feature or someone actively working on it?

I'm still working on the feature on-and-off, but I can't give an ETA. I rebased it on top of master a few weeks ago, but several items remain to be implemented before I can open a PR.

I'm working the pre-production of a UI-heavy project of considerable size and I'm unsure if I should account for this being in the engine sometime in the near future regarding the game's UI, and currently working around it requires to define a specific behavior for most if not all Buttons in a game.

I wouldn't expect this to be finished in time for 4.4, especially since the PR would need to be reviewed and the diff is quite large.

If you need the feature right now, you can always take the branch, rebase it on top of master and compile custom editor and export template binaries. There are some missing features and quirks noted in the commit message though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants