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
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user