first commit
This commit is contained in:
commit
4b1fc0835a
14 files changed
+1133
No files matched your search
@@ -0,0 +1,214 @@
|
||||
class_name CompanionController
|
||||
extends Node2D
|
||||
|
||||
enum State {
|
||||
IDLE,
|
||||
DANCE,
|
||||
SLEEP
|
||||
}
|
||||
|
||||
@export_group("Audio Reactivity")
|
||||
@export_range(0.01, 1.0, 0.01) var audio_threshold: float = 0.04
|
||||
@export var dance_decay_time: float = 0.5
|
||||
@export var idle_to_sleep_time: float = 2.5
|
||||
|
||||
@export_group("Rhythm & Squish")
|
||||
@export_range(0.05, 0.5, 0.01) var squish_amount: float = 0.22
|
||||
@export var min_dance_animation_speed: float = 0.9
|
||||
@export var max_dance_animation_speed: float = 2.0
|
||||
|
||||
@export_group("Dependencies")
|
||||
@export var audio_analyzer: SystemAudioAnalyzer
|
||||
@export var sprite: AnimatedSprite2D
|
||||
@export var debug_label: Label
|
||||
@export var volume_bar: ProgressBar
|
||||
@export var app_selector: OptionButton
|
||||
|
||||
var current_state: State = State.IDLE
|
||||
var _silence_timer: float = 0.0
|
||||
var _current_rms: float = 0.0
|
||||
|
||||
# BPM dinamico: parte da 0 finché la canzone non viene agganciata
|
||||
var _current_bpm: float = 0.0
|
||||
var _target_bpm: float = 0.0
|
||||
var _base_scale: Vector2 = Vector2.ONE
|
||||
var _beat_phase: float = 0.0
|
||||
var _current_squish: float = 0.0
|
||||
var _breathe_time: float = 0.0
|
||||
|
||||
var _selected_pid: int = 0
|
||||
var _known_apps: Array[Dictionary] = []
|
||||
|
||||
func _ready() -> void:
|
||||
_base_scale = scale
|
||||
_current_bpm = 0.0
|
||||
_target_bpm = 0.0
|
||||
|
||||
if audio_analyzer:
|
||||
audio_analyzer.volume_updated.connect(_on_volume_updated)
|
||||
audio_analyzer.bpm_updated.connect(_on_bpm_updated)
|
||||
audio_analyzer.applications_updated.connect(_on_applications_updated)
|
||||
|
||||
if app_selector:
|
||||
app_selector.item_selected.connect(_on_app_selected)
|
||||
app_selector.clear()
|
||||
app_selector.add_item("Tutto il sistema", 0)
|
||||
|
||||
_set_state(State.IDLE)
|
||||
|
||||
func _on_applications_updated(apps: Array[Dictionary]) -> void:
|
||||
if not app_selector:
|
||||
return
|
||||
|
||||
# Evita di ricreare la lista se gli elementi sono identici (previene glitch della UI)
|
||||
var is_same = (apps.size() == _known_apps.size())
|
||||
if is_same:
|
||||
for i in range(apps.size()):
|
||||
if apps[i]["pid"] != _known_apps[i]["pid"] or apps[i]["name"] != _known_apps[i]["name"]:
|
||||
is_same = false
|
||||
break
|
||||
if is_same:
|
||||
return
|
||||
|
||||
_known_apps = apps
|
||||
var current_selected_pid = _selected_pid
|
||||
|
||||
app_selector.clear()
|
||||
var selected_idx = 0
|
||||
|
||||
for i in range(apps.size()):
|
||||
var app = apps[i]
|
||||
var label_text = app["name"]
|
||||
if app["pid"] == 0:
|
||||
label_text = "Tutto il sistema"
|
||||
else:
|
||||
label_text = "%s (PID %d)" % [app["name"], app["pid"]]
|
||||
|
||||
app_selector.add_item(label_text, i)
|
||||
if app["pid"] == current_selected_pid:
|
||||
selected_idx = i
|
||||
|
||||
app_selector.select(selected_idx)
|
||||
|
||||
func _on_app_selected(index: int) -> void:
|
||||
if index >= 0 and index < _known_apps.size():
|
||||
var chosen = _known_apps[index]
|
||||
_selected_pid = chosen["pid"]
|
||||
# Resetta il BPM per rilevare prontamente la nuova applicazione
|
||||
_target_bpm = 0.0
|
||||
_current_bpm = 0.0
|
||||
if audio_analyzer:
|
||||
audio_analyzer.select_application_pid(_selected_pid)
|
||||
|
||||
func _on_bpm_updated(detected_bpm: float) -> void:
|
||||
if detected_bpm > 50.0:
|
||||
_target_bpm = detected_bpm
|
||||
if _current_bpm <= 10.0:
|
||||
_current_bpm = detected_bpm
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _target_bpm > 50.0:
|
||||
_current_bpm = lerpf(_current_bpm, _target_bpm, delta * 3.0)
|
||||
|
||||
_handle_state_logic(delta)
|
||||
_handle_rhythm_squish(delta)
|
||||
_update_debug_ui()
|
||||
|
||||
func _on_volume_updated(rms: float) -> void:
|
||||
_current_rms = lerpf(_current_rms, rms, 0.35)
|
||||
|
||||
func _handle_state_logic(delta: float) -> void:
|
||||
var is_audio_playing: bool = _current_rms >= audio_threshold
|
||||
|
||||
match current_state:
|
||||
State.DANCE:
|
||||
if is_audio_playing:
|
||||
_silence_timer = 0.0
|
||||
_scale_dance_speed(_current_rms)
|
||||
else:
|
||||
_silence_timer += delta
|
||||
if _silence_timer >= dance_decay_time:
|
||||
_set_state(State.IDLE)
|
||||
|
||||
State.IDLE:
|
||||
if is_audio_playing:
|
||||
_set_state(State.DANCE)
|
||||
else:
|
||||
_silence_timer += delta
|
||||
if _silence_timer >= idle_to_sleep_time:
|
||||
_set_state(State.SLEEP)
|
||||
|
||||
State.SLEEP:
|
||||
if is_audio_playing:
|
||||
_set_state(State.DANCE)
|
||||
|
||||
func _set_state(new_state: State) -> void:
|
||||
current_state = new_state
|
||||
_silence_timer = 0.0
|
||||
|
||||
match current_state:
|
||||
State.IDLE:
|
||||
if sprite:
|
||||
sprite.speed_scale = 1.0
|
||||
_play_animation("idle")
|
||||
State.DANCE:
|
||||
_scale_dance_speed(_current_rms)
|
||||
_play_animation("dance")
|
||||
State.SLEEP:
|
||||
if sprite:
|
||||
sprite.speed_scale = 1.0
|
||||
_play_animation("sleep")
|
||||
|
||||
func _scale_dance_speed(rms: float) -> void:
|
||||
var normalized_intensity: float = clampf((rms - audio_threshold) * 2.5, 0.0, 1.0)
|
||||
var target_speed: float = lerpf(min_dance_animation_speed, max_dance_animation_speed, normalized_intensity)
|
||||
if sprite:
|
||||
sprite.speed_scale = target_speed
|
||||
|
||||
func _play_animation(anim_name: String) -> void:
|
||||
if sprite and sprite.sprite_frames and sprite.sprite_frames.has_animation(anim_name):
|
||||
sprite.play(anim_name)
|
||||
|
||||
func _handle_rhythm_squish(delta: float) -> void:
|
||||
match current_state:
|
||||
State.DANCE:
|
||||
var effective_bpm = _current_bpm if _current_bpm > 50.0 else 130.0
|
||||
var beats_per_second: float = effective_bpm / 60.0
|
||||
_beat_phase += delta * beats_per_second * TAU
|
||||
|
||||
var raw_wave: float = -cos(_beat_phase)
|
||||
var shaped_wave: float = sign(raw_wave) * pow(abs(raw_wave), 0.75)
|
||||
|
||||
var dynamic_depth: float = squish_amount * clampf(0.8 + _current_rms * 0.8, 0.8, 1.5)
|
||||
var target_squish: float = shaped_wave * dynamic_depth
|
||||
|
||||
_current_squish = lerpf(_current_squish, target_squish, delta * 25.0)
|
||||
|
||||
scale.x = _base_scale.x * (1.0 + _current_squish * 0.85)
|
||||
scale.y = _base_scale.y * (1.0 - _current_squish)
|
||||
|
||||
State.IDLE:
|
||||
_current_squish = lerpf(_current_squish, 0.0, delta * 6.0)
|
||||
_breathe_time += delta * 2.2
|
||||
var breathe: float = sin(_breathe_time) * 0.025
|
||||
scale = Vector2(_base_scale.x * (1.0 + breathe), _base_scale.y * (1.0 - breathe))
|
||||
|
||||
State.SLEEP:
|
||||
_current_squish = lerpf(_current_squish, 0.0, delta * 6.0)
|
||||
_breathe_time += delta * 1.2
|
||||
var deep_breathe: float = sin(_breathe_time) * 0.018
|
||||
scale = Vector2(_base_scale.x * (1.0 - deep_breathe), _base_scale.y * (1.0 + deep_breathe))
|
||||
|
||||
func _update_debug_ui() -> void:
|
||||
if volume_bar:
|
||||
volume_bar.value = _current_rms
|
||||
if debug_label:
|
||||
var bpm_str = "Rilevamento..."
|
||||
if _current_bpm > 50.0:
|
||||
bpm_str = "%d BPM" % round(_current_bpm)
|
||||
|
||||
debug_label.text = "Stato: %s | Volume: %.2f | %s" % [
|
||||
State.keys()[current_state],
|
||||
_current_rms,
|
||||
bpm_str
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8bfvlbipx8j8
|
||||
@@ -0,0 +1,111 @@
|
||||
class_name SystemAudioAnalyzer
|
||||
extends Node
|
||||
|
||||
## Emette il valore RMS continuo [0.0 - 1.0] esclusivo dell'audio di sistema/applicazione
|
||||
signal volume_updated(rms_value: float)
|
||||
## Emette il BPM rilevato in tempo reale dalla musica
|
||||
signal bpm_updated(bpm: float)
|
||||
## Emette la lista delle applicazioni audio attive (es. Spotify, Chrome, Discord)
|
||||
signal applications_updated(apps: Array[Dictionary])
|
||||
|
||||
@export var udp_port: int = 4242
|
||||
@export var cmd_port: int = 4243
|
||||
|
||||
@export_group("Gain & Sensitivity")
|
||||
@export var system_audio_gain: float = 3.8
|
||||
@export var system_noise_floor: float = 0.0003
|
||||
|
||||
var _udp_peer: PacketPeerUDP
|
||||
var _udp_send: PacketPeerUDP
|
||||
var _loopback_pid: int = -1
|
||||
var _loopback_rms: float = 0.0
|
||||
var _last_bpm: float = 0.0
|
||||
var _watchdog_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
_init_native_wasapi_loopback()
|
||||
|
||||
func _init_native_wasapi_loopback() -> void:
|
||||
_udp_peer = PacketPeerUDP.new()
|
||||
var err = _udp_peer.bind(udp_port, "*")
|
||||
if err != OK:
|
||||
err = _udp_peer.bind(udp_port, "127.0.0.1")
|
||||
|
||||
_udp_send = PacketPeerUDP.new()
|
||||
_udp_send.set_dest_address("127.0.0.1", cmd_port)
|
||||
|
||||
if err == OK:
|
||||
var exe_path = ProjectSettings.globalize_path("res://bin/LoopbackCapture.exe")
|
||||
if FileAccess.file_exists("res://bin/LoopbackCapture.exe"):
|
||||
_loopback_pid = OS.create_process(exe_path, [str(udp_port)], false)
|
||||
|
||||
## Invia al bridge la richiesta di ascoltare SOLO l'applicazione con il PID specificato (0 = Tutto)
|
||||
func select_application_pid(pid: int) -> void:
|
||||
if _udp_send:
|
||||
var msg = "PID:%d" % pid
|
||||
_udp_send.put_packet(msg.to_utf8_buffer())
|
||||
print("[SystemAudioAnalyzer] Applicazione audio selezionata PID: ", pid)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Watchdog: se LoopbackCapture non è in esecuzione, riavvialo automaticamente
|
||||
_watchdog_timer += delta
|
||||
if _watchdog_timer >= 2.0:
|
||||
_watchdog_timer = 0.0
|
||||
if _loopback_pid <= 0 or not OS.is_process_running(_loopback_pid):
|
||||
var exe_path = ProjectSettings.globalize_path("res://bin/LoopbackCapture.exe")
|
||||
if FileAccess.file_exists("res://bin/LoopbackCapture.exe"):
|
||||
_loopback_pid = OS.create_process(exe_path, [str(udp_port)], false)
|
||||
|
||||
# Lettura esclusiva dell'Audio di Sistema o dell'Applicazione selezionata (Zero Microfono)
|
||||
if _udp_peer:
|
||||
var packet_received: bool = false
|
||||
while _udp_peer.get_available_packet_count() > 0:
|
||||
var packet: PackedByteArray = _udp_peer.get_packet()
|
||||
|
||||
# Pacchetto con la lista delle applicazioni attive (APPS:...)
|
||||
if packet.size() > 5 and packet[0] == 65 and packet[1] == 80 and packet[2] == 80 and packet[3] == 83 and packet[4] == 58:
|
||||
var text: String = packet.get_string_from_utf8()
|
||||
_parse_applications_list(text)
|
||||
continue
|
||||
|
||||
# Pacchetto audio standard: 8 byte (4 byte RMS + 4 byte BPM)
|
||||
if packet.size() >= 4:
|
||||
_loopback_rms = packet.decode_float(0)
|
||||
packet_received = true
|
||||
|
||||
if packet.size() >= 8:
|
||||
var incoming_bpm: float = packet.decode_float(4)
|
||||
if incoming_bpm > 50.0:
|
||||
_last_bpm = incoming_bpm
|
||||
bpm_updated.emit(incoming_bpm)
|
||||
|
||||
if not packet_received:
|
||||
_loopback_rms = move_toward(_loopback_rms, 0.0, delta * 3.0)
|
||||
|
||||
# Calcolo Volume Percepito (Curva Loudness)
|
||||
var processed_rms: float = 0.0
|
||||
if _loopback_rms > system_noise_floor:
|
||||
processed_rms = clampf(sqrt(_loopback_rms) * system_audio_gain, 0.0, 1.0)
|
||||
|
||||
volume_updated.emit(processed_rms)
|
||||
|
||||
func _parse_applications_list(raw_text: String) -> void:
|
||||
var payload = raw_text.substr(5)
|
||||
var tokens = payload.split(";")
|
||||
var apps: Array[Dictionary] = []
|
||||
for token in tokens:
|
||||
var parts = token.split(":")
|
||||
if parts.size() >= 2:
|
||||
var pid = int(parts[0])
|
||||
var name = parts[1]
|
||||
apps.append({"pid": pid, "name": name})
|
||||
if not apps.is_empty():
|
||||
applications_updated.emit(apps)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _loopback_pid > 0:
|
||||
OS.kill(_loopback_pid)
|
||||
if _udp_peer:
|
||||
_udp_peer.close()
|
||||
if _udp_send:
|
||||
_udp_send.close()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c76vsjv0asi61
|
||||
Reference in new issue
Block a user