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()