# 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 ```