Files
Automixbot/tf5_cly.py
2026-03-02 20:14:16 +01:00

930 lines
41 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
╔══════════════════════════════════════════════════════════╗
║ TF5 MIXER — CLI INTERATTIVO DI TEST ║
║ Yamaha TF5 · Socket Protocol Explorer ║
╚══════════════════════════════════════════════════════════╝
Script per esplorare e testare le funzionalità del controller TF5.
Modalità: SIMULAZIONE (mock) o REALE (connessione socket).
"""
import sys
import os
import json
import time
import math
import socket
import threading
import queue
try:
import readline # history navigazione CLI (Linux/macOS)
except ImportError:
pass # Windows: readline non disponibile, nessun problema
from typing import Optional, List
# ─────────────────────────────────────────────
# COLORI ANSI
# ─────────────────────────────────────────────
class C:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
WHITE = "\033[97m"
GRAY = "\033[90m"
def cprint(text, color=C.WHITE, bold=False):
prefix = C.BOLD if bold else ""
print(f"{prefix}{color}{text}{C.RESET}")
def header(text):
w = 58
print()
cprint("" + "" * w + "", C.CYAN)
cprint("" + f" {text} ".center(w) + "", C.CYAN, bold=True)
cprint("" + "" * w + "", C.CYAN)
def section(text):
print()
cprint(f" ┌─ {text} {'' * max(0, 50 - len(text))}", C.BLUE)
def ok(msg): cprint(f"{msg}", C.GREEN)
def err(msg): cprint(f"{msg}", C.RED)
def info(msg): cprint(f" · {msg}", C.GRAY)
def warn(msg): cprint(f"{msg}", C.YELLOW)
def result(d):
"""Stampa un dict risultato in modo leggibile."""
if isinstance(d, dict):
status = d.get("status", "?")
color = C.GREEN if status == "success" else C.RED if status == "error" else C.YELLOW
cprint(f"\n STATUS: {status}", color, bold=True)
for k, v in d.items():
if k == "status": continue
if isinstance(v, list):
cprint(f" {k}:", C.CYAN)
for item in v[:20]: # max 20 righe
print(f" {item}")
if len(v) > 20:
cprint(f" ... e altri {len(v)-20} elementi", C.GRAY)
elif isinstance(v, dict):
cprint(f" {k}:", C.CYAN)
for sk, sv in v.items():
print(f" {sk}: {sv}")
else:
cprint(f" {k}: ", C.CYAN, bold=False)
print(f" {v}")
else:
print(f" {d}")
def meter_bar(raw, width=30):
"""Barra visuale per i meter (0-127)."""
if raw is None:
return f"[{'?':^{width}}]"
filled = int((raw / 127.0) * width)
# Colore: verde → giallo → rosso
if raw < 80: col = C.GREEN
elif raw < 110: col = C.YELLOW
else: col = C.RED
bar = "" * filled + "" * (width - filled)
return f"{col}[{bar}]{C.RESET} {raw:3d}/127"
# ─────────────────────────────────────────────
# MOCK CONTROLLER (per test senza mixer fisico)
# ─────────────────────────────────────────────
class MockTF5Controller:
"""
Simulazione del TF5MixerController per testing offline.
Risponde con dati fake realistici.
"""
METER_DEFS = {
"InCh": {"channels": 40, "types": ["PreHPF", "PreFader", "PostOn"]},
"StInCh": {"channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
"FxRtnCh": {"channels": 4, "types": ["PreFader", "PostOn"]},
"Mix": {"channels": 20, "types": ["PreEQ", "PreFader", "PostOn"]},
"Mtrx": {"channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
"St": {"channels": 2, "types": ["PreEQ", "PreFader", "PostOn"]},
"Mono": {"channels": 1, "types": ["PreEQ", "PreFader", "PostOn"]},
}
CH_NAMES = ["Kick", "Snare", "HiHat", "OH L", "OH R", "Tom1", "Tom2", "Tom3",
"Bass", "Gtr L", "Gtr R", "Keys L", "Keys R", "Vox 1", "Vox 2",
"Vox 3", "BGV1", "BGV2", "BGV3", "Sax", "Trumpet", "Trombone",
"Perc", "Click", "Playback"] + [f"Ch {i}" for i in range(26, 41)]
def __init__(self):
import random
self._r = random
self._state = {
"channels": {str(i): {"channel": i, "name": self.CH_NAMES[i-1],
"on": True, "level_db": -10.0 + self._r.uniform(-20, 10),
"pan": self._r.randint(-63, 63)} for i in range(1, 41)},
"mixes": {str(i): {"mix": i, "name": f"MX {i}",
"on": True, "level_db": 0.0} for i in range(1, 21)},
"steinch": {str(i): {"channel": i, "name": f"Rt{i}L",
"on": True, "level_db": -12.0} for i in range(1, 5)},
"fxrtn": {str(i): {"channel": i, "name": f"Fx{i}L",
"on": True, "level_db": -6.0} for i in range(1, 5)},
"dcas": {str(i): {"dca": i, "name": f"DCA {i}",
"on": True, "level_db": 0.0} for i in range(1, 9)},
"matrices": {str(i): {"matrix": i, "name": f"MT {i}",
"on": True, "level_db": 0.0} for i in range(1, 5)},
"stereo": {"1": {"bus": 1, "name": "ST L", "on": True, "level_db": 0.0},
"2": {"bus": 2, "name": "ST R", "on": True, "level_db": 0.0}},
"mono": {"name": "MONO", "on": True, "level_db": -6.0},
"mute_masters": {str(i): {"group": i, "name": f"MUTE {i}", "on": False} for i in range(1, 7)},
}
warn("Modalità SIMULAZIONE attiva (nessun mixer fisico collegato)")
def _fake_raw(self):
import random
return random.randint(0, 127)
def _meter_raw_to_db(self, raw):
if raw <= 0: return float('-inf')
return round((raw / 127.0) * 90.0 - 72.0, 1)
def get_meter(self, section, channel, meter_type=2):
if section not in self.METER_DEFS:
return {"status": "error", "message": f"Sezione '{section}' non valida"}
defn = self.METER_DEFS[section]
if not 1 <= channel <= defn["channels"]:
return {"status": "error", "message": f"Canale fuori range (1-{defn['channels']})"}
if not 0 <= meter_type < len(defn["types"]):
return {"status": "error", "message": f"Tipo fuori range (0-{len(defn['types'])-1})"}
raw = self._fake_raw()
db = self._meter_raw_to_db(raw)
return {"status": "success", "section": section, "channel": channel,
"type": meter_type, "type_name": defn["types"][meter_type],
"raw": raw, "level_db": db,
"message": f"{section} ch{channel} [{defn['types'][meter_type]}]: {raw}/127 → {db} dBFS"}
def get_all_meters(self, section, meter_type=2):
if section not in self.METER_DEFS:
return {"status": "error", "message": f"Sezione '{section}' non valida"}
defn = self.METER_DEFS[section]
safe_type = min(meter_type, len(defn["types"]) - 1)
readings = []
for ch in range(1, defn["channels"] + 1):
raw = self._fake_raw()
readings.append({"channel": ch, "raw": raw, "level_db": self._meter_raw_to_db(raw)})
return {"status": "success", "section": section, "type": safe_type,
"type_name": defn["types"][safe_type], "total_channels": defn["channels"],
"readings": readings, "message": f"Meter {section}: {defn['channels']} canali"}
def get_meter_snapshot(self, meter_type=2):
snapshot = {}
for section in self.METER_DEFS:
snapshot[section] = self.get_all_meters(section, meter_type)
return {"status": "success", "sections": snapshot,
"message": f"Snapshot meter completo ({len(self.METER_DEFS)} sezioni)"}
def get_meter_types(self, section=None):
if section:
if section not in self.METER_DEFS:
return {"status": "error", "message": f"Sezione '{section}' non valida"}
d = self.METER_DEFS[section]
return {"status": "success", "section": section,
"channels": d["channels"], "types": {i: n for i, n in enumerate(d["types"])}}
return {"status": "success", "sections": {
s: {"channels": d["channels"], "types": {i: n for i, n in enumerate(d["types"])}}
for s, d in self.METER_DEFS.items()}}
def get_channel_info(self, channel, force_refresh=False):
if not 1 <= channel <= 40:
return {"status": "error", "message": "Canale deve essere tra 1 e 40"}
return {"status": "success", "source": "mock", **self._state["channels"][str(channel)]}
def get_all_channels_summary(self):
return {"status": "success", "total_channels": 40,
"channels": list(self._state["channels"].values()),
"cache_age_seconds": 0, "message": "40 canali (mock)"}
def set_channel_level(self, channel, level_db):
if not 1 <= channel <= 40:
return {"status": "error", "message": "Canale deve essere tra 1 e 40"}
self._state["channels"][str(channel)]["level_db"] = level_db
return {"status": "success", "message": f"Canale {channel}{level_db:+.1f} dB", "response": "OK (mock)"}
def set_channel_on_off(self, channel, state):
if not 1 <= channel <= 40:
return {"status": "error", "message": "Canale deve essere tra 1 e 40"}
self._state["channels"][str(channel)]["on"] = state
return {"status": "success", "message": f"Canale {channel} {'ON' if state else 'OFF'}", "response": "OK (mock)"}
def set_channel_pan(self, channel, pan_value):
if not 1 <= channel <= 40:
return {"status": "error", "message": "Canale deve essere tra 1 e 40"}
self._state["channels"][str(channel)]["pan"] = pan_value
return {"status": "success", "message": f"Canale {channel} pan → {pan_value}", "response": "OK (mock)"}
def get_mix_info(self, mix_number, force_refresh=False):
if not 1 <= mix_number <= 20:
return {"status": "error", "message": "Mix deve essere tra 1 e 20"}
return {"status": "success", "source": "mock", **self._state["mixes"][str(mix_number)]}
def get_all_mixes_summary(self):
return {"status": "success", "total_mixes": 20,
"mixes": list(self._state["mixes"].values()),
"cache_age_seconds": 0, "message": "20 mix (mock)"}
def set_mix_level(self, mix_number, level_db):
if not 1 <= mix_number <= 20:
return {"status": "error", "message": "Mix deve essere tra 1 e 20"}
self._state["mixes"][str(mix_number)]["level_db"] = level_db
return {"status": "success", "message": f"Mix {mix_number}{level_db:+.1f} dB", "response": "OK (mock)"}
def set_mix_on_off(self, mix_number, state):
if not 1 <= mix_number <= 20:
return {"status": "error", "message": "Mix deve essere tra 1 e 20"}
self._state["mixes"][str(mix_number)]["on"] = state
return {"status": "success", "message": f"Mix {mix_number} {'ON' if state else 'OFF'}", "response": "OK (mock)"}
def get_dca_info(self, dca, force_refresh=False):
if not 1 <= dca <= 8:
return {"status": "error", "message": "DCA deve essere tra 1 e 8"}
return {"status": "success", "source": "mock", **self._state["dcas"][str(dca)]}
def get_all_dca_summary(self):
return {"status": "success", "total_dcas": 8,
"dcas": list(self._state["dcas"].values()),
"cache_age_seconds": 0, "message": "8 DCA (mock)"}
def set_dca_level(self, dca, level_db):
if not 1 <= dca <= 8:
return {"status": "error", "message": "DCA deve essere tra 1 e 8"}
self._state["dcas"][str(dca)]["level_db"] = level_db
return {"status": "success", "message": f"DCA {dca}{level_db:+.1f} dB", "response": "OK (mock)"}
def set_dca_on_off(self, dca, state):
if not 1 <= dca <= 8:
return {"status": "error", "message": "DCA deve essere tra 1 e 8"}
self._state["dcas"][str(dca)]["on"] = state
return {"status": "success", "message": f"DCA {dca} {'ON' if state else 'OFF'}", "response": "OK (mock)"}
def get_mono_info(self, force_refresh=False):
return {"status": "success", "source": "mock", **self._state["mono"]}
def set_mono_level(self, level_db):
self._state["mono"]["level_db"] = level_db
return {"status": "success", "message": f"Mono → {level_db:+.1f} dB", "response": "OK (mock)"}
def set_mono_on_off(self, state):
self._state["mono"]["on"] = state
return {"status": "success", "message": f"Mono {'ON' if state else 'OFF'}", "response": "OK (mock)"}
def get_stereo_info(self, bus=1, force_refresh=False):
if not 1 <= bus <= 2:
return {"status": "error", "message": "Bus stereo deve essere 1 o 2"}
return {"status": "success", "source": "mock", **self._state["stereo"][str(bus)]}
def set_stereo_level(self, bus, level_db):
if not 1 <= bus <= 2:
return {"status": "error", "message": "Bus stereo deve essere 1 o 2"}
self._state["stereo"][str(bus)]["level_db"] = level_db
return {"status": "success", "message": f"St {bus}{level_db:+.1f} dB", "response": "OK (mock)"}
def get_steinch_info(self, channel, force_refresh=False):
if not 1 <= channel <= 4:
return {"status": "error", "message": "StInCh deve essere tra 1 e 4"}
return {"status": "success", "source": "mock", **self._state["steinch"][str(channel)]}
def get_fxrtn_info(self, channel, force_refresh=False):
if not 1 <= channel <= 4:
return {"status": "error", "message": "FxRtnCh deve essere tra 1 e 4"}
return {"status": "success", "source": "mock", **self._state["fxrtn"][str(channel)]}
def get_mute_master_state(self, group):
if not 1 <= group <= 6:
return {"status": "error", "message": "Mute group deve essere tra 1 e 6"}
return {"status": "success", "source": "mock", **self._state["mute_masters"][str(group)]}
def set_mute_master(self, group, state):
if not 1 <= group <= 6:
return {"status": "error", "message": "Mute group deve essere tra 1 e 6"}
self._state["mute_masters"][str(group)]["on"] = state
return {"status": "success", "message": f"MuteMaster {group} {'ON' if state else 'OFF'}", "response": "OK (mock)"}
def get_all_mute_masters_summary(self):
return {"status": "success", "total_groups": 6,
"groups": list(self._state["mute_masters"].values()),
"message": "6 Mute Master (mock)"}
def search_channels_by_name(self, term):
found = [v for v in self._state["channels"].values()
if term.lower() in v.get("name", "").lower()]
return {"status": "success", "found_count": len(found), "channels": found}
def refresh_cache(self):
return {"status": "success", "message": "Cache refresh (mock) completato"}
def recall_scene(self, bank, scene_number):
return {"status": "success", "message": f"Scena {bank.upper()}{scene_number} richiamata (mock)", "response": "OK (mock)"}
def close(self):
pass
# ─────────────────────────────────────────────
# CARICAMENTO CONTROLLER REALE
# ─────────────────────────────────────────────
def try_load_real_controller(host, port):
"""Prova a importare e istanziare il TF5MixerController reale."""
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mixer_controller import TF5MixerController
ctrl = TF5MixerController(host=host, port=port)
return ctrl, None
except ImportError as e:
return None, f"Import fallito: {e}"
except Exception as e:
return None, f"Connessione fallita: {e}"
# ─────────────────────────────────────────────
# HELPERS INPUT
# ─────────────────────────────────────────────
def ask(prompt, default=None):
try:
dp = f" [{default}]" if default is not None else ""
val = input(f"{C.CYAN}{prompt}{dp}: {C.RESET}").strip()
return val if val else str(default) if default is not None else ""
except (EOFError, KeyboardInterrupt):
return str(default) if default is not None else ""
def ask_int(prompt, default=None, min_val=None, max_val=None):
while True:
raw = ask(prompt, default)
try:
v = int(raw)
if min_val is not None and v < min_val:
err(f"Valore minimo: {min_val}")
continue
if max_val is not None and v > max_val:
err(f"Valore massimo: {max_val}")
continue
return v
except ValueError:
err("Inserisci un numero intero valido")
def ask_float(prompt, default=None):
while True:
raw = ask(prompt, default)
try:
return float(raw)
except ValueError:
err("Inserisci un numero decimale valido (es. -10.5)")
def ask_bool(prompt, default=True):
d = "s/N" if not default else "S/n"
raw = ask(f"{prompt} ({d})", "s" if default else "n").lower()
return raw in ("s", "si", "", "y", "yes", "1")
def pause():
try:
input(f"\n{C.GRAY} [Invio per continuare...]{C.RESET}")
except (EOFError, KeyboardInterrupt):
pass
# ─────────────────────────────────────────────
# MENU METER
# ─────────────────────────────────────────────
def menu_meter(ctrl):
SECTIONS = ["InCh", "StInCh", "FxRtnCh", "Mix", "Mtrx", "St", "Mono"]
while True:
header("METER READING")
cprint(" 1) Leggi un singolo canale meter", C.WHITE)
cprint(" 2) Leggi tutti i canali di una sezione", C.WHITE)
cprint(" 3) Snapshot completo (tutte le sezioni)", C.WHITE)
cprint(" 4) Visualizza tipi meter disponibili", C.WHITE)
cprint(" 5) Monitor live (aggiornamento continuo)", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
section("Meter singolo canale")
cprint(f" Sezioni: {', '.join(SECTIONS)}", C.GRAY)
sec = ask("Sezione", "InCh")
if sec not in SECTIONS:
err(f"Sezione non valida"); continue
defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS
defn = defs.get(sec, {})
max_ch = defn.get("channels", 40)
types = defn.get("types", [])
cprint(f" Tipi disponibili: {', '.join(f'{i}={t}' for i,t in enumerate(types))}", C.GRAY)
ch = ask_int("Canale", 1, 1, max_ch)
mt = ask_int("Tipo meter", len(types)-1, 0, len(types)-1)
r = ctrl.get_meter(sec, ch, mt)
result(r)
if r.get("status") == "success":
raw = r.get("raw", 0)
print(f"\n {meter_bar(raw)}")
pause()
elif cmd == "2":
section("Meter sezione completa")
cprint(f" Sezioni: {', '.join(SECTIONS)}", C.GRAY)
sec = ask("Sezione", "InCh")
if sec not in SECTIONS:
err(f"Sezione non valida"); continue
defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS
defn = defs.get(sec, {})
types = defn.get("types", [])
cprint(f" Tipi disponibili: {', '.join(f'{i}={t}' for i,t in enumerate(types))}", C.GRAY)
mt = ask_int("Tipo meter", len(types)-1, 0, len(types)-1)
r = ctrl.get_all_meters(sec, mt)
if r.get("status") == "success":
print()
cprint(f" {'CH':>4} {'RAW':>5} {'dBFS':>8} LIVELLO", C.CYAN, bold=True)
cprint(" " + "" * 52, C.GRAY)
for rd in r.get("readings", []):
ch = rd["channel"]
raw = rd.get("raw")
db = rd.get("level_db")
db_str = f"{db:+6.1f}" if db is not None and db != float('-inf') else " -inf"
bar = meter_bar(raw, 20)
print(f" {ch:>4} {(raw or 0):>5} {db_str} dB {bar}")
else:
result(r)
pause()
elif cmd == "3":
section("Snapshot completo")
defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS
mt = ask_int("Tipo meter preferito (verrà clampato per sezione)", 2, 0, 2)
r = ctrl.get_meter_snapshot(mt)
if r.get("status") == "success":
for sec_name, sec_data in r.get("sections", {}).items():
readings = sec_data.get("readings", [])
type_name = sec_data.get("type_name", "?")
if readings:
avg_raw = sum(rd.get("raw") or 0 for rd in readings) / len(readings)
peak_raw = max((rd.get("raw") or 0 for rd in readings), default=0)
print(f"\n {C.CYAN}{C.BOLD}{sec_name:10}{C.RESET} [{type_name}] "
f"ch:{len(readings)} avg:{avg_raw:5.1f} peak:{meter_bar(peak_raw, 15)}")
else:
result(r)
pause()
elif cmd == "4":
section("Tipi meter disponibili")
defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS
r = ctrl.get_meter_types()
print()
cprint(f" {'SEZIONE':12} {'CH':>5} TIPI DISPONIBILI", C.CYAN, bold=True)
cprint(" " + "" * 50, C.GRAY)
if r.get("status") == "success":
for sec_name, data in r.get("sections", {}).items():
types_str = " ".join(f"{i}={t}" for i, t in data.get("types", {}).items())
print(f" {sec_name:12} {data.get('channels', '?'):>5} {types_str}")
pause()
elif cmd == "5":
section("Monitor live meter")
cprint(" Premi CTRL+C per fermare", C.GRAY)
defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS
sec = ask("Sezione da monitorare", "InCh")
if sec not in SECTIONS:
err("Sezione non valida"); continue
mt = len(defs.get(sec, {}).get("types", [])) - 1
interval = ask_float("Intervallo secondi", 1.0)
try:
iteration = 0
while True:
r = ctrl.get_all_meters(sec, mt)
os.system("clear" if os.name != "nt" else "cls")
header(f"LIVE METER — {sec} [{r.get('type_name','?')}]")
cprint(f" Aggiornamento #{iteration+1} · {time.strftime('%H:%M:%S')}", C.GRAY)
print()
if r.get("status") == "success":
for rd in r.get("readings", []):
ch = rd["channel"]
raw = rd.get("raw")
db = rd.get("level_db")
db_str = f"{db:+6.1f}" if db is not None and db != float('-inf') else " -inf"
bar = meter_bar(raw, 25)
print(f" ch{ch:02d} {db_str} dBFS {bar}")
time.sleep(interval)
iteration += 1
except KeyboardInterrupt:
ok("Monitor fermato")
pause()
# ─────────────────────────────────────────────
# MENU CANALI
# ─────────────────────────────────────────────
def menu_channels(ctrl):
while True:
header("CANALI INPUT (InCh)")
cprint(" 1) Info canale", C.WHITE)
cprint(" 2) Imposta livello fader", C.WHITE)
cprint(" 3) Abilita / Disabilita canale", C.WHITE)
cprint(" 4) Imposta pan", C.WHITE)
cprint(" 5) Riepilogo tutti i canali", C.WHITE)
cprint(" 6) Cerca canale per nome", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
ch = ask_int("Numero canale", 1, 1, 40)
result(ctrl.get_channel_info(ch))
pause()
elif cmd == "2":
ch = ask_int("Numero canale", 1, 1, 40)
db = ask_float("Livello dB (es. -10, 0, +5)", -10)
result(ctrl.set_channel_level(ch, db))
pause()
elif cmd == "3":
ch = ask_int("Numero canale", 1, 1, 40)
state = ask_bool("Abilitare il canale?", True)
result(ctrl.set_channel_on_off(ch, state))
pause()
elif cmd == "4":
ch = ask_int("Numero canale", 1, 1, 40)
pan = ask_int("Pan (-63=L, 0=C, +63=R)", 0, -63, 63)
result(ctrl.set_channel_pan(ch, pan))
pause()
elif cmd == "5":
r = ctrl.get_all_channels_summary()
if r.get("status") == "success":
print()
cprint(f" {'CH':>4} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True)
cprint(" " + "" * 40, C.GRAY)
for ch in r.get("channels", []):
on_str = f"{C.GREEN}ON {C.RESET}" if ch["on"] else f"{C.RED}OFF{C.RESET}"
db = ch.get("level_db", -120)
db_str = f"{db:+6.1f}" if db > -120 else " -inf"
print(f" {ch['channel']:>4} {ch['name']:15} {on_str} {db_str} dB")
pause()
elif cmd == "6":
term = ask("Nome da cercare")
result(ctrl.search_channels_by_name(term))
pause()
# ─────────────────────────────────────────────
# MENU MIX BUS
# ─────────────────────────────────────────────
def menu_mixes(ctrl):
while True:
header("MIX BUSES")
cprint(" 1) Info mix bus", C.WHITE)
cprint(" 2) Imposta livello fader", C.WHITE)
cprint(" 3) Abilita / Disabilita mix bus", C.WHITE)
cprint(" 4) Riepilogo tutti i mix", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
m = ask_int("Numero mix", 1, 1, 20)
result(ctrl.get_mix_info(m))
pause()
elif cmd == "2":
m = ask_int("Numero mix", 1, 1, 20)
db = ask_float("Livello dB", 0)
result(ctrl.set_mix_level(m, db))
pause()
elif cmd == "3":
m = ask_int("Numero mix", 1, 1, 20)
state = ask_bool("Abilitare?", True)
result(ctrl.set_mix_on_off(m, state))
pause()
elif cmd == "4":
r = ctrl.get_all_mixes_summary()
if r.get("status") == "success":
print()
cprint(f" {'MIX':>5} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True)
cprint(" " + "" * 40, C.GRAY)
for m in r.get("mixes", []):
on_str = f"{C.GREEN}ON {C.RESET}" if m["on"] else f"{C.RED}OFF{C.RESET}"
db = m.get("level_db", -120)
db_str = f"{db:+6.1f}" if db > -120 else " -inf"
print(f" {m['mix']:>5} {m['name']:15} {on_str} {db_str} dB")
pause()
# ─────────────────────────────────────────────
# MENU DCA
# ─────────────────────────────────────────────
def menu_dca(ctrl):
while True:
header("DCA GROUPS")
cprint(" 1) Info DCA", C.WHITE)
cprint(" 2) Imposta livello", C.WHITE)
cprint(" 3) Abilita / Disabilita DCA", C.WHITE)
cprint(" 4) Riepilogo tutti i DCA", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
d = ask_int("Numero DCA", 1, 1, 8)
result(ctrl.get_dca_info(d))
pause()
elif cmd == "2":
d = ask_int("Numero DCA", 1, 1, 8)
db = ask_float("Livello dB", 0)
result(ctrl.set_dca_level(d, db))
pause()
elif cmd == "3":
d = ask_int("Numero DCA", 1, 1, 8)
state = ask_bool("Abilitare?", True)
result(ctrl.set_dca_on_off(d, state))
pause()
elif cmd == "4":
r = ctrl.get_all_dca_summary()
if r.get("status") == "success":
print()
cprint(f" {'DCA':>5} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True)
cprint(" " + "" * 40, C.GRAY)
for d in r.get("dcas", []):
on_str = f"{C.GREEN}ON {C.RESET}" if d["on"] else f"{C.RED}OFF{C.RESET}"
db = d.get("level_db", 0)
print(f" {d['dca']:>5} {d['name']:15} {on_str} {db:+6.1f} dB")
pause()
# ─────────────────────────────────────────────
# MENU BUS MASTER
# ─────────────────────────────────────────────
def menu_masters(ctrl):
while True:
header("BUS MASTER (Stereo / Mono)")
cprint(" 1) Info Stereo bus", C.WHITE)
cprint(" 2) Imposta livello Stereo", C.WHITE)
cprint(" 3) Info Mono bus", C.WHITE)
cprint(" 4) Imposta livello Mono", C.WHITE)
cprint(" 5) Mute Masters — stato", C.WHITE)
cprint(" 6) Attiva / Disattiva Mute Master group", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
bus = ask_int("Bus stereo (1=L, 2=R)", 1, 1, 2)
result(ctrl.get_stereo_info(bus))
pause()
elif cmd == "2":
bus = ask_int("Bus stereo (1=L, 2=R)", 1, 1, 2)
db = ask_float("Livello dB", 0)
result(ctrl.set_stereo_level(bus, db))
pause()
elif cmd == "3":
result(ctrl.get_mono_info())
pause()
elif cmd == "4":
db = ask_float("Livello dB", -6)
result(ctrl.set_mono_level(db))
pause()
elif cmd == "5":
result(ctrl.get_all_mute_masters_summary())
pause()
elif cmd == "6":
g = ask_int("Numero gruppo (1-6)", 1, 1, 6)
state = ask_bool("Attivare il mute?", False)
result(ctrl.set_mute_master(g, state))
pause()
# ─────────────────────────────────────────────
# MENU COMANDI RAW
# ─────────────────────────────────────────────
def menu_raw(ctrl):
"""Invia comandi raw al mixer (solo modalità reale)."""
if isinstance(ctrl, MockTF5Controller):
warn("I comandi raw non sono disponibili in modalità simulazione.")
pause()
return
header("COMANDI RAW")
cprint(" Invia comandi testuali diretti al mixer.", C.GRAY)
cprint(" Esempi:", C.GRAY)
cprint(" get MIXER:Current/InCh/Fader/Level 0 0", C.GRAY)
cprint(" set MIXER:Current/InCh/Fader/On 0 0 0", C.GRAY)
cprint(" get MIXER:Current/Meter/InCh 0 2", C.GRAY)
cprint(" Digita 'exit' per uscire.", C.GRAY)
print()
while True:
try:
cmd = input(f"{C.MAGENTA} RAW> {C.RESET}").strip()
if not cmd:
continue
if cmd.lower() in ("exit", "quit", "0"):
break
response = ctrl._send_command(cmd)
cprint(f"{response}", C.YELLOW)
except KeyboardInterrupt:
break
# ─────────────────────────────────────────────
# MENU SCENE
# ─────────────────────────────────────────────
def menu_scenes(ctrl):
while True:
header("SCENE")
cprint(" 1) Richiama scena (banco A o B)", C.WHITE)
cprint(" 2) Refresh cache", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
bank = ask("Banco (a/b)", "a").lower()
num = ask_int("Numero scena (0-99)", 0, 0, 99)
result(ctrl.recall_scene(bank, num))
pause()
elif cmd == "2":
ok("Avvio refresh cache...")
result(ctrl.refresh_cache())
pause()
# ─────────────────────────────────────────────
# MENU ALTRI CANALI
# ─────────────────────────────────────────────
def menu_other_channels(ctrl):
while True:
header("ALTRI CANALI")
cprint(" 1) Stereo Input (StInCh) — info", C.WHITE)
cprint(" 2) FX Return (FxRtnCh) — info", C.WHITE)
cprint(" 0) ← Torna al menu principale", C.GRAY)
cmd = ask("\nScelta")
if cmd == "0":
break
elif cmd == "1":
ch = ask_int("Canale StInCh (1-4)", 1, 1, 4)
result(ctrl.get_steinch_info(ch))
pause()
elif cmd == "2":
ch = ask_int("Canale FxRtnCh (1-4)", 1, 1, 4)
result(ctrl.get_fxrtn_info(ch))
pause()
# ─────────────────────────────────────────────
# HELP / REFERENCE
# ─────────────────────────────────────────────
def show_help():
header("REFERENCE — PROTOCOLLO TF5")
section("Struttura comandi")
print("""
get <PATH> <IDX1> <IDX2> → legge un parametro
set <PATH> <IDX1> <IDX2> <V> → scrive un parametro
ssrecall_ex scene_a <N> → richiama scena banco A
Risposta OK: OK <valore>
Risposta ERR: ERROR <codice> <messaggio>
Notify: NOTIFY <PATH> <IDX1> <IDX2> <V>
""")
section("Livelli fader")
print("""
Valore interno: intero × 100 (es. -1000 = -10.0 dB)
Minimo: -32768 = -∞ (fader giù)
Massimo: +1000 = +10.0 dB
Unity gain: 0 = 0.0 dB
""")
section("Meter")
print("""
Valore raw: 0127 (intero)
Conversione: dBFS = (raw/127) × 90 - 72
0 → -inf dBFS
76 → ~ 0 dBFS (approssimato)
127→ +18 dBFS
SEZIONI METER:
┌──────────┬─────┬────────────────────────────────────┐
│ InCh │ 40 │ 0=PreHPF 1=PreFader 2=PostOn │
│ StInCh │ 4 │ 0=PreEQ 1=PreFader 2=PostOn │
│ FxRtnCh │ 4 │ 0=PreFader 1=PostOn │
│ Mix │ 20 │ 0=PreEQ 1=PreFader 2=PostOn │
│ Mtrx │ 4 │ 0=PreEQ 1=PreFader 2=PostOn │
│ St │ 2 │ 0=PreEQ 1=PreFader 2=PostOn │
│ Mono │ 1 │ 0=PreEQ 1=PreFader 2=PostOn │
└──────────┴─────┴────────────────────────────────────┘
""")
section("Pan")
print("""
Valore: -63 (full left) → 0 (center) → +63 (full right)
""")
pause()
# ─────────────────────────────────────────────
# MAIN / SETUP
# ─────────────────────────────────────────────
def startup_screen():
os.system("clear" if os.name != "nt" else "cls")
print()
cprint("╔══════════════════════════════════════════════════════════╗", C.CYAN)
cprint("║ ║", C.CYAN)
cprint("║ YAMAHA TF5 — CLI INTERATTIVO DI TEST ║", C.CYAN, bold=True)
cprint("║ Socket Protocol Explorer ║", C.CYAN)
cprint("║ ║", C.CYAN)
cprint("╚══════════════════════════════════════════════════════════╝", C.CYAN)
print()
def choose_mode():
cprint(" Modalità di connessione:", C.WHITE, bold=True)
cprint(" 1) SIMULAZIONE (mock — nessun mixer necessario)", C.GREEN)
cprint(" 2) REALE (connessione socket al TF5)", C.YELLOW)
print()
choice = ask("Scelta", "1")
if choice == "2":
host = ask("Indirizzo IP del mixer", "192.168.0.128")
port = ask_int("Porta", 49280, 1, 65535)
info(f"Connessione a {host}:{port}...")
ctrl, error = try_load_real_controller(host, port)
if ctrl is None:
err(f"Impossibile connettersi: {error}")
warn("Avvio in modalità simulazione come fallback.")
return MockTF5Controller()
ok(f"Connesso a {host}:{port}")
return ctrl
else:
return MockTF5Controller()
def main():
startup_screen()
ctrl = choose_mode()
while True:
header("MENU PRINCIPALE")
cprint(f" Modalità: {'SIMULAZIONE (mock)' if isinstance(ctrl, MockTF5Controller) else 'REALE'}", C.GRAY)
print()
cprint(" M) Meter reading ──────────── leggi livelli meter", C.MAGENTA, bold=True)
cprint(" C) Canali input ────────────── InCh 1-40", C.WHITE)
cprint(" X) Mix buses ───────────────── MX 1-20", C.WHITE)
cprint(" D) DCA groups ──────────────── DCA 1-8", C.WHITE)
cprint(" B) Bus master ──────────────── Stereo / Mono / Mute", C.WHITE)
cprint(" A) Altri canali ────────────── StInCh / FxRtnCh", C.WHITE)
cprint(" S) Scene ───────────────────── recall / store", C.WHITE)
cprint(" R) Comandi RAW ─────────────── protocollo diretto", C.WHITE)
cprint(" H) Help / Reference ────────── documentazione", C.GRAY)
cprint(" Q) Esci", C.RED)
cmd = ask("\nScelta").upper()
if cmd == "Q":
cprint("\n Chiusura in corso...", C.GRAY)
ctrl.close()
cprint(" Arrivederci! 🎚️\n", C.CYAN)
break
elif cmd == "M":
menu_meter(ctrl)
elif cmd == "C":
menu_channels(ctrl)
elif cmd == "X":
menu_mixes(ctrl)
elif cmd == "D":
menu_dca(ctrl)
elif cmd == "B":
menu_masters(ctrl)
elif cmd == "A":
menu_other_channels(ctrl)
elif cmd == "S":
menu_scenes(ctrl)
elif cmd == "R":
menu_raw(ctrl)
elif cmd == "H":
show_help()
else:
warn("Scelta non valida")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print(f"\n\n{C.GRAY} Interrotto dall'utente.{C.RESET}\n")
sys.exit(0)