fix: code review batch 2 — multi-channel, thread safety, DSP reset, MIDI, hotspot, NAM, docs/CI, code quality
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
This commit is contained in:
+80
-9
@@ -18,6 +18,7 @@ import logging
|
||||
import math
|
||||
import mimetypes
|
||||
import datetime
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -340,6 +341,8 @@ class WebServer:
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._tonedownload: Optional[Tone3000Client] = None
|
||||
self._preset_write_locks: dict[str, asyncio.Lock] = {}
|
||||
self._debounce_timers: dict[str, threading.Timer] = {}
|
||||
self._pending_presets: dict[str, Any] = {}
|
||||
self._needs_reboot: bool = False
|
||||
self._app = self._build_app()
|
||||
|
||||
@@ -365,6 +368,51 @@ class WebServer:
|
||||
self._preset_write_locks[key] = asyncio.Lock()
|
||||
return self._preset_write_locks[key]
|
||||
|
||||
# ── Debounced preset save (write-behind) ─────────────────────────
|
||||
|
||||
def _debounced_save_preset(self, channel: str, bank: int, program: int,
|
||||
preset: Any) -> None:
|
||||
"""Save preset to disk with a 500 ms write-behind timer.
|
||||
|
||||
Each call cancels any pending save for the same ``(channel, bank,
|
||||
program)``, then schedules a new one. This prevents a storm of
|
||||
disk writes when the user drags a slider in the UI — only the
|
||||
final value is persisted, 500 ms after the last tweak.
|
||||
|
||||
Args:
|
||||
channel: Channel name (e.g. ``\"guitar\"``).
|
||||
bank: Bank number.
|
||||
program: Program number.
|
||||
preset: The :class:`~src.presets.types.Preset` to persist.
|
||||
"""
|
||||
key = f"{channel}:{bank}:{program}"
|
||||
|
||||
# Cancel any pending timer
|
||||
old = self._debounce_timers.pop(key, None)
|
||||
if old is not None:
|
||||
old.cancel()
|
||||
|
||||
# Hold a reference so GC doesn't collect it before the timer fires
|
||||
self._pending_presets[key] = preset
|
||||
|
||||
def _flush() -> None:
|
||||
try:
|
||||
pm, _pl, _nam, _ir = self._channel_deps(channel)
|
||||
if pm is not None:
|
||||
from ..presets.types import Channel
|
||||
pm.save(preset, channel=Channel(channel))
|
||||
logger.debug("Debounced save: ch=%s b=%d p=%d", channel, bank, program)
|
||||
except Exception as exc:
|
||||
logger.warning("Debounced preset save failed: %s", exc)
|
||||
finally:
|
||||
# Clean up the held reference on completion
|
||||
self._pending_presets.pop(key, None)
|
||||
|
||||
t = threading.Timer(0.5, _flush)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
self._debounce_timers[key] = t
|
||||
|
||||
# ── App factory ─────────────────────────────────────────────────────
|
||||
|
||||
# ── Auth helpers ─────────────────────────────────────────────
|
||||
@@ -979,10 +1027,15 @@ class WebServer:
|
||||
if b.block_id == block_id or (not data.get("block_id") and b.fx_type.value == block_id):
|
||||
b.enabled = bool(enabled)
|
||||
b.bypass = not bool(enabled) # sync bypass with enabled
|
||||
pm.save(preset, channel=Channel(channel))
|
||||
# Reload pipeline if needed
|
||||
if pl:
|
||||
pl.load_preset(preset)
|
||||
# In-place pipeline update — preserves DSP state
|
||||
if pl is not None:
|
||||
pl.set_block_in_place(
|
||||
b.block_id,
|
||||
enabled=b.enabled,
|
||||
bypass=b.bypass,
|
||||
)
|
||||
# Deferred disk write — 500 ms debounce
|
||||
self._debounced_save_preset(channel, bank, program, preset)
|
||||
await self._manager.broadcast({
|
||||
"type": "block_toggled",
|
||||
"channel": channel,
|
||||
@@ -1051,9 +1104,27 @@ class WebServer:
|
||||
else:
|
||||
# Safe: key already validated against schema
|
||||
b.params[key] = float(val)
|
||||
pm.save(preset, channel=Channel(channel))
|
||||
if pl:
|
||||
pl.load_preset(preset)
|
||||
# In-place pipeline update — preserves DSP state
|
||||
if pl is not None:
|
||||
changed_params = {
|
||||
k: float(v) for k, v in data.items()
|
||||
if k not in ("id", "block_id", "nam_model_path", "ir_file_path", "bypass", "enabled", "subtype")
|
||||
and k in valid_param_keys
|
||||
}
|
||||
changed_enabled = None
|
||||
changed_bypass = None
|
||||
if "enabled" in data:
|
||||
changed_enabled = bool(data["enabled"])
|
||||
if "bypass" in data:
|
||||
changed_bypass = bool(data["bypass"])
|
||||
pl.set_block_in_place(
|
||||
b.block_id,
|
||||
params=changed_params or None,
|
||||
enabled=changed_enabled,
|
||||
bypass=changed_bypass,
|
||||
)
|
||||
# Deferred disk write — 500 ms debounce
|
||||
self._debounced_save_preset(channel, bank, program, preset)
|
||||
await self._manager.broadcast({
|
||||
"type": "block_params_changed",
|
||||
"channel": channel,
|
||||
@@ -1411,12 +1482,12 @@ class WebServer:
|
||||
|
||||
Body: {ssid: str (optional), password: str (optional)}
|
||||
"""
|
||||
from ..system.network import hotspot_enable
|
||||
from ..system.network import hotspot_enable, get_hotspot_password
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None, partial(hotspot_enable,
|
||||
ssid=data.get("ssid", "Pi-Pedal"),
|
||||
password=data.get("password", "pedal1234"),
|
||||
password=data.get("password", get_hotspot_password()),
|
||||
),
|
||||
)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user