feat: Advanced DSP configuration (#48)#49
Open
thomaseleff wants to merge 18 commits into
Open
Conversation
Workstream 1 of #48 (Advanced DSP Configuration): make EQ bands the source of truth and move volume ownership to the CamillaDSP daemon. - models/dsp: add Band; reshape DSPConfig to {id, preamp_db, bands, enabled}; delete the dynamic-loudness machinery - models/player: add dsp_id FK with None->'' coercion for the mixed old/new read_json_auto window - dal/dsp: re-key by config id; add resolve_for_player (get-or-mint-and-link) - clients/camilladsp: add DEFAULT_PERCENT_VOLUME = 25 - ui/streamer: de-wire loudness; seed volume from the daemon, drop the app-side replica and push-on-render - os/dietpi: seed a fresh statefile at --gain -12.04 (= percent_to_db(25)) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS-2 lands the pure computation layer behind its own import seam (`audera/domains/dsp/`), importing only `models` so it is unit-testable with zero I/O (no disk, daemon, or Docker). - compiler — `compile_pipeline(current_config, config)` compiles `preamp_db + bands` into a CamillaDSP config: deep-copies the base, strips managed `audera_`-prefixed filters/steps, re-adds one preamp Gain + one Biquad per band (own stereo `channels: [0, 1]` step, `bypassed = not band.enabled`). Foreign filters/devices survive; idempotent by construction. - headroom — `response_peak_db(config)` sums each enabled band's `eval_filter` magnitude element-wise over the shared grid + scalar preamp, matching the daemon's math to drive the headroom guard. - presets — `loudness_preset()` mints two editable shelf bands. Adds `camilladsp-plot @ v3.0.2` as a core dep (numpy-free at runtime); hatchling needs `allow-direct-references` for the git ref. Tests: 19 pure, Docker-free tests in `tests/domains/dsp/`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS-3: add three v3 websocket methods to CamillaDSPClient so Save can be
gated on validation and runtime clipping surfaced in the editor footer.
- validate_config(config) -> None: sends the config as a JSON-as-YAML
string via the v3 ValidateConfig command (v4-only ValidateConfigJson
avoided); raises RuntimeError on any non-Ok result, stricter than
_call's literal-Error check since v3 reports failures as a message.
- get_clipped_samples() -> int: unwraps the {result, value} count,
defaulting to 0 (mirrors get_volume's defensive default).
- reset_clipped_samples() -> None: zeroes the daemon's clip counter.
Docker-gated tests against the real v3.0.1 daemon: valid config
validates + round-trips with filters/steps intact; invalid config
raises (parameterized over missing-filter-name and invalid-biquad-type);
clipped-sample read/reset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS-4 · EQ editor page at /player/{id}/dsp — pre-amp, band table
(On/Type/Freq/Gain/Q), Loudness/Flat presets, live headroom read-out,
Reset. Per-page staged state lives in closures; scalar edits mutate in
place while structural changes refresh the band table.
WS-5 · Save orchestration (Pattern B, live apply): get_config →
compile_pipeline → validate_config (gate) → set_config → persist via
dsp_dal.update → reset_clipped_samples. Adds a "protect headroom" guard
and a 3s clip poll. Recovers the persisted dsp_id before resolving so
opening the page never mints an orphan config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thomaseleff
force-pushed
the
v0.1.0-beta.1-48
branch
from
July 8, 2026 13:10
e4d547d to
de66afa
Compare
The camilladsp systemd unit passed `--gain -12.04` to seed a cold-boot node at 25%. clap parses the leading `-` of the space-separated `-12.04` as short flags, so camilladsp exited (`unexpected argument '-1'`) before binding the websocket port; `Restart=always` just crash-looped it. The DSP editor's Save then hit `ws://<host>:1234` with nothing listening and failed with `[Errno 111] Connection refused`. The daemon already persists volume via `--statefile`, so the `--gain` seed was redundant. Remove it: camilladsp now starts, binds 1234, and self-creates the statefile (fader restored across restarts). Verified against the real CamillaDSP v3.0.1 container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS-6 of the advanced DSP v2 plan. Split the response curve from the peak in the headroom domain, add an auto-protecting pre-amp ceiling, and surface the combined EQ curve as a live ECharts line in the editor. - domains/dsp/headroom: factor `_summed_magnitude`; add `response_curve` (npoints=400) and `auto_preamp_db(bands, ...)`; keep `response_peak_db` numerically identical (delegates at npoints=1000) - ui/components/response_plot: new module — log x-axis 20 Hz-20 kHz, fixed -18..+18 dB box, single clipped accent line - ui/streamer/pages: draw the chart above the band table with a band-less info empty-state; clamp pre-amp to the clip-safe ceiling on load and in `_mark_changed`; drop the manual protect button, `_on_protect`, and the clip-warning branch; debounce band number inputs - tests: extend headroom + streamer suites for the curve, auto ceiling, and chart/empty-state visibility Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Import and export are inverses over bands, so they ship together. A new pure-domain rew.py adds parse_rew/format_rew (mapped to a RewImport model) that round-trip modulo fresh ids and float-format precision. The editor gains a Config ▾ menu beside Presets: Import REW… appends parsed bands and re-clamps the auto pre-amp; Export… surfaces the saved config as re-importable REW text with Copy/Download and an unsaved-changes banner. Pre-amp is deliberately not round-tripped — the auto-ceiling owns it on import. Note: ui.clipboard.write is synchronous in NiceGUI 3.11.1 (not async as the plan assumed), so the Copy handler is a plain sync function. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WS-8 (v2 plan) lets a user save the current EQ's bands as a named,
reusable preset and append it to any player. Apply = append + clone
(never replace); cloned bands get fresh uuids so audera_peq_<id> filter
names can't collide within a config.
- models/dsp — new Preset BaseModel (id, name, bands); serializes via
pydantic model_dump/model_validate (no hand-written to_dict).
- domains/dsp — pure clone_bands (deep copy + fresh uuids), reused by
both apply and save; exported from domains/dsp/__init__.py.
- dal/presets — own namespace ~/.audera/dsp/presets/{id}.json, plain
json + glob: get_all_presets (missing-dir → [], skip-malformed,
name-sorted), save_preset (wrapped {'preset': ...}), delete_preset.
Namespace-isolated from player configs; registered in dal/__init__.py.
- ui/streamer/pages — refreshable Presets ▾ menu with save-as dialog,
append-on-click, and per-row delete (click.stop). Actions row stays
at four buttons; DSPConfig schema and the dsp_id load path untouched.
Docs: AGENTS.md notes the dsp models are pydantic BaseModel; Preset
serializes via model_dump/model_validate. Tests: Preset round-trip,
clone_bands independence/unique-ids, DAL round-trip + name-sort +
skip-malformed + namespace isolation, and two streamer tests (seeded
preset appears + appends; save-current persists + notifies).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rs DAL (#50) Player is now a live-only DTO. A player's DSPConfig is keyed by the player's own Snapcast id (dsp/{player_id}.json) — the filename is the link — so the dsp_id FK, the write-once-never-read player shadow file, the page-load recovery hack, and the entire dal/players.py module (FK + three unused DuckDB queries) are removed. resolve_for_player reduces to a keyed get-or-create; the page resolves straight from the live player id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…50) Delete the unused groups/streams persistence (zero production callers) and re-home the Group/Stream to_dict/from_dict round-trips under tests/models/, preserving the only model coverage the DAL tests held. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thomaseleff
commented
Jul 9, 2026
thomaseleff
left a comment
Collaborator
Author
There was a problem hiding this comment.
Please address the review comments.
#49) Land all 12 inline review comments with a net-simpler codebase: - DAL: revert `dsp` to key on `DSPConfig.player_id`, drop `resolve_for_player` and the artifact docstring note (#1, #2). - Models: adopt CamillaDSP casing on `Band.type` (Peaking/Lowshelf/Highshelf/Lowpass/Highpass) and delete `compiler._TYPE_MAP` (#4). - Constants: centralize `DEFAULT_Q` (with Butterworth rationale) and `PASS_TYPES` in `models/dsp.py`; remove the three duplicate copies (#7, #9). - REW: rewrite `domains/dsp/rew.py` to speak CamillaDSP YAML — the only format REW v5.20.14+ round-trips — replacing the regex parser; declare `pyyaml` (#8). - Cleanup: strip `WS-*` plan refs, the over-hot load clamp, "legacy" framing, and the dietpi why-not comment (#6, #10, #12). - Docs: strengthen the presets plain-json rationale in the DAL + AGENTS.md (#3). - Split `streamer/pages.py` into a `pages/` package (`__init__`, `index`, `dsp`, `_plex`, `_clients`); document the pattern in `ui/AGENTS.md` (#11). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Condense the verbose module-level docstrings across the DSP domain, UI, and streamer packages to concise summaries; fix "Auder app" -> "Audera app" in `audera/ui/__init__.py`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centralize env-driven deployment config in one pydantic-settings singleton loaded once at startup, with backwards-compatible defaults (today's constants). `audera.__init__` re-exports the port values from the singleton so every `audera.*_PORT` call site is unchanged but now overridable via `AUDERA_*` env vars (and `.env`) — e.g. `AUDERA_SERVER_PORT=8080` for local Windows dev. - New `audera/settings.py`: `Settings(BaseSettings)` — hosts, service ports, and web-UI bind, all `AUDERA_`-prefixed. - Migrate scattered `os.getenv` host reads and hardcoded ports: streamer/setup `run()` bind to `server_host`/`server_port`; `_clients` seeds hosts from the singleton and lets an explicitly-set env host override a stale persisted `settings.json` (detected via `model_fields_set`). - Add `pydantic-settings>=2.0`; document the knobs in `.env.example`. - Docs: trim retired Group/Stream refs from AGENTS.md; README wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UI — DSP editor (streamer/pages/dsp.py):
- Breadcrumb header (Players › {name} › DSP) + back button, replacing the plain
title/link
- Presets ▾ / Config ▾ promoted to their own row as dropdown buttons; menu items
relabeled (Loudness, Clear, New preset, Import, Export)
- Widen the auto-protected pre-amp field so its label no longer truncates;
wrap=False keeps Reset/Save inline at narrow widths
- Band-table column header renders only once a band exists
- Empty-state hint restyled as a Material for MkDocs "info" admonition (blue,
info icon, bold + ADD BAND)
UI — player cards (streamer/pages/index.py):
- DSP button uses sym_o_airwave (sound waves) and leads the settings gear; drop
the bordered circle (rendered a sub-pixel oval); add a latency hint
UI — response chart (components/response_plot.py):
- Compact styling: tighter grid margins, endpoint-only axis labels
(20 Hz / 20k, ±18 dB), shorter height
Fix — cross-platform filenames (dal/path.py, dal/dsp.py, dal/presets.py):
- to_filename() sanitizes reserved characters so a MAC-address player id (colons)
maps to a valid Windows filename; the dsp/presets DALs route through it
Docs/tests:
- Document the screenshot-loop workflow in audera/ui/AGENTS.md
- Cover the MAC-address round-trip; update streamer assertions for the new
breadcrumb + info tip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thomaseleff
commented
Jul 11, 2026
thomaseleff
left a comment
Collaborator
Author
There was a problem hiding this comment.
Please address the review comments.
Turn the parametric-EQ band-row rendering into a runtime feature flag
(audera/ui/features.py DSP_BAND_EDITOR_KEY) with three options, defaulting
to `full`:
- full — the existing inline On/Type/Freq/Gain/Q row with column headers
- expanded — compact one-line rows in per-band cards; ✏ grows the card to
reveal labeled controls inline (one open at a time)
- dialog — compact rows in per-band cards; ✏ raises a modal with the
controls stacked vertically full-width for narrow phone screens
Refactor dsp.py around shared `_band_controls` (label/width switches) and
`_compact_row` builders plus a `_band_summary` helper; `_on_type` now takes
a refresh target so a type-change re-renders whichever tree owns the gain
field (table body vs. dialog body). Add per-mode page tests and value pins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-protected pre-amp used a min() clamp that only ratcheted down, so removing a boost band (or reducing gain / disabling a band / widening Q) left the pre-amp stuck low. Make the pre-amp a pure function of the bands — a read-only field recomputed on every change — and drop the speculative manual sub-ceiling attenuation. Legacy pre-amp values are normalized on load so the editor opens clean. Also snap float-noise ceilings to positive 0.0: eval_filter leaves a ~1e-14 dB residual on a flat/unity response (e.g. the default 0 dB Peaking band), which negated to a tiny -0.0 and rendered as "-0.0" in the field. Docs: fill in the ui/AGENTS.md "Running a local dev server" section, rename the preview harness to _preview.py, and unwrap the hard-wrapped prose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…collision Response chart (response_plot.py): - y-axis max fixed at +5 dB display headroom (auto pre-amp keeps the curve ≤ 0 dB); min floors at -18 dB and auto-extends downward in 6 dB steps when a deep cut dips below, tracking only the visible [20, 20 kHz] window. - x-axis: full vertical decade lines at 100/1k/10k, minor ticks at the primary sub-decade frequencies, and the axis line + 20/20k labels dropped to the grid bottom (onZero=False). DSP editor layout (dsp.py): - Sticky editor: the chart, info line, "+ Add band", and full-mode column headers are pinned; only the band rows scroll. Wrapped the scroll area in a relative grow box with an absolute-inset child so QScrollArea keeps a concrete height instead of collapsing at short viewports. Preset save (dsp.py): - Case-insensitive, trimmed name-collision check before saving. On a match a confirm dialog offers Replace (reuses the existing id, overwrites in place) or Cancel (leaves the save dialog open to rename). Tests: preset-collision Replace/Cancel flows + a response_plot axis unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DSP editor's hand-tuned `height: calc(100dvh - 6rem)` root-column constant with a reusable, page-scoped primitive (audera/ui/components/layout.py). `fill_viewport()` turns `.q-page`/`.nicegui-content` into a filling flex chain so the editor's `grow min-h-0` column inherits Quasar's header-aware, dvh-equivalent managed height — no per-page pixel/rem constant, and it re-derives on resize. The primitive also sets `w-full min-w-0` on `.nicegui-content`: as a flex item it otherwise sizes to its content (e.g. the chart canvas), pushing the page past the viewport and forcing a mobile zoom-out. `ui.query()` is page-scoped, so no other route is affected. Also folds in the DSP add-band UX tweaks: prepend new bands (always in view, ready to edit) and move `+ Add band` to the right of the info line for one-handed use. Verified via mobile CDP capture (390x844): fixed region stays put, only the band rows scroll, no horizontal overflow; players list (/) untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #48 — reshapes DSP from an app-owned, hand-mutated CamillaDSP
pipelineinto a parametric-EQ editor where bands are the source of truth, compiled to CamillaDSP biquads on Save. Loudness becomes a static preset; volume ownership moves to the CamillaDSP daemon (persisted via--statefile).The branch lands the v1 reshape (#48) and then the v2 feature plan (#50) that promotes four features onto the shipped editor. Full design lives in
plans/advanced-dsp-configuration.md(v1) and-v2.md(v2).What ships
Editor — a full-page parametric-EQ editor at
/player/{id}/dsp, reached from an EQ icon on each player card. A pre-amp field, a band table (On / Type / Freq / Gain / Q), and an actions row:Presets ▾·Config ▾·Reset·Save. Per-page state is staged in closures (never onself, which is shared across clients); Save compiles → validates → pushes → persists.Domain layer (
audera/domains/dsp/, imports onlymodels, zero I/O) —compiler— compilespreamp_db + bandsinto a CamillaDSP config, idempotent by construction (strips managedaudera_-prefixed filters/steps, re-adds one per band). Foreign filters/steps survive untouched.headroom— the true combined magnitude peak/curve viacamilladsp_plot, reusing the compiler's shaper so eval and compile can't drift. Drives the live response chart and the auto-protected pre-amp ceiling.presets—loudness_preset(static shelf bands),clone_bands(deep copy + fresh ids).rew— REW ↔ CamillaDSP YAML import/export (the format REW v5.20.14+ natively round-trips).Client —
CamillaDSPClientgainsvalidate_config(Save-time gate), clipped-sample read/reset.Feature checklist
v1 (#48): models/DAL reshape · DSP domain package · CamillaDSP client · EQ editor page · Save + headroom safety
v2 (#50): live response chart + auto-protected pre-amp · REW paste import + export (
Config ▾) · named user presets · player-ownership refactor (config keyed by Snapcast id,dsp_idFK +dal/players.pyretired) · retire dead groups/streams DALs (surviving DALs are all Audera-owned, plain-json)Review fixes (this review round)
Addressed all 12 inline comments: reverted the
dspDAL to key onplayer_id; adopted CamillaDSP casing onBand.typeand deleted_TYPE_MAP; centralizedDEFAULT_Q/PASS_TYPESonto one source of truth; rewrote REW interop to structured CamillaDSP YAML (importable back into REW) in place of the regex parser; stripped plan artifacts (WS-*refs, unshipped backwards-compat code, why-not comments); and split the 850-linestreamer/pages.pyinto apages/sub-package.Local-dev environment settings
Adds a cached pydantic-settings singleton (
audera/settings.py) that loads all env-driven deployment config once at startup, with backwards-compatible defaults (today's constants).audera/__init__.pyre-exports the port values from it, so everyaudera.*_PORTcall site is unchanged but now overridable viaAUDERA_*env vars (and.env):AUDERA_SNAPSERVER_HOST/AUDERA_PLEXAMP_HOST) and ports (AUDERA_SERVER_PORT,AUDERA_SNAPSERVER_PORT,AUDERA_CAMILLADSP_PORT,AUDERA_PLEXAMP_PORT) now drive the streamer/setuprun()binds and client factories. Folds the scatteredos.getenvreads, theSERVER_PORT = 80constant, and the strayport=80setup literal into one source of truth._load_settingslets an explicitly-set env host override a stale persisted~/.audera/settings.json(detected viamodel_fields_set); unset leaves the file authoritative, so behavior is unchanged when no var is set.AUDERA_SNAPSERVER_HOST=192.168.1.36 AUDERA_PLEXAMP_HOST=192.168.1.36 AUDERA_SERVER_PORT=8080 audera streamer start(port 8080 sidesteps the port-80 elevation cost). Knobs documented in.env.example.UI polish + cross-platform filenames
A round of UI refinement on the shipped editor and player cards, plus the filename fix that unblocked driving the real UI on Windows:
Players › {name} › DSP) + back button replacing the plain title/link;Presets ▾/Config ▾promoted to their own row as dropdown buttons with tightened labels (Loudness,Clear,New preset,Import,Export); the auto-protected pre-amp field widened so its label no longer truncates (wrap=Falsekeeps Reset/Save inline); the band-table column header renders only once a band exists; and the empty-state hint restyled as a Material for MkDocs "info" admonition (blue, info icon, bold + ADD BAND).sym_o_airwave(sound waves) and leads the settings gear; the bordered circle is dropped (it rendered a sub-pixel oval at some widths); the settings dialog's latency field gains a hint.20 Hz/20k,±18 dB), shorter height.dal/path.to_filename()sanitizes reserved characters so a MAC-address player id (whose colons are illegal in a Windows filename) maps to a validdsp/{id}.json; thedsp/presetsDALs route through it. Covered by a MAC-address round-trip test.audera/ui/AGENTS.md.Testing
ruff/ruff format/tyclean; the daemon-free suite (tests/{models,dal,domains,ui}) is green, plus Docker-gatedtests/clientsagainst the real CamillaDSP v3 container. REW round-trip is property-tested (parse_rew(format_rew(...)) ≈ bands).🤖 Generated with Claude Code