Files
agame/addons/raytraced_audio/raytraced_audio_player_3d.gd
T
2026-09-13 00:17:24 +08:00

238 lines
9.0 KiB
GDScript

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