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.
This commit is contained in:
@@ -1,24 +1,41 @@
|
||||
# OPLabs Mixer App
|
||||
|
||||
A standalone mixer application that connects to PiPedal (at `192.168.0.245:8080`) via WebSocket to discover and display OPLabsBandChannel and OPLabsBandBus LV2 plugin instances loaded on the pedalboard.
|
||||
A standalone digital mixer application with a C++ real-time audio engine,
|
||||
Python/FastAPI backend, and React frontend.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (React SPA) ←→ FastAPI Backend (localhost:8081) ←→ PiPedal (192.168.0.245:8080)
|
||||
│ │
|
||||
└── WS /ws/pipedal └── WS /pipedal (upstream)
|
||||
└── REST /api/discover └── REST proxied via WS
|
||||
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)
|
||||
```
|
||||
|
||||
- **Backend:** FastAPI on port 8081 — serves the SPA, proxies WebSocket connections to PiPedal, and provides a REST API for plugin discovery.
|
||||
- **Frontend:** Vite + React + TypeScript — connects to the local WS proxy and shows discovered OPLabs plugin instances with their parameter values.
|
||||
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+
|
||||
- PiPedal running at `192.168.0.245:8080` with OPLabs plugin instances loaded on the pedalboard
|
||||
- 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
|
||||
|
||||
@@ -36,92 +53,249 @@ pip install -r requirements.txt
|
||||
```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
|
||||
|
||||
### Development Mode
|
||||
|
||||
Start both backend and frontend:
|
||||
### 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
|
||||
# Terminal 1: Backend
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python app.py # runs on port 8081
|
||||
|
||||
# Terminal 2: Frontend (with hot-reload)
|
||||
cd frontend
|
||||
npm run dev # runs on port 5173, proxies API/WS to 8081
|
||||
python app.py # runs on port 8081
|
||||
```
|
||||
|
||||
Then open http://localhost:5173 in your browser.
|
||||
|
||||
### Production Mode
|
||||
|
||||
Build the frontend and serve from the backend:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
|
||||
# Then start the backend (it will serve the built SPA):
|
||||
cd ../backend
|
||||
source venv/bin/activate
|
||||
python app.py
|
||||
|
||||
# Open http://localhost:8081
|
||||
```
|
||||
Open http://localhost:8081 in your browser.
|
||||
|
||||
Or use the convenience script:
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
## WebSocket Protocol
|
||||
## IPC Protocol (C++ Mixer Daemon <-> Python FastAPI)
|
||||
|
||||
The app uses PiPedal's native WebSocket protocol:
|
||||
### Transport
|
||||
|
||||
- Messages are JSON arrays: `[{"message": "msgName", "replyTo": N}, optionalBody]`
|
||||
- Replies: `[{"reply": N, "message": "msgName"}, optionalBody]`
|
||||
- The browser connects to `ws://localhost:8081/ws/pipedal`, which proxies to PiPedal's `ws://192.168.0.245:8080/pipedal`
|
||||
- **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
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/ping` | GET | Health check + connection status |
|
||||
| `/api/discover` | GET | Discover OPLabs plugin instances from current pedalboard |
|
||||
| `/api/plugins` | GET | List all available plugins from PiPedal |
|
||||
### Mixer Daemon Endpoints (new)
|
||||
|
||||
## OPLabs Plugin URIs
|
||||
| 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 |
|
||||
|
||||
- OPLabsBandChannel: `http://ourpad.casa/plugins/oplabs-band-channel`
|
||||
- OPLabsBandBus: `http://ourpad.casa/plugins/oplabs-band-bus#`
|
||||
### 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 (routes, SPA serving, WS proxy)
|
||||
│ ├── ws_proxy.py # PiPedal WebSocket proxy client
|
||||
│ ├── 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/
|
||||
│ ├── main.tsx
|
||||
│ ├── App.tsx
|
||||
│ ├── App.css
|
||||
│ ├── hooks/
|
||||
│ │ └── usePiPedalWS.ts
|
||||
│ └── components/
|
||||
│ ├── ConnectionStatus.tsx
|
||||
│ └── PluginList.tsx
|
||||
├── README.md
|
||||
└── start.sh
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user