feat: Web Client — canvas визуализация от satellite RMS
Изменения: - Удалён локальный микрофон (initMicVisualizer) - Canvas рисует RMS историю от satellite - 64 столбика с градиентом (зелёный → красный) - Меньше логов (только если RMS > 0.05) Преимущества: - ✅ Показывает реальную громкость satellite - ✅ Нет зависимости от микрофона браузера - ✅ Меньше спама в логах
This commit is contained in:
+33
-48
@@ -281,7 +281,7 @@
|
||||
<!-- заголовок -->
|
||||
<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;">
|
||||
⚡ LVA • NEXUS
|
||||
⚡ Wyoming Droid Assistant ⚡
|
||||
</div>
|
||||
<div class="connection-status">
|
||||
<div class="led" id="statusLed"></div>
|
||||
@@ -482,7 +482,9 @@
|
||||
// Audio level events
|
||||
if(eventType === 'audio-level') {
|
||||
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 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Визуализация микрофона (если доступен) — рисуем абстракцию ритма
|
||||
let fakeAudioArray = new Uint8Array(64);
|
||||
function drawVisualization() {
|
||||
// Визуализация на canvas — рисуем RMS от satellite
|
||||
let satelliteRmsHistory = new Array(64).fill(0); // История RMS значений
|
||||
|
||||
function drawSatelliteVisualization() {
|
||||
if(!ctx) return;
|
||||
requestAnimationFrame(drawVisualization);
|
||||
requestAnimationFrame(drawSatelliteVisualization);
|
||||
|
||||
if(!isConnected) {
|
||||
ctx.clearRect(0,0,canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#0a1222';
|
||||
@@ -595,51 +599,34 @@
|
||||
ctx.fillText('⚡ idle', canvas.width-50, 20);
|
||||
return;
|
||||
}
|
||||
// имитация уровня громкости + случайный шум, если нет микрофона, но выглядит живо
|
||||
let avg = 20;
|
||||
if(analyser && analyser.fftSize) {
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
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;
|
||||
}
|
||||
|
||||
// Добавляем текущее RMS в историю
|
||||
satelliteRmsHistory.shift();
|
||||
satelliteRmsHistory.push(currentRms);
|
||||
|
||||
// Рисуем
|
||||
ctx.clearRect(0,0,canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#03151f';
|
||||
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);
|
||||
|
||||
const barWidth = (canvas.width / satelliteRmsHistory.length);
|
||||
|
||||
for(let i=0; i<satelliteRmsHistory.length; i++) {
|
||||
const rms = satelliteRmsHistory[i];
|
||||
const barHeight = Math.min(rms * canvas.height * 1.5, canvas.height - 5);
|
||||
const x = i * barWidth;
|
||||
|
||||
// Градиент от зелёного к красному
|
||||
const hue = 120 - (rms * 120); // 120 = зелёный, 0 = красный
|
||||
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
|
||||
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
|
||||
}
|
||||
}
|
||||
drawVisualization();
|
||||
|
||||
// Подключение микрофона для визуализации (опционально)
|
||||
async function initMicVisualizer() {
|
||||
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); }
|
||||
}
|
||||
drawSatelliteVisualization();
|
||||
|
||||
// Подключение микрофона для визуализации (опционально) — УДАЛЕНО
|
||||
// Теперь используем RMS от satellite вместо локального микрофона
|
||||
|
||||
// WebSocket подключение
|
||||
function connectWebSocket() {
|
||||
@@ -655,7 +642,7 @@
|
||||
stateText.innerText = 'ONLINE';
|
||||
orb.style.animation = 'pulse-glow 0.8s';
|
||||
setTimeout(()=> orb.style.animation='',700);
|
||||
initMicVisualizer();
|
||||
// initMicVisualizer() — удалено, используем satellite RMS
|
||||
};
|
||||
ws.onmessage = handleIncomingMessage;
|
||||
ws.onerror = (err) => { addEventToLog(`⚠️ WebSocket ошибка`); };
|
||||
@@ -666,8 +653,6 @@
|
||||
addEventToLog(`🔌 Отключено`);
|
||||
stateText.innerText = 'OFFLINE';
|
||||
document.body.classList.remove('listening-active');
|
||||
if(mediaStream) mediaStream.getTracks().forEach(t=>t.stop());
|
||||
if(audioCtx) audioCtx.close();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1236,6 +1236,10 @@ class SatelliteBase:
|
||||
)
|
||||
await self._broadcast_to_websocket(event)
|
||||
|
||||
# Логировать только если уровень выше порога (чтобы не спамить)
|
||||
if self._last_mic_level > 0.05:
|
||||
_LOGGER.debug(f"Audio level: {self._last_mic_level:.2f}")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Info
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user