diff --git a/.tf5_mixer_cache/mixer_ip.json b/.tf5_mixer_cache/mixer_ip.json index 26fd4e5..e39c941 100644 --- a/.tf5_mixer_cache/mixer_ip.json +++ b/.tf5_mixer_cache/mixer_ip.json @@ -1,4 +1,4 @@ { - "ip": "192.168.1.57", - "timestamp": 1770578444.177839 + "ip": "192.168.1.59", + "timestamp": 1771271281.9053261 } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 43da559..3526bf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ dotenv google-genai +yaml \ No newline at end of file diff --git a/task_runner.py b/task_runner.py new file mode 100644 index 0000000..df12d55 --- /dev/null +++ b/task_runner.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +task_runner.py +============== +Esegue file di task YAML sul Yamaha TF5 tramite TF5MixerController. + +Uso da CLI: + python task_runner.py tasks_concerto_live.yaml # lista le cue disponibili + python task_runner.py tasks_concerto_live.yaml CUE_01 # esegue una cue specifica + python task_runner.py tasks_concerto_live.yaml --all # esegue tutte le cue in sequenza +""" + +import sys +import time +import yaml +from pathlib import Path +from typing import Optional + +from mixer_controller import TF5MixerController + + +# ────────────────────────────────────────────────────────────── +# DISPATCHER: mappa action → metodo del controller +# ────────────────────────────────────────────────────────────── + +class TaskRunner: + def __init__(self, controller: TF5MixerController): + self.ctrl = controller + + # Mappa nome azione → callable + self._action_map = { + "set_channel_level": self._set_channel_level, + "set_channel_on_off": self._set_channel_on_off, + "set_channel_pan": self._set_channel_pan, + "mute_channels": self._mute_channels, + "unmute_channels": self._unmute_channels, + "set_mix_level": self._set_mix_level, + "set_mix_on_off": self._set_mix_on_off, + "set_channel_to_mix_level": self._set_channel_to_mix_level, + "set_channel_to_mix_on_off":self._set_channel_to_mix_on_off, + "recall_scene": self._recall_scene, + "wait": self._wait, + "refresh_cache": self._refresh_cache, + } + + # ── Handlers specifici ────────────────────────────────── + + def _set_channel_level(self, step: dict) -> dict: + return self.ctrl.set_channel_level(step["channel"], float(step["level_db"])) + + def _set_channel_on_off(self, step: dict) -> dict: + return self.ctrl.set_channel_on_off(step["channel"], bool(step["state"])) + + def _set_channel_pan(self, step: dict) -> dict: + return self.ctrl.set_channel_pan(step["channel"], int(step["pan"])) + + def _mute_channels(self, step: dict) -> dict: + return self.ctrl.mute_multiple_channels(list(step["channels"])) + + def _unmute_channels(self, step: dict) -> dict: + return self.ctrl.unmute_multiple_channels(list(step["channels"])) + + def _set_mix_level(self, step: dict) -> dict: + return self.ctrl.set_mix_level(step["mix"], float(step["level_db"])) + + def _set_mix_on_off(self, step: dict) -> dict: + return self.ctrl.set_mix_on_off(step["mix"], bool(step["state"])) + + def _set_channel_to_mix_level(self, step: dict) -> dict: + return self.ctrl.set_channel_to_mix_level(step["channel"], step["mix"], float(step["level_db"])) + + def _set_channel_to_mix_on_off(self, step: dict) -> dict: + return self.ctrl.set_channel_to_mix_on_off(step["channel"], step["mix"], bool(step["state"])) + + def _recall_scene(self, step: dict) -> dict: + return self.ctrl.recall_scene(str(step["bank"]), int(step["scene"])) + + def _wait(self, step: dict) -> dict: + ms = int(step.get("ms", 0)) + print(f" ⏱ Wait puro: {ms} ms") + time.sleep(ms / 1000.0) + return {"status": "success", "message": f"Atteso {ms} ms"} + + def _refresh_cache(self, step: dict) -> dict: + return self.ctrl.refresh_cache() + + # ── Esecuzione di un singolo step ─────────────────────── + + def _run_step(self, step_num: int, step: dict) -> bool: + """Esegue un singolo step. Ritorna True se OK, False se errore.""" + action = step.get("action") + delay_ms = int(step.get("delay_ms", 0)) + wait_ms = int(step.get("wait_ms", 0)) + + if delay_ms > 0: + print(f" ⏳ delay_ms={delay_ms} prima dell'azione...") + time.sleep(delay_ms / 1000.0) + + handler = self._action_map.get(action) + if handler is None: + print(f" ⚠️ Step {step_num}: azione '{action}' non riconosciuta — saltato.") + return False + + print(f" ▶ Step {step_num}: {action} | params: { {k:v for k,v in step.items() if k not in ('action','delay_ms','wait_ms')} }") + try: + result = handler(step) + status = result.get("status", "unknown") + msg = result.get("message", "") + icon = "✅" if status in ("success", "partial") else "❌" + print(f" {icon} [{status}] {msg}") + + if wait_ms > 0: + time.sleep(wait_ms / 1000.0) + + return status in ("success", "partial") + + except Exception as e: + print(f" ❌ Eccezione durante '{action}': {e}") + return False + + # ── Esecuzione di una cue ──────────────────────────────── + + def run_cue(self, cue: dict) -> bool: + """ + Esegue tutti gli step di una cue. + Ritorna True se tutti gli step sono andati a buon fine. + """ + cue_id = cue.get("id", "???") + name = cue.get("name", "") + on_error = cue.get("on_error", "stop") + steps = cue.get("steps", []) + + print(f"\n{'═'*60}") + print(f"🎬 CUE: {cue_id} — {name}") + if cue.get("description"): + print(f" 📋 {cue['description']}") + print(f" Steps: {len(steps)} | on_error: {on_error}") + print(f"{'─'*60}") + + all_ok = True + for i, step in enumerate(steps, start=1): + ok = self._run_step(i, step) + if not ok: + all_ok = False + if on_error == "stop": + print(f"\n 🛑 on_error=stop: cue {cue_id} interrotta allo step {i}.") + return False + # on_error == "continue": vai avanti + + result_icon = "✅" if all_ok else "⚠️ (con errori)" + print(f"\n {result_icon} Cue {cue_id} completata.") + return all_ok + + +# ────────────────────────────────────────────────────────────── +# LOADER YAML +# ────────────────────────────────────────────────────────────── + +def load_task_file(path: str) -> dict: + """Carica e valida un file di task YAML.""" + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"File non trovato: {path}") + with open(p, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if "cues" not in data or not isinstance(data["cues"], list): + raise ValueError("Il file YAML deve contenere una lista 'cues'.") + return data + + +def print_event_info(data: dict): + event = data.get("event", {}) + if event: + print(f"\n{'═'*60}") + print(f"📅 Evento : {event.get('name', 'N/D')}") + print(f" Data : {event.get('date', 'N/D')}") + print(f" Venue : {event.get('venue', 'N/D')}") + if event.get("notes"): + print(f" Note : {event['notes']}") + + +def list_cues(data: dict): + print_event_info(data) + print(f"\n{'═'*60}") + print("📋 CUE DISPONIBILI:") + print(f"{'─'*60}") + for cue in data["cues"]: + steps_count = len(cue.get("steps", [])) + print(f" {cue.get('id'):12s} │ {cue.get('name','')} ({steps_count} steps)") + print(f"{'═'*60}\n") + + +# ────────────────────────────────────────────────────────────── +# ENTRY POINT +# ────────────────────────────────────────────────────────────── + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + + task_file = sys.argv[1] + cue_arg = sys.argv[2] if len(sys.argv) > 2 else None + + try: + data = load_task_file(task_file) + except (FileNotFoundError, ValueError) as e: + print(f"❌ Errore nel caricamento del file: {e}") + sys.exit(1) + + # Solo lista cue senza mixer + if cue_arg is None: + list_cues(data) + sys.exit(0) + + # Connessione al mixer ed esecuzione + print("🔌 Connessione al mixer...") + import config + + + with TF5MixerController(host=config.get_host()) as ctrl: + runner = TaskRunner(ctrl) + + if cue_arg == "--all": + print_event_info(data) + print("\n▶▶ Esecuzione di TUTTE le cue in sequenza...") + for cue in data["cues"]: + ok = runner.run_cue(cue) + if not ok and cue.get("on_error", "stop") == "stop": + print(f"\n🛑 Esecuzione interrotta a causa di un errore in {cue.get('id')}.") + sys.exit(1) + else: + # Cerca la cue per ID + cue_found = next((c for c in data["cues"] if c.get("id") == cue_arg), None) + if cue_found is None: + print(f"❌ Cue '{cue_arg}' non trovata nel file.") + list_cues(data) + sys.exit(1) + runner.run_cue(cue_found) + + print("\n✅ Task runner completato.\n") + + +if __name__ == "__main__": + main() diff --git a/task_schema.yaml b/task_schema.yaml new file mode 100644 index 0000000..0c94cf4 --- /dev/null +++ b/task_schema.yaml @@ -0,0 +1,71 @@ +# ============================================================ +# SCHEMA DEI TASK - Yamaha TF5 Mixer Controller +# ============================================================ +# Ogni file di task descrive UN evento (concerto, conferenza, ecc.) +# composto da N cues, ognuna con N steps eseguiti in sequenza. +# +# STRUTTURA GENERALE: +# +# event: → metadati dell'evento (opzionale) +# cues: → lista delle cue eseguibili +# - id: → identificatore univoco (es. "CUE_01") +# name: → nome leggibile +# description: → note operative (opzionale) +# on_error: → "stop" | "continue" (default: "stop") +# steps: → lista di azioni in sequenza +# - action: → tipo di azione (vedi sotto) +# ...params → parametri specifici dell'azione +# delay_ms: → attesa PRIMA di eseguire questo step (ms) +# wait_ms: → attesa DOPO l'esecuzione (ms) +# +# ============================================================ +# TIPI DI AZIONE DISPONIBILI: +# ============================================================ +# +# 1. set_channel_level +# channel: int (1-40) +# level_db: float (es. 0.0, -10.5, -inf → usa -999) +# +# 2. set_channel_on_off +# channel: int +# state: bool (true = ON, false = OFF / mute) +# +# 3. set_channel_pan +# channel: int +# pan: int (-63 = sinistra, 0 = centro, +63 = destra) +# +# 4. mute_channels +# channels: [int, int, ...] +# +# 5. unmute_channels +# channels: [int, int, ...] +# +# 6. set_mix_level +# mix: int (1-20) +# level_db: float +# +# 7. set_mix_on_off +# mix: int +# state: bool +# +# 8. set_channel_to_mix_level +# channel: int +# mix: int +# level_db: float +# +# 9. set_channel_to_mix_on_off +# channel: int +# mix: int +# state: bool +# +# 10. recall_scene +# bank: "a" | "b" +# scene: int (0-99) +# +# 11. wait +# ms: int → pausa pura senza azioni sul mixer +# +# 12. refresh_cache +# (nessun parametro) → forza un aggiornamento completo della cache +# +# ============================================================ diff --git a/tasks_concerto_live.yaml b/tasks_concerto_live.yaml new file mode 100644 index 0000000..4e5fd9e --- /dev/null +++ b/tasks_concerto_live.yaml @@ -0,0 +1,238 @@ +# ============================================================ +# TASK FILE - Concerto Live "Band Example" +# Yamaha TF5 Mixer Controller +# ============================================================ + +event: + name: "Concerto Live - Band Example" + date: "2025-06-14" + venue: "Teatro Comunale" + notes: "FOH setup. Mix 1=IEM Vocalist, Mix 2=IEM Chitarra, Mix 3=Wedge Batteria" + +cues: + + # ---------------------------------------------------------- + # PRE-SHOW: tutto silenzioso, scene base caricata + # ---------------------------------------------------------- + - id: "CUE_01" + name: "Pre-Show - Carica scena base" + description: "Richiama la scena di partenza e porta tutto a zero" + on_error: "stop" + steps: + - action: recall_scene + bank: "a" + scene: 1 + + - action: wait + ms: 3000 # attendi che il mixer elabori la scena + + - action: refresh_cache + wait_ms: 500 + + - action: mute_channels + channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + - action: set_mix_on_off + mix: 1 + state: false + - action: set_mix_on_off + mix: 2 + state: false + - action: set_mix_on_off + mix: 3 + state: false + + # ---------------------------------------------------------- + # SOUNDCHECK: apri i canali uno alla volta con fade-in + # ---------------------------------------------------------- + - id: "CUE_02" + name: "Soundcheck - Fade-in canali batteria" + description: "Canali 1-4 (kick, snare, OH sx, OH dx) portati a -10 dB gradualmente" + on_error: "continue" + steps: + # Kick + - action: set_channel_level + channel: 1 + level_db: -40.0 + - action: set_channel_on_off + channel: 1 + state: true + wait_ms: 200 + - action: set_channel_level + channel: 1 + level_db: -20.0 + wait_ms: 200 + - action: set_channel_level + channel: 1 + level_db: -10.0 + wait_ms: 300 + + # Snare + - action: set_channel_level + channel: 2 + level_db: -40.0 + delay_ms: 500 + - action: set_channel_on_off + channel: 2 + state: true + wait_ms: 200 + - action: set_channel_level + channel: 2 + level_db: -10.0 + wait_ms: 300 + + # Overhead Sx + Dx insieme + - action: set_channel_level + channel: 3 + level_db: -40.0 + delay_ms: 500 + - action: set_channel_level + channel: 4 + level_db: -40.0 + - action: set_channel_on_off + channel: 3 + state: true + - action: set_channel_on_off + channel: 4 + state: true + wait_ms: 200 + - action: set_channel_pan + channel: 3 + pan: -30 # OH sx → sinistra + - action: set_channel_pan + channel: 4 + pan: 30 # OH dx → destra + wait_ms: 200 + - action: set_channel_level + channel: 3 + level_db: -14.0 + - action: set_channel_level + channel: 4 + level_db: -14.0 + + # ---------------------------------------------------------- + # IEM MIX 1 (Vocalist): setup invii + # ---------------------------------------------------------- + - id: "CUE_03" + name: "IEM Mix 1 - Setup invii vocalist" + description: "Configura il mix 1 per il vocalist: voce principale alta, resto basso" + on_error: "continue" + steps: + - action: set_mix_level + mix: 1 + level_db: 0.0 + - action: set_mix_on_off + mix: 1 + state: true + wait_ms: 200 + + # Voce principale (ch 9) → Mix 1 alta + - action: set_channel_to_mix_level + channel: 9 + mix: 1 + level_db: 0.0 + - action: set_channel_to_mix_on_off + channel: 9 + mix: 1 + state: true + + # Chitarra ritmica (ch 5) → Mix 1 media + - action: set_channel_to_mix_level + channel: 5 + mix: 1 + level_db: -6.0 + delay_ms: 100 + - action: set_channel_to_mix_on_off + channel: 5 + mix: 1 + state: true + + # Kick (ch 1) → Mix 1 bassa (solo click per tempo) + - action: set_channel_to_mix_level + channel: 1 + mix: 1 + level_db: -12.0 + delay_ms: 100 + - action: set_channel_to_mix_on_off + channel: 1 + mix: 1 + state: true + + # ---------------------------------------------------------- + # SHOW START: tutto ON, master a 0 dB + # ---------------------------------------------------------- + - id: "CUE_04" + name: "SHOW START - Apri tutto" + description: "Porta tutti i canali attivi e il master a nominal" + on_error: "stop" + steps: + - action: unmute_channels + channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + - action: set_mix_on_off + mix: 1 + state: true + - action: set_mix_on_off + mix: 2 + state: true + - action: set_mix_on_off + mix: 3 + state: true + + - action: recall_scene + bank: "a" + scene: 10 # scena "SHOW" pre-salvata + wait_ms: 2000 + + - action: refresh_cache + + # ---------------------------------------------------------- + # PAUSA: muta rapidamente tutto il palco + # ---------------------------------------------------------- + - id: "CUE_05" + name: "PAUSA - Muta palco" + description: "Muta tutti i canali di palco, lascia accesa la diffusione BG music" + on_error: "continue" + steps: + - action: mute_channels + channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + - action: set_channel_on_off # ch 16 = BG music playback + channel: 16 + state: true + delay_ms: 200 + - action: set_channel_level + channel: 16 + level_db: -6.0 + + # ---------------------------------------------------------- + # FINE SHOW: fade-out master in 5 step da 1 secondo + # ---------------------------------------------------------- + - id: "CUE_06" + name: "FINE SHOW - Fade out generale" + description: "Abbassa tutti i canali gradualmente in 5 secondi" + on_error: "continue" + steps: + - action: set_channel_level + channel: 9 + level_db: -6.0 + wait_ms: 1000 + - action: set_channel_level + channel: 9 + level_db: -12.0 + wait_ms: 1000 + - action: set_channel_level + channel: 9 + level_db: -20.0 + wait_ms: 1000 + - action: set_channel_level + channel: 9 + level_db: -40.0 + wait_ms: 1000 + - action: mute_channels + channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16] + + - action: recall_scene + bank: "a" + scene: 0 # scena vuota / safe + delay_ms: 500