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
+33 -48
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; const barWidth = (canvas.width / satelliteRmsHistory.length);
for(let i=0;i<numBars;i++) {
let barHeight = (avg * (Math.sin(i * 0.5) + 1)/2 + 5) * (0.5 + (Math.random()*0.3)); for(let i=0; i<satelliteRmsHistory.length; i++) {
if(stateText.innerText.includes('SPEAK')) barHeight = avg * 1.6 * (Math.sin(i*0.3)+1.2); const rms = satelliteRmsHistory[i];
barHeight = Math.min(barHeight, canvas.height-10); const barHeight = Math.min(rms * canvas.height * 1.5, canvas.height - 5);
const x = i * (barWidth+2); const x = i * barWidth;
const grad = ctx.createLinearGradient(x, canvas.height-barHeight, x, canvas.height);
grad.addColorStop(0, '#0ff'); // Градиент от зелёного к красному
grad.addColorStop(1, '#3f87ff'); const hue = 120 - (rms * 120); // 120 = зелёный, 0 = красный
ctx.fillStyle = grad; ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.fillRect(x, canvas.height-barHeight, barWidth, barHeight); ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
} }
} }
drawVisualization();
drawSatelliteVisualization();
// Подключение микрофона для визуализации (опционально) // Подключение микрофона для визуализации (опционально) — УДАЛЕНО
async function initMicVisualizer() { // Теперь используем RMS от satellite вместо локального микрофона
try {
if(audioCtx && audioCtx.state !== 'closed') await audioCtx.close();
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 128;
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaStream = stream;
source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
if(audioCtx.state === 'suspended') await audioCtx.resume();
} catch(e) { console.log('mic visual not available', e); }
}
// 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
@@ -1235,6 +1235,10 @@ class SatelliteBase:
data={"level": self._last_mic_level} data={"level": self._last_mic_level}
) )
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