This commit is contained in:
Nick
2026-03-02 20:14:16 +01:00
parent 1975a6f1d0
commit deee299af5
7 changed files with 1245 additions and 90 deletions

View File

@@ -1582,4 +1582,215 @@ class TF5MixerController:
command = f"set MIXER:Lib/Bank/Scene/Store {1 if bank.lower() == 'b' else 0} {scene_number}"
response = self._send_command(command)
return {"status": "success" if "OK" in response else "error",
"message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response}
"message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response}
# =========================================================================
# METER READING
# Riferimento prminfo:
# 2000 InCh 40ch 3 tipi: 0=PreHPF, 1=PreFader, 2=PostOn
# 2001 StInCh 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
# 2002 FxRtnCh 4ch 2 tipi: 0=PreFader, 1=PostOn
# 2100 Mix 20ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
# 2101 Mtrx 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
# 2102 St 2ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
# 2103 Mono 1ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
#
# Il valore raw è un intero 0127.
# Conversione: 0 = -inf dB, 1127 lineare su scala ~-72..+18 dBFS.
# Formula Yamaha TF: dBFS = (raw / 127) * 90 - 72 (approssimazione lineare)
# Oppure si restituisce direttamente il raw per display proporzionale.
# =========================================================================
# Mappa dei meter disponibili
_METER_DEFS = {
"InCh": {"path": "MIXER:Current/Meter/InCh", "channels": 40, "types": ["PreHPF", "PreFader", "PostOn"]},
"StInCh": {"path": "MIXER:Current/Meter/StInCh", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
"FxRtnCh": {"path": "MIXER:Current/Meter/FxRtnCh", "channels": 4, "types": ["PreFader", "PostOn"]},
"Mix": {"path": "MIXER:Current/Meter/Mix", "channels": 20, "types": ["PreEQ", "PreFader", "PostOn"]},
"Mtrx": {"path": "MIXER:Current/Meter/Mtrx", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
"St": {"path": "MIXER:Current/Meter/St", "channels": 2, "types": ["PreEQ", "PreFader", "PostOn"]},
"Mono": {"path": "MIXER:Current/Meter/Mono", "channels": 1, "types": ["PreEQ", "PreFader", "PostOn"]},
}
def _meter_raw_to_db(self, raw: int) -> float:
"""
Converte il valore raw del meter (0-127) in dBFS approssimato.
Scala Yamaha TF: 127 = +18 dBFS, 76 = 0 dBFS, 1 = -72 dBFS, 0 = -inf.
"""
if raw <= 0:
return float('-inf')
# Interpolazione lineare: 0..127 → -72..+18 dBFS (range 90 dB)
return round((raw / 127.0) * 90.0 - 72.0, 1)
def _parse_meter_response(self, response: str) -> Optional[int]:
"""Estrae il valore intero raw dal response di un comando get meter."""
try:
first_line = response.split('\n')[0].strip()
parts = first_line.split()
if parts and parts[0] == "OK":
return int(parts[-1])
except (ValueError, IndexError):
pass
return None
def get_meter(self, section: str, channel: int, meter_type: int = 2) -> dict:
"""
Legge il livello meter di un singolo canale.
Args:
section: Sezione del mixer: "InCh", "StInCh", "FxRtnCh",
"Mix", "Mtrx", "St", "Mono"
channel: Numero canale (1-based)
meter_type: Indice del tipo meter (0-based, vedi _METER_DEFS per i nomi)
Returns:
dict con raw (0-127), level_db (dBFS), section, channel, type_name
"""
if section not in self._METER_DEFS:
return {"status": "error",
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
defn = self._METER_DEFS[section]
num_ch = defn["channels"]
num_types = len(defn["types"])
if not 1 <= channel <= num_ch:
return {"status": "error", "message": f"Canale {section} deve essere tra 1 e {num_ch}"}
if not 0 <= meter_type < num_types:
return {"status": "error",
"message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"}
ch_idx = channel - 1
command = f"get {defn['path']} {ch_idx} {meter_type}"
response = self._send_command(command)
raw = self._parse_meter_response(response)
if raw is None:
return {"status": "error", "message": f"Risposta non valida: {response}"}
level_db = self._meter_raw_to_db(raw)
type_name = defn["types"][meter_type]
return {
"status": "success",
"section": section,
"channel": channel,
"type": meter_type,
"type_name": type_name,
"raw": raw,
"level_db": level_db,
"message": f"{section} ch{channel} [{type_name}]: {raw}/127 → {level_db} dBFS"
}
def get_all_meters(self, section: str, meter_type: int = 2) -> dict:
"""
Legge tutti i canali meter di una sezione.
Args:
section: "InCh", "StInCh", "FxRtnCh", "Mix", "Mtrx", "St", "Mono"
meter_type: Indice tipo meter (0-based)
Returns:
dict con lista di letture meter per tutti i canali della sezione
"""
if section not in self._METER_DEFS:
return {"status": "error",
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
defn = self._METER_DEFS[section]
num_ch = defn["channels"]
num_types = len(defn["types"])
if not 0 <= meter_type < num_types:
return {"status": "error",
"message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"}
type_name = defn["types"][meter_type]
readings = []
for ch in range(1, num_ch + 1):
ch_idx = ch - 1
command = f"get {defn['path']} {ch_idx} {meter_type}"
response = self._send_command(command)
raw = self._parse_meter_response(response)
if raw is not None:
level_db = self._meter_raw_to_db(raw)
readings.append({
"channel": ch,
"raw": raw,
"level_db": level_db
})
else:
readings.append({
"channel": ch,
"raw": None,
"level_db": None,
"error": response
})
return {
"status": "success",
"section": section,
"type": meter_type,
"type_name": type_name,
"total_channels": num_ch,
"readings": readings,
"message": f"Meter {section} [{type_name}]: {num_ch} canali letti"
}
def get_meter_snapshot(self, meter_type: int = 2) -> dict:
"""
Legge un snapshot dei meter di tutte le sezioni principali.
Args:
meter_type: Tipo meter da usare per tutte le sezioni.
Nota: FxRtnCh ha solo 2 tipi (0-1), se si passa 2
viene automaticamente usato il tipo 1 (PostOn).
Returns:
dict con un sotto-dict per ogni sezione, contenente le letture.
"""
snapshot = {}
for section, defn in self._METER_DEFS.items():
# Clamp meter_type al massimo disponibile per la sezione
safe_type = min(meter_type, len(defn["types"]) - 1)
result = self.get_all_meters(section, safe_type)
snapshot[section] = result
sections_ok = sum(1 for r in snapshot.values() if r["status"] == "success")
return {
"status": "success",
"meter_type_requested": meter_type,
"sections": snapshot,
"message": f"Snapshot meter: {sections_ok}/{len(self._METER_DEFS)} sezioni lette"
}
def get_meter_types(self, section: str = None) -> dict:
"""
Restituisce i tipi di meter disponibili per una sezione (o tutte).
Args:
section: Nome sezione opzionale. Se None, restituisce tutte.
"""
if section:
if section not in self._METER_DEFS:
return {"status": "error",
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
defn = self._METER_DEFS[section]
return {
"status": "success",
"section": section,
"channels": defn["channels"],
"types": {i: name for i, name in enumerate(defn["types"])}
}
else:
return {
"status": "success",
"sections": {
sec: {
"channels": d["channels"],
"types": {i: name for i, name in enumerate(d["types"])}
}
for sec, d in self._METER_DEFS.items()
}
}