feat: Web Client — canvas визуализация от satellite RMS

Изменения:
- Удалён локальный микрофон (initMicVisualizer)
- Canvas рисует RMS историю от satellite
- 64 столбика с градиентом (зелёный → красный)
- Меньше логов (только если RMS > 0.05)

Преимущества:
- ✅ Показывает реальную громкость satellite
- ✅ Нет зависимости от микрофона браузера
- ✅ Меньше спама в логах
This commit is contained in:
2026-05-21 19:26:53 +03:00
parent 6913625830
commit 4a79874807
2 changed files with 37 additions and 48 deletions
+34 -49
View File
@@ -281,7 +281,7 @@
<!-- заголовок --> <!-- заголовок -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<div style="font-family: 'Orbitron'; font-weight: 700; font-size: 1.6rem; letter-spacing: 2px; background: linear-gradient(135deg, #aaffff, #3b8eff); -webkit-background-clip: text; background-clip: text; color: transparent;"> <div style="font-family: 'Orbitron'; font-weight: 700; font-size: 1.6rem; letter-spacing: 2px; background: linear-gradient(135deg, #aaffff, #3b8eff); -webkit-background-clip: text; background-clip: text; color: transparent;">
⚡ LVA • NEXUS ⚡ Wyoming Droid Assistant ⚡
</div> </div>
<div class="connection-status"> <div class="connection-status">
<div class="led" id="statusLed"></div> <div class="led" id="statusLed"></div>
@@ -482,7 +482,9 @@
// Audio level events // Audio level events
if(eventType === 'audio-level') { if(eventType === 'audio-level') {
if(json.data && json.data.level !== undefined) { if(json.data && json.data.level !== undefined) {
updateRmsMeter(json.data.level); const level = json.data.level;
currentRms = level; // ✅ Обновляем currentRms для визуализации
updateRmsMeter(level);
} }
} }
@@ -581,11 +583,13 @@
} }
} }
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма // Визуализация на canvas — рисуем RMS от satellite
let fakeAudioArray = new Uint8Array(64); let satelliteRmsHistory = new Array(64).fill(0); // История RMS значений
function drawVisualization() {
function drawSatelliteVisualization() {
if(!ctx) return; if(!ctx) return;
requestAnimationFrame(drawVisualization); requestAnimationFrame(drawSatelliteVisualization);
if(!isConnected) { if(!isConnected) {
ctx.clearRect(0,0,canvas.width, canvas.height); ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#0a1222'; ctx.fillStyle = '#0a1222';
@@ -595,51 +599,34 @@
ctx.fillText('⚡ idle', canvas.width-50, 20); ctx.fillText('⚡ idle', canvas.width-50, 20);
return; return;
} }
// имитация уровня громкости + случайный шум, если нет микрофона, но выглядит живо
let avg = 20; // Добавляем текущее RMS в историю
if(analyser && analyser.fftSize) { satelliteRmsHistory.shift();
const dataArray = new Uint8Array(analyser.frequencyBinCount); satelliteRmsHistory.push(currentRms);
analyser.getByteFrequencyData(dataArray);
let sum = 0; // Рисуем
for(let i=0;i<dataArray.length;i++) sum+=dataArray[i];
avg = Math.min(100, sum/(dataArray.length||1));
} else {
avg = 12 + Math.random() * 25;
if(stateText.innerText.includes('LISTEN') || stateText.innerText.includes('SPEAK')) avg = 45 + Math.random() * 55;
}
ctx.clearRect(0,0,canvas.width, canvas.height); ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = '#03151f'; ctx.fillStyle = '#03151f';
ctx.fillRect(0,0,canvas.width, canvas.height); ctx.fillRect(0,0,canvas.width, canvas.height);
const barWidth = (canvas.width / 42);
const numBars = 42;
for(let i=0;i<numBars;i++) {
let barHeight = (avg * (Math.sin(i * 0.5) + 1)/2 + 5) * (0.5 + (Math.random()*0.3));
if(stateText.innerText.includes('SPEAK')) barHeight = avg * 1.6 * (Math.sin(i*0.3)+1.2);
barHeight = Math.min(barHeight, canvas.height-10);
const x = i * (barWidth+2);
const grad = ctx.createLinearGradient(x, canvas.height-barHeight, x, canvas.height);
grad.addColorStop(0, '#0ff');
grad.addColorStop(1, '#3f87ff');
ctx.fillStyle = grad;
ctx.fillRect(x, canvas.height-barHeight, barWidth, barHeight);
}
}
drawVisualization();
// Подключение микрофона для визуализации (опционально) const barWidth = (canvas.width / satelliteRmsHistory.length);
async function initMicVisualizer() {
try { for(let i=0; i<satelliteRmsHistory.length; i++) {
if(audioCtx && audioCtx.state !== 'closed') await audioCtx.close(); const rms = satelliteRmsHistory[i];
audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const barHeight = Math.min(rms * canvas.height * 1.5, canvas.height - 5);
analyser = audioCtx.createAnalyser(); const x = i * barWidth;
analyser.fftSize = 128;
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Градиент от зелёного к красному
mediaStream = stream; const hue = 120 - (rms * 120); // 120 = зелёный, 0 = красный
source = audioCtx.createMediaStreamSource(stream); ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
source.connect(analyser); ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
if(audioCtx.state === 'suspended') await audioCtx.resume();
} catch(e) { console.log('mic visual not available', e); }
} }
}
drawSatelliteVisualization();
// Подключение микрофона для визуализации (опционально) — УДАЛЕНО
// Теперь используем RMS от satellite вместо локального микрофона
// WebSocket подключение // WebSocket подключение
function connectWebSocket() { function connectWebSocket() {
@@ -655,7 +642,7 @@
stateText.innerText = 'ONLINE'; stateText.innerText = 'ONLINE';
orb.style.animation = 'pulse-glow 0.8s'; orb.style.animation = 'pulse-glow 0.8s';
setTimeout(()=> orb.style.animation='',700); setTimeout(()=> orb.style.animation='',700);
initMicVisualizer(); // initMicVisualizer() — удалено, используем satellite RMS
}; };
ws.onmessage = handleIncomingMessage; ws.onmessage = handleIncomingMessage;
ws.onerror = (err) => { addEventToLog(`⚠️ WebSocket ошибка`); }; ws.onerror = (err) => { addEventToLog(`⚠️ WebSocket ошибка`); };
@@ -666,8 +653,6 @@
addEventToLog(`🔌 Отключено`); addEventToLog(`🔌 Отключено`);
stateText.innerText = 'OFFLINE'; stateText.innerText = 'OFFLINE';
document.body.classList.remove('listening-active'); document.body.classList.remove('listening-active');
if(mediaStream) mediaStream.getTracks().forEach(t=>t.stop());
if(audioCtx) audioCtx.close();
}; };
} }
+4
View File
@@ -1236,6 +1236,10 @@ class SatelliteBase:
) )
await self._broadcast_to_websocket(event) await self._broadcast_to_websocket(event)
# Логировать только если уровень выше порога (чтобы не спамить)
if self._last_mic_level > 0.05:
_LOGGER.debug(f"Audio level: {self._last_mic_level:.2f}")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Info # Info
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------