web console

This commit is contained in:
Nick
2026-04-13 20:57:13 +02:00
parent deee299af5
commit 84c1f0da35
9 changed files with 1023 additions and 357 deletions
+389
View File
@@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TF5 Web Mixer</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #121212;
color: #e0e0e0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
overflow: hidden; /* Evita lo scroll dell'intera pagina */
}
input[type=range][orient=vertical] {
appearance: slider-vertical;
width: 40px;
height: 250px;
cursor: grab;
outline: none;
}
input[type=range][orient=vertical]:active {
cursor: grabbing;
}
/* Colori Fader */
.fader-main { accent-color: #3b82f6; }
.fader-send { accent-color: #f59e0b; }
.fader-stereo { accent-color: #ef4444; }
.fader-strip {
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
border: 1px solid #374151;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
}
.btn-on {
background-color: #ef4444;
color: white;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
border-color: #991b1b;
}
.btn-off {
background-color: #374151;
color: #9ca3af;
border-color: #1f2937;
}
.nav-tab.active {
border-bottom: 2px solid #3b82f6;
color: #60a5fa;
}
.mix-btn.active {
background-color: #f59e0b;
color: #fff;
font-weight: bold;
border-color: #d97706;
}
/* Stili per i Meter Audio */
.meter-track {
width: 12px;
height: 250px;
background-color: #111827;
border: 1px solid #4b5563;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.meter-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(to top, #4ade80, #facc15 85%, #ef4444 95%);
transition: height 0.05s linear; /* Transizione fluida */
}
</style>
</head>
<body>
<div id="app" class="h-screen flex flex-col">
<!-- Header -->
<header class="bg-gray-900 p-4 shadow-lg border-b border-gray-800 flex justify-between items-center flex-none">
<h1 class="text-2xl font-bold tracking-wider text-blue-400">YAMAHA TF5 <span class="text-gray-500 text-sm">Web Controller</span></h1>
<div class="flex items-center gap-2 text-sm">
<span class="relative flex h-3 w-3">
<span :class="wsConnected ? 'animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75' : ''"></span>
<span class="relative inline-flex rounded-full h-3 w-3" :class="wsConnected ? 'bg-green-500' : 'bg-red-500'"></span>
</span>
{{ wsConnected ? 'Connesso' : 'Disconnesso' }}
</div>
</header>
<!-- Navigation Main -->
<nav class="flex overflow-x-auto bg-gray-800 p-2 gap-4 flex-none">
<button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id"
class="nav-tab px-4 py-2 font-semibold uppercase text-sm whitespace-nowrap" :class="{'active': activeTab === tab.id}">
{{ tab.label }}
</button>
</nav>
<!-- Sub-Navigation per SENDS ON FADERS -->
<div v-if="activeTab === 'mix_sends'" class="bg-gray-900 p-2 border-b border-gray-700 flex overflow-x-auto gap-2 items-center flex-none">
<span class="text-gray-400 text-sm font-bold ml-2 mr-2 whitespace-nowrap">SELEZIONA MIX:</span>
<button v-for="i in 20" :key="'mix'+i" @click="loadMixSends(i)"
class="mix-btn px-3 py-1 rounded border border-gray-600 text-sm text-gray-300 transition-colors"
:class="{'active': activeMixId === i}">
M{{ i }}
</button>
<div v-if="isLoadingSends" class="ml-4 text-yellow-500 text-sm animate-pulse">Caricamento fader...</div>
</div>
<!-- Mixer Surface -->
<main class="flex-1 flex overflow-hidden bg-gray-950">
<!-- ZONA CANALI (Scorrevole) -->
<div class="flex-1 overflow-x-auto p-6 flex gap-4 items-start">
<div v-if="currentFaders.length === 0 && !isLoadingSends" class="text-gray-500 m-auto text-center w-full mt-20">
Nessun fader disponibile in questa vista.
</div>
<!-- Fader strips -->
<div v-for="fader in currentFaders" :key="fader.id"
class="fader-strip rounded-lg p-3 flex flex-col items-center min-w-[80px]"
:class="{'border-yellow-700': activeTab === 'mix_sends'}">
<div class="h-10 mb-2 w-full text-center overflow-hidden text-ellipsis px-1 border border-gray-700 bg-gray-800 rounded flex items-center justify-center font-bold text-xs">
{{ fader.name }}
</div>
<button @click="toggleOn(fader.id, fader.on)"
class="w-full h-10 rounded border-b-4 mb-6 font-bold text-sm transition-all"
:class="fader.on ? 'btn-on' : 'btn-off'">ON</button>
<!-- Contenitore Fader + Meter -->
<div class="flex items-end gap-2 mb-4">
<!-- Meter -->
<div class="meter-track">
<div class="meter-bar" :style="{ height: getMeterHeight(fader.id) + '%' }"></div>
</div>
<!-- Fader -->
<div class="relative flex justify-center bg-gray-950 p-2 rounded-lg border border-gray-800">
<div class="absolute left-1 top-0 bottom-0 w-1 flex flex-col justify-between py-2 text-[8px] text-gray-500 font-bold">
<span>+10</span><span>0</span><span>-20</span><span>-60</span><span>-∞</span>
</div>
<input type="range" orient="vertical" min="0" max="100" step="0.1"
:class="activeTab === 'mix_sends' ? 'fader-send' : 'fader-main'" :value="fader.slider_pos"
@pointerdown="startDrag(fader.id)" @pointerup="stopDrag(fader.id)"
@pointerleave="stopDrag(fader.id)" @input="handleSliderMove(fader.id, $event.target.value)"
@change="sendFinalLevel(fader.id)">
</div>
</div>
<div class="bg-black font-mono text-[10px] py-1 w-full text-center rounded border border-gray-700"
:class="activeTab === 'mix_sends' ? 'text-yellow-400' : 'text-green-400'">
{{ formatDb(fader.level_db) }}
</div>
<div class="text-gray-500 text-[10px] mt-2 font-bold uppercase">{{ getFaderLabel(fader.id) }}</div>
</div>
<div class="min-w-[20px] h-full">&nbsp;</div>
</div>
<!-- ZONA MASTER (Fissa a destra) -->
<div class="flex-none w-[130px] p-6 bg-gray-900 border-l border-gray-800 shadow-[-10px_0_20px_rgba(0,0,0,0.5)] z-10 flex flex-col items-center">
<!-- MASTER DEL MIX SELEZIONATO (Sends on Faders) -->
<div v-if="activeTab === 'mix_sends' && currentMixMaster"
class="fader-strip rounded-lg p-3 flex flex-col items-center w-full border-yellow-600 border bg-gray-800">
<div class="h-10 mb-2 w-full text-center bg-yellow-700 text-black rounded flex flex-col items-center justify-center font-bold text-[10px] leading-tight">
<span>MIX {{ activeMixId }}</span>
<span class="uppercase opacity-80 truncate w-full px-1">{{ currentMixMaster.name }}</span>
</div>
<button @click="toggleOn(activeMixId, currentMixMaster.on, 'mix')"
class="w-full h-10 rounded border-b-4 mb-6 font-bold text-sm transition-all"
:class="currentMixMaster.on ? 'btn-on' : 'btn-off'">ON</button>
<!-- Contenitore Fader + Meter Master Mix -->
<div class="flex items-end gap-2 mb-4">
<div class="meter-track"><div class="meter-bar" :style="{ height: getMeterHeight(activeMixId, 'mix') + '%' }"></div></div>
<div class="relative flex justify-center bg-gray-950 p-2 rounded-lg border border-gray-800">
<input type="range" orient="vertical" min="0" max="100" step="0.1" class="fader-send"
:value="currentMixMaster.slider_pos" @pointerdown="startDrag('mix-master')"
@pointerup="stopDrag('mix-master')" @pointerleave="stopDrag('mix-master')"
@input="handleSliderMove(activeMixId, $event.target.value, 'mix')"
@change="sendFinalLevel(activeMixId, 'mix')">
</div>
</div>
<div class="bg-black text-yellow-400 font-mono text-[10px] py-1 w-full text-center rounded border border-gray-700">{{ formatDb(currentMixMaster.level_db) }}</div>
<div class="text-yellow-600 text-[10px] mt-2 font-bold uppercase">MASTER</div>
</div>
<!-- MASTER STEREO (Default) -->
<div v-if="activeTab !== 'mix_sends' && state.stereo !== undefined"
class="fader-strip rounded-lg p-3 flex flex-col items-center w-full border-red-900 border bg-gray-800">
<div class="h-10 mb-2 w-full text-center bg-red-900 text-white rounded flex items-center justify-center font-bold text-[10px] uppercase px-1">
Stereo L/R</div>
<button @click="toggleOn('stereo', state.stereo_on, 'stereo')"
class="w-full h-10 rounded border-b-4 mb-6 font-bold text-sm transition-all"
:class="state.stereo_on ? 'btn-on' : 'btn-off'">ON</button>
<!-- Contenitore Fader + Meter Master Stereo -->
<div class="flex items-end gap-2 mb-4">
<div class="meter-track"><div class="meter-bar" :style="{ height: getMeterHeight(1, 'stereo') + '%' }"></div></div>
<div class="relative flex justify-center bg-gray-950 p-2 rounded-lg border border-gray-800">
<input type="range" orient="vertical" min="0" max="100" step="0.1" class="fader-stereo"
:value="stereoFader.slider_pos" @pointerdown="startDrag('stereo')"
@pointerup="stopDrag('stereo')" @pointerleave="stopDrag('stereo')"
@input="handleSliderMove('stereo', $event.target.value, 'stereo')"
@change="sendFinalLevel('stereo', 'stereo')">
</div>
</div>
<div class="bg-black text-red-400 font-mono text-[10px] py-1 w-full text-center rounded border border-gray-700">{{ formatDb(state.stereo) }}</div>
<div class="text-red-500 text-[10px] mt-2 font-bold uppercase">MAIN</div>
</div>
</div>
</main>
</div>
<script>
const { createApp } = Vue;
const mapRange = (val, in_min, in_max, out_min, out_max) => (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
function sliderToDb(pos) {
pos = parseFloat(pos);
if (pos <= 0) return -120;
if (pos <= 20) return mapRange(pos, 0, 20, -120, -60);
if (pos <= 50) return mapRange(pos, 20, 50, -60, -20);
if (pos <= 80) return mapRange(pos, 50, 80, -20, 0);
return mapRange(pos, 80, 100, 0, 10);
}
function dbToSlider(db) {
db = parseFloat(db);
if (db <= -120) return 0;
if (db <= -60) return mapRange(db, -120, -60, 0, 20);
if (db <= -20) return mapRange(db, -60, -20, 20, 50);
if (db <= 0) return mapRange(db, -20, 0, 50, 80);
if (db <= 10) return mapRange(db, 0, 10, 80, 100);
return 100;
}
createApp({
data() {
return {
ws: null,
wsConnected: false,
activeTab: 'channel',
tabs: [
{ id: 'channel', label: 'Inputs 1-40', prefix: 'CH' },
{ id: 'mix_sends', label: 'Sends on Faders (Spie)', prefix: 'CH' },
{ id: 'steinch', label: 'ST IN 1-2', prefix: 'ST' },
{ id: 'fxrtn', label: 'FX Returns', prefix: 'FX' },
{ id: 'mix', label: 'Mixes Masters', prefix: 'MIX' },
{ id: 'dca', label: 'DCA 1-8', prefix: 'DCA' }
],
state: { channels: [], steinch: [], mixes: [], dcas: [], fxrtn: [], stereo: -120, stereo_on: false },
meterState: {},
activeMixId: 1,
mixSendsData: [],
isLoadingSends: false,
draggingFaders: {}, localLevels: {}, lastSendTime: {}, releaseTimers: {}
}
},
computed: {
currentFaders() {
let dataArray = [];
if (this.activeTab === 'channel') dataArray = this.state.channels;
else if (this.activeTab === 'steinch') dataArray = this.state.steinch;
else if (this.activeTab === 'mix') dataArray = this.state.mixes;
else if (this.activeTab === 'dca') dataArray = this.state.dcas;
else if (this.activeTab === 'fxrtn') dataArray = this.state.fxrtn;
else if (this.activeTab === 'mix_sends') dataArray = this.mixSendsData;
return dataArray.map(item => {
const id = item.channel || item.mix || item.dca || item.id;
const isDragging = this.draggingFaders[id];
const displayDb = isDragging ? this.localLevels[id] : (item.level_db === -Infinity || item.level_db <= -120 ? -120 : item.level_db);
return { id: id, name: item.name, on: item.on, level_db: displayDb, slider_pos: dbToSlider(displayDb) };
});
},
currentMixMaster() {
if (!this.state.mixes) return null;
const mixObj = this.state.mixes.find(m => m.mix == this.activeMixId);
if (!mixObj) return null;
const id = 'mix-master';
const isDragging = this.draggingFaders[id];
const displayDb = isDragging ? this.localLevels[id] : (mixObj.level_db <= -120 ? -120 : mixObj.level_db);
return { name: mixObj.name || ('Mix ' + this.activeMixId), on: mixObj.on, level_db: displayDb, slider_pos: dbToSlider(displayDb) };
},
stereoFader() {
const isDragging = this.draggingFaders['stereo'];
const displayDb = isDragging ? this.localLevels['stereo'] : (this.state.stereo <= -120 ? -120 : this.state.stereo);
return { level_db: displayDb, slider_pos: dbToSlider(displayDb) };
},
},
mounted() { this.connectWebSocket(); },
methods: {
connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
this.ws.onopen = () => { this.wsConnected = true; };
this.ws.onclose = () => { this.wsConnected = false; setTimeout(this.connectWebSocket, 3000); };
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "state_update") { this.state = msg.data; }
else if (msg.type === "meter_update") { this.meterState = msg.data; }
else if (msg.type === "mix_sends_data" && msg.mix === this.activeMixId) { this.mixSendsData = msg.data; this.isLoadingSends = false; }
};
},
getMeterHeight(id, overrideType = null) {
const type = overrideType || (this.activeTab === 'mix_sends' ? 'channel' : this.activeTab);
if (!this.meterState[type] || !this.meterState[type].readings) return 0;
const meter = this.meterState[type].readings.find(m => m.channel == id);
if (!meter || meter.raw === null) return 0;
return (meter.raw / 127) * 100;
},
getFaderLabel(id) {
const tabInfo = this.tabs.find(t => t.id === this.activeTab || (this.activeTab === 'mix_sends' && t.id === 'channel'));
return `${tabInfo.prefix || 'CH'} ${id}`;
},
loadMixSends(mixNumber) {
this.activeMixId = mixNumber;
this.mixSendsData = [];
this.isLoadingSends = true;
if (this.wsConnected) {
this.ws.send(JSON.stringify({ action: 'get_mix_sends', mix: mixNumber }));
}
},
formatDb(val) { return val <= -120 ? "-inf dB" : (val > 0 ? "+" : "") + parseFloat(val).toFixed(1) + " dB"; },
startDrag(id) {
if (this.releaseTimers[id]) clearTimeout(this.releaseTimers[id]);
this.draggingFaders[id] = true;
},
stopDrag(id) {
if (this.draggingFaders[id]) {
this.sendFinalLevel(id);
this.releaseTimers[id] = setTimeout(() => { this.draggingFaders[id] = false; }, 600);
}
},
handleSliderMove(id, sliderValue, overrideType = null) {
const realDb = sliderToDb(sliderValue);
this.localLevels[id] = realDb;
const now = Date.now();
if (!this.lastSendTime[id] || now - this.lastSendTime[id] > 50) {
this.sendCommand(id, realDb, overrideType);
this.lastSendTime[id] = now;
}
},
sendFinalLevel(id, overrideType = null) {
if (this.localLevels[id] !== undefined) this.sendCommand(id, this.localLevels[id], overrideType);
},
sendCommand(id, dbValue, overrideType = null) {
if (!this.wsConnected) return;
if (this.activeTab === 'mix_sends' && !overrideType) {
const fader = this.mixSendsData.find(f => f.id === id);
if (fader) fader.level_db = dbValue;
this.ws.send(JSON.stringify({ action: 'set_send_level', id: id, mix: this.activeMixId, value: dbValue }));
} else {
const type = overrideType || this.activeTab;
this.ws.send(JSON.stringify({ action: 'set_level', type: type, id: id, value: dbValue }));
}
},
toggleOn(id, currentState, overrideType = null) {
if (!this.wsConnected) return;
const type = overrideType || this.activeTab;
if (type === 'mix_sends') {
const fader = this.mixSendsData.find(f => f.id === id);
if (fader) fader.on = !currentState;
this.ws.send(JSON.stringify({ action: 'set_send_on', id: id, mix: this.activeMixId, value: !currentState }));
} else {
this.ws.send(JSON.stringify({ action: 'set_on', type: type, id: id, value: !currentState }));
}
}
}
}).mount('#app');
</script>
</body>
</html>