diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 32f3f58..054acb0 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -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; diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index c8ccc76..299c46d 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -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; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index ab1507a..5539827 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -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) { diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index f117c2a..c6ca6df 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -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 &pedalboard) = 0; diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index 84d0850..c5f2665 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -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; diff --git a/src/MixerApi.cpp b/src/MixerApi.cpp index 8b3c7bb..fc292b4 100644 --- a/src/MixerApi.cpp +++ b/src/MixerApi.cpp @@ -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 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); +} diff --git a/src/MixerApi.hpp b/src/MixerApi.hpp index d692b3d..b5f9440 100644 --- a/src/MixerApi.hpp +++ b/src/MixerApi.hpp @@ -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; }; diff --git a/src/MixerEngine.cpp b/src/MixerEngine.cpp index cfaea0b..682143c 100644 --- a/src/MixerEngine.cpp +++ b/src/MixerEngine.cpp @@ -7,6 +7,7 @@ #include "MixerBus.hpp" #include "Lv2Pedalboard.hpp" #include "IHost.hpp" +#include "MidiEvent.hpp" #include #include @@ -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& 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::findOutputRoutesForBus(int64_t busId) const +{ + std::vector 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 diff --git a/src/MixerEngine.hpp b/src/MixerEngine.hpp index cf385a0..9adb23c 100644 --- a/src/MixerEngine.hpp +++ b/src/MixerEngine.hpp @@ -11,6 +11,7 @@ #include #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& outputRoutes() const { return outputRoutes_; } + + /// Set the entire output routing table. + void setOutputRoutes(const std::vector& 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 findOutputRoutesForBus(int64_t busId) const; + /// Get all current routes. const std::vector& 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 routes_; + // Output routing entries (bus → physical output channel mapping) + std::vector 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 nextBusId_; diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index a6986d7..2be8538 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -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& 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) diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 4161295..a244e20 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -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 */ /************************************************************************/ diff --git a/src/PipeWireDriver.cpp b/src/PipeWireDriver.cpp index f2145ea..170b454 100644 --- a/src/PipeWireDriver.cpp +++ b/src/PipeWireDriver.cpp @@ -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;