diff --git a/MEMLNaut-NISPS.ino b/MEMLNaut-NISPS.ino index 63e4531..3400ce1 100644 --- a/MEMLNaut-NISPS.ino +++ b/MEMLNaut-NISPS.ino @@ -25,13 +25,15 @@ #include "modes/MEMLNautModeXIASRI.hpp" #include "modes/MEMLNautModeBreakOr.hpp" #include "modes/MEMLNautModeVerbFX.hpp" +#include "modes/MEMLNautModeElysiamorfs.hpp" -//hook up the memlnaut mode +//hook up the memlnaut mode // #define MEMLNAUT_MODE_TYPE MEMLNautModeSoundAnalysisMIDI // #define MEMLNAUT_MODE_TYPE MEMLNautModeXIASRI // #define MEMLNAUT_MODE_TYPE MEMLNautModeVerbFX -#define MEMLNAUT_MODE_TYPE MEMLNautModeBreakOr +// #define MEMLNAUT_MODE_TYPE MEMLNautModeBreakOr +#define MEMLNAUT_MODE_TYPE MEMLNautModeElysiamorfs // #define MEMLNAUT_MODE_TYPE MEMLNautModeChannelStrip // #define MEMLNAUT_MODE_TYPE MEMLNautModePAFSynth diff --git a/modes/AudioApps/BreakOrAudioApp.hpp b/modes/AudioApps/BreakOrAudioApp.hpp index 52d0de0..9d96a40 100644 --- a/modes/AudioApps/BreakOrAudioApp.hpp +++ b/modes/AudioApps/BreakOrAudioApp.hpp @@ -17,47 +17,7 @@ #include "../../voicespaces/VoiceSpaces.hpp" -struct ratioSeqState { - std::array ratios{1.f}; - float phasor=0.f; - float phasorInc=0.f; - float phaseOffset = 0.f; - bool lastTrig = false; - float phasorMul = 1.f; - float ratioSum=1.f; - int midiNote = 36; - float pulseWidth = 0.5f; - - std::array ampRatios{1.f}; - float ampRatioSum=1.f; - -}; - -template -inline bool __not_in_flash_func(ratioSeq)(float phasor, float phaseOffset, float ratioSum, const std::array &ratios, float pulseWidth) { - bool trig = 0; - float offsetPhase = phaseOffset + phasor; - if (offsetPhase >= 1.f) { - offsetPhase -= 1.f; - } - float phaseAdj = ratioSum * offsetPhase; - float accumulatedSum = 0; - float lastAccumulatedSum = 0; - for (size_t v : ratios) - { - accumulatedSum += v; - if (phaseAdj <= accumulatedSum) - { - // check pulse width - float beatPhase = (phaseAdj - lastAccumulatedSum) / - (accumulatedSum - lastAccumulatedSum); - trig = beatPhase <= pulseWidth; - break; - } - lastAccumulatedSum = accumulatedSum; - } - return trig; -} +#include "RatioSeq.hpp" template @@ -143,7 +103,6 @@ public: int phasorResetValue=0; if (queue_try_remove(&barPhaseResetQueue, &phasorResetValue)) { barPhasor = 0.f; - Serial.println("Bar phase reset triggered by queue"); } } int i2cIdx=0; diff --git a/modes/AudioApps/ElysiamorfAudioApp.hpp b/modes/AudioApps/ElysiamorfAudioApp.hpp new file mode 100644 index 0000000..7913822 --- /dev/null +++ b/modes/AudioApps/ElysiamorfAudioApp.hpp @@ -0,0 +1,324 @@ +#ifndef __ELYSIAMORF_AUDIO_APP_HPP__ +#define __ELYSIAMORF_AUDIO_APP_HPP__ + +#include "../../src/memllib/audio/AudioAppBase.hpp" +#include "../../src/memllib/synth/maximilian.h" +#include "../../src/memllib/interface/MIDIInOut.hpp" + + +#include +#include +#include // Added for std::shared_ptr + +#include "../../src/memllib/interface/InterfaceBase.hpp" + + +#include + +#include "../../voicespaces/VoiceSpaces.hpp" + + +struct FMOp { + float prevOutput = 0.f; + + // phase: external phasor 0-1 (e.g. barPhasor * phasorMul) + // modIn: signal from another operator [-1, 1], 0 = none + // freqMul: cycles per phase period (1 = once per bar, 2 = twice, 0.5 = half, etc.) + // modIndex: depth of modulation — scales how much modIn shifts the phase + // fbLevel: feedback 0-1 — feeds prevOutput back into own phase + float __force_inline process(float phase, float modIn, + float freqMul, float modIndex, + float fbLevel) { + float p = fmodf(phase * freqMul + modIndex * modIn + fbLevel * prevOutput, 1.f); + if (p < 0.f) p += 1.f; + float out = sinf(TWO_PI * p); + prevOutput = out; + return out; + } +}; + +// Convenience: 2-op FM pair matching the (phase, carrierFreq, modFreq, modIndex, fbLevel) signature. +// fbLevel sits on the modulator (DX7 style). carrier and modulator are caller-owned FMOp instances. +float __force_inline fmPair(float phase, + float carrierFreq, float modFreq, + float modIndex, float fbLevel, + FMOp& carrier, FMOp& modulator) { + float modOut = modulator.process(phase, 0.f, modFreq, 0.f, fbLevel); + return carrier.process(phase, modOut, carrierFreq, modIndex, 0.f); +} + +struct fmSeqState { + float phaseOffset = 0.f; + float phasorMul = 1.f; + float ratio=1.f; + float carrierFreq=1.f; + float modFreq = 2.f; + float modIndex = 2.f; + float fbLevel = 0.f; + FMOp carrier; + FMOp modulator; +}; + + +#include "RatioSeq.hpp" + +template +class ElysiamorfAudioApp : public AudioAppBase +{ +public: + static constexpr size_t kN_Params = NPARAMS; + static constexpr size_t nVoiceSpaces=0; + + queue_t bpmQueue; + queue_t sequencerControlQueue; + queue_t i2cOutQueue; + queue_t barPhaseResetQueue; + + enum SequencerClockModes { + INTERNAL, + MIDI_CLOCK + } sequencerClockMode = INTERNAL; + + + bool sequencerPlaying = false; + + std::shared_ptr midiIO; + + std::array, nVoiceSpaces> voiceSpaces; + + VoiceSpaceFn currentVoiceSpace; + + std::array getVoiceSpaceNames() { + std::array names; + for(size_t i=0; i < voiceSpaces.size(); i++) { + names[i] = voiceSpaces[i].name; + } + return names; + } + + void setVoiceSpace(size_t i) { + if (i < voiceSpaces.size()) { + currentVoiceSpace = voiceSpaces[i].mappingFunction; + } + } + + ElysiamorfAudioApp() : AudioAppBase() { + // currentVoiceSpace = voiceSpaces[0].mappingFunction; + queue_init(&bpmQueue, sizeof(float), 1); + queue_init(&sequencerControlQueue, sizeof(int), 1); + queue_init(&i2cOutQueue, sizeof(float) * 8, 1); + queue_init(&barPhaseResetQueue, sizeof(int), 1); + }; + + bool __force_inline euclidean(float phase, const size_t n, const size_t k, const size_t offset, const float pulseWidth) + { + // Euclidean function + const float fi = phase * n; + int i = static_cast(fi); + const float rem = fi - i; + if (i == n) + { + i--; + } + const int idx = ((i + n - offset) * k) % n; + return (idx < k && rem < pulseWidth) ? 1 : 0; + } + + stereosample_t __force_inline Process(const stereosample_t x) override + { + if (sequencerPlaying) { + if (sequencerClockMode == INTERNAL){ + midiClockPhasor += midiClockPhasorInc; + if (midiClockPhasor >= 1.f) { + midiClockPhasor -= 1.f; + midiIO->queueClock(); + midiIO->flushQueue(); + } + } + if (sequencingSampleCounter==0) { + + barPhasor += barPhasorInc; + if (barPhasor >= 1.f) { + barPhasor -= 1.f; + } + if (sequencerClockMode == MIDI_CLOCK) { + int phasorResetValue=0; + if (queue_try_remove(&barPhaseResetQueue, &phasorResetValue)) { + barPhasor = 0.f; + } + } + // int i2cIdx=0; + int ccIndex=0; + static int ccNumbers[8] = {1,2,3,4,5,9,11,12}; + // for(auto &seq: ratioSeqStates) { + // //update phasor + // float seqPhasor = barPhasor * seq.phasorMul; + // seqPhasor = fmodf(seqPhasor + seq.phaseOffset, 1.f); // Wrap phasor to [0,1] + + // bool trig = ratioSeq<3>(seqPhasor, seq.phaseOffset, seq.ratioSum, seq.ratios, seq.pulseWidth); + // bool highAmp = ratioSeq<2>(seqPhasor, seq.phaseOffset, seq.ampRatioSum, seq.ampRatios, 0.5f); + // if (trig != seq.lastTrig) { + // midiIO->queueCC(ccNumbers[ccIndex++], trig ? highAmp ? 127 : 64 : 0); + // // Serial.printf("Seq %d - Phasor: %f, Trig: %d, HighAmp: %d\n", &seq - &ratioSeqStates[0], seqPhasor, trig, highAmp); + // } + // seq.lastTrig = trig; + // // i2cValues[i2cIdx++] = trig; + // } + for(auto &seq: fmSeqStates) { + //update phasor + float seqPhasor = barPhasor * seq.phasorMul; + seqPhasor = fmodf(seqPhasor + seq.phaseOffset, 1.f); // Wrap phasor to [0,1] + float fmVal = fmPair(seqPhasor, seq.carrierFreq, seq.modFreq, seq.modIndex, seq.fbLevel, seq.carrier, seq.modulator); + fmVal = (fmVal + 1.f) * 0.5f; // Normalize to [0,1] + fmVal *= 127.f; // Scale to MIDI range + // if (ccIndex==0) { + // Serial.printf("Seq %d - Phasor: %f, FM Val: %f\n", &seq - &fmSeqStates[0], seqPhasor, fmVal); + // } + midiIO->queueCC(ccNumbers[ccIndex++], static_cast(fmVal)); + } + midiIO->flushQueue(); + // queue_try_add(&i2cOutQueue, &i2cValues); + } + + sequencingSampleCounter ++; + if (sequencingSampleCounter >= sequencingSampleDiv) { + sequencingSampleCounter = 0; + } + + + } + stereosample_t ret { 0.f,0.f }; + return ret; + } + + void Setup(float sample_rate, std::shared_ptr interface, SequencerClockModes clockMode) + { + AudioAppBase::Setup(sample_rate, interface); + maxiSettings::sampleRate = sample_rate; + sampleRatef = static_cast(sample_rate); + sequencerClockMode = clockMode; + updateBPM(90.f); + } + + void setupMIDI(std::shared_ptr new_midi_interf) { + midiIO = new_midi_interf; + } + + void loop() override { + AudioAppBase::loop(); + } + + void ProcessParams(const std::array& params) + { + firstParamsReceived = true; + if (queue_try_remove(&bpmQueue, &bpm)) { + updateBPM(bpm); + Serial.printf("BPM updated: %f\n", bpm); + } + int sequencerControl; + if (queue_try_remove(&sequencerControlQueue, &sequencerControl)) { + sequencerPlaying = sequencerControl == 1; + if (!sequencerPlaying) { + midiIO->queueClockStop(); + // Send note offs for all sequences when stopping + for(auto &seq: ratioSeqStates) { + midiIO->queueNoteOff(seq.midiNote, 0); + seq.phasor = 0.f; // Reset phasor to start when stopping + } + midiIO->flushQueue(); + midiClockPhasor = 0.f; // Reset MIDI clock phasor when stopping + sequencingSampleCounter = 0; // Reset sequencing sample counter + + }else{ + midiIO->queueClockStart(); + midiIO->flushQueue(); + } + Serial.printf("Sequencer %s\n", sequencerPlaying ? "Playing" : "Stopped"); + } + // currentVoiceSpace(params); + // size_t paramIdx = 0; + // for(auto &v: ratioSeqStates) { + // float sum=0.f; + // for(size_t i=0; i < v.ratios.size(); i++) { + // v.ratios[i] = (float)(int)(params[paramIdx++] * 3.f) + 1.f; + // sum += v.ratios[i]; + // } + // v.ratioSum = sum; + // // static float muls[7] = {0.25f, 0.33f, 0.5f, 1.f, 1.5f, 2.f, 3.f}; + // // v.phasorMul = muls[(int)(params[paramIdx++] * 6.999999f)]; + // static float muls[4] = {1.f, 2.f, 4.f, 8.f}; + // v.phasorMul = muls[(int)(params[paramIdx++] * 3.999999f)]; + // v.phaseOffset = ((int)(params[paramIdx++] * timeSigBeats)) * timeSigBeatsInv; + + // sum=0.f; + // for(size_t i=0; i < v.ampRatios.size(); i++) { + // v.ampRatios[i] = (float)(int)(params[paramIdx++] * 3.f) + 1.f; + // sum += v.ampRatios[i]; + // } + // v.ampRatioSum = sum; + // } + size_t paramIdx = 0; + for(auto &v: fmSeqStates) { + v.carrierFreq = (0.25f + params[paramIdx++] * 0.75f) * 0.125f; // 1 to 10 + v.modFreq = (0.25f + params[paramIdx++] * 0.75f) * 0.25f; // 1 to 10 + v.modIndex = params[paramIdx++] * 4.f; // 0 to 10 + v.fbLevel = 0.f;//params[paramIdx++] ; // 0 to 1 + static float muls[4] = {1.f}; + v.phasorMul = 1.f;//muls[(int)(params[paramIdx++])]; + v.phaseOffset = ((int)(params[paramIdx++] * timeSigBeats)) * timeSigBeatsInv; + } + // Serial.printf("pm: %f", ratioSeqStates[0].phasorMul); + + } + + void updateBPM(float newBPM) { + bpm = newBPM; + float beatLengthInSeconds = 60.f / bpm; + float barLengthInSeconds = beatLengthInSeconds * timeSigBeats; + float barLengthInSamples = barLengthInSeconds * (sampleRatef/ sequencingSampleDiv); + barPhasorInc = 1.f/ barLengthInSamples; + // for(auto &v: ratioSeqStates) { + // v.phasorInc = barPhasorInc; + // } + float midiClockLengthInSeconds = beatLengthInSeconds / 24.f; + float midiClockLengthInSamples = midiClockLengthInSeconds * sampleRatef; + midiClockPhasorInc = 1.f / midiClockLengthInSamples; + } + + void setTimeSignature(float beats, float division) { + timeSigBeats = beats; + timeSigDivision = division; + updateBPM(bpm); // Recalculate phasor increment with new time signature + } + + + +protected: + + float i2cValues[8] = {0,0,0,0,0,0,0,0}; + + float sampleRatef; + bool firstParamsReceived = false; + + float bpm=90.f; + float timeSigBeats=4.f; + float timeSigBeatsInv=1.f/timeSigBeats; + float timeSigDivision=4.f; + + float barPhasorInc=0.f; + float barPhasor; + + size_t sequencingSampleDiv = 500; + size_t sequencingSampleCounter = 0; + + std::array ratioSeqStates; + std::array fmSeqStates; + + + float midiClockPhasor=0; + float midiClockPhasorInc=0; + +}; + +#endif // __ELYSIAMORF_AUDIO_APP_HPP__ + diff --git a/modes/AudioApps/RatioSeq.hpp b/modes/AudioApps/RatioSeq.hpp new file mode 100644 index 0000000..c522764 --- /dev/null +++ b/modes/AudioApps/RatioSeq.hpp @@ -0,0 +1,49 @@ +#ifndef __RATIO_SEQ_HPP__ +#define __RATIO_SEQ_HPP__ + +#include +#include + +struct ratioSeqState { + std::array ratios{1.f}; + float phasor=0.f; + float phasorInc=0.f; + float phaseOffset = 0.f; + bool lastTrig = false; + float phasorMul = 1.f; + float ratioSum=1.f; + int midiNote = 36; + float pulseWidth = 0.5f; + + std::array ampRatios{1.f}; + float ampRatioSum=1.f; + +}; + +template +inline bool __not_in_flash_func(ratioSeq)(float phasor, float phaseOffset, float ratioSum, const std::array &ratios, float pulseWidth) { + bool trig = 0; + float offsetPhase = phaseOffset + phasor; + if (offsetPhase >= 1.f) { + offsetPhase -= 1.f; + } + float phaseAdj = ratioSum * offsetPhase; + float accumulatedSum = 0; + float lastAccumulatedSum = 0; + for (size_t v : ratios) + { + accumulatedSum += v; + if (phaseAdj <= accumulatedSum) + { + // check pulse width + float beatPhase = (phaseAdj - lastAccumulatedSum) / + (accumulatedSum - lastAccumulatedSum); + trig = beatPhase <= pulseWidth; + break; + } + lastAccumulatedSum = accumulatedSum; + } + return trig; +} + +#endif // __RATIO_SEQ_HPP__ diff --git a/modes/MEMLNautModeElysiamorfs.hpp b/modes/MEMLNautModeElysiamorfs.hpp new file mode 100644 index 0000000..5d6f6f8 --- /dev/null +++ b/modes/MEMLNautModeElysiamorfs.hpp @@ -0,0 +1,121 @@ +#ifndef MEMLNAUT_MODE_ELYSIAMORFS_HPP +#define MEMLNAUT_MODE_ELYSIAMORFS_HPP + +//Elysiamorfs + +#include "../src/memllib/interface/MIDIInOut.hpp" +#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp" +#include "./AudioApps/ElysiamorfAudioApp.hpp" +#include "../src/memllib/examples/InterfaceRL.hpp" +#include "../src/memllib/PicoDefs.hpp" +#include "MEMLNautMode.hpp" +#include +#include +#include "../src/memllib/interface/USeqI2C.hpp" + + +class MEMLNautModeElysiamorfs { +public: + constexpr static size_t kN_InputParams = 4; // joystick x, y, rotate + + USeqI2C i2cOut; + inline static ElysiamorfAudioApp<> audioAppElysiamorfs; + std::array::nVoiceSpaces> voiceSpaceList; + + InterfaceRL interface; + std::shared_ptr interfacePtr; + + ElysiamorfAudioApp<>::SequencerClockModes clockMode = ElysiamorfAudioApp<>::MIDI_CLOCK; + + bool sequencerPlaying = false; + + void setupInterface() { + interface.setup(kN_InputParams, ElysiamorfAudioApp<>::kN_Params); + interface.bindInterface(InterfaceRL::INPUT_MODES::JOYSTICK, true); + interfacePtr = make_non_owning(interface); + + MEMLNaut::Instance()->setTogA2Callback([this](bool state) { // scr_ref no longer captured directly + Serial.println(state ? "TogA2 ON" : "TogA2 OFF"); + if (state) { + sequencerPlaying = !sequencerPlaying; + queue_try_add(&audioAppElysiamorfs.sequencerControlQueue, &sequencerPlaying); + } + + }); + + i2cOut.begin(); + } + + String getHelpTitle() { + return "Elysiamorfs"; + } + + __force_inline stereosample_t process(stereosample_t x) { + return audioAppElysiamorfs.Process(x); + } + + void setupAudio(float sample_rate) { + audioAppElysiamorfs.Setup(sample_rate, interfacePtr, clockMode); + voiceSpaceList = audioAppElysiamorfs.getVoiceSpaceNames(); + audioAppElysiamorfs.setupMIDI(midi_interf); + MEMLNaut::Instance()->setRVGain1Callback([this](float value) { + if (clockMode == ElysiamorfAudioApp<>::INTERNAL) { + float bpm = 30.f + (value * 200.f); // Scale BPM from [0,1] to [30,230] + queue_try_add(&audioAppElysiamorfs.bpmQueue, &bpm); + } + }, 0); // Set threshold to 0 to trigger on any change + + } + + __force_inline void loop() { + audioAppElysiamorfs.loop(); + } + + std::shared_ptr midi_interf; + + void setupMIDI(std::shared_ptr new_midi_interf) { + midi_interf = new_midi_interf; + midi_interf->Setup(0); + // midi_interf->SetMIDISendChannel(1); + // midi_interf->SetMIDINoteChannel(10); + midi_interf->SetNoteCallback([this](bool noteon, uint8_t note_number, uint8_t vel_value) { + // Serial.printf("MIDI Note %d: %d %d\n", note_number, vel_value, noteon); + if (note_number==0) { + int resetTrigger=1; + queue_try_add(&audioAppElysiamorfs.barPhaseResetQueue, &resetTrigger); // value doesn't matter, just a trigger + Serial.println("Bar phase reset triggered by MIDI note 0"); + + } + }); + midi_interf->SetBPMCallback([this](float bpm) { + if (clockMode == ElysiamorfAudioApp<>::MIDI_CLOCK) { + queue_try_add(&audioAppElysiamorfs.bpmQueue, &bpm); + } + + }); + interface.bindMIDI(midi_interf); + } + + void addViews() { + }; + + inline void processAnalysisParams() {} + + void analyse(stereosample_t) {} + + float i2cValues[8]; + void loopCore0() { + // PERIODIC_RUN_US( + // if (queue_try_remove(&audioAppElysiamorfs.i2cOutQueue, &i2cValues)) { + // // Serial.printf("i2c received: %f %f %f %f %f %f %f %f\n", i2cValues[0], i2cValues[1], i2cValues[2], i2cValues[3], i2cValues[4], i2cValues[5], i2cValues[6], i2cValues[7]); + // bool i2cSuccess = i2cOut.sendValues(i2cValues, 8); + // } + // , 5000) + + } + +}; + + + +#endif // MEMLNAUT_MODE_ELYSIAMORFS_HPP