Files
shawn d19e0ea7a8 Add MasterBus component + enhance BusStrip with routing selector
MasterBus: stereo master fader, mute, level meters in PiPedal theme
(purple accent per mixer-engine branch reference).

BusStrip: added per-channel routing selector (ch1Route..ch8Route)
for selecting output bus target (Main, Bus 2-8).

MixerPage: integrated MasterBus with levels derived from bus
master data. Local state for volume/mute until backend exposes
master output.
2026-06-23 12:15:09 -04:00

302 lines
10 KiB
Markdown

# OPLabs Mixer App
A standalone digital mixer application with a C++ real-time audio engine,
Python/FastAPI backend, and React frontend.
## Architecture
```
REST / WS
Browser (React SPA) ───────── FastAPI Backend (:8081)
│ │
│ WS /ws/pipedal │ WS /pipedal (upstream, legacy)
└── PiPedal (192.168.0.245) └── Legacy PiPedal scenes
│ │
│ WS /ws/mixer │ Unix Socket IPC
└── Mixer Daemon Relay ──────┴── /tmp/oplabs-mixer.sock
C++ Mixer Daemon
(MixerEngine + JACK)
```
Two parallel backends:
1. **Mixer Daemon (primary):** C++ daemon running the real-time mixer engine
(MixerEngine, MixerChannelStrip, MixerBus). Communicates with FastAPI
via Unix domain socket IPC.
2. **PiPedal (legacy):** WebSocket proxy to PiPedal pedalboard for backward
compatibility with existing OPLabsBandChannel/Bus LV2 plugins.
## Prerequisites
- Python 3.10+
- Node.js 18+
- C++20 compiler (gcc 11+ or clang 14+)
- CMake 3.16+
- JACK Audio Connection Kit (libjack-jackd2-dev)
- PiPedal at `192.168.0.245:8080` (optional, for legacy mode)
## Setup
### Backend
```bash
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### Frontend
```bash
cd frontend
npm install
npm run build
```
### Mixer Daemon (C++)
```bash
cd daemon
mkdir build && cd build
cmake ..
make
```
The daemon requires the MixerEngine sources from the `op-pedal` mixer-engine branch.
Either:
- Build inside the op-pedal CMake tree, or
- Set `-DMIXER_ENGINE_SOURCE_DIR=/path/to/op-pedal/src/`
## Running
### 1. Start JACK (if using the daemon with audio I/O)
```bash
jackd -d alsa -r 48000 -p 512 -n 2 &
```
### 2. Start the Mixer Daemon
```bash
./daemon/build/oplabs-mixer-daemon \
--socket-path /tmp/oplabs-mixer.sock \
--sample-rate 48000 \
--buffer-size 512
```
### 3. Start the Backend
```bash
cd backend
source venv/bin/activate
python app.py # runs on port 8081
```
Open http://localhost:8081 in your browser.
Or use the convenience script:
```bash
./start.sh
```
## IPC Protocol (C++ Mixer Daemon <-> Python FastAPI)
### Transport
- **Type:** Unix domain socket (`AF_UNIX`, `SOCK_STREAM`)
- **Path:** `/tmp/oplabs-mixer.sock`
- **Permissions:** 0666 (read/write for owner and group)
### Framing
Every message is prefixed with a 4-byte big-endian unsigned integer
(uint32_t) specifying the length of the JSON payload in bytes.
```
[4-byte length BE] [UTF-8 JSON payload]
```
### Request Format
```json
{
"id": 1,
"method": "getState",
"params": {}
}
```
### Response Format
```json
{
"id": 1,
"result": { ... },
"error": null
}
```
On error, `result` is null and `error` contains a string description.
### Push Messages (Daemon -> Client)
Unsolicited messages from the daemon are tagged with `"type": "push"`:
```json
{
"type": "push",
"event": "vuMeter",
"data": {
"channels": [{"index": 0, "left": -12.5, "right": -10.2}, ...],
"buses": [{"id": 1, "left": -6.0, "right": -5.8}]
}
}
```
### Message Reference
| Method | Params | Result |
|---------------------|------------------------------------------------------|------------------------------------------------------|
| `getState` | `{}` | Full mixer state object (channels, buses, routes, output routes) |
| `setChannelVolume` | `{"channelIndex": int, "volumeDb": float}` | `{}` |
| `setChannelPan` | `{"channelIndex": int, "pan": float}` | `{}` |
| `setChannelMute` | `{"channelIndex": int, "mute": bool}` | `{}` |
| `setChannelSolo` | `{"channelIndex": int, "solo": bool}` | `{}` |
| `addChannel` | `{"physicalInputIndex": int}` | `{"channelIndex": int}` |
| `removeChannel` | `{"channelIndex": int}` | `{}` |
| `getOutputRoutes` | `{}` | `{"routes": [sourceBusId, sourceStartChannel, targetStartChannel, channels]}` |
| `setOutputRoutes` | `{"routes": [sourceBusId, sourceStartChannel, targetStartChannel, channels]}` | `{}` |
| `saveScene` | `{"name": string}` | `{"id": int, "name": string}` |
| `loadScene` | `{"sceneId": string}` | `{"ok": bool}` |
| `listScenes` | `{}` | `{"scenes": [{"id": int, "name": string}, ...]}` |
| `deleteScene` | `{"sceneId": string}` | `{"ok": bool}` |
### getState Result Schema
```json
{
"channels": [
{
"channelIndex": 0,
"volume": -6.0,
"pan": 0.0,
"mute": false,
"solo": false,
"type": "Instrument",
"label": "Channel 1",
"hpEnabled": false,
"hpFrequency": 80.0,
"vuLeft": -12.5,
"vuRight": -10.2
}
],
"buses": [
{
"id": 1,
"name": "Master",
"type": "Master",
"volume": 0.0,
"mute": false
}
],
"routes": [
{
"sourceId": 1001,
"targetBusId": 1,
"level": 0.0,
"sourceType": "channel"
}
],
"outputRoutes": [
{
"sourceBusId": 1,
"sourceStartChannel": 0,
"targetStartChannel": 0,
"channels": 2
}
]
}
```
## REST API
### Mixer Daemon Endpoints (new)
| Endpoint | Method | Description |
|-------------------------------------------------------|--------|--------------------------------------|
| `/api/mixer/state` | GET | Get full mixer state |
| `/api/mixer/channel/{index}/volume` | POST | Set channel volume |
| `/api/mixer/channel/{index}/pan` | POST | Set channel pan |
| `/api/mixer/channel/{index}/mute` | POST | Set channel mute |
| `/api/mixer/channel/{index}/solo` | POST | Set channel solo |
| `/api/mixer/channel/add` | POST | Add a new channel |
| `/api/mixer/channel/{index}` | DELETE | Remove a channel |
| `/api/mixer/output-routes` | GET | Get output routing table |
| `/api/mixer/output-routes` | POST | Set output routing table |
| `/api/mixer/scenes` | GET | List all scenes |
| `/api/mixer/scenes` | POST | Save current state as scene |
| `/api/mixer/scenes/{id}/load` | POST | Load (recall) a scene |
| `/api/mixer/scenes/{id}` | DELETE | Delete a scene |
### PiPedal Endpoints (legacy)
| Endpoint | Method | Description |
|-------------------|--------|------------------------------------------------|
| `/api/ping` | GET | Health check + connection status |
| `/api/discover` | GET | Discover OPLabs plugin instances from PiPedal |
| `/api/plugins` | GET | List all available plugins from PiPedal |
| `/api/scenes` | GET | List saved scenes (legacy file-based) |
| `/api/scenes` | POST | Create new scene |
| `/api/scenes/{id}`| PUT | Update scene |
| `/api/scenes/{id}`| DELETE | Delete scene |
| `/api/scenes/{id}/recall` | POST | Recall scene via PiPedal |
## WebSocket Endpoints
| Endpoint | Description |
|-----------------|-----------------------------------------------------|
| `/ws/pipedal` | Proxy WebSocket: browser <-> FastAPI <-> PiPedal |
| `/ws/mixer` | Direct mixer daemon control (JSON method calls) |
### `/ws/mixer` Protocol
The mixer WebSocket accepts JSON messages in the same format as the IPC protocol:
```json
// Request
{"method": "getState", "id": 1}
{"method": "setChannelVolume", "params": {"channelIndex": 0, "volumeDb": -6.0}, "id": 2}
// Response
{"id": 1, "result": {...}, "error": null}
{"id": 2, "result": {}, "error": null}
```
## Project Structure
```
~/projects/oplabs-mixer-app/
├── backend/
│ ├── app.py # FastAPI app (mixer + legacy PiPedal routes)
│ ├── mixer_ipc.py # Unix socket IPC client for Mixer Daemon
│ ├── ws_proxy.py # PiPedal WebSocket proxy
│ └── requirements.txt
├── daemon/
│ ├── CMakeLists.txt # Build configuration
│ ├── MixerIpcServer.hpp # C++ Unix socket IPC server
│ ├── MixerIpcServer.cpp # IPC server implementation
│ ├── MixerDaemon.cpp # Main entry point
│ └── oplabs-mixer-daemon.service.in # Systemd service template
├── frontend/
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ └── src/
├── README.md
└── start.sh
```