A self-hosted web app for browsing, playing, and practicing interactive music notation — a scrolling note highway, standard notation, synced lyrics, and live note detection from your instrument, all running on hardware you own.
Charts come from importing Guitar Pro (GP3–GP8) or MusicXML, or from authoring in the Song Editor plugin. FeedBack stores them in its own open, hand-editable package format.
FeedBack ships as a Docker container.
git clone https://github.com/get-flashbacks/feedBack.git
cd feedBack
LIBRARY_PATH=/path/to/your/songs docker compose up -dOpen http://localhost:8000.
LIBRARY_PATH points at the folder holding your songs; it defaults to
./library if unset. Config and caches persist in a named feedBack-config
volume, so they survive a container rebuild.
There is a separate docker-compose.nas.yml for NAS deployments, and
build-proxmox-ct.sh for a Proxmox container.
pip install -r requirements.txt
python main.py # 0.0.0.0:8000
HOST=127.0.0.1 PORT=8001 python main.py # or pick your ownSet these in docker-compose.yml or the environment:
| Variable | Purpose |
|---|---|
DLC_DIR |
Song library folder inside the container (default /dlc) |
CONFIG_DIR |
Persistent config + cache. /config inside the container; on bare metal it defaults to ~/.local/share/feedback |
LOG_LEVEL |
DEBUG | INFO | WARNING | ERROR (default INFO) |
LOG_FORMAT |
json | text (default text, coloured console) |
APP_SOURCE_URL |
Overrides the Settings → About source link |
APP_LICENSE_URL |
Overrides the licence link — set this explicitly if you host on a non-GitHub forge |
FeedBack is meant to be reached directly — over the LAN, or through a VPN into
your own network. The LAN-share relay (/ws/sync/…, used by Split Screen)
caps how fast one address may open connections. No feedBack code reads a
client-IP header: the cap is keyed on websocket.client.host, the client
address the ASGI layer hands the app — the connecting proxy's address unless
the server's proxy-header middleware rewrites it. So whether clients keep
separate buckets comes down to the topology:
- Proxy on the same host as feedBack — uvicorn applies
X-Forwarded-Forfrom an address it trusts,127.0.0.1by default (recent versions add::1), so a proxy on IPv4 loopback gives every client its own bucket. SetFORWARDED_ALLOW_IPSif it connects over::1or from another address you trust, and make sure it overwrites the header rather than appending to it. - Proxy reaching feedBack from an address it doesn't trust — a tunnel or
gateway on another machine, say. The header is ignored, so every client
behind it shares one bucket and a join storm big enough to exhaust that
shared budget is turned away before it reaches the room. That budget is sized
for a full room's worth of near-simultaneous joins, which covers ordinary
reconnect bursts; for a larger deployment, move the proxy onto the feedBack
host or widen the cap (
lib/routers/ws_sync.py,CONN_BURST).
Nothing else in feedBack distinguishes clients by address, so a proxy that doesn't preserve the client address costs the relay its per-client rate accounting — not its functionality.
Loose folder — a directory of arrangement XML plus an audio file, with an
optional manifest.json and album art. Played directly, no import step.
Sloppak — FeedBack's own package format, and the preferred one for new
work. It exists interchangeably as a .sloppak zip (for distribution) or a
.sloppak/ directory (for authoring), holding a YAML manifest, per-arrangement
note data, audio stems, cover art, and syllable-level lyrics.
The format itself is specified outside this repo, in got-feedback/feedpak-spec — published there as feedpak; the same on-disk format this codebase still calls sloppak internally. The spec is the authority: a new manifest key is not part of the format until it lands there. CI enforces this, and there is no in-repo bypass. See docs/sloppak-spec.md for the local code map and docs/feedpak-spec-gate.md for how the gate works.
Plugins are the main extension point, and most of FeedBack's features are built
as one. A plugin lives in plugins/<id>/ with a plugin.json manifest and can
contribute any mix of frontend screen, backend routes, and settings panel.
Name the directory to match the manifest's
id(case-sensitive). That pairing is what marks a plugin as a bundled core one for duplicate resolution — it is not a discovery requirement, since the loader registers plugins by their manifestidwhatever the folder is called. A plugin that genuinely won't load is usually missingplugin.json, has a manifest that fails to parse, or has anidthat is absent, empty, or not a string.
A few of the things a plugin can do:
- Replace the highway renderer — declare
"type": "visualization"and export awindow.feedBackViz_<id>factory. Works in the main player and per-panel under split screen. - Layer an overlay on top of whichever renderer is active — fretboard diagrams, chord labels, practice feedback.
- Score playing and feed per-note judgments back so any renderer can light up the gems.
- Register a mixer fader, keyboard shortcuts, a library source, or a detachable pane the user can pop into its own window.
Start with feedBack-plugin-template. The contracts are documented in CLAUDE.md, with deeper guides in docs/ — see plugin-v3-ui.md, plugin-styles.md, plugin-modules.md, plugin-panes.md, capability-recipes.md, plugin-host-compat.md (how to probe required vs optional host APIs so a missing feature degrades alone, never the plugin), and compatibility.md (which host commit each plugin feature needs, pinned by hash rather than version string).
pip install -r requirements.txt -r requirements-test.txt
pytest # Python suite
npm run test:js # JS unit tests
npm test # Playwright end-to-end
npm run lint # ESLint
bash scripts/build-tailwind.sh # regenerate static/tailwind.min.cssTailwind is served as a prebuilt stylesheet, never the Play CDN — the CDN's runtime JIT rescanned the DOM on the main thread and cost ~26% of frames. CI rebuilds and diffs the committed CSS, so run the build script and commit the result when you add new classes.
VERSION at the repo root is the single source of truth for the version, and
is synced automatically when a desktop release is cut. GET /api/version
serves it.
Library-scan cost is measured and documented in
docs/scaling-envelope.md — per-song scan time,
warm-rescan time, metadata-cache growth, and the per-worker memory footprint — so
you can size a deployment (FEEDBACK_MAX_SCAN_WORKERS) instead of guessing.
Please read CONTRIBUTING.md first. Two things to know up front:
- Every commit needs a DCO sign-off (
git commit -s). - Never push directly to
main— branch, then open a PR.
Curated plugins should be AGPL-3.0 or AGPL-compatible (MIT, BSD, Apache-2.0).
AGPL-3.0-only. Contributions are inbound = outbound under the same terms.