feat(processed-audio-sources): phase 4 - runtime filter integration
Add AudioFilterPipeline for chained filter execution on AudioAnalysis. Wire filter pipelines into AudioColorStripStream, AudioValueStream, and WebSocket test endpoint. Add hot-update support via ProcessorManager.refresh_audio_filter_pipelines(). Thread AudioProcessingTemplateStore through dependency injection hierarchy.
This commit is contained in:
@@ -6,6 +6,7 @@ from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_processing_template_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.audio_processing import (
|
||||
AudioProcessingTemplateCreate,
|
||||
@@ -129,6 +130,12 @@ async def update_audio_processing_template(
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_processing_template", "updated", template_id)
|
||||
# Hot-update: rebuild filter pipelines for running streams using this template
|
||||
try:
|
||||
pm = get_processor_manager()
|
||||
pm.refresh_audio_filter_pipelines(template_id)
|
||||
except Exception:
|
||||
pass # Non-critical: streams will pick up changes on next restart
|
||||
return _apt_to_response(template)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -154,6 +161,12 @@ async def delete_audio_processing_template(
|
||||
# TODO: Phase 3 will add reference checks against ProcessedAudioSource
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("audio_processing_template", "deleted", template_id)
|
||||
# Hot-update: rebuild filter pipelines for running streams that used this template
|
||||
try:
|
||||
pm = get_processor_manager()
|
||||
pm.refresh_audio_filter_pipelines(template_id)
|
||||
except Exception:
|
||||
pass # Non-critical
|
||||
except HTTPException:
|
||||
raise
|
||||
except EntityNotFoundError as e:
|
||||
|
||||
@@ -9,6 +9,7 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_processing_template_store,
|
||||
get_audio_source_store,
|
||||
get_audio_template_store,
|
||||
get_color_strip_store,
|
||||
@@ -210,11 +211,12 @@ async def test_audio_source_ws(
|
||||
ManagedAudioStream (ref-counted — shares with running targets), and streams
|
||||
AudioAnalysis snapshots as JSON at ~20 Hz.
|
||||
|
||||
NOTE: Audio processing filters from the template chain are NOT applied in
|
||||
this WebSocket yet — that will be wired in Phase 4 when the stream runtime
|
||||
integrates filter instances.
|
||||
Audio processing filters from the template chain are applied to the
|
||||
analysis before sending, so the WebSocket output matches what running
|
||||
streams see.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from wled_controller.core.audio.filters.pipeline import build_pipeline_from_template_ids
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -223,6 +225,7 @@ async def test_audio_source_ws(
|
||||
# Resolve source → device info + processing template chain
|
||||
store = get_audio_source_store()
|
||||
template_store = get_audio_template_store()
|
||||
apt_store = get_audio_processing_template_store()
|
||||
manager = get_processor_manager()
|
||||
|
||||
try:
|
||||
@@ -247,6 +250,15 @@ async def test_audio_source_ws(
|
||||
logger.debug("Audio template not found, falling back to best available engine: %s", e)
|
||||
pass # Fall back to best available engine
|
||||
|
||||
# Build filter pipeline from processing template chain
|
||||
pipeline = None
|
||||
if resolved.audio_processing_template_ids and apt_store:
|
||||
pipeline = build_pipeline_from_template_ids(
|
||||
resolved.audio_processing_template_ids, apt_store
|
||||
)
|
||||
if pipeline.empty:
|
||||
pipeline = None
|
||||
|
||||
# Acquire shared audio stream
|
||||
audio_mgr = manager.audio_capture_manager
|
||||
try:
|
||||
@@ -265,7 +277,10 @@ async def test_audio_source_ws(
|
||||
if analysis is not None and analysis.timestamp != last_ts:
|
||||
last_ts = analysis.timestamp
|
||||
|
||||
# Send raw analysis — filter processing will be added in Phase 4
|
||||
# Apply filter pipeline (channel extract, band extract, gain, etc.)
|
||||
if pipeline is not None:
|
||||
analysis = pipeline.process(analysis)
|
||||
|
||||
await websocket.send_json(
|
||||
{
|
||||
"spectrum": analysis.spectrum.tolist(),
|
||||
@@ -283,5 +298,7 @@ async def test_audio_source_ws(
|
||||
except Exception as e:
|
||||
logger.error(f"Audio test WebSocket error for {source_id}: {e}")
|
||||
finally:
|
||||
if pipeline is not None:
|
||||
pipeline.close()
|
||||
audio_mgr.release(device_index, is_loopback, engine_type)
|
||||
logger.info(f"Audio test WebSocket disconnected for source {source_id}")
|
||||
|
||||
Reference in New Issue
Block a user