feat: Web Client — canvas визуализация от satellite RMS
Изменения: - Удалён локальный микрофон (initMicVisualizer) - Canvas рисует RMS историю от satellite - 64 столбика с градиентом (зелёный → красный) - Меньше логов (только если RMS > 0.05) Преимущества: - ✅ Показывает реальную громкость satellite - ✅ Нет зависимости от микрофона браузера - ✅ Меньше спама в логах
This commit is contained in:
+34
-49
@@ -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();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user