// Copyright (c) 2026 Ourpad Network // See LICENSE file in the project root for full license text. #include "pch.h" #include "MidiMapper.hpp" #include "MixerEngine.hpp" #include "MixerChannelStrip.hpp" #include "MixerBus.hpp" #include "MidiEvent.hpp" #include "json.hpp" #include #include #include #include using namespace pipedal; // ─── Target type string conversion ────────────────────────────────────────── MidiTargetType MidiMappingEntry::targetTypeFromString(const std::string& str) { if (str == "channelVolume") return MidiTargetType::ChannelVolume; if (str == "channelPan") return MidiTargetType::ChannelPan; if (str == "channelMute") return MidiTargetType::ChannelMute; if (str == "channelSolo") return MidiTargetType::ChannelSolo; if (str == "busVolume") return MidiTargetType::BusVolume; if (str == "busMute") return MidiTargetType::BusMute; if (str == "masterVolume") return MidiTargetType::MasterVolume; if (str == "masterMute") return MidiTargetType::MasterMute; return MidiTargetType::ChannelVolume; } const char* MidiMappingEntry::targetTypeToString(MidiTargetType type) { switch (type) { case MidiTargetType::ChannelVolume: return "channelVolume"; case MidiTargetType::ChannelPan: return "channelPan"; case MidiTargetType::ChannelMute: return "channelMute"; case MidiTargetType::ChannelSolo: return "channelSolo"; case MidiTargetType::BusVolume: return "busVolume"; case MidiTargetType::BusMute: return "busMute"; case MidiTargetType::MasterVolume: return "masterVolume"; case MidiTargetType::MasterMute: return "masterMute"; } return "channelVolume"; } // ─── MidiMapper ───────────────────────────────────────────────────────────── MidiMapper::MidiMapper() { } MidiMapper::~MidiMapper() { } bool MidiMapper::processEvent(const MidiEvent& event) { if (!mixerEngine_) return false; // Only process MIDI CC messages (0xB0) if (event.size < 3) return false; uint8_t command = event.buffer[0] & 0xF0; if (command != 0xB0) return false; int midiChannel = static_cast(event.buffer[0] & 0x0F); int ccNumber = static_cast(event.buffer[1]); uint8_t ccValue = event.buffer[2]; // ── Learn mode: capture the CC event ── if (learnMode_) { std::lock_guard lock(learnMutex_); lastLearnedMidiChannel_ = midiChannel; lastLearnedCcNumber_ = ccNumber; } // ── Snapshot the current mapping table ── std::vector mappingsSnapshot; { std::lock_guard lock(mappingsMutex_); mappingsSnapshot = mappings_; } // ── Apply matching mappings ── bool consumed = false; for (const auto& entry : mappingsSnapshot) { // Match MIDI channel (-1 = omni) if (entry.midiChannel >= 0 && entry.midiChannel != midiChannel) { continue; } if (entry.ccNumber != ccNumber) { continue; } applyValue(entry, ccValue); consumed = true; } return consumed; } void MidiMapper::applyValue(const MidiMappingEntry& entry, uint8_t ccValue) { if (!mixerEngine_) return; // Map CC 0-127 to parameter range float range = entry.maxValue - entry.minValue; float normalized = static_cast(ccValue) / 127.0f; float mappedValue = entry.minValue + normalized * range; switch (entry.targetType) { case MidiTargetType::ChannelVolume: { auto* ch = mixerEngine_->getChannel(static_cast(entry.targetId)); if (ch) ch->setVolume(mappedValue); break; } case MidiTargetType::ChannelPan: { auto* ch = mixerEngine_->getChannel(static_cast(entry.targetId)); if (ch) ch->setPan(mappedValue); break; } case MidiTargetType::ChannelMute: { auto* ch = mixerEngine_->getChannel(static_cast(entry.targetId)); if (ch) ch->setMute(mappedValue >= 0.5f); break; } case MidiTargetType::ChannelSolo: { auto* ch = mixerEngine_->getChannel(static_cast(entry.targetId)); if (ch) ch->setSolo(mappedValue >= 0.5f); break; } case MidiTargetType::BusVolume: { auto* bus = mixerEngine_->getBus(entry.targetId); if (bus) bus->setVolume(mappedValue); break; } case MidiTargetType::BusMute: { auto* bus = mixerEngine_->getBus(entry.targetId); if (bus) bus->setMute(mappedValue >= 0.5f); break; } case MidiTargetType::MasterVolume: { auto* bus = mixerEngine_->masterBus(); if (bus) bus->setVolume(mappedValue); break; } case MidiTargetType::MasterMute: { auto* bus = mixerEngine_->masterBus(); if (bus) bus->setMute(mappedValue >= 0.5f); break; } } } // ─── Mapping table management ─────────────────────────────────────────────── void MidiMapper::setMappings(const std::vector& mappings) { std::lock_guard lock(mappingsMutex_); mappings_ = mappings; } void MidiMapper::addMapping(const MidiMappingEntry& entry) { std::lock_guard lock(mappingsMutex_); mappings_.push_back(entry); } bool MidiMapper::removeMapping(int midiChannel, int ccNumber) { std::lock_guard lock(mappingsMutex_); auto it = std::remove_if(mappings_.begin(), mappings_.end(), [midiChannel, ccNumber](const MidiMappingEntry& e) { return e.midiChannel == midiChannel && e.ccNumber == ccNumber; }); bool removed = (it != mappings_.end()); mappings_.erase(it, mappings_.end()); return removed; } bool MidiMapper::removeMappingByIndex(size_t index) { std::lock_guard lock(mappingsMutex_); if (index >= mappings_.size()) return false; mappings_.erase(mappings_.begin() + static_cast(index)); return true; } void MidiMapper::clearMappings() { std::lock_guard lock(mappingsMutex_); mappings_.clear(); } std::vector MidiMapper::getMappings() const { std::lock_guard lock(mappingsMutex_); return mappings_; } // ─── JSON serialization ───────────────────────────────────────────────────── std::string MidiMapper::getMappingsJson() const { auto mappings = getMappings(); std::stringstream ss; json_writer writer(ss, false); writer.start_array(); for (const auto& entry : mappings) { writer.start_object(); writer.write_member("midiChannel", (int64_t)entry.midiChannel); writer.write_member("ccNumber", (int64_t)entry.ccNumber); writer.write_member("targetType", MidiMappingEntry::targetTypeToString(entry.targetType)); writer.write_member("targetId", entry.targetId); writer.write_member("minValue", (double)entry.minValue); writer.write_member("maxValue", (double)entry.maxValue); writer.end_object(); } writer.end_array(); return ss.str(); } void MidiMapper::setMappingsFromJson(const std::string& json) { std::vector entries; std::stringstream ss(json); json_reader reader(ss); // Parse array: [ ... ] reader.consume('['); while (reader.peek() != ']') { MidiMappingEntry entry; std::string targetTypeStr; // Parse object: { "key": value, ... } reader.consume('{'); while (reader.peek() != '}') { std::string key; reader.read(&key); reader.consume(':'); if (key == "midiChannel") { int64_t v; reader.read(&v); entry.midiChannel = (int)v; } else if (key == "ccNumber") { int64_t v; reader.read(&v); entry.ccNumber = (int)v; } else if (key == "targetType") { reader.read(&targetTypeStr); } else if (key == "targetId") { reader.read(&entry.targetId); } else if (key == "minValue") { double v; reader.read(&v); entry.minValue = (float)v; } else if (key == "maxValue") { double v; reader.read(&v); entry.maxValue = (float)v; } else { reader.skip_property(); } // Consume comma separator if (reader.peek() == ',') { reader.consume(','); } } reader.consume('}'); // end object if (!targetTypeStr.empty()) { entry.targetType = MidiMappingEntry::targetTypeFromString(targetTypeStr); } entries.push_back(entry); // Consume comma separator between array elements if (reader.peek() == ',') { reader.consume(','); } } reader.consume(']'); // end array setMappings(entries); } std::string MidiMapper::defaultConfigPath() { // Store alongside other pipedal config const char* home = std::getenv("HOME"); if (home) { return std::string(home) + "/.config/pipedal/midi_map.json"; } return "/etc/pipedal/config/midi_map.json"; } void MidiMapper::loadFromFile() { std::string path = defaultConfigPath(); std::ifstream file(path); if (!file.is_open()) return; std::stringstream ss; ss << file.rdbuf(); std::string content = ss.str(); if (!content.empty()) { setMappingsFromJson(content); } } void MidiMapper::saveToFile() const { std::string path = defaultConfigPath(); // Ensure directory exists std::filesystem::path dir = std::filesystem::path(path).parent_path(); std::error_code ec; std::filesystem::create_directories(dir, ec); std::string json = getMappingsJson(); std::ofstream file(path); if (file.is_open()) { file << json; } } // ─── Learn mode ───────────────────────────────────────────────────────────── void MidiMapper::setLearnMode(bool enabled) { { std::lock_guard lock(learnMutex_); learnMode_ = enabled; if (!enabled) { // Clear last learned on exit lastLearnedMidiChannel_ = -1; lastLearnedCcNumber_ = -1; } } } void MidiMapper::setPendingLearnTarget(MidiTargetType type, int64_t id) { std::lock_guard lock(learnMutex_); pendingTargetType_ = type; pendingTargetId_ = id; } bool MidiMapper::getLastLearnedEvent(int& outMidiChannel, int& outCcNumber) const { std::lock_guard lock(learnMutex_); if (lastLearnedMidiChannel_ < 0 || lastLearnedCcNumber_ < 0) return false; outMidiChannel = lastLearnedMidiChannel_; outCcNumber = lastLearnedCcNumber_; return true; } void MidiMapper::clearLastLearnedEvent() { std::lock_guard lock(learnMutex_); lastLearnedMidiChannel_ = -1; lastLearnedCcNumber_ = -1; } bool MidiMapper::commitLearnMapping() { std::lock_guard lock(learnMutex_); if (lastLearnedMidiChannel_ < 0 || lastLearnedCcNumber_ < 0) { return false; // No CC event captured yet } MidiMappingEntry entry; entry.midiChannel = lastLearnedMidiChannel_; entry.ccNumber = lastLearnedCcNumber_; entry.targetType = pendingTargetType_; entry.targetId = pendingTargetId_; // Set sensible defaults based on target type switch (entry.targetType) { case MidiTargetType::ChannelVolume: case MidiTargetType::BusVolume: case MidiTargetType::MasterVolume: entry.minValue = -96.0f; // -inf dB entry.maxValue = 12.0f; // +12 dB max break; case MidiTargetType::ChannelPan: entry.minValue = -1.0f; // full left entry.maxValue = 1.0f; // full right break; case MidiTargetType::ChannelMute: case MidiTargetType::ChannelSolo: case MidiTargetType::BusMute: case MidiTargetType::MasterMute: entry.minValue = 0.0f; // off entry.maxValue = 1.0f; // on (threshold 0.5) break; } // Reset learned event so we don't recomit the same one lastLearnedMidiChannel_ = -1; lastLearnedCcNumber_ = -1; // Add to mapping table { std::lock_guard lockMap(mappingsMutex_); mappings_.push_back(entry); } saveToFile(); return true; } bool MidiMapper::getPendingLearnTarget(MidiTargetType& outType, int64_t& outId) const { std::lock_guard lock(learnMutex_); outType = pendingTargetType_; outId = pendingTargetId_; return true; }