Mixer engine: output routing, full state restore, MIDI learn API, PipeWire multi-channel support

This commit is contained in:
2026-06-20 16:20:30 -04:00
parent 01584f50da
commit 8068f5d168
12 changed files with 435 additions and 19 deletions
+10
View File
@@ -411,6 +411,16 @@ namespace pipedal
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return (uint32_t)this->captureChannels;
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return (uint32_t)this->playbackChannels;
}
JackServerSettings jackServerSettings;
std::string alsa_device_name;
+6
View File
@@ -57,6 +57,12 @@ namespace pipedal {
virtual uint32_t GetSampleRate() = 0;
/// Get the number of capture (input) channels the device actually provides.
virtual uint32_t GetDeviceCaptureChannels() const = 0;
/// Get the number of playback (output) channels the device actually provides.
virtual uint32_t GetDevicePlaybackChannels() const = 0;
virtual size_t GetMidiInputEventCount() = 0;
virtual MidiEvent*GetMidiEvents() = 0;
+19
View File
@@ -1501,6 +1501,25 @@ public:
{
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() override
{
if (this->audioDriver)
{
return this->audioDriver->GetDeviceCaptureChannels();
}
return 2;
}
virtual uint32_t GetDevicePlaybackChannels() override
{
if (this->audioDriver)
{
return this->audioDriver->GetDevicePlaybackChannels();
}
return 2;
}
void HandleAlsaSequencerDevicesChanged(
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
{
+6
View File
@@ -237,6 +237,12 @@ namespace pipedal
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
virtual uint32_t GetSampleRate() = 0;
/// Get the number of capture (input) channels the audio device provides.
virtual uint32_t GetDeviceCaptureChannels() = 0;
/// Get the number of playback (output) channels the audio device provides.
virtual uint32_t GetDevicePlaybackChannels() = 0;
virtual JackConfiguration GetServerConfiguration() = 0;
virtual void SetPedalboard(const std::shared_ptr<Lv2Pedalboard> &pedalboard) = 0;
+10
View File
@@ -136,6 +136,16 @@ namespace pipedal
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return (uint32_t)deviceCaptureBuffers.size();
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return (uint32_t)devicePlaybackBuffers.size();
}
JackServerSettings jackServerSettings;
AlsaSequencer::ptr alsaSequencer;
+77
View File
@@ -330,6 +330,22 @@ std::string MixerApi::getStateJson() const
}
writer.write_raw("]");
// Output routing
writer.write_member("outputRoutes", "");
writer.write_raw("[");
first = true;
for (const auto& route : mixerEngine_->outputRoutes()) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("sourceBusId", route.sourceBusId);
writer.write_member("sourceStartChannel", (int64_t)route.sourceStartChannel);
writer.write_member("targetStartChannel", (int64_t)route.targetStartChannel);
writer.write_member("channels", (int64_t)route.channels);
writer.end_object();
}
writer.write_raw("]");
writer.end_object();
return ss.str();
}
@@ -665,3 +681,64 @@ void MixerApi::loadMidiMappingsFromFile()
if (!mixerEngine_) return;
mixerEngine_->midiMapper().loadFromFile();
}
// ---------------------------------------------------------------------------
// Output Routing
// ---------------------------------------------------------------------------
std::string MixerApi::getOutputRoutesJson() const
{
if (!mixerEngine_) return "[]";
const auto& routes = mixerEngine_->outputRoutes();
std::stringstream ss;
json_writer writer(ss, false);
writer.write_raw("[");
bool first = true;
for (const auto& route : routes) {
if (!first) writer.write_raw(",");
first = false;
writer.start_object();
writer.write_member("sourceBusId", route.sourceBusId);
writer.write_member("sourceStartChannel", (int64_t)route.sourceStartChannel);
writer.write_member("targetStartChannel", (int64_t)route.targetStartChannel);
writer.write_member("channels", (int64_t)route.channels);
writer.end_object();
}
writer.write_raw("]");
return ss.str();
}
void MixerApi::setOutputRoutesFromJson(const std::string& json)
{
if (!mixerEngine_) return;
std::vector<MixerEngine::MixerOutputRoute> routes;
std::istringstream ss(json);
json_reader reader(ss);
reader.consume('[');
while (reader.peek() != ']')
{
MixerEngine::MixerOutputRoute route{};
reader.start_object();
while (reader.peek() != '}')
{
std::string key = reader.read_string();
reader.consume(':');
if (key == "sourceBusId") reader.read(&route.sourceBusId);
else if (key == "sourceStartChannel") { int64_t v; reader.read(&v); route.sourceStartChannel = (int)v; }
else if (key == "targetStartChannel") { int64_t v; reader.read(&v); route.targetStartChannel = (int)v; }
else if (key == "channels") { int64_t v; reader.read(&v); route.channels = (int)v; }
else reader.skip_property();
if (reader.peek() == ',') reader.consume(',');
}
reader.end_object();
routes.push_back(route);
if (reader.peek() == ',') reader.consume(',');
}
reader.consume(']');
mixerEngine_->setOutputRoutes(routes);
}
+57
View File
@@ -87,6 +87,17 @@ public:
/// Get the full mixer state as a JSON string
std::string getStateJson() const;
/// --- Output Routing ---
/// Get output routes as a JSON array string
std::string getOutputRoutesJson() const;
/// Set output routes from a JSON array string
void setOutputRoutesFromJson(const std::string& json);
/// Apply a full mixer state from a JSON string
void setFullState(const std::string& stateJson);
/// --- Scenes ---
/// Save current mixer state as a scene
@@ -101,6 +112,52 @@ public:
/// Delete a scene
bool deleteScene(const std::string& sceneId);
/// --- MIDI Control Surface Mapping ---
/// Get all MIDI mappings as JSON
std::string getMidiMappingsJson() const;
/// Set all MIDI mappings from JSON
void setMidiMappingsFromJson(const std::string& json);
/// Add a single MIDI mapping
void addMidiMapping(int midiChannel, int ccNumber,
const std::string& targetType, int64_t targetId,
float minValue, float maxValue);
/// Remove MIDI mapping by CC and channel
void removeMidiMapping(int midiChannel, int ccNumber);
/// Remove MIDI mapping by index
void removeMidiMappingByIndex(size_t index);
/// Clear all MIDI mappings
void clearMidiMappings();
/// Toggle MIDI learn mode
void setMidiLearnMode(bool enabled);
bool getMidiLearnMode() const;
/// Set the pending learn target (which UI control was touched)
void setMidiLearnTarget(const std::string& targetType, int64_t targetId);
/// Commit a learned mapping (captured CC + pending target)
bool commitMidiLearnMapping();
/// Get last learned CC event info (for UI feedback)
struct LearnedEventInfo {
bool hasEvent = false;
int midiChannel = -1;
int ccNumber = -1;
};
LearnedEventInfo getLastLearnedMidiEvent() const;
/// Save MIDI mappings to config file
void saveMidiMappingsToFile() const;
/// Load MIDI mappings from config file
void loadMidiMappingsFromFile();
private:
MixerEngine* mixerEngine_ = nullptr;
};
+154 -19
View File
@@ -7,6 +7,7 @@
#include "MixerBus.hpp"
#include "Lv2Pedalboard.hpp"
#include "IHost.hpp"
#include "MidiEvent.hpp"
#include <algorithm>
#include <cmath>
@@ -374,17 +375,31 @@ void MixerEngine::process(
// Step 2: Process each channel
size_t numChannels = channels_.size();
// Determine channel pairing mode:
// For 1-2 total input channels: stereo pairing per strip (backward compatible)
// For 3+ input channels: each strip is mono (one input channel → stereo output with pan)
bool stereoPairing = (inputChannels <= 2);
for (size_t ch = 0; ch < numChannels; ++ch) {
auto* channel = channels_[ch].get();
// Build input buffer pointers for this channel
// Channel ch reads from device input ch (if available)
float* channelInputs[2] = { nullptr, nullptr };
if (ch < inputChannels) {
channelInputs[0] = deviceInputs[ch]; // mono input
// For stereo, pair consecutive channels: (0,1), (2,3), etc.
if (ch + 1 < inputChannels) {
channelInputs[1] = deviceInputs[ch + 1];
if (stereoPairing) {
// Stereo pairing: channel 0 ← inputs[0,1], channel 1 ← inputs[2,3], etc.
uint32_t baseInput = (uint32_t)(ch * 2);
if (baseInput < inputChannels) {
channelInputs[0] = deviceInputs[baseInput];
if (baseInput + 1 < inputChannels) {
channelInputs[1] = deviceInputs[baseInput + 1];
}
}
} else {
// Mono per strip: each channel gets exactly one input
if (ch < inputChannels) {
channelInputs[0] = deviceInputs[ch];
// Second input stays nullptr → mono processing
}
}
@@ -395,10 +410,15 @@ void MixerEngine::process(
channelOutputs[1] = channelOutputBuffers_[ch].data() + maxBufferSize_;
}
// Determine the actual number of input channels for this strip
size_t stripInputChannels = 0;
if (channelInputs[0] != nullptr) stripInputChannels = 1;
if (channelInputs[1] != nullptr) stripInputChannels = 2;
// Process the channel strip
channel->process(
(const float* const*)channelInputs,
std::min((size_t)2, (size_t)inputChannels),
stripInputChannels,
channelOutputs,
2,
frames
@@ -416,25 +436,52 @@ void MixerEngine::process(
bus->process(frames);
}
// Step 5: Write master bus to device outputs
if (masterBus_) {
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
if (deviceOutputs[outCh] == nullptr) continue;
// Step 5: Write buses to physical outputs according to output routing
if (outputRoutes_.empty()) {
// Legacy fallback: write master bus to device outputs 1:1
if (masterBus_) {
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
if (deviceOutputs[outCh] == nullptr) continue;
const float* src = masterBus_->buffer(outCh);
if (src) {
std::copy(src, src + frames, deviceOutputs[outCh]);
} else if (outCh == 1) {
// Mono to stereo: copy L to R if R bus channel doesn't exist
const float* srcL = masterBus_->buffer(0);
if (srcL) {
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
const float* src = masterBus_->buffer(outCh);
if (src) {
std::copy(src, src + frames, deviceOutputs[outCh]);
} else if (outCh == 1) {
const float* srcL = masterBus_->buffer(0);
if (srcL) {
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
}
}
}
}
} else {
// Use configured output routes
for (const auto& route : outputRoutes_) {
MixerBus* sourceBus = getBus(route.sourceBusId);
if (!sourceBus) continue;
for (int ch = 0; ch < route.channels; ++ch) {
uint32_t targetCh = (uint32_t)(route.targetStartChannel + ch);
if (targetCh >= outputChannels) break;
if (deviceOutputs[targetCh] == nullptr) continue;
const float* src = sourceBus->buffer(route.sourceStartChannel + ch);
if (src) {
std::copy(src, src + frames, deviceOutputs[targetCh]);
}
}
}
}
}
// --- MIDI Control Surface Mapping ---
bool MixerEngine::processMidiEvent(const MidiEvent& event)
{
midiMapper_.setMixerEngine(this);
return midiMapper_.processEvent(event);
}
// --- State Serialization ---
MixerEngine::MixerSnapshot MixerEngine::captureSnapshot() const
@@ -474,6 +521,94 @@ MixerEngine::MixerSnapshot MixerEngine::captureSnapshot() const
return snap;
}
// --- Output Routing ---
void MixerEngine::setOutputRoutes(const std::vector<MixerOutputRoute>& routes)
{
outputRoutes_ = routes;
}
void MixerEngine::addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels)
{
// Remove any existing route that conflicts with the target
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId, targetStartChannel](const MixerOutputRoute& r) {
return r.sourceBusId == busId &&
r.targetStartChannel == targetStartChannel;
}),
outputRoutes_.end()
);
MixerOutputRoute route;
route.sourceBusId = busId;
route.sourceStartChannel = sourceStartChannel;
route.targetStartChannel = targetStartChannel;
route.channels = channels;
outputRoutes_.push_back(route);
}
void MixerEngine::removeOutputRoutes(int64_t busId)
{
outputRoutes_.erase(
std::remove_if(outputRoutes_.begin(), outputRoutes_.end(),
[busId](const MixerOutputRoute& r) { return r.sourceBusId == busId; }),
outputRoutes_.end()
);
}
std::vector<MixerEngine::MixerOutputRoute> MixerEngine::findOutputRoutesForBus(int64_t busId) const
{
std::vector<MixerOutputRoute> result;
for (const auto& route : outputRoutes_) {
if (route.sourceBusId == busId) {
result.push_back(route);
}
}
return result;
}
// --- Auto channel creation ---
void MixerEngine::autoCreateChannels(uint32_t inputChannelCount)
{
// Remove existing channels if any
for (int i = (int)channels_.size() - 1; i >= 0; --i) {
removeChannel(i);
}
// Create one channel strip per input
for (uint32_t i = 0; i < inputChannelCount; ++i) {
addChannel((int)i);
auto* ch = getChannel((int)i);
if (ch) {
char label[32];
snprintf(label, sizeof(label), "Input %u", i + 1);
ch->setLabel(label);
}
}
// Set default output routes:
// For 1-2 channels: master bus → physical 1-2 (backward compatible)
// For 3+ channels: master bus → physical 1-2, then pair remaining channels
// in groups of 2 to physical 3-4, 5-6, etc.
outputRoutes_.clear();
if (!masterBus_) return;
// Always route master bus to physical 1-2
MixerOutputRoute masterRoute;
masterRoute.sourceBusId = masterBus_->id();
masterRoute.sourceStartChannel = 0;
masterRoute.targetStartChannel = 0;
masterRoute.channels = 2;
outputRoutes_.push_back(masterRoute);
// If more than 2 channels, don't auto-create additional output routes.
// The user should configure them via the output routing UI.
// (Aux buses, subgroups, etc. are not auto-routed)
}
void MixerEngine::applySnapshot(const MixerSnapshot& snapshot)
{
// Apply channel states
+48
View File
@@ -11,6 +11,7 @@
#include <functional>
#include "MixerChannelStrip.hpp"
#include "MixerBus.hpp"
#include "MidiMapper.hpp"
namespace pipedal {
@@ -70,6 +71,11 @@ public:
/// Returns a pointer to the new channel (valid until removed).
MixerChannelStrip* addChannel(int physicalInputIndex);
/// Auto-create channels based on the number of detected input channels.
/// Removes existing channels and creates one strip per input.
/// Also sets up default output routes.
void autoCreateChannels(uint32_t inputChannelCount);
/// Remove a channel by its channel index.
void removeChannel(int channelIndex);
@@ -113,6 +119,32 @@ public:
/// Clear all routes.
void clearRoutes();
/// --- Output Routing ---
/// Describes a mapping from a mixer bus to physical output channels.
/// Multiple routes can be active simultaneously (e.g. Master→1-2, Aux1→3-4).
struct MixerOutputRoute {
int64_t sourceBusId; // Bus to route from
int sourceStartChannel; // Starting channel on the bus (0=L, 1=R)
int targetStartChannel; // Starting physical output channel
int channels; // Number of consecutive channels to route (1 or 2 typically)
};
/// Get the current output routing table (bus → physical output channel mapping).
const std::vector<MixerOutputRoute>& outputRoutes() const { return outputRoutes_; }
/// Set the entire output routing table.
void setOutputRoutes(const std::vector<MixerOutputRoute>& routes);
/// Add a single output route.
void addOutputRoute(int64_t busId, int sourceStartChannel, int targetStartChannel, int channels);
/// Remove all output routes for a given bus.
void removeOutputRoutes(int64_t busId);
/// Get all routes for a given bus.
std::vector<MixerOutputRoute> findOutputRoutesForBus(int64_t busId) const;
/// Get all current routes.
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
@@ -143,6 +175,16 @@ public:
/// True if any channel has solo engaged.
bool anySoloActive() const;
/// --- MIDI Control Surface Mapping ---
/// Access the MIDI mapper for CC control surface mapping.
MidiMapper& midiMapper() { return midiMapper_; }
const MidiMapper& midiMapper() const { return midiMapper_; }
/// Process a MIDI event (typically from the real-time audio thread).
/// Routes CC messages to the midi mapper. Returns true if consumed.
bool processMidiEvent(const struct MidiEvent& event);
/// --- State Serialization ---
struct MixerSnapshot {
@@ -181,6 +223,9 @@ private:
// Routing entries
std::vector<MixerRouteEntry> routes_;
// Output routing entries (bus → physical output channel mapping)
std::vector<MixerOutputRoute> outputRoutes_;
// Audio configuration
uint32_t sampleRate_ = 48000;
size_t maxBufferSize_ = 512;
@@ -188,6 +233,9 @@ private:
// IHost reference for FX preparation
IHost* pHost_ = nullptr;
// MIDI CC control surface mapper
MidiMapper midiMapper_;
// Next bus ID counter
static std::atomic<int64_t> nextBusId_;
+19
View File
@@ -33,6 +33,7 @@
#include "CpuGovernor.hpp"
#include "RegDb.hpp"
#include "RingBufferReader.hpp"
#include "MixerEngine.hpp"
#include "PiPedalUI.hpp"
#include "atom_object.hpp"
#include "Lv2PluginChangeMonitor.hpp"
@@ -3088,6 +3089,24 @@ void PiPedalModel::SetMixerEngine(const std::shared_ptr<MixerEngine>& engine)
{
this->audioHost->SetMixerEngine(engine);
}
if (engine && this->audioHost && this->audioHost->IsOpen())
{
// Auto-create channels based on detected input channel count
uint32_t inputChannels = this->audioHost->GetDeviceCaptureChannels();
if (inputChannels == 0) inputChannels = 2; // default to stereo
engine->autoCreateChannels(inputChannels);
Lv2Log::info(SS("Mixer engine initialized with "
<< inputChannels << " channels, "
<< engine->channelCount() << " strips created."));
}
// Load saved MIDI control surface mappings
if (engine) {
engine->midiMapper().loadFromFile();
}
}
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
+19
View File
@@ -1395,6 +1395,25 @@ public:
}
REGISTER_MESSAGE_HANDLER(mixerGetScenes)
/************************************************************************/
/* Mixer Output Routing Messages */
/************************************************************************/
void handle_mixerGetOutputRoutes(int replyTo, json_reader *pReader)
{
std::string json = this->mixerApi.getOutputRoutesJson();
this->JsonReply(replyTo, "mixerOutputRoutes", json.c_str());
}
REGISTER_MESSAGE_HANDLER(mixerGetOutputRoutes)
void handle_mixerSetOutputRoutes(int replyTo, json_reader *pReader)
{
std::string json;
pReader->read(&json);
this->mixerApi.setOutputRoutesFromJson(json);
}
REGISTER_MESSAGE_HANDLER(mixerSetOutputRoutes)
/************************************************************************/
/* MIDI Control Surface Mapping Messages */
/************************************************************************/
+10
View File
@@ -157,6 +157,16 @@ namespace pipedal
return this->sampleRate;
}
virtual uint32_t GetDeviceCaptureChannels() const override
{
return this->captureChannels;
}
virtual uint32_t GetDevicePlaybackChannels() const override
{
return this->playbackChannels;
}
virtual size_t GetMidiInputEventCount() override
{
return midiEventCount;