add onnx and metadata

This commit is contained in:
Overside srl committed 2026-11-04 18:10:32 +01:00
1 parent 98d9fa24cf
commit 5f3f915258
18 files changed
+2229 -78

No files matched your search

+86 -10
View File
@@ -7,13 +7,14 @@ signal volume_updated(rms_value: float)
signal bpm_updated(bpm: float)
## Emette la lista delle applicazioni audio attive (es. Spotify, Chrome, Discord)
signal applications_updated(apps: Array[Dictionary])
signal genre_updated(genre: String, confidence: float, probs: Dictionary)
signal genre_updated(genre: String, confidence: float, probs: Dictionary, source: String, detail: String)
@export var udp_port: int = 4242
@export var cmd_port: int = 4243
@export var heartbeat_timeout_ms: int = 1500
@export var lease_enabled: bool = true
@export var genre_enabled: bool = true
@export var diag_enabled: bool = true
const LEASE_MS: int = 10000
const GENRE_LABELS = ["latin", "electronic", "hiphop", "rock", "classical"]
@@ -44,6 +45,9 @@ var _last_ping_msec: int = 0
var _genre_probs: Dictionary = {}
var _genre_argmax: String = ""
var _genre_last_emit_msec: int = -5000
var _genre_source: String = ""
var _genre_last_packet_msec: int = -1
var _diag_last_status: String = ""
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
@@ -70,6 +74,10 @@ func _spawn_bridge() -> void:
while _udp_peer.get_available_packet_count() > 0:
_udp_peer.get_packet()
_bridge_protocol = 0
if _genre_source == "metadata":
genre_updated.emit("unknown", 0.0, {}, "rules", "")
_genre_source = ""
_genre_last_packet_msec = -1
_genre_probs.clear()
_genre_argmax = ""
_genre_last_emit_msec = -5000
@@ -180,6 +188,12 @@ func _process(delta: float) -> void:
_parse_genre(packet.get_string_from_utf8())
continue
# Pacchetto diagnostico per-sorgente (DIAG:...). Riconosciuto PRIMA del ramo
# binario e ignorato ai fini del genere.
if packet.size() >= 5 and packet[0] == 68 and packet[1] == 73 and packet[2] == 65 and packet[3] == 71 and packet[4] == 58:
_parse_diag(packet.get_string_from_utf8())
continue
# Convenzione: le future estensioni testuali non devono MAI essere lunghe
# esattamente 8 byte (messaggio completo, non solo prefisso).
# HELLO:/HB:/APPS:/GENR: sono riconosciuti sopra; PING: e i futuri STATUS:/SILENT:
@@ -202,6 +216,14 @@ func _process(delta: float) -> void:
if not packet_received:
_loopback_rms = move_toward(_loopback_rms, 0.0, delta * 3.0)
# A stalled metadata stream must not leave a permanent 100% label on screen.
if _genre_source == "metadata" and _genre_last_packet_msec >= 0 and Time.get_ticks_msec() - _genre_last_packet_msec > 10000:
_genre_probs.clear()
_genre_argmax = ""
_genre_source = "rules"
_genre_last_packet_msec = -1
genre_updated.emit("unknown", 0.0, {}, "rules", "")
# Read queued heartbeats before checking the watchdog to avoid false timeouts.
_watchdog_timer += delta
if _watchdog_timer >= 0.5:
@@ -223,13 +245,31 @@ func _parse_genre(raw_text: String) -> void:
var fields: PackedStringArray = raw_text.substr(5).split(";")
if fields.size() < 8:
return
# Version, raw label and raw confidence precede the five probabilities.
# The displayed label/confidence are recomputed from the smoothed distribution.
var source: String = "rules"
var first_probability: int = 3
if fields[0] == "2":
if fields.size() < 9 or not ["metadata", "onnx", "rules"].has(fields[3]):
return
source = fields[3]
first_probability = 4
elif fields[0] != "1":
return
if source == "metadata" and (not GENRE_LABELS.has(fields[1]) or not fields[2].is_valid_float() or fields[2].to_float() != 1.0):
return
var incoming: Dictionary = {}
var total: float = 0.0
for i in range(3, fields.size()):
var pair: PackedStringArray = fields[i].split("=")
if pair.size() != 2 or not GENRE_LABELS.has(pair[0]):
var detail: String = ""
for i in range(first_probability, fields.size()):
# maxsplit=1: un valore puo' contenere '=' senza rompere il parsing.
var pair: PackedStringArray = fields[i].split("=", true, 1)
if pair.size() != 2:
continue
# Genere specifico dal catalogo ("Hard Rock", "Hip-Hop/Rap"): solo descrittivo,
# non partecipa alle probabilita' e non entra nella media esponenziale.
if pair[0] == "detail":
detail = pair[1].strip_edges()
continue
if not GENRE_LABELS.has(pair[0]):
continue
if incoming.has(pair[0]) or not pair[1].is_valid_float():
return
@@ -240,7 +280,13 @@ func _parse_genre(raw_text: String) -> void:
total += probability
if incoming.size() != GENRE_LABELS.size() or total <= 0.0:
return
var first: bool = _genre_probs.is_empty()
if source == "metadata":
# Discrete lookup: require a coherent one-hot distribution, never EMA-blend it.
for genre in GENRE_LABELS:
if float(incoming[genre]) != (1.0 if genre == fields[1] else 0.0):
return
var source_changed: bool = source != _genre_source
var first: bool = _genre_probs.is_empty() or source_changed or source == "metadata"
var best: String = ""
var confidence: float = -1.0
for genre in GENRE_LABELS:
@@ -253,12 +299,42 @@ func _parse_genre(raw_text: String) -> void:
confidence = float(_genre_probs[genre])
best = genre
var label: String = best if confidence >= GENRE_MIN_CONFIDENCE else "unknown"
if source != "metadata":
detail = ""
var now: int = Time.get_ticks_msec()
if best != _genre_argmax or now - _genre_last_emit_msec >= 5000:
_genre_last_packet_msec = now
_genre_source = source
if source_changed or best != _genre_argmax or now - _genre_last_emit_msec >= 5000:
_genre_argmax = best
_genre_last_emit_msec = now
print("[SystemAudioAnalyzer] Genere: %s (%.0f%%)" % [label, confidence * 100.0])
genre_updated.emit(label, confidence, _genre_probs.duplicate())
var shown: String = detail if not detail.is_empty() else label
print("[SystemAudioAnalyzer] Genere: %s (%.0f%%) [%s]" % [shown, confidence * 100.0, source])
genre_updated.emit(label, confidence, _genre_probs.duplicate(), source, detail)
## Stampa una riga compatta dello stato per-sorgente solo quando uno dei campi di
## stato cambia (non a ogni pacchetto). Il contenuto DIAG non influenza il genere.
func _parse_diag(raw_text: String) -> void:
if not diag_enabled:
return
var fields: PackedStringArray = raw_text.substr(5).split(";")
if fields.size() < 2:
return
var diag: Dictionary = {}
for i in range(1, fields.size()):
var pair: PackedStringArray = fields[i].split("=", true, 1)
if pair.size() != 2:
continue
diag[pair[0]] = pair[1]
var meta: String = diag.get("meta", "")
var onnx: String = diag.get("onnx", "")
var itunes: String = diag.get("itunes", "")
var deezer: String = diag.get("deezer", "")
var bpm: String = diag.get("bpm", "0.0")
var anchor: String = diag.get("anchor", "0.0")
var status_key: String = "%s|%s|%s|%s" % [meta, onnx, itunes, deezer]
if status_key != _diag_last_status:
_diag_last_status = status_key
print("[DIAG] meta=%s onnx=%s itunes=%s deezer=%s bpm=%s anchor=%s" % [meta, onnx, itunes, deezer, bpm, anchor])
func _parse_applications_list(raw_text: String) -> void:
var payload = raw_text.substr(5)