#ifndef INTERFACERL_HPP #define INTERFACERL_HPP #include "../interface/InterfaceBase.hpp" #include "../../memlp/StaticMLP.h" #include "../../memlp/ReplayMemory.hpp" #include "../../memlp/OrnsteinUhlenbeckNoise.h" #include #include "../utils/sharedMem.hpp" #include "../PicoDefs.hpp" //#include "../hardware/memlnaut/display.hpp" #include "../hardware/memlnaut/display/MessageView.hpp" #include "../interface/UARTInput.hpp" #include "../interface/MIDIInOut.hpp" #include "../hardware/memlnaut/display/MessageView.hpp" #include "../hardware/memlnaut/display/BarGraphView.hpp" #include "../hardware/memlnaut/display/RLView.hpp" #include "../hardware/memlnaut/display/BlockSelectView.hpp" #include "../hardware/memlnaut/display/SingleSelectView.hpp" #include "../hardware/memlnaut/display/RotarySelectView.hpp" #include "../hardware/memlnaut/display/NameInputView.hpp" #include "../hardware/memlnaut/display/CCSelectView.hpp" #include "InterfaceRLFileFormat.hpp" #define RL_MEM __not_in_flash("rlmem") struct trainStatelessRLItem { std::vector input ; std::vector action; float reward; }; // Non-template base holding the nested types + constants that callers reference // without a template argument (e.g. InterfaceRLBase::INPUT_MODES in the modes // and the .ino). Because this (and InterfaceBase) are *non-dependent* bases of // InterfaceRL, the template sees every inherited member by ordinary // lookup — no this->/using-declarations required. class InterfaceRLBase : public InterfaceBase { public: using OnMIDICtrlCallback = std::function; // Training data type (independent of the network's output width). using training_pair_t = std::pair>, std::vector>>; static constexpr size_t kMaxNNInputs = 10; enum class INPUT_MODES { JOYSTICK, MACHINE_LISTENING, JOYSTICK_AND_MACHINE_LISTENING, SERIAL_INPUT }; enum class INPUT_SOURCE : uint8_t { JOYSTICK_3D = 0, JOYSTICK_4D, MACHINE_LISTENING, MIDI_1CC, MIDI_3CC, MIDI_8CC, COMBINED, COUNT }; enum class MEMORY_STORE_MODES { ADD, REPLACE_5_PERCENT, REPLACE_10_PERCENT, REPLACE_15_PERCENT, REWARD_DECAY_10_PERCENT, REWARD_DECAY_20_PERCENT }; // Input-source state is independent of the network's output width, so it // lives in the base: this lets N-agnostic helpers (e.g. MachineListeningMixin) // hold an InterfaceRLBase* and still query/configure the input source. INPUT_SOURCE getInputSource() const { return input_source_; } void setHasMachineListening(bool v) { hasMachineListening_ = v; } protected: INPUT_SOURCE input_source_ = INPUT_SOURCE::JOYSTICK_3D; bool hasMachineListening_ = false; }; // The RL interface. N_OUTPUTS (the active mode's parameter count) is fixed at // compile time, so the synth-mapping network is a static-memory StaticMLP with // no heap allocation. The mode declares e.g. InterfaceRL. template class InterfaceRL : public InterfaceRLBase { public: // Compile-time mapping network: kMaxNNInputs -> 16 -> 16 -> N_OUTPUTS. using SynthMLP = smlp::StaticMLP, smlp::Activations, loss::LOSS_FUNCTIONS::LOSS_MSE>; InterfaceRL() : InterfaceRLBase() // , ou_noise(0.02f, 0.0f, 0.2f, 0.001f, 0.0f) { } void setup(size_t n_inputs, size_t n_outputs, bool addMessageView = true); void optimise(); inline void setState(const size_t index, float value) { controlInput[index] = value; newInput = true; } // Force the next loop to regenerate + re-send the action, even if no input changed. // Use when an output-stage parameter (e.g. a fade/home value) changes. inline void markInputDirty() { newInput = true; } void readAnalysisParameters(std::vector params) override; void generateAction(bool donthesitate=false); inline void optimiseSometimes() { if (optimiseCounter>=optimiseDivisor) { optimise(); optimiseCounter=0; newInput = true; }else{ optimiseCounter++; } } void storeExperience(float reward, std::vector &experienceState, std::vector &experienceAction ); #define randomWeightVariance 1.f inline void randomiseTheNetwork() { synthMapping.RandomiseWeightsAndBiasesLin(-0.9f,1.1f, -0.9f, 0.3f); newInput = true; resetMinMaxFlag = true; } inline void setOptimiseDivisor(size_t newDiv) { optimiseDivisor = newDiv; } void setOptimiseDivisorInterf(float value); inline void forgetMemory() { replayMem.clear(); } inline void setRewardScale(float scale) { rewardScale = scale; } inline void setLRScale(const float scale) { learningRateScaled = learningRate * scale; // knob at 0 -> LR 0 -> training off (intended) String msg = "LR scale: " + String(scale); if (msgView) msgView->post(msg); } void setRewardScaleInterf(float value); inline void setNoiseLevel(float level) { // Knob [0,1] -> roaming amplitude (the OU walk's stationary std) in param space. // theta/dt (set in setup) fix the smoothness; this only sets how far each param // drifts from the mapping output. Low = gentle local wander; full = slow sweeps // across the whole [0,1] range. kMaxAmplitude sets the "depth": higher reaches // deeper into the param space (and interacts more with the [0,1] rails via the // gentle reflection in generateAction), lower keeps it shallower/more local. constexpr float kMaxAmplitude = 0.65f; float amplitude = level * kMaxAmplitude; if (amplitude < 0.01f) { amplitude = 0.f; if (msgView) msgView->post("Noise off"); } else { String msg = "Explore amount: " + String(amplitude, 3); if (msgView) msgView->post(msg); } for(auto& ou_noise: ou_noises) { ou_noise->setStationaryStd(amplitude); } // Learning stays active during exploration on purpose: likes/dislikes given while // the noise roams are what steer the network toward sounds the player wants. if (nnOutputsGraphView) nnOutputsGraphView->setNoiseActive(amplitude > 0.f); } // Jolt = permanent weight modulation (B2, held). At press, pick a random subset of // weights scattered across the net and roll a bounded random target for each. While // held, EMA-glide each toward its target (stepJolt, called per loop); release just // freezes them, so the change persists. Bounded by construction (interpolation toward // targets in the weight-init range can't run away) and smooth (no per-tick jitter). // Runs on the main loop / under the mlpActive lock, so touching the MLP here is safe. inline float randomJoltTarget() const { return kJoltWeightMin + (static_cast(rand()) / RAND_MAX) * (kJoltWeightMax - kJoltWeightMin); } inline void startJolt() { joltActive_ = true; joltWeightLoc_.clear(); joltTarget_.clear(); // StaticMLP exposes a flat view over all weights (layer 0 first); pick // random global indices to modulate. const size_t total = SynthMLP::TotalWeights(); if (total == 0) return; for (size_t i = 0; i < kJoltNumWeights; i++) { joltWeightLoc_.push_back(rand() % total); joltTarget_.push_back(randomJoltTarget()); } if (nnOutputsGraphView) nnOutputsGraphView->setLastAction("jolt"); if (msgView) msgView->post("Jolt: morphing weights"); } inline void stepJolt() { const size_t total = SynthMLP::TotalWeights(); for (size_t i = 0; i < joltWeightLoc_.size(); i++) { const size_t idx = joltWeightLoc_[i]; if (idx >= total) continue; // stale after a model load float* wp = synthMapping.WeightPtrAt(idx); if (!wp) continue; float& w = *wp; w += kJoltMorphRate * (joltTarget_[i] - w); // Reached this target (EMA only asymptotes, so use a threshold) -> roll a new // one, keeping the weight in motion for as long as the button is held. float gap = joltTarget_[i] - w; if (gap < 0.f) gap = -gap; if (gap < kJoltTargetEpsilon) joltTarget_[i] = randomJoltTarget(); } markInputDirty(); // weights changed -> regenerate + re-send the action } inline void stopJolt() { joltActive_ = false; // weights stay where they morphed to (permanent) joltLRRamp_ = 0.f; // resume learning from 0, ramping back to full over ~5s } void bind_RL_interface(INPUT_MODES input_mode = INPUT_MODES::JOYSTICK, bool joystick4D = false); void bindInterface(INPUT_MODES input_mode = INPUT_MODES::JOYSTICK,bool joystick4D = false) { bind_RL_interface(input_mode, joystick4D); } void bindInterface(bool disable_joystick=false, bool joystick4D = false) { bind_RL_interface(disable_joystick ? INPUT_MODES::MACHINE_LISTENING : INPUT_MODES::JOYSTICK, joystick4D); } void bindUARTInput(std::shared_ptr uart_input, const std::vector& kUARTListenInputs) { uart_input->SetCallback([this](size_t channel, float value) { // Serial.println("UART input: " + String(channel) + " value: " + String(value)); if (channel < controlInput.size()) { setState(channel, value); } }); } void bindMIDI(std::shared_ptr midi_interf, bool enableFootcontroller=false); void setModeInfo(const String& modeRoot, const String& modeTag); using ExtraSaveDataFn = std::function()>; using ExtraLoadDataFn = std::function; void setExtraSaveCallback(ExtraSaveDataFn fn) { _extraSaveFn = fn; } void setExtraLoadCallback(ExtraLoadDataFn fn) { _extraLoadFn = fn; } using RVCallback = std::function; void setRVX1Override(RVCallback fn) { rvX1Override = std::move(fn); } void setRVY1Override(RVCallback fn) { rvY1Override = std::move(fn); } void setRVZ1Override(RVCallback fn) { rvZ1Override = std::move(fn); } void setActiveDims(std::vector dims) { activeDims_ = std::move(dims); } std::function&)> inputInjectionHook; void trigger_like(); void trigger_dislike(); inline void getAction(std::vector &out_action) { out_action = action; } size_t getActiveInputCount() const { switch (input_source_) { case INPUT_SOURCE::JOYSTICK_3D: return 3; case INPUT_SOURCE::JOYSTICK_4D: return 4; case INPUT_SOURCE::MACHINE_LISTENING: return 6; case INPUT_SOURCE::MIDI_1CC: return 1; case INPUT_SOURCE::MIDI_3CC: return 3; case INPUT_SOURCE::MIDI_8CC: return 8; case INPUT_SOURCE::COMBINED: return kMaxNNInputs; default: return kMaxNNInputs; } } const std::vector& getControlInput() const { return controlInput; } // Recompute the unused-input pad value. Done only when the input mode changes (not per // frame). Scales as 1.1/n_unused so the total constant injected into layer 1 stays // bounded regardless of how many dims are unused — avoids over-driving the net. void updateUnusedInputDefault() { const size_t used = getActiveInputCount(); const size_t unused = (used < kMaxNNInputs) ? (kMaxNNInputs - used) : 0; unusedInputDefault_ = (unused > 0) ? (1.1f / static_cast(unused)) : 0.f; } // persist=false applies the change in memory + updates the bar graph without writing // flash. A flash write stalls XIP execution on the RP2040 (blanking the display), so // the rotary-driven path applies immediately but debounces the save (see loopCallback). void setInputSource(INPUT_SOURCE src, bool persist = true) { input_source_ = src; updateUnusedInputDefault(); if (persist) saveInputSource(); if (nnInputsGraphView) nnInputsGraphView->setNumDisplayBars(getActiveInputCount()); } // ISR-safe entry point (the rotary-encoder dispatch runs in interrupt context). // setInputSource() does heap allocation (bar-graph resize), SPI (fillRect) and flash // file IO — all unsafe in an ISR — so only record the request here and let the main // loop apply it via the pendingInputSourceChange_ handler in bind_RL_interface(). void requestInputSource(INPUT_SOURCE src) { pendingInputSource_ = src; pendingInputSourceChange_ = true; } void addInputSourceView(bool includeCCSelect = true); void SetMIDI5Callback(OnMIDICtrlCallback _cb_) { midi5cb = _cb_; } void SetMIDI6Callback(OnMIDICtrlCallback _cb_) { midi6cb = _cb_; } // Display views std::shared_ptr msgView; std::shared_ptr fileSaveView; std::shared_ptr fileLoadView; std::shared_ptr nameInputView; std::shared_ptr nnInputsGraphView; std::shared_ptr nnOutputsGraphView; std::shared_ptr memoryStoreModeView; std::shared_ptr ccSelectView; const std::vector& getLastAction() const { return action; } protected: // Helper methods for trigger actions void _perform_like_action(); void _perform_dislike_action(); void _perform_randomiseRL_action(); bool _save_RL_to_SD(String id); bool _load_RL_from_SD(String id); void _forget_replay_mem_interf(); void _saveSlotNames(); void _loadSlotNames(); static constexpr int kNumSlots = 12; String slotNames[kNumSlots]; int pendingSaveSlot = -1; private: OnMIDICtrlCallback midi5cb = nullptr; OnMIDICtrlCallback midi6cb = nullptr; RVCallback rvX1Override; RVCallback rvY1Override; RVCallback rvZ1Override; static constexpr size_t bias=1; size_t optimiseDivisor = 1; size_t optimiseCounter = 0; bool newInput=false; bool actionBeingDragged=false; std::vector itemsToRemove; float raw_joystick_[4] = {}; float raw_ml_[6] = {}; float raw_midi_[8] = {}; // Constant used to pad the unused NN input dims; recomputed only on input-mode change. float unusedInputDefault_ = 0.5f; static constexpr const char* kInputSourceFile = "/input_source.bin"; void assembleInputs(); void copyAndZero(const float* src, size_t n); void saveInputSource(); void loadInputSource(); void saveCCNumbers(); void loadCCNumbers(); size_t analysisParamsOffset = 0; MEMORY_STORE_MODES memoryStoreMode = MEMORY_STORE_MODES::REPLACE_10_PERCENT; std::array memOptions = {"Add", "Replace 5%", "Replace 10%", "Replace 15%", "Reward Decay 10%", "Reward Decay 20%"}; // Dislike repulsion: how far a 'no' moves the disliked action's training target away // from the liked region (untapered), and the negative-batch LR base. Bigger = a 'no' // slides the sound clearly further away; >1 tends to push params to the [0,1] rails. static constexpr float kGeometricPushScale = 1.0f; static constexpr float kNegLRBase = 1.5f; // negLRRatio = kNegLRBase - 0.4*negFraction // A 'no' pushes at full strength for this long (wall-clock), then expires — no decay. // The number of optimise cycles within the window depends on the mode's NN update rate. static constexpr uint32_t kDislikeLifetimeMs = 2500; static constexpr size_t kCentroidK = 4; std::vector activeDims_; bool removeItemsAtDistance(std::vector &experienceState, const float distThreshold, const float reward); void decayItemsAtDistance(std::vector &experienceState, const float distThreshold); std::vector layers_nodes; const bool use_constant_weight_init = false; const float constant_weight_init = 0; SynthMLP synthMapping; // value member -> lives in the (static) mode object: zero heap float learningRate = 1e-3; float learningRateScaled = learningRate; std::vector action; ReplayMemory replayMem; static constexpr size_t memoryLimit = 64; static constexpr size_t batchSize = 8; std::vector mappingOutput; std::vector controlInput; std::vector savedAction; float rewardScale = 1.0f; // OrnsteinUhlenbeckNoise ou_noise; std::vector> ou_noises; // Exploration-noise travel speed. static constexpr float kNoiseDt = 0.004f; // normal OU travel speed (set in setup) // Jolt = permanent weight modulation while B2 is held (see startJolt/stepJolt). static constexpr size_t kJoltNumWeights = 40; // random weights perturbed per press static constexpr float kJoltMorphRate = 0.017f; // EMA per tick (~1s to target @200Hz) static constexpr float kJoltWeightMin = -1.2f; // target range == weight-init range static constexpr float kJoltWeightMax = 0.9f; static constexpr float kJoltTargetEpsilon = 0.05f; // re-roll target once within this static constexpr float kJoltLRRampStep = 0.001f; // LR recovery rate: 1/(5s * 200Hz) std::vector joltWeightLoc_; // global flat weight indices (StaticMLP::WeightPtrAt) std::vector joltTarget_; // per-selected-weight target value bool joltActive_ = false; // After a jolt releases, learning resumes gently: effective LR *= joltLRRamp_, which // climbs 0 -> 1 over ~5s so fresh training doesn't immediately drag the net off the // jolted sound. 1.0 = normal (full LR). float joltLRRamp_ = 1.0f; bool resetMinMaxFlag = false; // Deferred actions: set from ISR, consumed in main-loop loopCallback before optimise() volatile bool pendingLike_{false}; volatile bool pendingDislike_{false}; volatile bool pendingDragStore_{false}; // drag-release: store savedAction volatile bool pendingInputSourceChange_{false}; // input-source change: deferred from rotary ISR INPUT_SOURCE pendingInputSource_{INPUT_SOURCE::JOYSTICK_3D}; // Debounced flash persistence: a save is scheduled this many ms after the last change, // so scrolling through sources doesn't trigger a flash write (XIP stall) per detent. static constexpr uint32_t kInputSourceSaveDelayMs = 600; uint32_t inputSourceSaveDueMs_ = 0; // 0 = no save pending spin_lock_t *mlpActive; String _modeRoot{"mlp_rl"}; String _modeTag{"Unknown"}; ExtraSaveDataFn _extraSaveFn; ExtraLoadDataFn _extraLoadFn; }; // Template method definitions (header-only so the per-mode instantiation is // available wherever InterfaceRL is used). #include "InterfaceRL.tpp" #endif // INTERFACERL_HPP