d19e0ea7a8
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.
302 lines
10 KiB
Markdown
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
|
|
```
|