A first pass over an unanalysed library is hours of librosa, and there was no reason for a desktop to spend them one core at a time. Analysis now runs in a process pool sized by `general.library.analysis_workers`, defaulting to one per core bar one (capped at 8) when the key is absent. Two consequences worth knowing before touching this: whatever an `Analyzer` returns has to be picklable, and an analyzer that keeps state on its own instance - a test double counting calls - only behaves as written with `analysis_workers=1`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.5 KiB
MusicMouse — repo overview
Orientation doc for AI agents (or humans) working on this repo for the first time.
What this is
MusicMouse is a DIY, Toniebox-style physical music player for kids, shaped like a mouse
and living on a shelf. Small 3D-printed animal figurines (fox, owl, dog, elephant,
squirrel, crocodile, rabbit, snowman, puppy — see hardware/3dprints/figures/) each
carry an RFID tag. Placing a figurine on the mouse starts that figure's playlist. The
mouse also has a rotary encoder and capacitive touch areas (ears/feet) for
volume/skip control, addressable RGBW LED rings with animated effects, and MQTT/Home
Assistant integration.
Repo layout
| Path | What it is |
|---|---|
python-backend/ |
Python host application — the main runtime. Has its own README.md with the architecture; start there for backend work. |
web/ |
React + TypeScript front-end (Vite). Browse the whole library and play any of it from a browser. Built output is served by python-backend itself. |
esp-firmware/ |
ESP32 firmware (C++, Arduino framework via PlatformIO). Reads the RFID reader and buttons, drives the LED strips, talks to python-backend over serial. |
hardware/ |
3D-print models for the figurines and enclosure (FreeCAD/Blender/OBJ/STL), a Fritzing electronics sketch, datasheets, and pinout.md (RFID reader + button-board wiring). |
claude-design/ |
The interaction and visual spec the web front-end was built from ("Dolphin Beats"), as a standalone HTML mockup with hardcoded data. web/ is the real implementation; the mockup is kept as the reference for the keyboard model and the styling. It also contains a second page, "Mein Zimmer" (room lights), which is not implemented. |
How the pieces talk to each other
-
ESP32 firmware ↔
python-backend: a length-prefixed binary protocol over serial. Frames areuint32 magic | uint8 type | uint16 size | payload, little-endian, with firmware log text interleaved on the same link. The Python side lives inpython-backend/musicmouse/devices/wire.py, the firmware side inesp-firmware/src/Messages.h.The contract is hand-duplicated in two languages.
tests/test_wire.pyparsesMessages.hand fails if the message ids drift, andtests/test_effects.pypins the exact bytes of every effect payload — so a firmware change that breaks the host now breaks a test instead of just the LEDs. -
python-backend↔web/: JSON over HTTP for the library and for commands, plus a push-only websocket at/api/wsfor state. Commands emit exactly the same intents the physical buttons emit, so the web UI has no privileged path — and no way to get out of step with a figure someone puts on the reader. Seepython-backend/README.mdfor the endpoint list. -
python-backend↔ Home Assistant: MQTT only. The backend publishes three discoverable lights, a player sensor, a volume number, transport buttons, device triggers for every button and touch area, and a tag scanner for the RFID reader. It does not call Home Assistant services directly any more; behaviour like "the left ear turns the room light pink" is an HA automation. Seepython-backend/README.mdfor the trigger topics and the old colour mapping.
Running it
python -m musicmouse --config /media/musicmouse/config.yml
On a machine with no mouse attached — real audio and a real web UI, no serial port:
python -m musicmouse --config ./config.yml --no-hardware
general.serial_port: simulate does the same thing from the config, and
general.alsa_device: simulate swaps in a silent player. Both warn at startup, and
both keys are required - omitting one is an error rather than an implicit simulation.
Or with no hardware and no audio:
python -m musicmouse --config ./config.yml --simulate
The simulator runs the whole app against a fake serial link and a fake player, either
interactively or from a scenario file. python-backend/musicmouse.service is the
systemd unit for the device.
For front-end work, run the backend (either way above) and then:
cd web && npm install && npm run dev # http://localhost:5173, /api proxied to :8080
npm run build writes web/dist, which general.web.static_dir points at in
production so one process serves both the UI and the API.
Config
python-backend/config.yml.example documents the schema. In short:
general.{library.*, serial_port, baudrate, reconnect_interval, alsa_device, min_volume, max_volume, initial_volume, volume_increment, button_leds_brightness, audio_extensions, mqtt.*, web.*}figures.<name>.{id, colors}—idis a 5-byte hex RFID tag, unique per figure;colorsis exactly four (primary, secondary, bg, accent), each"#rrggbb"or"wNN".
general.library.root is the one path to the music. The shelves under it are fixed
names, not settings, because each has quirks the scanner knows about:
<root>/Figuren/<figure name>/ one folder per figurine
<root>/Musik/<Artist> - <Album>/ albums, grouped by artist
<root>/Hörbücher/<Artist> - <Album>/ audiobooks, grouped by character
<root>/Kinderpodcasts/<Show>/ shows, newest episode first
Config is validated with pydantic: unknown keys are errors, and every problem is reported at once.
Notes for agents
- The backend has
pytest,ruffandmypy --strictconfigured inpython-backend/pyproject.toml, and no CI. Run all three before proposing changes. - Prefer adding a scenario in
python-backend/scenarios/over a hand-rolled test when the behaviour is end-to-end — those files are executed by the test suite. python-backend/notebooks/is university course material on chord recognition, not part of the app.- The firmware has no automated tests beyond a PlatformIO
nativeenv for LED effects. - The front-end has
vitestover the pure modules (web/src/lib/) andtsc --noEmit; there is no component-level test harness.npm run testandnpx tsc --noEmitare the two checks. - The library index is cached under
general.library.cache. It is keyed by file mtime and size, so a change to how the scanner derives a title, artist or series is invisible until the cache is invalidated — bump_INDEX_VERSIONinmusicmouse/library/cache.pywhen you touch that logic. - Track analysis (librosa) runs in a pool of worker processes - see
musicmouse/library/workers.py. Anything anAnalyzerreturns therefore has to be picklable, and an analyzer that records state in its own instance (a test double counting calls) only behaves as written withanalysis_workers=1.