Files
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

109 lines
3.7 KiB
GDScript

extends Node
## Two arrangements share one sample clock; changing layer never restarts music.
const FRONT_PATH := "res://assets/audio/bgm/tree_theme_front_candidate.wav"
const BACK_PATH := "res://assets/audio/bgm/tree_theme_back_candidate.wav"
const CROSSFADE_SECONDS := 1.25
var mix := 0.0
var target_mix := 0.0
var playback_starts := 0
var startup_error := ""
var _start_mix := 0.0
var _elapsed := CROSSFADE_SECONDS
var _player: AudioStreamPlayer
var _synchronized: AudioStreamSynchronized
var _closed := false
func configure(initial_layer: int) -> void:
mix = float(clampi(initial_layer, 0, 1))
target_mix = mix
_start_mix = mix
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
var front := ResourceLoader.load(FRONT_PATH, "AudioStreamWAV", ResourceLoader.CACHE_MODE_IGNORE) as AudioStreamWAV
var back := ResourceLoader.load(BACK_PATH, "AudioStreamWAV", ResourceLoader.CACHE_MODE_IGNORE) as AudioStreamWAV
if front == null or back == null:
startup_error = "MISSING_PAIRED_BGM"
return
if front.format != AudioStreamWAV.FORMAT_16_BITS or back.format != front.format or not front.stereo or not back.stereo or front.mix_rate != back.mix_rate or front.data.size() != back.data.size() or front.data.is_empty():
startup_error = "INCOMPATIBLE_PAIRED_BGM"
return
for stream: AudioStreamWAV in [front, back]:
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
stream.loop_begin = 0
stream.loop_end = int(stream.data.size() / 4.0)
_synchronized = AudioStreamSynchronized.new()
_synchronized.stream_count = 2
_synchronized.set_sync_stream(0, front)
_synchronized.set_sync_stream(1, back)
_apply_mix()
_player = AudioStreamPlayer.new()
_player.name = "PairedBGMPlayer"
_player.process_mode = Node.PROCESS_MODE_ALWAYS
_player.bus = &"Master"
_player.stream = _synchronized
add_child(_player)
_player.play()
playback_starts += 1
func set_layer(layer: int) -> void:
if _closed:
return
var destination := float(clampi(layer, 0, 1))
if destination == target_mix:
return
_start_mix = mix
target_mix = destination
_elapsed = 0.0
func _process(delta: float) -> void:
if _closed or _elapsed >= CROSSFADE_SECONDS or not is_finite(delta) or delta <= 0.0:
return
_elapsed = minf(CROSSFADE_SECONDS, _elapsed + delta)
mix = lerpf(_start_mix, target_mix, _elapsed / CROSSFADE_SECONDS)
_apply_mix()
func _apply_mix() -> void:
if _synchronized == null:
return
var front_gain := cos(mix * PI / 2.0)
var back_gain := sin(mix * PI / 2.0)
_synchronized.set_sync_stream_volume(0, linear_to_db(front_gain) if front_gain > 0.0001 else -80.0)
_synchronized.set_sync_stream_volume(1, linear_to_db(back_gain) if back_gain > 0.0001 else -80.0)
func capture_owned_targets() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for entry: Array in [[&"bgm_owner", self], [&"bgm_player", _player], [&"bgm_synchronized", _synchronized]]:
if is_instance_valid(entry[1]):
result.append({&"role": &"bgm", &"kind": entry[0], &"weak": weakref(entry[1]), &"instance_id": entry[1].get_instance_id()})
if _synchronized != null:
for index: int in range(2):
var stream := _synchronized.get_sync_stream(index)
if stream != null:
result.append({&"role": &"front" if index == 0 else &"back", &"kind": &"bgm_stream", &"weak": weakref(stream), &"instance_id": stream.get_instance_id()})
if is_instance_valid(_player) and _player.has_stream_playback():
var playback := _player.get_stream_playback()
result.append({&"role": &"bgm", &"kind": &"bgm_playback", &"weak": weakref(playback), &"instance_id": playback.get_instance_id()})
return result
func prepare_owner_shutdown() -> void:
_closed = true
set_process(false)
if is_instance_valid(_player):
_player.stop()
_player.stream = null
_synchronized = null
func _exit_tree() -> void:
prepare_owner_shutdown()