diff --git a/assets/audio/CREDITS.md b/assets/audio/CREDITS.md index b12b9a0..fdbb0e8 100644 --- a/assets/audio/CREDITS.md +++ b/assets/audio/CREDITS.md @@ -39,6 +39,21 @@ Breviceps (https://freesound.org/people/Breviceps/sounds/465697/, License: Creative Commons 0; the preview is in `tools/audio/sources/`), cut at 0.12 s and 2.65 s (leaving out the short sound between them), then levelled. Thank you. +## Thunder (CC0) +`sfx/thunder/` is cut from six Freesound recordings, all License: Creative +Commons 0 (the previews are in `tools/audio/sources/thunder_*`), each round +its loudest strike, faded out and levelled by +`tools/audio/make_audio.py thunder`. Close cracks: "Thunder 08" by Emulius +(https://freesound.org/people/Emulius/sounds/744723/), "thunder11" by +saha213131 (https://freesound.org/people/saha213131/sounds/696550/) and +"Thunder strike" by AyaDrevis +(https://freesound.org/people/AyaDrevis/sounds/652690/). Distant rolls: +"Short_Thunder_Mid" by SholeColtis +(https://freesound.org/people/SholeColtis/sounds/683421/), "thunder7" by +nednednerb (https://freesound.org/people/nednednerb/sounds/38250/) and +"Thunder Clap OWB KY 441x16.wav" by Dave Welsh +(https://freesound.org/people/Dave%20Welsh/sounds/194364/). Thank you. + ## Old Tam's voice `voice/` is spoken by **Kokoro** (Kokoro-82M by hexgrad, Apache-2.0, https://huggingface.co/hexgrad/Kokoro-82M) with its British "George" voice, diff --git a/assets/audio/sfx/thunder/thunder_far_0.ogg b/assets/audio/sfx/thunder/thunder_far_0.ogg new file mode 100644 index 0000000..acbc3ad Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_far_0.ogg differ diff --git a/assets/audio/sfx/thunder/thunder_far_1.ogg b/assets/audio/sfx/thunder/thunder_far_1.ogg new file mode 100644 index 0000000..77465f2 Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_far_1.ogg differ diff --git a/assets/audio/sfx/thunder/thunder_far_2.ogg b/assets/audio/sfx/thunder/thunder_far_2.ogg new file mode 100644 index 0000000..de08a75 Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_far_2.ogg differ diff --git a/assets/audio/sfx/thunder/thunder_near_0.ogg b/assets/audio/sfx/thunder/thunder_near_0.ogg new file mode 100644 index 0000000..0793923 Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_near_0.ogg differ diff --git a/assets/audio/sfx/thunder/thunder_near_1.ogg b/assets/audio/sfx/thunder/thunder_near_1.ogg new file mode 100644 index 0000000..dc36e0f Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_near_1.ogg differ diff --git a/assets/audio/sfx/thunder/thunder_near_2.ogg b/assets/audio/sfx/thunder/thunder_near_2.ogg new file mode 100644 index 0000000..f9fabb5 Binary files /dev/null and b/assets/audio/sfx/thunder/thunder_near_2.ogg differ diff --git a/scripts/core/sfx.gd b/scripts/core/sfx.gd index f34901d..22e5f1e 100644 --- a/scripts/core/sfx.gd +++ b/scripts/core/sfx.gd @@ -19,6 +19,7 @@ extends Node const K := "res://assets/audio/sfx/kenney/" const S := "res://assets/audio/sfx/synth/" const C := "res://assets/audio/sfx/creatures/" +const T := "res://assets/audio/sfx/thunder/" const EARSHOT := 45.0 ## a server sends a sound to players this close ## id -> files (or a [prefix, count] pair), volume (dB), pitch range, reach (m), @@ -28,6 +29,10 @@ const SOUNDS := { "tree_fall": {"set": [K + "wood_heavy_%d.ogg", 5], "db": 0.0, "pitch": [0.6, 0.7], "reach": 60.0, "with": "creak"}, "creak": {"set": [K + "creak_%d.ogg", 3], "db": -2.0, "pitch": [0.8, 0.95], "reach": 50.0}, "mine": {"set": [K + "mine_%d.ogg", 5], "db": -1.0, "pitch": [0.9, 1.1]}, + # Thunder (0.6.2, CC0 recordings: assets/audio/CREDITS.md). The weather + # plays the storm's own (weather_fx.gd); a strike on a tree cracks from it. + "thunder_near": {"set": [T + "thunder_near_%d.ogg", 3], "db": 0.0, "pitch": [0.92, 1.05], "reach": 400.0}, + "thunder_far": {"set": [T + "thunder_far_%d.ogg", 3], "db": -5.0, "pitch": [0.9, 1.05], "reach": 400.0}, "rock_break": {"set": [K + "mine_%d.ogg", 5], "db": 1.0, "pitch": [0.6, 0.7], "reach": 50.0}, "rustle": {"set": [K + "cloth_%d.ogg", 4], "db": -4.0, "pitch": [0.7, 0.9]}, "pickup": {"set": [K + "leather_%d.ogg", 3], "db": -6.0, "pitch": [0.95, 1.15], "reach": 15.0}, @@ -196,6 +201,17 @@ func me(id: String) -> void: _log(id, null) _play2(id) +## A random one of sound `id`'s files, and its volume: for a player of your +## own (the weather's thunder). Not logged: call log_heard() when it plays. +func stream_for(id: String) -> AudioStream: + return _pick(id) if SOUNDS.has(id) else null + +func db_for(id: String) -> float: + return float(SOUNDS.get(id, {}).get("db", 0.0)) + +func log_heard(id: String) -> void: + _log(id, null) + ## Menus and the HUD. func ui(id: String) -> void: _log(id, null) diff --git a/scripts/world/hazards.gd b/scripts/world/hazards.gd index e0cbd6e..ffd90e4 100644 --- a/scripts/world/hazards.gd +++ b/scripts/world/hazards.gd @@ -92,6 +92,7 @@ func pick_tree() -> Node3D: func strike(n: Node3D) -> void: strikes += 1 show_strike(n) + Sfx.at("thunder_near", n.global_position + Vector3(0, 6, 0)) # (a crack from the tree itself) if world.server_mode: WorldNet.struck(n) for p in world.players(): diff --git a/scripts/world/weather_fx.gd b/scripts/world/weather_fx.gd index f0ce735..0b24aab 100644 --- a/scripts/world/weather_fx.gd +++ b/scripts/world/weather_fx.gd @@ -7,8 +7,11 @@ extends Node3D ## - Ground wetness, settled snow and wind strength are global shader uniforms ## (weather_wet, weather_snow, weather_wind) read by the terrain, grass, ## foliage and rock shaders. -## - Sound is synthesised: filtered noise for rain and wind, a decaying -## low rumble for thunder. No audio assets, nothing to license. +## - Rain and wind are synthesised (filtered noise). Thunder is recorded +## (CC0, 0.6.2: assets/audio/sfx/thunder, Sfx "thunder_far"/"thunder_near"): +## after each flash in a storm, usually a distant roll a few seconds later, +## sometimes a nearer crack sooner. Quieter under a roof. (A strike on a +## tree near you cracks from the tree itself: hazards.gd.) ## ## Client-side only: in M2 the server runs weather.gd and never this. @@ -31,10 +34,11 @@ var _rng := RandomNumberGenerator.new() var _rain_lp := 0.0 var _rain_hp := 0.0 var _brown := 0.0 -var _brown2 := 0.0 var _gust_t := 0.0 -var _thunder_env := 0.0 var _thunder_delay := -1.0 +var _thunder_id := "" +var _thunder: AudioStreamPlayer +const NEAR_CHANCE := 0.3 ## of a storm's flashes, the share that are close var _seen_strikes := 0 var _rain_amp := 0.0 var _wind_amp := 0.0 @@ -83,6 +87,10 @@ func setup(w: Node, p: Node3D) -> void: add_child(_audio) _audio.play() _playback = _audio.get_stream_playback() + _thunder = AudioStreamPlayer.new() + _thunder.name = "Thunder" + _thunder.bus = "SFX" if AudioServer.get_bus_index("SFX") >= 0 else "Master" + add_child(_thunder) func _make_particles(amount: int, life: float, pm: ParticleProcessMaterial, mesh: Mesh) -> GPUParticles3D: var p := GPUParticles3D.new() @@ -173,13 +181,32 @@ func _process(delta: float) -> void: _wind_amp = weather.wind * weather.wind * 0.22 * (0.35 if sheltered else 1.0) if weather.lightning_count != _seen_strikes: _seen_strikes = weather.lightning_count - _thunder_delay = _rng.randf_range(0.5, 2.5) + lightning() if _thunder_delay >= 0.0: _thunder_delay -= delta if _thunder_delay < 0.0: - _thunder_env = 1.0 + _thunder_now() _fill_audio() +## A flash in the sky: its thunder follows, after the time the sound takes +## to come (a near one sooner, a distant one later). +func lightning() -> void: + var near := _rng.randf() < NEAR_CHANCE + _thunder_id = "thunder_near" if near else "thunder_far" + _thunder_delay = _rng.randf_range(0.3, 0.9) if near else _rng.randf_range(1.5, 4.0) + +func _thunder_now() -> void: + Sfx.log_heard(_thunder_id) + if _thunder == null: + return + var st: AudioStream = Sfx.stream_for(_thunder_id) + if st == null: + return + _thunder.stream = st + _thunder.volume_db = Sfx.db_for(_thunder_id) - 8.0 * _muffle + _thunder.pitch_scale = _rng.randf_range(0.9, 1.05) + _thunder.play() + func _set_amount(p: GPUParticles3D, a: float) -> void: p.amount_ratio = a p.emitting = a > 0.0 @@ -194,7 +221,6 @@ func _fill_audio() -> void: var rain_cut := lerpf(0.55, 0.12, _muffle) var buf := PackedVector2Array() buf.resize(n) - var thunder_decay := exp(-1.0 / (MIX_RATE * 0.9)) for i in range(n): var white := _rng.randf() * 2.0 - 1.0 # Rain: band-limited hiss (low-pass, minus a slower low-pass). @@ -206,12 +232,6 @@ func _fill_audio() -> void: _gust_t += 1.0 / MIX_RATE var gust := 0.6 + 0.4 * sin(_gust_t * 0.7) * sin(_gust_t * 0.23 + 1.0) var wind_s := _brown * _wind_amp * gust * 2.0 - # Thunder: a heavier brown rumble with a long decay. - var thunder := 0.0 - if _thunder_env > 0.001: - _brown2 = clampf(_brown2 * 0.9995 + white * 0.02, -1.0, 1.0) - thunder = _brown2 * _thunder_env * 2.2 - _thunder_env *= thunder_decay - var s := clampf(rain + wind_s + thunder, -1.0, 1.0) + var s := clampf(rain + wind_s, -1.0, 1.0) buf[i] = Vector2(s, s) _playback.push_buffer(buf) diff --git a/tests/test_hazards.gd b/tests/test_hazards.gd index dd30366..1500fa9 100644 --- a/tests/test_hazards.gd +++ b/tests/test_hazards.gd @@ -269,6 +269,31 @@ func _lightning() -> void: var tree: Node3D = hz._burning.keys()[0] check(tree.global_position.distance_to(at) < hz.STRIKE_RANGE, "near you (%.0f m away)" % tree.global_position.distance_to(at)) check(_toast_has("Lightning struck"), "you see it (%s)" % [_toasts]) + # Gitea #4: thunder is recorded now; the strike cracks from the tree. + var SX: Node = root.get_node("Sfx") + var cracks: Array = SX.heard.filter(func(h): return h[0] == "thunder_near" and h[1] is Vector3 and (h[1] as Vector3).distance_to(tree.global_position) < 8.0) + check(cracks.size() == 1, "and hear it: a thunder crack from the tree") + check(SX.files("thunder_near").size() == 3 and SX.files("thunder_far").size() == 3 and + SX.files("thunder_near").all(func(f): return ResourceLoader.exists(f)) and SX.files("thunder_far").all(func(f): return ResourceLoader.exists(f)), + "(three recorded cracks and three distant rolls)") + var fx: Node3D = load("res://scripts/world/weather_fx.gd").new() + world.add_child(fx) + fx.setup(wx, player) + var kinds := {} + var delays := {"thunder_near": [], "thunder_far": []} + for _i in range(60): + fx.lightning() + kinds[fx._thunder_id] = int(kinds.get(fx._thunder_id, 0)) + 1 + delays[fx._thunder_id].append(fx._thunder_delay) + check(kinds.has("thunder_near") and kinds.has("thunder_far") and int(kinds.thunder_far) > int(kinds.thunder_near), + "a storm's flashes: mostly distant rolls, sometimes a crack close by (%s)" % [kinds]) + check(delays.thunder_near.all(func(d): return d < 1.0) and delays.thunder_far.all(func(d): return d >= 1.5), + "a close one follows its flash quickly, a distant one seconds later") + var h0: int = SX.heard.size() + fx._thunder_delay = 0.01 + fx._process(0.05) + check(SX.heard.size() > h0 and SX.heard.slice(h0).any(func(h): return String(h[0]).begins_with("thunder_")), "and then you hear it") + fx.queue_free() check(world.heat_at(tree.global_position + Vector3(2.5, 0, 0)) > 0.3, "it burns: you can warm yourself by it") check(not tree.depleted, "still standing while it burns") for _i in range(41): diff --git a/tools/audio/make_audio.py b/tools/audio/make_audio.py index de1a599..5ac6d96 100644 --- a/tools/audio/make_audio.py +++ b/tools/audio/make_audio.py @@ -699,6 +699,44 @@ def _decode(src, sr, af=None): return [x * 0.06 / rms for x in left], [x * 0.06 / rms for x in right] +def make_thunder(): + """Thunder (0.6.2, Gitea #4): CC0 recordings (tools/audio/sources/ + thunder_near_* and thunder_far_*, credited in assets/audio/CREDITS.md), + each cut round its loudest strike, faded out and evened out in level, into + assets/audio/sfx/thunder/thunder_near_N.ogg (a crack close by) and + thunder_far_N.ogg (a roll in the distance). Replaces the synthesised rumble.""" + d = os.path.join(OUT, "sfx", "thunder") + os.makedirs(d, exist_ok=True) + print("thunder ->", os.path.relpath(d, ROOT)) + sr = 44100 + for kind, lead, most in (("near", 0.15, 7.0), ("far", 0.6, 10.0)): + for n, src in enumerate(sources("thunder_" + kind)): + raw = subprocess.run(["ffmpeg", "-v", "error", "-i", src, "-ac", "1", "-ar", str(sr), "-f", "s16le", "-"], + capture_output=True, check=True).stdout + x = [struct.unpack_from(" 0 and env[j] > top * 0.1: + j -= 1 + start = max(0, int((j * 0.05 - lead) * sr)) + # On until it's died away (40 dB down), or `most` seconds. + k = ip + while k < len(env) - 1 and env[k] > top * 0.01 and (k - j) * 0.05 < most: + k += 1 + end = min(len(x), int(k * 0.05 * sr) + win) + seg = x[start:end] + fade_in, fade_out = int(0.02 * sr), min(int(1.5 * sr), len(seg) // 3) + for i in range(fade_in): + seg[i] *= i / fade_in + for i in range(fade_out): + seg[-1 - i] *= i / fade_out + _encode(os.path.join(d, "thunder_%s_%d.ogg" % (kind, n)), [seg], sr, 5) + + def make_recorded(name, srcs=None): """A field-recorded ambience loop (assets/audio/ambience/.ogg) from CC0 recordings (tools/audio/sources/_*, or the files given): each @@ -808,6 +846,9 @@ if __name__ == "__main__": if what in RECORDED: # one recorded loop: birds | crickets make_recorded(what) sys.exit(0) + if what == "thunder": # the recorded thunder (not part of "all": it's from sources/) + make_thunder() + sys.exit(0) if what in ("all", "sfx"): make_sfx() if what in ("all", "sfx", "voices"): diff --git a/tools/audio/sources/thunder_far_davewelsh_194364.mp3 b/tools/audio/sources/thunder_far_davewelsh_194364.mp3 new file mode 100644 index 0000000..864853e Binary files /dev/null and b/tools/audio/sources/thunder_far_davewelsh_194364.mp3 differ diff --git a/tools/audio/sources/thunder_far_nednednerb_38250.mp3 b/tools/audio/sources/thunder_far_nednednerb_38250.mp3 new file mode 100644 index 0000000..c512c1a Binary files /dev/null and b/tools/audio/sources/thunder_far_nednednerb_38250.mp3 differ diff --git a/tools/audio/sources/thunder_far_sholecoltis_683421.mp3 b/tools/audio/sources/thunder_far_sholecoltis_683421.mp3 new file mode 100644 index 0000000..fd35e0e Binary files /dev/null and b/tools/audio/sources/thunder_far_sholecoltis_683421.mp3 differ diff --git a/tools/audio/sources/thunder_near_ayadrevis_652690.mp3 b/tools/audio/sources/thunder_near_ayadrevis_652690.mp3 new file mode 100644 index 0000000..f88098b Binary files /dev/null and b/tools/audio/sources/thunder_near_ayadrevis_652690.mp3 differ diff --git a/tools/audio/sources/thunder_near_emulius_744723.mp3 b/tools/audio/sources/thunder_near_emulius_744723.mp3 new file mode 100644 index 0000000..2bc1ecc Binary files /dev/null and b/tools/audio/sources/thunder_near_emulius_744723.mp3 differ diff --git a/tools/audio/sources/thunder_near_saha213131_696550.mp3 b/tools/audio/sources/thunder_near_saha213131_696550.mp3 new file mode 100644 index 0000000..eae6289 Binary files /dev/null and b/tools/audio/sources/thunder_near_saha213131_696550.mp3 differ