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.
109 lines
3.7 KiB
GDScript
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()
|