Add audio-reactive color strip sources, improve delete error messages
Add new "audio" color strip source type with three visualization modes (spectrum analyzer, beat pulse, VU meter) supporting WASAPI loopback and microphone input via PyAudioWPatch. Includes shared audio capture with ref counting, real-time FFT spectrum analysis, and beat detection. Improve all referential integrity 409 error messages across delete endpoints to include specific names of referencing entities instead of generic "one or more" messages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
18
server/src/wled_controller/api/routes/audio.py
Normal file
18
server/src/wled_controller/api/routes/audio.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Audio device routes: enumerate available audio devices."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.core.audio.audio_capture import AudioCaptureManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/v1/audio-devices", tags=["Audio"])
|
||||
async def list_audio_devices(_auth: AuthRequired):
|
||||
"""List available audio input/output devices for audio-reactive sources."""
|
||||
try:
|
||||
devices = AudioCaptureManager.enumerate_devices()
|
||||
return {"devices": devices, "count": len(devices)}
|
||||
except Exception as e:
|
||||
return {"devices": [], "count": 0, "error": str(e)}
|
||||
@@ -80,6 +80,11 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
||||
frame_interpolation=getattr(source, "frame_interpolation", None),
|
||||
animation=getattr(source, "animation", None),
|
||||
layers=getattr(source, "layers", None),
|
||||
visualization_mode=getattr(source, "visualization_mode", None),
|
||||
audio_device_index=getattr(source, "audio_device_index", None),
|
||||
audio_loopback=getattr(source, "audio_loopback", None),
|
||||
sensitivity=getattr(source, "sensitivity", None),
|
||||
color_peak=getattr(source, "color_peak", None),
|
||||
overlay_active=overlay_active,
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
@@ -156,6 +161,11 @@ async def create_color_strip_source(
|
||||
scale=data.scale,
|
||||
mirror=data.mirror,
|
||||
layers=layers,
|
||||
visualization_mode=data.visualization_mode,
|
||||
audio_device_index=data.audio_device_index,
|
||||
audio_loopback=data.audio_loopback,
|
||||
sensitivity=data.sensitivity,
|
||||
color_peak=data.color_peak,
|
||||
)
|
||||
return _css_to_response(source)
|
||||
|
||||
@@ -224,6 +234,11 @@ async def update_color_strip_source(
|
||||
scale=data.scale,
|
||||
mirror=data.mirror,
|
||||
layers=layers,
|
||||
visualization_mode=data.visualization_mode,
|
||||
audio_device_index=data.audio_device_index,
|
||||
audio_loopback=data.audio_loopback,
|
||||
sensitivity=data.sensitivity,
|
||||
color_peak=data.color_peak,
|
||||
)
|
||||
|
||||
# Hot-reload running stream (no restart needed for in-place param changes)
|
||||
@@ -250,17 +265,21 @@ async def delete_color_strip_source(
|
||||
):
|
||||
"""Delete a color strip source. Returns 409 if referenced by any LED target."""
|
||||
try:
|
||||
if target_store.is_referenced_by_color_strip_source(source_id):
|
||||
target_names = target_store.get_targets_referencing_css(source_id)
|
||||
if target_names:
|
||||
names = ", ".join(target_names)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Color strip source is referenced by one or more LED targets. "
|
||||
"Delete or reassign the targets first.",
|
||||
detail=f"Color strip source is referenced by target(s): {names}. "
|
||||
"Delete or reassign the target(s) first.",
|
||||
)
|
||||
if store.is_referenced_by_composite(source_id):
|
||||
composite_names = store.get_composites_referencing(source_id)
|
||||
if composite_names:
|
||||
names = ", ".join(composite_names)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Color strip source is used as a layer in a composite source. "
|
||||
"Remove it from the composite first.",
|
||||
detail=f"Color strip source is used as a layer in composite source(s): {names}. "
|
||||
"Remove it from the composite(s) first.",
|
||||
)
|
||||
store.delete_source(source_id)
|
||||
except HTTPException:
|
||||
|
||||
@@ -131,10 +131,12 @@ async def delete_pattern_template(
|
||||
):
|
||||
"""Delete a pattern template."""
|
||||
try:
|
||||
if store.is_referenced_by(template_id, target_store):
|
||||
target_names = store.get_targets_referencing(template_id, target_store)
|
||||
if target_names:
|
||||
names = ", ".join(target_names)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Cannot delete pattern template: it is referenced by one or more key colors targets. "
|
||||
detail=f"Cannot delete pattern template: it is referenced by target(s): {names}. "
|
||||
"Please reassign those targets before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
|
||||
@@ -263,10 +263,12 @@ async def delete_picture_source(
|
||||
"""Delete a picture source."""
|
||||
try:
|
||||
# Check if any target references this stream
|
||||
if store.is_referenced_by_target(stream_id, target_store):
|
||||
target_names = store.get_targets_referencing(stream_id, target_store)
|
||||
if target_names:
|
||||
names = ", ".join(target_names)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Cannot delete picture source: it is assigned to one or more targets. "
|
||||
detail=f"Cannot delete picture source: it is assigned to target(s): {names}. "
|
||||
"Please reassign those targets before deleting.",
|
||||
)
|
||||
store.delete_stream(stream_id)
|
||||
|
||||
@@ -142,10 +142,12 @@ async def delete_pp_template(
|
||||
"""Delete a postprocessing template."""
|
||||
try:
|
||||
# Check if any picture source references this template
|
||||
if store.is_referenced_by(template_id, stream_store):
|
||||
source_names = store.get_sources_referencing(template_id, stream_store)
|
||||
if source_names:
|
||||
names = ", ".join(source_names)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Cannot delete postprocessing template: it is referenced by one or more picture sources. "
|
||||
detail=f"Cannot delete postprocessing template: it is referenced by picture source(s): {names}. "
|
||||
"Please reassign those streams before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
|
||||
Reference in New Issue
Block a user