39 KiB
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:
- Multi-channel audio foundation (PipeWire + multi-channel driver)
- Channel strip engine (per-input FX chain, volume, pan, sends)
- Mix bus core (subgroups, aux sends, master bus, audio graph)
- Mixer UI (faders, meters, routing matrix, channel strip controls)
- 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
// src/PipeWireDriver.hpp
#pragma once
#include "AudioDriver.hpp"
#include <pipewire/pipewire.h>
#include <pipewire/stream.h>
#include <vector>
#include <memory>
#include <atomic>
#include <thread>
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<float*>& DeviceInputBuffers() override;
size_t DeviceInputBufferCount() const override;
float* GetDeviceInputBuffer(size_t channel) const override;
std::vector<float*>& DeviceOutputBuffers() override;
size_t DeviceOutputBufferCount() const override;
float* GetDeviceOutputBuffer(size_t channel) const override;
std::vector<float*>& MainInputBuffers() override;
size_t MainInputBufferCount() const override;
float* GetMainInputBuffer(size_t channel) override;
std::vector<float*>& MainOutputBuffers() override;
size_t MainOutputBufferCount() const override;
float* GetMainOutputBuffer(size_t channel) override;
std::vector<float*>& AuxInputBuffers() override;
std::vector<float*>& 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<Impl> pImpl;
};
} // namespace pipedal
Step 3: Implement PipeWireDriver (core PipeWire stream setup)
Key implementation points:
pw_stream_connect()withPW_DIRECTION_INPUTandPW_DIRECTION_OUTPUTfor full duplex- Create
ninput andnoutput ports based onChannelSelection - 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:
find_package(PipeWire REQUIRED)
target_link_libraries(op-pedal PRIVATE PipeWire::libpipewire)
Step 5: Verify build
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
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.
// In ChannelRouterSettings.hpp
// Change defaults to support up to 16 channels
std::vector<int64_t> mainInputChannels_ = {0, 1, 2, 3, 4, 5, 6, 7};
std::vector<int64_t> mainOutputChannels_ = {0, 1, 2, 3, 4, 5, 6, 7};
Add a method to resize based on device:
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
cd build && make -j$(nproc) 2>&1 | tail -10
Step 4: Commit
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
#!/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
# 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
pw-cli list-objects | grep -i node
# Expected: Shows audio nodes for the USB audio interface
Step 4: Commit
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
// src/ChannelStrip.hpp
#pragma once
#include "Pedalboard.hpp"
#include "Lv2Pedalboard.hpp"
#include "IEffect.hpp"
#include <memory>
#include <vector>
#include <functional>
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<AuxSendConfig> 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<Lv2Pedalboard> fxProcessor_;
// Aux sends (pre/post fader)
std::vector<AuxSendConfig> auxSends_;
static std::atomic<int64_t> nextChannelId;
};
} // namespace pipedal
Step 2: Implement ChannelStrip
Key implementation details:
fxProcessor_is a miniLv2Pedalboardthat processes this channel's audiocalculateAuxSendLevel()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:
target_sources(op-pedal PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ChannelStrip.cpp
)
Step 4: Verify build
cd build && make -j$(nproc) 2>&1 | tail -10
Step 5: Commit
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
// src/MixerBus.hpp
#pragma once
#include <vector>
#include <string>
#include <functional>
#include <memory>
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<std::vector<float>> buffers_; // [channel][sample]
static std::atomic<int64_t> nextBusId;
};
} // namespace pipedal
Step 2: Write MixerEngine header
// src/MixerEngine.hpp
#pragma once
#include "ChannelStrip.hpp"
#include "MixerBus.hpp"
#include "AudioDriver.hpp"
#include <memory>
#include <vector>
#include <map>
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<std::unique_ptr<ChannelStrip>> channels_;
std::map<int64_t, std::unique_ptr<MixerBus>> 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<ChannelRoute> channelRoutes_;
// Bus → bus routing
struct BusRoute {
int64_t sourceBusId;
int64_t targetBusId;
};
std::vector<BusRoute> busRoutes_;
};
} // namespace pipedal
Step 3: Implement the process loop
The audio thread process method:
- 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
- Process each bus (apply bus volume)
- Route buses to buses according to routing matrix
- Copy master bus to device outputs
Step 4: Verify build
cd build && make -j$(nproc) 2>&1 | tail -10
Step 5: Commit
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:
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<MixerEngine> engine);
// Override audio processing to use mixer
void OnProcess(size_t nFrames) override;
private:
std::unique_ptr<MixerEngine> mixerEngine_;
};
Step 2: Modify OnProcess
In PluginHost::OnProcess:
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
cd build && make -j$(nproc) 2>&1 | tail -10
Step 5: Commit
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:
// 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
OnMessagedispatcher - Route to
MixerEnginemethods - 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
cd build && make -j$(nproc) 2>&1 | tail -10
Step 5: Commit
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
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).
// Navigation toggle
<IconButtonEx onClick={() => setViewMode('mixer')}>
<MixerIcon /> {/* or EQ-like mixer icon */}
</IconButtonEx>
<IconButtonEx onClick={() => setViewMode('pedalboard')}>
<PedalboardIcon />
</IconButtonEx>
Step 2: Create MixerPage shell
// 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<MixerState | null>(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 <div>Loading mixer...</div>;
return (
<div className="mixer-page">
<div className="mixer-channels">
{mixerState.channels.map(ch => (
<ChannelStripView key={ch.channelIndex} channel={ch} />
))}
<AddChannelButton />
</div>
<div className="mixer-buses">
{mixerState.buses.map(bus => (
<BusStripView key={bus.id} bus={bus} />
))}
</div>
<MixerMasterSection masterBus={mixerState.buses.find(b => b.type === 'Master')} />
</div>
);
}
Step 3: Commit
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:
// 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
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
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
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
// 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
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
# 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
# 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:
# With single NAM model running
ps -C op-pedal -o %cpu,%mem
# With PipeWire
pw-top # Shows DSP load per node
Optimization targets:
- Process audio with the same buffer size for all channels (vectorize)
- Use RT kernel (already available on Ubuntu)
- Pin audio thread to a dedicated CPU core
- 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.
# /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
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:
- Build test:
make -j$(nproc)succeeds - Backend test: Start op-pedal, check WebSocket connects
- Frontend test:
cd vite && npx tsc --noEmitpasses - Audio test: Play audio through the mixer, verify routing
- Latency test: Measure round-trip latency with
jack_delay - 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
- Target hardware: Existing desktop-oplabs (Ubuntu VM), or a dedicated small PC (N150 mini PC on the Proxmox host)?
- Audio interface: Do you already own one, or should the plan include budget for a Behringer/Focusrite?
- Channel count: What's the realistic max channel count for your use case? (8? 16? 24?)
- NAM models: Use the existing Tone3000 integration for model downloads, or also support local .nam files?
- Control surface: Do you have a MIDI controller you want to use as a physical fader surface?
- 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?