Files
op-pedal/src/MidiMapper.hpp
T
shawn 5fd5946ff6 P4: MIDI control surface mapping — faders/buttons over USB MIDI
C++ backend (fully implemented):
- MidiMapper: CC processing, mapping table, JSON persistence, learn mode
- MidiLearnMode: 3-step learn workflow state machine
- MixerEngine::processMidiEvent() wired into AudioHost MIDI pipeline
- MixerApi + PiPedalSocket: all WS handlers (getMidiMappings,
  setMidiLearnMode, setMidiLearnTarget, commitMidiLearn, etc.)

React frontend (new):
- MidiMappingPanel: dialog with learn mode toggle, CC capture polling,
  commit workflow, current mappings list with delete, manual add
- MixerPage: MIDI button in toolbar, learn mode state management
- ChannelStrip + MasterBus: learn mode callbacks on fader/mute/solo touch
2026-06-20 16:14:12 -04:00

150 lines
4.9 KiB
C++

// Copyright (c) 2026 Ourpad Network
// See LICENSE file in the project root for full license text.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
namespace pipedal {
class MixerEngine;
struct MidiEvent;
/// Types of mixer parameters that can be mapped from MIDI CC.
enum class MidiTargetType {
ChannelVolume, ///< Channel fader (-inf .. +12 dB)
ChannelPan, ///< Channel pan (-1 .. +1)
ChannelMute, ///< Channel mute toggle
ChannelSolo, ///< Channel solo toggle
BusVolume, ///< Bus fader (-inf .. +12 dB)
BusMute, ///< Bus mute toggle
MasterVolume, ///< Master bus volume
MasterMute, ///< Master bus mute
};
/// A single mapping entry: MIDI CC# + channel → mixer parameter.
struct MidiMappingEntry {
int midiChannel = -1; ///< MIDI channel (-1 = omni / any)
int ccNumber = 0; ///< MIDI CC number (0-127)
MidiTargetType targetType = MidiTargetType::ChannelVolume;
int64_t targetId = 0; ///< channel index for Channel*, bus ID for Bus*
/// Output range: CC=0 maps to minValue, CC=127 maps to maxValue.
float minValue = 0.0f;
float maxValue = 1.0f;
/// Convert between enum and string (for JSON serialization).
static MidiTargetType targetTypeFromString(const std::string& str);
static const char* targetTypeToString(MidiTargetType type);
};
/// MIDI CC → mixer parameter mapper.
///
/// Receives MIDI CC events (0xB0) from the real-time audio thread and applies
/// them to the MixerEngine via atomic parameter setters.
///
/// The mapping table is configured from the non-real-time thread; a mutex
/// protects the table while the RT path snapshots the current mapping set.
class MidiMapper {
public:
MidiMapper();
~MidiMapper();
/// Set the mixer engine to control. Must be set before processing events.
void setMixerEngine(MixerEngine* engine) { mixerEngine_ = engine; }
/// Process a MIDI event. Returns true if a mapping consumed the event.
/// RT-safe: uses atomic mixer setters directly.
bool processEvent(const MidiEvent& event);
// --- Mapping table management (non-RT thread) ---
/// Replace the entire mapping table.
void setMappings(const std::vector<MidiMappingEntry>& mappings);
/// Add a single mapping entry.
void addMapping(const MidiMappingEntry& entry);
/// Remove all mappings matching the given MIDI channel and CC number.
bool removeMapping(int midiChannel, int ccNumber);
/// Remove a specific mapping entry by index.
bool removeMappingByIndex(size_t index);
/// Clear all mappings.
void clearMappings();
/// Get a copy of the current mapping table.
std::vector<MidiMappingEntry> getMappings() const;
// --- Persistence ---
/// Serialize mappings to JSON string.
std::string getMappingsJson() const;
/// Deserialize mappings from JSON string.
void setMappingsFromJson(const std::string& json);
/// Default config file path.
static std::string defaultConfigPath();
/// Load mappings from default config file.
void loadFromFile();
/// Save mappings to default config file.
void saveToFile() const;
// --- Learn mode ---
/// Enable/disable MIDI learn mode.
/// When enabled, each incoming CC event is captured and cached
/// so the next call to commitLearnMapping() will create a mapping.
void setLearnMode(bool enabled);
/// True if learn mode is active.
bool learnMode() const { return learnMode_; }
/// Set the target parameter for the next learned mapping.
/// Call this when the user touches a UI control.
void setPendingLearnTarget(MidiTargetType type, int64_t id);
/// Get the last-learned MIDI event info.
/// Returns true if a CC event was captured since learn mode was enabled
/// or since the last clearLastLearnedEvent().
bool getLastLearnedEvent(int& outMidiChannel, int& outCcNumber) const;
/// Commit the current pending learn target + last CC into a mapping entry.
/// Returns the new entry, or nullopt if no CC was captured.
bool commitLearnMapping();
/// Clear cached last-learned CC event.
void clearLastLearnedEvent();
/// Get pending learn target info.
bool getPendingLearnTarget(MidiTargetType& outType, int64_t& outId) const;
private:
MixerEngine* mixerEngine_ = nullptr;
// Mapping table — mutable for snapshot-copy in RT path
std::vector<MidiMappingEntry> mappings_;
mutable std::mutex mappingsMutex_;
// Learn mode state
bool learnMode_ = false;
MidiTargetType pendingTargetType_ = MidiTargetType::ChannelVolume;
int64_t pendingTargetId_ = 0;
int lastLearnedMidiChannel_ = -1;
int lastLearnedCcNumber_ = -1;
mutable std::mutex learnMutex_;
// Apply a single mapping entry value to the mixer (RT-safe).
void applyValue(const MidiMappingEntry& entry, uint8_t ccValue);
};
} // namespace pipedal