A self-hosted web console for the miniDSP 2x4 HD, built to replace the official desktop app ("miniDSP Console") for day-to-day tweaking from any browser — phone, laptop, whatever's nearby. It closely follows the layout of the official console (channel strips, routing matrix, PEQ with a live frequency-response chart, crossover, compressor, FIR, delay/phase, presets) and talks to the device through minidsp-rs (minidspd).
Built for running alongside moOde Audio on a Raspberry Pi with the 2x4 HD connected over USB, but nothing here is moOde-specific — any Linux box that can host the USB device and run Node.js works.
- Input/output channel strips: gain, mute, invert, delay, level meters
- Routing matrix (which input feeds which output, with per-connection gain)
- Parametric EQ: 10 slots per channel, PEAK/LOW_SHELF/HIGH_SHELF/ALL_PASS, a draggable frequency-response chart, and an import tool for Room EQ Wizard exports ("Export Filter Settings as text" or biquad-coefficient exports)
- Crossover: Butterworth (6–48 dB/oct), Linkwitz-Riley (12/24/48 dB/oct), Bessel (12 dB/oct), with per-output overlay charts
- Compressor and FIR (file or manual-entry coefficient) editors
- 4 on-device presets plus unlimited named JSON backups, with export/import
- Hardware-in-the-loop verification: plays a real logarithmic sine sweep through the DSP and measures the actual output level to confirm a filter is doing what it's configured to do — see Hardware-in-the-loop verification below. This is the only way to check the device's real behavior, because of the read-back limitation described next.
minidsp-webui/
backend/ Node.js + TypeScript + Express - the device's source of truth
frontend/ React + TypeScript + Vite - the console UI
deploy/ systemd units, install/sync/smoke-test scripts
The 2x4 HD (and therefore minidsp-rs) can only write PEQ/crossover/ compressor/FIR/routing settings — it cannot be read back. This isn't a limitation of minidsp-rs; it's the device's own protocol. Only the active preset number, source, volume/mute, and the live level meters can be read from the device.
Because of that, the backend is the sole source of truth for the full DSP
configuration. It keeps its own copy on disk (backend/data/presets/preset-<0-3>.json), and every change made through the UI is written to that
file and sent to minidspd as the same operation. If something else
writes to the device directly (the official app, the minidsp CLI, a
different tool) the backend has no way to notice — see
Known issues below.
- A Raspberry Pi (or similar Linux box) with the 2x4 HD connected via USB
- minidsp-rs (
minidspd+minidspCLI) —deploy/install-minidsp-rs.shinstalls this for you (see Quick start) - Node.js 20+ and npm —
deploy/install.shinstalls this viaaptif missing
On the target machine (as root, or with sudo):
git clone https://github.com/nitrousptm/minidsp-webui.git
cd minidsp-webui
sudo bash deploy/install.shThis will:
- Install
minidsp-rsif it isn't already present (deploy/install-minidsp-rs.sh) - Install Node.js/npm if missing
npm install,npm test, andnpm run buildfor bothbackend/andfrontend/- Install and enable the
minidsp-webuisystemd service - Install and enable
minidspd-watchdog(see Known issues — minidsp-rs occasionally needs a kick) - Run a read-only smoke test against the running service
The console is then served at http://<host>:5381 — the backend serves
both the API and the built frontend from a single port.
Check status / logs:
systemctl status minidsp-webui
journalctl -u minidsp-webui -fIf you're editing the code on a workstation and deploying to a Pi over
SSH rather than cloning directly onto the Pi, deploy/sync.sh rsyncs the
whole project over (never partially — see the comment in the script for
why that matters) and deploy/install.sh then builds and restarts it:
PI_HOST=user@raspberrypi.local bash deploy/sync.sh
ssh user@raspberrypi.local 'cd minidsp-webui && sudo bash deploy/install.sh'PI_HOST, PI_PORT, PI_DEST, and SSH_KEY are all overridable env vars
(see the top of deploy/sync.sh for defaults).
All via environment variables, set in deploy/minidsp-webui.service or your own process manager:
| Variable | Default | Purpose |
|---|---|---|
PORT |
5381 |
Port the web UI/API listens on |
HOST |
0.0.0.0 |
Bind address |
MINIDSP_HOST |
127.0.0.1 |
Where minidspd's HTTP API is |
MINIDSP_PORT |
5380 |
minidspd's HTTP port |
MINIDSP_DEVICE_INDEX |
0 |
Which device, if minidspd sees more than one |
PRESETS_DIR |
data/presets |
Where the 4 preset JSON files live |
BACKUPS_DIR |
data/backups |
Where named backups live |
MINIDSP_MOCK |
unset | Set to 1 to run against an in-memory mock instead of a real device (local development, tests) |
MINIDSP_ALSA_DEVICE |
hw:CARD=m2x4HD,DEV=0 |
ALSA device used for hardware-in-the-loop sweeps |
HIL_SWEEP_DURATION_SEC |
6 (prod) / 0.05 (mock) |
Length of one hardware-in-the-loop sweep |
HIL_SWEEP_POLL_INTERVAL_MS |
40 (prod) / 5 (mock) |
How often the level meter is sampled during a sweep |
Backend against an in-memory mock of minidspd:
cd backend && MINIDSP_MOCK=1 npm run devFrontend with hot reload (proxies /api and /ws to localhost:5381):
cd frontend && npm run devcd backend && npm test # unit + integration tests (Supertest against the mock)
cd frontend && npm test # biquad/crossover math, WS frame merging
cd frontend && npm run e2e # Playwright E2E against the mock backend
cd frontend && npm run e2e:flaky-check # same, x5 repeats, for timing-sensitive changesdeploy/smoke-test.sh <url> is a read-only post-deployment check (device
detected, master status readable) — it never writes to the device.
See TESTING.md for lessons learned about why the test setup looks the way it does (mock fidelity, error-handling coverage, flakiness, deploy discipline).
Because the device can't be read back, there was previously no way to confirm that a configured filter was actually being applied correctly by the DSP chip — only that the software believed it had sent the right values. The verify feature closes that gap: it plays a continuous logarithmic sine sweep (20 Hz–20 kHz) through the DSP via the system's ALSA device, reads the device's own post-filter level meter throughout the sweep, and compares the measured response against the mathematically predicted one.
- PEQ verify tests each of the 10 slots individually (all others held bypassed) plus the combined effect of all 10 as actually configured.
- Crossover verify tests the high-pass and low-pass groups individually plus their combined effect.
Both are available as a "Verify against hardware" button in the respective editor. A full PEQ verify takes roughly 1–2 minutes (12 sweeps back to back); crossover verify takes 30–40 seconds (4 sweeps). The original configuration is always restored afterward, whether the sweep succeeds or is interrupted.
- No authentication (matching minidsp-rs itself) — only run this on a trusted LAN.
- The backend can't detect out-of-band changes. If you use the official
miniDSP Console, the mobile app, or the
minidspCLI directly, the backend's stored state silently goes stale, and the next change made through this UI will push that stale state back to the device, overwriting whatever you just set elsewhere. Don't mix control surfaces without reconciling state in between. - minidsp-rs 0.1.12's HTTP/WS server occasionally hangs completely (even
on plain
GETrequests), independently of the USB link to the device, which keeps working.deploy/minidspd-watchdog.{sh,service,timer}health- checks it every 2 minutes and restartsminidsp.serviceautomatically. - A crossover group or FIR filter with an empty coefficients array
(
{bypass: true, coefficients: []}— the untouched default) reliably wedges minidspd until it's restarted, confirmed by isolating every individual request in a full preset push.pushFullConfigToDevice(backend/src/routes/presets.ts) skips these writes entirely, since a bypassed filter with nothing configured has nothing worth writing anyway. - Dirac Live purchase/activation isn't implemented — the official software triggers a Stripe payment for that, which is out of scope here.
- FIR file import expects raw IEEE-754 float32 binary data; exceeding the device's tap budget (4096 total / 2048 per channel) is reported as whatever error minidspd itself returns for that.