extends Node ## Wind beds plus sparse, localised foliage and wildlife field recordings. const ROOT := "res://assets/sounds/surface/" const VOICE_LIMIT := 3 const WIND_DB := [-18.0,-17.0,-16.0,-13.0,-12.0,-14.0] var source: Control var _streams: Dictionary = {} var _wind: AudioStreamPlayer var _foliage: AudioStreamPlayer var _world_audio: Node2D var _listener: AudioListener2D var _voices: Array[AudioStreamPlayer2D] = [] var _rng := RandomNumberGenerator.new() var _elapsed := 0.0 var _local_refresh := 0.0 var _near_trees: Array[Vector2] = [] var _density := 0.0 var _biome := 0 var _oxygen := 100.0 var _next_gust := 10.0 var _next_leaves := 6.0 var _next_creak := 20.0 var _next_fauna := 12.0 var _last_leaf := "" var _last_bird := "" var played_count := 0 func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS _biome = int(source.world.data.landscape.index) _oxygen = float(source.progress.active_profile.get("oxygen",100)) _rng.seed = int(source.progress.visit_seed)^0x4e415455 for id in ["wind_loop","wind_gust","foliage_loop","leaves_1","leaves_2","leaves_3","branch_creak_1","branch_creak_2","bird_1","bird_2","bird_3","insects","marsh_call"]: _streams[id] = load(ROOT+id+".wav") _wind = _bed("wind_loop") _foliage = _bed("foliage_loop") _world_audio = Node2D.new() _world_audio.name = "NatureAudioWorld" _world_audio.process_mode = Node.PROCESS_MODE_ALWAYS source._surface_viewport.audio_listener_enable_2d = true source._surface_viewport.add_child(_world_audio) _listener = AudioListener2D.new() _world_audio.add_child(_listener) _listener.make_current() for index in VOICE_LIMIT: var voice := AudioStreamPlayer2D.new() voice.name = "NatureVoice%d" % index voice.max_distance = 1050 voice.attenuation = 1.1 voice.panning_strength = 0.65 _world_audio.add_child(voice) _voices.append(voice) _refresh_local() _next_gust = _rng.randf_range(8,16) _next_fauna = _rng.randf_range(7,15) func _bed(id: String) -> AudioStreamPlayer: var bed := AudioStreamPlayer.new() var stream := (_streams[id] as AudioStreamWAV).duplicate() as AudioStreamWAV stream.loop_mode = AudioStreamWAV.LOOP_FORWARD stream.loop_end = roundi(stream.get_length()*stream.mix_rate) bed.stream = stream bed.volume_db = -48 add_child(bed) bed.play() return bed func _refresh_local() -> void: _near_trees.clear() for tree in source.world.data.trees: if source.lander.position.distance_squared_to(tree.position) < 560.0*560.0: _near_trees.append(tree.position) _density = clampf(float(_near_trees.size())/22.0,0,1) func fauna_choices() -> Array[String]: if _oxygen < 12: return [] match _biome: 2: return ["marsh_call","insects","bird_2"] 4: return ["insects"] 5: return ["bird_3"] _: return ["bird_1","bird_2","bird_3","insects"] func _play(id: String, position: Vector2, level: float) -> bool: for voice in _voices: if voice.playing: continue voice.stream = _streams[id] voice.position = position voice.volume_db = level voice.pitch_scale = _rng.randf_range(0.95,1.03) voice.play() played_count += 1 return true return false func _tree_position() -> Vector2: return _near_trees[_rng.randi_range(0,_near_trees.size()-1)] func _process(delta: float) -> void: if not is_instance_valid(source) or not is_instance_valid(_listener): return _elapsed += delta _local_refresh += delta _listener.position = source.lander.position if _local_refresh > 0.65: _local_refresh = 0 _refresh_local() var gust := sin(_elapsed*0.19)*1.4+sin(_elapsed*0.073)*0.8 var engine_mask := -3.0 if source.engine_thrust_active() else 0.0 if is_instance_valid(source.weather): gust += source.weather.intensity*2.5 _wind.volume_db = move_toward(_wind.volume_db,WIND_DB[_biome]+gust+engine_mask,delta*26) var leaf_target := -60.0 if _near_trees.is_empty() else lerpf(-34,-23,_density)+gust+engine_mask _foliage.volume_db = move_toward(_foliage.volume_db,leaf_target,delta*8) if _elapsed >= _next_gust: _play("wind_gust",_listener.position+Vector2(_rng.randf_range(-250,250),-120),-23.0 if _biome < 3 else -20.0) _next_gust = _elapsed+_rng.randf_range(18,38) if _elapsed >= _next_leaves: if not _near_trees.is_empty(): var choices := ["leaves_1","leaves_2","leaves_3"] choices.erase(_last_leaf) _last_leaf = choices[_rng.randi_range(0,choices.size()-1)] _play(_last_leaf,_tree_position(),-14.0) _next_leaves = _elapsed+_rng.randf_range(9,20) if _elapsed >= _next_creak: if not _near_trees.is_empty(): _play("branch_creak_%d" % _rng.randi_range(1,2),_tree_position(),-17.0) _next_creak = _elapsed+_rng.randf_range(26,52) if _elapsed >= _next_fauna: var choices := fauna_choices() if choices.size() > 1: choices.erase(_last_bird) if not choices.is_empty(): _last_bird = choices[_rng.randi_range(0,choices.size()-1)] var at := _listener.position+Vector2.from_angle(_rng.randf_range(-PI,PI))*_rng.randf_range(200,520) _play(_last_bird,at,-15.0 if _biome < 3 else -19.0) _next_fauna = _elapsed+_rng.randf_range(15,33)*(2.0 if _biome in [3,4,5] else 1.0) func _exit_tree() -> void: if is_instance_valid(_wind): _wind.stop() if is_instance_valid(_foliage): _foliage.stop() for voice in _voices: if is_instance_valid(voice): voice.stop() if is_instance_valid(_world_audio): _world_audio.queue_free()