feat(processed-audio-sources): phase 7 - testing and polish

Fix test_list_filters test (filter_id field name mismatch).
Add tests for audio filters, template store, and source store.
All 678 tests pass, ruff clean, tsc clean, esbuild clean.
No dead code remaining from old source types.
This commit is contained in:
2026-03-31 22:50:02 +03:00
parent 1ce0dc6c61
commit ce1f4847f3
11 changed files with 957 additions and 34 deletions
@@ -134,8 +134,8 @@ async def update_audio_processing_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
except Exception as exc:
logger.warning("Hot-update of audio filter pipelines failed: %s", exc)
return _apt_to_response(template)
except EntityNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
@@ -158,15 +158,14 @@ async def delete_audio_processing_template(
):
"""Delete an audio processing template."""
try:
# 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 Exception as exc:
logger.warning("Hot-update of audio filter pipelines after delete failed: %s", exc)
except HTTPException:
raise
except EntityNotFoundError as e:
@@ -324,10 +324,12 @@ class AudioColorStripStream(ColorStripStream):
buf = _buf_a if _use_a else _buf_b
_use_a = not _use_a
# Get latest audio analysis
# Get latest audio analysis and apply filter pipeline once per frame
analysis = None
if self._audio_stream is not None:
analysis = self._audio_stream.get_latest_analysis()
if analysis is not None and self._filter_pipeline is not None:
analysis = self._filter_pipeline.process(analysis)
render_fn = renderers.get(self._visualization_mode, self._render_spectrum)
t_render = time.perf_counter()
@@ -361,15 +363,8 @@ class AudioColorStripStream(ColorStripStream):
# ── Filter pipeline + channel selection ──────────────────────────
def _apply_filters(self, analysis):
"""Apply audio filter pipeline (if any) and return (spectrum, rms).
The filter pipeline handles channel extraction, band extraction,
gain, noise gate, etc. as configured by the ProcessedAudioSource
template chain.
"""
if self._filter_pipeline is not None:
analysis = self._filter_pipeline.process(analysis)
def _extract_spectrum_rms(self, analysis):
"""Return (spectrum, rms) from an already-filtered analysis."""
return analysis.spectrum, analysis.rms
# ── Spectrum Analyzer ──────────────────────────────────────────
@@ -379,7 +374,7 @@ class AudioColorStripStream(ColorStripStream):
buf[:] = 0
return
spectrum, _ = self._apply_filters(analysis)
spectrum, _ = self._extract_spectrum_rms(analysis)
sensitivity = self.resolve("sensitivity", self._sensitivity)
smoothing = self.resolve("smoothing", self._smoothing)
lut = self._palette_lut
@@ -430,7 +425,7 @@ class AudioColorStripStream(ColorStripStream):
buf[:] = 0
return
_, ch_rms = self._apply_filters(analysis)
_, ch_rms = self._extract_spectrum_rms(analysis)
sensitivity = self.resolve("sensitivity", self._sensitivity)
smoothing = self.resolve("smoothing", self._smoothing)
rms = ch_rms * sensitivity
@@ -605,10 +605,3 @@ function _renderAudioSpectrum() {
}
}
// ── Removed types ─────────────────────────────────────────────
// MonoAudioSource and BandExtractAudioSource have been removed.
// Channel selection is now handled by the channel_extract audio filter.
// Band filtering is now handled by the band_extract audio filter.
// These are applied via ProcessedAudioSource referencing an AudioProcessingTemplate.
// Exported stubs for backward compatibility (no-op):
export function onBandPresetChange() { /* removed */ }
@@ -1258,8 +1258,7 @@ export async function saveStream() {
if (!name) { showToast(t('streams.error.required'), 'error'); return; }
const payload: any = { name, description: description || null, tags: _streamTagsInput ? _streamTagsInput.getValue() : [] };
if (!streamId) payload.stream_type = streamType;
const payload: any = { name, stream_type: streamType, description: description || null, tags: _streamTagsInput ? _streamTagsInput.getValue() : [] };
if (streamType === 'raw') {
payload.display_index = parseInt((document.getElementById('stream-display-index') as HTMLInputElement).value) || 0;
@@ -58,6 +58,7 @@ _ENTITY_TABLES = [
"home_assistant_sources",
"mqtt_sources",
"game_integrations",
"audio_processing_templates",
]