BabelPod uses Socket.IO for real-time communication between clients and the server. All payloads are JSON objects. The API version is included in the state event on connection.
Additions marked v1.1 are additive and backward compatible — v1 clients keep working unchanged. New capabilities are signaled by the presence of their field in state (capability by presence); servers without a feature omit the field entirely and clients hide the corresponding controls.
Connect to the server's Socket.IO endpoint (default http://<host>:3000). On connection, the server emits a single state event containing the full current state. Subsequent changes are sent as individual events.
Sent once immediately after connection. Contains the full server state.
{
"version": 1,
"sessionOwner": "socket-id" | null,
"inputs": [{ "id": "plughw:0,0", "name": "USB Audio" }, ...],
"outputs": [{ "id": "air:Kitchen", "name": "Kitchen (AirPlay)", "volume": 50 }, ...],
"selectedInput": "plughw:0,0",
"selectedOutputs": ["air:Kitchen", "air:Bedroom"],
"volume": 50,
"turntablePower": { "on": true, "reachable": true }
}Each output's optional volume (v1.1) is present only on outputs that support per-output volume — see Per-Output Volume. turntablePower (v1.1) is present only when the server has a turntable smart plug configured — see Turntable Power & Silence Auto-Off.
Sent when the available input device list changes.
{ "inputs": [{ "id": "void", "name": "None" }, { "id": "plughw:0,0", "name": "USB Audio" }] }Sent when the available output device list changes (AirPlay devices discovered/lost, etc.).
{ "outputs": [{ "id": "air:Kitchen", "name": "Kitchen (AirPlay)", "volume": 50 }] }Each output object may carry an optional integer volume (0–100) — see Per-Output Volume (v1.1).
Sent when the active input device changes.
{ "id": "plughw:0,0" }Sent when the active output selection changes.
{ "ids": ["air:Kitchen", "air:Bedroom"] }Sent when the master volume changes.
{ "value": 75 }Sent whenever one output's per-output volume changes — including changes the server makes itself (e.g. a master setVolume "set all" emits one outputVolume per affected output). value is an integer 0–100.
{ "id": "air:Kitchen", "value": 60 }Only AirPlay outputs have independent volume; see Per-Output Volume.
Sent on every turntable plug state or reachability change — including changes made from Apple Home, the plug's physical button, or the server's silence auto-off — not just in response to setTurntablePower. The payload always reflects the real hardware state (the server subscribes to the plug's Matter OnOff attribute and never synthesizes this from a command).
{ "on": true, "reachable": true }reachable: false means the server currently cannot confirm or command the plug (Matter node offline); on is then the last known state and may be stale. Clients should disable their power toggle while unreachable.
Only sent by servers with a configured turntable plug. The state event includes the same object as turntablePower — its presence is the capability signal for showing the power control.
Sent when session ownership changes. The owner is the only client allowed to make changes. null means no owner.
{ "owner": "socket-id" }Informational message from the server.
{ "message": "Input switched to plughw:0,0" }Error message from the server.
{ "message": "Failed to connect to Bluetooth: Connection timeout" }All commands require session ownership. If a non-owner sends a command, it is silently ignored. Use takeover to claim ownership.
Select an input device.
{ "id": "plughw:0,0" }Special values:
"void"- No input (silence)"bluealsa:SRV=org.bluealsa,DEV=AA:BB:CC:DD:EE:FF,PROFILE=a2dp"- Bluetooth device
Set the active outputs. Always send the full list of desired outputs (not a toggle).
{ "ids": ["air:Kitchen", "air:Bedroom"] }Output ID prefixes:
plughw:- Local ALSA PCM deviceair:- Single AirPlay deviceairpair:- Stereo-paired AirPlay devices
Set the master (group) volume (0-100). Requires session ownership. Follows Apple's group-volume model: the master is the average of the selected speakers, and changing it shifts every selected speaker by the same delta, preserving the relative balance between them (rather than flattening them to one number). Emits volume plus one outputVolume per affected speaker. See Per-Output Volume.
{ "value": 75 }Set one output's per-output volume. Requires session ownership. value is clamped to 0–100 and rounded to an integer; unknown ids and outputs that don't support volume are ignored. Applies the gain to that output's stream and broadcasts outputVolume.
{ "id": "air:Kitchen", "value": 60 }Request a turntable plug power change. Requires session ownership. The server does not echo this back directly — it commands the plug and lets the Matter OnOff subscription drive the turntablePower broadcast, so the confirmation always reflects actual hardware state (and self-corrects if the command failed). Clients should show a pending state rather than flipping their toggle optimistically, and give up after ~8 seconds without a turntablePower broadcast.
{ "on": false }If the plug is unreachable, the server broadcasts serverError plus turntablePower with the last known state and reachable: false.
Commission a Matter smart plug at runtime from a manual pairing code (no server restart). Requires session ownership. Ignored if a plug is already configured. The server discovers the plug on the LAN using the code (which encodes a discriminator), commissions it onto BabelPod's fabric, and persists only a "configured" flag — never the code.
{ "pairingCode": "1406-013-3112" }Dashes are optional. Flow for clients: emit this, show a pending state (commissioning can take up to ~60s), then resolve on the first turntablePower broadcast (success — the power control now appears) or a serverError (failure — stay on the setup form). The server also emits a status while commissioning and on success. Only one plug is supported; there is no in-app removal.
Claim session ownership. No payload required.
{}Update instance configuration. Partial updates allowed — only fields present are changed. Requires session ownership.
{
"displayName": "Living Room Pi",
"defaultInputId": "plughw:0,0",
"defaultOutputIds": ["air:Kitchen", "air:Living Room"],
"defaultVolume": 50,
"autoconnectEnabled": true,
"autoconnectThreshold": 0.01,
"autoOffEnabled": true,
"autoOffSilenceThresholdDb": -43,
"autoOffNoiseFloorDb": -62,
"autoOffSilenceMinutes": 20
}The autoOff* fields (v1.1) configure silence auto-off for the turntable plug. To calibrate: watch rmsLevel (20·log10(level) = dBFS) with the turntable off to find the line's noise floor, and again with a record spinning in the runout groove to find the surface-noise level. Set autoOffNoiseFloorDb a few dB above the former and autoOffSilenceThresholdDb ~5 dB above the latter — runout noise must land between the two or auto-off will never fire (music is far louder, typically −30 dBFS and up, so the threshold has plenty of headroom). The plug itself (turntablePlugEnabled, turntablePlugPairingCode) can only be configured in babelpod.config.json on the server, since commissioning is a one-time operator step that requires a server restart.
Toggle the autoconnect state machine. "listening" arms autoconnect (enters idle/listening). "paused" is a master kill switch — stops all outputs immediately. Requires session ownership.
{ "state": "listening" }BabelPod stores instance configuration in babelpod.config.json alongside the server. Configuration persists across restarts and is editable from any client via the setConfig event.
config — Sent when configuration changes. Also included in the state event on connection.
{
"displayName": "PattyPi",
"defaultInputId": "plughw:0,0",
"defaultOutputIds": ["air:Kitchen", "airpair:Living Room"],
"defaultVolume": 50,
"defaultOutputVolumes": { "air:Kitchen": 65, "airpair:Living Room": 40 },
"autoconnectEnabled": true,
"autoconnectThreshold": 0.01
}airplay2Enabled (default true, file-only — not settable via setConfig and not surfaced in any client) selects the AirPlay 2 path (HAP transient pairing + SETUP plist) for receivers whose mDNS features advertise transient pairing (bit 48: HomePods, Apple TVs). Everything else keeps the legacy RAOP path. HomePod software / tvOS 27 accept a RAOP session (every RTSP request 200 OK) but never render its audio, so this is required for those receivers; set it to false in babelpod.config.json only to force RAOP everywhere while debugging.
defaultOutputVolumes (v1.1) is a per-speaker default volume map ({ outputId: 0–100 }). When autoconnect brings up the default outputs, each speaker comes up at its own level here, falling back to defaultVolume for any default speaker without an entry — so the default setup keeps its balance instead of all speakers starting at one number. AirPlay outputs only; the server clamps/rounds values and drops malformed entries on save. Settable via setConfig (the web UI Settings shows a slider per default speaker).
BabelPod can automatically detect audio on the input and route it to configured default speakers. Uses RMS level monitoring with an exponential moving average to distinguish sustained music from transient surface noise.
autoconnect — Sent when the autoconnect state changes.
{ "state": "idle" }Valid states: "paused", "idle", "detecting", "connected", "silence"
rmsLevel — Sent at ~4Hz with the current input audio level (0.0-1.0 range).
{ "level": 0.042 }- paused — Master kill switch. All outputs stopped. Autoconnect won't trigger.
- idle — Armed and listening. Monitoring input RMS level. Outputs disconnected.
- detecting — Signal above threshold detected, sustaining for 250ms to filter transient bumps (e.g., table bumps). Uses raw RMS.
- connected — Audio routing active. Default outputs connected. Uses smoothed RMS with hysteresis (threshold/4) to avoid flip-flopping during quiet passages.
- silence — Silence detected while connected. Outputs disconnect after 5 minutes of sustained silence. Uses smoothed RMS.
The "listening" client command enters idle (arms autoconnect). The "paused" command stops everything immediately. On server restart, state is determined by config.autoconnectEnabled.
- Phono mode (with preamp): threshold
0.01— music at 0.03-0.08 smoothed, surface noise at 0.001-0.002 smoothed - Line mode (no preamp): threshold
0.0005— much quieter signal
The state event includes config and autoconnect state:
{
...existing fields...,
"config": { ... },
"autoconnectState": "idle"
}Each output can carry its own volume independent of the master. The contract is additive over v1:
- Capability by presence. Every output object in
state.outputsand theoutputsevent may include an optional integervolume(0–100). Clients enable per-speaker control based on the presence of this field on any output; a server without the feature omits it entirely and clients fall back to the single master slider. - Which outputs support it. Only AirPlay outputs (
air:/airpair:) have a software gain knob (vianode_airtunes2per-device volume), so only they carryvolume. Local ALSA (plughw:) outputs are piped raw toaplaywith no gain control and omit the field. - Per-output volume is the authoritative device gain (an absolute 0–100 level).
setOutputVolume {id, value}sets one speaker directly. - Master volume follows Apple's group model.
state.volumeis the average of the selected speakers' per-output volumes.setVolumeshifts every selected speaker by the same delta to reach the requested average, preserving their relative balance — so a speaker you set quieter stays proportionally quieter (60 on a stereo pair and 45 on a mini hold their offset when you move the group). When all speakers are equal this is identical to "set all"; balance is only preserved once you've trimmed speakers apart. At the 0/100 rails a clamped speaker's offset compresses (the additive model can't preserve an offset past a rail). Changing one speaker viasetOutputVolumelikewise re-broadcastsvolumewith the new average, and a newly selected speaker joins at the current group level. - Events:
setOutputVolume {id, value}(client→server, owner-only, clamped/rounded, unknown ids ignored) → applies the gain and broadcastsoutputVolume {id, value}to all clients.setVolumebroadcastsvolumeplus oneoutputVolumeper shifted speaker. - Persistence. Live per-output volumes are runtime state, like the master
volume— they survive reconnects (re-read fromstate) but reset on server restart. (This is deliberate: persisting every slider movement tobabelpod.config.jsonwould hammer the Pi's SD card.) For a setup that should start balanced, configuredefaultOutputVolumes(see Instance Configuration) — those per-speaker defaults are saved to config and applied each time autoconnect brings the default speakers up.
The turntable can be plugged into a HomeKit-enabled Matter smart plug. BabelPod commissions the plug onto its own Matter fabric (multi-admin — it stays paired with Apple Home) and:
- Exposes the plug over the API:
setTurntablePower/turntablePowerandstate.turntablePower. - Subscribes to the plug's OnOff attribute, so changes made from Apple Home or the physical button are broadcast to all clients.
- Optionally powers the plug off after sustained input silence (a record left spinning in the runout groove). The silence detector taps the same RMS pipeline as autoconnect, converts to dBFS (with ~1s smoothing), and fires only after
autoOffSilenceMinutesof continuous signal in the dead band betweenautoOffNoiseFloorDbandautoOffSilenceThresholdDb— the signature of a powered turntable with nothing playing. Louder audio (music) resets the window, so inter-track gaps and quiet passages never trigger; so does a level below the noise floor, which means the turntable itself is already off and cutting the outlet would only be an annoyance. After firing it disarms until power returns.
In-app (recommended): open the web UI Settings → Turntable Power Plug. In Apple Home, open the plug's settings and Turn On Pairing Mode to get a fresh setup code, enter it, and Set Up. The server commissions the plug live (no restart) via setupTurntablePlug; on success the power control appears. Only one plug is supported.
File-based (still works): set "turntablePlugEnabled": true and "turntablePlugPairingCode": "<code>" in babelpod.config.json and restart — the server commissions from the code on boot.
Either way, commissioning credentials persist in .matter-storage/, so the code is only needed once; the plug stays paired with Apple Home (multi-admin). The pairing code is a setup secret and is never broadcast — it's stripped from the config event and state.config. A Wi-Fi Matter plug is reachable directly over IP; a Thread-only plug would additionally require a Thread Border Router on the Pi.
setTurntablePower is owner-gated like every other client command. Internal automation (the silence auto-off) uses a privileged path that bypasses the session-owner check without changing session ownership — it never emits session, only the resulting turntablePower/status broadcasts.
Only one client controls BabelPod at a time. The session model works as follows:
- The first client to connect becomes the owner automatically.
- Additional clients connect as observers and can see all state but cannot make changes.
- A non-owner can call
takeoverto claim control. - When the owner disconnects, if only one client remains it automatically becomes the owner. Otherwise ownership is released (
session.ownerbecomesnull). - All clients receive
sessionevents when ownership changes.
BabelPod advertises itself via mDNS/Bonjour as _babelpod._tcp on the configured port. Clients can use this to discover servers on the local network without manual configuration.
The server broadcasts state changes to all connected clients, including the client that initiated the change. Clients should implement echo detection to avoid feedback loops (e.g., a volume slider snapping back when the server echoes the client's own change). A recommended approach:
- Track the last value emitted to the server (
lastEmitted). - When a server event arrives, compare against
lastEmitted. - If they match, skip updating the UI (it already reflects the user's action).
- Reset
lastEmittedon reconnection and ownership changes.