Mixer engine: output routing, full state restore, MIDI learn API, PipeWire multi-channel support
This commit is contained in:
@@ -411,6 +411,16 @@ namespace pipedal
|
|||||||
return this->sampleRate;
|
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;
|
JackServerSettings jackServerSettings;
|
||||||
|
|
||||||
std::string alsa_device_name;
|
std::string alsa_device_name;
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ namespace pipedal {
|
|||||||
|
|
||||||
virtual uint32_t GetSampleRate() = 0;
|
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 size_t GetMidiInputEventCount() = 0;
|
||||||
virtual MidiEvent*GetMidiEvents() = 0;
|
virtual MidiEvent*GetMidiEvents() = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1501,6 +1501,25 @@ public:
|
|||||||
{
|
{
|
||||||
return this->sampleRate;
|
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(
|
void HandleAlsaSequencerDevicesChanged(
|
||||||
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
|
AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ namespace pipedal
|
|||||||
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0;
|
||||||
virtual uint32_t GetSampleRate() = 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 JackConfiguration GetServerConfiguration() = 0;
|
||||||
|
|
||||||
virtual void SetPedalboard(const std::shared_ptr<Lv2Pedalboard> &pedalboard) = 0;
|
virtual void SetPedalboard(const std::shared_ptr<Lv2Pedalboard> &pedalboard) = 0;
|
||||||
|
|||||||
@@ -136,6 +136,16 @@ namespace pipedal
|
|||||||
return this->sampleRate;
|
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;
|
JackServerSettings jackServerSettings;
|
||||||
AlsaSequencer::ptr alsaSequencer;
|
AlsaSequencer::ptr alsaSequencer;
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,22 @@ std::string MixerApi::getStateJson() const
|
|||||||
}
|
}
|
||||||
writer.write_raw("]");
|
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();
|
writer.end_object();
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
@@ -665,3 +681,64 @@ void MixerApi::loadMidiMappingsFromFile()
|
|||||||
if (!mixerEngine_) return;
|
if (!mixerEngine_) return;
|
||||||
mixerEngine_->midiMapper().loadFromFile();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ public:
|
|||||||
/// Get the full mixer state as a JSON string
|
/// Get the full mixer state as a JSON string
|
||||||
std::string getStateJson() const;
|
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 ---
|
/// --- Scenes ---
|
||||||
|
|
||||||
/// Save current mixer state as a scene
|
/// Save current mixer state as a scene
|
||||||
@@ -101,6 +112,52 @@ public:
|
|||||||
/// Delete a scene
|
/// Delete a scene
|
||||||
bool deleteScene(const std::string& sceneId);
|
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:
|
private:
|
||||||
MixerEngine* mixerEngine_ = nullptr;
|
MixerEngine* mixerEngine_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
+143
-8
@@ -7,6 +7,7 @@
|
|||||||
#include "MixerBus.hpp"
|
#include "MixerBus.hpp"
|
||||||
#include "Lv2Pedalboard.hpp"
|
#include "Lv2Pedalboard.hpp"
|
||||||
#include "IHost.hpp"
|
#include "IHost.hpp"
|
||||||
|
#include "MidiEvent.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -374,17 +375,31 @@ void MixerEngine::process(
|
|||||||
|
|
||||||
// Step 2: Process each channel
|
// Step 2: Process each channel
|
||||||
size_t numChannels = channels_.size();
|
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) {
|
for (size_t ch = 0; ch < numChannels; ++ch) {
|
||||||
auto* channel = channels_[ch].get();
|
auto* channel = channels_[ch].get();
|
||||||
|
|
||||||
// Build input buffer pointers for this channel
|
// Build input buffer pointers for this channel
|
||||||
// Channel ch reads from device input ch (if available)
|
|
||||||
float* channelInputs[2] = { nullptr, nullptr };
|
float* channelInputs[2] = { nullptr, nullptr };
|
||||||
|
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) {
|
if (ch < inputChannels) {
|
||||||
channelInputs[0] = deviceInputs[ch]; // mono input
|
channelInputs[0] = deviceInputs[ch];
|
||||||
// For stereo, pair consecutive channels: (0,1), (2,3), etc.
|
// Second input stays nullptr → mono processing
|
||||||
if (ch + 1 < inputChannels) {
|
|
||||||
channelInputs[1] = deviceInputs[ch + 1];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,10 +410,15 @@ void MixerEngine::process(
|
|||||||
channelOutputs[1] = channelOutputBuffers_[ch].data() + maxBufferSize_;
|
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
|
// Process the channel strip
|
||||||
channel->process(
|
channel->process(
|
||||||
(const float* const*)channelInputs,
|
(const float* const*)channelInputs,
|
||||||
std::min((size_t)2, (size_t)inputChannels),
|
stripInputChannels,
|
||||||
channelOutputs,
|
channelOutputs,
|
||||||
2,
|
2,
|
||||||
frames
|
frames
|
||||||
@@ -416,7 +436,9 @@ void MixerEngine::process(
|
|||||||
bus->process(frames);
|
bus->process(frames);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Write master bus to device outputs
|
// 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_) {
|
if (masterBus_) {
|
||||||
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
|
for (uint32_t outCh = 0; outCh < outputChannels; ++outCh) {
|
||||||
if (deviceOutputs[outCh] == nullptr) continue;
|
if (deviceOutputs[outCh] == nullptr) continue;
|
||||||
@@ -425,7 +447,6 @@ void MixerEngine::process(
|
|||||||
if (src) {
|
if (src) {
|
||||||
std::copy(src, src + frames, deviceOutputs[outCh]);
|
std::copy(src, src + frames, deviceOutputs[outCh]);
|
||||||
} else if (outCh == 1) {
|
} else if (outCh == 1) {
|
||||||
// Mono to stereo: copy L to R if R bus channel doesn't exist
|
|
||||||
const float* srcL = masterBus_->buffer(0);
|
const float* srcL = masterBus_->buffer(0);
|
||||||
if (srcL) {
|
if (srcL) {
|
||||||
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
|
std::copy(srcL, srcL + frames, deviceOutputs[outCh]);
|
||||||
@@ -433,6 +454,32 @@ void MixerEngine::process(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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 ---
|
// --- State Serialization ---
|
||||||
@@ -474,6 +521,94 @@ MixerEngine::MixerSnapshot MixerEngine::captureSnapshot() const
|
|||||||
return snap;
|
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)
|
void MixerEngine::applySnapshot(const MixerSnapshot& snapshot)
|
||||||
{
|
{
|
||||||
// Apply channel states
|
// Apply channel states
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include "MixerChannelStrip.hpp"
|
#include "MixerChannelStrip.hpp"
|
||||||
#include "MixerBus.hpp"
|
#include "MixerBus.hpp"
|
||||||
|
#include "MidiMapper.hpp"
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal {
|
||||||
|
|
||||||
@@ -70,6 +71,11 @@ public:
|
|||||||
/// Returns a pointer to the new channel (valid until removed).
|
/// Returns a pointer to the new channel (valid until removed).
|
||||||
MixerChannelStrip* addChannel(int physicalInputIndex);
|
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.
|
/// Remove a channel by its channel index.
|
||||||
void removeChannel(int channelIndex);
|
void removeChannel(int channelIndex);
|
||||||
|
|
||||||
@@ -113,6 +119,32 @@ public:
|
|||||||
/// Clear all routes.
|
/// Clear all routes.
|
||||||
void clearRoutes();
|
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.
|
/// Get all current routes.
|
||||||
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
|
const std::vector<MixerRouteEntry>& routes() const { return routes_; }
|
||||||
|
|
||||||
@@ -143,6 +175,16 @@ public:
|
|||||||
/// True if any channel has solo engaged.
|
/// True if any channel has solo engaged.
|
||||||
bool anySoloActive() const;
|
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 ---
|
/// --- State Serialization ---
|
||||||
|
|
||||||
struct MixerSnapshot {
|
struct MixerSnapshot {
|
||||||
@@ -181,6 +223,9 @@ private:
|
|||||||
// Routing entries
|
// Routing entries
|
||||||
std::vector<MixerRouteEntry> routes_;
|
std::vector<MixerRouteEntry> routes_;
|
||||||
|
|
||||||
|
// Output routing entries (bus → physical output channel mapping)
|
||||||
|
std::vector<MixerOutputRoute> outputRoutes_;
|
||||||
|
|
||||||
// Audio configuration
|
// Audio configuration
|
||||||
uint32_t sampleRate_ = 48000;
|
uint32_t sampleRate_ = 48000;
|
||||||
size_t maxBufferSize_ = 512;
|
size_t maxBufferSize_ = 512;
|
||||||
@@ -188,6 +233,9 @@ private:
|
|||||||
// IHost reference for FX preparation
|
// IHost reference for FX preparation
|
||||||
IHost* pHost_ = nullptr;
|
IHost* pHost_ = nullptr;
|
||||||
|
|
||||||
|
// MIDI CC control surface mapper
|
||||||
|
MidiMapper midiMapper_;
|
||||||
|
|
||||||
// Next bus ID counter
|
// Next bus ID counter
|
||||||
static std::atomic<int64_t> nextBusId_;
|
static std::atomic<int64_t> nextBusId_;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "CpuGovernor.hpp"
|
#include "CpuGovernor.hpp"
|
||||||
#include "RegDb.hpp"
|
#include "RegDb.hpp"
|
||||||
#include "RingBufferReader.hpp"
|
#include "RingBufferReader.hpp"
|
||||||
|
#include "MixerEngine.hpp"
|
||||||
#include "PiPedalUI.hpp"
|
#include "PiPedalUI.hpp"
|
||||||
#include "atom_object.hpp"
|
#include "atom_object.hpp"
|
||||||
#include "Lv2PluginChangeMonitor.hpp"
|
#include "Lv2PluginChangeMonitor.hpp"
|
||||||
@@ -3088,6 +3089,24 @@ void PiPedalModel::SetMixerEngine(const std::shared_ptr<MixerEngine>& engine)
|
|||||||
{
|
{
|
||||||
this->audioHost->SetMixerEngine(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)
|
void PiPedalModel::SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId)
|
||||||
|
|||||||
@@ -1395,6 +1395,25 @@ public:
|
|||||||
}
|
}
|
||||||
REGISTER_MESSAGE_HANDLER(mixerGetScenes)
|
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 */
|
/* MIDI Control Surface Mapping Messages */
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ namespace pipedal
|
|||||||
return this->sampleRate;
|
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
|
virtual size_t GetMidiInputEventCount() override
|
||||||
{
|
{
|
||||||
return midiEventCount;
|
return midiEventCount;
|
||||||
|
|||||||
Reference in New Issue
Block a user