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.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user