Repository: WhoStoleMyCoffee/raytraced-audio Branch: main Commit: a1915cb511cc Files: 17 Total size: 38.4 KB Directory structure: gitextract_3z3d8ftl/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── addons/ │ └── raytraced_audio/ │ ├── LICENSE │ ├── README.md │ ├── audio_ray.gd │ ├── plugin.cfg │ ├── plugin.gd │ ├── raytraced_audio_listener.gd │ └── raytraced_audio_player_3d.gd ├── default_bus_layout.tres ├── icon.png.import ├── icon.svg.import └── project.godot ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 ================================================ FILE: .gitattributes ================================================ # Normalize EOL for all files that Git considers text files. * text=auto eol=lf # Only include the adodns folder when downloading from the Asset Library /** export-ignore /addons !export-ignore /addons/** !export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: tienne_k tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ test_scene/ *.uid # Godot 4+ specific ignores .godot/ /android/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Tienne_k Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # raytraced-audio Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle. [Showcase Video](https://youtu.be/rFauZQ-tFTg?si=eUklNxLSs8ew2Zp6) Check out [this amazing video](https://youtu.be/u6EuAUjq92k?si=6W-sGozYBQITEgQo) by **Vercidium** for insights on how this works! No need to create and maintain zones while creating levels, just put the `RaytracedAudioListener` node in your scene and most features will be available for you to use! Additionally, by using `RaytracedAudioPlayer3D`s instead of regular `AudioPlayer3D`s, sounds will get automatically muffled behind walls. ### Audio buses Right off the bat, this plugin creates 2 new audio buses for you to use across your project: a *"Reverb"* bus, and an *"Ambient"* bus. Note: both buses' names can be changed under `Project Settings > Raytraced Audio`. The reverb bus controls echo / reverb. For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open. The ambient bus controls the strength and pan of sounds coming from outside. For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it. ### Performace Because the rays need to gather different informations about the environment, the actual number of processed rays can go up to: `rays_count * (2 + n)` where `n` is the number of enabled `RaytracedAudioPlayer3D`s in the scene. That is: `(rays_count: configurable) * (1 ray that bounces around + 1 echo ray + (1 muffle ray per enabled RaytracedAudioPlayer3D))` Raytraced Audio also adds 2 performance monitors: - `raytraced_audio/raycast_updates`: How many raycast updates happened in the update tick - `raytraced_audio/enabled_players_count`: How many `RaytracedAudioPlayer3D`s are currently enabled in the scene ### Installation #### Manual installation - Download or clone this repository - Copy the `addons/raytraced_audio` folder into your project's `addons/` folder - Enable the plugin in `Project Settings > Plugins > Raytraced Audio` ================================================ FILE: addons/raytraced_audio/LICENSE ================================================ MIT License Copyright (c) 2025 Tienne_k Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: addons/raytraced_audio/README.md ================================================ # raytraced-audio Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle. Check out [this amazing video](https://youtu.be/u6EuAUjq92k?si=6W-sGozYBQITEgQo) by **Vercidium** for insights on how this works! No need to create and maintain zones while creating levels, just put the `RaytracedAudioListener` node in your scene and most features will be available for you to use! Additionally, by using `RaytracedAudioPlayer3D`s instead of regular `AudioPlayer3D`s, sounds will get automatically muffled behind walls. ### Audio buses Right off the bat, this plugin creates 2 new audio buses for you to use across your project: a *"Reverb"* bus, and an *"Ambient"* bus. Note: both buses' names can be changed under `Project Settings > Raytraced Audio`. The reverb bus controls echo / reverb. For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open. The ambient bus controls the strength and pan of sounds coming from outside. For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it. ### Performace Because the rays need to gather different informations about the environment, the actual number of processed rays can go up to: `rays_count * (2 + n)` where `n` is the number of enabled `RaytracedAudioPlayer3D`s in the scene. That is: `(rays_count: configurable) * (1 ray that bounces around + 1 echo ray + (1 muffle ray per enabled RaytracedAudioPlayer3D))` Raytraced Audio also adds 2 performance monitors: - `raytraced_audio/raycast_updates`: How many raycast updates happened in the update tick - `raytraced_audio/enabled_players_count`: How many `RaytracedAudioPlayer3D`s are currently enabled in the scene ### Installation #### Manual installation - Download or clone this repository - Copy the `addons/raytraced_audio` folder into your project's `addons/` folder - Enable the plugin in `Project Settings > Plugins > Raytraced Audio` ================================================ FILE: addons/raytraced_audio/audio_ray.gd ================================================ extends RayCast3D # dont you worry about this class, habibi # shhhhhh its okay # ( - ͜ʖ -)☞(ʘ_ʘ; ) var cast_dist: float = 0.0 var max_bounces: int = 1 var ray_scatter: Callable var echo_dist: float = 0.0 var echo_count: int = 0 var bounces: int = 0 var has_bounced_this_tick: bool = false var ray_casts_this_tick: int = 0 var escaped: bool = false var escape_dir: Vector3 = Vector3.ZERO var _space_state: PhysicsDirectSpaceState3D func _init(raycast_dist: float, max_bounce_count: int) -> void: top_level = true enabled = false cast_dist = raycast_dist max_bounces = max_bounce_count func _enter_tree() -> void: owner = get_parent() assert(owner is RaytracedAudioListener) func _ready() -> void: _space_state = get_world_3d().direct_space_state reset() # TODO: optimize func update(): # Reset if needed if escaped or bounces > max_bounces: reset() force_raycast_update() ray_casts_this_tick += 1 bounces += 1 # Escaped outside if !is_colliding(): escaped = true global_position += target_position # Muffle ray: Check for line of sight with audio players for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME): var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty() player._lowpass_rays_count += int(has_line_of_sight) return # Bounce var hit_pos: Vector3 = get_collision_point() var normal: Vector3 = get_collision_normal() global_position = hit_pos + normal * 0.1 target_position = target_position.bounce(normal) has_bounced_this_tick = true # Muffle ray: Check for line of sight with audio players for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME): var has_line_of_sight: bool = _cast_ray(player.global_position).is_empty() player._lowpass_rays_count += int(has_line_of_sight) # Same as: # if has_line_of_sight: # player.lowpass_rays_count += 1 # Echo ray: Check for line of light with listener # Optimization: no need to raycast for first echo bounce if bounces == 1: echo_dist = hit_pos.distance_to(owner.global_position) echo_count += 1 escape_dir = target_position.normalized() elif _cast_ray(owner.global_position).is_empty(): # The way is clear -> echo echo_dist = hit_pos.distance_to(owner.global_position) echo_count += 1 escape_dir = owner.global_position.direction_to(hit_pos) # Return to the listener with a random direction func reset(): var dir: Vector3 = ray_scatter.call() global_position = owner.global_position target_position = dir * cast_dist bounces = 0 escaped = false escape_dir = dir reset_tick_stats() # Called after this ray is done ticking, in RaytracedAudioListener func reset_tick_stats() -> void: has_bounced_this_tick = false ray_casts_this_tick = 0 echo_dist = 0.0 echo_count = 0 func set_scatter_model(model: RaytracedAudioListener.RayScatterModel) -> void: match model: RaytracedAudioListener.RayScatterModel.RANDOM: ray_scatter = _random_dir RaytracedAudioListener.RayScatterModel.XZ: ray_scatter = _random_dir_xz_plane _: push_error("Unknown ray scatter model: '", model, "'") ray_scatter = _random_dir func _cast_ray(to: Vector3) -> Dictionary: var params: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create( global_position, to, 0b01 ) ray_casts_this_tick += 1 return _space_state.intersect_ray(params) func _random_dir_xz_plane() -> Vector3: var yaw = randf_range(0.0, TAU) return Vector3(-sin(yaw), 0.0, -cos(yaw)) func _random_dir() -> Vector3: var theta: float = randf_range(0.0, TAU) var y: float = randf_range(-1.0, 1.0) var k: float = sqrt(1.0 - y*y) return Vector3(k * cos(theta), k * sin(theta), y) ================================================ FILE: addons/raytraced_audio/plugin.cfg ================================================ [plugin] name="Raytraced Audio" description="Adds procedural audio effects to Godot like echo, ambient outdoor sounds, and muffle" author="Tienne_k" version="1.0" script="plugin.gd" ================================================ FILE: addons/raytraced_audio/plugin.gd ================================================ @tool extends EditorPlugin func _enter_tree() -> void: _setup_settings() # For some reason, adding custom types like this fails to generate documentation sometimes?? # add_custom_type( # "RaytracedAudioListener", # "AudioListener3D", # load("res://addons/raytraced_audio/raytraced_audio_listener.gd"), # null # ) # add_custom_type( # "RaytracedAudioPlayer3D", # "AudioStreamPlayer3D", # load("res://addons/raytraced_audio/raytraced_audio_player_3d.gd"), # null # ) _setup_audio_buses() func _exit_tree() -> void: # remove_custom_type("RaytracedAudioListener") # remove_custom_type("RaytracedAudioPlayer3D") _clean_up_settings() _clean_up_audio_buses() func _setup_settings() -> void: ProjectSettings.set_setting("raytraced_audio/reverb_bus", &"RaytracedReverb") ProjectSettings.set_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient") if ProjectSettings.get_setting("audio/general/3d_panning_strength") < 1.0: print("[INFO] RaytracedAudio: I recommend setting Audio/General/3d_panning_strength in Project Settings to 1.0 or above") func _clean_up_settings() -> void: ProjectSettings.set_setting("raytraced_audio/reverb_bus", null) ProjectSettings.set_setting("raytraced_audio/ambient_bus", null) func _setup_audio_buses() -> void: print("[INFO] Raytraced Audio: setting up audio buses") _clean_up_audio_buses() # Reverb var i: int = AudioServer.bus_count AudioServer.add_bus() AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb")) AudioServer.set_bus_send(i, &"Master") var reverb: AudioEffectReverb = AudioEffectReverb.new() reverb.hipass = 1.0 reverb.resource_name = "reverb" AudioServer.add_bus_effect(i, reverb) # Ambient i = AudioServer.bus_count AudioServer.add_bus() AudioServer.set_bus_name(i, ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient")) AudioServer.set_bus_send(i, &"Master") var panner: AudioEffectPanner = AudioEffectPanner.new() panner.resource_name = "pan" AudioServer.add_bus_effect(i, panner) # There's a bug in Godot that makes it so the ambient bus doesnt show properly in the editor # This is "fixable" by adding a temporary bus afterwards and deleting it immediately because ofc. # fuck you (respectfully) i = AudioServer.bus_count AudioServer.add_bus() AudioServer.remove_bus(i) func _clean_up_audio_buses() -> void: var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus", &"RaytracedReverb")) if i != -1: AudioServer.remove_bus(i) i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus", &"RaytracedAmbient")) if i != -1: AudioServer.remove_bus(i) ================================================ FILE: addons/raytraced_audio/raytraced_audio_listener.gd ================================================ class_name RaytracedAudioListener extends AudioListener3D ## 3D audio listener for raytraced audio ## ## [b]Audio buses[/b] ## [br]This plugin creates 2 new audio buses for you to use across your project: ## a [i]"Reverb"[/i] bus, and an [i]"Ambient"[/i] bus. ## [br]Note: both buses' names can be changed under [code]Project Settings > Raytraced Audio[/code]. ## [br] ## [br]The reverb bus controls echo / reverb. ## [br]For example, there will be a much bigger reverb in large enclosed rooms compared to small ones, or outside in the open ## [br] ## [br]The ambient bus controls the strength and pan of sounds coming from outside. ## [br]For example, in a room with a single opening leading outisde, sounds in this bus will appear to come from that opening, and will fade based on the player's distance to it ## [br] ## [br][b]Performace[/b] ## [br]Raytraced Audio adds 2 performance monitors: ## [br] - [code]raytraced_audio/raycast_updates[/code]: How many raycast updates happened in one update tick ## [br] - [code]raytraced_audio/enabled_players_count[/code]: How many [RaytracedAudioPlayer3D]s are enabled in the scene ## [br] ## [br]Note: There should be only one [RaytracedAudioListener] in a given scene, just like how there should be only one AudioListener3D in a scene. ## [br]See also [RaytracedAudioPlayer3D] ## Speed of sound in m/s const SPEED_OF_SOUND: float = 343.0 ## All [RaytracedAudioListener]s will be in this group const GROUP_NAME: StringName = &"raytraced_audio_listener" const AudioRay: Script = preload("res://addons/raytraced_audio/audio_ray.gd") enum RayScatterModel { ## Rays will be shot out in a random 3d direction RANDOM, ## Rays will be shot out on the listener's XZ plane (i.e. [code]Vector3(random, 0, random)[/code]) XZ, } ## Emitted when this node is enabled signal enabled ## Emitted when this node is disabled signal disabled ## Emitted when any configuration for the rays are changed ## [br]This includes: [member rays_count], [member max_raycast_dist], [member max_bounces], and [member ray_scatter_model] signal ray_configuration_changed ## List of rays instanced by this node var rays: Array[AudioRay] = [] ## Enable or disable raycasting ## [br]Disabled nodes can't be updated (see [method update]) even if [member auto_update] is set to [code]true[/code] @export var is_enabled: bool = true: set(v): if is_enabled == v: return is_enabled = v if is_enabled: if is_node_ready(): setup() enabled.emit() else: disabled.emit() if is_node_ready(): clear() ## Whether to update automatically ## [br]If set to [code]true[/code], this [RaytracedAudioListener] will update every [i]process[/i] frame @export var auto_update: bool = true: set(v): auto_update = v set_process(auto_update) ## Number of rays to use ## [br]More rays and more bounces mean a more accurate model of the environment can be made ## [br] See also [member max_bounces] ## [br] ## [br][i]Technical note[/i]: ## [br] Because the rays need to gather different informations about the environment, ## the actual number of processed rays can go up to: ## [br] [code]rays_count * (2 + n)[/code] where [code]n[/code] is the number of enabled [RaytracedAudioPlayer3D]s in the scene ## [br] ([code]rays_count * (1 ray that bounces around + 1 echo ray + 1 muffle ray per enabled RaytracedAudioPlayer3D[/code]) @export var rays_count: int = 4: set(v): if v == rays_count: return rays_count = maxi(v, 1) clear() setup() ray_configuration_changed.emit() ## The maximum distance which any given ray instanced by this node will cast @export var max_raycast_dist: float = SPEED_OF_SOUND: set(v): max_raycast_dist = v _update_ray_configuration() ray_configuration_changed.emit() ## [br]More rays and more bounces mean a more accurate model of the environment can be made ## [br] See also [member rays_count] @export var max_bounces: int = 3: set(v): max_bounces = v _update_ray_configuration() ray_configuration_changed.emit() ## Controls how rays will be instanced @export var ray_scatter_model: RayScatterModel = RayScatterModel.RANDOM: set(v): ray_scatter_model = v _update_ray_configuration() ray_configuration_changed.emit() @export_category("Muffle") ## Enables [RaytracedAudioPlayer3D]s muffling behind walls @export var muffle_enabled: bool = true ## The interpolation strength of the muffle ## [br]See [member muffle_enabled] @export_range(1.0, 25.0, 0.1) var muffle_interpolation: float = 5.0 @export_category("Echo") ## Enables updates to the reverb audio bus ## [br]See [code]Project Settings > Raytraced Audio > Reverb Bus[/code] @export var echo_enabled: bool = true ## The "intensity" of the reverb ## [br]More specifically, multiplies the reverb's room size by this amount ## [br]The default is [code]2.0[/code] to account for sound waves coming back to the listener after hitting a wall @export var echo_room_size_multiplier: float = 2.0 ## The interpolation strength of the echo ## [br]See [member echo_enabled] @export_range(1.0, 25.0, 0.1) var echo_interpolation: float = 5.0 @export_category("Ambient") ## Enables updates to the ambient audio bus ## [br]See [code]Project Settings > Raytraced Audio > Ambient Bus[/code] @export var ambient_enabled: bool = true ## The interpolation strength of ambient sounds' direction (pan) ## [br]See [member ambient_enabled], [member ambient_pan_strength] @export_range(1.0, 25.0, 0.1) var ambient_pan_interpolation: float = 5.0 ## How strong the pan between right and left ear will be ## Setting this to 0 disables panning completely ## [br]See [member ambient_enabled], [member ambient_pan_interpolation] @export_range(0.0, 1.0, 0.01) var ambient_pan_strength: float = 1.0 ## The interpolation strength of ambient sounds' volume ## [br]See [member ambient_enabled] @export_range(1.0, 25.0, 0.1) var ambient_volume_interpolation: float = 5.0 ## How smoothly the ambient sounds will fade away when the [AudioListener3D] is no longer "outside" ## Values close to 1 will make sounds linger for longer, while values close to 0 will make them drop suddenly ## [br]See [member ambient_enabled] @export_range(0.0, 1.0, 0.001) var ambient_volume_attenuation: float = 0.998 ## The size of the room based on data gathered from rays, in world units ## [br]Used in reverb calculation var room_size: float = 0.0 ## How "outside" this [RaytracedAudioListener] is based on data gathered from rays, between 0 and 1 var ambience: float = 0.0 ## The direction to "outside" based on data gathered from rays ## [br]Should average out to 0 when this [RaytracedAudioListener] is completely outside var ambient_dir: Vector3 = Vector3.ZERO ## For debugging purposes ## [br]See the [code]raytraced_audio/raycast_updates[/code] performance monitor var ray_casts_this_tick: int = 0 var _reverb_effect: AudioEffectReverb var _pan_effect: AudioEffectPanner # Keep track of whether we've set up debug monitors var _debug_monitors_setup: bool = false func _enter_tree() -> void: add_to_group(GROUP_NAME) func _ready() -> void: # Reverb effect var i: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/reverb_bus")) if i == -1: push_error("Failed to get reverb bus for raytraced audio. Disabling echo...") echo_enabled = false else: _reverb_effect = AudioServer.get_bus_effect(i, 0) # Pan effect i = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus")) if i == -1: push_error("Failed to get ambient bus for raytraced audio. Disabling ambience...") ambient_enabled = false else: _pan_effect = AudioServer.get_bus_effect(i, 0) if is_enabled: setup() set_process(auto_update) if is_enabled: make_current() _setup_debug() func _exit_tree() -> void: _cleanup_debug() func _setup_debug() -> void: if _debug_monitors_setup: return _debug_monitors_setup = true Performance.add_custom_monitor(&"raytraced_audio/raycast_updates", func(): # Check if this instance is still valid if not is_instance_valid(self): return 0 return ray_casts_this_tick ) Performance.add_custom_monitor(&"raytraced_audio/enabled_players_count", func(): # Check if this instance is still valid and if we're in a scene tree if not is_instance_valid(self) or not is_inside_tree(): return 0 return get_tree().get_node_count_in_group(RaytracedAudioPlayer3D.ENABLED_GROUP_NAME) ) func _cleanup_debug() -> void: if not _debug_monitors_setup: return _debug_monitors_setup = false # Remove the custom monitors Performance.remove_custom_monitor(&"raytraced_audio/raycast_updates") Performance.remove_custom_monitor(&"raytraced_audio/enabled_players_count") ## Initiates this [RaytracedAudioListener]'s rays func setup() -> void: for __ in rays_count: var rc: AudioRay = AudioRay.new(max_raycast_dist, max_bounces) rc.set_scatter_model(ray_scatter_model) add_child(rc, INTERNAL_MODE_BACK) rays.push_back(rc) ## Clears all created rays func clear(): for ray: AudioRay in rays: remove_child(ray) ray.queue_free() rays.clear() func _process(delta: float) -> void: if !auto_update: set_process(false) return update(delta) ## Updates this [RaytracedAudioListener] ## [br]If you are updating this node manually (i.e. [member auto_update] is [code]false[/code]), this is the method to call func update(delta: float): ray_casts_this_tick = 0 if !is_enabled: return var echo: float = 0.0 # Avg echo from all rays var echo_count: int = 0 # Number of echo rays that came back var bounces_this_tick: int = 0 var escaped_count: int = 0 var escaped_dir: Vector3 = Vector3.ZERO # Avg escape direction var escaped_strength: float = 0.0 # Gather data for ray: AudioRay in rays: ray.update() ray_casts_this_tick += ray.ray_casts_this_tick echo += ray.echo_dist echo_count += ray.echo_count bounces_this_tick += int(ray.has_bounced_this_tick) if ray.escaped: escaped_count += 1 escaped_strength += 1.0 / ray.bounces escaped_dir += ray.escape_dir ray.reset_tick_stats() echo = 0.0 if echo_count == 0 else (echo / float(echo_count)) escaped_dir = Vector3.ZERO if escaped_count == 0 else (escaped_dir / float(escaped_count)) if muffle_enabled: _update_muffle(delta) if echo_enabled: _update_echo(echo, echo_count, bounces_this_tick, delta) if ambient_enabled: _update_ambient(escaped_strength, escaped_dir, delta) func _update_muffle(delta: float) -> void: for player: RaytracedAudioPlayer3D in get_tree().get_nodes_in_group(RaytracedAudioPlayer3D.GROUP_NAME): player.update(self, delta) func _update_echo(echo: float, echo_count: int, bounces: int, delta: float) -> void: # Length -> echo delay room_size = lerpf(room_size, echo, 1.0 - exp(-delta * echo_interpolation)) var e: float = (room_size * echo_room_size_multiplier) / SPEED_OF_SOUND # print("e = ", e) _reverb_effect.room_size = lerpf(_reverb_effect.room_size, clampf(e, 0.0, 1.0), 1.0 - exp(-delta * echo_interpolation)) _reverb_effect.predelay_msec = lerpf(_reverb_effect.predelay_msec, e * 1000, 1.0 - exp(-delta * echo_interpolation)) _reverb_effect.predelay_feedback = lerpf(_reverb_effect.predelay_feedback, clampf(e, 0.0, 0.98), 1.0 - exp(-delta * echo_interpolation)) # More rays % -> echo strength var return_ratio: float = 0.0 if bounces == 0 else float(echo_count) / float(bounces) _reverb_effect.hipass = lerpf(_reverb_effect.hipass, 1.0 - return_ratio, 1.0 - exp(-delta * echo_interpolation)) func _update_ambient(escaped_strength: float, escaped_dir: Vector3, delta: float) -> void: var ambience_ratio: float = float(escaped_strength) / float(rays_count) # More rays % -> louder if escaped_strength > 0: #should it use delta? #ambience = lerpf(ambience, 1.0, 1.0 - exp(-delta * ambience_ratio)) ambience = lerpf(ambience, 1.0, ambience_ratio) else: ambience *= ambient_volume_attenuation var ambient_bus_idx: int = AudioServer.get_bus_index(ProjectSettings.get_setting("raytraced_audio/ambient_bus")) var volume: float = AudioServer.get_bus_volume_linear(ambient_bus_idx) AudioServer.set_bus_volume_linear(ambient_bus_idx, lerpf(volume, ambience, 1.0 - exp(-delta * ambient_volume_interpolation))) # Avg escape direction -> pan ambient_dir = ambient_dir.slerp(escaped_dir, 1.0 - exp(-delta * ambient_pan_interpolation)) var target_pan: float = 0.0 if ambient_dir.is_zero_approx() else owner.transform.basis.x.dot(ambient_dir.normalized()) _pan_effect.pan = target_pan * ambient_pan_strength # ✨ the name says it all ✨ func _update_ray_configuration() -> void: for ray: AudioRay in rays: ray.cast_dist = max_raycast_dist ray.max_bounces = max_bounces ray.set_scatter_model(ray_scatter_model) ================================================ FILE: addons/raytraced_audio/raytraced_audio_player_3d.gd ================================================ class_name RaytracedAudioPlayer3D extends AudioStreamPlayer3D ## Audio stream player that allows for audio muffling ## ## You can already use the reverb and ambient audio buses (see the documentation for [RaytracedAudioListener] for more details) ## to make use of raytraced audio effects, but using [RaytracedAudioPlayer3D]s allows you to use more capabilities. ## Namely, muffling sounds behind walls. ## [br] ## [br][b][color=yellow]Warning[/color][/b]: [RaytracedAudioPlayer3D]s will default to using the reverb audio bus ## [br]If you wish to use another bus instead, please set it after this node has been added to the scene tree (i.e. after [code]_enter_tree()[/code]) ## [br] ## [br][i]Technical note[/i]: ## [br]Currently, there is no way to muffle only one audio player (or apply any effect for that matter). ## [br]So we create a new bus for every audio player that is audible from the [RaytracedAudioListener] ## [br]Godot doesn't provide methods to calculate the volume of an audio at certain distances, so we calculate ## that ourselves (see [method calculate_audible_distance_threshold()]) ## All [RaytracedAudioPlayer3D]s (regardless of state) will be in this group const GROUP_NAME: StringName = &"raytraced_audio_player_3d" ## All [b]enabled[/b] [RaytracedAudioPlayer3D]s will be in this group const ENABLED_GROUP_NAME: StringName = &"enabled_raytraced_audio_player_3d" ## The lowpass frequency when completely muffled const LOWPASS_MIN_HZ: float = 250.0 ## The lowpass frequency when completely not-muffled (?) const LOWPASS_MAX_HZ: float = 20000.0 ## The factor the nyquist frequency is multiplied with to get a safe max_cutoff const NYQUIST_MULTIPLIER: float = 0.95 ## Used in internal calculations const LOG2: float = log(2.0) ## Used in internal calculations const LOG_MIN_HZ: float = log(LOWPASS_MIN_HZ) / LOG2 ## Used in internal calculations const LOG_MAX_HZ: float = log(LOWPASS_MAX_HZ) / LOG2 ## Emitted when this node is enabled ## [br]See also [constant ENABLED_GROUP_NAME] signal enabled ## Emitted when this node is disabled signal disabled ## Emitted when the maximum audible distance for this node is changed ## [br]See also [member AudioStreamPlayer3D.max_distance] and [member audibility_threshold_db] signal audible_distance_updated(distance: float) ## The threshold (in decibels) at which sounds will be considered inaudible ## [br]This is used to enable / disable this node when it's not audible to save resources ## [br]The distance at which this node is no longer considered audible is automatically stored inside [member AudioStreamPlayer3D.max_distance] ## (if not already set), also to save resources ## [br]Though, setting [member AudioStreamPlayer3D.max_distance] doesn't update this field automatically ## [br]See also [method get_volume_db_from_pos] and [method calculate_audible_distance_threshold] @export var audibility_threshold_db: float = -30.0: set(v): audibility_threshold_db = v # Max distance not configured if is_node_ready() and max_distance == 0.0: max_distance = calculate_audible_distance_threshold() audible_distance_updated.emit(max_distance) var _lowpass_rays_count: int = 0 var _lowpass_max_safe_cutoff: float = LOWPASS_MAX_HZ var _is_enabled: bool = false func _enter_tree() -> void: add_to_group(GROUP_NAME) bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback func _ready() -> void: # Max distance not configured if max_distance == 0.0: max_distance = calculate_audible_distance_threshold() audible_distance_updated.emit(max_distance) # Update the lowpass limits on start _lowpass_max_safe_cutoff = calculate_lowpass_limit() ## Enables this node ## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually ## [br]See [method update] func enable(): if _is_enabled: return _is_enabled = true var i: int = _create_bus() bus = AudioServer.get_bus_name(i) add_to_group(ENABLED_GROUP_NAME) enabled.emit() ## Enables this node ## [br]Note: you should almost never have to worry about enabling / disabling [RaytracedAudioPlayer3D]s manually ## [br]See [method update] func disable(): if !_is_enabled: return # Don't remove the fallback bus lol if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"): _disable() return # Remove this node's specific bus var idx: int = AudioServer.get_bus_index(bus) if idx == -1: push_warning("audio bus ", bus, " not found") _disable() return _disable() AudioServer.remove_bus(idx) func _disable(): _is_enabled = false bus = ProjectSettings.get_setting("raytraced_audio/reverb_bus") # Fallback remove_from_group(ENABLED_GROUP_NAME) _lowpass_rays_count = 0 disabled.emit() # Returns the index of the created bus func _create_bus() -> int: var i: int = AudioServer.bus_count AudioServer.add_bus() AudioServer.set_bus_name(i, generate_bus_name()) AudioServer.set_bus_send(i, ProjectSettings.get_setting("raytraced_audio/reverb_bus")) AudioServer.add_bus_effect(i, AudioEffectLowPassFilter.new()) return i ## Returns a name for the audio bus created for this node func generate_bus_name() -> String: return str("RTAudioPlayer3D_", name, randi()) ## ✨ the name says it all ✨ func is_enabled() -> bool: return _is_enabled ## Updates this [RaytracedAudioPlayer3D] ## [br]This method will adjust the muffle of the played stream, as well as enable or disable it ## based on whether it's audible from the given [RaytracedAudioListener] ## [br]If you are updating this node manually, this is the method to call func update(listener: RaytracedAudioListener, delta: float) -> void: if _is_enabled: _update(listener.rays_count, listener.muffle_interpolation, delta) _lowpass_rays_count = 0 # Enable based on position var dist_sq: float = global_position.distance_squared_to(listener.global_position) if dist_sq > max_distance*max_distance or !playing: disable() else: enable() func _update(rays_count: int, interpolation: float, delta: float): if bus == ProjectSettings.get_setting("raytraced_audio/reverb_bus"): _disable() return var idx: int = AudioServer.get_bus_index(bus) if idx == -1: push_error("audio bus ", bus, " not found") _disable() else: var ratio: float = float(_lowpass_rays_count) / float(rays_count) ratio = clamp(ratio, 0, 1) var lowpass: AudioEffectLowPassFilter = AudioServer.get_bus_effect(idx, 0) # Frequencies aren't linear, they scale logarithmically (log2 space) +1 octave = 2x the frequency # So we scale frequencies down before lerping, then scale them back up var log_t: float = lerpf(LOG_MIN_HZ, LOG_MAX_HZ, ratio) var log_hz: float = log(lowpass.cutoff_hz) / LOG2 # Scale current frequency down: log2(x) = ln(x) / ln(2) log_hz = lerpf(log_hz, log_t, 1.0 - exp(-delta * interpolation)) # Lerp in scaled down space lowpass.cutoff_hz = min(pow(2, log_hz), _lowpass_max_safe_cutoff) # Limit at max cutoff # Translated from the godot repo ## Calculates the volume (in decibels) of the audio stream from the given position ## [br]Please note that the final volume will vary depending on the stream that is being played func get_volume_db_from_pos(from_pos: Vector3) -> float: const CMP_EPSILON: float = 0.0001 var dist: float = from_pos.distance_to(global_position) var vol: float = 0.0 match attenuation_model: ATTENUATION_INVERSE_DISTANCE: vol = linear_to_db(1.0 / ((dist / unit_size) + CMP_EPSILON)) ATTENUATION_INVERSE_SQUARE_DISTANCE: var d: float = (dist / unit_size) vol = linear_to_db(1.0 / (d*d + CMP_EPSILON)) ATTENUATION_LOGARITHMIC: vol = -20.0 * log(dist / unit_size + CMP_EPSILON) ATTENUATION_DISABLED: pass _: push_error("Unknown attenuation type: '", attenuation_model, "'") vol = minf(vol + volume_db, max_db) return vol ## Calculates the distance at which this [RaytracedAudioPlayer3D] is no longer considered audible ## [br]See also [member audibility_threshold_db] func calculate_audible_distance_threshold() -> float: if max_distance > 0.0: return max_distance match attenuation_model: ATTENUATION_INVERSE_DISTANCE: var t_lin: float = db_to_linear(audibility_threshold_db - volume_db) # Unit_size / dist < T_lin return unit_size / t_lin ATTENUATION_INVERSE_SQUARE_DISTANCE: var t_lin: float = db_to_linear(audibility_threshold_db - volume_db) # (Unit_size / dist)^2 < T_lin return sqrt(unit_size*unit_size / t_lin) ATTENUATION_LOGARITHMIC: var t_db: float = audibility_threshold_db - volume_db # -20 * log(dist / Unit_size) > T_db return exp(t_db / -20.0) * unit_size ATTENUATION_DISABLED: return 0.0 _: push_error("Unknown attenuation model: '", attenuation_model, "'") return 0.0 ## Gets the current sampling rate of the AudioServer and calculates ## a safe cutoff_hz value for the lowpass filter. func calculate_lowpass_limit() -> float: var nyquist_frequency = AudioServer.get_mix_rate() * 0.5 return nyquist_frequency * NYQUIST_MULTIPLIER ## Checks wheter this [RaytracedAudioPlayer3D] is considered audible from the given position ## [br]See also [member audibility_threshold_db] func is_audible(from_pos: Vector3) -> bool: return get_volume_db_from_pos(from_pos) >= audibility_threshold_db ================================================ FILE: default_bus_layout.tres ================================================ [gd_resource type="AudioBusLayout" format=3 uid="uid://dfuwe48pov8j3"] [sub_resource type="AudioEffectReverb" id="AudioEffectReverb_j3pel"] resource_name = "reverb" hipass = 1.0 [sub_resource type="AudioEffectPanner" id="AudioEffectPanner_g28q7"] resource_name = "pan" [resource] bus/1/name = &"RaytracedReverb" bus/1/solo = false bus/1/mute = false bus/1/bypass_fx = false bus/1/volume_db = 0.0 bus/1/send = &"Master" bus/1/effect/0/effect = SubResource("AudioEffectReverb_j3pel") bus/1/effect/0/enabled = true bus/2/name = &"RaytracedAmbient" bus/2/solo = false bus/2/mute = false bus/2/bypass_fx = false bus/2/volume_db = 0.0 bus/2/send = &"Master" bus/2/effect/0/effect = SubResource("AudioEffectPanner_g28q7") bus/2/effect/0/enabled = true ================================================ FILE: icon.png.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://cswkq85176f4o" path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false } [deps] source_file="res://icon.png" dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] [params] compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 detect_3d/compress_to=1 ================================================ FILE: icon.svg.import ================================================ [remap] importer="texture" type="CompressedTexture2D" uid="uid://dxvcb2moy11s4" path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" metadata={ "vram_texture": false } [deps] source_file="res://icon.svg" dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] [params] compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 detect_3d/compress_to=1 svg/scale=1.0 editor/scale_with_editor_scale=false editor/convert_colors_with_editor_theme=false ================================================ FILE: project.godot ================================================ ; Engine configuration file. ; It's best edited using the editor UI and not directly, ; since the parameters that go here are not all obvious. ; ; Format: ; [section] ; section goes between [] ; param=value ; assign values to parameters config_version=5 [animation] compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Raytraced Audio" run/main_scene="uid://dgx3qrieojg3x" config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="uid://cswkq85176f4o" [audio] general/3d_panning_strength=1.0 [editor_plugins] enabled=PackedStringArray("res://addons/raytraced_audio/plugin.cfg") [raytraced_audio] reverb_bus=&"RaytracedReverb" ambient_bus=&"RaytracedAmbient" [rendering] renderer/rendering_method="gl_compatibility" renderer/rendering_method.mobile="gl_compatibility"