diff --git a/.hermes/plans/2026-06-20_204500-band-in-a-box-digital-mixer.md b/.hermes/plans/2026-06-20_204500-band-in-a-box-digital-mixer.md new file mode 100644 index 0000000..17ad1dd --- /dev/null +++ b/.hermes/plans/2026-06-20_204500-band-in-a-box-digital-mixer.md @@ -0,0 +1,1325 @@ +# Band-in-a-Box Digital Mixer / Processor Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Evolve OP-Pedal (PiPedal fork) from a guitar multi-FX pedal into a "Band-in-a-Box" digital mixer/processor — think Neural Cortex × small-format digital mixer — capable of running NAM core, LV2, and VST3 plugins with flexible routing for guitar, backing tracks, and vocals on a Raspberry Pi 4B + Valeton GP5 USB audio interface. + +**Architecture:** Extend op-pedal's existing C++ audio engine (JACK/ALSA) from a serial pedalboard chain into a channel-strip mixer architecture. The GP5 provides stereo USB I/O (guitar in on CH1, aux/backing on CH2). Each logical source gets its own FX chain (NAM amp, cab sim, pedals, vocal chain), aux sends, pan, and fader. Mix buses (FX returns, master) provide flexible routing for parallel processing chains. The existing React/TypeScript web UI gets a mixer surface alongside the pedalboard view. Scene recall lets you switch between guitar solo, backing-track-with-guitar, and vocal chain configurations. + +**Tech Stack:** +- **Backend:** C++20, JACK2 / ALSA (existing), Lilv (LV2), Steinberg VST3 SDK, NAM Core (subprocess) +- **Frontend:** React 18, TypeScript, Vite, Material UI (existing) +- **Audio Interface:** Valeton GP5 (prototype) → Behringer XR18/Focusrite 18i20 (production) +- **Hardware target:** Raspberry Pi 4B (prototype) → small x86 PC/NUC (production — already own desktop-oplabs, or ourpad N150) +- **Repo:** `gitea.ourpad.casa/shawn/op-pedal` (fork of PiPedal v2.0.107) +- **Scalability:** Design for 16+ channel mixing from day one. Phase 1 prototypes on Pi 4B + GP5 stereo. All subsequent phases target full multi-channel with PipeWire. The mixer engine, routing, and UI all assume 8+ channels — the Pi prototype just tests the core at low channel count. + +**Phases:** +1. Multi-channel audio foundation (PipeWire + multi-channel driver) +2. Channel strip engine (per-input FX chain, volume, pan, sends) +3. Mix bus core (subgroups, aux sends, master bus, audio graph) +4. Mixer UI (faders, meters, routing matrix, channel strip controls) +5. Integration (scenes, MIDI control, low-latency tuning, deployment) + +--- + +## Phase 1: Multi-Channel Audio Foundation + +### Task 1.1: Extend PipeWire driver for full duplex multi-channel I/O + +**Objective:** Create a PipeWire-based audio driver that supports 8+ input channels and 8+ output channels with JACK compatibility. + +**Files:** +- Create: `src/PipeWireDriver.hpp` +- Create: `src/PipeWireDriver.cpp` +- Modify: `src/CMakeLists.txt` (add PipeWire driver target) +- Modify: `src/AudioDriver.hpp` (add PipeWire driver factory) +- Modify: `cmake/FindPipeWire.cmake` (ensure proper linkage) + +**Step 1: Research PipeWire API patterns** + +Review existing `PipewireInputStream.hpp/cpp` — it only handles input capture. We need a full duplex driver. + +PipeWire JACK compatibility means we can also run existing JACK clients via `pw-jack` — but a native PipeWire driver avoids the translation layer. + +**Step 2: Write PipeWireDriver header** + +```cpp +// src/PipeWireDriver.hpp +#pragma once +#include "AudioDriver.hpp" +#include +#include +#include +#include +#include +#include + +namespace pipedal { + +class PipeWireDriver : public AudioDriver { +public: + PipeWireDriver(AudioDriverHost* host); + ~PipeWireDriver(); + + // AudioDriver interface + float CpuUse() override; + float CpuOverhead() override; + uint32_t GetSampleRate() override; + + size_t GetMidiInputEventCount() override; + MidiEvent* GetMidiEvents() override; + + const ChannelSelection& GetChannelSelection() const override; + + std::vector& DeviceInputBuffers() override; + size_t DeviceInputBufferCount() const override; + float* GetDeviceInputBuffer(size_t channel) const override; + + std::vector& DeviceOutputBuffers() override; + size_t DeviceOutputBufferCount() const override; + float* GetDeviceOutputBuffer(size_t channel) const override; + + std::vector& MainInputBuffers() override; + size_t MainInputBufferCount() const override; + float* GetMainInputBuffer(size_t channel) override; + + std::vector& MainOutputBuffers() override; + size_t MainOutputBufferCount() const override; + float* GetMainOutputBuffer(size_t channel) override; + + std::vector& AuxInputBuffers() override; + std::vector& AuxOutputBuffers() override; + size_t AuxInputBufferCount() const override; + float* GetAuxInputBuffer(size_t channel) override; + size_t AuxOutputBufferCount() const override; + float* GetAuxOutputBuffer(size_t channel) override; + + float* GetZeroInputBuffer() override; + float* GetDiscardOutputBuffer() override; + + void Open(const JackServerSettings& settings, const ChannelSelection& selection) override; + void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override; + void Activate() override; + void Deactivate() override; + void Close() override; + std::string GetConfigurationDescription() override; + +private: + struct Impl; + std::unique_ptr pImpl; +}; + +} // namespace pipedal +``` + +**Step 3: Implement PipeWireDriver** (core PipeWire stream setup) + +Key implementation points: +- `pw_stream_connect()` with `PW_DIRECTION_INPUT` and `PW_DIRECTION_OUTPUT` for full duplex +- Create `n` input and `n` output ports based on `ChannelSelection` +- Process callback copies data from PW buffers to internal buffers +- Support dynamically querying available device channels + +**Step 4: Integrate into build system** + +Add to `src/CMakeLists.txt`: +```cmake +find_package(PipeWire REQUIRED) +target_link_libraries(op-pedal PRIVATE PipeWire::libpipewire) +``` + +**Step 5: Verify build** + +```bash +cd /home/oplabs/projects/op-pedal && mkdir -p build && cd build && cmake .. -DPIPEDAL_EXCLUDE_TESTS=ON && make -j$(nproc) 2>&1 | tail -20 +``` +Expected: Compiles without errors. + +**Step 6: Commit** + +```bash +git add src/PipeWireDriver.hpp src/PipeWireDriver.cpp src/CMakeLists.txt cmake/FindPipeWire.cmake +git commit -m "feat: add full-duplex PipeWire audio driver for multi-channel I/O" +``` + +--- + +### Task 1.2: Extend ChannelRouterSettings for 8+ channels + +**Objective:** Update the channel routing model to support up to 16 input and 16 output channels instead of the current 2-channel model. + +**Files:** +- Modify: `src/ChannelRouterSettings.hpp` +- Modify: `src/ChannelRouterSettings.cpp` +- Modify: `src/Pedalboard.hpp` (add new control IDs for per-channel) + +**Step 1: Modify ChannelRouterSettings** + +Change default vectors from 2 elements to `n` elements based on device capabilities. + +```cpp +// In ChannelRouterSettings.hpp +// Change defaults to support up to 16 channels +std::vector mainInputChannels_ = {0, 1, 2, 3, 4, 5, 6, 7}; +std::vector mainOutputChannels_ = {0, 1, 2, 3, 4, 5, 6, 7}; +``` + +Add a method to resize based on device: +```cpp +void ResizeToChannelCount(uint64_t inputChannels, uint64_t outputChannels); +``` + +**Step 2: Add auto-routing preset generation** + +Create presets for common configurations: +- 8x8 straight routing (1→1, 2→2, ..., 8→8) +- 8x2 summing (all inputs → L/R) +- 2x8 (stereo input to 8 outputs for monitoring) + +**Step 3: Validate tests compile** + +```bash +cd build && make -j$(nproc) 2>&1 | tail -10 +``` + +**Step 4: Commit** + +```bash +git add src/ChannelRouterSettings.hpp src/ChannelRouterSettings.cpp +git commit -m "feat: extend channel routing to support 8+ channel configurations" +``` + +--- + +### Task 1.3: Install and configure PipeWire on the target PC + +**Objective:** Set up PipeWire with JACK compatibility and low-latency tuning on the target small PC. + +**Files:** +- Create: `scripts/setup-pipewire.sh` + +**Step 1: Write PipeWire setup script** + +```bash +#!/bin/bash +# scripts/setup-pipewire.sh - Install and configure PipeWire for pro audio + +set -euo pipefail + +# Install PipeWire and JACK compat layer +sudo apt update +sudo apt install -y pipewire pipewire-pulse pipewire-audio-client-libraries \ + libspa-0.2-jack libspa-0.2-bluetooth wireplumber + +# Install pro-audio tools +sudo apt install -y jackd2 jack-tools qjackctl carla \ + lv2-dev lilv-utils + +# Configure PipeWire for low latency +mkdir -p ~/.config/pipewire/ +cp /usr/share/pipewire/pipewire.conf ~/.config/pipewire/ || true +patch ~/.config/pipewire/pipewire.conf << 'PATCH' +--- a/pipewire.conf ++++ b/pipewire.conf +@@ -1,3 +1,10 @@ ++default.clock.rate = 48000 ++default.clock.min-quantum = 64 ++default.clock.max-quantum = 2048 ++default.clock.quantum = 256 ++ ++settings.check-quantum = false ++settings.check-rate = false + PATCH + +# Replace JACK server with PipeWire JACK compat +sudo apt install -y pipewire-jack +sudo apt remove -y jackd2 || true # Keep only for headers/libs + +# Start services +systemctl --user enable --now pipewire pipewire-pulse wireplumber + +echo "PipeWire installed. Verify with: pw-cli info" +``` + +**Step 2: Run the script on target** + +```bash +# On the target PC (could be desktop-oplabs or dedicated machine) +scp scripts/setup-pipewire.sh target-pc:~ +ssh target-pc bash setup-pipewire.sh +``` + +**Step 3: Verify PipeWire operation** + +```bash +pw-cli list-objects | grep -i node +# Expected: Shows audio nodes for the USB audio interface +``` + +**Step 4: Commit** + +```bash +git add scripts/setup-pipewire.sh +git commit -m "chore: add PipeWire setup script for pro audio" +``` + +--- + +## Phase 2: Channel Strip Engine + +### Task 2.1: Create ChannelStrip model class + +**Objective:** Design and implement a `ChannelStrip` C++ class that wraps a mini-pedalboard per input channel with volume, pan, mute, solo, and aux sends. + +**Files:** +- Create: `src/ChannelStrip.hpp` +- Create: `src/ChannelStrip.cpp` +- Modify: `src/CMakeLists.txt` + +**Step 1: Write ChannelStrip header** + +```cpp +// src/ChannelStrip.hpp +#pragma once +#include "Pedalboard.hpp" +#include "Lv2Pedalboard.hpp" +#include "IEffect.hpp" +#include +#include +#include + +namespace pipedal { + +struct AuxSendConfig { + float level = -inf; // -inf = no send + bool preFader = true; + int busIndex = 0; +}; + +enum class ChannelType { + Instrument, // guitar, bass, keys + Mic, // vocals + Line, // aux line input +}; + +class ChannelStrip { +public: + ChannelStrip(int channelIndex); + ~ChannelStrip(); + + // Unique ID for this channel + int64_t channelId() const { return channelId_; } + + // Channel identity + int channelIndex() const { return channelIndex_; } + void setChannelType(ChannelType type); + ChannelType channelType() const { return channelType_; } + + // Channel metadata + std::string label() const { return label_; } + void setLabel(const std::string& label); + + // Channel strip controls + float volume() const { return volume_; } + void setVolume(float db); // -inf to +12dB + float pan() const { return pan_; } + void setPan(float pan); // -1.0 (L) to +1.0 (R) + bool mute() const { return mute_; } + void setMute(bool mute); + bool solo() const { return solo_; } + void setSolo(bool solo); + + // Input processing + bool hasHighPassFilter() const { return hpEnabled_; } + void setHighPassFilter(bool enabled, float freq = 80.0f); + float highPassFilterFreq() const { return hpFrequency_; } + + // FX chain — each channel has its own pedalboard + Pedalboard& fxChain() { return fxChain_; } + const Pedalboard& fxChain() const { return fxChain_; } + Lv2Pedalboard* fxProcessor() { return fxProcessor_.get(); } + void prepareFx(IHost* pHost); + void processFx(float** input, float** output, uint32_t frames, + RealtimeRingBufferWriter* writer); + + // Aux sends + void setAuxSend(int auxIndex, const AuxSendConfig& config); + const AuxSendConfig& auxSend(int auxIndex) const; + float calculateAuxSendLevel(int auxIndex) const; + + // State + struct State { + int64_t channelId; + float volume; + float pan; + bool mute; + bool solo; + ChannelType channelType; + std::string label; + bool hpEnabled; + float hpFrequency; + std::vector auxSends; + Pedalboard fxChain; + }; + State captureState() const; + void applyState(const State& state); + +private: + int64_t channelId_; + int channelIndex_; + ChannelType channelType_ = ChannelType::Instrument; + std::string label_; + + // Controls + float volume_ = 0.0f; // dB + float pan_ = 0.0f; + bool mute_ = false; + bool solo_ = false; + + // Input processing + bool hpEnabled_ = false; + float hpFrequency_ = 80.0f; + + // FX chain + Pedalboard fxChain_; + std::unique_ptr fxProcessor_; + + // Aux sends (pre/post fader) + std::vector auxSends_; + + static std::atomic nextChannelId; +}; + +} // namespace pipedal +``` + +**Step 2: Implement ChannelStrip** + +Key implementation details: +- `fxProcessor_` is a mini `Lv2Pedalboard` that processes this channel's audio +- `calculateAuxSendLevel()` returns the post-volume (or pre-volume if preFader) level for the aux bus +- Mute mutes the output; solo sets up solo bus routing +- HPF is a simple biquad filter (can use LV2 filter plugin as the first insert) + +**Step 3: Add to build** + +Add to `src/CMakeLists.txt`: +```cmake +target_sources(op-pedal PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/ChannelStrip.cpp +) +``` + +**Step 4: Verify build** + +```bash +cd build && make -j$(nproc) 2>&1 | tail -10 +``` + +**Step 5: Commit** + +```bash +git add src/ChannelStrip.hpp src/ChannelStrip.cpp src/CMakeLists.txt +git commit -m "feat: add ChannelStrip model with per-channel FX chain, volume, pan, aux sends" +``` + +--- + +### Task 2.2: Implement MixerBus and MixerEngine + +**Objective:** Create the mixer bus architecture supporting subgroups, aux sends, FX returns, and a master bus. The MixerEngine orchestrates all channels and buses for the real-time audio processing graph. + +**Files:** +- Create: `src/MixerBus.hpp` +- Create: `src/MixerBus.cpp` +- Create: `src/MixerEngine.hpp` +- Create: `src/MixerEngine.cpp` + +**Step 1: Write MixerBus header** + +```cpp +// src/MixerBus.hpp +#pragma once +#include +#include +#include +#include + +namespace pipedal { + +enum class BusType { + Master, // Main L/R output + Subgroup, // Named subgroup (Drums, Guitars, Vocals...) + Aux, // Aux send bus (monitor mix or FX send) + FxReturn, // Return from FX processor (reverb, delay) +}; + +class MixerBus { +public: + MixerBus(int64_t id, BusType type, const std::string& name, int channels = 2); + + int64_t id() const { return id_; } + BusType type() const { return type_; } + std::string name() const { return name_; } + void setName(const std::string& name); + + // Bus controls + float volume() const { return volume_; } + void setVolume(float db); + float pan() const { return pan_; } + void setPan(float pan); + bool mute() const { return mute_; } + void setMute(bool mute); + + // Audio processing + int channelCount() const { return channelCount_; } + float* buffer(int channel) { return buffers_[channel].data(); } + const float* buffer(int channel) const { return buffers_[channel].data(); } + void allocateBuffers(size_t maxFrames); + + // Accumulate (add) a channel's contribution to this bus + void accumulate(const float** source, uint32_t frames, float gain, int sourceChannels); + + // Clear bus buffers + void clear(); + + // Apply bus processing (master bus effects, etc.) + void process(uint32_t frames); + +private: + int64_t id_; + BusType type_; + std::string name_; + int channelCount_; + float volume_ = 0.0f; // dB + float pan_ = 0.0f; + bool mute_ = false; + std::vector> buffers_; // [channel][sample] + + static std::atomic nextBusId; +}; + +} // namespace pipedal +``` + +**Step 2: Write MixerEngine header** + +```cpp +// src/MixerEngine.hpp +#pragma once +#include "ChannelStrip.hpp" +#include "MixerBus.hpp" +#include "AudioDriver.hpp" +#include +#include +#include + +namespace pipedal { + +class MixerEngine { +public: + MixerEngine(); + ~MixerEngine(); + + // Channel management + int addChannel(int physicalInputIndex); + void removeChannel(int channelIndex); + ChannelStrip* getChannel(int channelIndex); + const ChannelStrip* getChannel(int channelIndex) const; + size_t channelCount() const { return channels_.size(); } + + // Bus management + int64_t addBus(BusType type, const std::string& name, int channels = 2); + void removeBus(int64_t busId); + MixerBus* getBus(int64_t busId); + MixerBus* masterBus() { return masterBus_; } + + // Routing + void routeChannelToBus(int channelIndex, int64_t busId, float level); + void routeChannelToAux(int channelIndex, int auxBusIndex); + void routeBusToBus(int64_t sourceBusId, int64_t targetBusId); + + // Audio processing (called from audio thread) + void setSampleRate(uint32_t sampleRate); + void setMaxBufferSize(size_t frames); + + // Process one audio cycle + void process(float** deviceInputs, uint32_t inputChannels, + float** deviceOutputs, uint32_t outputChannels, + uint32_t frames, + RealtimeRingBufferWriter* writer); + + // Solo management + bool anySoloActive() const; + bool isChannelAudible(int channelIndex) const; + + // State serialization + // TODO: JSON save/load for scene management + + void Prepare(IHost* pHost, Lv2PedalboardErrorList& errorList); + +private: + std::vector> channels_; + std::map> buses_; + MixerBus* masterBus_ = nullptr; + + uint32_t sampleRate_ = 48000; + size_t maxBufferSize_ = 512; + + // Channel → bus routing matrix + struct ChannelRoute { + int channelIndex; + int64_t busId; + float level; + }; + std::vector channelRoutes_; + + // Bus → bus routing + struct BusRoute { + int64_t sourceBusId; + int64_t targetBusId; + }; + std::vector busRoutes_; +}; + +} // namespace pipedal +``` + +**Step 3: Implement the process loop** + +The audio thread process method: +1. For each channel strip that is audible: + a. Copy device input buffer to channel input buffer + b. Run channel HPF + c. Run channel FX chain (the mini-pedalboard) + d. Calculate aux sends (accumulate to aux buses) + e. Apply pan law, multiply by volume * (mute ? 0 : 1) * (solo membership) + f. Accumulate to assigned subgroup or master bus +2. Process each bus (apply bus volume) +3. Route buses to buses according to routing matrix +4. Copy master bus to device outputs + +**Step 4: Verify build** + +```bash +cd build && make -j$(nproc) 2>&1 | tail -10 +``` + +**Step 5: Commit** + +```bash +git add src/MixerBus.hpp src/MixerBus.cpp src/MixerEngine.hpp src/MixerEngine.cpp +git commit -m "feat: add MixerBus and MixerEngine for mixing console architecture" +``` + +--- + +### Task 2.3: Integrate MixerEngine into PluginHost / main audio pipeline + +**Objective:** Replace the existing single-pedalboard processing in PluginHost with the new MixerEngine, so that audio flows through channel strips and buses instead of a single serial chain. + +**Files:** +- Modify: `src/PluginHost.hpp` +- Modify: `src/PluginHost.cpp` +- Modify: `src/Lv2Pedalboard.hpp` (keep but use per-channel) + +**Step 1: Add MixerEngine to PluginHost** + +In `PluginHost.hpp`: +```cpp +class MixerEngine; // forward decl + +class PluginHost : public IHost, public AudioDriverHost { + // ... existing members ... + + // New mixer integration + MixerEngine* mixerEngine() { return mixerEngine_.get(); } + void setMixerEngine(std::unique_ptr engine); + + // Override audio processing to use mixer + void OnProcess(size_t nFrames) override; + +private: + std::unique_ptr mixerEngine_; +}; +``` + +**Step 2: Modify OnProcess** + +In `PluginHost::OnProcess`: +```cpp +void PluginHost::OnProcess(size_t nFrames) { + // Old: just Lv2Pedalboard::Run() + // New: + if (mixerEngine_) { + auto* driver = GetAudioDriver(); + mixerEngine_->process( + driver->DeviceInputBuffers().data(), + driver->DeviceInputBufferCount(), + driver->DeviceOutputBuffers().data(), + driver->DeviceOutputBufferCount(), + nFrames, + GetRingBufferWriter() + ); + } else { + // Fallback to legacy single-pedalboard mode + if (lv2Pedalboard_) { + lv2Pedalboard_->Run( + driver->MainInputBuffers().data(), + driver->MainOutputBuffers().data(), + nFrames, + GetRingBufferWriter() + ); + } + } +} +``` + +**Step 3: Create mixer initialization path** + +Add a configuration option to switch between `legacy` (single pedalboard) and `mixer` (channel strip) modes. + +**Step 4: Verify build** + +```bash +cd build && make -j$(nproc) 2>&1 | tail -10 +``` + +**Step 5: Commit** + +```bash +git add src/PluginHost.hpp src/PluginHost.cpp +git commit -m "feat: integrate MixerEngine into PluginHost audio pipeline" +``` + +--- + +## Phase 3: Mixer WebSocket API + +### Task 3.1: Add mixer state WebSocket endpoints + +**Objective:** Extend the WebSocket API to expose mixer state (channels, buses, routing) and receive control changes. This is how the web UI will talk to the mixer engine. + +**Files:** +- Modify: `src/WebServer.hpp` +- Modify: `src/WebServer.cpp` + +**Step 1: Define WebSocket messages** + +New message types for mixer operations: + +```json +// Client → Server (control changes) +{ + "type": "mixer:setChannelVolume", + "channelIndex": 0, + "volume": -6.0 +} +{ + "type": "mixer:setChannelPan", + "channelIndex": 0, + "pan": -0.5 +} +{ + "type": "mixer:setChannelMute", + "channelIndex": 0, + "mute": true +} +{ + "type": "mixer:setChannelSolo", + "channelIndex": 0, + "solo": true +} +{ + "type": "mixer:setChannelType", + "channelIndex": 0, + "type": "Mic" +} +{ + "type": "mixer:setChannelLabel", + "channelIndex": 0, + "label": "Shawn's Guitar" +} +{ + "type": "mixer:setBusVolume", + "busId": 1, + "volume": -3.0 +} + +// Server → Client (state updates) +{ + "type": "mixer:stateUpdate", + "channels": [ + { + "channelIndex": 0, + "label": "Guitar", + "type": "Instrument", + "volume": -6.0, + "pan": -0.3, + "mute": false, + "solo": false, + "hpEnabled": true, + "hpFrequency": 80.0, + "auxSends": [ + {"busIndex": 0, "level": -12.0, "preFader": false} + ] + } + ], + "buses": [ + { + "id": 1, + "name": "Master", + "type": "Master", + "volume": 0.0, + "mute": false + } + ] +} +``` + +**Step 2: Implement message handlers in WebServer** + +Add handlers similar to existing pedalboard message routing: +- Parse mixer messages in the `OnMessage` dispatcher +- Route to `MixerEngine` methods +- Broadcast state updates to all connected clients + +**Step 3: Backwards compatibility** + +Keep all existing pedalboard messages working when in legacy mode. When mixer mode is active, the pedalboard per-channel messages are still handled for the channel's mini-pedalboard. + +**Step 4: Verify build** + +```bash +cd build && make -j$(nproc) 2>&1 | tail -10 +``` + +**Step 5: Commit** + +```bash +git add src/WebServer.hpp src/WebServer.cpp +git commit -m "feat: add mixer WebSocket API endpoints for channel/bus control" +``` + +--- + +### Task 3.2: Implement mixer configuration endpoint + +**Objective:** Add REST endpoints to save/load mixer scenes, and to query available mixer state. + +**Files:** +- Modify: `src/WebServer.cpp` +- Modify: `src/WebServer.hpp` + +**Step 1: REST API endpoints** + +``` +GET /api/mixer/state → Full mixer state (channels, buses, routing) +POST /api/mixer/state → Load mixer state +POST /api/mixer/reset → Reset to default configuration + +GET /api/mixer/scenes → List saved scenes +POST /api/mixer/scenes/{id} → Save current state as scene +PUT /api/mixer/scenes/{id} → Load scene +DELETE /api/mixer/scenes/{id} → Delete scene +``` + +**Step 2: Scene storage** + +Store scenes as JSON files in `~/.config/pipedal/scenes/` following the existing config file pattern. + +**Step 3: Commit** + +```bash +git add src/WebServer.hpp src/WebServer.cpp +git commit -m "feat: add mixer REST API endpoints for state and scenes" +``` + +--- + +## Phase 4: Mixer Web UI + +### Task 4.1: Create mixer page shell with routing + +**Objective:** Add a new "Mixer" page to the Vite React app alongside existing views, with navigation between pedalboard and mixer modes. + +**Files:** +- Create: `vite/src/pipedal/MixerPage.tsx` +- Create: `vite/src/pipedal/MixerPage.css` (or Emotion styles) +- Modify: `vite/src/pipedal/MainPage.tsx` (add mixer toggle/route) +- Modify: `vite/src/pipedal/AppThemed.tsx` (add mixer route) + +**Step 1: Add navigation** + +In `MainPage.tsx`, add a toggle between "Pedalboard" and "Mixer" views (top toolbar). + +```tsx +// Navigation toggle + setViewMode('mixer')}> + {/* or EQ-like mixer icon */} + + setViewMode('pedalboard')}> + + +``` + +**Step 2: Create MixerPage shell** + +```tsx +// vite/src/pipedal/MixerPage.tsx +import React, { useState, useEffect } from 'react'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import ChannelStripView from './ChannelStripView'; +import BusStripView from './BusStripView'; +import MixerMasterSection from './MixerMasterSection'; + +interface MixerState { + channels: ChannelState[]; + buses: BusState[]; +} + +interface ChannelState { + channelIndex: number; + label: string; + type: 'Instrument' | 'Mic' | 'Line'; + volume: number; + pan: number; + mute: boolean; + solo: boolean; + hpEnabled: boolean; + hpFrequency: number; + auxSends: { busIndex: number; level: number; preFader: boolean }[]; +} + +interface BusState { + id: number; + name: string; + type: 'Master' | 'Subgroup' | 'Aux' | 'FxReturn'; + volume: number; + mute: boolean; +} + +export default function MixerPage() { + const [mixerState, setMixerState] = useState(null); + const model = PiPedalModelFactory.getInstance(); + + useEffect(() => { + // Subscribe to mixer state updates from WebSocket + const handler = (state: MixerState) => setMixerState(state); + model.on('mixer:stateUpdate', handler); + + // Request initial state + model.send({ type: 'mixer:getState' }); + + return () => model.off('mixer:stateUpdate', handler); + }, [model]); + + if (!mixerState) return
Loading mixer...
; + + return ( +
+
+ {mixerState.channels.map(ch => ( + + ))} + +
+
+ {mixerState.buses.map(bus => ( + + ))} +
+ b.type === 'Master')} /> +
+ ); +} +``` + +**Step 3: Commit** + +```bash +git add vite/src/pipedal/MixerPage.tsx vite/src/pipedal/AppThemed.tsx vite/src/pipedal/MainPage.tsx +git commit -m "feat: add MixerPage navigation and shell component" +``` + +--- + +### Task 4.2: Build ChannelStripView component + +**Objective:** Create a vertical channel strip UI component with fader, pan pot, mute/solo buttons, label, VU meter, and FX chain access. + +**Files:** +- Create: `vite/src/pipedal/ChannelStripView.tsx` +- Create: `vite/src/pipedal/VerticalSlider.tsx` (if needed for fader) +- Modify: `vite/src/pipedal/VuMeter.tsx` (reuse/adapt for vertical orientation) + +**Step 1: ChannelStripView layout** + +``` +┌─────────────┐ +│ VU Meter │ ← Vertical VU meter (reuse VuMeter) +│ ██ │ +│ ██ │ +├─────────────┤ +│ [FX Chain] │ ← Button to open pedalboard for this channel +├─────────────┤ +│ [Label] │ ← Editable label (e.g. "Guitar") +│─────────────│ +│ ○ │ ← Pan knob +│─────────────│ +│ ┌─────────┐ │ +│ │ │ │ +│ │ Fader │ │ ← Vertical volume fader +│ │ ─── │ │ +│ │ │ │ +│ └─────────┘ │ +│ -6.0 dB │ ← Numeric readout +│─────────────│ +│ [M] [S] │ ← Mute / Solo buttons +│─────────────│ +│ [Aux ♪] │ ← Aux sends expander +└─────────────┘ +``` + +**Step 2: Implement fader as a touch-friendly vertical slider** + +Use the existing zoomed-dial pattern but in vertical orientation: + +```tsx +// Simplified fader component +function ChannelFader({ value, onChange }: FaderProps) { + // Touch drag / mouse drag to adjust + // -inf (bottom) to +12 dB (top) + // Show numeric readout + // Color: green (nominal) → yellow (hot) → red (clipping) +} +``` + +**Step 3: VU meter reuse** + +The existing `VuMeter.tsx` component already exists — add a `vertical` prop and use it for channel strip metering. + +**Step 4: FX chain access** + +"FX" button opens the pedalboard view for this channel's mini-pedalboard. This reuses the existing `PedalboardView.tsx` component but scoped to the specific channel. + +**Step 5: Commit** + +```bash +git add vite/src/pipedal/ChannelStripView.tsx vite/src/pipedal/VerticalSlider.tsx +git commit -m "feat: add ChannelStripView component with fader, pan, VU, FX access" +``` + +--- + +### Task 4.3: Build MixerRoutingMatrix component + +**Objective:** Create a routing matrix view showing which channels route to which buses (subgroups, aux sends). + +**Files:** +- Create: `vite/src/pipedal/MixerRoutingMatrix.tsx` + +**Step 1: Routing matrix grid** + +Rows = channels, Columns = buses (subgroups, aux sends). Cells = toggle or level knob. + +``` + Bus1 Bus2 Aux1 Aux2 + ││ ││ ││ ││ +Channel 1 ── ── ██─ ██─ +Channel 2 ██─ ── ██─ ── +Channel 3 ── ██─ ── ██─ +Channel 4 ── ██─ ── ── +``` + +Each cell is a clickable slot: +- Empty = no route +- Level indicator = active route with drag-to-adjust level +- Bold = direct output route + +**Step 2: Commit** + +```bash +git add vite/src/pipedal/MixerRoutingMatrix.tsx +git commit -m "feat: add MixerRoutingMatrix component for channel-to-bus routing" +``` + +--- + +### Task 4.4: Build MixerMasterSection and BusStripView + +**Objective:** Create the master section with main L/R fader, bus view list, and output metering. + +**Files:** +- Create: `vite/src/pipedal/MixerMasterSection.tsx` +- Create: `vite/src/pipedal/BusStripView.tsx` + +**Step 1: Master section** + +- Stereo VU meters (L/R) +- Master volume fader +- Mono button / dim button +- Output configuration + +**Step 2: Bus strip** + +Simpler version of channel strip for buses/subgroups: +- Horizontal fader +- Label +- Mute +- AUX send level (if bus is a subgroup that feeds other buses) + +**Step 3: Commit** + +```bash +git add vite/src/pipedal/MixerMasterSection.tsx vite/src/pipedal/BusStripView.tsx +git commit -m "feat: add master section and bus strip components" +``` + +--- + +### Task 4.5: Wire up mixer WebSocket messages in the frontend model + +**Objective:** Add the WebSocket message passing for all mixer controls from UI to backend. + +**Files:** +- Modify: `vite/src/pipedal/PiPedalModel.ts` (or equivalent state management) + +**Step 1: Add mixer message handlers** + +```typescript +// In PiPedalModel or a new MixerModel class + +class MixerModel { + private ws: WebSocket; + + setChannelVolume(channelIndex: number, volume: number) { + this.send({ type: 'mixer:setChannelVolume', channelIndex, volume }); + } + + setChannelPan(channelIndex: number, pan: number) { + this.send({ type: 'mixer:setChannelPan', channelIndex, pan }); + } + + setChannelMute(channelIndex: number, mute: boolean) { + this.send({ type: 'mixer:setChannelMute', channelIndex, mute }); + } + + setChannelSolo(channelIndex: number, solo: boolean) { + this.send({ type: 'mixer:setChannelSolo', channelIndex, solo }); + } + + setChannelLabel(channelIndex: number, label: string) { + this.send({ type: 'mixer:setChannelLabel', channelIndex, label }); + } + + setChannelType(channelIndex: number, type: string) { + this.send({ type: 'mixer:setChannelType', channelIndex, type }); + } + + setBusVolume(busId: number, volume: number) { + this.send({ type: 'mixer:setBusVolume', busId, volume }); + } + + setBusMute(busId: number, mute: boolean) { + this.send({ type: 'mixer:setBusMute', busId, mute }); + } + + setAuxSend(channelIndex: number, busIndex: number, level: number, preFader: boolean) { + this.send({ type: 'mixer:setAuxSend', channelIndex, busIndex, level, preFader }); + } +} +``` + +**Step 2: Hook components to model** + +All mixer UI components call these methods on user interaction. + +**Step 3: Commit** + +```bash +git add vite/src/pipedal/PiPedalModel.ts +git commit -m "feat: wire mixer UI controls to WebSocket backend" +``` + +--- + +## Phase 5: Integration, Audio Interface, and Polish + +### Task 5.1: Acquire and integrate multi-channel audio interface + +**Step 1: Choose audio interface** + +Recommendations for multi-channel I/O with Linux PipeWire: + +| Device | I/O | Price | Notes | +|--------|-----|-------|-------| +| Behringer XR18 | 16in / 6out | ~$700 | 16 Midas preamps, digital mixer, can also run standalone | +| Focusrite Scarlett 18i20 (3rd gen) | 18in / 20out | ~$500 | Proven Linux compatibility, good preamps | +| Behringer UMC1820 | 18in / 8out | ~$300 | Budget option, ADAT expandable | +| Tascam US-16x08 | 16in / 8out | ~$400 | Good Linux support | + +**Step 2: Verify Linux compatibility** + +```bash +# Test device detection +lsusb | grep -i "audio\|focusrite\|behringer\|tascam" + +# Check PipeWire sees all channels +pw-dump | jq '.info.props["audio.channels"]' + +# Test all I/O with ALSA +arecord -l # Should list all input channels +aplay -l # Should list all output channels +``` + +**Step 3: Configure PipeWire for the interface** + +```bash +# Apply the setup from Task 1.3 +# Test round-trip latency +jack_delay # Or use pw-measure +``` + +**Step 4: Document** + +Write wiki page at `~/obsidian/wiki/band-in-a-box-audio-interface.md` with connection diagram. + +--- + +### Task 5.2: CPU profiling and RT optimization + +The main bottleneck: running multiple NAM instances (one per channel) + LV2/VST3 plugins simultaneously on a small PC. + +**Profile current CPU usage:** +```bash +# With single NAM model running +ps -C op-pedal -o %cpu,%mem + +# With PipeWire +pw-top # Shows DSP load per node +``` + +**Optimization targets:** +1. Process audio with the same buffer size for all channels (vectorize) +2. Use RT kernel (already available on Ubuntu) +3. Pin audio thread to a dedicated CPU core +4. Tune PipeWire quantum for lowest stable latency (start at 256 @ 48kHz = 5.3ms) + +**Task output:** Document `scripts/optimize-audio-pc.sh` with tuning parameters. + +--- + +### Task 5.3: Scene management UI + +**Objective:** Add scene save/load to the mixer UI (presets for the full mixer state). + +**Files:** +- Create: `vite/src/pipedal/SceneManagerDialog.tsx` +- Modify: `vite/src/pipedal/MixerPage.tsx` + +**Features:** +- Save current mixer state as a named scene +- Load a scene (restores all channel/bus settings) +- Quick snapshot (like existing pedalboard snapshots but for full mixer) +- Scene list with load/save/delete + +--- + +### Task 5.4: Headless / auto-start mode + +**Objective:** The mixer should start automatically with the last-used scene when the PC boots, no display required. + +**Files:** +- Modify: `src/main.cpp` (add headless mixer mode) +- Create: `debian/op-pedal-mixer.service` + +**Step 1: Headless service** + +Create a systemd service that starts op-pedal in mixer mode and loads the last scene. + +```ini +# /etc/systemd/system/op-pedal-mixer.service +[Unit] +Description=OP-Pedal Band-in-a-Box Mixer +After=pipewire.service wireplumber.service + +[Service] +Type=simple +User=oplabs +ExecStart=/usr/bin/op-pedal --mode mixer --load-last-scene +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target +``` + +**Step 2: Commit** + +```bash +git add debian/op-pedal-mixer.service +git commit -m "feat: add headless auto-start systemd service for mixer mode" +``` + +--- + +### Task 5.5: MIDI control surface mapping + +**Objective:** Allow MIDI controllers (like a Behringer X-Touch Mini or Mackie MCU) to control mixer faders, mutes, and solos. + +**Files:** +- Create: `src/MidiControlSurface.hpp` +- Create: `src/MidiControlSurface.cpp` + +**Mackie Control Universal (MCU) protocol** is the industry standard for DAW controllers. Implement MCU bindings: +- Channel faders → CC 7 (volume) per channel +- Mute buttons → CC 8 +- Transport controls +- LED feedback for mute/solo/record status + +**Alternative:** Simple MIDI learn mode where user taps a fader on the UI then moves a MIDI control to map it. + +--- + +## Verification Plan + +After each phase, verify with: + +1. **Build test:** `make -j$(nproc)` succeeds +2. **Backend test:** Start op-pedal, check WebSocket connects +3. **Frontend test:** `cd vite && npx tsc --noEmit` passes +4. **Audio test:** Play audio through the mixer, verify routing +5. **Latency test:** Measure round-trip latency with `jack_delay` +6. **Stress test:** Load 8 channels with NAM models + plugins per channel, check for xruns + +--- + +## Risks and Tradeoffs + +| Risk | Mitigation | +|------|-----------| +| **CPU overload** with multiple NAM instances | Profile aggressively; use simpler NAM A2 models (more efficient); limit channels per CPU core; allow "NAM-free" mode per channel | +| **Audio dropouts** with low buffer sizes | Start with 256 @ 48kHz, test incrementally; document minimum hardware | +| **Code complexity** mixing legacy pedalboard and new mixer | Keep both modes; legacy mode stays unchanged; mixer mode is opt-in | +| **WebSocket message volume** during fast fader moves | Rate-limit UI updates; batch changes every 30ms | +| **PipeWire vs JACK** compatibility | PipeWire can run JACK clients via `pw-jack`; existing JACK code remains as fallback | +| **React re-render performance** with 16+ channel strips | Virtualize visible channels; use `React.memo` on strip components | +| **Hardware dependency** — user must own multi-channel audio interface | Mixer mode also works with stereo interfaces (2 channels only) | +| **VST3 license concerns** | Only use with legally obtained VST3 plugins; the engine itself doesn't distribute them | + +--- + +## Open Questions + +1. **Target hardware:** Existing desktop-oplabs (Ubuntu VM), or a dedicated small PC (N150 mini PC on the Proxmox host)? +2. **Audio interface:** Do you already own one, or should the plan include budget for a Behringer/Focusrite? +3. **Channel count:** What's the realistic max channel count for your use case? (8? 16? 24?) +4. **NAM models:** Use the existing Tone3000 integration for model downloads, or also support local .nam files? +5. **Control surface:** Do you have a MIDI controller you want to use as a physical fader surface? +6. **Prioritization:** Which phase is most urgent — basic multi-channel working, or the full mixer UI? + +--- + +> **For Hermes:** Ready to execute using subagent-driven-development — dispatch a fresh subagent per task with two-stage review (spec compliance then code quality). Shall I proceed with Phase 1?