LAN-synchronized single-stream audio playback: one global stream, all devices in sync. Like an old school radio!
- Single global stream — no rooms, no codes. One playlist and one playback state for the entire LAN.
- Sub-10 ms sync on LAN using NTP-style clock sync and Web Audio API.
- Per-device delay (0–500 ms) to compensate for speaker placement.
- Multiple library folders — add paths from the UI (under an allowed base); scan recursively. Upload to a chosen folder.
- Upload or scan, collaborative playlist, simple chat.
I created Roomz because I was annoyed that I cannot connect more than 1 BT Speaker to my phone for wider 'covarage'.
Now I can play music throughout my house from multiple devices.
I'm pretty sure something else already exists for this. . . but so-what.
- Add a centralized control, for managing per device delays and volume from one client.
- Create a client for rpi or headless devices.
- Maybe add a 'Announcement' function to the 'Main' client, to broadcast voice over the stream.
Roomz is meant to be run on a private LAN/Home network. There is no security built in, and anybody connected to the network has full access. If enough interest is shown for it, then security might be added at a later date.
Copy .env.example to .env and adjust if needed (e.g. ROOMZ_LIBRARY_BASE). The app loads .env automatically at startup.
uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000Then open http://<this-machine-ip>:8000 in a browser.
Install with dev dependencies (pytest, ruff, black), then run tests and lint:
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run black --check . # optional- Run the server on a machine on your LAN (e.g.
192.168.1.10). - On multiple devices (phones, laptops, same LAN), open
http://192.168.1.10:8000. - Add or scan music, press play. All clients should play the same track at the same position.
- Use Speaker delay (ms) on each device to align with physical speaker placement (e.g. farther speakers get more delay).
- Library folders: In Library folders, add a path (and optional name). Paths must be under the allowed base (default: project
data/; setROOMZ_LIBRARY_BASEto allow e.g./home/you/Music). Remove a folder only when it has no tracks. - Upload: Choose a destination folder in the Upload to dropdown, then Upload and select MP3/WAV/OGG/M4A/FLAC files. They are saved under that folder and added to the playlist.
- Scan: Scan all scans every library folder recursively; or click Scan next to a folder to scan only that one. New files are added to the DB and playlist.
Use the Speaker delay compensation (ms) slider (0–500) in the header to fine-tune each device. No server round-trip; applies instantly to the sync position so playback stays aligned with others.
On a typical LAN, sync within <10 ms is achievable with Web Audio API and clock sync. Browser and device output latency can add a constant offset; use the delay slider to compensate so all speakers sound in phase.
Roomz/
├── pyproject.toml
├── uv.lock
├── tests/
│ ├── conftest.py
│ └── test_app.py
├── app/
│ ├── main.py # FastAPI app, /music/track/{id}, /ws, static mount
│ ├── config.py # LIBRARY_BASE, validate_library_path
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ ├── db.py
│ ├── ws_manager.py
│ ├── clock_sync.py
│ ├── audio_utils.py
│ └── routers/
│ └── api.py # upload, scan, playlist CRUD
├── data/
│ ├── music/ # audio files
│ └── app.db # SQLite
├── frontend/
│ ├── index.html
│ └── static/ # CSS and JS assets
│ ├── app.js
│ ├── colors.css
│ ├── _reset.css
│ ├── base.css
│ ├── layout.css
│ └── components.css
└── alembic/
- Backend: FastAPI, Uvicorn, SQLAlchemy 2.0 (async), Alembic, SQLite, mutagen, aiofiles. Managed with uv.
- Frontend: Vanilla HTML/CSS/JS, layered vanilla CSS (reset, base, colors, layout, components), Web Audio API (with <audio> fallback).
No auth (LAN-only). Production run: uv run uvicorn app.main:app --host 0.0.0.0 --port 8000.