Initial project sync
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
class_name AudioLayerController
|
||||
extends Node
|
||||
|
||||
## Presentation-layer dual music loops (AnchorV1.0 chapter 20).
|
||||
## Past and Future layers start together and stay in sync forever; a phase
|
||||
## switch only crossfades volumes. The clock authority stays with
|
||||
## RhythmManager — drift correction seeks THESE players, never the clock.
|
||||
##
|
||||
## Placeholder assets: both layers use the same song; the Future layer is
|
||||
## routed through a low-pass bus so the switch is audible until a real
|
||||
## Future loop asset lands (BPM/beat-aligned per the design doc).
|
||||
|
||||
const FUTURE_BUS_NAME := "TimePhaseFuture"
|
||||
const MUSIC_BUS_NAME := "Music"
|
||||
|
||||
@export var past_stream: AudioStream
|
||||
@export var future_stream: AudioStream
|
||||
@export var default_stream_path := "res://assets/audio/ev_past1.mp3"
|
||||
@export var crossfade_seconds := 0.35
|
||||
@export var active_volume_db := 0.0
|
||||
@export var inactive_volume_db := -60.0
|
||||
@export var drift_check_interval := 2.0
|
||||
@export var drift_threshold_seconds := 0.015
|
||||
@export var mute_clock := true
|
||||
@export var future_lowpass_cutoff_hz := 2200.0
|
||||
|
||||
var past_player: AudioStreamPlayer
|
||||
var future_player: AudioStreamPlayer
|
||||
|
||||
var _fade_tween: Tween
|
||||
var _drift_accum := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
if past_stream == null and ResourceLoader.exists(default_stream_path):
|
||||
past_stream = load(default_stream_path)
|
||||
if future_stream == null:
|
||||
future_stream = past_stream
|
||||
_ensure_future_bus()
|
||||
# 双音轨走 Music 总线(Future 层经 TimePhaseFuture 低通后 send 到 Music),
|
||||
# 与标题音乐一致,音乐音量设置才能同时约束标题与局内音乐。
|
||||
past_player = _make_layer_player("PastLoop", past_stream, MUSIC_BUS_NAME if AudioServer.get_bus_index(MUSIC_BUS_NAME) != -1 else "Master")
|
||||
future_player = _make_layer_player("FutureLoop", future_stream, FUTURE_BUS_NAME)
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and bus.has_signal("time_phase_changed") and not bus.is_connected("time_phase_changed", _on_time_phase_changed):
|
||||
bus.connect("time_phase_changed", _on_time_phase_changed)
|
||||
_start_layers()
|
||||
_apply_phase_volumes(_current_time_phase(), true)
|
||||
if mute_clock:
|
||||
var rhythm := _rhythm_manager_or_null()
|
||||
if rhythm != null:
|
||||
rhythm.set("volume_db", -80.0)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if past_player == null:
|
||||
return
|
||||
_drift_accum += delta
|
||||
if _drift_accum < drift_check_interval:
|
||||
return
|
||||
_drift_accum = 0.0
|
||||
_correct_layer_drift()
|
||||
|
||||
|
||||
## Snaps both layers back onto the clock immediately (a fresh run just
|
||||
## re-zeroed RhythmManager) instead of waiting for the next drift check.
|
||||
func restart_layers() -> void:
|
||||
_start_layers()
|
||||
_apply_phase_volumes(_current_time_phase(), true)
|
||||
_drift_accum = 0.0
|
||||
|
||||
|
||||
func _start_layers() -> void:
|
||||
var rhythm := _rhythm_manager_or_null()
|
||||
var start_position := 0.0
|
||||
if rhythm != null and rhythm.has_method("song_position"):
|
||||
start_position = maxf(0.0, float(rhythm.call("song_position")))
|
||||
for player: AudioStreamPlayer in [past_player, future_player]:
|
||||
if player != null and player.stream != null:
|
||||
player.play(start_position)
|
||||
|
||||
|
||||
func _correct_layer_drift() -> void:
|
||||
var rhythm := _rhythm_manager_or_null()
|
||||
if rhythm == null or not rhythm.has_method("song_position"):
|
||||
return
|
||||
if not bool(rhythm.get("running")):
|
||||
return
|
||||
var target := float(rhythm.call("song_position"))
|
||||
for player: AudioStreamPlayer in [past_player, future_player]:
|
||||
if player == null or not player.playing or player.stream == null:
|
||||
continue
|
||||
var length := float(player.stream.get_length())
|
||||
var wrapped_target := fmod(target, length) if length > 0.0 else target
|
||||
var raw_drift := absf(player.get_playback_position() - wrapped_target)
|
||||
var drift := minf(raw_drift, length - raw_drift) if length > 0.0 else raw_drift
|
||||
if drift > drift_threshold_seconds:
|
||||
player.seek(wrapped_target)
|
||||
|
||||
|
||||
func _on_time_phase_changed(_previous: StringName, current: StringName, _reason: StringName) -> void:
|
||||
_apply_phase_volumes(current, false)
|
||||
|
||||
|
||||
func _apply_phase_volumes(time_phase: StringName, instant: bool) -> void:
|
||||
if past_player == null or future_player == null:
|
||||
return
|
||||
var past_target := active_volume_db if time_phase != &"future" else inactive_volume_db
|
||||
var future_target := active_volume_db if time_phase == &"future" else inactive_volume_db
|
||||
if _fade_tween != null and _fade_tween.is_valid():
|
||||
_fade_tween.kill()
|
||||
if instant:
|
||||
past_player.volume_db = past_target
|
||||
future_player.volume_db = future_target
|
||||
return
|
||||
_fade_tween = create_tween()
|
||||
_fade_tween.set_parallel(true)
|
||||
_fade_tween.tween_property(past_player, "volume_db", past_target, crossfade_seconds)
|
||||
_fade_tween.tween_property(future_player, "volume_db", future_target, crossfade_seconds)
|
||||
|
||||
|
||||
func _make_layer_player(player_name: String, stream: AudioStream, bus_name: String) -> AudioStreamPlayer:
|
||||
var player := AudioStreamPlayer.new()
|
||||
player.name = player_name
|
||||
player.stream = stream
|
||||
player.volume_db = inactive_volume_db
|
||||
if AudioServer.get_bus_index(bus_name) != -1:
|
||||
player.bus = bus_name
|
||||
add_child(player)
|
||||
return player
|
||||
|
||||
|
||||
func _ensure_future_bus() -> void:
|
||||
var existing := AudioServer.get_bus_index(FUTURE_BUS_NAME)
|
||||
if existing != -1:
|
||||
_route_bus_to_music(existing)
|
||||
return
|
||||
AudioServer.add_bus()
|
||||
var bus_index := AudioServer.bus_count - 1
|
||||
AudioServer.set_bus_name(bus_index, FUTURE_BUS_NAME)
|
||||
var lowpass := AudioEffectLowPassFilter.new()
|
||||
lowpass.cutoff_hz = future_lowpass_cutoff_hz
|
||||
AudioServer.add_bus_effect(bus_index, lowpass)
|
||||
_route_bus_to_music(bus_index)
|
||||
|
||||
|
||||
func _route_bus_to_music(bus_index: int) -> void:
|
||||
if AudioServer.get_bus_index(MUSIC_BUS_NAME) != -1:
|
||||
AudioServer.set_bus_send(bus_index, MUSIC_BUS_NAME)
|
||||
|
||||
|
||||
func _current_time_phase() -> StringName:
|
||||
var manager := get_tree().root.get_node_or_null("TimePhaseManager") if is_inside_tree() else null
|
||||
if manager != null:
|
||||
return StringName(str(manager.get("current_time_phase")))
|
||||
return &"past"
|
||||
|
||||
|
||||
func _rhythm_manager_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("RhythmManager")
|
||||
|
||||
|
||||
func _event_bus_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("EventBus")
|
||||
Reference in New Issue
Block a user