Add real-time audio spectrum test for audio sources and templates
- Add WebSocket endpoints for live audio spectrum streaming at ~20Hz - Audio source test: resolves device/channel, shares stream via ref-counting - Audio template test: includes device picker dropdown for selecting input - Canvas-based 64-band spectrum visualizer with falling peaks and beat flash - Channel-aware: mono sources show left/right/mixed spectrum correctly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
"""Audio capture template and engine routes."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
import asyncio
|
||||
import json
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_audio_template_store, get_audio_source_store
|
||||
from wled_controller.api.dependencies import get_audio_template_store, get_audio_source_store, get_processor_manager
|
||||
from wled_controller.api.schemas.audio_templates import (
|
||||
AudioEngineInfo,
|
||||
AudioEngineListResponse,
|
||||
@@ -12,6 +17,7 @@ from wled_controller.api.schemas.audio_templates import (
|
||||
AudioTemplateResponse,
|
||||
AudioTemplateUpdate,
|
||||
)
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.audio.factory import AudioEngineRegistry
|
||||
from wled_controller.storage.audio_template_store import AudioTemplateStore
|
||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
||||
@@ -157,3 +163,77 @@ async def list_audio_engines(_auth: AuthRequired):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list audio engines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ===== REAL-TIME AUDIO TEMPLATE TEST WEBSOCKET =====
|
||||
|
||||
|
||||
@router.websocket("/api/v1/audio-templates/{template_id}/test/ws")
|
||||
async def test_audio_template_ws(
|
||||
websocket: WebSocket,
|
||||
template_id: str,
|
||||
token: str = Query(""),
|
||||
device_index: int = Query(-1),
|
||||
is_loopback: int = Query(1),
|
||||
):
|
||||
"""WebSocket for real-time audio spectrum test of a template with a chosen device.
|
||||
|
||||
Auth via ?token=<api_key>. Device specified via ?device_index=N&is_loopback=0|1.
|
||||
Streams AudioAnalysis snapshots as JSON at ~20 Hz.
|
||||
"""
|
||||
# Authenticate
|
||||
authenticated = False
|
||||
cfg = get_config()
|
||||
if token and cfg.auth.api_keys:
|
||||
for _label, api_key in cfg.auth.api_keys.items():
|
||||
if secrets.compare_digest(token, api_key):
|
||||
authenticated = True
|
||||
break
|
||||
|
||||
if not authenticated:
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
return
|
||||
|
||||
# Resolve template
|
||||
store = get_audio_template_store()
|
||||
try:
|
||||
template = store.get_template(template_id)
|
||||
except ValueError:
|
||||
await websocket.close(code=4004, reason="Template not found")
|
||||
return
|
||||
|
||||
# Acquire shared audio stream
|
||||
manager = get_processor_manager()
|
||||
audio_mgr = manager.audio_capture_manager
|
||||
loopback = is_loopback != 0
|
||||
|
||||
try:
|
||||
stream = audio_mgr.acquire(device_index, loopback, template.engine_type, template.engine_config)
|
||||
except RuntimeError as e:
|
||||
await websocket.close(code=4003, reason=str(e))
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
logger.info(f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}")
|
||||
|
||||
last_ts = 0.0
|
||||
try:
|
||||
while True:
|
||||
analysis = stream.get_latest_analysis()
|
||||
if analysis is not None and analysis.timestamp != last_ts:
|
||||
last_ts = analysis.timestamp
|
||||
await websocket.send_json({
|
||||
"spectrum": analysis.spectrum.tolist(),
|
||||
"rms": round(analysis.rms, 4),
|
||||
"peak": round(analysis.peak, 4),
|
||||
"beat": analysis.beat,
|
||||
"beat_intensity": round(analysis.beat_intensity, 4),
|
||||
})
|
||||
await asyncio.sleep(0.05)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Audio template test WS error: {e}")
|
||||
finally:
|
||||
audio_mgr.release(device_index, loopback, template.engine_type)
|
||||
logger.info(f"Audio template test WS disconnected: template={template_id}")
|
||||
|
||||
Reference in New Issue
Block a user