diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 18c331bf..c77e2958 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -15,8 +15,8 @@ jobs: days-before-issue-stale: 10 days-before-issue-close: 10 stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." - close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + stale-issue-message: "This issue is stale because it has been open for 10 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 10 days since being marked as stale." days-before-pr-stale: -1 days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b04a0d2d..a7a28d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +# v0.2.0 (29 Febrary 2024) +- Stochastic Track + - global octave modifier + - launchpad control general octave + - loop and lock loop + - reseed + - rest probability 2,4,8 steps + - global gate length modifier + - clipboard actions + - generators +- Load/Save Sequence to use a sequence library (fast switch on loading) +- Launchpad Performance Mode `8`+`3` (`2`+`GRID2` -> quick set lenght sequence; `2`+`GRID1` -> overview page ) +- Submenu shortcuts (double click F[1-5] to enter project, layout, routing, midi out, user scale) +- Page buttons on launchpad circuit note edit +- Extend gate Lenght to 4bits [@glibersat](https://github.com/glibersat) +- Multi Curve CV Recording (cv curve input has been moved in track page) +- quick change octave shortuct (step+F[1-5]) 1-5V +- quick gate accent launchpad control on gate page and circuit page (`7`+`GRID`) +- add steps to stop feature in project page. Once started when the engine reaches the steps to stop value the clock will stop. +- improved overview page. quick edit tracks +- + +> **testers** : +> +> mebitek, Guillaume Libersat, Nick Ansell, XponentOne, dblu2000, hales2488, XQSTKRPS, KittenVillage, Andreas Hieninger + # v0.1.4.48 (30 January 2024) - Moving steps in a sequence - INIT by step selected @@ -15,7 +41,7 @@ # v0.1.4.47 (24 January 2024) - launchpad circuit mode improvements - random generator: random seed just on init method -- patter follow +- patter follow [@glibersat](https://github.com/glibersat) - pattern chain quick shortcut from pattern page - scale edit: scales are changed only if the encoder is pressed - on scale change the sequence notes are changed according to the previous scale. if a note in the previous scale is also present in the new scale the value is preeserved. if it is not present the nearest note in the new scale will be selected @@ -23,7 +49,7 @@ ## v0.1.46 (4 January 2024) -- UI note edit page reaggment +- UI note edit page reaggment [@aclangor](https://github.com/aclangor) - song sync with clock - fix reverse shape feature - add new shapes diff --git a/README.md b/README.md index 470eca6b..14b7322d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a fork of the [original repository](https://github.com/westlicht/perform [Full updated documentation](https://mebitek.github.io/performer/manual/) -To find out more about improvements changes, check [changelog](https://github.com/mebitek/performer/blob/master/CHANGELOG.md) and the [WIKI page ](https://github.com/mebitek/performer/wiki) +To find out more about improvements changes, check [changelog](https://github.com/mebitek/performer/blob/master/CHANGELOG.md) --- original documentation below --- diff --git a/src/apps/sequencer/CMakeLists.txt b/src/apps/sequencer/CMakeLists.txt index 23de6069..8cc0f9f1 100644 --- a/src/apps/sequencer/CMakeLists.txt +++ b/src/apps/sequencer/CMakeLists.txt @@ -14,6 +14,7 @@ set(sources engine/MidiLearn.cpp engine/MidiOutputEngine.cpp engine/NoteTrackEngine.cpp + engine/StochasticEngine.cpp engine/RoutingEngine.cpp engine/SequenceState.cpp # engine/generators @@ -38,6 +39,8 @@ set(sources model/ModelUtils.cpp model/NoteSequence.cpp model/NoteTrack.cpp + model/StochasticSequence.cpp + model/StochasticTrack.cpp model/PlayState.cpp model/Project.cpp model/Routing.cpp @@ -84,6 +87,8 @@ set(sources ui/pages/MonitorPage.cpp ui/pages/NoteSequenceEditPage.cpp ui/pages/NoteSequencePage.cpp + ui/pages/StochasticSequencePage.cpp + ui/pages/StochasticSequenceEditPage.cpp ui/pages/OverviewPage.cpp ui/pages/PatternPage.cpp ui/pages/PerformerPage.cpp diff --git a/src/apps/sequencer/Config.h b/src/apps/sequencer/Config.h index 97507284..2598580a 100644 --- a/src/apps/sequencer/Config.h +++ b/src/apps/sequencer/Config.h @@ -6,8 +6,8 @@ #define CONFIG_VERSION_MAGIC 0xfadebabe #define CONFIG_VERSION_NAME "PER|FORMER SEQUENCER" #define CONFIG_VERSION_MAJOR 0 -#define CONFIG_VERSION_MINOR 1 -#define CONFIG_VERSION_REVISION 48 +#define CONFIG_VERSION_MINOR 2 +#define CONFIG_VERSION_REVISION 0 // Task priorities #define CONFIG_DRIVER_TASK_PRIORITY 5 diff --git a/src/apps/sequencer/engine/Clock.cpp b/src/apps/sequencer/engine/Clock.cpp index ab340416..425e7c1b 100644 --- a/src/apps/sequencer/engine/Clock.cpp +++ b/src/apps/sequencer/engine/Clock.cpp @@ -419,7 +419,7 @@ void Clock::outputTick(uint32_t tick) { if (tick == _output.nextTick) { uint32_t divisor = _output.divisor; - uint32_t clockDuration = std::max(uint32_t(10), uint32_t(_masterBpm * _ppqn * _output.pulse / (60 * 1000))); + uint32_t clockDuration = std::max(uint32_t(1), uint32_t(_masterBpm * _ppqn * _output.pulse / (60 * 1000))); _output.nextTickOn = applySwing(_output.nextTick); _output.nextTickOff = std::min(_output.nextTickOn + clockDuration, applySwing(_output.nextTick + divisor) - 1); diff --git a/src/apps/sequencer/engine/CurveTrackEngine.cpp b/src/apps/sequencer/engine/CurveTrackEngine.cpp index 98894d32..06c34b68 100644 --- a/src/apps/sequencer/engine/CurveTrackEngine.cpp +++ b/src/apps/sequencer/engine/CurveTrackEngine.cpp @@ -80,6 +80,11 @@ TrackEngine::TickResult CurveTrackEngine::tick(uint32_t tick) { uint32_t resetDivisor = sequence.resetMeasure() * _engine.measureDivisor(); uint32_t relativeTick = resetDivisor == 0 ? tick : tick % resetDivisor; + + if (int(_model.project().stepsToStop()) != 0 && int(relativeTick / divisor) == int(_model.project().stepsToStop())) { + _engine.clockStop(); + } + // handle reset measure if (relativeTick == 0) { reset(); @@ -232,14 +237,15 @@ void CurveTrackEngine::updateOutput(uint32_t relativeTick, uint32_t divisor) { bool CurveTrackEngine::isRecording() const { return _engine.state().recording() && - _model.project().curveCvInput() != Types::CurveCvInput::Off && - _model.project().selectedTrackIndex() == _track.trackIndex(); + _curveTrack.curveCvInput() != Types::CurveCvInput::Off; + //&& + //_model.project().selectedTrackIndex() == _track.trackIndex(); } void CurveTrackEngine::updateRecordValue() { auto &sequence = *_sequence; const auto &range = Types::voltageRangeInfo(sequence.range()); - auto curveCvInput = _model.project().curveCvInput(); + auto curveCvInput = _curveTrack.curveCvInput(); switch (curveCvInput) { case Types::CurveCvInput::Cv1: diff --git a/src/apps/sequencer/engine/Engine.cpp b/src/apps/sequencer/engine/Engine.cpp index 1a1509ae..278e8673 100644 --- a/src/apps/sequencer/engine/Engine.cpp +++ b/src/apps/sequencer/engine/Engine.cpp @@ -435,6 +435,11 @@ void Engine::updateTrackSetups() { track.midiCvTrack().setName(str); } break; + case Track::TrackMode::Stochastic: + trackEngine = trackContainer.create(*this, _model, track, linkedTrackEngine); + if (sizeof(track.stochasticTrack().name()==0)) { + track.stochasticTrack().setName(str); + } break; case Track::TrackMode::Last: break; diff --git a/src/apps/sequencer/engine/Engine.h b/src/apps/sequencer/engine/Engine.h index 8b98c578..c5d1e571 100644 --- a/src/apps/sequencer/engine/Engine.h +++ b/src/apps/sequencer/engine/Engine.h @@ -12,6 +12,7 @@ #include "CvOutput.h" #include "RoutingEngine.h" #include "MidiOutputEngine.h" +#include "StochasticEngine.h" #include "MidiPort.h" #include "MidiLearn.h" #include "CvGateToMidiConverter.h" @@ -33,7 +34,7 @@ class Engine : private Clock::Listener { public: - typedef Container TrackEngineContainer; + typedef Container TrackEngineContainer; typedef std::array TrackEngineContainerArray; typedef std::array TrackEngineArray; typedef std::array, CONFIG_TRACK_COUNT> TrackUpdateReducerArray; diff --git a/src/apps/sequencer/engine/NoteTrackEngine.cpp b/src/apps/sequencer/engine/NoteTrackEngine.cpp index 7f3355bd..5c2737d0 100644 --- a/src/apps/sequencer/engine/NoteTrackEngine.cpp +++ b/src/apps/sequencer/engine/NoteTrackEngine.cpp @@ -142,6 +142,10 @@ TrackEngine::TickResult NoteTrackEngine::tick(uint32_t tick) { uint32_t resetDivisor = sequence.resetMeasure() * _engine.measureDivisor(); uint32_t relativeTick = resetDivisor == 0 ? tick : tick % resetDivisor; + if (int(_model.project().stepsToStop()) != 0 && int(relativeTick / divisor) == int(_model.project().stepsToStop())) { + _engine.clockStop(); + } + // handle reset measure if (relativeTick == 0) { reset(); @@ -173,23 +177,34 @@ TrackEngine::TickResult NoteTrackEngine::tick(uint32_t tick) { _freeRelativeTick = 0; } if (relativeTick == 0) { - if (_currentStageRepeat == 1) { _sequenceState.advanceFree(sequence.runMode(), sequence.firstStep(), sequence.lastStep(), rng); + _sequenceState.calculateNextStepFree( + sequence.runMode(), sequence.firstStep(), sequence.lastStep(), rng); } recordStep(tick, divisor); const auto &step = sequence.step(_sequenceState.step()); - bool isLastStageStep = ((int) (step.stageRepeats()+1) - (int) _currentStageRepeat) <= 0; + bool isLastStageStep = ((int) step.stageRepeats() - (int) _currentStageRepeat) <= 0; + + if (step.gateOffset() >= 0) { + triggerStep(tick, divisor); + } - triggerStep(tick+divisor, divisor); + if (!isLastStageStep && step.gateOffset() < 0) { + triggerStep(tick + divisor, divisor, false); + } + if (isLastStageStep + && sequence.step(_sequenceState.nextStep()).gateOffset() < 0) { + triggerStep(tick + divisor, divisor, true); + } + if (isLastStageStep) { - _currentStageRepeat = 1; + _currentStageRepeat = 1; } else { _currentStageRepeat++; } - } break; case Types::PlayMode::Last: @@ -390,7 +405,7 @@ void NoteTrackEngine::triggerStep(uint32_t tick, uint32_t divisor, bool forNextS break; case NoteSequence::StageRepeatMode::Random: srand((unsigned int)time(NULL)); - int rndMode = 0 + ( std::rand() % ( 6 - 0 + 1 ) ); + int rndMode = rng.nextRange(6); switch (rndMode) { case 0: break; @@ -467,7 +482,7 @@ void NoteTrackEngine::recordStep(uint32_t tick, uint32_t divisor) { step.setNoteVariationRange(0); step.setNoteVariationProbability(NoteSequence::NoteVariationProbability::Max); step.setCondition(Types::Condition::Off); - step.setStageRepeats(1); + stepWritten = true; }; diff --git a/src/apps/sequencer/engine/SequenceState.cpp b/src/apps/sequencer/engine/SequenceState.cpp index 61c2a3aa..5d209f22 100644 --- a/src/apps/sequencer/engine/SequenceState.cpp +++ b/src/apps/sequencer/engine/SequenceState.cpp @@ -17,80 +17,21 @@ void SequenceState::reset() { } void SequenceState::advanceFree(Types::RunMode runMode, int firstStep, int lastStep, Random &rng) { - ASSERT(firstStep <= lastStep, "invalid first/last step"); - - _prevStep = _step; - - if (_step == -1) { - // first step - switch (runMode) { - case Types::RunMode::Forward: - case Types::RunMode::Pendulum: - case Types::RunMode::PingPong: - _step = firstStep; - break; - case Types::RunMode::Backward: - _step = lastStep; - break; - case Types::RunMode::Random: - case Types::RunMode::RandomWalk: - _step = randomStep(firstStep, lastStep, rng); - break; - case Types::RunMode::Last: - break; - } - } else { - // advance step - _step = clamp(int(_step), firstStep, lastStep); + ASSERT(firstStep <= lastStep, "invalid first/last step"); - switch (runMode) { - case Types::RunMode::Forward: - if (_step >= lastStep) { - _step = firstStep; - ++_iteration; - } else { - ++_step; - } - break; - case Types::RunMode::Backward: - if (_step <= firstStep) { - _step = lastStep; - ++_iteration; - } else { - --_step; - } - break; - case Types::RunMode::Pendulum: - case Types::RunMode::PingPong: - if (_direction > 0 && _step >= lastStep) { - _direction = -1; - } else if (_direction < 0 && _step <= firstStep) { - _direction = 1; - ++_iteration; - } else { - if (runMode == Types::RunMode::Pendulum) { - _step += _direction; - } - } - if (runMode == Types::RunMode::PingPong) { - _step += _direction; - } - break; - case Types::RunMode::Random: - _step = randomStep(firstStep, lastStep, rng); - break; - case Types::RunMode::RandomWalk: - advanceRandomWalk(firstStep, lastStep, rng); - break; - case Types::RunMode::Last: - break; - } + if (_nextStep < 0) { + calculateNextStepFree(runMode, firstStep, lastStep, rng); } + + _iteration = _nextIteration; + _prevStep = _step; + _step = _nextStep; + _nextStep = -1; } void SequenceState::calculateNextStepFree(Types::RunMode runMode, int firstStep, int lastStep, Random &rng) { - if (_nextStep == -1) { +if (_step == -1) { // first step switch (runMode) { case Types::RunMode::Forward: @@ -116,20 +57,18 @@ void SequenceState::calculateNextStepFree(Types::RunMode runMode, int firstStep, case Types::RunMode::Forward: if (_step >= lastStep) { _nextStep = firstStep; - ++_nextIteration; + _nextIteration = _iteration + 1 ; } else { - ++_nextStep; + _nextStep = _step + 1; } - _direction = 1; break; case Types::RunMode::Backward: if (_step <= firstStep) { _nextStep = lastStep; - ++_nextIteration; + _nextIteration = _iteration + 1 ; } else { - --_nextStep; + _nextStep = _step - 1; } - _direction = -1; break; case Types::RunMode::Pendulum: case Types::RunMode::PingPong: @@ -137,14 +76,14 @@ void SequenceState::calculateNextStepFree(Types::RunMode runMode, int firstStep, _direction = -1; } else if (_direction < 0 && _step <= firstStep) { _direction = 1; - ++_nextIteration; + _nextIteration = _iteration + 1 ; } else { if (runMode == Types::RunMode::Pendulum) { - _nextStep += _direction; + _nextStep = _step + _direction; } } if (runMode == Types::RunMode::PingPong) { - _nextStep += _direction; + _nextStep = _step + _direction; } break; case Types::RunMode::Random: diff --git a/src/apps/sequencer/engine/SequenceState.h b/src/apps/sequencer/engine/SequenceState.h index 5eb09c3e..6647aabc 100644 --- a/src/apps/sequencer/engine/SequenceState.h +++ b/src/apps/sequencer/engine/SequenceState.h @@ -9,6 +9,7 @@ class SequenceState { public: int step() const { return _step; } + void setStep(int8_t step) { _step = step; } int prevStep() const { return _prevStep; } int nextStep() const { return _nextStep; } int direction() { return _direction; } diff --git a/src/apps/sequencer/engine/StochasticEngine.cpp b/src/apps/sequencer/engine/StochasticEngine.cpp new file mode 100644 index 00000000..e1ce6135 --- /dev/null +++ b/src/apps/sequencer/engine/StochasticEngine.cpp @@ -0,0 +1,761 @@ +#include "StochasticEngine.h" + +#include "Engine.h" +#include "Groove.h" +#include "SequenceState.h" +#include "Slide.h" +#include "SequenceUtils.h" + +#include "core/Debug.h" +#include "core/utils/Random.h" +#include "core/math/Math.h" + +#include "model/StochasticSequence.h" +#include "model/Scale.h" +#include "ui/MatrixMap.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static Random rng; + +bool sortTaskByProbRev(const StochasticStep& lhs, const StochasticStep& rhs) { + return lhs.probability() > rhs.probability(); +} + +// evaluate if step gate is active +static bool evalStepGate(const StochasticSequence::Step &step, int probabilityBias) { + int probability = clamp(step.gateProbability() + probabilityBias, -1, StochasticSequence::GateProbability::Max); + if (probability==0) { + return false; + } + return step.gate() && int(rng.nextRange(StochasticSequence::GateProbability::Range)) <= probability; +} + +// evaluate if step gate is active + int StochasticEngine::evalRestProbability(StochasticSequence &sequence) { + int sum = 0; + std::vector probability; + for (int i = 0; i < 4; i++) { + + switch (i) { + case 0: { + probability.insert(probability.end(), StochasticStep(i, sequence.restProbability())); + } + break; + case 1: { + probability.insert(probability.end(), StochasticStep(i, sequence.restProbability2())); + break; + } + case 2: { + probability.insert(probability.end(), StochasticStep(i, sequence.restProbability4())); + break; + } + case 3: { + probability.insert(probability.end(), StochasticStep(i, sequence.restProbability8())); + break; + } + default: + break; + } + + sum = sum + probability.at(i).probability(); + } + if (sum==0) { return -1;} + std::sort (std::begin(probability), std::end(probability), sortTaskByProbRev); + int stepIndex = getNextWeightedPitch(probability, probability.size()); + switch (stepIndex) { + case 0: + return 0; + case 1: + return 1; + case 2: + return 3; + case 3: + return 7; + } + return -1; +} + +// evaluate step condition +static bool evalStepCondition(const StochasticSequence::Step &step, int iteration, bool fill, bool &prevCondition) { + auto condition = step.condition(); + switch (condition) { + case Types::Condition::Off: return true; + case Types::Condition::Fill: prevCondition = fill; return prevCondition; + case Types::Condition::NotFill: prevCondition = !fill; return prevCondition; + case Types::Condition::Pre: return prevCondition; + case Types::Condition::NotPre: return !prevCondition; + case Types::Condition::First: prevCondition = iteration == 0; return prevCondition; + case Types::Condition::NotFirst: prevCondition = iteration != 0; return prevCondition; + default: + int index = int(condition); + if (index >= int(Types::Condition::Loop) && index < int(Types::Condition::Last)) { + auto loop = Types::conditionLoop(condition); + prevCondition = iteration % loop.base == loop.offset; + if (loop.invert) prevCondition = !prevCondition; + return prevCondition; + } + } + return true; +} + +// evaluate step retrigger count +static int evalStepRetrigger(const StochasticSequence::Step &step, int probabilityBias) { + int probability = clamp(step.retriggerProbability() + probabilityBias, -1, StochasticSequence::RetriggerProbability::Max); + return int(rng.nextRange(StochasticSequence::RetriggerProbability::Range)) <= probability ? step.retrigger() + 1 : 1; +} + +// evaluate step length +static int evalStepLength(const StochasticSequence::Step &step, int lengthBias) { + int length = StochasticSequence::Length::clamp(step.length() + lengthBias) + 1; + int probability = step.lengthVariationProbability(); + if (int(rng.nextRange(StochasticSequence::LengthVariationProbability::Range)) <= probability) { + int offset = step.lengthVariationRange() == 0 ? 0 : rng.nextRange(std::abs(step.lengthVariationRange()) + 1); + if (step.lengthVariationRange() < 0) { + offset = -offset; + } + length = clamp(length + offset, 0, StochasticSequence::Length::Range); + } + return length; +} + +// evaluate transposition +static int evalTransposition(const Scale &scale, int octave, int transpose) { + return octave * scale.notesPerOctave() + transpose; +} + +// evaluate note voltage +static float evalStepNote(const StochasticSequence::Step &step, int probabilityBias, const Scale &scale, int rootNote, int octave, int transpose, StochasticSequence sequence, bool useVariation = true) { + if (step.bypassScale()) { + const Scale &bypassScale = Scale::get(0); + int note = step.note() + evalTransposition(bypassScale, octave, transpose); + int probability = clamp(step.noteOctaveProbability() + probabilityBias, -1, StochasticSequence::NoteOctaveProbability::Max); + if (step.noteOctaveProbability()==0) { + probability = 0; + } + if (useVariation && int(rng.nextRange(StochasticSequence::NoteOctaveProbability::Range)) <= probability && probability!= 0) { + int oct = step.noteOctave() + sequence.lowOctaveRange() + ( std::rand() % ( sequence.highOctaveRange() - sequence.lowOctaveRange() + 1 ) ); + note = StochasticSequence::Note::clamp(note + (bypassScale.notesPerOctave()*oct)); + } + return bypassScale.noteToVolts(note) + (bypassScale.isChromatic() ? rootNote : 0) * (1.f / 12.f); + } + int note = step.note() + evalTransposition(scale, octave, transpose); + int probability = clamp(step.noteOctaveProbability() + probabilityBias, -1, StochasticSequence::NoteOctaveProbability::Max); + if (useVariation && int(rng.nextRange(StochasticSequence::NoteOctaveProbability::Range)) <= probability && probability != 0) { + int oct = step.noteOctave() + sequence.lowOctaveRange() + ( std::rand() % ( sequence.highOctaveRange() - sequence.lowOctaveRange() + 1 ) ); + note = StochasticSequence::Note::clamp(note + (scale.notesPerOctave()*oct)); + } + return scale.noteToVolts(note) + (scale.isChromatic() ? rootNote : 0) * (1.f / 12.f); +} + + + +void StochasticEngine::reset() { + _freeRelativeTick = 0; + _sequenceState.reset(); + _currentStep = -1; + _prevCondition = false; + _activity = false; + _gateOutput = false; + _slideActive = false; + _gateQueue.clear(); + _cvQueue.clear(); + _recordHistory.clear(); + + changePattern(); +} + +void StochasticEngine::restart() { + _freeRelativeTick = 0; + _sequenceState.reset(); + _currentStep = -1; + +} + +TrackEngine::TickResult StochasticEngine::tick(uint32_t tick) { + ASSERT(_sequence != nullptr, "invalid sequence"); + const auto &sequence = *_sequence; + const auto *linkData = _linkedTrackEngine ? _linkedTrackEngine->linkData() : nullptr; + + if (linkData) { + _linkData = *linkData; + _sequenceState = *linkData->sequenceState; + + if (linkData->relativeTick % linkData->divisor == 0) { + recordStep(tick, linkData->divisor); + triggerStep(tick, linkData->divisor); + } + } else { + uint32_t divisor = sequence.divisor() * (CONFIG_PPQN / CONFIG_SEQUENCE_PPQN); + uint32_t resetDivisor = sequence.resetMeasure() * _engine.measureDivisor(); + uint32_t relativeTick = resetDivisor == 0 ? tick : tick % resetDivisor; + + + if (int(_model.project().stepsToStop()) != 0 && int(relativeTick / divisor) == int(_model.project().stepsToStop())) { + _engine.clockStop(); + } + + // handle reset measure + if (relativeTick == 0) { + reset(); + _currentStageRepeat = 1; + } + const auto &sequence = *_sequence; + + // advance sequence + + switch (_stochasticTrack.playMode()) { + case Types::PlayMode::Aligned: { + if (relativeTick % divisor == 0) { + + if (sequence.useLoop()) { + _sequenceState.calculateNextStepAligned( + relativeTick / divisor, + sequence.runMode(), + sequence.sequenceFirstStep(), + sequence.sequenceLastStep(), + rng + ); + triggerStep(tick, divisor, true); + } else { + _sequenceState.advanceAligned(relativeTick / divisor, sequence.runMode(), sequence.firstStep(), sequence.lastStep(), rng); + triggerStep(tick, divisor); + } + } + } + break; + case Types::PlayMode::Free: + relativeTick = _freeRelativeTick; + if (++_freeRelativeTick >= divisor) { + _freeRelativeTick = 0; + } + if (relativeTick == 0) { + if (_currentStageRepeat == 1) { + if (sequence.useLoop()) { + _sequenceState.advanceFree(sequence.runMode(), sequence.sequenceFirstStep(), sequence.sequenceLastStep(), rng); + _sequenceState.calculateNextStepFree( + sequence.runMode(), sequence.sequenceFirstStep(), sequence.lastStep(), rng); + } else { + _sequenceState.advanceFree(sequence.runMode(), sequence.firstStep(), sequence.lastStep(), rng); + _sequenceState.calculateNextStepFree( + sequence.runMode(), sequence.firstStep(), sequence.lastStep(), rng); + } + + } + + const auto &step = sequence.step(_sequenceState.step()); + bool isLastStageStep = ((int) step.stageRepeats() - (int) _currentStageRepeat) <= 0; + + if (step.gateOffset() >= 0) { + triggerStep(tick, divisor); + } + + if (!isLastStageStep && step.gateOffset() < 0) { + triggerStep(tick + divisor, divisor, false); + } + + if (isLastStageStep + && sequence.step(_sequenceState.nextStep()).gateOffset() < 0) { + triggerStep(tick + divisor, divisor, true); + } + + if (isLastStageStep) { + _currentStageRepeat = 1; + } else { + _currentStageRepeat++; + } + } + break; + case Types::PlayMode::Last: + break; + } + + _linkData.divisor = divisor; + _linkData.relativeTick = relativeTick; + _linkData.sequenceState = &_sequenceState; + } + + auto &midiOutputEngine = _engine.midiOutputEngine(); + + TickResult result = TickResult::NoUpdate; + + while (!_gateQueue.empty() && tick >= _gateQueue.front().tick) { + if (!_monitorOverrideActive) { + result |= TickResult::GateUpdate; + _activity = _gateQueue.front().gate; + _gateOutput = (!mute() || fill()) && _activity; + midiOutputEngine.sendGate(_track.trackIndex(), _gateOutput); + } + _gateQueue.pop(); + + } + + while (!_cvQueue.empty() && tick >= _cvQueue.front().tick) { + if (!mute() || _stochasticTrack.cvUpdateMode() == StochasticTrack::CvUpdateMode::Always) { + if (!_monitorOverrideActive) { + result |= TickResult::CvUpdate; + _cvOutputTarget = _cvQueue.front().cv; + _slideActive = _cvQueue.front().slide; + midiOutputEngine.sendCv(_track.trackIndex(), _cvOutputTarget); + midiOutputEngine.sendSlide(_track.trackIndex(), _slideActive); + } + } + _cvQueue.pop(); + } + + return result; +} + +void StochasticEngine::update(float dt) { + bool running = _engine.state().running(); + const auto &sequence = *_sequence; + const auto &scale = sequence.selectedScale(_model.project().scale()); + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + int octave = _stochasticTrack.octave(); + int transpose = _stochasticTrack.transpose(); + + // helper to send gate/cv from monitoring to midi output engine + auto sendToMidiOutputEngine = [this] (bool gate, float cv = 0.f) { + auto &midiOutputEngine = _engine.midiOutputEngine(); + midiOutputEngine.sendGate(_track.trackIndex(), gate); + if (gate) { + midiOutputEngine.sendCv(_track.trackIndex(), cv); + midiOutputEngine.sendSlide(_track.trackIndex(), false); + } + }; + + // set monitor override + auto setOverride = [&] (float cv) { + _cvOutputTarget = cv; + _activity = _gateOutput = true; + _monitorOverrideActive = true; + // pass through to midi engine + sendToMidiOutputEngine(true, cv); + }; + + // clear monitor override + auto clearOverride = [&] () { + if (_monitorOverrideActive) { + _activity = _gateOutput = false; + _monitorOverrideActive = false; + sendToMidiOutputEngine(false); + } + }; + + // check for step monitoring + bool stepMonitoring = (!running && _monitorStepIndex >= 0); + + // check for live monitoring + auto monitorMode = _model.project().monitorMode(); + bool liveMonitoring = + (monitorMode == Types::MonitorMode::Always) || + (monitorMode == Types::MonitorMode::Stopped && !running); + + if (stepMonitoring) { + const auto &step = sequence.step(_monitorStepIndex); + setOverride(evalStepNote(step, 0, scale, rootNote, octave, transpose, sequence, false)); + } else if (liveMonitoring && _recordHistory.isNoteActive()) { + int note = noteFromMidiNote(_recordHistory.activeNote()) + evalTransposition(scale, octave, transpose); + setOverride(scale.noteToVolts(note) + (scale.isChromatic() ? rootNote : 0) * (1.f / 12.f)); + } else { + clearOverride(); + } + + if (_slideActive && _stochasticTrack.slideTime() > 0) { + _cvOutput = Slide::applySlide(_cvOutput, _cvOutputTarget, _stochasticTrack.slideTime(), dt); + } else { + _cvOutput = _cvOutputTarget; + } +} + +void StochasticEngine::changePattern() { + _sequence = &_stochasticTrack.sequence(pattern()); + _fillSequence = &_stochasticTrack.sequence(std::min(pattern() + 1, CONFIG_PATTERN_COUNT - 1)); +} + +void StochasticEngine::monitorMidi(uint32_t tick, const MidiMessage &message) { + _recordHistory.write(tick, message); +} + +void StochasticEngine::clearMidiMonitoring() { + _recordHistory.clear(); +} + +void StochasticEngine::setMonitorStep(int index) { + _monitorStepIndex = (index >= 0 && index < CONFIG_STEP_COUNT) ? index : -1; + + // in step record mode, select step to start recording recording from + if (_engine.recording() && _model.project().recordMode() == Types::RecordMode::StepRecord && + index >= _sequence->firstStep() && index <= _sequence->lastStep()) { + _stepRecorder.setStepIndex(index); + } +} + +bool canLoop = false; + +void StochasticEngine::triggerStep(uint32_t tick, uint32_t divisor, bool forNextStep) { + int octave = _stochasticTrack.octave(); + int transpose = _stochasticTrack.transpose(); + + auto &sequence = *_sequence; + + int stepIndex; + + uint32_t resetDivisor = sequence.resetMeasure() * _engine.measureDivisor(); + uint32_t relativeTick = resetDivisor == 0 ? tick : tick % resetDivisor; + auto abstoluteStep = relativeTick / divisor; + + _index = abstoluteStep % sequence.sequenceLength(); + + StochasticSequence::Step step; + uint32_t stepTick = 0; + bool stepGate = false; + float noteValue = 0; + uint32_t stepLength = 0; + int stepRetrigger = 0; + + // clear the in memory sequence when reaches the max size + if (int(_inMemSteps.size()) >= (sequence.bufferLoopLength())) { + if (sequence.clearLoop()) { + _lockedSteps = _inMemSteps; + } + _inMemSteps.clear(); + } + + if (!sequence.useLoop() && sequence.reseed() && !sequence.isEmpty()) { + int rnd = -StochasticSequence::NoteVariationProbability::Range/2 + ( std::rand() % ( (StochasticSequence::NoteVariationProbability::Range/2) - (-StochasticSequence::NoteVariationProbability::Range/2)) + 1 ); + _stochasticTrack.setNoteProbabilityBias(rnd); + rng = Random(time(NULL)); + sequence.setReseed(0, false); + } + + // clear the locked memory sequence and reset it to the in memory sequence + if (sequence.clearLoop()) { + + if (_index == 0) { + _lockedSteps.clear(); + _inMemSteps.clear(); + _lockedSteps = _inMemSteps; + sequence.setClearLoop(false); + sequence.setUseLoop(false); + _sequenceState.reset(); + _index = -1; + int rest = evalRestProbability(sequence); + if (rest != -1) { + _skips = rest; + } + } + } + + // fill in memory step when sequence is running or when the in memory loop is not full filled + if (!sequence.useLoop() || int(_lockedSteps.size()) < sequence.bufferLoopLength()) { + if (_skips != 0 && _index > 0) { + --_skips; + _inMemSteps.insert(_inMemSteps.end(), StochasticLoopStep(-1, false, step, 0, 0, 0)); + if (int(_lockedSteps.size()) < sequence.bufferLoopLength()) { + _lockedSteps.insert(_lockedSteps.end(), StochasticLoopStep(-1, false, step, 0, 0, 0)); + } + return; + } + if (_index == 0 || _index % 2 == 0) { + int rest = evalRestProbability(sequence); + if (rest != -1) { + _skips = rest; + } + } + + std::vector probability; + int sum =0; + for (int i = 0; i < 12; i++) { + if (sequence.step(i).gate()) { + probability.insert(probability.end(), StochasticStep(i, clamp(sequence.step(i).noteVariationProbability() + _stochasticTrack.noteProbabilityBias(), -1, StochasticSequence::NoteVariationProbability::Max))); + } else { + probability.insert(probability.end(), StochasticStep(i, 0)); + } + sum = sum + probability.at(i).probability(); + } + if (sum==0) { return;} + std::sort (std::begin(probability), std::end(probability), sortTaskByProbRev); + stepIndex = getNextWeightedPitch(probability, probability.size()); + + step = sequence.step(stepIndex); + _currentStep = stepIndex; + + int gateOffset = ((int) divisor * step.gateOffset()) / (StochasticSequence::GateOffset::Max + 1); + stepTick = (int) tick + gateOffset; + + stepGate = evalStepGate(step, _stochasticTrack.gateProbabilityBias());; + if (stepGate) { + stepGate = evalStepCondition(step, _sequenceState.iteration(), false, _prevCondition); + } + const auto &scale = sequence.selectedScale(_model.project().scale()); + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + noteValue = evalStepNote(step, _stochasticTrack.noteProbabilityBias(), scale, rootNote, octave, transpose, sequence); + stepLength = (divisor * evalStepLength(step, _stochasticTrack.lengthBias())) / StochasticSequence::Length::Range; + + int rnd = 0; + if (sequence.lengthModifier()!= 0) { + int m = -StochasticSequence::NoteVariationProbability::Range + ( std::rand() % ( StochasticSequence::NoteVariationProbability::Range - -StochasticSequence::NoteVariationProbability::Range + 1 ) ); + int mean = sequence.lengthModifier(); + std::mt19937 e2(m); + std::normal_distribution normal_dist(mean, 2); + rnd = std::round(normal_dist(e2)); + } + stepLength = stepLength + (rnd*2); + stepRetrigger = evalStepRetrigger(step, _stochasticTrack.retriggerProbabilityBias()); + _inMemSteps.insert(_inMemSteps.end(), StochasticLoopStep(stepIndex, stepGate, step, noteValue, stepLength, stepRetrigger)); + + if (int(_lockedSteps.size()) < sequence.bufferLoopLength()) { + _lockedSteps.insert(_lockedSteps.end(), StochasticLoopStep(stepIndex, stepGate, step, noteValue, stepLength, stepRetrigger)); + } + + + + if (stepGate) { + sequence.setStepBounds(stepIndex); + if (stepRetrigger > 1) { + uint32_t retriggerLength = divisor / stepRetrigger; + uint32_t retriggerOffset = 0; + while (stepRetrigger-- > 0 && retriggerOffset <= stepLength) { + _gateQueue.pushReplace({ Groove::applySwing(stepTick + retriggerOffset, swing()), true }); + _gateQueue.pushReplace({ Groove::applySwing(stepTick + retriggerOffset + retriggerLength / 2, swing()), false }); + retriggerOffset += retriggerLength; + } + } else { + _gateQueue.pushReplace({ Groove::applySwing(stepTick, swing()), true }); + _gateQueue.pushReplace({ Groove::applySwing(stepTick + stepLength, swing()), false }); + } + } + + if (stepGate || _stochasticTrack.cvUpdateMode() == StochasticTrack::CvUpdateMode::Always) { + + _cvQueue.push({ Groove::applySwing(stepTick, swing()), noteValue, step.slide() }); + } + return; + + } + + // use the locked loop to retrieve steps data + if (sequence.useLoop() && int(_lockedSteps.size()) >= sequence.bufferLoopLength()) { + if (forNextStep) { + if (sequence.runMode() == Types::RunMode::RandomWalk) { + if (rng.nextRange(2) == 0) { + _sequenceState.setStep(-1); + } else { + _sequenceState.setStep(_index); + } + } + _index = _sequenceState.nextStep(); + } + + if (int(_lockedSteps.size()) >= sequence.bufferLoopLength()) { + _lockedSteps = slicing(_lockedSteps, 0, sequence.bufferLoopLength()); + } else { + _lockedSteps = slicing(_inMemSteps, _inMemSteps.size() - sequence.bufferLoopLength(), _inMemSteps.size()-1); + } + + + auto subArray = slicing(_lockedSteps, sequence.sequenceFirstStep(), sequence.sequenceLastStep()); + _index = _index%sequence.sequenceLength(); + stepIndex = subArray.at(_index).index(); + if (stepIndex == -1) { + return; + } + _currentStep = stepIndex; + + stepGate = subArray.at(_index).gate(); + step = subArray.at(_index).step(); + switch (step.stageRepeatMode()) { + case StochasticSequence::StageRepeatMode::Each: + break; + case StochasticSequence::StageRepeatMode::First: + stepGate = stepGate && _currentStageRepeat == 1; + break; + case StochasticSequence::StageRepeatMode::Last: + stepGate = stepGate && _currentStageRepeat == step.stageRepeats()+1; + break; + case StochasticSequence::StageRepeatMode::Middle: + stepGate = stepGate && _currentStageRepeat == (step.stageRepeats()+1)/2; + break; + case StochasticSequence::StageRepeatMode::Odd: + stepGate = stepGate && _currentStageRepeat % 2 != 0; + break; + case StochasticSequence::StageRepeatMode::Even: + stepGate = stepGate && _currentStageRepeat % 2 == 0; + break; + case StochasticSequence::StageRepeatMode::Triplets: + stepGate = stepGate && (_currentStageRepeat - 1) % 3 == 0; + break; + case StochasticSequence::StageRepeatMode::Random: + srand((unsigned int)time(NULL)); + int rndMode = rng.nextRange(6); + switch (rndMode) { + case 0: + break; + case 1: + stepGate = stepGate && _currentStageRepeat == 1; + break; + case 2: + stepGate = stepGate && _currentStageRepeat == step.stageRepeats()+1; + break; + case 3: + stepGate = stepGate && _currentStageRepeat % (((step.stageRepeats()+1)/2)+1) == 0; + break; + case 4: + stepGate = stepGate && _currentStageRepeat % 2 != 0; + break; + case 5: + stepGate = stepGate && _currentStageRepeat % 2 == 0; + break; + case 6: + stepGate = stepGate && (_currentStageRepeat - 1) % 3 == 0; + break; + + } + break; + } + + + int gateOffset = ((int) divisor * step.gateOffset()) / (StochasticSequence::GateOffset::Max + 1); + stepTick = (int) tick + gateOffset; + noteValue = subArray.at(_index).noteValue(); + stepLength = subArray.at(_index).stepLength(); + stepRetrigger = _lockedSteps.at(_index).stepRetrigger(); + + + + if (stepGate) { + sequence.setStepBounds(stepIndex); + if (stepRetrigger > 1) { + uint32_t retriggerLength = divisor / stepRetrigger; + uint32_t retriggerOffset = 0; + while (stepRetrigger-- > 0 && retriggerOffset <= stepLength) { + _gateQueue.pushReplace({ Groove::applySwing(stepTick + retriggerOffset, swing()), true }); + _gateQueue.pushReplace({ Groove::applySwing(stepTick + retriggerOffset + retriggerLength / 2, swing()), false }); + retriggerOffset += retriggerLength; + } + } else { + _gateQueue.pushReplace({ Groove::applySwing(stepTick, swing()), true }); + _gateQueue.pushReplace({ Groove::applySwing(stepTick + stepLength, swing()), false }); + } + } + + if (stepGate || _stochasticTrack.cvUpdateMode() == StochasticTrack::CvUpdateMode::Always) { + + _cvQueue.push({ Groove::applySwing(stepTick, swing()), noteValue, step.slide() }); + } + } +} + + +int StochasticEngine::getNextWeightedPitch(std::vector distr, int notesPerOctave) { + int total_weights = 0; + + for(int i = 0; i < notesPerOctave; i++) { + total_weights += distr.at(i % notesPerOctave).probability(); + } + + int rnd = rng.nextRange(total_weights); + + for(int i = 0; i < notesPerOctave; i++) { + int weight = distr.at(i % notesPerOctave).probability(); + if (rnd <= weight && weight > 0) { + return distr.at(i % notesPerOctave).index(); + } + rnd -= weight; + } + return -1; + } + + + +void StochasticEngine::triggerStep(uint32_t tick, uint32_t divisor) { + triggerStep(tick, divisor, false); +} + +void StochasticEngine::recordStep(uint32_t tick, uint32_t divisor) { + if (!_engine.state().recording() || _model.project().recordMode() == Types::RecordMode::StepRecord || _sequenceState.prevStep() < 0) { + return; + } + + bool stepWritten = false; + + auto writeStep = [this, divisor, &stepWritten] (int stepIndex, int note, int lengthTicks) { + auto &step = _sequence->step(stepIndex); + int length = (lengthTicks * StochasticSequence::Length::Range) / divisor; + + step.setGate(true); + step.setGateProbability(StochasticSequence::GateProbability::Max); + step.setRetrigger(0); + step.setRetriggerProbability(StochasticSequence::RetriggerProbability::Max); + step.setLength(length); + step.setLengthVariationRange(0); + step.setLengthVariationProbability(StochasticSequence::LengthVariationProbability::Max); + step.setNote(noteFromMidiNote(note)); + step.setNoteOctave(0); + step.setNoteVariationProbability(StochasticSequence::NoteVariationProbability::Max); + step.setCondition(Types::Condition::Off); + step.setStageRepeats(1); + + stepWritten = true; + }; + + auto clearStep = [this] (int stepIndex) { + auto &step = _sequence->step(stepIndex); + + step.clear(); + }; + + uint32_t stepStart = tick - divisor; + uint32_t stepEnd = tick; + uint32_t margin = divisor / 2; + + for (size_t i = 0; i < _recordHistory.size(); ++i) { + if (_recordHistory[i].type != RecordHistory::Type::NoteOn) { + continue; + } + + int note = _recordHistory[i].note; + uint32_t noteStart = _recordHistory[i].tick; + uint32_t noteEnd = i + 1 < _recordHistory.size() ? _recordHistory[i + 1].tick : tick; + + if (noteStart >= stepStart - margin && noteStart < stepStart + margin) { + // note on during step start phase + if (noteEnd >= stepEnd) { + // note hold during step + int length = std::min(noteEnd, stepEnd) - stepStart; + writeStep(_sequenceState.prevStep(), note, length); + } else { + // note released during step + int length = noteEnd - noteStart; + writeStep(_sequenceState.prevStep(), note, length); + } + } else if (noteStart < stepStart && noteEnd > stepStart) { + // note on during previous step + int length = std::min(noteEnd, stepEnd) - stepStart; + writeStep(_sequenceState.prevStep(), note, length); + } + } + + if (isSelected() && !stepWritten && _model.project().recordMode() == Types::RecordMode::Overwrite) { + clearStep(_sequenceState.prevStep()); + } +} + +int StochasticEngine::noteFromMidiNote(uint8_t midiNote) const { + const auto &scale = _sequence->selectedScale(_model.project().scale()); + int rootNote = _sequence->selectedRootNote(_model.project().rootNote()); + + if (scale.isChromatic()) { + return scale.noteFromVolts((midiNote - 60 - rootNote) * (1.f / 12.f)); + } else { + return scale.noteFromVolts((midiNote - 60) * (1.f / 12.f)); + } +} diff --git a/src/apps/sequencer/engine/StochasticEngine.h b/src/apps/sequencer/engine/StochasticEngine.h new file mode 100644 index 00000000..794a3eaf --- /dev/null +++ b/src/apps/sequencer/engine/StochasticEngine.h @@ -0,0 +1,212 @@ +#pragma once + +#include "TrackEngine.h" +#include "SequenceState.h" +#include "SortedQueue.h" +#include "Groove.h" +#include "RecordHistory.h" +#include "model/StochasticSequence.h" +#include "StepRecorder.h" +#include +#include + +class StochasticStep { + public: + + StochasticStep() {} + StochasticStep(int index, int probability) { + _index = index; + _probability = probability; + } + + int index() { + return _index; + } + + int probability() const { + return _probability; + } + + private: + int _index; + int _probability; + + }; + +class StochasticLoopStep { + public: + StochasticLoopStep() {} + StochasticLoopStep(int index, bool gate, StochasticSequence::Step step, float noteValue, uint32_t stepLength, int stepRetrigger ) { + _index = index; + _gate = gate; + _step = step; + _noteValue = noteValue; + _stepLength = stepLength; + _stepRetrigger = stepRetrigger; + } + + int index() { + return _index; + } + + bool gate() { + return _gate; + } + + StochasticSequence::Step step() { + return _step; + } + + float noteValue() { + return _noteValue; + } + + uint32_t stepLength() { + return _stepLength; + } + + int stepRetrigger() { + return _stepRetrigger; + } + + + private: + int _index; + bool _gate; + StochasticSequence::Step _step; + float _noteValue; + uint32_t _stepLength; + int _stepRetrigger; +}; + +class StochasticEngine : public TrackEngine { +public: + StochasticEngine(Engine &engine, const Model &model, Track &track, const TrackEngine *linkedTrackEngine) : + TrackEngine(engine, model, track, linkedTrackEngine), + _stochasticTrack(track.stochasticTrack()) + { + reset(); + } + + virtual Track::TrackMode trackMode() const override { return Track::TrackMode::Stochastic; } + + virtual void reset() override; + virtual void restart() override; + virtual TickResult tick(uint32_t tick) override; + virtual void update(float dt) override; + + virtual void changePattern() override; + + virtual void monitorMidi(uint32_t tick, const MidiMessage &message) override; + virtual void clearMidiMonitoring() override; + + virtual const TrackLinkData *linkData() const override { return &_linkData; } + + virtual bool activity() const override { return _activity; } + virtual bool gateOutput(int index) const override { return _gateOutput; } + virtual float cvOutput(int index) const override { return _cvOutput; } + virtual float sequenceProgress() const override { + return _currentStep < 0 ? 0.f : float(_currentStep - _sequence->firstStep()) / (_sequence->lastStep() - _sequence->firstStep()); + } + + const StochasticSequence &sequence() const { return *_sequence; } + bool isActiveSequence(const StochasticSequence &sequence) const { return &sequence == _sequence; } + + int currentStep() const { return _currentStep; } + int currentRecordStep() const { return _stepRecorder.stepIndex(); } + + int currentIndex() const { return _index; } + + void setMonitorStep(int index); + Types::PlayMode playMode() const { return _stochasticTrack.playMode(); } + + + int getNextWeightedPitch(std::vector distr, int notesPerOctave = 12); + int evalRestProbability(StochasticSequence &sequence); + + +private: + void triggerStep(uint32_t tick, uint32_t divisor, bool nextStep); + void triggerStep(uint32_t tick, uint32_t divisor); + void recordStep(uint32_t tick, uint32_t divisor); + int noteFromMidiNote(uint8_t midiNote) const; + + bool fill() const { + return false; + } + + std::vector slicing(std::vector &arr, int X, int Y) + { + + // Starting and Ending iterators + auto start = arr.begin() + X; + auto end = arr.begin() + Y + 1; + + // To store the sliced vector + std::vector result(Y - X + 1); + + // Copy vector using copy function() + copy(start, end, result.begin()); + + // Return the final sliced vector + return result; + } + + StochasticTrack &_stochasticTrack; + + TrackLinkData _linkData; + + StochasticSequence *_sequence; + const StochasticSequence *_fillSequence; + + uint32_t _freeRelativeTick; + SequenceState _sequenceState; + int _currentStep; + int _index; + bool _prevCondition; + + int _monitorStepIndex = -1; + + RecordHistory _recordHistory; + bool _monitorOverrideActive = false; + StepRecorder _stepRecorder; + + bool _activity; + bool _gateOutput; + float _cvOutput; + float _cvOutputTarget; + bool _slideActive; + unsigned int _currentStageRepeat; + + int _skips; + + std::vector _inMemSteps; + std::vector _lockedSteps; + + struct Gate { + uint32_t tick; + bool gate; + }; + + struct GateCompare { + bool operator()(const Gate &a, const Gate &b) { + return a.tick < b.tick; + } + }; + + SortedQueue _gateQueue; + + struct Cv { + uint32_t tick; + float cv; + bool slide; + }; + + struct CvCompare { + bool operator()(const Cv &a, const Cv &b) { + return a.tick < b.tick; + } + }; + + SortedQueue _cvQueue; +}; diff --git a/src/apps/sequencer/engine/generators/SequenceBuilder.h b/src/apps/sequencer/engine/generators/SequenceBuilder.h index 1f3ddebd..239d602a 100644 --- a/src/apps/sequencer/engine/generators/SequenceBuilder.h +++ b/src/apps/sequencer/engine/generators/SequenceBuilder.h @@ -2,6 +2,7 @@ #include "model/NoteSequence.h" #include "model/CurveSequence.h" +#include "model/StochasticSequence.h" class SequenceBuilder { public: @@ -93,3 +94,4 @@ class SequenceBuilderImpl : public SequenceBuilder { typedef SequenceBuilderImpl NoteSequenceBuilder; typedef SequenceBuilderImpl CurveSequenceBuilder; +typedef SequenceBuilderImpl StochasticSequenceBuilder; diff --git a/src/apps/sequencer/model/BaseTrack.h b/src/apps/sequencer/model/BaseTrack.h index c1b95b7e..7cb1cdcc 100644 --- a/src/apps/sequencer/model/BaseTrack.h +++ b/src/apps/sequencer/model/BaseTrack.h @@ -2,7 +2,6 @@ #include "Config.h" #include "Types.h" -#include "NoteSequence.h" #include "Serialize.h" #include "Routing.h" #include "FileDefs.h" diff --git a/src/apps/sequencer/model/BaseTrackPatternFollow.h b/src/apps/sequencer/model/BaseTrackPatternFollow.h index 03c189ce..9aa21165 100644 --- a/src/apps/sequencer/model/BaseTrackPatternFollow.h +++ b/src/apps/sequencer/model/BaseTrackPatternFollow.h @@ -2,7 +2,6 @@ #include "Config.h" #include "Types.h" -#include "NoteSequence.h" #include "Serialize.h" #include "Routing.h" #include "FileDefs.h" diff --git a/src/apps/sequencer/model/ClipBoard.cpp b/src/apps/sequencer/model/ClipBoard.cpp index f34150b8..a7ed6e46 100644 --- a/src/apps/sequencer/model/ClipBoard.cpp +++ b/src/apps/sequencer/model/ClipBoard.cpp @@ -2,6 +2,7 @@ #include "Model.h" #include "ModelUtils.h" +#include "StochasticSequence.h" ClipBoard::ClipBoard(Project &project) : _project(project) @@ -43,6 +44,18 @@ void ClipBoard::copyCurveSequenceSteps(const CurveSequence &curveSequence, const curveSequenceSteps.selected = selectedSteps; } +void ClipBoard::copyStochasticSequence(const StochasticSequence &sequence) { + _type = Type::StochasticSequence; + _container.as() = sequence; +} + +void ClipBoard::copyStochasticSequenceSteps(const StochasticSequence &sequence, const SelectedSteps &selectedSteps) { + _type = Type::StochasticSequenceSteps; + auto &stochasticSequenceSteps = _container.as(); + stochasticSequenceSteps.sequence = sequence; + stochasticSequenceSteps.selected = selectedSteps; +} + void ClipBoard::copyPattern(int patternIndex) { _type = Type::Pattern; auto &pattern = _container.as(); @@ -103,6 +116,21 @@ void ClipBoard::pasteCurveSequenceSteps(CurveSequence &curveSequence, const Sele } } + +void ClipBoard::pasteStochasticSequence(StochasticSequence &sequence) const { + if (canPasteStochasticSequence()) { + Model::WriteLock lock; + sequence = _container.as(); + } +} + +void ClipBoard::pasteStochasticSequenceSteps(StochasticSequence &sequence, const SelectedSteps &selectedSteps) const { + if (canPasteStochasticSequenceSteps()) { + const auto &stochasticSequenceSteps = _container.as(); + ModelUtils::copySteps(stochasticSequenceSteps.sequence.steps(), stochasticSequenceSteps.selected, sequence.steps(), selectedSteps); + } +} + void ClipBoard::pastePattern(int patternIndex) const { if (canPastePattern()) { Model::WriteLock lock; @@ -151,6 +179,14 @@ bool ClipBoard::canPasteCurveSequenceSteps() const { return _type == Type::CurveSequenceSteps; } +bool ClipBoard::canPasteStochasticSequence() const { + return _type == Type::StochasticSequence; +} + +bool ClipBoard::canPasteStochasticSequenceSteps() const { + return _type == Type::StochasticSequenceSteps; +} + bool ClipBoard::canPastePattern() const { return _type == Type::Pattern; } diff --git a/src/apps/sequencer/model/ClipBoard.h b/src/apps/sequencer/model/ClipBoard.h index cc932fc4..8b3e3f5f 100644 --- a/src/apps/sequencer/model/ClipBoard.h +++ b/src/apps/sequencer/model/ClipBoard.h @@ -2,6 +2,7 @@ #include "Config.h" +#include "StochasticSequence.h" #include "Track.h" #include "NoteSequence.h" #include "CurveSequence.h" @@ -25,6 +26,8 @@ class ClipBoard { void copyNoteSequenceSteps(const NoteSequence ¬eSequence, const SelectedSteps &selectedSteps); void copyCurveSequence(const CurveSequence &curveSequence); void copyCurveSequenceSteps(const CurveSequence &curveSequence, const SelectedSteps &selectedSteps); + void copyStochasticSequence(const StochasticSequence ¬eSequence); + void copyStochasticSequenceSteps(const StochasticSequence ¬eSequence, const SelectedSteps &selectedSteps); void copyPattern(int patternIndex); void copyUserScale(const UserScale &userScale); @@ -33,6 +36,8 @@ class ClipBoard { void pasteNoteSequenceSteps(NoteSequence ¬eSequence, const SelectedSteps &selectedSteps) const; void pasteCurveSequence(CurveSequence &curveSequence) const; void pasteCurveSequenceSteps(CurveSequence &curveSequence, const SelectedSteps &selectedSteps) const; + void pasteStochasticSequence(StochasticSequence ¬eSequence) const; + void pasteStochasticSequenceSteps(StochasticSequence ¬eSequence, const SelectedSteps &selectedSteps) const; void pastePattern(int patternIndex) const; void pasteUserScale(UserScale &userScale) const; @@ -41,6 +46,8 @@ class ClipBoard { bool canPasteNoteSequenceSteps() const; bool canPasteCurveSequence() const; bool canPasteCurveSequenceSteps() const; + bool canPasteStochasticSequence() const; + bool canPasteStochasticSequenceSteps() const; bool canPastePattern() const; bool canPasteUserScale() const; @@ -52,6 +59,8 @@ class ClipBoard { NoteSequenceSteps, CurveSequence, CurveSequenceSteps, + StochasticSequence, + StochasticSequenceSteps, Pattern, UserScale, }; @@ -66,6 +75,11 @@ class ClipBoard { SelectedSteps selected; }; + struct StochasticSequenceSteps { + StochasticSequence sequence; + SelectedSteps selected; + }; + struct Pattern { struct { Track::TrackMode trackMode; @@ -78,5 +92,5 @@ class ClipBoard { Project &_project; Type _type = Type::None; - Container _container; + Container _container; }; diff --git a/src/apps/sequencer/model/CurveSequence.cpp b/src/apps/sequencer/model/CurveSequence.cpp index 4a6bb8d3..4da64e5b 100644 --- a/src/apps/sequencer/model/CurveSequence.cpp +++ b/src/apps/sequencer/model/CurveSequence.cpp @@ -160,6 +160,7 @@ void CurveSequence::writeRouted(Routing::Target target, int intValue, float floa } void CurveSequence::clear() { + setName("INIT"); setRange(Types::VoltageRange::Bipolar5V); setDivisor(12); setResetMeasure(0); @@ -209,7 +210,7 @@ void CurveSequence::setShapes(std::initializer_list shapes) { void CurveSequence::shiftSteps(const std::bitset &selected, int direction) { if (selected.any()) { - ModelUtils::shiftSteps(_steps, selected, direction); + ModelUtils::shiftSteps(_steps, selected, firstStep(), lastStep(), direction); } else { ModelUtils::shiftSteps(_steps, firstStep(), lastStep(), direction); } @@ -229,9 +230,12 @@ void CurveSequence::write(VersionedSerializedWriter &writer) const { writer.write(_lastStep.base); writeArray(writer, _steps); + writer.write(_name, NameLength + 1); + writer.write(_slot); + writer.writeHash(); } -void CurveSequence::read(VersionedSerializedReader &reader) { +bool CurveSequence::read(VersionedSerializedReader &reader) { reader.read(_range); if (reader.dataVersion() < ProjectVersion::Version10) { reader.readAs(_divisor.base); @@ -244,4 +248,17 @@ void CurveSequence::read(VersionedSerializedReader &reader) { reader.read(_lastStep.base); readArray(reader, _steps); + if (reader.dataVersion() >= ProjectVersion::Version35) { + reader.read(_name, NameLength + 1, ProjectVersion::Version35); + reader.read(_slot); + bool success = reader.checkHash(); + if (!success) { + clear(); + } + return success; + } else { + return true; + } + + } diff --git a/src/apps/sequencer/model/CurveSequence.h b/src/apps/sequencer/model/CurveSequence.h index b1a96f1d..cc6bcbbe 100644 --- a/src/apps/sequencer/model/CurveSequence.h +++ b/src/apps/sequencer/model/CurveSequence.h @@ -7,9 +7,11 @@ #include "Types.h" #include "Curve.h" #include "Routing.h" +#include "FileDefs.h" #include "core/math/Math.h" #include "core/utils/StringBuilder.h" +#include "core/utils/StringUtils.h" #include #include @@ -54,6 +56,8 @@ class CurveSequence { return nullptr; } + static constexpr size_t NameLength = FileHeader::NameLength; + static Types::LayerRange layerRange(Layer layer); static int layerDefaultValue(Layer layer); @@ -169,6 +173,24 @@ class CurveSequence { // Properties //---------------------------------------- + // slot + + int slot() const { return _slot; } + void setSlot(int slot) { + _slot = slot; + } + bool slotAssigned() const { + return _slot != uint8_t(-1); + } + + // name + + const char *name() const { return _name; } + void setName(const char *name) { + StringUtils::copy(_name, name, sizeof(_name)); + } + + // trackIndex int trackIndex() const { return _trackIndex; } @@ -334,7 +356,14 @@ class CurveSequence { void duplicateSteps(); void write(VersionedSerializedWriter &writer) const; - void read(VersionedSerializedReader &reader); + bool read(VersionedSerializedReader &reader); + + int section() { return _section; } + const int section() const { return _section; } + + void setSecion(int section) { + _section = section; + } private: void setTrackIndex(int trackIndex) { _trackIndex = trackIndex; } @@ -350,6 +379,8 @@ class CurveSequence { } } + uint8_t _slot = uint8_t(-1); + char _name[NameLength + 1]; int8_t _trackIndex = -1; Types::VoltageRange _range; Routable _divisor; @@ -360,5 +391,7 @@ class CurveSequence { StepArray _steps; + int _section = 0; + friend class CurveTrack; }; diff --git a/src/apps/sequencer/model/CurveTrack.cpp b/src/apps/sequencer/model/CurveTrack.cpp index a77bdd6b..fd056b29 100644 --- a/src/apps/sequencer/model/CurveTrack.cpp +++ b/src/apps/sequencer/model/CurveTrack.cpp @@ -32,6 +32,7 @@ void CurveTrack::clear() { setRotate(0); setShapeProbabilityBias(0); setGateProbabilityBias(0); + setCurveCvInput(Types::CurveCvInput::Off); for (auto &sequence : _sequences) { sequence.clear(); @@ -49,6 +50,7 @@ void CurveTrack::write(VersionedSerializedWriter &writer) const { writer.write(_rotate.base); writer.write(_shapeProbabilityBias.base); writer.write(_gateProbabilityBias.base); + writer.write(_curveCvInput); writeArray(writer, _sequences); } @@ -62,5 +64,6 @@ void CurveTrack::read(VersionedSerializedReader &reader) { reader.read(_rotate.base); reader.read(_shapeProbabilityBias.base, ProjectVersion::Version15); reader.read(_gateProbabilityBias.base, ProjectVersion::Version15); + reader.read(_curveCvInput, ProjectVersion::Version36); readArray(reader, _sequences); } diff --git a/src/apps/sequencer/model/CurveTrack.h b/src/apps/sequencer/model/CurveTrack.h index 85499c33..fadbd8d2 100644 --- a/src/apps/sequencer/model/CurveTrack.h +++ b/src/apps/sequencer/model/CurveTrack.h @@ -198,6 +198,21 @@ class CurveTrack : public BaseTrack, public BaseTrackPatternFollow { str("%+.1f%%", gateProbabilityBias() * 12.5f); } + // curveCvInput + + Types::CurveCvInput curveCvInput() const { return _curveCvInput; } + void setCurveCvInput(Types::CurveCvInput curveCvInput) { + _curveCvInput = ModelUtils::clampedEnum(curveCvInput); + } + + void editCurveCvInput(int value, bool shift) { + _curveCvInput = ModelUtils::adjustedEnum(_curveCvInput, value); + } + + void printCurveCvInput(StringBuilder &str) const { + str(Types::curveCvInput(_curveCvInput)); + } + // sequences const CurveSequenceArray &sequences() const { return _sequences; } @@ -247,6 +262,8 @@ class CurveTrack : public BaseTrack, public BaseTrackPatternFollow { Routable _shapeProbabilityBias; Routable _gateProbabilityBias; + Types::CurveCvInput _curveCvInput; + CurveSequenceArray _sequences; friend class Track; diff --git a/src/apps/sequencer/model/FileDefs.h b/src/apps/sequencer/model/FileDefs.h index b89290bd..b6fc8738 100644 --- a/src/apps/sequencer/model/FileDefs.h +++ b/src/apps/sequencer/model/FileDefs.h @@ -1,5 +1,6 @@ #pragma once +#include "Routing.h" #include #include @@ -8,6 +9,8 @@ enum class FileType : uint8_t { Project = 0, UserScale = 1, + NoteSequence= 2, + CurveSequence=3, Settings = 255 }; diff --git a/src/apps/sequencer/model/FileManager.cpp b/src/apps/sequencer/model/FileManager.cpp index 09ca7fef..af9e63e5 100644 --- a/src/apps/sequencer/model/FileManager.cpp +++ b/src/apps/sequencer/model/FileManager.cpp @@ -1,6 +1,7 @@ #include "FileManager.h" #include "ProjectVersion.h" +#include "Routing.h" #include "core/utils/StringBuilder.h" #include "core/fs/FileSystem.h" #include "core/fs/FileWriter.h" @@ -30,6 +31,8 @@ struct FileTypeInfo { FileTypeInfo fileTypeInfos[] = { { "PROJECTS", "PRO" }, { "SCALES", "SCA" }, + {"SEQS", "NSQ"}, + {"SEQS", "CSQ"} }; static void slotPath(StringBuilder &str, FileType type, int slot) { @@ -105,6 +108,31 @@ fs::Error FileManager::readUserScale(UserScale &userScale, int slot) { }); } + +fs::Error FileManager::writeNoteSequence(const NoteSequence ¬eSequence, int slot) { + return writeFile(FileType::NoteSequence, slot, [&] (const char *path) { + return writeNoteSequence(noteSequence, path); + }); +} + +fs::Error FileManager::readNoteSequence(NoteSequence ¬eSequence, int slot) { + return readFile(FileType::NoteSequence, slot, [&] (const char *path) { + return readNoteSequence(noteSequence, path); + }); +} + +fs::Error FileManager::writeCurveSequence(const CurveSequence &curveSequence, int slot) { + return writeFile(FileType::CurveSequence, slot, [&] (const char *path) { + return writeCurveSequence(curveSequence, path); + }); +} + +fs::Error FileManager::readCurveSequence(CurveSequence &curveSequence, int slot) { + return readFile(FileType::CurveSequence, slot, [&] (const char *path) { + return readCurveSequence(curveSequence, path); + }); +} + fs::Error FileManager::writeProject(const Project &project, const char *path) { fs::FileWriter fileWriter(path); if (fileWriter.error() != fs::OK) { @@ -191,6 +219,92 @@ fs::Error FileManager::readUserScale(UserScale &userScale, const char *path) { return error; } +fs::Error FileManager::writeNoteSequence(const NoteSequence ¬eSequence, const char *path) { + fs::FileWriter fileWriter(path); + if (fileWriter.error() != fs::OK) { + return fileWriter.error(); + } + + FileHeader header(FileType::NoteSequence, 0, noteSequence.name()); + fileWriter.write(&header, sizeof(header)); + + VersionedSerializedWriter writer( + [&fileWriter] (const void *data, size_t len) { fileWriter.write(data, len); }, + ProjectVersion::Latest + ); + + noteSequence.write(writer); + + return fileWriter.finish(); +} + +fs::Error FileManager::readNoteSequence(NoteSequence ¬eSequence, const char *path) { + fs::FileReader fileReader(path); + if (fileReader.error() != fs::OK) { + return fileReader.error(); + } + + FileHeader header; + fileReader.read(&header, sizeof(header)); + + VersionedSerializedReader reader( + [&fileReader] (void *data, size_t len) { fileReader.read(data, len); }, + ProjectVersion::Latest + ); + + bool success = noteSequence.read(reader); + + auto error = fileReader.finish(); + if (error == fs::OK && !success) { + error = fs::INVALID_CHECKSUM; + } + + return error; +} + +fs::Error FileManager::writeCurveSequence(const CurveSequence &curveSequence, const char *path) { + fs::FileWriter fileWriter(path); + if (fileWriter.error() != fs::OK) { + return fileWriter.error(); + } + + FileHeader header(FileType::CurveSequence, 0, curveSequence.name()); + fileWriter.write(&header, sizeof(header)); + + VersionedSerializedWriter writer( + [&fileWriter] (const void *data, size_t len) { fileWriter.write(data, len); }, + ProjectVersion::Latest + ); + + curveSequence.write(writer); + + return fileWriter.finish(); +} + +fs::Error FileManager::readCurveSequence(CurveSequence &curveSequence, const char *path) { + fs::FileReader fileReader(path); + if (fileReader.error() != fs::OK) { + return fileReader.error(); + } + + FileHeader header; + fileReader.read(&header, sizeof(header)); + + VersionedSerializedReader reader( + [&fileReader] (void *data, size_t len) { fileReader.read(data, len); }, + ProjectVersion::Latest + ); + + bool success = curveSequence.read(reader); + + auto error = fileReader.finish(); + if (error == fs::OK && !success) { + error = fs::INVALID_CHECKSUM; + } + + return error; +} + fs::Error FileManager::writeSettings(const Settings &settings, const char *path) { fs::FileWriter fileWriter(path); if (fileWriter.error() != fs::OK) { diff --git a/src/apps/sequencer/model/FileManager.h b/src/apps/sequencer/model/FileManager.h index c400042c..d7956623 100644 --- a/src/apps/sequencer/model/FileManager.h +++ b/src/apps/sequencer/model/FileManager.h @@ -2,6 +2,7 @@ #include "FileDefs.h" #include "Project.h" +#include "Routing.h" #include "UserScale.h" #include "Settings.h" @@ -28,12 +29,22 @@ class FileManager { static fs::Error writeUserScale(const UserScale &userScale, int slot); static fs::Error readUserScale(UserScale &userScale, int slot); + static fs::Error writeNoteSequence(const NoteSequence ¬eSequence, int slot); + static fs::Error readNoteSequence(NoteSequence ¬eSequence, int slot); + static fs::Error writeCurveSequence(const CurveSequence &curveSequence, int slot); + static fs::Error readCurveSequence(CurveSequence &curveSequence, int slot); + static fs::Error writeProject(const Project &project, const char *path); static fs::Error readProject(Project &project, const char *path); static fs::Error writeUserScale(const UserScale &userScale, const char *path); static fs::Error readUserScale(UserScale &userScale, const char *path); + static fs::Error writeNoteSequence(const NoteSequence ¬eSequence, const char *path); + static fs::Error readNoteSequence(NoteSequence ¬eSequence, const char *path); + static fs::Error writeCurveSequence(const CurveSequence &curveSequence, const char *path); + static fs::Error readCurveSequence(CurveSequence &curveSequence, const char *path); + static fs::Error writeSettings(const Settings &settings, const char *path); static fs::Error readSettings(Settings &settings, const char *path); diff --git a/src/apps/sequencer/model/ModelUtils.h b/src/apps/sequencer/model/ModelUtils.h index 984a280b..75cf799a 100644 --- a/src/apps/sequencer/model/ModelUtils.h +++ b/src/apps/sequencer/model/ModelUtils.h @@ -66,7 +66,7 @@ static void shiftSteps(std::array &steps, int first, int last, int dire } template -static void shiftSteps(std::array &steps, const std::bitset &selected, int direction) +static void shiftSteps(std::array &steps, const std::bitset &selected, int first, int last, int direction) { uint8_t indices[N]; int count = 0; @@ -74,18 +74,18 @@ static void shiftSteps(std::array &steps, const std::bitset &selecte if (selected[i]) indices[count++] = i; } if (direction == 1) { - for (int i = count - 1; i >= 0; --i) { + for (int i = last - 1; i >= first; --i) { int index = indices[i]+1; - if (index == N) { + if (index == last) { index = 0; } std::swap(steps[indices[i]], steps[index]); } } else if (direction == -1) { - for (int i = 0; i < count; ++i) { + for (int i = first; i < last; ++i) { int index = indices[i]-1; if (index == -1) { - index = N-1; + index = last-1; } std::swap(steps[indices[i]], steps[index]); } diff --git a/src/apps/sequencer/model/NoteSequence.cpp b/src/apps/sequencer/model/NoteSequence.cpp index c81e86a5..3c074a6f 100644 --- a/src/apps/sequencer/model/NoteSequence.cpp +++ b/src/apps/sequencer/model/NoteSequence.cpp @@ -204,6 +204,7 @@ void NoteSequence::Step::write(VersionedSerializedWriter &writer) const { } void NoteSequence::Step::read(VersionedSerializedReader &reader) { + if (reader.dataVersion() < ProjectVersion::Version27) { reader.read(_data0.raw); reader.readAs(_data1.raw); @@ -219,9 +220,20 @@ void NoteSequence::Step::read(VersionedSerializedReader &reader) { } else { reader.read(_data0.raw); reader.read(_data1.raw); + if (reader.dataVersion() < ProjectVersion::Version36) { + bool bypassScale = (bool)((_data0.raw >> 31) & 0x1); + + _data0.raw = (_data0.raw & 0x3 ) | (((_data0.raw ) & 0xFFFFFFFC) << 1); + _data1.raw = 0x7FFFFFFF & (_data1.raw << 1); + _data1.bypassScale = bypassScale; + } + if (reader.dataVersion() < ProjectVersion::Version34) { setBypassScale(false); } + + + } } @@ -252,6 +264,7 @@ void NoteSequence::writeRouted(Routing::Target target, int intValue, float float } void NoteSequence::clear() { + setName("INIT"); setScale(-1); setRootNote(-1); setDivisor(12); @@ -311,7 +324,7 @@ void NoteSequence::setNotes(std::initializer_list notes) { void NoteSequence::shiftSteps(const std::bitset &selected, int direction) { if (selected.any()) { - ModelUtils::shiftSteps(_steps, selected, direction); + ModelUtils::shiftSteps(_steps, selected, firstStep(), lastStep(), direction); } else { ModelUtils::shiftSteps(_steps, firstStep(), lastStep(), direction); } @@ -332,9 +345,12 @@ void NoteSequence::write(VersionedSerializedWriter &writer) const { writer.write(_lastStep.base); writeArray(writer, _steps); + writer.write(_name, NameLength + 1); + writer.write(_slot); + writer.writeHash(); } -void NoteSequence::read(VersionedSerializedReader &reader) { +bool NoteSequence::read(VersionedSerializedReader &reader) { reader.read(_scale.base); reader.read(_rootNote.base); if (reader.dataVersion() < ProjectVersion::Version10) { @@ -348,4 +364,17 @@ void NoteSequence::read(VersionedSerializedReader &reader) { reader.read(_lastStep.base); readArray(reader, _steps); + + if (reader.dataVersion() >= ProjectVersion::Version35) { + reader.read(_name, NameLength + 1, ProjectVersion::Version35); + reader.read(_slot); + bool success = reader.checkHash(); + if (!success) { + clear(); + } + + return success; + } else { + return true; + } } diff --git a/src/apps/sequencer/model/NoteSequence.h b/src/apps/sequencer/model/NoteSequence.h index efe39255..aea55edd 100644 --- a/src/apps/sequencer/model/NoteSequence.h +++ b/src/apps/sequencer/model/NoteSequence.h @@ -7,9 +7,11 @@ #include "Types.h" #include "Scale.h" #include "Routing.h" +#include "FileDefs.h" #include "core/math/Math.h" #include "core/utils/StringBuilder.h" +#include "core/utils/StringUtils.h" #include #include @@ -28,7 +30,7 @@ class NoteSequence { typedef SignedValue<4> GateOffset; typedef UnsignedValue<3> Retrigger; typedef UnsignedValue<4> RetriggerProbability; - typedef UnsignedValue<3> Length; + typedef UnsignedValue<4> Length; typedef SignedValue<4> LengthVariationRange; typedef UnsignedValue<4> LengthVariationProbability; typedef SignedValue<7> Note; @@ -98,13 +100,16 @@ class NoteSequence { }; + static constexpr size_t NameLength = FileHeader::NameLength; + + class Step { public: //---------------------------------------- // Properties //---------------------------------------- - + // stage void setStageRepeats(int repeats) { _data1.stageRepeats = StageRepeats::clamp(repeats); @@ -115,9 +120,9 @@ class NoteSequence { _data1.stageRepeatMode = mode; } - StageRepeatMode stageRepeatMode() const { + StageRepeatMode stageRepeatMode() const { int value = _data1.stageRepeatMode; - return static_cast(value); + return static_cast(value); } // gate @@ -216,9 +221,9 @@ class NoteSequence { int layerValue(Layer layer) const; void setLayerValue(Layer layer, int value); - bool bypassScale() const { return _data0.bypassScale ? true : false; } + bool bypassScale() const { return _data1.bypassScale ? true : false; } void setBypassScale(bool bypass) { - _data0.bypassScale = bypass; + _data1.bypassScale = bypass; } void toggleBypassScale() { setBypassScale(!bypassScale()); @@ -249,23 +254,23 @@ class NoteSequence { BitField gate; BitField slide; BitField length; - BitField lengthVariationRange; - BitField lengthVariationProbability; - BitField note; - BitField noteVariationRange; - BitField noteVariationProbability; - BitField bypassScale; + BitField lengthVariationRange; + BitField lengthVariationProbability; + BitField note; + BitField noteVariationRange; + BitField noteVariationProbability; } _data0; union { uint32_t raw; - BitField retrigger; - BitField gateProbability; - BitField retriggerProbability; - BitField gateOffset; - BitField condition; - BitField stageRepeats; - BitField stageRepeatMode; - // 5 bits left + BitField bypassScale; + BitField retrigger; + BitField gateProbability; + BitField retriggerProbability; + BitField gateOffset; + BitField condition; + BitField stageRepeats; + BitField stageRepeatMode; + // 4 bits left } _data1; }; @@ -275,6 +280,23 @@ class NoteSequence { // Properties //---------------------------------------- + // slot + + int slot() const { return _slot; } + void setSlot(int slot) { + _slot = slot; + } + bool slotAssigned() const { + return _slot != uint8_t(-1); + } + + // name + + const char *name() const { return _name; } + void setName(const char *name) { + StringUtils::copy(_name, name, sizeof(_name)); + } + // trackIndex int trackIndex() const { return _trackIndex; } @@ -314,7 +336,7 @@ class NoteSequence { } } - + } } @@ -517,12 +539,19 @@ class NoteSequence { void duplicateSteps(); void write(VersionedSerializedWriter &writer) const; - void read(VersionedSerializedReader &reader); + bool read(VersionedSerializedReader &reader); int trackIndex() { return _trackIndex; } + int section() { return _section; } + const int section() const { return _section; } + + void setSecion(int section) { + _section = section; + } + private: void setTrackIndex(int trackIndex) { _trackIndex = trackIndex; } @@ -537,6 +566,8 @@ class NoteSequence { } } + uint8_t _slot = uint8_t(-1); + char _name[NameLength + 1]; int8_t _trackIndex = -1; Routable _scale; @@ -553,5 +584,7 @@ class NoteSequence { uint8_t _edited; + int _section = 0; + friend class NoteTrack; }; diff --git a/src/apps/sequencer/model/Project.h b/src/apps/sequencer/model/Project.h index 0c2beb9e..01820c33 100644 --- a/src/apps/sequencer/model/Project.h +++ b/src/apps/sequencer/model/Project.h @@ -385,6 +385,24 @@ class Project { const MidiOutput &midiOutput() const { return _midiOutput; } MidiOutput &midiOutput() { return _midiOutput; } + const int stepsToStop() const { return _stepsToStop;} + int stepsToStop() { return _stepsToStop; } + + void setStepsToStop(int steps) { + _stepsToStop = clamp(steps, 0 , CONFIG_STEP_COUNT); + } + + void editStepsToStop(int steps) { + setStepsToStop(steps + stepsToStop()); + } + + void printStepsToStop(StringBuilder &str) const { + if (_stepsToStop == 0) { + str("Off"); + } else { + str("%d", stepsToStop()); + } + } // selectedTrackIndex int selectedTrackIndex() const { return _selectedTrackIndex; } @@ -404,6 +422,9 @@ class Project { case Track::TrackMode::MidiCv: StringUtils::copy(_selectedTrackName, selectedTrack().midiCvTrack().name(), sizeof(_selectedTrackName)); break; + case Track::TrackMode::Stochastic: + StringUtils::copy(_selectedTrackName, selectedTrack().stochasticTrack().name(), sizeof(_selectedTrackName)); + break; case Track::TrackMode::Last: break; } @@ -438,6 +459,10 @@ class Project { NoteSequence::Layer selectedNoteSequenceLayer() const { return _selectedNoteSequenceLayer; } void setSelectedNoteSequenceLayer(NoteSequence::Layer layer) { _selectedNoteSequenceLayer = layer; } + + StochasticSequence::Layer selectedStochasticSequenceLayer() const { return _selectedStochasticSequenceLayer; } + void setSelectedStochasticSequenceLayer(StochasticSequence::Layer layer) { _selectedStochasticSequenceLayer = layer; } + // selectedCurveSequenceLayer CurveSequence::Layer selectedCurveSequenceLayer() const { return _selectedCurveSequenceLayer; } @@ -476,6 +501,16 @@ class Project { const CurveSequence &selectedCurveSequence() const { return curveSequence(_selectedTrackIndex, selectedPatternIndex()); } CurveSequence &selectedCurveSequence() { return curveSequence(_selectedTrackIndex, selectedPatternIndex()); } + // curveSequence + + const StochasticSequence &stochasticSequence(int trackIndex, int patternIndex) const { return _tracks[trackIndex].stochasticTrack().sequence(patternIndex); } + StochasticSequence &stochasticSequence(int trackIndex, int patternIndex) { return _tracks[trackIndex].stochasticTrack().sequence(patternIndex); } + + // selectedCurveSequence + + const StochasticSequence &selectedStochasticSequence() const { return stochasticSequence(_selectedTrackIndex, selectedPatternIndex()); } + StochasticSequence &selectedStochasticSequence() { return stochasticSequence(_selectedTrackIndex, selectedPatternIndex()); } + //---------------------------------------- // Routing //---------------------------------------- @@ -540,12 +575,15 @@ class Project { Routing _routing; MidiOutput _midiOutput; + uint8_t _stepsToStop; + int _selectedTrackIndex = 0; int _selectedPatternIndex = 0; char _selectedTrackName[FileHeader::NameLength+1] = ""; NoteSequence::Layer _selectedNoteSequenceLayer = NoteSequence::Layer(0); CurveSequence::Layer _selectedCurveSequenceLayer = CurveSequence::Layer(0); + StochasticSequence::Layer _selectedStochasticSequenceLayer = StochasticSequence::Layer(10); Observable _observable; }; diff --git a/src/apps/sequencer/model/ProjectVersion.h b/src/apps/sequencer/model/ProjectVersion.h index 5dd4d32b..83700fac 100644 --- a/src/apps/sequencer/model/ProjectVersion.h +++ b/src/apps/sequencer/model/ProjectVersion.h @@ -99,6 +99,13 @@ enum ProjectVersion { // add bypass scale Version34 = 34, + // add sequence name + Version35 = 35, + + // change note length form 3 to 4 bits + Version36 = 36, + + // automatically derive latest version Last, Latest = Last - 1, diff --git a/src/apps/sequencer/model/Routing.cpp b/src/apps/sequencer/model/Routing.cpp index 14acdda5..cd780031 100644 --- a/src/apps/sequencer/model/Routing.cpp +++ b/src/apps/sequencer/model/Routing.cpp @@ -215,6 +215,15 @@ void Routing::writeTarget(Target target, uint8_t tracks, float normalized) { track.midiCvTrack().writeRouted(target, intValue, floatValue); } break; + case Track::TrackMode::Stochastic: + if (isTrackTarget(target)) { + track.stochasticTrack().writeRouted(target, intValue, floatValue); + } else { + for (int patternIndex = 0; patternIndex < CONFIG_PATTERN_COUNT; ++patternIndex) { + track.stochasticTrack().sequence(patternIndex).writeRouted(target, intValue, floatValue); + } + } + break; case Track::TrackMode::Last: break; } @@ -307,6 +316,15 @@ static const TargetInfo targetInfos[int(Routing::Target::Last)] = { [int(Routing::Target::Divisor)] = { 1, 768, 6, 24, 1 }, [int(Routing::Target::Scale)] = { 0, 23, 0, 23, 1 }, [int(Routing::Target::RootNote)] = { 0, 11, 0, 11, 1 }, + [int(Routing::Target::Reseed)] = { 0, 1, 0, 1, 1 }, + [int(Routing::Target::RestProbability2)] = { -8, 8, -8, 8, 8 }, + [int(Routing::Target::RestProbability4)] = { -8, 8, -8, 8, 8 }, + [int(Routing::Target::RestProbability8)] = { -8, 8, -8, 8, 8 }, + [int(Routing::Target::SequenceFirstStep)] = { 0, 63, 1, 63, 16 }, + [int(Routing::Target::SequenceLastStep)] = { 0, 63, 0, 63, 16 }, + [int(Routing::Target::LowOctaveRange)] = {-10, 10, -1, 1, 1 }, + [int(Routing::Target::HighOctaveRange)] = {-10, 10, -1, 1, 1 }, + [int(Routing::Target::LengthModifier)] = { -8, 8, -8, 8, 8 }, }; float Routing::normalizeTargetValue(Routing::Target target, float value) { @@ -347,6 +365,8 @@ void Routing::printTargetValue(Routing::Target target, float normalized, StringB case Target::Octave: case Target::Transpose: case Target::Rotate: + case Target::LowOctaveRange: + case Target::HighOctaveRange: str("%+d", intValue); break; case Target::Offset: @@ -357,6 +377,12 @@ void Routing::printTargetValue(Routing::Target target, float normalized, StringB case Target::LengthBias: case Target::NoteProbabilityBias: case Target::ShapeProbabilityBias: + case Target::RestProbability2: + case Target::RestProbability4: + case Target::RestProbability8: + case Target::SequenceFirstStep: + case Target::SequenceLastStep: + case Target::LengthModifier: str("%+.1f%%", value * 12.5f); break; case Target::Divisor: diff --git a/src/apps/sequencer/model/Routing.h b/src/apps/sequencer/model/Routing.h index 85685e8d..56133b13 100644 --- a/src/apps/sequencer/model/Routing.h +++ b/src/apps/sequencer/model/Routing.h @@ -72,7 +72,16 @@ class Routing { Divisor, Scale, RootNote, - SequenceLast = RootNote, + Reseed, + RestProbability2, + RestProbability4, + RestProbability8, + SequenceFirstStep, + SequenceLastStep, + LowOctaveRange, + HighOctaveRange, + LengthModifier, + SequenceLast = LengthModifier, Last, }; @@ -112,6 +121,17 @@ class Routing { case Target::Divisor: return "Divisor"; case Target::Scale: return "Scale"; case Target::RootNote: return "Root Note"; + case Target::Reseed: return "Reseed"; + case Target::RestProbability2: return "Rest Prob. 2"; + case Target::RestProbability4: return "Rest Prob. 4"; + case Target::RestProbability8: return "Rest Prob. 8"; + case Target::SequenceFirstStep: return "Seq First Step"; + case Target::SequenceLastStep: return "Seq Last Step"; + + case Target::LowOctaveRange: return "L Oct Range"; + case Target::HighOctaveRange: return "H Oct Range"; + + case Target::LengthModifier: return "Length Mod"; case Target::Last: break; } @@ -154,6 +174,15 @@ class Routing { case Target::PlayToggle: return 26; case Target::RecordToggle: return 27; + case Target::Reseed: return 28; + case Target::SequenceFirstStep: return 30; + case Target::SequenceLastStep: return 31; + case Target::LowOctaveRange: return 32; + case Target::HighOctaveRange: return 33; + case Target::RestProbability2: return 34; + case Target::RestProbability4: return 35; + case Target::RestProbability8: return 36; + case Target::LengthModifier: return 37; case Target::Last: break; } diff --git a/src/apps/sequencer/model/StochasticSequence.cpp b/src/apps/sequencer/model/StochasticSequence.cpp new file mode 100644 index 00000000..45f1a612 --- /dev/null +++ b/src/apps/sequencer/model/StochasticSequence.cpp @@ -0,0 +1,389 @@ +#include "StochasticSequence.h" +#include "ProjectVersion.h" + +#include "ModelUtils.h" +#include "Routing.h" +#include + +Types::LayerRange StochasticSequence::layerRange(Layer layer) { + #define CASE(_layer_) \ + case Layer::_layer_: \ + return { _layer_::Min, _layer_::Max }; + + switch (layer) { + case Layer::Gate: + return { 0, 1 }; + case Layer::Slide: + return { 0, 1 }; + CASE(GateOffset) + CASE(GateProbability) + CASE(Retrigger) + CASE(RetriggerProbability) + CASE(Length) + CASE(LengthVariationRange) + CASE(LengthVariationProbability) + CASE(NoteOctave) + CASE(NoteOctaveProbability) + CASE(NoteVariationProbability) + CASE(Condition) + CASE(StageRepeats) + CASE(StageRepeatsMode) + case Layer::Last: + break; + } + + #undef CASE + + return { 0, 0 }; +} + +int StochasticSequence::layerDefaultValue(Layer layer) +{ + StochasticSequence::Step step; + + switch (layer) { + case Layer::Gate: + return step.gate(); + case Layer::GateProbability: + return step.gateProbability(); + case Layer::GateOffset: + return step.gateOffset(); + case Layer::Slide: + return step.slide(); + case Layer::Retrigger: + return step.retrigger(); + case Layer::RetriggerProbability: + return step.retriggerProbability(); + case Layer::Length: + return step.length(); + case Layer::LengthVariationRange: + return step.lengthVariationRange(); + case Layer::LengthVariationProbability: + return step.lengthVariationProbability(); + case Layer::NoteOctave: + return step.noteOctave(); + case Layer::NoteOctaveProbability: + return step.noteOctaveProbability(); + case Layer::NoteVariationProbability: + return step.noteVariationProbability(); + case Layer::Condition: + return int(step.condition()); + case Layer::StageRepeats: + return step.stageRepeats(); + case Layer::StageRepeatsMode: + return step.stageRepeatMode(); + case Layer::Last: + break; + } + + return 0; +} + +int StochasticSequence::Step::layerValue(Layer layer) const { + switch (layer) { + case Layer::Gate: + return gate() ? 1 : 0; + case Layer::Slide: + return slide() ? 1 : 0; + case Layer::GateProbability: + return gateProbability(); + case Layer::GateOffset: + return gateOffset(); + case Layer::Retrigger: + return retrigger(); + case Layer::RetriggerProbability: + return retriggerProbability(); + case Layer::Length: + return length(); + case Layer::LengthVariationRange: + return lengthVariationRange(); + case Layer::LengthVariationProbability: + return lengthVariationProbability(); + case Layer::NoteOctave: + return noteOctave(); + case Layer::NoteOctaveProbability: + return noteOctaveProbability(); + case Layer::NoteVariationProbability: + return noteVariationProbability(); + case Layer::Condition: + return int(condition()); + case Layer::StageRepeats: + return stageRepeats(); + case Layer::StageRepeatsMode: + return stageRepeatMode(); + case Layer::Last: + break; + } + + return 0; +} + +void StochasticSequence::Step::setLayerValue(Layer layer, int value) { + switch (layer) { + case Layer::Gate: + setGate(value); + break; + case Layer::Slide: + setSlide(value); + break; + case Layer::GateProbability: + setGateProbability(value); + break; + case Layer::GateOffset: + setGateOffset(value); + break; + case Layer::Retrigger: + setRetrigger(value); + break; + case Layer::RetriggerProbability: + setRetriggerProbability(value); + break; + case Layer::Length: + setLength(value); + break; + case Layer::LengthVariationRange: + setLengthVariationRange(value); + break; + case Layer::LengthVariationProbability: + setLengthVariationProbability(value); + break; + case Layer::NoteOctave: + setNoteOctave(value); + break; + case Layer::NoteOctaveProbability: + setNoteOctaveProbability(value); + break; + case Layer::NoteVariationProbability: + setNoteVariationProbability(value); + break; + case Layer::Condition: + setCondition(Types::Condition(value)); + break; + case Layer::StageRepeats: + setStageRepeats(value); + break; + case Layer::StageRepeatsMode: + setStageRepeatsMode(static_cast(value)); + break; + case Layer::Last: + break; + } +} + +void StochasticSequence::Step::clear() { + _data0.raw = 0; + _data1.raw = 1; + setGate(false); + setGateProbability(GateProbability::Max); + setGateOffset(0); + setSlide(false); + setBypassScale(true); + setRetrigger(0); + setRetriggerProbability(RetriggerProbability::Max); + setLength(Length::Max / 2); + setLengthVariationRange(0); + setLengthVariationProbability(LengthVariationProbability::Max); + setNote(0); + setNoteOctave(0); + setNoteOctaveProbability(NoteOctaveProbability::Max); + setNoteVariationProbability(0); + setCondition(Types::Condition::Off); + setStageRepeats(0); + setStageRepeatsMode(StageRepeatMode::Each); +} + +void StochasticSequence::Step::write(VersionedSerializedWriter &writer) const { + writer.write(_data0.raw); + writer.write(_data1.raw); +} + +void StochasticSequence::Step::read(VersionedSerializedReader &reader) { + if (reader.dataVersion() < ProjectVersion::Version27) { + reader.read(_data0.raw); + reader.readAs(_data1.raw); + if (reader.dataVersion() < ProjectVersion::Version5) { + _data1.raw &= 0x1f; + } + if (reader.dataVersion() < ProjectVersion::Version7) { + setGateOffset(0); + } + if (reader.dataVersion() < ProjectVersion::Version12) { + setCondition(Types::Condition(0)); + } + } else { + reader.read(_data0.raw); + reader.read(_data1.raw); + } +} + +void StochasticSequence::writeRouted(Routing::Target target, int intValue, float floatValue) { + switch (target) { + case Routing::Target::Scale: + setScale(intValue, true); + break; + case Routing::Target::RootNote: + setRootNote(intValue, true); + break; + case Routing::Target::Divisor: + setDivisor(intValue, true); + break; + case Routing::Target::RunMode: + setRunMode(Types::RunMode(intValue), true); + break; + case Routing::Target::FirstStep: + setFirstStep(intValue, true); + break; + case Routing::Target::LastStep: + setLastStep(intValue, true); + break; + case Routing::Target::Reseed: + setReseed(intValue, true); + break; + case Routing::Target::RestProbability2: + setRestProbability2(intValue, true); + break; + case Routing::Target::RestProbability4: + setRestProbability4(intValue, true); + break; + case Routing::Target::RestProbability8: + setRestProbability8(intValue, true); + break; + case Routing::Target::SequenceFirstStep: + setSequenceFirstStep(intValue, true); + break; + case Routing::Target::SequenceLastStep: + setSequenceLastStep(intValue, true); + break; + case Routing::Target::LowOctaveRange: + setLowOctaveRange(intValue, true); + break; + case Routing::Target::HighOctaveRange: + setHighOctaveRange(intValue, true); + break; + case Routing::Target::LengthModifier: + setLengthModifier(intValue, true); + break; + default: + break; + } +} + +void StochasticSequence::clear() { + setScale(-1); + setRootNote(-1); + setDivisor(12); + setResetMeasure(0); + setRunMode(Types::RunMode::Forward); + setFirstStep(0); + setLastStep(0); + //setRestProbability(0); + setSequenceFirstStep(0); + setSequenceLastStep(15); + setLengthModifier(0); + setRestProbability2(0); + setRestProbability4(0); + setRestProbability8(0); + setLowOctaveRange(0); + setHighOctaveRange(0); + setUseLoop(false); + setReseed(false); + + clearSteps(); +} + +void StochasticSequence::clearSteps() { + for (auto &step : _steps) { + step.clear(); + } + + for (int i = 0; i < 12; ++i) { + _steps[i].setNote(i); + } +} + +bool StochasticSequence::isEdited() const { + auto clearStep = Step(); + + for (int i = 0; i < 12; ++i) { + auto step = _steps[i]; + clearStep.setNote(i); + + if (step != clearStep) { + return true; + } + } + return false; +} + +void StochasticSequence::setGates(std::initializer_list gates) { + size_t step = 0; + for (auto gate : gates) { + if (step < _steps.size()) { + _steps[step++].setGate(gate); + } + } +} + +void StochasticSequence::setNotes(std::initializer_list notes) { + size_t step = 0; + for (auto note : notes) { + if (step < _steps.size()) { + _steps[step++].setNote(note); + } + } +} + +void StochasticSequence::shiftSteps(const std::bitset &selected, int direction) { + if (selected.any()) { + ModelUtils::shiftSteps(_steps, selected, firstStep(), lastStep(), direction); + } else { + ModelUtils::shiftSteps(_steps, firstStep(), lastStep(), direction); + } +} + +void StochasticSequence::duplicateSteps() { + ModelUtils::duplicateSteps(_steps, firstStep(), lastStep()); + setLastStep(lastStep() + (lastStep() - firstStep() + 1)); +} + +void StochasticSequence::write(VersionedSerializedWriter &writer) const { + writer.write(_scale.base); + writer.write(_rootNote.base); + writer.write(_divisor.base); + writer.write(_resetMeasure); + writer.write(_runMode.base); + writer.write(_firstStep); + writer.write(_lastStep); + writer.write(_restProbability2); + writer.write(_restProbability4); + writer.write(_restProbability8); + writer.write(_lengthModifier); + writer.write(_lowOctaveRange); + writer.write(_highOctaveRange); + + writeArray(writer, _steps); +} + +void StochasticSequence::read(VersionedSerializedReader &reader) { + reader.read(_scale.base); + reader.read(_rootNote.base); + if (reader.dataVersion() < ProjectVersion::Version10) { + reader.readAs(_divisor.base); + } else { + reader.read(_divisor.base); + } + reader.read(_resetMeasure); + reader.read(_runMode.base); + reader.read(_firstStep); + reader.read(_lastStep); + reader.read(_restProbability2, ProjectVersion::Version36); + reader.read(_restProbability4, ProjectVersion::Version36); + reader.read(_restProbability8, ProjectVersion::Version36); + reader.read(_lengthModifier, ProjectVersion::Version36); + reader.read(_lowOctaveRange, ProjectVersion::Version36); + reader.read(_highOctaveRange, ProjectVersion::Version36); + + + + readArray(reader, _steps); +} diff --git a/src/apps/sequencer/model/StochasticSequence.h b/src/apps/sequencer/model/StochasticSequence.h new file mode 100644 index 00000000..44e81c33 --- /dev/null +++ b/src/apps/sequencer/model/StochasticSequence.h @@ -0,0 +1,815 @@ +#pragma once + +#include "Config.h" +#include "Bitfield.h" +#include "Serialize.h" +#include "ModelUtils.h" +#include "Types.h" +#include "Scale.h" +#include "Routing.h" + +#include "core/math/Math.h" +#include "core/utils/StringBuilder.h" + +#include +#include +#include +#include +#include + +class StochasticSequence { +public: + //---------------------------------------- + // Types + //---------------------------------------- + + typedef UnsignedValue<4> GateProbability; + typedef SignedValue<4> GateOffset; + typedef UnsignedValue<3> Retrigger; + typedef UnsignedValue<4> RetriggerProbability; + typedef UnsignedValue<4> Length; + typedef SignedValue<4> LengthVariationRange; + typedef UnsignedValue<4> LengthVariationProbability; + typedef SignedValue<7> Note; + typedef UnsignedValue<4> NoteVariationProbability; + typedef SignedValue<3> NoteOctave; + typedef UnsignedValue<4> NoteOctaveProbability; + typedef UnsignedValue<7> Condition; + typedef UnsignedValue<3> StageRepeats; + typedef UnsignedValue<3> StageRepeatsMode; + + static_assert(int(Types::Condition::Last) <= Condition::Max + 1, "Condition enum does not fit"); + + enum class Layer { + Gate, + GateProbability, + GateOffset, + Retrigger, + RetriggerProbability, + StageRepeats, + StageRepeatsMode, + Length, + LengthVariationRange, + LengthVariationProbability, + NoteVariationProbability, + NoteOctave, + NoteOctaveProbability, + Slide, + Condition, + Last + }; + + static const char *layerName(Layer layer) { + switch (layer) { + case Layer::Gate: return "GATE"; + case Layer::GateProbability: return "GATE PROB"; + case Layer::GateOffset: return "GATE OFFSET"; + case Layer::Slide: return "SLIDE"; + case Layer::Retrigger: return "RETRIG"; + case Layer::RetriggerProbability: return "RETRIG PROB"; + case Layer::Length: return "LENGTH"; + case Layer::LengthVariationRange: return "LENGTH RANGE"; + case Layer::LengthVariationProbability: return "LENGTH PROB"; + case Layer::NoteOctave: return "OCTAVE"; + case Layer::NoteOctaveProbability: return "OCTAVE PROB"; + case Layer::NoteVariationProbability: return "NOTE PROB"; + case Layer::Condition: return "CONDITION"; + case Layer::StageRepeats: return "REPEAT"; + case Layer::StageRepeatsMode: return "REPEAT MODE"; + case Layer::Last: break; + } + return nullptr; + } + + static Types::LayerRange layerRange(Layer layer); + static int layerDefaultValue(Layer layer); + + enum StageRepeatMode { + Each, + First, + Middle, + Last, + Odd, + Even, + Triplets, + Random, + + }; + + enum Message { + None, + LoopOn, + LoopOff, + Cleared, + ReSeed, + }; + + class Step { + + public: + //---------------------------------------- + // Properties + //---------------------------------------- + + // stage + void setStageRepeats(int repeats) { + _data1.stageRepeats = StageRepeats::clamp(repeats); + } + unsigned int stageRepeats() const { return _data1.stageRepeats; } + + void setStageRepeatsMode(StageRepeatMode mode) { + _data1.stageRepeatMode = mode; + } + + StageRepeatMode stageRepeatMode() const { + int value = _data1.stageRepeatMode; + return static_cast(value); + } + + // gate + + bool gate() const { return _data0.gate ? true : false; } + void setGate(bool gate) { _data0.gate = gate; } + void toggleGate() { setGate(!gate()); } + + // gateProbability + + int gateProbability() const { return _data1.gateProbability; } + void setGateProbability(int gateProbability) { + _data1.gateProbability = GateProbability::clamp(gateProbability); + } + + // gateOffset + + int gateOffset() const { return GateOffset::Min + (int) _data1.gateOffset; } + void setGateOffset(int gateOffset) { + _data1.gateOffset = GateOffset::clamp(gateOffset) - GateOffset::Min; + } + + // slide + + bool slide() const { return _data0.slide ? true : false; } + void setSlide(bool slide) { + _data0.slide = slide; + } + void toggleSlide() { + setSlide(!slide()); + } + + // retrigger + + int retrigger() const { return _data1.retrigger; } + void setRetrigger(int retrigger) { + _data1.retrigger = Retrigger::clamp(retrigger); + } + + // retriggerProbability + + int retriggerProbability() const { return _data1.retriggerProbability; } + void setRetriggerProbability(int retriggerProbability) { + _data1.retriggerProbability = RetriggerProbability::clamp(retriggerProbability); + } + + // length + + int length() const { return _data0.length; } + void setLength(int length) { + _data0.length = Length::clamp(length); + } + + // lengthVariationRange + + int lengthVariationRange() const { return LengthVariationRange::Min + _data0.lengthVariationRange; } + void setLengthVariationRange(int lengthVariationRange) { + _data0.lengthVariationRange = LengthVariationRange::clamp(lengthVariationRange) - LengthVariationRange::Min; + } + + // lengthVariationProbability + + int lengthVariationProbability() const { return _data0.lengthVariationProbability; } + void setLengthVariationProbability(int lengthVariationProbability) { + _data0.lengthVariationProbability = LengthVariationProbability::clamp(lengthVariationProbability); + } + + // note + + int note() const { return Note::Min + _data0.note; } + void setNote(int note) { + _data0.note = Note::clamp(note) - Note::Min; + } + + // noteOctave + + int noteOctave() const { return NoteOctave::Min + _data0.noteOctave; } + void setNoteOctave(int noteOctave) { + _data0.noteOctave = NoteOctave::clamp(noteOctave) - NoteOctave::Min; + } + + // noteOctaveProbability + + int noteOctaveProbability() const { return _data0.noteOctaveProbability; } + void setNoteOctaveProbability(int noteOctaveProbability) { + _data0.noteOctaveProbability = NoteOctaveProbability::clamp(noteOctaveProbability); + } + + // noteVariationProbability + + int noteVariationProbability() const { return _data0.noteVariationProbability; } + void setNoteVariationProbability(int noteVariationProbability) { + _data0.noteVariationProbability = NoteVariationProbability::clamp(noteVariationProbability); + } + + // condition + + Types::Condition condition() const { return Types::Condition(int(_data1.condition)); } + void setCondition(Types::Condition condition) { + _data1.condition = int(ModelUtils::clampedEnum(condition)); + } + + int layerValue(Layer layer) const; + void setLayerValue(Layer layer, int value); + + + bool bypassScale() const { return _data1.bypassScale ? true : false; } + void setBypassScale(bool bypass) { + _data1.bypassScale = bypass; + } + void toggleBypassScale() { + setBypassScale(!bypassScale()); + } + + //---------------------------------------- + // Methods + //---------------------------------------- + + Step() { clear(); } + + void clear(); + + void write(VersionedSerializedWriter &writer) const; + void read(VersionedSerializedReader &reader); + + bool operator==(const Step &other) const { + return _data0.raw == other._data0.raw && _data1.raw == other._data1.raw; + } + + bool operator!=(const Step &other) const { + return !(*this == other); + } + + private: + union { + uint32_t raw; + BitField gate; + BitField slide; + BitField length; + BitField lengthVariationRange; + BitField lengthVariationProbability; + BitField note; + BitField noteOctave; + BitField noteVariationProbability; + BitField noteOctaveProbability; + + } _data0; + union { + uint32_t raw; + BitField bypassScale; + BitField retrigger; + BitField gateProbability; + BitField retriggerProbability; + BitField gateOffset; + BitField condition; + BitField stageRepeats; + BitField stageRepeatMode; + // 5 bits left + } _data1; + }; + + typedef std::array StepArray; + + //---------------------------------------- + // Properties + //---------------------------------------- + + // trackIndex + + int trackIndex() const { return _trackIndex; } + + // scale + + int scale() const { return _scale.get(isRouted(Routing::Target::Scale)); } + void setScale(int scale, bool routed = false) { + _scale.set(clamp(scale, -1, Scale::Count - 1), routed); + } + + int indexedScale() const { return scale() + 1; } + void setIndexedScale(int index) { + setScale(index - 1); + } + + void editScale(int value, bool shift, int defaultScale = 0) { + if (!isRouted(Routing::Target::Scale)) { + setScale(value, false); + } + } + + void printScale(StringBuilder &str) const { + printRouted(str, Routing::Target::Scale); + str(scale() < 0 ? "Default" : Scale::name(scale())); + } + + const Scale &selectedScale(int defaultScale) const { + return Scale::get(scale() < 0 ? defaultScale : scale()); + } + + // rootNote + + int rootNote() const { return _rootNote.get(isRouted(Routing::Target::RootNote)); } + void setRootNote(int rootNote, bool routed = false) { + _rootNote.set(clamp(rootNote, -1, 11), routed); + } + + int indexedRootNote() const { return rootNote() + 1; } + void setIndexedRootNote(int index) { + setRootNote(index - 1); + } + + void editRootNote(int value, bool shift) { + if (!isRouted(Routing::Target::RootNote)) { + setRootNote(rootNote() + value); + } + } + + void printRootNote(StringBuilder &str) const { + printRouted(str, Routing::Target::RootNote); + if (rootNote() < 0) { + str("Default"); + } else { + Types::printNote(str, rootNote()); + } + } + + int selectedRootNote(int defaultRootNote) const { + return rootNote() < 0 ? defaultRootNote : rootNote(); + } + + // divisor + + int divisor() const { return _divisor.get(isRouted(Routing::Target::Divisor)); } + void setDivisor(int divisor, bool routed = false) { + _divisor.set(ModelUtils::clampDivisor(divisor), routed); + } + + int indexedDivisor() const { return ModelUtils::divisorToIndex(divisor()); } + void setIndexedDivisor(int index) { + int divisor = ModelUtils::indexToDivisor(index); + if (divisor > 0) { + setDivisor(divisor); + } + } + + void editDivisor(int value, bool shift) { + if (!isRouted(Routing::Target::Divisor)) { + setDivisor(ModelUtils::adjustedByDivisor(divisor(), value, shift)); + } + } + + void printDivisor(StringBuilder &str) const { + printRouted(str, Routing::Target::Divisor); + ModelUtils::printDivisor(str, divisor()); + } + + // resetMeasure + + int resetMeasure() const { return _resetMeasure; } + void setResetMeasure(int resetMeasure) { + _resetMeasure = clamp(resetMeasure, 0, 128); + } + + void editResetMeasure(int value, bool shift) { + setResetMeasure(ModelUtils::adjustedByPowerOfTwo(resetMeasure(), value, shift)); + } + + void printResetMeasure(StringBuilder &str) const { + if (resetMeasure() == 0) { + str("off"); + } else { + str("%d %s", resetMeasure(), resetMeasure() > 1 ? "bars" : "bar"); + } + } + + // runMode + + Types::RunMode runMode() const { return _runMode.get(isRouted(Routing::Target::RunMode)); } + void setRunMode(Types::RunMode runMode, bool routed = false) { + _runMode.set(ModelUtils::clampedEnum(runMode), routed); + } + + void editRunMode(int value, bool shift) { + if (!isRouted(Routing::Target::RunMode)) { + setRunMode(ModelUtils::adjustedEnum(runMode(), value)); + } + } + + void printRunMode(StringBuilder &str) const { + printRouted(str, Routing::Target::RunMode); + str(Types::runModeName(runMode())); + } + + // firstStep + + int firstStep() const { + return _firstStep; + } + + void setFirstStep(int firstStep, bool routed = false) { + _firstStep= clamp(firstStep, 0, lastStep()); + } + + void editFirstStep(int value, bool shift) { + if (shift) { + offsetFirstAndLastStep(value); + } else { + setFirstStep(firstStep() + value); + } + } + + void printFirstStep(StringBuilder &str) const { + str("%d", firstStep() + 1); + } + + // lastStep + + int lastStep() const { + // make sure last step is always >= first step even if stored value is invalid (due to routing changes) + return std::max(firstStep(), int(_lastStep)); + } + + void setLastStep(int lastStep, bool routed = false) { + _lastStep = clamp(lastStep, firstStep(), CONFIG_STEP_COUNT - 1); + } + + void editLastStep(int value, bool shift) { + if (shift) { + offsetFirstAndLastStep(value); + } else { + setLastStep(lastStep() + value); + } + } + + void printLastStep(StringBuilder &str) const { + str("%d", lastStep() + 1); + } + + void setStepBounds(int index) { + _firstStep = index; + _lastStep = index; } + + // rest probability 1 step + + int restProbability() const { + int prob = 8 - restProbability2() - restProbability4() - restProbability8(); + if (prob < 0) { + prob = 0; + } + return prob; + } + + void printRestProbability(StringBuilder &str) const { + str("%+.1f%%", restProbability() * 12.5f); + } + + // rest probability 2 steps + + int restProbability2() const { return _restProbability2.get(isRouted(Routing::Target::RestProbability2)); } + void setRestProbability2(int restProbability, bool routed = false) { + _restProbability2.set(clamp(restProbability, 0, 8), routed); + } + + void editRestProbability2(int value, bool shift) { + if (!isRouted(Routing::Target::RestProbability2)) { + setRestProbability2(restProbability2() + value); + } + } + + void printRestProbability2(StringBuilder &str) const { + printRouted(str, Routing::Target::RestProbability2); + str("%+.1f%%", restProbability2() * 12.5f); + } + + // rest probability 4 steps + + int restProbability4() const { return _restProbability4.get(isRouted(Routing::Target::RestProbability4)); } + void setRestProbability4(int restProbability, bool routed = false) { + _restProbability4.set(clamp(restProbability, 0, 8), routed); + } + + void editRestProbability4(int value, bool shift) { + if (!isRouted(Routing::Target::RestProbability4)) { + setRestProbability4(restProbability4() + value); + } + } + + void printRestProbability4(StringBuilder &str) const { + printRouted(str, Routing::Target::RestProbability4); + str("%+.1f%%", restProbability4() * 12.5f); + } + + // rest probability 8 steps + + int restProbability8() const { return _restProbability8.get(isRouted(Routing::Target::RestProbability8)); } + void setRestProbability8(int restProbability, bool routed = false) { + _restProbability8.set(clamp(restProbability, 0, 8), routed); + } + + void editRestProbability8(int value, bool shift) { + if (!isRouted(Routing::Target::RestProbability8)) { + setRestProbability8(restProbability8() + value); + } + } + + void printRestProbability8(StringBuilder &str) const { + printRouted(str, Routing::Target::RestProbability8); + str("%+.1f%%", restProbability8() * 12.5f); + } + + // reseed + + bool reseed() const { + return _reseed.get(isRouted(Routing::Target::Reseed)); + } + + void setReseed(int r, bool routed = false) { + if (!isRouted(Routing::Target::Reseed)) { + _reseed.set(r, routed); + } + if (reseed()) { + setMessage(Message::ReSeed); + } + } + + // sequence loop last step + + int sequenceLastStep() const { + return std::max(sequenceFirstStep(), int(_sequenceLastStep.get(isRouted(Routing::Target::SequenceLastStep)))); + } + + void setSequenceLastStep(int lastStep, bool routed = false) { + _sequenceLastStep.set(clamp(lastStep, sequenceFirstStep(), CONFIG_STEP_COUNT - 1), routed); + } + + void editSequenceLastStep(int value, bool shift) { + if (shift) { + offsetSequenceFirstAndLastStep(value); + } else if (!isRouted(Routing::Target::SequenceLastStep)) { + setSequenceLastStep(sequenceLastStep() + value); + } + } + + void printSequenceLastStep(StringBuilder &str) const { + printRouted(str, Routing::Target::SequenceLastStep); + str("%d", sequenceLastStep()+1); + } + + // sequence loop first step + + int sequenceFirstStep() const { + return _sequenceFirstStep.get(isRouted(Routing::Target::SequenceFirstStep)); + } + + void setSequenceFirstStep(int firstStep, bool routed = false) { + _sequenceFirstStep.set(clamp(firstStep, 0, sequenceLastStep()), routed); + } + + void editSequenceFirstStep(int value, bool shift) { + if (shift) { + offsetSequenceFirstAndLastStep(value); + } else if (!isRouted(Routing::Target::FirstStep)) { + setSequenceFirstStep(sequenceFirstStep() + value); + } + } + + void printSequenceFirstStep(StringBuilder &str) const { + printRouted(str, Routing::Target::SequenceFirstStep); + str("%d", sequenceFirstStep()+1); + } + + // sequence length + + int sequenceLength() { + return _sequenceLastStep.base - _sequenceFirstStep.base + 1; + } + + // buffer loop length + + int bufferLoopLength() { + int bufferLoopLength = 16; + if (_sequenceLastStep.base > 16) { + bufferLoopLength = _sequenceLastStep.base+1; + } + return bufferLoopLength; + } + + // use loop + + void setUseLoop() { + _useLoop = !_useLoop; + if (_useLoop) { + setMessage(Message::LoopOn); + } else { + setMessage(Message::LoopOff); + } + } + + void setUseLoop(bool value) { + _useLoop = value; + } + + bool useLoop() { + return _useLoop; + } + + const bool useLoop() const { return _useLoop;} + + // clear loop + + void setClearLoop(bool clearLoop) { + _clearLoop = clearLoop; + if (_clearLoop) { + setMessage(Message::Cleared); + } + } + + bool clearLoop() { + return _clearLoop; + } + + const bool clearLoop() const { return _clearLoop; } + + // low octave range + + int lowOctaveRange() const { return _lowOctaveRange.get(isRouted(Routing::Target::LowOctaveRange)); } + void setLowOctaveRange(int octave, bool routed = false) { + _lowOctaveRange.set(clamp(octave, -10, highOctaveRange()), routed); + } + + void editLowOctaveRange(int value, bool shift) { + if (!isRouted(Routing::Target::LowOctaveRange)) { + setLowOctaveRange(lowOctaveRange() + value); + } + } + + void printLowOctaveRange(StringBuilder &str) const { + printRouted(str, Routing::Target::LowOctaveRange); + str("%+d", lowOctaveRange()); + } + + // high octave range + + int highOctaveRange() const { return _highOctaveRange.get(isRouted(Routing::Target::HighOctaveRange)); } + void setHighOctaveRange(int octave, bool routed = false) { + _highOctaveRange.set(clamp(octave, lowOctaveRange(), 10), routed); + } + + void editHighOctaveRange(int value, bool shift) { + if (!isRouted(Routing::Target::HighOctaveRange)) { + setHighOctaveRange(highOctaveRange() + value); + } + } + + void printHighOctaveRange(StringBuilder &str) const { + printRouted(str, Routing::Target::HighOctaveRange); + str("%+d", highOctaveRange()); + } + + // length modifier + + int lengthModifier() const { return _lengthModifier.get(isRouted(Routing::Target::LengthModifier)); } + void setLengthModifier(int length, bool routed = false) { + _lengthModifier.set(clamp(length, -StochasticSequence::NoteVariationProbability::Range, StochasticSequence::NoteVariationProbability::Range), routed); + } + + void editLengthModifier(int value, bool shift) { + if (!isRouted(Routing::Target::LengthModifier)) { + setLengthModifier(lengthModifier() + value); + } + } + + void printLengthModifier(StringBuilder &str) const { + printRouted(str, Routing::Target::LengthModifier); + str("%+.1f%%", lengthModifier() * 12.5f); + } + + + Message message() { + return _message; + } + + void setMessage(Message message) { + _message = message; + } + + const bool isEmpty() const { + for (int i = 0; i < 12; ++i) { + auto s = step(i); + if (s.gate()) { + return false; + } + } + return true; + } + + + // steps + + const StepArray &steps() const { return _steps; } + StepArray &steps() { return _steps; } + + const Step &step(int index) const { return _steps[index]; } + Step &step(int index) { return _steps[index]; } + + //---------------------------------------- + // Routing + //---------------------------------------- + + inline bool isRouted(Routing::Target target) const { return Routing::isRouted(target, _trackIndex); } + inline void printRouted(StringBuilder &str, Routing::Target target) const { Routing::printRouted(str, target, _trackIndex); } + void writeRouted(Routing::Target target, int intValue, float floatValue); + + //---------------------------------------- + // Methods + //---------------------------------------- + + StochasticSequence() { clear(); } + + void clear(); + void clearSteps(); + + bool isEdited() const; + + void setGates(std::initializer_list gates); + void setNotes(std::initializer_list notes); + + void shiftSteps(const std::bitset &selected, int direction); + + void duplicateSteps(); + + void write(VersionedSerializedWriter &writer) const; + void read(VersionedSerializedReader &reader); + +private: + void setTrackIndex(int trackIndex) { _trackIndex = trackIndex; } + + void offsetFirstAndLastStep(int value) { + value = clamp(value, -firstStep(), CONFIG_STEP_COUNT - 1 - lastStep()); + if (value > 0) { + editLastStep(value, false); + editFirstStep(value, false); + } else { + editFirstStep(value, false); + editLastStep(value, false); + } + } + + void offsetSequenceFirstAndLastStep(int value) { + value = clamp(value, -sequenceFirstStep(), CONFIG_STEP_COUNT - 1 - sequenceLastStep()); + if (value > 0) { + editSequenceLastStep(value, false); + editSequenceFirstStep(value, false); + } else { + editSequenceFirstStep(value, false); + editSequenceLastStep(value, false); + } + } + + int8_t _trackIndex = -1; + Routable _scale; + Routable _rootNote; + Routable _divisor; + uint8_t _resetMeasure; + Routable _runMode; + uint8_t _firstStep; + uint8_t _lastStep; + Routable _reseed; + + Routable _restProbability2; + Routable _restProbability4; + Routable _restProbability8; + + Routable _sequenceLastStep; + Routable _sequenceFirstStep; + + Routable _lowOctaveRange; + Routable _highOctaveRange; + + Routable _lengthModifier; + + StepArray _steps; + + bool _useLoop = false; + bool _clearLoop = false; + + Message _message = Message::None; + + friend class StochasticTrack; +}; diff --git a/src/apps/sequencer/model/StochasticTrack.cpp b/src/apps/sequencer/model/StochasticTrack.cpp new file mode 100644 index 00000000..5a8b6589 --- /dev/null +++ b/src/apps/sequencer/model/StochasticTrack.cpp @@ -0,0 +1,66 @@ +#include "StochasticTrack.h" +#include "ProjectVersion.h" +#include +#include "core/utils/StringBuilder.h" + + +void StochasticTrack::writeRouted(Routing::Target target, int intValue, float floatValue) { + switch (target) { + case Routing::Target::SlideTime: + setSlideTime(intValue, true); + break; + case Routing::Target::Octave: + setOctave(intValue, true); + break; + case Routing::Target::Transpose: + setTranspose(intValue, true); + break; + case Routing::Target::Rotate: + setRotate(intValue, true); + break; + default: + break; + } +} + +void StochasticTrack::clear() { + setPlayMode(Types::PlayMode::Aligned); + setSlideTime(50); + setOctave(0); + setTranspose(0); + setRotate(0); + + for (auto &sequence : _sequences) { + sequence.clear(); + } +} + +void StochasticTrack::write(VersionedSerializedWriter &writer) const { + writer.write(_name, NameLength + 1); + writer.write(_playMode); + writer.write(_slideTime.base); + writer.write(_octave.base); + writer.write(_transpose.base); + writer.write(_rotate.base); + writeArray(writer, _sequences); +} + +void StochasticTrack::read(VersionedSerializedReader &reader) { + reader.backupHash(); + + reader.read(_name, NameLength + 1, ProjectVersion::Version33); + + reader.read(_playMode); + reader.read(_slideTime.base); + reader.read(_octave.base); + reader.read(_transpose.base); + reader.read(_rotate.base); + + // There is a bug in previous firmware versions where writing the properties + // of a note track did not update the hash value. + if (reader.dataVersion() < ProjectVersion::Version23) { + reader.restoreHash(); + } + + readArray(reader, _sequences); +} diff --git a/src/apps/sequencer/model/StochasticTrack.h b/src/apps/sequencer/model/StochasticTrack.h new file mode 100644 index 00000000..754541e9 --- /dev/null +++ b/src/apps/sequencer/model/StochasticTrack.h @@ -0,0 +1,325 @@ +#pragma once + +#include "Config.h" +#include "StochasticSequence.h" +#include "Types.h" +#include "Serialize.h" +#include "Routing.h" +#include "FileDefs.h" +#include "core/utils/StringUtils.h" + +class StochasticTrack { +public: + //---------------------------------------- + // Types + //---------------------------------------- + static constexpr size_t NameLength = FileHeader::NameLength; + + typedef std::array StochasticSequenceArray; + + // FillMode + + enum class FillMode : uint8_t { + None, + Gates, + NextPattern, + Condition, + Last + }; + + static const char *fillModeName(FillMode fillMode) { + switch (fillMode) { + case FillMode::None: return "None"; + case FillMode::Gates: return "Gates"; + case FillMode::NextPattern: return "Next Pattern"; + case FillMode::Condition: return "Condition"; + case FillMode::Last: break; + } + return nullptr; + } + + // CvUpdateMode + + enum class CvUpdateMode : uint8_t { + Gate, + Always, + Last + }; + + static const char *cvUpdateModeName(CvUpdateMode mode) { + switch (mode) { + case CvUpdateMode::Gate: return "Gate"; + case CvUpdateMode::Always: return "Always"; + case CvUpdateMode::Last: break; + } + return nullptr; + } + + //---------------------------------------- + // Properties + //---------------------------------------- + + // trackName + const char *name() const { return _name; } + void setName(const char *name) { + StringUtils::copy(_name, name, sizeof(_name)); + } + + // playMode + + Types::PlayMode playMode() const { return _playMode; } + void setPlayMode(Types::PlayMode playMode) { + _playMode = ModelUtils::clampedEnum(playMode); + } + + void editPlayMode(int value, bool shift) { + setPlayMode(ModelUtils::adjustedEnum(playMode(), value)); + } + + void printPlayMode(StringBuilder &str) const { + str(Types::playModeName(playMode())); + } + + // fillMode + + FillMode fillMode() const { return _fillMode; } + void setFillMode(FillMode fillMode) { + _fillMode = ModelUtils::clampedEnum(fillMode); + } + + void editFillMode(int value, bool shift) { + setFillMode(ModelUtils::adjustedEnum(fillMode(), value)); + } + + void printFillMode(StringBuilder &str) const { + str(fillModeName(fillMode())); + } + + // fillMuted + + bool fillMuted() const { return _fillMuted; } + void setFillMuted(bool fillMuted) { + _fillMuted = fillMuted; + } + + void editFillMuted(int value, bool shift) { + setFillMuted(value > 0); + } + + void printFillMuted(StringBuilder &str) const { + ModelUtils::printYesNo(str, fillMuted()); + } + + // cvUpdateMode + + CvUpdateMode cvUpdateMode() const { return _cvUpdateMode; } + void setCvUpdateMode(CvUpdateMode cvUpdateMode) { + _cvUpdateMode = ModelUtils::clampedEnum(cvUpdateMode); + } + + void editCvUpdateMode(int value, bool shift) { + setCvUpdateMode(ModelUtils::adjustedEnum(cvUpdateMode(), value)); + } + + void printCvUpdateMode(StringBuilder &str) const { + str(cvUpdateModeName(cvUpdateMode())); + } + + // slideTime + + int slideTime() const { return _slideTime.get(isRouted(Routing::Target::SlideTime)); } + void setSlideTime(int slideTime, bool routed = false) { + _slideTime.set(clamp(slideTime, 0, 100), routed); + } + + void editSlideTime(int value, bool shift) { + if (!isRouted(Routing::Target::SlideTime)) { + setSlideTime(ModelUtils::adjustedByStep(slideTime(), value, 5, !shift)); + } + } + + void printSlideTime(StringBuilder &str) const { + printRouted(str, Routing::Target::SlideTime); + str("%d%%", slideTime()); + } + + // octave + + int octave() const { return _octave.get(isRouted(Routing::Target::Octave)); } + void setOctave(int octave, bool routed = false) { + _octave.set(clamp(octave, -10, 10), routed); + } + + void editOctave(int value, bool shift) { + if (!isRouted(Routing::Target::Octave)) { + setOctave(octave() + value); + } + } + + void printOctave(StringBuilder &str) const { + printRouted(str, Routing::Target::Octave); + str("%+d", octave()); + } + + // transpose + + int transpose() const { return _transpose.get(isRouted(Routing::Target::Transpose)); } + void setTranspose(int transpose, bool routed = false) { + _transpose.set(clamp(transpose, -100, 100), routed); + } + + void editTranspose(int value, bool shift) { + if (!isRouted(Routing::Target::Transpose)) { + setTranspose(transpose() + value); + } + } + + void printTranspose(StringBuilder &str) const { + printRouted(str, Routing::Target::Transpose); + str("%+d", transpose()); + } + + // rotate + + int rotate() const { return _rotate.get(isRouted(Routing::Target::Rotate)); } + void setRotate(int rotate, bool routed = false) { + _rotate.set(clamp(rotate, -64, 64), routed); + } + + void editRotate(int value, bool shift) { + if (!isRouted(Routing::Target::Rotate)) { + setRotate(rotate() + value); + } + } + + void printRotate(StringBuilder &str) const { + printRouted(str, Routing::Target::Rotate); + str("%+d", rotate()); + } + + // gateProbabilityBias + + int gateProbabilityBias() const { return _gateProbabilityBias.get(isRouted(Routing::Target::GateProbabilityBias)); } + void setGateProbabilityBias(int gateProbabilityBias, bool routed = false) { + _gateProbabilityBias.set(clamp(gateProbabilityBias, -StochasticSequence::GateProbability::Range, StochasticSequence::GateProbability::Range), routed); + } + + void editGateProbabilityBias(int value, bool shift) { + if (!isRouted(Routing::Target::GateProbabilityBias)) { + setGateProbabilityBias(gateProbabilityBias() + value); + } + } + + void printGateProbabilityBias(StringBuilder &str) const { + printRouted(str, Routing::Target::GateProbabilityBias); + str("%+.1f%%", gateProbabilityBias() * 12.5f); + } + + // retriggerProbabilityBias + + int retriggerProbabilityBias() const { return _retriggerProbabilityBias.get(isRouted(Routing::Target::RetriggerProbabilityBias)); } + void setRetriggerProbabilityBias(int retriggerProbabilityBias, bool routed = false) { + _retriggerProbabilityBias.set(clamp(retriggerProbabilityBias, -StochasticSequence::RetriggerProbability::Range, StochasticSequence::RetriggerProbability::Range), routed); + } + + void editRetriggerProbabilityBias(int value, bool shift) { + if (!isRouted(Routing::Target::RetriggerProbabilityBias)) { + setRetriggerProbabilityBias(retriggerProbabilityBias() + value); + } + } + + void printRetriggerProbabilityBias(StringBuilder &str) const { + printRouted(str, Routing::Target::RetriggerProbabilityBias); + str("%+.1f%%", retriggerProbabilityBias() * 12.5f); + } + + // lengthBias + + int lengthBias() const { return _lengthBias.get(isRouted(Routing::Target::LengthBias)); } + void setLengthBias(int lengthBias, bool routed = false) { + _lengthBias.set(clamp(lengthBias, -StochasticSequence::Length::Range, StochasticSequence::Length::Range), routed); + } + + void editLengthBias(int value, bool shift) { + if (!isRouted(Routing::Target::LengthBias)) { + setLengthBias(lengthBias() + value); + } + } + + void printLengthBias(StringBuilder &str) const { + printRouted(str, Routing::Target::LengthBias); + str("%+.1f%%", lengthBias() * 12.5f); + } + + // noteProbabilityBias + + int noteProbabilityBias() const { return _noteProbabilityBias.get(isRouted(Routing::Target::NoteProbabilityBias)); } + void setNoteProbabilityBias(int noteProbabilityBias, bool routed = false) { + _noteProbabilityBias.set(clamp(noteProbabilityBias, -StochasticSequence::NoteVariationProbability::Range, StochasticSequence::NoteVariationProbability::Range), routed); + } + + void editNoteProbabilityBias(int value, bool shift) { + if (!isRouted(Routing::Target::NoteProbabilityBias)) { + setNoteProbabilityBias(noteProbabilityBias() + value); + } + } + + void printNoteProbabilityBias(StringBuilder &str) const { + printRouted(str, Routing::Target::NoteProbabilityBias); + str("%+.1f%%", noteProbabilityBias() * 12.5f); + } + + // sequences + + const StochasticSequenceArray &sequences() const { return _sequences; } + StochasticSequenceArray &sequences() { return _sequences; } + + const StochasticSequence &sequence(int index) const { return _sequences[index]; } + StochasticSequence &sequence(int index) { return _sequences[index]; } + + //---------------------------------------- + // Routing + //---------------------------------------- + + inline bool isRouted(Routing::Target target) const { return Routing::isRouted(target, _trackIndex); } + inline void printRouted(StringBuilder &str, Routing::Target target) const { Routing::printRouted(str, target, _trackIndex); } + void writeRouted(Routing::Target target, int intValue, float floatValue); + + //---------------------------------------- + // Methods + //---------------------------------------- + + StochasticTrack() { clear(); } + + void clear(); + + void write(VersionedSerializedWriter &writer) const; + void read(VersionedSerializedReader &reader); + +private: + void setTrackIndex(int trackIndex) { + _trackIndex = trackIndex; + for (auto &sequence : _sequences) { + sequence.setTrackIndex(trackIndex); + } + } + + int8_t _trackIndex = -1; + char _name[NameLength + 1]; + Types::PlayMode _playMode; + FillMode _fillMode; + bool _fillMuted; + CvUpdateMode _cvUpdateMode; + Routable _slideTime; + Routable _octave; + Routable _transpose; + Routable _rotate; + Routable _gateProbabilityBias; + Routable _retriggerProbabilityBias; + Routable _lengthBias; + Routable _noteProbabilityBias; + + StochasticSequenceArray _sequences; + + friend class Track; +}; diff --git a/src/apps/sequencer/model/Track.cpp b/src/apps/sequencer/model/Track.cpp index 1734fd7d..3ce1a93c 100644 --- a/src/apps/sequencer/model/Track.cpp +++ b/src/apps/sequencer/model/Track.cpp @@ -16,6 +16,9 @@ void Track::clearPattern(int patternIndex) { case TrackMode::Curve: _track.curve->sequence(patternIndex).clear(); break; + case TrackMode::Stochastic: + _track.stochastic->sequence(patternIndex).clear(); + break; case TrackMode::MidiCv: break; case TrackMode::Last: @@ -31,6 +34,9 @@ void Track::copyPattern(int src, int dst) { case TrackMode::Curve: _track.curve->sequence(dst) = _track.curve->sequence(src); break; + case TrackMode::Stochastic: + _track.stochastic->sequence(dst) = _track.stochastic->sequence(src); + break; case TrackMode::MidiCv: break; case TrackMode::Last: @@ -50,6 +56,7 @@ void Track::gateOutputName(int index, StringBuilder &str) const { switch (_trackMode) { case TrackMode::Note: case TrackMode::Curve: + case TrackMode::Stochastic: str("Gate"); break; case TrackMode::MidiCv: @@ -64,6 +71,7 @@ void Track::cvOutputName(int index, StringBuilder &str) const { switch (_trackMode) { case TrackMode::Note: case TrackMode::Curve: + case TrackMode::Stochastic: str("CV"); break; case TrackMode::MidiCv: @@ -88,6 +96,9 @@ void Track::write(VersionedSerializedWriter &writer) const { case TrackMode::MidiCv: _track.midiCv->write(writer); break; + case TrackMode::Stochastic: + _track.stochastic->write(writer); + break; case TrackMode::Last: break; } @@ -109,6 +120,9 @@ void Track::read(VersionedSerializedReader &reader) { case TrackMode::MidiCv: _track.midiCv->read(reader); break; + case TrackMode::Stochastic: + _track.stochastic->read(reader); + break; case TrackMode::Last: break; } @@ -118,6 +132,7 @@ void Track::initContainer() { _track.note = nullptr; _track.curve = nullptr; _track.midiCv = nullptr; + _track.stochastic = nullptr; switch (_trackMode) { case TrackMode::Note: @@ -129,6 +144,8 @@ void Track::initContainer() { case TrackMode::MidiCv: _track.midiCv = _container.create(); break; + case TrackMode::Stochastic: + _track.stochastic = _container.create(); case TrackMode::Last: break; } @@ -152,6 +169,9 @@ void Track::setContainerTrackIndex(int trackIndex) { case TrackMode::MidiCv: _track.midiCv->setTrackIndex(trackIndex); break; + case TrackMode::Stochastic: + _track.stochastic->setTrackIndex(trackIndex); + break; case TrackMode::Last: break; } diff --git a/src/apps/sequencer/model/Track.h b/src/apps/sequencer/model/Track.h index bbf81a0a..40287127 100644 --- a/src/apps/sequencer/model/Track.h +++ b/src/apps/sequencer/model/Track.h @@ -7,6 +7,7 @@ #include "NoteTrack.h" #include "CurveTrack.h" #include "MidiCvTrack.h" +#include "StochasticTrack.h" #include "core/Debug.h" #include "core/math/Math.h" @@ -35,6 +36,7 @@ class Track { Note, Curve, MidiCv, + Stochastic, Last, Default = Note }; @@ -44,6 +46,7 @@ class Track { case TrackMode::Note: return "Note"; case TrackMode::Curve: return "Curve"; case TrackMode::MidiCv: return "MIDI/CV"; + case TrackMode::Stochastic: return "Stochastic"; case TrackMode::Last: break; } return nullptr; @@ -54,6 +57,7 @@ class Track { case TrackMode::Note: return 0; case TrackMode::Curve: return 1; case TrackMode::MidiCv: return 2; + case TrackMode::Stochastic: return 3; case TrackMode::Last: break; } return 0; @@ -116,6 +120,9 @@ class Track { const MidiCvTrack &midiCvTrack() const { SANITIZE_TRACK_MODE(_trackMode, TrackMode::MidiCv); return *_track.midiCv; } MidiCvTrack &midiCvTrack() { SANITIZE_TRACK_MODE(_trackMode, TrackMode::MidiCv); return *_track.midiCv; } + const StochasticTrack &stochasticTrack() const { SANITIZE_TRACK_MODE(_trackMode, TrackMode::Stochastic); return *_track.stochastic; } + StochasticTrack &stochasticTrack() { SANITIZE_TRACK_MODE(_trackMode, TrackMode::Stochastic); return *_track.stochastic; } + //---------------------------------------- // Methods //---------------------------------------- @@ -163,11 +170,12 @@ class Track { TrackMode _trackMode; int8_t _linkTrack; - Container _container; + Container _container; union { NoteTrack *note; CurveTrack *curve; MidiCvTrack *midiCv; + StochasticTrack *stochastic; } _track; friend class Project; diff --git a/src/apps/sequencer/python/project.cpp b/src/apps/sequencer/python/project.cpp index fcfb5633..ea53e5a6 100644 --- a/src/apps/sequencer/python/project.cpp +++ b/src/apps/sequencer/python/project.cpp @@ -256,6 +256,7 @@ void register_project(py::module &m) { .def_property_readonly("noteTrack", [] (Track &track) { return &track.noteTrack(); }) .def_property_readonly("curveTrack", [] (Track &track) { return &track.curveTrack(); }) .def_property_readonly("midiCvTrack", [] (Track &track) { return &track.midiCvTrack(); }) + .def_property_readonly("stochasticTrack", [] (Track &track) { return &track.stochasticTrack(); }) .def("clear", &Track::clear) .def("clearPattern", &Track::clearPattern, "patternIndex"_a) .def("copyPattern", &Track::copyPattern, "srcIndex"_a, "dstIndex"_a) @@ -265,6 +266,7 @@ void register_project(py::module &m) { .value("Note", Track::TrackMode::Note) .value("Curve", Track::TrackMode::Curve) .value("MidiCv", Track::TrackMode::MidiCv) + .value("Stochastic", Track::TrackMode::Stochastic) .export_values() ; @@ -309,6 +311,47 @@ void register_project(py::module &m) { .export_values() ; + // ------------------------------------------------------------------------ + // Stochastic Track + // ------------------------------------------------------------------------ + + py::class_ stochasticTrack(m, "StochasticTrack"); + stochasticTrack + .def_property("playMode", &StochasticTrack::playMode, &StochasticTrack::setPlayMode) + .def_property("fillMode", &StochasticTrack::fillMode, &StochasticTrack::setFillMode) + .def_property("cvUpdateMode", &StochasticTrack::cvUpdateMode, &StochasticTrack::setCvUpdateMode) + .def_property("slideTime", &StochasticTrack::slideTime, &StochasticTrack::setSlideTime) + .def_property("octave", &StochasticTrack::octave, &StochasticTrack::setOctave) + .def_property("transpose", &StochasticTrack::transpose, &StochasticTrack::setTranspose) + .def_property("rotate", &StochasticTrack::rotate, &StochasticTrack::setRotate) + .def_property("gateProbabilityBias", &StochasticTrack::gateProbabilityBias, &StochasticTrack::setGateProbabilityBias) + .def_property("retriggerProbabilityBias", &StochasticTrack::retriggerProbabilityBias, &StochasticTrack::setRetriggerProbabilityBias) + .def_property("lengthBias", &StochasticTrack::lengthBias, &StochasticTrack::setLengthBias) + .def_property("noteProbabilityBias", &StochasticTrack::noteProbabilityBias, &StochasticTrack::setNoteProbabilityBias) + .def_property_readonly("sequences", [] (StochasticTrack &StochasticTrack) { + py::list result; + for (int i = 0; i < CONFIG_PATTERN_COUNT; ++i) { + result.append(&StochasticTrack.sequence(i)); + } + return result; + }) + .def("clear", &StochasticTrack::clear) + ; + + py::enum_(stochasticTrack, "FillMode") + .value("None", StochasticTrack::FillMode::None) + .value("Gates", StochasticTrack::FillMode::Gates) + .value("NextPattern", StochasticTrack::FillMode::NextPattern) + .value("Condition", StochasticTrack::FillMode::Condition) + .export_values() + ; + + py::enum_(stochasticTrack, "CvUpdateMode") + .value("Gate", StochasticTrack::CvUpdateMode::Gate) + .value("Always", StochasticTrack::CvUpdateMode::Always) + .export_values() + ; + // ------------------------------------------------------------------------ // CurveTrack // ------------------------------------------------------------------------ @@ -323,6 +366,7 @@ void register_project(py::module &m) { .def_property("rotate", &CurveTrack::rotate, &CurveTrack::setRotate) .def_property("shapeProbabilityBias", &CurveTrack::shapeProbabilityBias, &CurveTrack::setShapeProbabilityBias) .def_property("gateProbabilityBias", &CurveTrack::gateProbabilityBias, &CurveTrack::setGateProbabilityBias) + .def_property("curveCvInput", &CurveTrack::curveCvInput, &CurveTrack::setCurveCvInput) .def_property_readonly("sequences", [] (CurveTrack &curveTrack) { py::list result; for (int i = 0; i < CONFIG_PATTERN_COUNT; ++i) { diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.cpp b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.cpp index 10fa332f..10dcf23b 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.cpp +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.cpp @@ -3,9 +3,11 @@ #include "LaunchpadDevice.h" #include "core/Debug.h" #include "os/os.h" +#include #include #include #include +#include #include #define CALL_MODE_FUNCTION(_mode_, _function_, ...) \ @@ -49,6 +51,11 @@ struct LayerMapItem { uint8_t col; }; +enum class PerforModeLayers { + SequenceLength, + Overview, + }; + static const LayerMapItem noteSequenceLayerMap[] = { [int(NoteSequence::Layer::Gate)] = { 0, 0 }, [int(NoteSequence::Layer::GateProbability)] = { 1, 0 }, @@ -82,8 +89,37 @@ static const LayerMapItem curveSequenceLayerMap[] = { [int(CurveSequence::Layer::GateProbability)] = { 1, 3 }, }; +static const LayerMapItem performLayerMap[] = { + [int(PerforModeLayers::SequenceLength)] = { 0, 0 }, + [int(PerforModeLayers::Overview)] = { 0, 1 }, +}; + +static constexpr int performLayerMapSize = sizeof(performLayerMap) / sizeof(performLayerMap[0]); + static constexpr int curveSequenceLayerMapSize = sizeof(curveSequenceLayerMap) / sizeof(curveSequenceLayerMap[0]); + +static const LayerMapItem stochasticSequenceLayerMap[] = { + [int(StochasticSequence::Layer::Gate)] = { 0, 0 }, + [int(StochasticSequence::Layer::GateProbability)] = { 1, 0 }, + [int(StochasticSequence::Layer::GateOffset)] = { 2, 0 }, + [int(StochasticSequence::Layer::Retrigger)] = { 0, 1 }, + [int(StochasticSequence::Layer::RetriggerProbability)] = { 1, 1 }, + [int(StochasticSequence::Layer::StageRepeats)] = { 2, 1 }, + [int(StochasticSequence::Layer::StageRepeatsMode)] = { 3, 1 }, + [int(StochasticSequence::Layer::Length)] = { 0, 2 }, + [int(StochasticSequence::Layer::LengthVariationRange)] = { 1, 2 }, + [int(StochasticSequence::Layer::LengthVariationProbability)] = { 2, 2 }, + [int(StochasticSequence::Layer::NoteVariationProbability)] = { 0, 3 }, + [int(StochasticSequence::Layer::NoteOctave)] = { 1, 3 }, + [int(StochasticSequence::Layer::NoteOctaveProbability)] = { 2, 3 }, + [int(StochasticSequence::Layer::Slide)] = { 3, 3 }, + [int(StochasticSequence::Layer::Condition)] = { 0, 4 }, +}; + + +static constexpr int stochasticSequenceLayerMapSize = sizeof(stochasticSequenceLayerMap) / sizeof(stochasticSequenceLayerMap[0]); + struct RangeMap { int16_t min[2]; int16_t max[2]; @@ -113,9 +149,9 @@ int _patternChangeDefault = 0; int _noteStyle = 0; static const std::map nativationRowMap = {{'\x03', 0}, {'\x02', 1}, {'\x01', 2}, {'\x00', 3}, {'\xff', 4}, {'\xfe', 5}, {'\xfd', 6}, {'\xfe', 7}}; -static const int noteGridValues[] = { 0,1,1,0,1,1,1,0, 1, 1, 1, 1, 1, 1, 1, 1}; +static const int noteGridValues[] = { 0,1,1,0,1,1,1,0, 1, 1, 1, 1, 1, 1, 1}; static const std::map semitones = {{1, 1}, {2, 3}, {4, 6}, {5,8}, {6, 10 }}; -static const std::map tones = {{0,0}, {1,2}, {2, 4}, {3, 5}, {4, 7}, {5, 9}, {6, 11}, {7, 12}}; +static const std::map tones = {{0,0}, {1,2}, {2, 4}, {3, 5}, {4, 7}, {5, 9}, {6, 11}}; static const std::map octaveMap = { {0, -4}, {1, -3}, {2, -2}, {3, -1}, {4, 0}, {5, 1}, {6, 2}, {7, 3}}; int selectedNote = -1; @@ -217,8 +253,7 @@ bool LaunchpadController::globalButton(const Button &button, ButtonAction action setMode(Mode::Pattern); break; case 2: - // TODO implement performer mode - // setMode(Mode::Performer); + setMode(Mode::Performer); break; case 6: _engine.togglePlay(); @@ -226,6 +261,62 @@ bool LaunchpadController::globalButton(const Button &button, ButtonAction action } return true; } + if (buttonState() && button.isGrid()) { + + if (_performSelectedLayer == 1) { + const auto &scale = Scale::get(0); + auto track = _project.track(button.row); + + int stepIndex = (_performNavigation.navigation.col*8)+button.col; + switch (track.trackMode()) { + case Track::TrackMode::Note: { + auto &sequence = track.noteTrack().sequence(_project.selectedPatternIndex()); + if (sequence.step(stepIndex).note()==(scale.notesPerOctave()*5)) { + sequence.step(stepIndex).setNote(0); + } else { + sequence.step(stepIndex).setNote(scale.notesPerOctave()*5); + } + } + break; + default: + break; + } + + return false; + } + + + if (_project.selectedTrack().trackMode() == Track::TrackMode::Note) { + const auto &scale = Scale::get(0); + + auto &sequence = _project.selectedNoteSequence(); + switch (_project.selectedNoteSequenceLayer()) { + case NoteSequence::Layer::Gate: + if (sequence.step(button.gridIndex()).note()==(scale.notesPerOctave()*5)) { + sequence.step(button.gridIndex()).setNote(0); + } else { + sequence.step(button.gridIndex()).setNote(scale.notesPerOctave()*5); + } + break; + case NoteSequence::Layer::Note: { + if (_noteStyle == 1) { + if (button.gridIndex()>15) { + return true; + } + int stepIndex = button.gridIndex()+(sequence.section()*16); + if (sequence.step(stepIndex).note()==(scale.notesPerOctave()*5)) { + sequence.step(stepIndex).setNote(0); + } else { + sequence.step(stepIndex).setNote(scale.notesPerOctave()*5); + } + } + } + break; + default: + break; + } + } + } } return false; } @@ -235,6 +326,8 @@ bool LaunchpadController::globalButton(const Button &button, ButtonAction action //---------------------------------------- void LaunchpadController::sequenceEnter() { + selectedNote = 0; + selectedOctave = 0; } void LaunchpadController::sequenceExit() { @@ -269,6 +362,10 @@ void LaunchpadController::sequenceDraw() { mirrorButton(_style); sequenceDrawRunMode(); } else if (buttonState()) { + if (_project.selectedTrack().trackMode() == Track::TrackMode::Stochastic) { + stochasticDrawRestProbability(); + return; + } mirrorButton(_style); sequenceDrawFollowMode(); } else { @@ -306,6 +403,10 @@ void LaunchpadController::sequenceButton(const Button &button, ButtonAction acti } } else if (buttonState()) { if (button.isGrid()) { + if (_project.selectedTrack().trackMode() == Track::TrackMode::Stochastic) { + sequenceSetRests(button); + return; + } sequenceSetFollowMode(button.col); } else if (button.isScene()) { _project.playState().toggleSoloTrack(button.scene()); @@ -322,7 +423,7 @@ void LaunchpadController::sequenceButton(const Button &button, ButtonAction acti sequenceEditStep(button.row, button.col); } else { switch (_project.selectedTrack().trackMode()) { - case (Track::TrackMode::Note): { + case (Track::TrackMode::Note):{ if (_project.selectedNoteSequenceLayer()==NoteSequence::Layer::Note) { manageCircuitKeyboard(button); } else { @@ -332,6 +433,15 @@ void LaunchpadController::sequenceButton(const Button &button, ButtonAction acti } case Track::TrackMode::Curve: sequenceEditStep(button.row, button.col); + break; + case (Track::TrackMode::Stochastic): { + if (_project.selectedStochasticSequenceLayer()==StochasticSequence::Layer::NoteVariationProbability) { + manageStochasticCircuitKeyboard(button); + } else { + sequenceEditStep(button.row, button.col); + } + } + break; default: break; @@ -362,8 +472,20 @@ void LaunchpadController::sequenceButton(const Button &button, ButtonAction acti !buttonState() && button.isGrid()) { // toggle gate - if (_project.selectedNoteSequenceLayer()!= NoteSequence::Layer::Note) { - sequenceToggleStep(button.row, button.col); + + switch (_project.selectedTrack().trackMode()) { + default: + break; + case Track::TrackMode::Stochastic: { + if (button.row >=3 && button.row <=4) { + if (button.col == 7) { + break; + } + auto &sequence = _project.selectedStochasticSequence(); + sequence.step(fullSelectedNote).toggleGate(); + } + break; + } } } } @@ -405,10 +527,11 @@ void LaunchpadController::manageCircuitKeyboard(const Button &button) { break; } else if (button.row >= 0 && button.row <= 2) { auto &sequence = _project.selectedNoteSequence(); + const auto &scale = sequence.selectedScale(_project.scale()); auto layer = _project.selectedNoteSequenceLayer(); int ofs = _sequence.navigation.col * 16; int linearIndex = button.col + ofs + (button.row*8); - if (isNoteKeyboardPressed()) { + if (isNoteKeyboardPressed(scale)) { auto step = sequence.step(linearIndex); if (step.bypassScale()) { @@ -452,6 +575,11 @@ void LaunchpadController::manageCircuitKeyboard(const Button &button) { default: break; } + } else if (button.row == 7) { + if (button.col <=3) { + Button btn = Button(3,button.col); + navigationButtonDown(_sequence.navigation, btn); + } } default: sequenceEditStep(button.row, button.col); @@ -460,9 +588,83 @@ void LaunchpadController::manageCircuitKeyboard(const Button &button) { } } -bool LaunchpadController::isNoteKeyboardPressed() { - const auto &sequence = _project.selectedNoteSequence(); - const auto &scale = Scale::get(0); +void LaunchpadController::manageStochasticCircuitKeyboard(const Button &button) { + auto &sequence = _project.selectedStochasticSequence(); + const auto &scale = sequence.selectedScale(_project.scale()); + const Scale &bypasssScale = Scale::get(0); + + switch ( _project.selectedStochasticSequenceLayer()) { + case StochasticSequence::Layer::NoteVariationProbability: + + if (button.row >=3 && button.row <= 4) { + + auto &stochasticTrack = _project.selectedTrack().stochasticTrack(); + auto currentOctave = stochasticTrack.octave(); + + if (button.row == 3 && button.col == 7) { + stochasticTrack.setOctave(currentOctave+1); + return; + } + if (button.row == 4 && button.col == 7) { + stochasticTrack.setOctave(currentOctave-1); + return; + } + + int ft = -1; + if (button.row == 3) { + ft = getMapValue(semitones, button.col); + } else if (button.row == 4) { + ft = getMapValue(tones, button.col); + } + if (scale.isNotePresent(ft)) { + int noteIndex = scale.getNoteIndex(ft); + selectedNote = noteIndex + (scale.notesPerOctave()*selectedOctave); + if (button.col == 7) { + selectedNote = selectedNote + scale.notesPerOctave(); + + } + fullNoteSelected = false; + } else { + fullNoteSelected = true; + + } + int noteIndex = bypasssScale.getNoteIndex(ft); + fullSelectedNote = noteIndex + (bypasssScale.notesPerOctave()*selectedOctave); + + break; + } else if (button.row >= 0 && button.row <= 2) { + auto &sequence = _project.selectedStochasticSequence(); + int linearIndex = button.col + (button.row*8); + + sequence.step(fullSelectedNote).setNoteVariationProbability(linearIndex); + + break; + } else if (button.row == 6) { + switch (button.col) { + case 0: + sequence.setUseLoop(); + break; + case 1: + sequence.setClearLoop(true); + break; + case 2: + if (!sequence.useLoop() && !sequence.isEmpty()) { + sequence.setReseed(1, false); + } + break; + default: + break; + } + break; + } + default: + sequenceEditStep(button.row, button.col); + break; + break; + } +} + +bool LaunchpadController::isNoteKeyboardPressed(const Scale &scale) { for (int col = 0; col <= 7; ++col) { if (buttonState(3, col)) { if (semitones.find(col) != semitones.end()) { @@ -510,13 +712,24 @@ void LaunchpadController::sequenceUpdateNavigation() { _sequence.navigation.bottom = (range.min - 7) / 8; break; } + case Track::TrackMode::Stochastic: { + auto layer = _project.selectedStochasticSequenceLayer(); + _sequence.navigation.left = 0; + _sequence.navigation.right = layer == StochasticSequence::Layer::Gate || layer == StochasticSequence::Layer::Slide || layer == StochasticSequence::Layer::NoteVariationProbability ? 0 : 1; + + auto range = StochasticSequence::layerRange(_project.selectedStochasticSequenceLayer()); + _sequence.navigation.top = layer == StochasticSequence::Layer::NoteVariationProbability ? 0 : range.max / 8; + _sequence.navigation.bottom = (range.min - 7) / 8; + + break; + } default: break; } } void LaunchpadController::sequenceSetLayer(int row, int col) { - switch (_project.selectedTrack().trackMode()) { + switch (_project.selectedTrack().trackMode()) { case Track::TrackMode::Note: { for (int i = 0; i < noteSequenceLayerMapSize; ++i) { const auto &item = noteSequenceLayerMap[i]; @@ -537,12 +750,22 @@ void LaunchpadController::sequenceSetLayer(int row, int col) { } break; } + case Track::TrackMode::Stochastic: + for (int i = 0; i < stochasticSequenceLayerMapSize; ++i) { + const auto &item = stochasticSequenceLayerMap[i]; + if (row == item.row && col == item.col) { + _project.setSelectedStochasticSequenceLayer(StochasticSequence::Layer(i)); + break; + } + } + break; default: break; } } void LaunchpadController::sequenceSetFirstStep(int step) { + _startingFirstStep[_project.selectedTrackIndex()] = step; switch (_project.selectedTrack().trackMode()) { case Track::TrackMode::Note: _project.selectedNoteSequence().setFirstStep(step); @@ -550,12 +773,15 @@ void LaunchpadController::sequenceSetFirstStep(int step) { case Track::TrackMode::Curve: _project.selectedCurveSequence().setFirstStep(step); break; + case Track::TrackMode::Stochastic: + _project.selectedStochasticSequence().setSequenceFirstStep(step); default: break; } } void LaunchpadController::sequenceSetLastStep(int step) { + _startingLastStep[_project.selectedTrackIndex()] = step; switch (_project.selectedTrack().trackMode()) { case Track::TrackMode::Note: _project.selectedNoteSequence().setLastStep(step); @@ -563,6 +789,8 @@ void LaunchpadController::sequenceSetLastStep(int step) { case Track::TrackMode::Curve: _project.selectedCurveSequence().setLastStep(step); break; + case Track::TrackMode::Stochastic: + _project.selectedStochasticSequence().setSequenceLastStep(step); default: break; } @@ -576,11 +804,48 @@ void LaunchpadController::sequenceSetRunMode(int mode) { case Track::TrackMode::Curve: _project.selectedCurveSequence().setRunMode(Types::RunMode(mode)); break; + case Track::TrackMode::Stochastic: + _project.selectedStochasticSequence().setRunMode(Types::RunMode(mode)); + break; default: break; } } +void LaunchpadController::sequenceSetRests(Button button) { + + int val = 0; + + if (button.row % 2 == 0) { + if (button.col == 0) { + val = 0; + } else { + val = (button.col /2)+1; + } + } else { + val = (button.col/2) + 5; + } + + if (button.row == 2) { + _project.selectedStochasticSequence().setRestProbability2(val); + } + if (button.row == 3) { + _project.selectedStochasticSequence().setRestProbability2(val); + } + if (button.row == 4) { + _project.selectedStochasticSequence().setRestProbability4(val); + } + if (button.row == 5) { + _project.selectedStochasticSequence().setRestProbability4(val); + } + if (button.row == 6) { + _project.selectedStochasticSequence().setRestProbability8(val); + } + if (button.row == 7) { + _project.selectedStochasticSequence().setRestProbability8(val); + } +} + void LaunchpadController::sequenceSetFollowMode(int col) { switch (_project.selectedTrack().trackMode()) { case Track::TrackMode::Note: @@ -628,6 +893,9 @@ void LaunchpadController::sequenceEditStep(int row, int col) { case Track::TrackMode::Curve: sequenceEditCurveStep(row, col); break; + case Track::Track::TrackMode::Stochastic: + sequenceEditStochasticStep(row, col); + break; default: break; } @@ -673,8 +941,37 @@ void LaunchpadController::sequenceEditCurveStep(int row, int col) { sequence.step(linearIndex).setLayerValue(layer, value); } +void LaunchpadController::sequenceEditStochasticStep(int row, int col) { + auto &sequence = _project.selectedStochasticSequence(); + auto layer = _project.selectedStochasticSequenceLayer(); + + int gridIndex = row * 8 + col; + + int linearIndex = col + _sequence.navigation.col * 8; + int value = (7 - row) + _sequence.navigation.row * 8; + + switch (layer) { + case StochasticSequence::Layer::Gate: + if (gridIndex>11) { + return; + } + sequence.step(gridIndex).toggleGate(); + break; + case StochasticSequence::Layer::Slide: + if (gridIndex>11) { + return; + } + sequence.step(gridIndex).toggleSlide(); + break; + default: + sequence.step(linearIndex).setLayerValue(layer, value); + break; + } +} + void LaunchpadController::sequenceDrawLayer() { switch (_project.selectedTrack().trackMode()) { + case Track::TrackMode::Note: for (int i = 0; i < noteSequenceLayerMapSize; ++i) { const auto &item = noteSequenceLayerMap[i]; @@ -694,6 +991,17 @@ void LaunchpadController::sequenceDrawLayer() { setGridLed(item.row, item.col, selected ? colorYellow() : colorGreen()); } break; + case Track::TrackMode::Stochastic: + for (int i = 0; i < stochasticSequenceLayerMapSize; ++i) { + const auto &item = stochasticSequenceLayerMap[i]; + bool selected = i == int(_project.selectedStochasticSequenceLayer()); + auto playMode = _engine.selectedTrackEngine().as().playMode(); + if (playMode == Types::PlayMode::Aligned && (i == 5 || i == 6)) { + continue; + } + setGridLed(item.row, item.col, selected ? colorYellow() : colorGreen()); + } + break; default: break; } @@ -711,11 +1019,25 @@ void LaunchpadController::sequenceDrawStepRange(int highlight) { drawRange(sequence.firstStep(), sequence.lastStep(), highlight == 0 ? sequence.firstStep() : sequence.lastStep()); break; } + case Track::TrackMode::Stochastic: { + const auto &sequence = _project.selectedStochasticSequence(); + drawRange(sequence.sequenceFirstStep(), sequence.sequenceLastStep(), highlight == 0 ? sequence.sequenceFirstStep() : sequence.sequenceLastStep()); + break; + } default: break; } } +void LaunchpadController::stochasticDrawRestProbability() { + const auto &sequence = _project.selectedStochasticSequence(); + drawBar(0, (sequence.restProbability()*2)-1); + drawBar(2, (sequence.restProbability2()*2)-1); + drawBar(4, (sequence.restProbability4()*2-1)); + drawBar(6, (sequence.restProbability8()*2)-1); + +} + void LaunchpadController::sequenceDrawRunMode() { switch (_project.selectedTrack().trackMode()) { case Track::TrackMode::Note: { @@ -726,6 +1048,10 @@ void LaunchpadController::sequenceDrawRunMode() { drawEnum(_project.selectedCurveSequence().runMode()); break; } + case Track::TrackMode::Stochastic: { + drawEnum(_project.selectedStochasticSequence().runMode()); + break; + } default: break; } @@ -754,6 +1080,9 @@ void LaunchpadController::sequenceDrawSequence() { case Track::TrackMode::Curve: sequenceDrawCurveSequence(); break; + case Track::TrackMode::Stochastic: + sequenceDrawStochasticSequence(); + break; default: break; } @@ -787,6 +1116,34 @@ void LaunchpadController::sequenceDrawNoteSequence() { } } +void LaunchpadController::sequenceDrawStochasticSequence() { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + + auto sequence = std::ref(_project.selectedStochasticSequence()); + if (_project.playState().songState().playing()) { + auto trackIndex = _project.selectedTrackIndex() ; + sequence = std::ref(_project.selectedTrack().stochasticTrack().sequence(_project.playState().trackState(trackIndex).pattern())); + } + auto layer = _project.selectedStochasticSequenceLayer(); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + + switch (layer) { + case StochasticSequence::Layer::Gate: + case StochasticSequence::Layer::Slide: + drawStochasticSequenceBits(sequence, layer, currentStep); + break; + case StochasticSequence::Layer::NoteVariationProbability: + drawStochasticSequenceNotes(sequence, layer, currentStep); + break; + case StochasticSequence::Layer::Condition: + drawStochasticSequenceDots(sequence, layer, currentStep); + break; + default: + drawStochasticSequenceBars(sequence, layer, currentStep); + break; + } +} + void LaunchpadController::sequenceDrawCurveSequence() { const auto &trackEngine = _engine.selectedTrackEngine().as(); @@ -862,6 +1219,10 @@ void LaunchpadController::patternDraw() { setGridLed(row, trackIndex, colorRed(1)); } break; + case Track::TrackMode::Stochastic: + if (track.stochasticTrack().sequence(patternIndex).isEdited()) { + setGridLed(row, trackIndex, colorRed(2)); + } default: break; } @@ -940,46 +1301,374 @@ void LaunchpadController::patternButton(const Button &button, ButtonAction actio //---------------------------------------- void LaunchpadController::performerEnter() { + + _performSelectedLayer = 0; + + for (int i = 0; i<8; ++i) { + switch (_project.track(i).trackMode()) { + case Track::TrackMode::Note: { + _startingFirstStep[i] = _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).firstStep(); + _startingLastStep[i] = _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).lastStep(); + } + break; + case Track::TrackMode::Curve: { + _startingFirstStep[i] = _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).firstStep(); + _startingLastStep[i] = _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).lastStep(); + } + break; + case Track::TrackMode::Stochastic: { + _startingFirstStep[i] = _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).sequenceFirstStep(); + _startingLastStep[i] = _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).sequenceLastStep(); + break; + } + default: + break; + } + } + } void LaunchpadController::performerExit() { + _performSelectedLayer = -1; } + void LaunchpadController::performerDraw() { -} -void LaunchpadController::performerButton(const Button &button, ButtonAction action) { -} + sequenceUpdateNavigation(); -//---------------------------------------- -// Navigation -//---------------------------------------- + mirrorButton(_style); -void LaunchpadController::navigationDraw(const Navigation &navigation) { - mirrorButton(_style); - mirrorButton(_style); - mirrorButton(_style); - mirrorButton(_style); + // selected track + if (buttonState()) { + drawTracksGateAndMute(_engine, _project.playState()); + return; + } else { + drawTracksGateAndSelected(_engine, _project.selectedTrackIndex()); + } - for (int row = navigation.bottom; row <= navigation.top; ++row) { - for (int col = navigation.left; col <= navigation.right; ++col) { - bool selected = row == navigation.row && col == navigation.col; - setGridLed(3 - row, col, selected ? colorYellow() : colorGreen()); + if (buttonState()) { + if (_performSelectedLayer==1) { + navigationDraw(_performNavigation.navigation); } - } -} + } else if (buttonState()) { + mirrorButton(_style); + performDrawLayer(); + } else if (buttonState()) { + mirrorButton(_style); + sequenceDrawStepRange(0); + } else if (buttonState()) { + mirrorButton(_style); + sequenceDrawStepRange(1); + } else if (buttonState()) { + mirrorButton(_style); + sequenceDrawRunMode(); + } else if (buttonState()) { + mirrorButton(_style); + sequenceDrawFollowMode(); + setGridLed(2, 0, _performFollowMode == 1 ? colorGreen() : colorYellow()); + } else { + mirrorButton(_style); + if (_performSelectedLayer == 0) { + if (_performButton.firstStepButton.row != -1) { + setGridLed(_performButton.firstStepButton.row, _performButton.firstStepButton.col, colorGreen()); + } + if (_performButton.lastStepButton.row != -1) { + setGridLed(_performButton.lastStepButton.row, _performButton.lastStepButton.col, colorGreen()); + } + } else { + const auto &scale = Scale::get(0); + int currentStep = -1; + for (int row = 0; row < 8; ++row) { + auto track = _project.track(row); + for (int col = 0; col < 8; ++col) { + int stepIndex = (_performNavigation.navigation.col*8)+col; + switch (track.trackMode()) { + case Track::TrackMode::Note: { + const auto &trackEngine = _engine.trackEngine(row).as(); + if (_performFollowMode || track.noteTrack().patternFollow() == Types::PatternFollow::DispAndLP || track.noteTrack().patternFollow() == Types::PatternFollow::LaunchPad) { + int stepOffset = (std::max(0, trackEngine.currentStep()) / 8) * 8; + stepIndex = stepOffset + col; + } + auto sequence = track.noteTrack().sequence(_project.selectedPatternIndex()); + currentStep = trackEngine.currentStep(); + Color color = colorOff(); + if (sequence.step(stepIndex).gate()) { + color = colorGreen(2); + } + if (sequence.step(stepIndex).gate() && sequence.step(stepIndex).note()== (scale.notesPerOctave()*5)) { + color = colorGreen(); + } + if (currentStep == stepIndex) { + color = colorRed(); + } + setGridLed(row, col, color); + } + break; + case Track::TrackMode::Stochastic: { + const auto &trackEngine = _engine.trackEngine(row).as(); + auto sequence = track.stochasticTrack().sequence(_project.selectedPatternIndex()); + currentStep = trackEngine.currentStep(); + Color color = colorOff(); + if (sequence.step(stepIndex).gate()) { + color = colorGreen(); + } + if (currentStep == stepIndex) { + color = colorRed(); + } + setGridLed(row, col, color); + } + break; + default: + break; -void LaunchpadController::navigationButtonDown(Navigation &navigation, const Button &button) { - int col = button.is() ? -1 : button.is() ? 1 : 0; - navigation.col = clamp(navigation.col + col, int(navigation.left), int(navigation.right)); + } + } + } - int row = button.is() ? -1 : button.is() ? 1 : 0; - navigation.row = clamp(navigation.row + row, int(navigation.bottom), int(navigation.top)); + auto selectedTrack = _project.selectedTrack(); - if (button.isGrid()) { - int col = button.col; - int row = 3 - button.row; - if (col >= navigation.left && col <= navigation.right && row >= navigation.bottom && row <= navigation.top) { + currentStep = 0; + switch (selectedTrack.trackMode()) { + case Track::TrackMode::Note: { + auto engine = _engine.selectedTrackEngine().as(); + currentStep = engine.currentStep(); + } + break; + case Track::TrackMode::Curve: { + auto engine = _engine.selectedTrackEngine().as(); + currentStep = engine.currentStep(); + } + break; + case Track::TrackMode::Stochastic: { + auto engine = _engine.selectedTrackEngine().as(); + currentStep = engine.currentStep(); + } + break; + default: + break; + + } + int section = int((currentStep) / 8); + for (int i = 0; i < 8; ++i) { + setFunctionLed(section, colorYellow()); + } + } + } + + + +} + +void LaunchpadController::performerButton(const Button &button, ButtonAction action) { + + if (action == ButtonAction::Down) { + + if (buttonState()) { + if (button.isScene()) { + _project.playState().toggleMuteTrack(button.scene()); + } + } else if (buttonState()) { + performNavigationButtonDown(_performNavigation.navigation, button); + + } else if(buttonState()) { + if (button.isGrid()) { + performSetLayer(button.row, button.col); + } + } else if (buttonState()) { + if (button.isGrid()) { + sequenceSetFirstStep(button.gridIndex()); + + } + } else if (buttonState()) { + if (button.isGrid()) { + sequenceSetLastStep(button.gridIndex()); + } + } else if (buttonState()) { + if (button.isGrid()) { + sequenceSetRunMode(button.gridIndex()); + } + } else if (buttonState()) { + if (button.isGrid()) { + sequenceSetFollowMode(button.col); + if (button.col==0 && button.row == 2) { + _performFollowMode = !_performFollowMode; + } + } else if (button.isScene()) { + _project.playState().toggleSoloTrack(button.scene()); + } + } else if (buttonState()) { + if (button.isScene()) { + _project.playState().fillTrack(button.scene(), true); + } + } else if (button.isGrid()) { + + if (_performSelectedLayer == 0) { + for (int row = 0; row < 8; ++row) { + for (int col = 0; col < 8; ++col) { + if (buttonState(row, col)) { + if (_performButton.firstStepButton.row != -1) { + _performButton.lastStepButton = button; + break; + } else { + _performButton.firstStepButton = button; + break; + } + } + } + } + int fs = (_performButton.firstStepButton.row * 8) + _performButton.firstStepButton.col; + int ls = (_performButton.lastStepButton.row * 8) + _performButton.lastStepButton.col; + if (ls <0) { + ls = fs; + } + + for (int i = 0; i < 8; ++i) { + if (_project.track(i).trackMode() == Track::TrackMode::Note) { + _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).setFirstStep(fs); + _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).setLastStep(ls); + } else if (_project.track(i).trackMode() == Track::TrackMode::Curve) { + _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).setFirstStep(fs); + _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).setLastStep(ls); + } else if (_project.track(i).trackMode() == Track::TrackMode::Stochastic) { + _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).setSequenceFirstStep(fs); + _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).setSequenceLastStep(ls); + } + } + } else if (_performSelectedLayer == 1) { + auto track = _project.track(button.row); + int stepIndex = (_performNavigation.navigation.col*8)+button.col; + + switch (track.trackMode()) { + case Track::TrackMode::Note: { + const auto &trackEngine = _engine.trackEngine(button.row).as(); + if (_performFollowMode) { + int stepOffset = (std::max(0, trackEngine.currentStep()) / 8) * 8; + stepIndex = stepOffset + button.col; + } + track.noteTrack().sequence(_project.selectedPatternIndex()).step(stepIndex).toggleGate(); + } + break; + case Track::TrackMode::Stochastic: { + if (stepIndex<12) { + track.stochasticTrack().sequence(_project.selectedPatternIndex()).step(stepIndex).toggleGate(); + } + } + default: + break; + + } + + + } + } else if (button.isScene()) { + _project.setSelectedTrackIndex(button.scene()); + } + } else if (action == ButtonAction::Up) { + if (button.isGrid() && (!buttonState() && !buttonState())) { + if (_performButton.firstStepButton.row != -1 && !buttonState(_performButton.firstStepButton.row, _performButton.firstStepButton.col )) { + _performButton.firstStepButton = Button(-1,-1); + } + if (_performButton.lastStepButton.row != - 1 && !buttonState(_performButton.lastStepButton.row, _performButton.lastStepButton.col)) { + _performButton.lastStepButton = Button(-1,-1); + } + + for (int i = 0; i < 8; ++i) { + if (_performButton.firstStepButton.row == -1 && _performButton.lastStepButton.row == -1) { + if (_project.track(i).trackMode() == Track::TrackMode::Note) { + _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).setFirstStep(_startingFirstStep[i]); + _project.track(i).noteTrack().sequence(_project.selectedPatternIndex()).setLastStep(_startingLastStep[i]); + } else if (_project.track(i).trackMode() == Track::TrackMode::Curve) { + _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).setFirstStep(_startingFirstStep[i]); + _project.track(i).curveTrack().sequence(_project.selectedPatternIndex()).setLastStep(_startingLastStep[i]); + } else if (_project.track(i).trackMode() == Track::TrackMode::Stochastic) { + _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).setSequenceFirstStep(_startingFirstStep[i]); + _project.track(i).stochasticTrack().sequence(_project.selectedPatternIndex()).setSequenceLastStep(_startingLastStep[i]); + } + + } + } + } + } + +} + +void LaunchpadController::performDrawLayer() { + for (int i = 0; i < performLayerMapSize; ++i) { + const auto &item = performLayerMap[i]; + bool selected = i == _performSelectedLayer; + setGridLed(item.row, item.col, selected ? colorYellow() : colorGreen()); + } +} + +void LaunchpadController::performSetLayer(int row, int col) { + for (int i = 0; i < performLayerMapSize; ++i) { + const auto &item = performLayerMap[i]; + if (row == item.row && col == item.col) { + _performSelectedLayer = i; + break; + } + } +} + +//---------------------------------------- +// Navigation +//---------------------------------------- + +void LaunchpadController::navigationDraw(const Navigation &navigation) { + mirrorButton(_style); + mirrorButton(_style); + mirrorButton(_style); + mirrorButton(_style); + + for (int row = navigation.bottom; row <= navigation.top; ++row) { + for (int col = navigation.left; col <= navigation.right; ++col) { + bool selected = row == navigation.row && col == navigation.col; + setGridLed(3 - row, col, selected ? colorYellow() : colorGreen()); + } + } +} + +void LaunchpadController::navigationButtonDown(Navigation &navigation, const Button &button) { + int col = button.is() ? -1 : button.is() ? 1 : 0; + navigation.col = clamp(navigation.col + col, int(navigation.left), int(navigation.right)); + + int row = button.is() ? -1 : button.is() ? 1 : 0; + navigation.row = clamp(navigation.row + row, int(navigation.bottom), int(navigation.top)); + + if (button.isGrid()) { + int col = button.col; + int row = 3 - button.row; + if (col >= navigation.left && col <= navigation.right && row >= navigation.bottom && row <= navigation.top) { + navigation.col = col; + navigation.row = row; + switch (_project.selectedTrack().trackMode()) { + case Track::TrackMode::Note: { + auto &sequence = _project.selectedNoteSequence(); + if (_project.selectedNoteSequenceLayer()==NoteSequence::Layer::Note) { + sequence.setSecion(col); + } + } + break; + default: + break; + } + + } + } +} + +void LaunchpadController::performNavigationButtonDown(Navigation &navigation, const Button &button) { + int col = button.is() ? -1 : button.is() ? 1 : 0; + navigation.col = clamp(navigation.col + col, int(navigation.left), int(navigation.right)); + + int row = button.is() ? -1 : button.is() ? 1 : 0; + navigation.row = clamp(navigation.row + row, int(navigation.bottom), int(navigation.top)); + + if (button.isGrid()) { + int col = button.col; + int row = 3 - button.row; + if (col >= navigation.left && col <= navigation.right && row >= navigation.bottom && row <= navigation.top) { navigation.col = col; navigation.row = row; } @@ -1037,6 +1726,8 @@ LaunchpadController::Color LaunchpadController::stepColor(bool active, bool curr } void LaunchpadController::drawNoteSequenceBits(const NoteSequence &sequence, NoteSequence::Layer layer, int currentStep) { + const auto &scale = Scale::get(0); + for (int row = 0; row < 8; ++row) { for (int col = 0; col < 8; ++col) { int stepIndex = row * 8 + col; @@ -1047,6 +1738,9 @@ void LaunchpadController::drawNoteSequenceBits(const NoteSequence &sequence, Not color = colorYellow(); } if (step.layerValue(layer) != 0) { + color = colorGreen(2); + } + if (step.gate() && step.note()== (scale.notesPerOctave()*5)) { color = colorGreen(); } if (stepIndex == currentStep) { @@ -1080,6 +1774,19 @@ void LaunchpadController::drawNoteSequenceDots(const NoteSequence &sequence, Not } } +void LaunchpadController::drawStochasticSequenceDots(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep) { + int ofs = _sequence.navigation.row * 8; + for (int col = 0; col < 8; ++col) { + int stepIndex = col + _sequence.navigation.col * 8; + const auto &step = sequence.step(stepIndex); + if (stepIndex>11) { + break; + } + int value = step.layerValue(layer); + setGridLed((7 - value) + ofs, col, stepColor(true, stepIndex == currentStep)); + } +} + void LaunchpadController::drawNoteSequenceNotes(const NoteSequence &sequence, NoteSequence::Layer layer, int currentStep) { int ofs = _sequence.navigation.col * 16; @@ -1097,7 +1804,10 @@ void LaunchpadController::drawNoteSequenceNotes(const NoteSequence &sequence, No const auto &step = sequence.step(stepIndex); Color color = colorOff(); if (step.gate()) { - color = colorGreen(); + color = colorGreen(2); + } + if (step.gate() && step.note()==(bypassScale.notesPerOctave()*5)) { + color = colorGreen(3); } if (stepIndex == currentStep) { color = colorRed(); @@ -1171,6 +1881,11 @@ void LaunchpadController::drawNoteSequenceNotes(const NoteSequence &sequence, No } } + // draw pages + for (int col = 0; col < 4; ++col) { + setGridLed(7, col, sequence.section() == col ? colorYellow(): colorYellow(1)); + } + } else { // draw octave lines @@ -1222,6 +1937,34 @@ void LaunchpadController::drawRunningKeyboardCircuit(int row, int col, const Not } } +void LaunchpadController::drawRunningStochasticKeyboardCircuit(int row, int col, const StochasticSequence::Step &step, const Scale &scale, int rootNote) { + int noteOctave = step.note() / scale.notesPerOctave(); + int s = step.note() - (scale.notesPerOctave()*noteOctave); + if (row == 4 && col == 7) { + s = step.note(); + } + + for (auto const& x : semitones) + { + if (step.gate() && s == scale.getNoteIndex(x.second)) { + setGridLed(3, x.first, step.gate() && s == scale.getNoteIndex(x.second) ? colorRed() : colorGreen()); + break; + } + } + for (auto const& x : tones) + { + + if (step.gate() && s == scale.getNoteIndex(x.second)+noteOctave*scale.notesPerOctave()) { + if (step.note() == rootNote+(scale.notesPerOctave()*noteOctave) && noteOctave == selectedOctave +1) { + setGridLed(4, 7, colorRed()); + } else { + setGridLed(4, x.first, step.gate() && s == scale.getNoteIndex(x.second)+noteOctave*scale.notesPerOctave() ? colorRed() : colorGreen()); + } + break; + } + } +} + void LaunchpadController::drawCurveSequenceBars(const CurveSequence &sequence, CurveSequence::Layer layer, int currentStep) { for (int col = 0; col < 8; ++col) { int stepIndex = col + _sequence.navigation.col * 8; @@ -1248,6 +1991,220 @@ void LaunchpadController::drawCurveSequenceDots(const CurveSequence &sequence, C } } +void LaunchpadController::drawStochasticSequenceBits(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep) { + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 8; ++col) { + int stepIndex = row * 8 + col; + const auto &step = sequence.step(stepIndex); + if (stepIndex>11) { + break; + } + + Color color = colorOff(); + if (step.gate()) { + color = colorYellow(); + } + if (step.layerValue(layer) != 0) { + color = colorGreen(); + } + if (stepIndex == currentStep) { + color = colorRed(); + } + + setGridLed(row, col, color); + } + } +} + +void LaunchpadController::drawStochasticSequenceBars(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep) { + for (int col = 0; col < 8; ++col) { + int stepIndex = col + _sequence.navigation.col * 8; + int lastStep = sequence.lastStep(); + followModeAction(currentStep, lastStep); + const auto &step = sequence.step(stepIndex); + if (stepIndex>11) { + break; + } + drawBar(col, step.layerValue(layer), true, stepIndex == currentStep); + } +} + +void LaunchpadController::drawStochasticSequenceNotes(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep) { + + const auto &scale = sequence.selectedScale(_project.scale()); + const Scale &bypassScale = Scale::get(0); + int stepIndex = fullSelectedNote; + + + + const auto &step = sequence.step(stepIndex); + if (step.noteVariationProbability() > 7) { + drawBarH(0, step.noteVariationProbability(), true, false); + drawBarH(1, step.noteVariationProbability()-8, true, false); + } else { + drawBarH(0, step.noteVariationProbability(), true, false); + } + + + auto stochasticEngine = _engine.selectedTrackEngine().as(); + for (int col = 0; col < 8; ++col) { + if (col == stochasticEngine.currentIndex()%8) { + if (stochasticEngine.currentIndex()<=8) { + setCustomGridLed(2, col, Color(1,3)); + } else { + setCustomGridLed(2, col, Color(2,0)); + } + } else { + setGridLed(2, col, colorOff()); + } + } + + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + + // draw keyboard + for (int row = 3; row<=4; ++row) { + for (int col = 0; col < 8; col++) { + int index = (col+((row-3)*8)); + + if (noteGridValues[index]==1) { + int n = getMapValue(semitones, index); + if (row == 4) { + n = getMapValue(tones, col); + } + if (scale.isNotePresent(n)) { + n = scale.getNoteIndex(n); + if (col == 7) { + n = n + scale.notesPerOctave(); + } + int stepIndex = -1; + if (row == 3) { + stepIndex = getMapValue(semitones, col); + } else if (row == 4) { + stepIndex = getMapValue(tones, col); + } + const auto &step = sequence.step(stepIndex); + Color alternate = colorGreen(2); + if (step.gate()) { + alternate = colorYellow(1); + } + + Color color = (selectedNote - (scale.notesPerOctave()*selectedOctave))== n && !fullNoteSelected ? colorYellow() : alternate; + setGridLed(row, col, color); + } else { + int stepIndex = -1; + if (row == 3) { + stepIndex = getMapValue(semitones, col); + } else if (row == 4) { + stepIndex = getMapValue(tones, col); + } + const auto &step = sequence.step(stepIndex); + Color alternate = colorGreen(1); + if (step.gate()) { + alternate = colorYellow(1); + } + n = bypassScale.getNoteIndex(n); + Color color = (fullSelectedNote - (bypassScale.notesPerOctave()*selectedOctave))== n && fullNoteSelected ? colorYellow() : alternate; + setGridLed(row, col, color); + + } + } + if (_engine.state().running()) { + const auto &step = sequence.step(currentStep); + + if (step.bypassScale()) { + drawRunningStochasticKeyboardCircuit(row, col, step, bypassScale, rootNote); + } else { + drawRunningStochasticKeyboardCircuit(row, col, step, scale, rootNote); + } + } + + Color transposeUpColor = colorOff(); + switch (_project.selectedTrack().stochasticTrack().octave()) { + case 0: + transposeUpColor = colorOff(); + break; + case 1: + transposeUpColor = Color(0,1); + break; + case 2: + transposeUpColor = Color(0,2); + break; + case 3: + transposeUpColor = Color(0,3); + break; + case 4: + transposeUpColor = Color(1,0); + break; + case 5: + transposeUpColor = Color(1,1); + break; + case 6: + transposeUpColor = Color(1,2); + break; + case 7: + transposeUpColor = Color(1, 3); + break; + case 8: + transposeUpColor = Color(2, 0); + break; + case 9: + transposeUpColor = Color(2,1); + break; + case 10: + transposeUpColor = Color(2,2); + break; + } + + setCustomGridLed(3, 7, transposeUpColor); + + Color transposeDownColor = colorOff(); + switch (_project.selectedTrack().stochasticTrack().octave()) { + case 0: + transposeDownColor = colorOff(); + break; + case -1: + transposeDownColor = Color(0,1); + break; + case -2: + transposeDownColor = Color(0,2); + break; + case -3: + transposeDownColor = Color(0,3); + break; + case -4: + transposeDownColor = Color(1,0); + break; + case -5: + transposeDownColor = Color(1,1); + break; + case -6: + transposeDownColor = Color(1,2); + break; + case -7: + transposeDownColor = Color(1, 3); + break; + case -8: + transposeDownColor = Color(2, 0); + break; + case -9: + transposeDownColor = Color(2,1); + break; + case -10: + transposeDownColor = Color(2,2); + break; + } + + setCustomGridLed(4, 7, transposeDownColor); + } + } + + + // draw options + setGridLed(6, 0, sequence.useLoop() ? colorYellow(): colorYellow(1)); + setGridLed(6, 1, sequence.clearLoop() ? colorYellow(): colorYellow(1)); + setGridLed(6,2, !sequence.useLoop() && !sequence.isEmpty() && sequence.reseed() == 1 ? colorYellow(): colorYellow(1)); +} + void LaunchpadController::followModeAction(int currentStep, int lastStep) { if (_engine.state().running()) { bool followMode = false; @@ -1299,6 +2256,13 @@ void LaunchpadController::drawBar(int col, int value, bool active, bool current) setGridLed((7 - value) + ofs, col, stepColor(active, current)); } +void LaunchpadController::drawBarH(int col, int value, bool active, bool current) { + for (int i = 0; i <= value; ++i) { + setGridLed(col, i, colorYellow()); + } + setGridLed(col, value, stepColor(active, current)); +} + //---------------------------------------- // Led handling //---------------------------------------- @@ -1309,6 +2273,12 @@ void LaunchpadController::setGridLed(int row, int col, Color color) { } } +void LaunchpadController::setCustomGridLed(int row, int col, Color color) { + if (row >= 0 && row < 8 && col >= 0 && col < 8) { + _device->setCustomLed(row, col, color, _style); + } +} + void LaunchpadController::setGridLed(int index, Color color) { if (index >= 0 && index < 64) { _device->setLed(index / 8, index % 8, color, _style); } diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.h index 9b82341a..2d1c7d32 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadController.h @@ -94,7 +94,7 @@ class LaunchpadController : public Controller { void sequenceDraw(); void sequenceButton(const Button &button, ButtonAction action); - bool isNoteKeyboardPressed(); + bool isNoteKeyboardPressed(const Scale &scale); void sequenceUpdateNavigation(); @@ -102,23 +102,30 @@ class LaunchpadController : public Controller { void sequenceSetFirstStep(int step); void sequenceSetLastStep(int step); void sequenceSetRunMode(int mode); + void sequenceSetRests(Button btton); void sequenceSetFollowMode(int col); void sequenceToggleStep(int row, int col); void sequenceToggleNoteStep(int row, int col); void sequenceEditStep(int row, int col); void sequenceEditNoteStep(int row, int col); void sequenceEditCurveStep(int row, int col); + void sequenceEditStochasticStep(int row, int col); + void sequenceDrawLayer(); void sequenceDrawStepRange(int highlight); + void stochasticDrawRestProbability(); void sequenceDrawRunMode(); void sequenceDrawFollowMode(); void sequenceDrawSequence(); void sequenceDrawNoteSequence(); void sequenceDrawCurveSequence(); + void sequenceDrawStochasticSequence(); void manageCircuitKeyboard(const Button &button); + void manageStochasticCircuitKeyboard(const Button &button); void drawRunningKeyboardCircuit(int row, int col, const NoteSequence::Step &step, const Scale &scale, int rootNote); + void drawRunningStochasticKeyboardCircuit(int row, int col, const StochasticSequence::Step &step, const Scale &scale, int rootNote); // Pattern mode void patternEnter(); @@ -131,10 +138,14 @@ class LaunchpadController : public Controller { void performerExit(); void performerDraw(); void performerButton(const Button &button, ButtonAction action); + void performDrawLayer(); + void performSetLayer(int row, int col); // Navigation void navigationDraw(const Navigation &navigation); void navigationButtonDown(Navigation &navigation, const Button &button); + void performNavigationButtonDown(Navigation &navigation, const Button &button); + // Drawing void drawTracksGateAndSelected(const Engine &engine, int selectedTrack); @@ -151,11 +162,45 @@ class LaunchpadController : public Controller { void drawNoteSequenceNotes(const NoteSequence &sequence, NoteSequence::Layer layer, int currentStep); void drawCurveSequenceBars(const CurveSequence &sequence, CurveSequence::Layer layer, int currentStep); void drawCurveSequenceDots(const CurveSequence &sequence, CurveSequence::Layer layer, int currentStep); + + void drawStochasticSequenceBits(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep); + void drawStochasticSequenceBars(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep); + void drawStochasticSequenceNotes(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep); + void drawStochasticSequenceDots(const StochasticSequence &sequence, StochasticSequence::Layer layer, int currentStep); + + + void drawBar(int row, int amount) { + for (int i = 0; i < 8; ++i) { + int p = amount; + if (i7) { + for (int i = 0; i < 8; ++i) { + int p = amount-8; + if (i _deviceContainer; @@ -204,4 +258,12 @@ class LaunchpadController : public Controller { struct { Navigation navigation = { 0, 0, 0, 0, -1, 0 }; } _pattern; + + struct { + Navigation navigation = { 0, 0, 0, 7,0,0}; + } _performNavigation; + + int _performSelectedLayer = 0; + + bool _performFollowMode = false; }; diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadDevice.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadDevice.h index fd3d2ab7..6cbdc9b6 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadDevice.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadDevice.h @@ -67,6 +67,10 @@ class LaunchpadDevice { _ledState[row * Cols + col] = color.data; } + virtual void setCustomLed(int row, int col, Color color, int style = 0) { + _ledState[row * Cols + col] = color.data; + } + virtual void setLed(int row, int col, int red, int green, int style = 0) { uint8_t state = (red & 0x3) | ((green & 0x3) << 4); _ledState[row * Cols + col] = state; diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk2Device.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk2Device.h index fe2e9312..9968af62 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk2Device.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk2Device.h @@ -19,6 +19,10 @@ class LaunchpadMk2Device : public LaunchpadDevice { _ledState[row * Cols + col] = mapColor(red, green, style);; } + void setCustomLed(int row, int col, Color color, int style = 0) override{ + _ledState[row * Cols + col] = mapCustomColor(color.red, color.green, style); + } + void syncLeds() override; private: @@ -45,4 +49,15 @@ class LaunchpadMk2Device : public LaunchpadDevice { return mapBlue[(red & 0x3) * 4 + (green & 0x3)]; } } + + inline uint8_t mapCustomColor(int x, int y, int style) const { + static const uint8_t map[] = { + // g0 g1 g2 g3 + 0, 1, 2, 3, // r0 + 32, 36, 40, 44, // r1 + 48, 52, 56, 60, // r2 + 5, 5, 9, 3, // r3 + }; + return map[(x & 0x3) * 4 + (y & 0x3)]; + } }; diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk3Device.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk3Device.h index 7f3fb55e..690fdeb6 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk3Device.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadMk3Device.h @@ -21,6 +21,10 @@ class LaunchpadMk3Device : public LaunchpadDevice { _ledState[row * Cols + col] = mapColor(red, green, style);; } + void setCustomLed(int row, int col, Color color, int style = 0) override{ + _ledState[row * Cols + col] = mapCustomColor(color.red, color.green, style); + } + void syncLeds() override; private: @@ -48,6 +52,17 @@ class LaunchpadMk3Device : public LaunchpadDevice { return mapBlue[(red & 0x3) * 4 + (green & 0x3)]; } } + + inline uint8_t mapCustomColor(int x, int y, int style) const { + static const uint8_t map[] = { + // g0 g1 g2 g3 + 0, 1, 2, 3, // r0 + 32, 36, 40, 44, // r1 + 48, 52, 56, 60, // r2 + 5, 5, 9, 3, // r3 + }; + return map[(x & 0x3) * 4 + (y & 0x3)]; + } }; diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProDevice.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProDevice.h index 3638345a..8057e529 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProDevice.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProDevice.h @@ -19,6 +19,10 @@ class LaunchpadProDevice : public LaunchpadDevice { _ledState[row * Cols + col] = mapColor(red, green, style);; } + void setCustomLed(int row, int col, Color color, int style = 0) override{ + _ledState[row * Cols + col] = mapCustomColor(color.red, color.green, style); + } + void syncLeds() override; private: @@ -47,4 +51,15 @@ class LaunchpadProDevice : public LaunchpadDevice { return mapBlue[(red & 0x3) * 4 + (green & 0x3)]; } } + + inline uint8_t mapCustomColor(int x, int y, int style) const { + static const uint8_t map[] = { + // g0 g1 g2 g3 + 0, 1, 2, 3, // r0 + 32, 36, 40, 44, // r1 + 48, 52, 56, 60, // r2 + 5, 5, 9, 3, // r3 + }; + return map[(x & 0x3) * 4 + (y & 0x3)]; + } }; diff --git a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProMk3Device.h b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProMk3Device.h index 5795f26e..ccf08b56 100644 --- a/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProMk3Device.h +++ b/src/apps/sequencer/ui/controllers/launchpad/LaunchpadProMk3Device.h @@ -21,6 +21,10 @@ class LaunchpadProMk3Device : public LaunchpadDevice { _ledState[row * Cols + col] = mapColor(red, green, style);; } + void setCustomLed(int row, int col, Color color, int style = 0) override{ + _ledState[row * Cols + col] = mapCustomColor(color.red, color.green, style); + } + void syncLeds() override; private: @@ -47,4 +51,15 @@ class LaunchpadProMk3Device : public LaunchpadDevice { return mapBlue[(red & 0x3) * 4 + (green & 0x3)]; } } + + inline uint8_t mapCustomColor(int x, int y, int style) const { + static const uint8_t map[] = { + // g0 g1 g2 g3 + 0, 1, 2, 3, // r0 + 32, 36, 40, 44, // r1 + 48, 52, 56, 60, // r2 + 5, 5, 9, 3, // r3 + }; + return map[(x & 0x3) * 4 + (y & 0x3)]; + } }; diff --git a/src/apps/sequencer/ui/model/CurveSequenceListModel.h b/src/apps/sequencer/ui/model/CurveSequenceListModel.h index b6b232f2..66282990 100644 --- a/src/apps/sequencer/ui/model/CurveSequenceListModel.h +++ b/src/apps/sequencer/ui/model/CurveSequenceListModel.h @@ -9,6 +9,7 @@ class CurveSequenceListModel : public RoutableListModel { public: enum Item { + Name, FirstStep, LastStep, RunMode, @@ -79,6 +80,7 @@ class CurveSequenceListModel : public RoutableListModel { private: static const char *itemName(Item item) { switch (item) { + case Name: return "Name"; case FirstStep: return "First Step"; case LastStep: return "Last Step"; case RunMode: return "Run Mode"; @@ -96,6 +98,9 @@ class CurveSequenceListModel : public RoutableListModel { void formatValue(Item item, StringBuilder &str) const { switch (item) { + case Name: + str(_sequence->name()); + break; case FirstStep: _sequence->printFirstStep(str); break; @@ -121,6 +126,8 @@ class CurveSequenceListModel : public RoutableListModel { void editValue(Item item, int value, bool shift) { switch (item) { + case Name: + break; case FirstStep: _sequence->editFirstStep(value, shift); break; @@ -146,6 +153,8 @@ class CurveSequenceListModel : public RoutableListModel { int indexedCountValue(Item item) const { switch (item) { + case Name: + break; case FirstStep: case LastStep: return 16; @@ -164,6 +173,8 @@ class CurveSequenceListModel : public RoutableListModel { int indexedValue(Item item) const { switch (item) { + case Name: + break; case FirstStep: return _sequence->firstStep(); case LastStep: @@ -184,6 +195,8 @@ class CurveSequenceListModel : public RoutableListModel { void setIndexedValue(Item item, int index) { switch (item) { + case Name: + break; case FirstStep: return _sequence->setFirstStep(index); case LastStep: diff --git a/src/apps/sequencer/ui/model/CurveTrackListModel.h b/src/apps/sequencer/ui/model/CurveTrackListModel.h index d6ba593f..5031ebfe 100644 --- a/src/apps/sequencer/ui/model/CurveTrackListModel.h +++ b/src/apps/sequencer/ui/model/CurveTrackListModel.h @@ -63,6 +63,7 @@ class CurveTrackListModel : public RoutableListModel { ShapeProbabilityBias, GateProbabilityBias, PatternFollow, + CurveCvInput, Last }; @@ -78,6 +79,7 @@ class CurveTrackListModel : public RoutableListModel { case ShapeProbabilityBias: return "Shape P. Bias"; case GateProbabilityBias: return "Gate P. Bias"; case PatternFollow: return "Pattern Follow"; + case CurveCvInput: return "Curve CV Input"; case Last: break; } return nullptr; @@ -119,6 +121,9 @@ class CurveTrackListModel : public RoutableListModel { case PatternFollow: _track->printPatternFollow(str); break; + case CurveCvInput: + _track->printCurveCvInput(str); + break; case Last: break; } @@ -155,6 +160,9 @@ class CurveTrackListModel : public RoutableListModel { case PatternFollow: _track->editPatternFollow(value, shift); break; + case CurveCvInput: + _track->editCurveCvInput(value, shift); + break; case Last: break; } diff --git a/src/apps/sequencer/ui/model/NoteSequenceListModel.h b/src/apps/sequencer/ui/model/NoteSequenceListModel.h index a4f69972..1eb99b85 100644 --- a/src/apps/sequencer/ui/model/NoteSequenceListModel.h +++ b/src/apps/sequencer/ui/model/NoteSequenceListModel.h @@ -11,6 +11,7 @@ class NoteSequenceListModel : public RoutableListModel { public: enum Item { + Name, FirstStep, LastStep, RunMode, @@ -106,6 +107,7 @@ class NoteSequenceListModel : public RoutableListModel { private: static const char *itemName(Item item) { switch (item) { + case Name: return "Name"; case FirstStep: return "First Step"; case LastStep: return "Last Step"; case RunMode: return "Run Mode"; @@ -124,6 +126,9 @@ class NoteSequenceListModel : public RoutableListModel { void formatValue(Item item, StringBuilder &str) const { switch (item) { + case Name: + str(_sequence->name()); + break; case FirstStep: _sequence->printFirstStep(str); break; @@ -160,6 +165,8 @@ class NoteSequenceListModel : public RoutableListModel { void editValue(Item item, int value, bool shift) { switch (item) { + case Name: + break; case FirstStep: _sequence->editFirstStep(value, shift); break; @@ -194,6 +201,8 @@ class NoteSequenceListModel : public RoutableListModel { int indexedCountValue(Item item) const { switch (item) { + case Name: + break; case FirstStep: case LastStep: return 16; @@ -214,6 +223,8 @@ class NoteSequenceListModel : public RoutableListModel { int indexedValue(Item item) const { switch (item) { + case Name: + break; case FirstStep: return _sequence->firstStep(); case LastStep: @@ -236,6 +247,8 @@ class NoteSequenceListModel : public RoutableListModel { void setIndexedValue(Item item, int index) { switch (item) { + case Name: + break; case FirstStep: return _sequence->setFirstStep(index); case LastStep: diff --git a/src/apps/sequencer/ui/model/ProjectListModel.h b/src/apps/sequencer/ui/model/ProjectListModel.h index 38d9ff86..39e16bd6 100644 --- a/src/apps/sequencer/ui/model/ProjectListModel.h +++ b/src/apps/sequencer/ui/model/ProjectListModel.h @@ -70,7 +70,8 @@ class ProjectListModel : public RoutableListModel { MidiInput, MidiPgmChange, CvGateInput, - CurveCvInput, + StepsToStop, + //CurveCvInput, Last }; @@ -88,7 +89,8 @@ class ProjectListModel : public RoutableListModel { case MidiInput: return "MIDI Input"; case MidiPgmChange: return "MIDI Pgm Chng"; case CvGateInput: return "CV/Gate Input"; - case CurveCvInput: return "Curve CV Input"; + case StepsToStop: return "Steps to stop"; + //case CurveCvInput: return "Curve CV Input"; case Last: break; } return nullptr; @@ -138,9 +140,12 @@ class ProjectListModel : public RoutableListModel { case CvGateInput: _project.printCvGateInput(str); break; - case CurveCvInput: - _project.printCurveCvInput(str); + case StepsToStop: + _project.printStepsToStop(str); break; + //case CurveCvInput: + // _project.printCurveCvInput(str); + // break; case Last: break; } @@ -183,9 +188,13 @@ class ProjectListModel : public RoutableListModel { case CvGateInput: _project.editCvGateInput(value, shift); break; - case CurveCvInput: - _project.editCurveCvInput(value, shift); + case StepsToStop: { + _project.editStepsToStop(value); break; + } + //case CurveCvInput: + // _project.editCurveCvInput(value, shift); + // break; case Last: break; } diff --git a/src/apps/sequencer/ui/model/StochasticSequenceListModel.h b/src/apps/sequencer/ui/model/StochasticSequenceListModel.h new file mode 100644 index 00000000..463e3913 --- /dev/null +++ b/src/apps/sequencer/ui/model/StochasticSequenceListModel.h @@ -0,0 +1,353 @@ +#pragma once + +#include "Config.h" + +#include "RoutableListModel.h" + +#include "model/StochasticSequence.h" +#include "model/Scale.h" + +class StochasticSequenceListModel : public RoutableListModel { +public: + enum Item { + RunMode, + Divisor, + ResetMeasure, + Scale, + RootNote, + RestProbability2, + RestProbability4, + RestProbability8, + SequenceFirstStep, + SequenceLastStep, + LowOctaveRange, + HighOctaveRange, + LengthModifier, + Last + }; + + StochasticSequenceListModel() + { + _scales[0] = -1; + for (int i = 1; i < 23; ++i) { + _scales[i] = i-1; + } + + for (int i = 0; i < 8; ++i) { + _selectedScale[i] = 0; + } + } + + void setSequence(StochasticSequence *sequence) { + _sequence = sequence; + if (sequence != nullptr) { + int trackIndex = _sequence->trackIndex(); + _selectedScale[trackIndex] = sequence->scale()+1; + } + } + + virtual int rows() const override { + return _sequence ? Last : 0; + } + + virtual int columns() const override { + return 2; + } + + virtual void cell(int row, int column, StringBuilder &str) const override { + if (column == 0) { + formatName(Item(row), str); + } else if (column == 1) { + formatValue(Item(row), str); + } + } + + virtual void edit(int row, int column, int value, bool shift) override { + if (column == 1) { + editValue(Item(row), value, shift); + } + } + + virtual int indexedCount(int row) const override { + return indexedCountValue(Item(row)); + } + + virtual int indexed(int row) const override { + return indexedValue(Item(row)); + } + + virtual void setIndexed(int row, int index) override { + if (index >= 0 && index < indexedCount(row)) { + setIndexedValue(Item(row), index); + } + } + + virtual Routing::Target routingTarget(int row) const override { + switch (Item(row)) { + case Divisor: + return Routing::Target::Divisor; + case RunMode: + return Routing::Target::RunMode; + case Scale: + return Routing::Target::Scale; + case RootNote: + return Routing::Target::RootNote; + case RestProbability2: + return Routing::Target::RestProbability2; + case RestProbability4: + return Routing::Target::RestProbability4; + case RestProbability8: + return Routing::Target::RestProbability8; + case SequenceFirstStep: + return Routing::Target::SequenceFirstStep; + case SequenceLastStep: + return Routing::Target::SequenceLastStep; + case LowOctaveRange: + return Routing::Target::LowOctaveRange; + case HighOctaveRange: + return Routing::Target::HighOctaveRange; + case LengthModifier: + return Routing::Target::LengthModifier; + default: + return Routing::Target::None; + } + } + + void setSelectedScale(int defaultScale, bool force= false) override { + if (_editScale || force) { + _sequence->editScale(_scales[_selectedScale[_sequence->trackIndex()]], false, defaultScale); + } + _editScale = !_editScale; + } + +private: + static const char *itemName(Item item) { + switch (item) { + case RunMode: return "Run Mode"; + case Divisor: return "Divisor"; + case ResetMeasure: return "Reset Measure"; + case Scale: return "Scale"; + case RootNote: return "Root Note"; + case RestProbability2: return "Rest Prob. 2"; + case RestProbability4: return "Rest Prob. 4"; + case RestProbability8: return "Rest Prob. 8"; + case SequenceFirstStep: return "Seq First Step"; + case SequenceLastStep: return "Seq Last Step"; + case LowOctaveRange: return "L Oct Range"; + case HighOctaveRange: return "H Oct Range"; + case LengthModifier: return "Length Mod"; + case Last: break; + } + return nullptr; + } + + void formatName(Item item, StringBuilder &str) const { + str(itemName(item)); + } + + void formatValue(Item item, StringBuilder &str) const { + switch (item) { + case RunMode: + _sequence->printRunMode(str); + break; + case Divisor: + _sequence->printDivisor(str); + break; + case ResetMeasure: + _sequence->printResetMeasure(str); + break; + case Scale:{ + int trackIndex = _sequence->trackIndex(); + bool isRouted = Routing::isRouted(Routing::Target::Scale, trackIndex); + if (isRouted) { + _sequence->printScale(str); + } else { + auto name = _scales[_selectedScale[trackIndex]] < 0 ? "Default" : Scale::name(_scales[_selectedScale[trackIndex]]); + str(name); + } + } + break; + case RootNote: + _sequence->printRootNote(str); + break; + case RestProbability2: + _sequence->printRestProbability2(str); + break; + case RestProbability4: + _sequence->printRestProbability4(str); + break; + case RestProbability8: + _sequence->printRestProbability8(str); + break; + case SequenceFirstStep: + _sequence->printSequenceFirstStep(str); + break; + case SequenceLastStep: + _sequence->printSequenceLastStep(str); + break; + case LowOctaveRange: + _sequence->printLowOctaveRange(str); + break; + case HighOctaveRange: + _sequence->printHighOctaveRange(str); + break; + case LengthModifier: + _sequence->printLengthModifier(str); + break; + case Last: + break; + } + } + + void editValue(Item item, int value, bool shift) { + switch (item) { + case RunMode: + _sequence->editRunMode(value, shift); + break; + case Divisor: + _sequence->editDivisor(value, shift); + break; + case ResetMeasure: + _sequence->editResetMeasure(value, shift); + break; + case Scale: { + int trackIndex = _sequence->trackIndex(); + bool isRouted = Routing::isRouted(Routing::Target::Scale, trackIndex); + if (!isRouted) { + int trackIndex = _sequence->trackIndex(); + _selectedScale[trackIndex] = clamp(_selectedScale[trackIndex] + value, 0, 23); + } + } + break; + case RootNote: + _sequence->editRootNote(value, shift); + break; + case RestProbability2: + _sequence->editRestProbability2(value, shift); + break; + case RestProbability4: + _sequence->editRestProbability4(value, shift); + break; + case RestProbability8: + _sequence->editRestProbability8(value, shift); + break; + case SequenceFirstStep: + _sequence->editSequenceFirstStep(value, shift); + break; + case SequenceLastStep: + _sequence->editSequenceLastStep(value, shift); + break; + case LowOctaveRange: + _sequence->editLowOctaveRange(value, shift); + break; + case HighOctaveRange: + _sequence->editHighOctaveRange(value, shift); + break; + case LengthModifier: + _sequence->editLengthModifier(value, shift); + break; + case Last: + break; + } + } + + int indexedCountValue(Item item) const { + switch (item) { + case SequenceFirstStep: + case SequenceLastStep: + return 16; + case RunMode: + return int(Types::RunMode::Last); + case Divisor: + case ResetMeasure: + return 16; + case Scale: + return Scale::Count + 1; + case RootNote: + return 12 + 1; + case RestProbability2: + case RestProbability4: + case RestProbability8: + case LowOctaveRange: + case HighOctaveRange: + case LengthModifier: + return 0; + case Last: + break; + } + return -1; + } + + int indexedValue(Item item) const { + switch (item) { + case RunMode: + return int(_sequence->runMode()); + case Divisor: + return _sequence->indexedDivisor(); + case ResetMeasure: + return _sequence->resetMeasure(); + case Scale: + return _sequence->indexedScale(); + case RootNote: + return _sequence->indexedRootNote(); + case RestProbability2: + return _sequence->restProbability2(); + case RestProbability4: + return _sequence->restProbability4(); + case RestProbability8: + return _sequence->restProbability8(); + case SequenceFirstStep: + return _sequence->sequenceFirstStep(); + case SequenceLastStep: + return _sequence->sequenceLastStep(); + case LowOctaveRange: + return _sequence->lowOctaveRange(); + case HighOctaveRange: + return _sequence->highOctaveRange(); + case LengthModifier: + return _sequence->lengthModifier(); + case Last: + break; + } + return -1; + } + + void setIndexedValue(Item item, int index) { + switch (item) { + case RunMode: + return _sequence->setRunMode(Types::RunMode(index)); + case Divisor: + return _sequence->setIndexedDivisor(index); + case ResetMeasure: + return _sequence->setResetMeasure(index); + case Scale: + return _sequence->setIndexedScale(index); + case RootNote: + return _sequence->setIndexedRootNote(index); + case RestProbability2: + return _sequence->setRestProbability2(index); + case RestProbability4: + return _sequence->setRestProbability4(index); + case RestProbability8: + return _sequence->setRestProbability8(index); + case SequenceFirstStep: + return _sequence->setSequenceFirstStep(index); + case SequenceLastStep: + return _sequence->setSequenceLastStep(index); + case LowOctaveRange: + return _sequence->setLowOctaveRange(index); + case HighOctaveRange: + return _sequence->setHighOctaveRange(index); + case LengthModifier: + return _sequence->setLengthModifier(index); + case Last: + break; + } + } + + StochasticSequence *_sequence; + private: + std::array _scales; + std::array _selectedScale; + bool _editScale = false; +}; diff --git a/src/apps/sequencer/ui/model/StochasticTrackListModel.h b/src/apps/sequencer/ui/model/StochasticTrackListModel.h new file mode 100644 index 00000000..394a4674 --- /dev/null +++ b/src/apps/sequencer/ui/model/StochasticTrackListModel.h @@ -0,0 +1,198 @@ +#pragma once + +#include "Config.h" + +#include "RoutableListModel.h" + +#include "model/StochasticTrack.h" + +class StochasticTrackListModel : public RoutableListModel { +public: + void setTrack(StochasticTrack &track) { + _track = &track; + } + + virtual int rows() const override { + return Last; + } + + virtual int columns() const override { + return 2; + } + + virtual void cell(int row, int column, StringBuilder &str) const override { + if (column == 0) { + formatName(Item(row), str); + } else if (column == 1) { + formatValue(Item(row), str); + } + } + + virtual void edit(int row, int column, int value, bool shift) override { + if (column == 1) { + editValue(Item(row), value, shift); + } + } + + virtual Routing::Target routingTarget(int row) const override { + switch (Item(row)) { + case SlideTime: + return Routing::Target::SlideTime; + case Octave: + return Routing::Target::Octave; + case Transpose: + return Routing::Target::Transpose; + case Rotate: + return Routing::Target::Rotate; + case GateProbabilityBias: + return Routing::Target::GateProbabilityBias; + case RetriggerProbabilityBias: + return Routing::Target::RetriggerProbabilityBias; + case LengthBias: + return Routing::Target::LengthBias; + case NoteProbabilityBias: + return Routing::Target::NoteProbabilityBias; + default: + return Routing::Target::None; + } + } + +private: + enum Item { + TrackName, + PlayMode, + //FillMode, + //FillMuted, + CvUpdateMode, + SlideTime, + Octave, + Transpose, + Rotate, + GateProbabilityBias, + RetriggerProbabilityBias, + LengthBias, + NoteProbabilityBias, + Last + }; + + static const char *itemName(Item item) { + switch (item) { + case TrackName: return "Name"; + case PlayMode: return "Play Mode"; + //case FillMode: return "Fill Mode"; + //case FillMuted: return "Fill Muted"; + case CvUpdateMode: return "CV Update Mode"; + case SlideTime: return "Slide Time"; + case Octave: return "Octave"; + case Transpose: return "Transpose"; + case Rotate: return "Rotate"; + case GateProbabilityBias: return "Gate P. Bias"; + case RetriggerProbabilityBias: return "Retrig P. Bias"; + case LengthBias: return "Length Bias"; + case NoteProbabilityBias: return "Note P. Bias"; + case Last: break; + } + return nullptr; + } + + void formatName(Item item, StringBuilder &str) const { + str(itemName(item)); + } + + void formatValue(Item item, StringBuilder &str) const { + switch (item) { + case TrackName: + str(_track->name()); + break; + case PlayMode: + _track->printPlayMode(str); + break; + /*case FillMode: + _track->printFillMode(str); + break; + case FillMuted: + _track->printFillMuted(str); + break;*/ + case CvUpdateMode: + _track->printCvUpdateMode(str); + break; + case SlideTime: + _track->printSlideTime(str); + break; + case Octave: + _track->printOctave(str); + break; + case Transpose: + _track->printTranspose(str); + break; + case Rotate: + _track->printRotate(str); + break; + case GateProbabilityBias: + _track->printGateProbabilityBias(str); + break; + case RetriggerProbabilityBias: + _track->printRetriggerProbabilityBias(str); + break; + case LengthBias: + _track->printLengthBias(str); + break; + case NoteProbabilityBias: + _track->printNoteProbabilityBias(str); + break; + case Last: + break; + } + } + + void editValue(Item item, int value, bool shift) { + switch (item) { + + case TrackName: + break; + case PlayMode: + _track->editPlayMode(value, shift); + break; + /*case FillMode: + _track->editFillMode(value, shift); + break; + case FillMuted: + _track->editFillMuted(value, shift); + break;*/ + case CvUpdateMode: + _track->editCvUpdateMode(value, shift); + break; + case SlideTime: + _track->editSlideTime(value, shift); + break; + case Octave: + _track->editOctave(value, shift); + break; + case Transpose: + _track->editTranspose(value, shift); + break; + case Rotate: + _track->editRotate(value, shift); + break; + case GateProbabilityBias: + _track->editGateProbabilityBias(value, shift); + break; + case RetriggerProbabilityBias: + _track->editRetriggerProbabilityBias(value, shift); + break; + case LengthBias: + _track->editLengthBias(value, shift); + break; + case NoteProbabilityBias: + _track->editNoteProbabilityBias(value, shift); + break; + case Last: + break; + } + } + + virtual void setSelectedScale(int defaultScale, bool force = false) override {}; + + + StochasticTrack *_track; +}; diff --git a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp index 0df44946..67a8d8cc 100644 --- a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp +++ b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp @@ -138,7 +138,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { WindowPainter::drawFooter(canvas, functionNames, pageKeyState(), activeFunctionKey()); auto &trackEngine = _engine.selectedTrackEngine().as(); - const auto &sequence = _project.selectedCurveSequence(); + auto &sequence = _project.selectedCurveSequence(); int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; bool isActiveSequence = trackEngine.isActiveSequence(sequence); @@ -160,8 +160,8 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { if (track.isPatternFollowDisplayOn() && _engine.state().running()) { bool section_change = bool((currentStep) % StepCount == 0); // StepCount is relative to screen int section_no = int((currentStep) / StepCount); - if (section_change && section_no != _section) { - _section = section_no; + if (section_change && section_no != sequence.section()) { + sequence.setSecion(section_no); } } @@ -314,9 +314,9 @@ void CurveSequenceEditPage::updateLeds(Leds &leds) { leds.set(MatrixMap::fromStep(i), red, green); } - LedPainter::drawSelectedSequenceSection(leds, _section); + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); - LedPainter::drawSelectedSequenceSection(leds, _section); + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); // show quick edit keys if (globalKeyState()[Key::Page] && !globalKeyState()[Key::Shift]) { @@ -367,6 +367,10 @@ void CurveSequenceEditPage::keyPress(KeyPressEvent &event) { return; } + if (key.pageModifier()) { + return; + } + if (key.isEncoder() && layer() == Layer::Shape && globalKeyState()[Key::Shift] && _stepSelection.count() > 1) { for (size_t stepIndex = 0; stepIndex < _stepSelection.size(); ++stepIndex) { if (_stepSelection[stepIndex]) { @@ -395,7 +399,7 @@ void CurveSequenceEditPage::keyPress(KeyPressEvent &event) { sequence.shiftSteps(_stepSelection.selected(), -1); } else { track.setPatternFollowDisplay(false); - _section = std::max(0, _section - 1); + sequence.setSecion(std::max(0, sequence.section() - 1)); } event.consume(); } @@ -404,7 +408,7 @@ void CurveSequenceEditPage::keyPress(KeyPressEvent &event) { sequence.shiftSteps(_stepSelection.selected(), 1); } else { track.setPatternFollowDisplay(false); - _section = std::min(3, _section + 1); + sequence.setSecion(std::min(3, sequence.section() + 1)); } event.consume(); } @@ -631,10 +635,10 @@ void CurveSequenceEditPage::drawDetail(Canvas &canvas, const CurveSequence::Step SequencePainter::drawProbability( canvas, 64 + 32 + 8, 32 - 4, 64 - 16, 8, - step.gateProbability() + 1, CurveSequence::GateProbability::Range + step.gateProbability(), CurveSequence::GateProbability::Range-1 ); str.reset(); - str("%.1f%%", 100.f * (step.gateProbability() + 1.f) / CurveSequence::GateProbability::Range); + str("%.1f%%", 100.f * (step.gateProbability()) / (CurveSequence::GateProbability::Range-1)); canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; diff --git a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.h b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.h index 814da1f8..f716c21b 100644 --- a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.h +++ b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.h @@ -29,7 +29,7 @@ class CurveSequenceEditPage : public BasePage { static const int StepCount = 16; - int stepOffset() const { return _section * StepCount; } + int stepOffset() const { return _project.selectedCurveSequence().section() * StepCount; } void switchLayer(int functionKey, bool shift); int activeFunctionKey(); @@ -55,7 +55,6 @@ class CurveSequenceEditPage : public BasePage { ContextMenu _contextMenu; - int _section = 0; bool _showDetail; uint32_t _showDetailTicks; diff --git a/src/apps/sequencer/ui/pages/CurveSequencePage.cpp b/src/apps/sequencer/ui/pages/CurveSequencePage.cpp index c466d897..276b8e62 100644 --- a/src/apps/sequencer/ui/pages/CurveSequencePage.cpp +++ b/src/apps/sequencer/ui/pages/CurveSequencePage.cpp @@ -16,6 +16,13 @@ enum class ContextAction { Last }; +enum class SaveContextAction { + Load, + Save, + SaveAs, + Last +}; + static const ContextMenuModel::Item contextMenuItems[] = { { "INIT" }, { "COPY" }, @@ -24,6 +31,12 @@ static const ContextMenuModel::Item contextMenuItems[] = { { "ROUTE" }, }; +static const ContextMenuModel::Item saveContextMenuItems[] = { + { "LOAD" }, + { "SAVE" }, + { "SAVE AS"}, +}; + CurveSequencePage::CurveSequencePage(PageManager &manager, PageContext &context) : ListPage(manager, context, _listModel) {} @@ -52,6 +65,12 @@ void CurveSequencePage::updateLeds(Leds &leds) { void CurveSequencePage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + if (key.shiftModifier() && event.count() == 2) { + saveContextShow(); + event.consume(); + return; + } + if (key.isContextMenu()) { contextShow(); event.consume(); @@ -69,6 +88,17 @@ void CurveSequencePage::keyPress(KeyPressEvent &event) { return; } + if (key.is(Key::Encoder) && selectedRow() == 0) { + _manager.pages().textInput.show("NAME:", _project.selectedCurveSequence().name(), CurveSequence::NameLength, [this] (bool result, const char *text) { + if (result) { + _project.selectedCurveSequence().setName(text); + } + }); + + return; + } + + if (!event.consumed()) { ListPage::keyPress(event); } @@ -84,6 +114,16 @@ void CurveSequencePage::contextShow(bool doubleClick) { )); } +void CurveSequencePage::saveContextShow(bool doubleClick) { + showContextMenu(ContextMenu( + saveContextMenuItems, + int(SaveContextAction::Last), + [&] (int index) { saveContextAction(index); }, + [&] (int index) { return true; }, + doubleClick + )); +} + void CurveSequencePage::contextAction(int index) { switch (ContextAction(index)) { case ContextAction::Init: @@ -106,6 +146,23 @@ void CurveSequencePage::contextAction(int index) { } } +void CurveSequencePage::saveContextAction(int index) { + switch (SaveContextAction(index)) { + case SaveContextAction::Load: + loadSequence(); + break; + case SaveContextAction::Save: + saveSequence(); + break; + case SaveContextAction::SaveAs: + saveAsSequence(); + break; + case SaveContextAction::Last: + break; + } +} + + bool CurveSequencePage::contextActionEnabled(int index) const { switch (ContextAction(index)) { case ContextAction::Paste: @@ -141,3 +198,82 @@ void CurveSequencePage::duplicateSequence() { void CurveSequencePage::initRoute() { _manager.pages().top.editRoute(_listModel.routingTarget(selectedRow()), _project.selectedTrackIndex()); } + +void CurveSequencePage::loadSequence() { + _manager.pages().fileSelect.show("LOAD SEQUENCE", FileType::CurveSequence, _project.selectedCurveSequence().slotAssigned() ? _project.selectedCurveSequence().slot() : 0, false, [this] (bool result, int slot) { + if (result) { + _manager.pages().confirmation.show("ARE YOU SURE?", [this, slot] (bool result) { + if (result) { + loadSequenceFromSlot(slot); + } + }); + } + }); +} + +void CurveSequencePage::saveSequence() { + + if (!_project.selectedCurveSequence().slotAssigned() || sizeof(_project.selectedCurveSequence().name())==0) { + saveAsSequence(); + return; + } + + saveSequenceToSlot(_project.selectedCurveSequence().slot()); + + showMessage("SEQUENCE SAVED"); +} + +void CurveSequencePage::saveAsSequence() { + _manager.pages().fileSelect.show("SAVE SEQUENCE", FileType::CurveSequence, _project.selectedCurveSequence().slotAssigned() ? _project.selectedCurveSequence().slot() : 0, true, [this] (bool result, int slot) { + if (result) { + if (FileManager::slotUsed(FileType::CurveSequence, slot)) { + _manager.pages().confirmation.show("ARE YOU SURE?", [this, slot] (bool result) { + if (result) { + saveSequenceToSlot(slot); + } + }); + } else { + saveSequenceToSlot(slot); + } + } + }); +} + +void CurveSequencePage::saveSequenceToSlot(int slot) { + //_engine.suspend(); + _manager.pages().busy.show("SAVING SEQUENCE ..."); + + FileManager::task([this, slot] () { + return FileManager::writeCurveSequence(_project.selectedCurveSequence(), slot); + }, [this] (fs::Error result) { + if (result == fs::OK) { + showMessage("SEQUENCE SAVED"); + } else { + showMessage(FixedStringBuilder<32>("FAILED (%s)", fs::errorToString(result))); + } + // TODO lock ui mutex + _manager.pages().busy.close(); + _engine.resume(); + }); +} + +void CurveSequencePage::loadSequenceFromSlot(int slot) { + //_engine.suspend(); + _manager.pages().busy.show("LOADING SEQUENCE ..."); + + FileManager::task([this, slot] () { + // TODO this is running in file manager thread but model notification affect ui + return FileManager::readCurveSequence(_project.selectedCurveSequence(), slot); + }, [this] (fs::Error result) { + if (result == fs::OK) { + showMessage("SEQUENCE LOADED"); + } else if (result == fs::INVALID_CHECKSUM) { + showMessage("INVALID SEQUENCE FILE"); + } else { + showMessage(FixedStringBuilder<32>("FAILED (%s)", fs::errorToString(result))); + } + // TODO lock ui mutex + _manager.pages().busy.close(); + _engine.resume(); + }); +} diff --git a/src/apps/sequencer/ui/pages/CurveSequencePage.h b/src/apps/sequencer/ui/pages/CurveSequencePage.h index f38591ee..277894a3 100644 --- a/src/apps/sequencer/ui/pages/CurveSequencePage.h +++ b/src/apps/sequencer/ui/pages/CurveSequencePage.h @@ -18,7 +18,10 @@ class CurveSequencePage : public ListPage { private: void contextShow(bool doubleClick = false); + void saveContextShow(bool doubleClick = false); + void saveContextAction(bool doubleClick = false); void contextAction(int index); + void saveContextAction(int index); bool contextActionEnabled(int index) const; void initSequence(); @@ -27,5 +30,11 @@ class CurveSequencePage : public ListPage { void duplicateSequence(); void initRoute(); + void loadSequence(); + void saveSequence(); + void saveAsSequence(); + void saveSequenceToSlot(int slot); + void loadSequenceFromSlot(int slot); + CurveSequenceListModel _listModel; }; diff --git a/src/apps/sequencer/ui/pages/GeneratorPage.cpp b/src/apps/sequencer/ui/pages/GeneratorPage.cpp index 54bfd0b8..ea2a20cb 100644 --- a/src/apps/sequencer/ui/pages/GeneratorPage.cpp +++ b/src/apps/sequencer/ui/pages/GeneratorPage.cpp @@ -114,6 +114,19 @@ void GeneratorPage::updateLeds(Leds &leds) { } } break; + case Track::TrackMode::Stochastic: { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + const auto &sequence = _project.selectedStochasticSequence(); + currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + for (int i = 0; i < 12; ++i) { + int stepIndex = stepOffset() + i; + bool red = (stepIndex == currentStep) || _stepSelection->at(stepIndex); + bool green = (stepIndex != currentStep) && (sequence.step(stepIndex).gate() || _stepSelection->at(stepIndex)); + leds.set(MatrixMap::fromStep(i), red, green); + } + } + break; + default: return; } @@ -220,6 +233,9 @@ void GeneratorPage::drawEuclideanGenerator(Canvas &canvas, const EuclideanGenera void GeneratorPage::drawRandomGenerator(Canvas &canvas, const RandomGenerator &generator) const { const auto &pattern = generator.pattern(); int steps = pattern.size(); + if (_project.selectedTrack().trackMode() == Track::TrackMode::Stochastic) { + steps = 12; + } int stepWidth = Width / steps; int stepHeight = 16; diff --git a/src/apps/sequencer/ui/pages/LayoutPage.cpp b/src/apps/sequencer/ui/pages/LayoutPage.cpp index 0ae8a176..4a05a470 100644 --- a/src/apps/sequencer/ui/pages/LayoutPage.cpp +++ b/src/apps/sequencer/ui/pages/LayoutPage.cpp @@ -35,6 +35,8 @@ void LayoutPage::draw(Canvas &canvas) { void LayoutPage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + functionShortcuts(event); + if (key.isFunction()) { if (key.function() == 4 && _mode == Mode::TrackMode && !_trackModeListModel.sameAsProject(_project)) { _manager.pages().confirmation.show("ARE YOU SURE?", [this] (bool result) { @@ -77,3 +79,24 @@ void LayoutPage::setMode(Mode mode) { } _mode = mode; } + +void LayoutPage::functionShortcuts(KeyPressEvent event) { + { + const auto &key = event.key(); + if (key.isFunction() && key.is(Key::F0) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Project); + } + + if (key.isFunction() && key.is(Key::F2) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Routing); + } + + if (key.isFunction() && key.is(Key::F3) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::MidiOutput); + } + + if (key.isFunction() && key.is(Key::F4) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::UserScale); + } + } +} diff --git a/src/apps/sequencer/ui/pages/LayoutPage.h b/src/apps/sequencer/ui/pages/LayoutPage.h index 9fd47009..5eacc054 100644 --- a/src/apps/sequencer/ui/pages/LayoutPage.h +++ b/src/apps/sequencer/ui/pages/LayoutPage.h @@ -37,6 +37,8 @@ class LayoutPage : public ListPage { void setMode(Mode mode); + void functionShortcuts(KeyPressEvent event); + Mode _mode = Mode::TrackMode; TrackModeListModel _trackModeListModel; LinkTrackListModel _linkTrackListModel; diff --git a/src/apps/sequencer/ui/pages/MidiOutputPage.cpp b/src/apps/sequencer/ui/pages/MidiOutputPage.cpp index cf9cffd1..928d8339 100644 --- a/src/apps/sequencer/ui/pages/MidiOutputPage.cpp +++ b/src/apps/sequencer/ui/pages/MidiOutputPage.cpp @@ -1,5 +1,7 @@ #include "MidiOutputPage.h" +#include "Pages.h" + #include "ui/painters/WindowPainter.h" #include "core/utils/StringBuilder.h" @@ -45,6 +47,8 @@ void MidiOutputPage::draw(Canvas &canvas) { void MidiOutputPage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + functionShortcuts(event); + if (key.isFunction()) { switch (Function(key.function())) { case Function::Prev: @@ -59,9 +63,12 @@ void MidiOutputPage::keyPress(KeyPressEvent &event) { setEdit(false); break; case Function::Commit: - *_output = _editOutput; - setEdit(false); - showMessage("OUTPUT CHANGED"); + bool showCommit = *_output != _editOutput; + if (showCommit) { + *_output = _editOutput; + setEdit(false); + showMessage("OUTPUT CHANGED"); + } break; } event.consume(); @@ -95,3 +102,24 @@ void MidiOutputPage::selectOutput(int outputIndex) { showOutput(outputIndex); } } + +void MidiOutputPage::functionShortcuts(KeyPressEvent event) { + { + const auto &key = event.key(); + if (key.isFunction() && key.is(Key::F0) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Project); + } + + if (key.isFunction() && key.is(Key::F1) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Layout); + } + + if (key.isFunction() && key.is(Key::F2) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Routing); + } + + if (key.isFunction() && key.is(Key::F4) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::UserScale); + } + } +} \ No newline at end of file diff --git a/src/apps/sequencer/ui/pages/MidiOutputPage.h b/src/apps/sequencer/ui/pages/MidiOutputPage.h index 478f59d0..0c317a09 100644 --- a/src/apps/sequencer/ui/pages/MidiOutputPage.h +++ b/src/apps/sequencer/ui/pages/MidiOutputPage.h @@ -25,6 +25,7 @@ class MidiOutputPage : public ListPage { private: void showOutput(int outputIndex); void selectOutput(int outputIndex); + void functionShortcuts(KeyPressEvent event); MidiOutput::Output _editOutput; OutputListModel _outputListModel; diff --git a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp index 7e9d2959..df7b2370 100644 --- a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp +++ b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp @@ -92,7 +92,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { const auto &trackEngine = _engine.selectedTrackEngine().as(); - const auto &sequence = _project.selectedNoteSequence(); + auto &sequence = _project.selectedNoteSequence(); const auto &scale = sequence.selectedScale(_project.scale()); int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; int currentRecordStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentRecordStep() : -1; @@ -106,8 +106,8 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { if (track.isPatternFollowDisplayOn() && _engine.state().running()) { bool section_change = bool((currentStep) % StepCount == 0); // StepCount is relative to screen int section_no = int((currentStep) / StepCount); - if (section_change && section_no != _section) { - _section = section_no; + if (section_change && section_no != sequence.section()) { + sequence.setSecion(section_no); } } @@ -309,7 +309,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { void NoteSequenceEditPage::updateLeds(Leds &leds) { const auto &trackEngine = _engine.selectedTrackEngine().as(); - const auto &sequence = _project.selectedNoteSequence(); + auto &sequence = _project.selectedNoteSequence(); int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; for (int i = 0; i < 16; ++i) { @@ -319,7 +319,7 @@ void NoteSequenceEditPage::updateLeds(Leds &leds) { leds.set(MatrixMap::fromStep(i), red, green); } - LedPainter::drawSelectedSequenceSection(leds, _section); + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); // show quick edit keys if (globalKeyState()[Key::Page] && !globalKeyState()[Key::Shift]) { @@ -386,6 +386,43 @@ void NoteSequenceEditPage::keyPress(KeyPressEvent &event) { return; } + + if (key.isFunction()) { + int v = 0; + switch (key.code()) { + case Key::F0: + v=1; + break; + case Key::F1: + v=2; + break; + case Key::F2: + v=3; + break; + case Key::F3: + v=4; + break; + case Key::F4: + v=5; + break; + } + for (int i=0; i<16; ++i) { + if (key.state(i)) { + const auto &scale = sequence.selectedScale(_project.scale()); + int stepIndex = 0; + if (i>=8) { + stepIndex = i -8; + } else { + stepIndex = i+8; + } + sequence.step(stepIndex).setNote(scale.notesPerOctave()*v); + event.consume(); + return; + + } + } + + } _stepSelection.keyPress(event, stepOffset()); updateMonitorStep(); @@ -443,7 +480,7 @@ void NoteSequenceEditPage::keyPress(KeyPressEvent &event) { _stepSelection.shiftLeft(); } else { track.setPatternFollowDisplay(false); - _section = std::max(0, _section - 1); + sequence.setSecion(std::max(0, sequence.section() - 1)); } event.consume(); } @@ -454,7 +491,7 @@ void NoteSequenceEditPage::keyPress(KeyPressEvent &event) { _stepSelection.shiftRight(); } else { track.setPatternFollowDisplay(false); - _section = std::min(3, _section + 1); + sequence.setSecion(std::min(3, sequence.section() + 1)); } event.consume(); } @@ -791,10 +828,10 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & SequencePainter::drawProbability( canvas, 64 + 32 + 8, 32 - 4, 64 - 16, 8, - step.gateProbability() + 1, NoteSequence::GateProbability::Range + step.gateProbability(), NoteSequence::GateProbability::Range-1 ); str.reset(); - str("%.1f%%", 100.f * (step.gateProbability() + 1.f) / NoteSequence::GateProbability::Range); + str("%.1f%%", 100.f * (step.gateProbability()) / (NoteSequence::GateProbability::Range-1)); canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; @@ -824,10 +861,10 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & SequencePainter::drawProbability( canvas, 64 + 32 + 8, 32 - 4, 64 - 16, 8, - step.retriggerProbability() + 1, NoteSequence::RetriggerProbability::Range + step.retriggerProbability(), NoteSequence::RetriggerProbability::Range-1 ); str.reset(); - str("%.1f%%", 100.f * (step.retriggerProbability() + 1.f) / NoteSequence::RetriggerProbability::Range); + str("%.1f%%", 100.f * (step.retriggerProbability()) / (NoteSequence::RetriggerProbability::Range-1)); canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; @@ -857,10 +894,10 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & SequencePainter::drawProbability( canvas, 64 + 32 + 8, 32 - 4, 64 - 16, 8, - step.lengthVariationProbability() + 1, NoteSequence::LengthVariationProbability::Range + step.lengthVariationProbability(), NoteSequence::LengthVariationProbability::Range-1 ); str.reset(); - str("%.1f%%", 100.f * (step.lengthVariationProbability() + 1.f) / NoteSequence::LengthVariationProbability::Range); + str("%.1f%%", 100.f * (step.lengthVariationProbability()) / (NoteSequence::LengthVariationProbability::Range-1)); canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; @@ -880,10 +917,10 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & SequencePainter::drawProbability( canvas, 64 + 32 + 8, 32 - 4, 64 - 16, 8, - step.noteVariationProbability() + 1, NoteSequence::NoteVariationProbability::Range + step.noteVariationProbability(), NoteSequence::NoteVariationProbability::Range-1 ); str.reset(); - str("%.1f%%", 100.f * (step.noteVariationProbability() + 1.f) / NoteSequence::NoteVariationProbability::Range); + str("%.1f%%", 100.f * (step.noteVariationProbability()) / (NoteSequence::NoteVariationProbability::Range-1)); canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; @@ -1069,4 +1106,4 @@ void NoteSequenceEditPage::setSelectedStepsGate(bool gate) { sequence.step(stepIndex).setGate(gate); } } -} +} \ No newline at end of file diff --git a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.h b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.h index 7a6edb28..8430e2f9 100644 --- a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.h +++ b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.h @@ -31,7 +31,7 @@ class NoteSequenceEditPage : public BasePage { static const int StepCount = 16; - int stepOffset() const { return _section * StepCount; } + int stepOffset() const { return _project.selectedNoteSequence().section() * StepCount; } void switchLayer(int functionKey, bool shift); int activeFunctionKey(); @@ -62,7 +62,6 @@ class NoteSequenceEditPage : public BasePage { NoteSequence::Layer layer() const { return _project.selectedNoteSequenceLayer(); }; void setLayer(NoteSequence::Layer layer) { _project.setSelectedNoteSequenceLayer(layer); } - int _section = 0; bool _showDetail; uint32_t _showDetailTicks; diff --git a/src/apps/sequencer/ui/pages/NoteSequencePage.cpp b/src/apps/sequencer/ui/pages/NoteSequencePage.cpp index e159b8ff..af9a58d5 100644 --- a/src/apps/sequencer/ui/pages/NoteSequencePage.cpp +++ b/src/apps/sequencer/ui/pages/NoteSequencePage.cpp @@ -17,6 +17,13 @@ enum class ContextAction { Last }; +enum class SaveContextAction { + Load, + Save, + SaveAs, + Last +}; + static const ContextMenuModel::Item contextMenuItems[] = { { "INIT" }, { "COPY" }, @@ -25,6 +32,12 @@ static const ContextMenuModel::Item contextMenuItems[] = { { "ROUTE" }, }; +static const ContextMenuModel::Item saveContextMenuItems[] = { + { "LOAD" }, + { "SAVE" }, + { "SAVE AS"}, +}; + NoteSequencePage::NoteSequencePage(PageManager &manager, PageContext &context) : ListPage(manager, context, _listModel) @@ -54,6 +67,12 @@ void NoteSequencePage::updateLeds(Leds &leds) { void NoteSequencePage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + if (key.shiftModifier() && event.count() == 2) { + saveContextShow(); + event.consume(); + return; + } + if (key.isContextMenu()) { contextShow(); event.consume(); @@ -71,12 +90,22 @@ void NoteSequencePage::keyPress(KeyPressEvent &event) { return; } + if (key.is(Key::Encoder) && selectedRow() == 0) { + _manager.pages().textInput.show("NAME:", _project.selectedNoteSequence().name(), NoteSequence::NameLength, [this] (bool result, const char *text) { + if (result) { + _project.selectedNoteSequence().setName(text); + } + }); + + return; + } + if (!event.consumed()) { ListPage::keyPress(event); } if (key.isEncoder()) { auto row = ListPage::selectedRow(); - if (row == 5) { + if (row == 6) { _listModel.setSelectedScale(_project.scale()); } } @@ -92,6 +121,16 @@ void NoteSequencePage::contextShow(bool doubleClick) { )); } +void NoteSequencePage::saveContextShow(bool doubleClick) { + showContextMenu(ContextMenu( + saveContextMenuItems, + int(SaveContextAction::Last), + [&] (int index) { saveContextAction(index); }, + [&] (int index) { return true; }, + doubleClick + )); +} + void NoteSequencePage::contextAction(int index) { switch (ContextAction(index)) { case ContextAction::Init: @@ -114,6 +153,22 @@ void NoteSequencePage::contextAction(int index) { } } +void NoteSequencePage::saveContextAction(int index) { + switch (SaveContextAction(index)) { + case SaveContextAction::Load: + loadSequence(); + break; + case SaveContextAction::Save: + saveSequence(); + break; + case SaveContextAction::SaveAs: + saveAsSequence(); + break; + case SaveContextAction::Last: + break; + } +} + bool NoteSequencePage::contextActionEnabled(int index) const { switch (ContextAction(index)) { case ContextAction::Paste: @@ -149,3 +204,82 @@ void NoteSequencePage::duplicateSequence() { void NoteSequencePage::initRoute() { _manager.pages().top.editRoute(_listModel.routingTarget(selectedRow()), _project.selectedTrackIndex()); } + +void NoteSequencePage::loadSequence() { + _manager.pages().fileSelect.show("LOAD SEQUENCE", FileType::NoteSequence, _project.selectedNoteSequence().slotAssigned() ? _project.selectedNoteSequence().slot() : 0, false, [this] (bool result, int slot) { + if (result) { + _manager.pages().confirmation.show("ARE YOU SURE?", [this, slot] (bool result) { + if (result) { + loadSequenceFromSlot(slot); + } + }); + } + }); +} + +void NoteSequencePage::saveSequence() { + + if (!_project.selectedNoteSequence().slotAssigned() || sizeof(_project.selectedNoteSequence().name())==0) { + saveAsSequence(); + return; + } + + saveSequenceToSlot(_project.selectedNoteSequence().slot()); + + showMessage("SEQUENCE SAVED"); +} + +void NoteSequencePage::saveAsSequence() { + _manager.pages().fileSelect.show("SAVE SEQUENCE", FileType::NoteSequence, _project.selectedNoteSequence().slotAssigned() ? _project.selectedNoteSequence().slot() : 0, true, [this] (bool result, int slot) { + if (result) { + if (FileManager::slotUsed(FileType::NoteSequence, slot)) { + _manager.pages().confirmation.show("ARE YOU SURE?", [this, slot] (bool result) { + if (result) { + saveSequenceToSlot(slot); + } + }); + } else { + saveSequenceToSlot(slot); + } + } + }); +} + +void NoteSequencePage::saveSequenceToSlot(int slot) { + //_engine.suspend(); + _manager.pages().busy.show("SAVING SEQUENCE ..."); + + FileManager::task([this, slot] () { + return FileManager::writeNoteSequence(_project.selectedNoteSequence(), slot); + }, [this] (fs::Error result) { + if (result == fs::OK) { + showMessage("SEQUENCE SAVED"); + } else { + showMessage(FixedStringBuilder<32>("FAILED (%s)", fs::errorToString(result))); + } + // TODO lock ui mutex + _manager.pages().busy.close(); + _engine.resume(); + }); +} + +void NoteSequencePage::loadSequenceFromSlot(int slot) { + //_engine.suspend(); + _manager.pages().busy.show("LOADING SEQUENCE ..."); + + FileManager::task([this, slot] () { + // TODO this is running in file manager thread but model notification affect ui + return FileManager::readNoteSequence(_project.selectedNoteSequence(), slot); + }, [this] (fs::Error result) { + if (result == fs::OK) { + showMessage("SEQUENCE LOADED"); + } else if (result == fs::INVALID_CHECKSUM) { + showMessage("INVALID SEQUENCE FILE"); + } else { + showMessage(FixedStringBuilder<32>("FAILED (%s)", fs::errorToString(result))); + } + // TODO lock ui mutex + _manager.pages().busy.close(); + _engine.resume(); + }); +} diff --git a/src/apps/sequencer/ui/pages/NoteSequencePage.h b/src/apps/sequencer/ui/pages/NoteSequencePage.h index 2ed17ffc..420febbf 100644 --- a/src/apps/sequencer/ui/pages/NoteSequencePage.h +++ b/src/apps/sequencer/ui/pages/NoteSequencePage.h @@ -18,7 +18,10 @@ class NoteSequencePage : public ListPage { private: void contextShow(bool doubleClick = false); + void saveContextShow(bool doubleClick = false); + void saveContextAction(bool doubleClick = false); void contextAction(int index); + void saveContextAction(int index); bool contextActionEnabled(int index) const; void initSequence(); @@ -27,5 +30,11 @@ class NoteSequencePage : public ListPage { void duplicateSequence(); void initRoute(); + void loadSequence(); + void saveSequence(); + void saveAsSequence(); + void saveSequenceToSlot(int slot); + void loadSequenceFromSlot(int slot); + NoteSequenceListModel _listModel; }; diff --git a/src/apps/sequencer/ui/pages/OverviewPage.cpp b/src/apps/sequencer/ui/pages/OverviewPage.cpp index b6eacee8..029fea7e 100644 --- a/src/apps/sequencer/ui/pages/OverviewPage.cpp +++ b/src/apps/sequencer/ui/pages/OverviewPage.cpp @@ -3,11 +3,18 @@ #include "model/NoteTrack.h" #include "ui/painters/WindowPainter.h" +#include "ui/LedPainter.h" +#include "ui/painters/SequencePainter.h" -static void drawNoteTrack(Canvas &canvas, int trackIndex, const NoteTrackEngine &trackEngine, const NoteSequence &sequence) { +static void drawNoteTrack(Canvas &canvas, int trackIndex, const NoteTrackEngine &trackEngine, NoteSequence &sequence, bool running, bool patternFollow) { canvas.setBlendMode(BlendMode::Set); - int stepOffset = (std::max(0, trackEngine.currentStep()) / 16) * 16; + int stepOffset = 16*sequence.section(); + if (patternFollow) { + stepOffset = (std::max(0, trackEngine.currentStep()) / 16) * 16*sequence.section(); + int section_no = int((trackEngine.currentStep()) / 16); + sequence.setSecion(section_no); + } int y = trackIndex * 8; for (int i = 0; i < 16; ++i) { @@ -53,11 +60,39 @@ static void drawCurve(Canvas &canvas, int x, int y, int w, int h, float &lastY, lastY = fy0; } -static void drawCurveTrack(Canvas &canvas, int trackIndex, const CurveTrackEngine &trackEngine, const CurveSequence &sequence) { +static void drawStochasticTrack(Canvas &canvas, int trackIndex, const StochasticEngine &trackEngine, const StochasticSequence &sequence) { + canvas.setBlendMode(BlendMode::Set); + + int stepOffset = (std::max(0, trackEngine.currentStep()) / 12) * 12; + int y = trackIndex * 8; + + for (int i = 0; i < 12; ++i) { + int stepIndex = stepOffset + i; + const auto &step = sequence.step(stepIndex); + + int x = 16 + (68 + i * 8); + + if (trackEngine.currentStep() == stepIndex) { + canvas.setColor(step.gate() ? Color::Bright : Color::MediumBright); + canvas.fillRect(x + 1, y + 1, 6, 6); + } else { + canvas.setColor(step.gate() ? Color::Medium : Color::Low); + canvas.fillRect(x + 1, y + 1, 6, 6); + } + } + +} + +static void drawCurveTrack(Canvas &canvas, int trackIndex, const CurveTrackEngine &trackEngine, CurveSequence &sequence, bool running, bool patternFollow) { canvas.setBlendMode(BlendMode::Add); canvas.setColor(Color::MediumBright); - int stepOffset = (std::max(0, trackEngine.currentStep()) / 16) * 16; + int stepOffset = 16*sequence.section(); + if (patternFollow) { + stepOffset = (std::max(0, trackEngine.currentStep()) / 16) * 16*sequence.section(); + int section_no = int((trackEngine.currentStep()) / 16); + sequence.setSecion(section_no); + } int y = trackIndex * 8; float lastY = -1.f; @@ -69,7 +104,7 @@ static void drawCurveTrack(Canvas &canvas, int trackIndex, const CurveTrackEngin float max = step.maxNormalized(); const auto function = Curve::function(Curve::Type(std::min(Curve::Last - 1, step.shape()))); - int x = 64 + i * 8; + int x = 68 + i * 8; drawCurve(canvas, x, y + 1, 8, 6, lastY, function, min, max); } @@ -88,9 +123,16 @@ OverviewPage::OverviewPage(PageManager &manager, PageContext &context) : {} void OverviewPage::enter() { + updateMonitorStep(); + _showDetail = false; } void OverviewPage::exit() { + if (_project.selectedTrack().trackMode()==Track::TrackMode::Note) { + _engine.selectedTrackEngine().as().setMonitorStep(-1); + } else if (_project.selectedTrack().trackMode()==Track::TrackMode::Stochastic) { + _engine.selectedTrackEngine().as().setMonitorStep(-1); + } } void OverviewPage::draw(Canvas &canvas) { @@ -106,7 +148,7 @@ void OverviewPage::draw(Canvas &canvas) { canvas.vline(196 + 2, 0, 68); for (int trackIndex = 0; trackIndex < 8; trackIndex++) { - const auto &track = _project.track(trackIndex); + auto &track = _project.track(trackIndex); const auto &trackState = _project.playState().trackState(trackIndex); const auto &trackEngine = _engine.trackEngine(trackIndex); @@ -127,6 +169,9 @@ void OverviewPage::draw(Canvas &canvas) { case Track::TrackMode::MidiCv: canvas.drawText(2, y, track.midiCvTrack().name()); break; + case Track::TrackMode::Stochastic: + canvas.drawText(2, y, track.stochasticTrack().name()); + break; default: break; } @@ -139,7 +184,7 @@ void OverviewPage::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Sub); canvas.drawText(46, y, FixedStringBuilder<8>("P%d", trackState.pattern() + 1)); canvas.setBlendMode(BlendMode::Set); - // gate output + bool gate = _engine.gateOutput() & (1 << trackIndex); canvas.setColor(gate ? Color::Bright : Color::Medium); canvas.fillRect(256 - 48 + 1, trackIndex * 8 + 1, 6, 6); @@ -149,11 +194,26 @@ void OverviewPage::draw(Canvas &canvas) { canvas.drawText(256 - 32, y, FixedStringBuilder<8>("%.2fV", _engine.cvOutput().channel(trackIndex))); switch (track.trackMode()) { - case Track::TrackMode::Note: - drawNoteTrack(canvas, trackIndex, trackEngine.as(), track.noteTrack().sequence(trackState.pattern())); + case Track::TrackMode::Note: { + bool patterFolow = false; + if (track.noteTrack().patternFollow()==Types::PatternFollow::Display || track.noteTrack().patternFollow()==Types::PatternFollow::DispAndLP) { + patterFolow = true; + canvas.drawText(256 - 54, y, FixedStringBuilder<8>("F")); + } + drawNoteTrack(canvas, trackIndex, trackEngine.as(), track.noteTrack().sequence(trackState.pattern()), _engine.state().running(), patterFolow); + } break; - case Track::TrackMode::Curve: - drawCurveTrack(canvas, trackIndex, trackEngine.as(), track.curveTrack().sequence(trackState.pattern())); + case Track::TrackMode::Curve: { + bool patterFolow = false; + if (track.curveTrack().patternFollow()==Types::PatternFollow::Display || track.curveTrack().patternFollow()==Types::PatternFollow::DispAndLP) { + patterFolow = true; + canvas.drawText(256 - 54, y, FixedStringBuilder<8>("F")); + } + drawCurveTrack(canvas, trackIndex, trackEngine.as(), track.curveTrack().sequence(trackState.pattern()), _engine.state().running(), patterFolow); + } + break; + case Track::TrackMode::Stochastic: + drawStochasticTrack(canvas, trackIndex, trackEngine.as(), track.stochasticTrack().sequence(trackState.pattern())); break; case Track::TrackMode::MidiCv: break; @@ -161,41 +221,544 @@ void OverviewPage::draw(Canvas &canvas) { break; } } + + + + if (!_stepSelection.any() || os::ticks() > _showDetailTicks + os::time::ms(500)) { + _showDetail = false; + } + if (_showDetail) { + + auto track = _project.selectedTrack(); + switch (track.trackMode()) { + case Track::TrackMode::Note: { + auto &sequence = _project.selectedNoteSequence(); + drawDetail(canvas, sequence.step(_stepSelection.first())); + } + break; + case Track::TrackMode::Stochastic: { + auto &sequence = _project.selectedStochasticSequence(); + drawStochasticDetail(canvas, sequence.step(_stepSelection.first())); + } + break; + case Track::TrackMode::Curve: { + auto &sequence = _project.selectedCurveSequence(); + drawCurveDetail(canvas, sequence.step(_stepSelection.first())); + } + default: + break; + } + + } + } void OverviewPage::updateLeds(Leds &leds) { + + switch (_project.selectedTrack().trackMode()) { + + case Track::TrackMode::Note: { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + auto &sequence = _project.selectedNoteSequence(); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + + for (int i = 0; i < 16; ++i) { + int stepIndex = stepOffset() + i; + bool red = (stepIndex == currentStep) || _stepSelection[stepIndex]; + bool green = (stepIndex != currentStep) && (sequence.step(stepIndex).gate() || _stepSelection[stepIndex]); + leds.set(MatrixMap::fromStep(i), red, green); + } + + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); + } + break; + case Track::TrackMode::Stochastic: { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + auto &sequence = _project.selectedStochasticSequence(); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + + for (int i = 0; i < 16; ++i) { + int stepIndex = stepOffset() + i; + bool red = (stepIndex == currentStep) || _stepSelection[stepIndex]; + bool green = (stepIndex != currentStep) && (sequence.step(stepIndex).gate() || _stepSelection[stepIndex]); + leds.set(MatrixMap::fromStep(i), red, green); + } + + LedPainter::drawSelectedSequenceSection(leds, 0); + } + break; + case Track::TrackMode::Curve: { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + const auto &sequence = _project.selectedCurveSequence(); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + + for (int i = 0; i < 16; ++i) { + int stepIndex = stepOffset() + i; + bool red = (stepIndex == currentStep) || _stepSelection[stepIndex]; + bool green = (stepIndex != currentStep) && (sequence.step(stepIndex).gate() > 0 || _stepSelection[stepIndex]); + leds.set(MatrixMap::fromStep(i), red, green); + } + + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); + + LedPainter::drawSelectedSequenceSection(leds, sequence.section()); + } + break; + default: + break; + } } void OverviewPage::keyDown(KeyEvent &event) { + _stepSelection.keyDown(event, stepOffset()); +} + +void OverviewPage::keyUp(KeyEvent &event) { + _stepSelection.keyUp(event, stepOffset()); +} + +void OverviewPage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + if (key.isGlobal()) { return; } - // event.consume(); -} + if (key.isQuickEdit()) { -void OverviewPage::keyUp(KeyEvent &event) { - const auto &key = event.key(); + switch (_project.selectedTrack().trackMode()) { + case Track::TrackMode::Note: { + auto &track = _project.selectedTrack().noteTrack(); + if (key.is(Key::Step15)) { + bool lpConnected = _engine.isLaunchpadConnected(); + track.togglePatternFollowDisplay(lpConnected); + } + } + break; + case Track::TrackMode::Curve: { + auto &track = _project.selectedTrack().curveTrack(); + if (key.is(Key::Step15)) { + bool lpConnected = _engine.isLaunchpadConnected(); + track.togglePatternFollowDisplay(lpConnected); + } + } + break; + default: + break; + } + } - if (key.isGlobal()) { + if (key.pageModifier()) { return; } - // event.consume(); + _stepSelection.keyPress(event, stepOffset()); + auto &track = _project.selectedTrack(); + + if (key.isEncoder() && _project.selectedTrack().trackMode() == Track::TrackMode::Curve) { + switch (_project.selectedCurveSequenceLayer()) { + case CurveSequence::Layer::Shape: + showMessage("Min"); + _project.setSelectedCurveSequenceLayer(CurveSequence::Layer::Min); + break; + case CurveSequence::Layer::Min: + showMessage("Max"); + _project.setSelectedCurveSequenceLayer(CurveSequence::Layer::Max); + break; + case CurveSequence::Layer::Max: + showMessage("Gate"); + _project.setSelectedCurveSequenceLayer(CurveSequence::Layer::Gate); + break; + case CurveSequence::Layer::Gate: + showMessage("Shape"); + _project.setSelectedCurveSequenceLayer(CurveSequence::Layer::Shape); + break; + + default: + break; + } + } + + if (key.isStep() && event.count() == 2) { + switch (track.trackMode()) { + case Track::TrackMode::Note: { + + auto &sequence = _project.selectedNoteSequence(); + int stepIndex = stepOffset() + key.step(); + sequence.step(stepIndex).toggleGate(); + event.consume(); + } + break; + case Track::TrackMode::Stochastic: { + int stepIndex = stepOffset() + key.step(); + auto &sequence = _project.selectedStochasticSequence(); + sequence.step(stepIndex).toggleGate(); + event.consume(); + } + break; + case Track::TrackMode::Curve: { + int stepIndex = stepOffset() + key.step(); + auto &sequence = _project.selectedCurveSequence(); + const auto step = sequence.step(stepIndex); + FixedStringBuilder<8> str; + switch (step.gate()) { + case 0: + str("____"); + break; + case 1: + str("|___"); + break; + case 2: + str("_|__"); + break; + case 3: + str("||__"); + break; + case 4: + str("__|_"); + break; + case 5: + str("|_|_"); + break; + case 6: + str("_||_"); + break; + case 7: + str("|||_"); + break; + case 8: + str("___|"); + break; + case 9: + str("|__|"); + break; + case 10: + str("_|_|"); + break; + case 11: + str("||_|"); + break; + case 12: + str("__||"); + break; + case 13: + str("|_||"); + break; + case 14: + str("_|||"); + break; + case 15: + str("||||"); + break; + } + + showMessage(str); + + } + default: + break; + } + } + + if (key.isLeft()) { + switch (track.trackMode()) { + case Track::TrackMode::Note: { + auto &sequence = _project.selectedNoteSequence(); + sequence.setSecion(std::max(0, sequence.section() - 1)); + track.noteTrack().setPatternFollowDisplay(false); + break; + } + case Track::TrackMode::Curve: { + auto &sequence = _project.selectedCurveSequence(); + sequence.setSecion(std::max(0, sequence.section() - 1)); + track.curveTrack().setPatternFollowDisplay(false); + break; + } + default: + break; + } + + event.consume(); + } + if (key.isRight()) { + switch (track.trackMode()) { + case Track::TrackMode::Note: { + auto &sequence = _project.selectedNoteSequence(); + sequence.setSecion(std::min(3, sequence.section() + 1)); + track.noteTrack().setPatternFollowDisplay(false); + break; + } + case Track::TrackMode::Curve: { + auto &sequence = _project.selectedCurveSequence(); + sequence.setSecion(std::min(3, sequence.section() + 1)); + track.curveTrack().setPatternFollowDisplay(false); + break; + } + default: + break; + } + + event.consume(); + } } -void OverviewPage::keyPress(KeyPressEvent &event) { - const auto &key = event.key(); +void OverviewPage::encoder(EncoderEvent &event) { - if (key.isGlobal()) { + auto &track = _project.selectedTrack(); + + if (!_stepSelection.any()) { return; } - // event.consume(); + _showDetail = true; + _showDetailTicks = os::ticks(); + + switch (track.trackMode()) { + + case Track::TrackMode::Note: { + auto &sequence = _project.selectedNoteSequence(); + const auto &scale = sequence.selectedScale(_project.scale()); + for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + bool shift = globalKeyState()[Key::Shift]; + step.setNote(step.note() + event.value() * ((shift && scale.isChromatic()) ? scale.notesPerOctave() : 1)); + updateMonitorStep(); + } + } + } + break; + case Track::TrackMode::Stochastic: { + auto &sequence = _project.selectedStochasticSequence(); + for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + step.setNoteVariationProbability(step.noteVariationProbability() + event.value()); + } + } + } + + case Track::TrackMode::Curve: { + auto &sequence = _project.selectedCurveSequence(); + for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + + switch (_project.selectedCurveSequenceLayer()) { + case CurveSequence::Layer::Shape: + step.setShape(step.shape() + event.value()); + break; + case CurveSequence::Layer::Min: + step.setMin(step.min() + event.value()*8); + break; + case CurveSequence::Layer::Max: + step.setMax(step.max() + event.value()*8); + break; + case CurveSequence::Layer::Gate: + step.setGate(step.gate()+ event.value()); + break; + default: + break; + } + } + } + } + default: + break; + } + + event.consume(); } -void OverviewPage::encoder(EncoderEvent &event) { - // event.consume(); +void OverviewPage::drawDetail(Canvas &canvas, const NoteSequence::Step &step) { + const auto &sequence = _project.selectedNoteSequence(); + const auto &scale = sequence.selectedScale(_project.scale()); + + FixedStringBuilder<16> str; + + WindowPainter::drawFrame(canvas, 64, 16, 128, 32); + + canvas.setBlendMode(BlendMode::Set); + canvas.setColor(Color::Bright); + canvas.vline(64 + 32, 16, 32); + + canvas.setFont(Font::Small); + str("%d", _stepSelection.first() + 1); + if (_stepSelection.count() > 1) { + str("*"); + } + canvas.drawTextCentered(64, 16, 32, 32, str); + + canvas.setFont(Font::Tiny); + + str.reset(); + scale.noteName(str, step.note(), sequence.selectedRootNote(_model.project().rootNote()), Scale::Long); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + canvas.setFont(Font::Tiny); + } + +void OverviewPage::drawCurveDetail(Canvas &canvas, const CurveSequence::Step &step) { + FixedStringBuilder<16> str; + + WindowPainter::drawFrame(canvas, 64, 16, 128, 32); + + canvas.setBlendMode(BlendMode::Set); + canvas.setColor(Color::Bright); + canvas.vline(64 + 32, 16, 32); + + canvas.setFont(Font::Small); + str("%d", _stepSelection.first() + 1); + if (_stepSelection.count() > 1) { + str("*"); + } + canvas.drawTextCentered(64, 16, 32, 32, str); + + canvas.setFont(Font::Tiny); + + str.reset(); + + switch (_project.selectedCurveSequenceLayer()) { + case CurveSequence::Layer::Shape: { + float min = step.minNormalized(); + float max = step.maxNormalized(); + float lastY = -1.f; + const auto function = Curve::function(Curve::Type(std::min(Curve::Last - 1, step.shape()))); + drawCurve(canvas, 64 + 64, 24 + 1, 18, 12, lastY, function, min, max); + } + break; + case CurveSequence::Layer::Min: { + str("%.1f%%", 100.f * (step.min()) / (CurveSequence::Min::Range-1)); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + } + break; + case CurveSequence::Layer::Max: { + str("%.1f%%", 100.f * (step.max()) / (CurveSequence::Max::Range-1)); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + } + break; + case CurveSequence::Layer::Gate: { + const std::bitset<4> mask = 0x1; + std::bitset<4> enabled; + switch (step.gate()) { + case 0: + enabled =0x0; + break; + case 1: + enabled = 0x1; + break; + case 2: + enabled = 0x2; + break; + case 3: + enabled = 0x3; + break; + case 4: + enabled = 0x4; + break; + case 5: + enabled = 0x5; + break; + case 6: + enabled = 0x6; + break; + case 7: + enabled = 0x7; + break; + case 8: + enabled = 0x8; + case 9: + enabled = 0x9; + break; + case 10: + enabled = 0xa; + break; + case 11: + enabled = 0xb; + break; + case 12: + enabled = 0xc; + break; + case 13: + enabled = 0xd; + break; + case 14: + enabled = 0xe; + break; + case 15: + enabled = 0xf; + break; + } + + for (int i = 0; i < 4; i++) { + if (((enabled >> i) & mask) == 1) { + canvas.vline(64 + 64 + (i*2), 24, 16); + } else { + canvas.hline(64 + 64 + (i*2), 24, 1); + } + } + } + default: + break; + } + + canvas.setFont(Font::Tiny); + +} + + +void OverviewPage::drawStochasticDetail(Canvas &canvas, const StochasticSequence::Step &step) { + FixedStringBuilder<16> str; + + WindowPainter::drawFrame(canvas, 64, 16, 128, 32); + + canvas.setBlendMode(BlendMode::Set); + canvas.setColor(Color::Bright); + canvas.vline(64 + 32, 16, 32); + + canvas.setFont(Font::Small); + str("%d", _stepSelection.first() + 1); + if (_stepSelection.count() > 1) { + str("*"); + } + canvas.drawTextCentered(64, 16, 32, 32, str); + + canvas.setFont(Font::Tiny); + + str.reset(); + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.noteVariationProbability() + 1, StochasticSequence::NoteVariationProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.noteVariationProbability()) / (StochasticSequence::NoteVariationProbability::Range -1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + canvas.setFont(Font::Tiny); + + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.gateProbability(), CurveSequence::GateProbability::Range-1 + ); + str.reset(); + str("%.1f%%", 100.f * (step.gateProbability()) / (CurveSequence::GateProbability::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + + canvas.setFont(Font::Tiny); +} + +void OverviewPage::updateMonitorStep() { + auto &trackEngine = _engine.selectedTrackEngine().as(); + + // TODO should we monitor an all layers not just note? + if (_stepSelection.any()) { + trackEngine.setMonitorStep(_stepSelection.first()); + } +} \ No newline at end of file diff --git a/src/apps/sequencer/ui/pages/OverviewPage.h b/src/apps/sequencer/ui/pages/OverviewPage.h index 830309d5..e6cd82b5 100644 --- a/src/apps/sequencer/ui/pages/OverviewPage.h +++ b/src/apps/sequencer/ui/pages/OverviewPage.h @@ -1,6 +1,7 @@ #pragma once #include "BasePage.h" +#include "ui/StepSelection.h" class OverviewPage : public BasePage { public: @@ -16,4 +17,35 @@ class OverviewPage : public BasePage { virtual void keyUp(KeyEvent &event) override; virtual void keyPress(KeyPressEvent &event) override; virtual void encoder(EncoderEvent &event) override; + +private: + + void drawDetail(Canvas &canvas, const NoteSequence::Step &step); + void drawStochasticDetail(Canvas &canvas, const StochasticSequence::Step &step); + void drawCurveDetail(Canvas &canvas, const CurveSequence::Step &step); + void updateMonitorStep(); + + static const int StepCount = 16; + + int stepOffset() const { + + switch (_project.selectedTrack().trackMode()) { + + case Track::TrackMode::Note: + return _project.selectedNoteSequence().section() * StepCount; + case Track::TrackMode::Stochastic: + return 0; + case Track::TrackMode::Curve: + return _project.selectedCurveSequence().section() * StepCount; + case Track::TrackMode::MidiCv: + return 0; + default: + return 0; + + } + } + + StepSelection _stepSelection; + bool _showDetail; + uint32_t _showDetailTicks; }; diff --git a/src/apps/sequencer/ui/pages/Pages.h b/src/apps/sequencer/ui/pages/Pages.h index 72d26240..e95c5aac 100644 --- a/src/apps/sequencer/ui/pages/Pages.h +++ b/src/apps/sequencer/ui/pages/Pages.h @@ -24,6 +24,8 @@ #include "RoutingPage.h" #include "SongPage.h" #include "StartupPage.h" +#include "StochasticSequenceEditPage.h" +#include "StochasticSequencePage.h" #include "SystemPage.h" #include "TempoPage.h" #include "TextInputPage.h" @@ -46,8 +48,10 @@ struct Pages { TrackPage track; NoteSequencePage noteSequence; CurveSequencePage curveSequence; + StochasticSequencePage stochasticSequence; NoteSequenceEditPage noteSequenceEdit; CurveSequenceEditPage curveSequenceEdit; + StochasticSequenceEditPage stochasticSequenceEdit; PatternPage pattern; PerformerPage performer; SongPage song; @@ -87,8 +91,10 @@ struct Pages { track(manager, context), noteSequence(manager, context), curveSequence(manager, context), + stochasticSequence(manager, context), noteSequenceEdit(manager, context), curveSequenceEdit(manager, context), + stochasticSequenceEdit(manager, context), pattern(manager, context), performer(manager, context), song(manager, context), diff --git a/src/apps/sequencer/ui/pages/ProjectPage.cpp b/src/apps/sequencer/ui/pages/ProjectPage.cpp index 567065f3..b8d5c087 100644 --- a/src/apps/sequencer/ui/pages/ProjectPage.cpp +++ b/src/apps/sequencer/ui/pages/ProjectPage.cpp @@ -1,5 +1,6 @@ #include "ProjectPage.h" +#include "TopPage.h" #include "ui/LedPainter.h" #include "ui/pages/Pages.h" #include "ui/painters/WindowPainter.h" @@ -63,6 +64,8 @@ void ProjectPage::keyPress(KeyPressEvent &event) { return; } + functionShortcuts(event); + if (key.pageModifier()) { // easter egg if (key.is(Key::Step15)) { @@ -230,3 +233,25 @@ void ProjectPage::loadProjectFromSlot(int slot) { _engine.resume(); }); } + +void ProjectPage::functionShortcuts(KeyPressEvent event) { + { + const auto &key = event.key(); + + if (key.isFunction() && key.is(Key::F1) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Layout); + } + + if (key.isFunction() && key.is(Key::F2) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Routing); + } + + if (key.isFunction() && key.is(Key::F3) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::MidiOutput); + } + + if (key.isFunction() && key.is(Key::F4) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::UserScale); + } + } +} diff --git a/src/apps/sequencer/ui/pages/ProjectPage.h b/src/apps/sequencer/ui/pages/ProjectPage.h index cf9f923e..1fba1d60 100644 --- a/src/apps/sequencer/ui/pages/ProjectPage.h +++ b/src/apps/sequencer/ui/pages/ProjectPage.h @@ -31,5 +31,7 @@ class ProjectPage : public ListPage { void saveProjectToSlot(int slot); void loadProjectFromSlot(int slot); + void functionShortcuts(KeyPressEvent event); + ProjectListModel _listModel; }; diff --git a/src/apps/sequencer/ui/pages/QuickEditPage.cpp b/src/apps/sequencer/ui/pages/QuickEditPage.cpp index 1894c72e..f36db6bc 100644 --- a/src/apps/sequencer/ui/pages/QuickEditPage.cpp +++ b/src/apps/sequencer/ui/pages/QuickEditPage.cpp @@ -68,13 +68,15 @@ void QuickEditPage::keyPress(KeyPressEvent &event) { if (key.isLeft()) { _listModel->edit(_row, 1, -1, key.shiftModifier()); + _listModel->setSelectedScale(_project.scale(), true); } else if (key.isRight()) { _listModel->edit(_row, 1, 1, key.shiftModifier()); + _listModel->setSelectedScale(_project.scale(), true); } else if (key.isStep()) { _listModel->setIndexed(_row, key.step()); } - if (_row == 5 && key.isEncoder()) { + if (key.isEncoder()) { _listModel->setSelectedScale(_project.scale(), true); close(); } diff --git a/src/apps/sequencer/ui/pages/RoutingPage.cpp b/src/apps/sequencer/ui/pages/RoutingPage.cpp index 4889f008..27869409 100644 --- a/src/apps/sequencer/ui/pages/RoutingPage.cpp +++ b/src/apps/sequencer/ui/pages/RoutingPage.cpp @@ -1,5 +1,7 @@ #include "RoutingPage.h" +#include "Pages.h" + #include "ui/painters/WindowPainter.h" #include "core/utils/StringBuilder.h" @@ -50,6 +52,8 @@ void RoutingPage::draw(Canvas &canvas) { void RoutingPage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + + functionShortcuts(event); if (edit() && selectedRow() == int(RouteListModel::Item::Tracks) && key.isTrack()) { _editRoute.toggleTrack(key.track()); @@ -83,14 +87,17 @@ void RoutingPage::keyPress(KeyPressEvent &event) { } break; case Function::Commit: - _engine.midiLearn().stop(); - int conflict = _project.routing().checkRouteConflict(_editRoute, *_route); - if (conflict >= 0) { - showMessage(FixedStringBuilder<64>("ROUTE SETTINGS CONFLICT WITH ROUTE %d", conflict + 1)); - } else { - *_route = _editRoute; - setEdit(false); - showMessage("ROUTE CHANGED"); + bool showCommit = *_route != _editRoute; + if (showCommit) { + _engine.midiLearn().stop(); + int conflict = _project.routing().checkRouteConflict(_editRoute, *_route); + if (conflict >= 0) { + showMessage(FixedStringBuilder<64>("ROUTE SETTINGS CONFLICT WITH ROUTE %d", conflict + 1)); + } else { + *_route = _editRoute; + setEdit(false); + showMessage("ROUTE CHANGED"); + } } break; } @@ -180,3 +187,24 @@ void RoutingPage::assignMidiLearn(const MidiLearn::Result &result) { setTopRow(int(RouteListModel::MidiSource)); setEdit(false); } + +void RoutingPage::functionShortcuts(KeyPressEvent event) { + { + const auto &key = event.key(); + if (key.isFunction() && key.is(Key::F0) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Project); + } + + if (key.isFunction() && key.is(Key::F1) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Layout); + } + + if (key.isFunction() && key.is(Key::F3) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::MidiOutput); + } + + if (key.isFunction() && key.is(Key::F4) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::UserScale); + } + } +} diff --git a/src/apps/sequencer/ui/pages/RoutingPage.h b/src/apps/sequencer/ui/pages/RoutingPage.h index cd5c8da3..55a1408c 100644 --- a/src/apps/sequencer/ui/pages/RoutingPage.h +++ b/src/apps/sequencer/ui/pages/RoutingPage.h @@ -30,6 +30,8 @@ class RoutingPage : public ListPage { void selectRoute(int routeIndex); void assignMidiLearn(const MidiLearn::Result &result); + void functionShortcuts(KeyPressEvent event); + Routing::Route _editRoute; RouteListModel _routeListModel; Routing::Route *_route; diff --git a/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.cpp b/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.cpp new file mode 100644 index 00000000..ca7473ca --- /dev/null +++ b/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.cpp @@ -0,0 +1,1072 @@ +#include "StochasticSequenceEditPage.h" + +#include "Pages.h" + +#include "model/StochasticSequence.h" +#include "ui/LedPainter.h" +#include "ui/painters/SequencePainter.h" +#include "ui/painters/WindowPainter.h" + +#include "model/Scale.h" + +#include "os/os.h" + +#include "core/utils/StringBuilder.h" +#include +#include + +enum class ContextAction { + Init, + Copy, + Paste, + Duplicate, + Generate, + Last +}; + +static const ContextMenuModel::Item contextMenuItems[] = { + { "INIT" }, + { "COPY" }, + { "PASTE" }, + { "DUPL" }, + { "GEN" }, +}; + +enum class Function { + Gate = 0, + Retrigger = 1, + Length = 2, + Note = 3, + Condition = 4, +}; + +static const char *functionNames[] = { "GATE", "RETRIG", "LENGTH", "NOTE", "COND" }; + +static const StochasticSequenceListModel::Item quickEditItems[8] = { + StochasticSequenceListModel::Item::Last, + StochasticSequenceListModel::Item::Last, + StochasticSequenceListModel::Item::RunMode, + StochasticSequenceListModel::Item::Divisor, + StochasticSequenceListModel::Item::ResetMeasure, + StochasticSequenceListModel::Item::Scale, + StochasticSequenceListModel::Item::RootNote, + StochasticSequenceListModel::Item::Last +}; + +StochasticSequenceEditPage::StochasticSequenceEditPage(PageManager &manager, PageContext &context) : + BasePage(manager, context) +{ + _stepSelection.setStepCompare([this] (int a, int b) { + auto layer = _project.selectedStochasticSequenceLayer(); + const auto &sequence = _project.selectedStochasticSequence(); + return sequence.step(a).layerValue(layer) == sequence.step(b).layerValue(layer); + }); +} + +void StochasticSequenceEditPage::enter() { + updateMonitorStep(); + auto &sequence = _project.selectedStochasticSequence(); + sequence.setMessage(StochasticSequence::Message::None); + + _showDetail = false; + _section = 0; +} + +void StochasticSequenceEditPage::exit() { + _engine.selectedTrackEngine().as().setMonitorStep(-1); +} + +void StochasticSequenceEditPage::draw(Canvas &canvas) { + WindowPainter::clear(canvas); + + /* Prepare flags shown before mode name (top right header) */ + auto &sequence = _project.selectedStochasticSequence(); + + displayMessage(sequence); + + const char *mode_flags = NULL; + if (sequence.useLoop()) { + const char *st_flag = "L"; + mode_flags = st_flag; + } + + WindowPainter::drawHeader(canvas, _model, _engine, "STEPS", mode_flags); + + WindowPainter::drawActiveFunction(canvas, StochasticSequence::layerName(layer())); + WindowPainter::drawFooter(canvas, functionNames, pageKeyState(), activeFunctionKey()); + + const auto &trackEngine = _engine.selectedTrackEngine().as(); + const auto &scale = sequence.selectedScale(_project.scale()); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + int currentRecordStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentRecordStep() : -1; + + const int stepWidth = Width / StepCount; + const int stepOffset = this->stepOffset(); + + int stepsToDraw = 12; + + // Track Pattern Section on the UI + if (_sectionTracking && _engine.state().running()) { + bool section_change = bool((currentStep) % StepCount == 0); // StepCount is relative to screen + int section_no = int((currentStep) / StepCount); + if (section_change && section_no != _section) { + _section = section_no; + } + } + + for (int i = 0; i < stepsToDraw; ++i) { + int stepIndex = stepOffset + i; + auto &step = sequence.step(stepIndex); + + int x = (i * stepWidth) + ((16 - stepsToDraw)*stepWidth)/2 ; + int y = 20; + + // step index + { + canvas.setColor(_stepSelection[stepIndex] ? Color::Bright : Color::Medium); + FixedStringBuilder<8> str("%d", stepIndex + 1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y - 2, str); + } + + // step gate + canvas.setColor(stepIndex == currentStep ? Color::Bright : Color::Medium); + canvas.drawRect(x + 2, y + 2, stepWidth - 4, stepWidth - 4); + if (step.gate()) { + canvas.setColor(_context.model.settings().userSettings().get(SettingDimSequence)->getValue() ? Color::Low : Color::Bright); + canvas.fillRect(x + 4, y + 4, stepWidth - 8, stepWidth - 8); + } + + // record step + if (stepIndex == currentRecordStep) { + // draw circle + canvas.setColor(step.gate() ? Color::None : Color::Bright); + canvas.fillRect(x + 6, y + 6, stepWidth - 12, stepWidth - 12); + canvas.setColor(Color::Medium); + canvas.hline(x + 7, y + 5, 2); + canvas.hline(x + 7, y + 10, 2); + canvas.vline(x + 5, y + 7, 2); + canvas.vline(x + 10, y + 7, 2); + } + + switch (layer()) { + case Layer::Gate: + break; + case Layer::GateProbability: + SequencePainter::drawProbability( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.gateProbability() + 1, StochasticSequence::GateProbability::Range + ); + break; + case Layer::GateOffset: + SequencePainter::drawOffset( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.gateOffset(), StochasticSequence::GateOffset::Min - 1, StochasticSequence::GateOffset::Max + 1 + ); + break; + case Layer::Retrigger: + SequencePainter::drawRetrigger( + canvas, + x, y + 18, stepWidth, 2, + step.retrigger() + 1, StochasticSequence::Retrigger::Range + ); + break; + case Layer::RetriggerProbability: + SequencePainter::drawProbability( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.retriggerProbability() + 1, StochasticSequence::RetriggerProbability::Range + ); + break; + case Layer::Length: + SequencePainter::drawLength( + canvas, + x + 2, y + 18, stepWidth - 4, 6, + step.length() + 1, StochasticSequence::Length::Range + ); + break; + case Layer::LengthVariationRange: + SequencePainter::drawLengthRange( + canvas, + x + 2, y + 18, stepWidth - 4, 6, + step.length() + 1, step.lengthVariationRange(), StochasticSequence::Length::Range + ); + break; + case Layer::LengthVariationProbability: + SequencePainter::drawProbability( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.lengthVariationProbability() + 1, StochasticSequence::LengthVariationProbability::Range + ); + break; + case Layer::NoteOctave: { + if (step.noteOctave() != 0) { + canvas.setColor(Color::Bright); + } + FixedStringBuilder<8> str; + + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + if (scale.isNotePresent(step.note())) { + canvas.setColor(Color::Bright); + } else { + canvas.setColor(Color::Low); + } + if (step.bypassScale()) { + const Scale &bypassScale = std::ref(Scale::get(0)); + bypassScale.noteName(str, step.note(), rootNote, Scale::Short1); + + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 20, str); + str.reset(); + str("%d", step.noteOctave()); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + scale.noteName(str, step.note(), rootNote, Scale::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + case Layer::NoteOctaveProbability: { + SequencePainter::drawProbability( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.noteOctaveProbability() + 1, StochasticSequence::NoteOctaveProbability::Range + ); + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + if (scale.isNotePresent(step.note())) { + canvas.setColor(Color::Bright); + } else { + canvas.setColor(Color::Low); + } + FixedStringBuilder<8> str; + if (step.bypassScale()) { + const Scale &bypassScale = std::ref(Scale::get(0)); + bypassScale.noteName(str, step.note(), rootNote, Scale::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + scale.noteName(str, step.note(), rootNote, Scale::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + case Layer::NoteVariationProbability: { + SequencePainter::drawProbability( + canvas, + x + 2, y + 18, stepWidth - 4, 2, + step.noteVariationProbability() + 1, StochasticSequence::NoteVariationProbability::Range + ); + int rootNote = sequence.selectedRootNote(_model.project().rootNote()); + + if (scale.isNotePresent(step.note())) { + canvas.setColor(Color::Bright); + } else { + canvas.setColor(Color::Low); + } + + FixedStringBuilder<8> str; + if (step.bypassScale()) { + const Scale &bypassScale = std::ref(Scale::get(0)); + bypassScale.noteName(str, step.note(), rootNote, Scale::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + scale.noteName(str, step.note(), rootNote, Scale::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + case Layer::Slide: + SequencePainter::drawSlide( + canvas, + x + 4, y + 18, stepWidth - 8, 4, + step.slide() + ); + break; + case Layer::Condition: { + canvas.setColor(Color::Bright); + FixedStringBuilder<8> str; + Types::printCondition(str, step.condition(), Types::ConditionFormat::Short1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 20, str); + str.reset(); + Types::printCondition(str, step.condition(), Types::ConditionFormat::Short2); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 27, str); + break; + } + case Layer::StageRepeats: { + canvas.setColor(Bright); + FixedStringBuilder<8> str("x%d", step.stageRepeats()+1); + canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 20, str); + break; + } + case Layer::StageRepeatsMode: { + SequencePainter::drawStageRepeatMode( + canvas, + x + 2, y + 18, stepWidth - 4, 6, + step.stageRepeatMode() + ); + break; + } + case Layer::Last: + break; + } + } + + // handle detail display + + if (_showDetail) { + if (layer() == Layer::Gate || layer() == Layer::Slide || _stepSelection.none()) { + _showDetail = false; + } + if (_stepSelection.isPersisted() && os::ticks() > _showDetailTicks + os::time::ms(500)) { + _showDetail = false; + } + } + + if (_showDetail) { + drawDetail(canvas, sequence.step(_stepSelection.first())); + } + + + +} + +void StochasticSequenceEditPage::updateLeds(Leds &leds) { + const auto &trackEngine = _engine.selectedTrackEngine().as(); + const auto &sequence = _project.selectedStochasticSequence(); + int currentStep = trackEngine.isActiveSequence(sequence) ? trackEngine.currentStep() : -1; + + for (int i = 0; i < 16; ++i) { + int stepIndex = stepOffset() + i; + bool red = (stepIndex == currentStep) || _stepSelection[stepIndex]; + bool green = (stepIndex != currentStep) && (sequence.step(stepIndex).gate() || _stepSelection[stepIndex]); + leds.set(MatrixMap::fromStep(i), red, green); + } + + LedPainter::drawSelectedSequenceSection(leds, _section); + + // show quick edit keys + if (globalKeyState()[Key::Page] && !globalKeyState()[Key::Shift]) { + for (int i = 0; i < 8; ++i) { + int index = MatrixMap::fromStep(i + 8); + leds.unmask(index); + leds.set(index, false, quickEditItems[i] != StochasticSequenceListModel::Item::Last); + leds.mask(index); + } + + for (int i : {4, 5, 6, 15}) { + int index = MatrixMap::fromStep(i); + leds.unmask(index); + leds.set(index, false, true); + leds.mask(index); + } + } +} + +void StochasticSequenceEditPage::keyDown(KeyEvent &event) { + const auto &key = event.key(); + if (key.is(Key::Step15) || key.is(Key::Step14)|| key.is(Key::Step13) || key.is(Key::Step12)) { + return; + } + _stepSelection.keyDown(event, stepOffset()); + updateMonitorStep(); +} + +void StochasticSequenceEditPage::keyUp(KeyEvent &event) { + const auto &key = event.key(); + if (key.is(Key::Step15) || key.is(Key::Step14)|| key.is(Key::Step13) || key.is(Key::Step12)) { + return; + } + _stepSelection.keyUp(event, stepOffset()); + updateMonitorStep(); +} + +void StochasticSequenceEditPage::keyPress(KeyPressEvent &event) { + const auto &key = event.key(); + auto &sequence = _project.selectedStochasticSequence(); + + if (key.isContextMenu()) { + contextShow(); + event.consume(); + return; + } + + if (key.pageModifier() && event.count() == 2) { + contextShow(true); + event.consume(); + return; + } + + if (key.isQuickEdit()) { + quickEdit(key.quickEdit()); + event.consume(); + return; + } + + if (key.pageModifier()) { + + if (key.is(Key::Step4) && !sequence.useLoop() && !sequence.isEmpty()) { + showMessage("Reseed"); + sequence.setReseed(1, false); + event.consume(); + } + + if (key.is(Key::Step5)) { + showMessage("Loop cleared"); + sequence.setClearLoop(true); + event.consume(); + } + + if (key.is(Key::Step6)) { + if (sequence.useLoop()) { + showMessage("Loop off"); + } else { + showMessage("Loop on"); + } + sequence.setUseLoop(); + event.consume(); + } + return; + } + + if (key.is(Key::Step15) || key.is(Key::Step14)|| key.is(Key::Step13) || key.is(Key::Step12)) { + return; + } + + _stepSelection.keyPress(event, stepOffset()); + updateMonitorStep(); + + if (!key.shiftModifier() && key.isStep()) { + int stepIndex = stepOffset() + key.step(); + switch (layer()) { + case Layer::Gate: + sequence.step(stepIndex).toggleGate(); + event.consume(); + break; + default: + break; + } + } + + KeyPressEvent keyPressEvent =_keyPressEventTracker.process(key); + + if (!key.shiftModifier() && key.isStep() && keyPressEvent.count() == 2) { + int stepIndex = stepOffset() + key.step(); + if (layer() != Layer::Gate) { + sequence.step(stepIndex).toggleGate(); + event.consume(); + } + } + + if (key.isFunction()) { + switchLayer(key.function(), key.shiftModifier()); + event.consume(); + } + + if (key.isEncoder()) { + if (!_showDetail && _stepSelection.any() && allSelectedStepsActive()) { + setSelectedStepsGate(false); + } else { + setSelectedStepsGate(true); + } + } +} + +void StochasticSequenceEditPage::encoder(EncoderEvent &event) { + auto &sequence = _project.selectedStochasticSequence(); + + if (!_stepSelection.any()) + { + switch (layer()) + { + case Layer::Gate: + setLayer(event.value() > 0 ? Layer::GateOffset : Layer::GateProbability); + break; + case Layer::GateOffset: + setLayer(event.value() > 0 ? Layer::GateProbability : Layer::Gate); + break; + case Layer::GateProbability: + setLayer(event.value() > 0 ? Layer::Gate : Layer::GateOffset); + break; + case Layer::Retrigger: + setLayer(event.value() > 0 ? Layer::RetriggerProbability : Layer::StageRepeatsMode); + break; + case Layer::RetriggerProbability: + setLayer(event.value() > 0 ? Layer::StageRepeats : Layer::Retrigger); + break; + case Layer::StageRepeats: + setLayer(event.value() > 0 ? Layer::StageRepeatsMode : Layer::RetriggerProbability); + break; + case Layer::StageRepeatsMode: + setLayer(event.value() > 0 ? Layer::Retrigger : Layer::StageRepeats); + break; + case Layer::Length: + setLayer(event.value() > 0 ? Layer::LengthVariationRange : Layer::LengthVariationProbability); + break; + case Layer::LengthVariationRange: + setLayer(event.value() > 0 ? Layer::LengthVariationProbability : Layer::Length); + break; + case Layer::LengthVariationProbability: + setLayer(event.value() > 0 ? Layer::Length : Layer::LengthVariationRange); + break; + case Layer::NoteVariationProbability: + setLayer(event.value() > 0 ? Layer::NoteOctave : Layer::Slide); + break; + case Layer::NoteOctave: + setLayer(event.value() > 0 ? Layer::NoteOctaveProbability : Layer::NoteVariationProbability); + break; + case Layer::NoteOctaveProbability: + setLayer(event.value() > 0 ? Layer::Slide : Layer::NoteOctave); + break; + case Layer::Slide: + setLayer(event.value() > 0 ? Layer::NoteVariationProbability : Layer::NoteOctaveProbability); + break; + default: + break; + } + return; + } + else + { + _showDetail = true; + _showDetailTicks = os::ticks(); + } + + for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + switch (layer()) { + case Layer::Gate: + step.setGate(event.value() > 0); + break; + case Layer::GateProbability: + step.setGateProbability(step.gateProbability() + event.value()); + break; + case Layer::GateOffset: + step.setGateOffset(step.gateOffset() + event.value()); + break; + case Layer::Retrigger: + step.setRetrigger(step.retrigger() + event.value()); + break; + case Layer::RetriggerProbability: + step.setRetriggerProbability(step.retriggerProbability() + event.value()); + break; + case Layer::Length: + step.setLength(step.length() + event.value()); + break; + case Layer::LengthVariationRange: + step.setLengthVariationRange(step.lengthVariationRange() + event.value()); + break; + case Layer::LengthVariationProbability: + step.setLengthVariationProbability(step.lengthVariationProbability() + event.value()); + break; + case Layer::NoteOctave: + step.setNoteOctave(step.noteOctave() + event.value()); + updateMonitorStep(); + break; + case Layer::NoteOctaveProbability: + step.setNoteOctaveProbability(step.noteOctaveProbability() + event.value()); + break; + case Layer::NoteVariationProbability: + step.setNoteVariationProbability(step.noteVariationProbability() + event.value()); + updateMonitorStep(); + break; + case Layer::Slide: + step.setSlide(event.value() > 0); + break; + case Layer::Condition: + step.setCondition(ModelUtils::adjustedEnum(step.condition(), event.value())); + break; + case Layer::StageRepeats: + step.setStageRepeats(step.stageRepeats() + event.value()); + break; + case Layer::StageRepeatsMode: + step.setStageRepeatsMode( + static_cast( + step.stageRepeatMode() + event.value() + ) + ); + break; + case Layer::Last: + break; + } + } + } + + event.consume(); +} + +void StochasticSequenceEditPage::midi(MidiEvent &event) { + if (!_engine.recording() && layer() == Layer::NoteVariationProbability && _stepSelection.any()) { + auto &trackEngine = _engine.selectedTrackEngine().as(); + auto &sequence = _project.selectedStochasticSequence(); + const auto &scale = sequence.selectedScale(_project.scale()); + const auto &message = event.message(); + + if (message.isNoteOn()) { + float volts = (message.note() - 60) * (1.f / 12.f); + int note = scale.noteFromVolts(volts); + + for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + step.setNote(note); + step.setGate(true); + } + } + + trackEngine.setMonitorStep(_stepSelection.first()); + updateMonitorStep(); + } + } +} + +void StochasticSequenceEditPage::switchLayer(int functionKey, bool shift) { + + auto engine = _engine.selectedTrackEngine().as(); + if (shift) { + switch (Function(functionKey)) { + case Function::Gate: + setLayer(Layer::GateProbability); + break; + case Function::Retrigger: + if (engine.playMode() == Types::PlayMode::Free) { + setLayer(Layer::StageRepeats); + break; + } + + case Function::Length: + if (engine.playMode() == Types::PlayMode::Free) { + setLayer(Layer::StageRepeatsMode); + } + break; + case Function::Note: + setLayer(Layer::NoteVariationProbability); + break; + case Function::Condition: + setLayer(Layer::Condition); + break; + } + return; + } + + switch (Function(functionKey)) { + case Function::Gate: + switch (layer()) { + case Layer::Gate: + setLayer(Layer::GateOffset); + break; + case Layer::GateOffset: + setLayer(Layer::GateProbability); + break; + default: + setLayer(Layer::Gate); + break; + } + break; + case Function::Retrigger: + switch (layer()) { + case Layer::Retrigger: + setLayer(Layer::RetriggerProbability); + break; + case Layer::RetriggerProbability: + if (engine.playMode() == Types::PlayMode::Free) { + setLayer(Layer::StageRepeats); + break; + } + + case Layer::StageRepeats: + if (engine.playMode() == Types::PlayMode::Free) { + setLayer(Layer::StageRepeatsMode); + break; + } + default: + setLayer(Layer::Retrigger); + break; + } + break; + case Function::Length: + switch (layer()) { + case Layer::Length: + setLayer(Layer::LengthVariationRange); + break; + case Layer::LengthVariationRange: + setLayer(Layer::LengthVariationProbability); + break; + default: + setLayer(Layer::Length); + break; + } + break; + case Function::Note: + switch (layer()) { + case Layer::NoteVariationProbability: + setLayer(Layer::NoteOctave); + break; + case Layer::NoteOctave: + setLayer(Layer::NoteOctaveProbability); + break; + case Layer::NoteOctaveProbability: + setLayer(Layer::Slide); + break; + case Layer::Slide: + setLayer(Layer::NoteVariationProbability); + default: + setLayer(Layer::NoteVariationProbability); + break; + } + break; + case Function::Condition: + setLayer(Layer::Condition); + break; + } +} + +int StochasticSequenceEditPage::activeFunctionKey() { + switch (layer()) { + case Layer::Gate: + case Layer::GateProbability: + case Layer::GateOffset: + return 0; + case Layer::Retrigger: + case Layer::RetriggerProbability: + case Layer::StageRepeats: + case Layer::StageRepeatsMode: + return 1; + case Layer::Length: + case Layer::LengthVariationRange: + case Layer::LengthVariationProbability: + return 2; + case Layer::NoteOctave: + case Layer::NoteVariationProbability: + case Layer::NoteOctaveProbability: + case Layer::Slide: + return 3; + case Layer::Condition: + return 4; + case Layer::Last: + break; + } + + return -1; +} + +void StochasticSequenceEditPage::updateMonitorStep() { + auto &trackEngine = _engine.selectedTrackEngine().as(); + + // TODO should we monitor an all layers not just note? + if (layer() == Layer::NoteVariationProbability && !_stepSelection.isPersisted() && _stepSelection.any()) { + trackEngine.setMonitorStep(_stepSelection.first()); + } else { + trackEngine.setMonitorStep(-1); + } +} + +void StochasticSequenceEditPage::drawDetail(Canvas &canvas, const StochasticSequence::Step &step) { + + + FixedStringBuilder<16> str; + + WindowPainter::drawFrame(canvas, 64, 16, 128, 32); + + canvas.setBlendMode(BlendMode::Set); + canvas.setColor(Color::Bright); + canvas.vline(64 + 32, 16, 32); + + canvas.setFont(Font::Small); + str("%d", _stepSelection.first() + 1); + if (_stepSelection.count() > 1) { + str("*"); + } + canvas.drawTextCentered(64, 16, 32, 32, str); + + canvas.setFont(Font::Tiny); + + switch (layer()) { + case Layer::Gate: + case Layer::Slide: + break; + case Layer::GateProbability: + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.gateProbability() + 1, StochasticSequence::GateProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.gateProbability()) / (StochasticSequence::GateProbability::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::GateOffset: + SequencePainter::drawOffset( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.gateOffset(), StochasticSequence::GateOffset::Min - 1, StochasticSequence::GateOffset::Max + 1 + ); + str.reset(); + str("%.1f%%", 100.f * step.gateOffset() / float(StochasticSequence::GateOffset::Max + 1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::Retrigger: + SequencePainter::drawRetrigger( + canvas, + 64+ 32 + 8, 32 - 4, 64 - 16, 8, + step.retrigger() + 1, StochasticSequence::Retrigger::Range + ); + str.reset(); + str("%d", step.retrigger() + 1); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::RetriggerProbability: + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.retriggerProbability() + 1, StochasticSequence::RetriggerProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.retriggerProbability()) / (StochasticSequence::RetriggerProbability::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::Length: + SequencePainter::drawLength( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.length() + 1, StochasticSequence::Length::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.length() + 1.f) / StochasticSequence::Length::Range); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::LengthVariationRange: + SequencePainter::drawLengthRange( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.length() + 1, step.lengthVariationRange(), StochasticSequence::Length::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.lengthVariationRange()) / (StochasticSequence::Length::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::LengthVariationProbability: + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.lengthVariationProbability() + 1, StochasticSequence::LengthVariationProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.lengthVariationProbability()) / (StochasticSequence::LengthVariationProbability::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::NoteOctave: + str.reset(); + str("%d", step.noteOctave()); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + break; + case Layer::NoteOctaveProbability: + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.noteOctaveProbability() + 1, StochasticSequence::NoteOctaveProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.noteOctaveProbability()) / (StochasticSequence::NoteOctaveProbability::Range-1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::NoteVariationProbability: + SequencePainter::drawProbability( + canvas, + 64 + 32 + 8, 32 - 4, 64 - 16, 8, + step.noteVariationProbability() + 1, StochasticSequence::NoteVariationProbability::Range + ); + str.reset(); + str("%.1f%%", 100.f * (step.noteVariationProbability()) / (StochasticSequence::NoteVariationProbability::Range -1)); + canvas.setColor(Color::Bright); + canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); + break; + case Layer::Condition: + str.reset(); + Types::printCondition(str, step.condition(), Types::ConditionFormat::Long); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 96, 32, str); + break; + case Layer::StageRepeats: + str.reset(); + str("x%d", step.stageRepeats()+1); + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + break; + case Layer::StageRepeatsMode: + str.reset(); + switch (step.stageRepeatMode()) { + case StochasticSequence::Each: + str("EACH"); + break; + case StochasticSequence::First: + str("FIRST"); + break; + case StochasticSequence::Middle: + str("MIDDLE"); + break; + case StochasticSequence::Last: + str("LAST"); + break; + case StochasticSequence::Odd: + str("ODD"); + break; + case StochasticSequence::Even: + str("EVEN"); + break; + case StochasticSequence::Triplets: + str("TRIPLET"); + break; + case StochasticSequence::Random: + str("RANDOM"); + break; + + default: + break; + } + canvas.setFont(Font::Small); + canvas.drawTextCentered(64 + 32, 16, 64, 32, str); + break; + case Layer::Last: + break; + } +} + +void StochasticSequenceEditPage::contextShow(bool doubleClick) { + showContextMenu(ContextMenu( + contextMenuItems, + int(ContextAction::Last), + [&] (int index) { contextAction(index); }, + [&] (int index) { return contextActionEnabled(index); }, doubleClick + )); +} + +void StochasticSequenceEditPage::contextAction(int index) { + switch (ContextAction(index)) { + case ContextAction::Init: + initSequence(); + break; + case ContextAction::Copy: + copySequence(); + break; + case ContextAction::Paste: + pasteSequence(); + break; + case ContextAction::Duplicate: + duplicateSequence(); + break; + case ContextAction::Generate: + generateSequence(); + break; + case ContextAction::Last: + break; + } +} + +bool StochasticSequenceEditPage::contextActionEnabled(int index) const { + switch (ContextAction(index)) { + case ContextAction::Paste: + return _model.clipBoard().canPasteStochasticSequenceSteps(); + default: + return true; + } +} + +void StochasticSequenceEditPage::initSequence() { + _project.selectedStochasticSequence().clearSteps(); + showMessage("STEPS INITIALIZED"); +} + +void StochasticSequenceEditPage::copySequence() { + _model.clipBoard().copyStochasticSequenceSteps(_project.selectedStochasticSequence(), _stepSelection.selected()); + showMessage("STEPS COPIED"); +} + +void StochasticSequenceEditPage::pasteSequence() { + _model.clipBoard().pasteStochasticSequenceSteps(_project.selectedStochasticSequence(), _stepSelection.selected()); + showMessage("STEPS PASTED"); +} + +void StochasticSequenceEditPage::duplicateSequence() { + _project.selectedStochasticSequence().duplicateSteps(); + showMessage("STEPS DUPLICATED"); +} + +void StochasticSequenceEditPage::generateSequence() { + _manager.pages().generatorSelect.show([this] (bool success, Generator::Mode mode) { + if (success) { + auto builder = _builderContainer.create(_project.selectedStochasticSequence(), layer()); + + if (_stepSelection.none()) { + _stepSelection.selectAll(); + } + + auto generator = Generator::execute(mode, *builder, _stepSelection.selected()); + if (generator) { + _manager.pages().generator.show(generator, &_stepSelection); + } + } + }); +} + + + +void StochasticSequenceEditPage::quickEdit(int index) { + + _listModel.setSequence(&_project.selectedStochasticSequence()); + if (quickEditItems[index] != StochasticSequenceListModel::Item::Last) { + _manager.pages().quickEdit.show(_listModel, int(quickEditItems[index])); + } +} + +bool StochasticSequenceEditPage::allSelectedStepsActive() const { + const auto &sequence = _project.selectedStochasticSequence(); + for (size_t stepIndex = 0; stepIndex < _stepSelection.size(); ++stepIndex) { + if (_stepSelection[stepIndex] && !sequence.step(stepIndex).gate()) { + return false; + } + } + return true; +} + +void StochasticSequenceEditPage::setSelectedStepsGate(bool gate) { + auto &sequence = _project.selectedStochasticSequence(); + for (size_t stepIndex = 0; stepIndex < _stepSelection.size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + sequence.step(stepIndex).setGate(gate); + } + } +} + +void StochasticSequenceEditPage::displayMessage(StochasticSequence &sequence) { + FixedStringBuilder<16> str; + if (sequence.message() != StochasticSequence::Message::None) { + + switch (sequence.message()) { + case StochasticSequence::Message::LoopOn: + str("Loop On"); + break; + case StochasticSequence::Message::LoopOff: + str("Loop Off"); + break; + case StochasticSequence::Message::Cleared: + str("Loop cleared"); + break; + case StochasticSequence::Message::ReSeed: + str("Reseed"); + break; + default: + break; + } + showMessage(str); + sequence.setMessage(StochasticSequence::Message::None); + } +} diff --git a/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.h b/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.h new file mode 100644 index 00000000..6b13b605 --- /dev/null +++ b/src/apps/sequencer/ui/pages/StochasticSequenceEditPage.h @@ -0,0 +1,79 @@ +#pragma once + +#include "BasePage.h" + +#include "ui/StepSelection.h" +#include "ui/model/StochasticSequenceListModel.h" + +#include "engine/generators/SequenceBuilder.h" +#include "ui/KeyPressEventTracker.h" + +#include "core/utils/Container.h" + +class StochasticSequenceEditPage : public BasePage { +public: + StochasticSequenceEditPage(PageManager &manager, PageContext &context); + + virtual void enter() override; + virtual void exit() override; + + virtual void draw(Canvas &canvas) override; + virtual void updateLeds(Leds &leds) override; + + virtual void keyDown(KeyEvent &event) override; + virtual void keyUp(KeyEvent &event) override; + virtual void keyPress(KeyPressEvent &event) override; + virtual void encoder(EncoderEvent &event) override; + virtual void midi(MidiEvent &event) override; + +private: + typedef StochasticSequence::Layer Layer; + + static const int StepCount = 16; + + int stepOffset() const { return _section * StepCount; } + + void switchLayer(int functionKey, bool shift); + int activeFunctionKey(); + + void updateMonitorStep(); + void drawDetail(Canvas &canvas, const StochasticSequence::Step &step); + + void contextShow(bool doubleClick = false); + void contextAction(int index); + bool contextActionEnabled(int index) const; + + void initSequence(); + void copySequence(); + void pasteSequence(); + void duplicateSequence(); + void tieNotes(); + void generateSequence(); + + void quickEdit(int index); + + bool allSelectedStepsActive() const; + void setSelectedStepsGate(bool gate); + + void setSectionTracking(bool track); + + void displayMessage(StochasticSequence &sequence); + + StochasticSequence::Layer layer() const { return _project.selectedStochasticSequenceLayer(); }; + void setLayer(StochasticSequence::Layer layer) { _project.setSelectedStochasticSequenceLayer(layer); } + + int _section = 0; + bool _sectionTracking = false; + bool _showDetail; + uint32_t _showDetailTicks; + + KeyPressEventTracker _keyPressEventTracker; + + + StochasticSequenceListModel _listModel; + + StepSelection _stepSelection; + + Container _builderContainer; + +}; diff --git a/src/apps/sequencer/ui/pages/StochasticSequencePage.cpp b/src/apps/sequencer/ui/pages/StochasticSequencePage.cpp new file mode 100644 index 00000000..87f935ae --- /dev/null +++ b/src/apps/sequencer/ui/pages/StochasticSequencePage.cpp @@ -0,0 +1,143 @@ +#include "StochasticSequencePage.h" + +#include "Pages.h" + +#include "ui/LedPainter.h" +#include "ui/painters/WindowPainter.h" + +#include "core/utils/StringBuilder.h" + +enum class ContextAction { + Init, + Copy, + Paste, + Duplicate, + Route, + Last +}; + +static const ContextMenuModel::Item contextMenuItems[] = { + { "INIT" }, + { "COPY" }, + { "PASTE" }, + { "DUPL" }, + { "ROUTE" }, +}; + + +StochasticSequencePage::StochasticSequencePage(PageManager &manager, PageContext &context) : + ListPage(manager, context, _listModel) +{} + +void StochasticSequencePage::enter() { + _listModel.setSequence(&_project.selectedStochasticSequence()); +} + +void StochasticSequencePage::exit() { + _listModel.setSequence(nullptr); +} + +void StochasticSequencePage::draw(Canvas &canvas) { + WindowPainter::clear(canvas); + WindowPainter::drawHeader(canvas, _model, _engine, "SEQUENCE"); + WindowPainter::drawActiveFunction(canvas, Track::trackModeName(_project.selectedTrack().trackMode())); + WindowPainter::drawFooter(canvas); + + ListPage::draw(canvas); +} + +void StochasticSequencePage::updateLeds(Leds &leds) { + ListPage::updateLeds(leds); +} + +void StochasticSequencePage::keyPress(KeyPressEvent &event) { + const auto &key = event.key(); + + if (key.isContextMenu()) { + contextShow(); + event.consume(); + return; + } + + if (key.pageModifier()) { + return; + } + + if (!event.consumed()) { + ListPage::keyPress(event); + } + + if (key.isEncoder()) { + auto row = ListPage::selectedRow(); + if (row == 3) { + _listModel.setSelectedScale(_project.scale()); + } + } +} + +void StochasticSequencePage::contextShow() { + showContextMenu(ContextMenu( + contextMenuItems, + int(ContextAction::Last), + [&] (int index) { contextAction(index); }, + [&] (int index) { return contextActionEnabled(index); } + )); +} + +void StochasticSequencePage::contextAction(int index) { + switch (ContextAction(index)) { + case ContextAction::Init: + initSequence(); + break; + case ContextAction::Copy: + copySequence(); + break; + case ContextAction::Paste: + pasteSequence(); + break; + case ContextAction::Duplicate: + duplicateSequence(); + break; + case ContextAction::Route: + initRoute(); + break; + case ContextAction::Last: + break; + } +} + +bool StochasticSequencePage::contextActionEnabled(int index) const { + switch (ContextAction(index)) { + case ContextAction::Paste: + return _model.clipBoard().canPasteStochasticSequence(); + case ContextAction::Route: + return _listModel.routingTarget(selectedRow()) != Routing::Target::None; + default: + return true; + } +} + +void StochasticSequencePage::initSequence() { + _project.selectedStochasticSequence().clear(); + showMessage("SEQUENCE INITIALIZED"); +} + +void StochasticSequencePage::copySequence() { + _model.clipBoard().copyStochasticSequence(_project.selectedStochasticSequence()); + showMessage("SEQUENCE COPIED"); +} + +void StochasticSequencePage::pasteSequence() { + _model.clipBoard().pasteStochasticSequence(_project.selectedStochasticSequence()); + showMessage("SEQUENCE PASTED"); +} + +void StochasticSequencePage::duplicateSequence() { + if (_project.selectedTrack().duplicatePattern(_project.selectedPatternIndex())) { + showMessage("SEQUENCE DUPLICATED"); + } +} + +void StochasticSequencePage::initRoute() { + _manager.pages().top.editRoute(_listModel.routingTarget(selectedRow()), _project.selectedTrackIndex()); +} diff --git a/src/apps/sequencer/ui/pages/StochasticSequencePage.h b/src/apps/sequencer/ui/pages/StochasticSequencePage.h new file mode 100644 index 00000000..e7062ed8 --- /dev/null +++ b/src/apps/sequencer/ui/pages/StochasticSequencePage.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ListPage.h" + +#include "ui/model/StochasticSequenceListModel.h" + +class StochasticSequencePage : public ListPage { +public: + StochasticSequencePage(PageManager &manager, PageContext &context); + + virtual void enter() override; + virtual void exit() override; + + virtual void draw(Canvas &canvas) override; + virtual void updateLeds(Leds &leds) override; + + virtual void keyPress(KeyPressEvent &event) override; + +private: + void contextShow(); + void contextAction(int index); + bool contextActionEnabled(int index) const; + + void initSequence(); + void copySequence(); + void pasteSequence(); + void duplicateSequence(); + void initRoute(); + + StochasticSequenceListModel _listModel; +}; diff --git a/src/apps/sequencer/ui/pages/TopPage.cpp b/src/apps/sequencer/ui/pages/TopPage.cpp index 74ef7975..70618aa9 100644 --- a/src/apps/sequencer/ui/pages/TopPage.cpp +++ b/src/apps/sequencer/ui/pages/TopPage.cpp @@ -228,6 +228,9 @@ void TopPage::setSequencePage() { case Track::TrackMode::MidiCv: setMainPage(pages.track); break; + case Track::TrackMode::Stochastic: + setMainPage(pages.stochasticSequence); + break; case Track::TrackMode::Last: break; } @@ -246,6 +249,9 @@ void TopPage::setSequenceEditPage() { case Track::TrackMode::MidiCv: setMainPage(pages.track); break; + case Track::TrackMode::Stochastic: + setMainPage(pages.stochasticSequenceEdit); + break; case Track::TrackMode::Last: break; } diff --git a/src/apps/sequencer/ui/pages/TopPage.h b/src/apps/sequencer/ui/pages/TopPage.h index 5a58f26b..25183390 100644 --- a/src/apps/sequencer/ui/pages/TopPage.h +++ b/src/apps/sequencer/ui/pages/TopPage.h @@ -10,18 +10,6 @@ class TopPage : public BasePage { public: TopPage(PageManager &manager, PageContext &context); - void init(); - - void editRoute(Routing::Target target, int trackIndex); - - virtual void updateLeds(Leds &leds) override; - - virtual void keyDown(KeyEvent &event) override; - virtual void keyUp(KeyEvent &event) override; - virtual void keyPress(KeyPressEvent &event) override; - virtual void encoder(EncoderEvent &event) override; - -private: enum Mode : uint8_t { // main modes Project = PageKeyMap::Project, @@ -45,7 +33,23 @@ class TopPage : public BasePage { Last, }; + void init(); + + void editRoute(Routing::Target target, int trackIndex); + + virtual void updateLeds(Leds &leds) override; + + virtual void keyDown(KeyEvent &event) override; + virtual void keyUp(KeyEvent &event) override; + virtual void keyPress(KeyPressEvent &event) override; + virtual void encoder(EncoderEvent &event) override; + + void setMode(Mode mode); +private: + + + void setMainPage(Page &page); void setSequencePage(); diff --git a/src/apps/sequencer/ui/pages/TrackPage.cpp b/src/apps/sequencer/ui/pages/TrackPage.cpp index 9fce99d2..74e3495c 100644 --- a/src/apps/sequencer/ui/pages/TrackPage.cpp +++ b/src/apps/sequencer/ui/pages/TrackPage.cpp @@ -92,7 +92,14 @@ void TrackPage::keyPress(KeyPressEvent &event) { _project.selectedTrack().midiCvTrack().setName(text); } }); - break; + break; + case Track::TrackMode::Stochastic: + _manager.pages().textInput.show("NAME:", _stochasticTrack->name(), StochasticTrack::NameLength, [this] (bool result, const char *text) { + if (result) { + _project.selectedTrack().stochasticTrack().setName(text); + } + }); + break; case Track::TrackMode::Last: break; } @@ -123,6 +130,11 @@ void TrackPage::setTrack(Track &track) { newListModel = &_midiCvTrackListModel; _midiCvTrack = &track.midiCvTrack(); break; + case Track::TrackMode::Stochastic: + _stochasticTrackListModel.setTrack(track.stochasticTrack()); + newListModel = &_stochasticTrackListModel; + _stochasticTrack = &track.stochasticTrack(); + break; case Track::TrackMode::Last: ASSERT(false, "invalid track mode"); break; diff --git a/src/apps/sequencer/ui/pages/TrackPage.h b/src/apps/sequencer/ui/pages/TrackPage.h index f3d7289c..890a6c0b 100644 --- a/src/apps/sequencer/ui/pages/TrackPage.h +++ b/src/apps/sequencer/ui/pages/TrackPage.h @@ -5,6 +5,8 @@ #include "ui/model/NoteTrackListModel.h" #include "ui/model/CurveTrackListModel.h" #include "ui/model/MidiCvTrackListModel.h" +#include "ui/model/StochasticTrackListModel.h" + class TrackPage : public ListPage { public: @@ -34,10 +36,12 @@ class TrackPage : public ListPage { NoteTrackListModel _noteTrackListModel; CurveTrackListModel _curveTrackListModel; MidiCvTrackListModel _midiCvTrackListModel; + StochasticTrackListModel _stochasticTrackListModel; Track *_track; NoteTrack *_noteTrack; CurveTrack *_curveTrack; MidiCvTrack *_midiCvTrack; + StochasticTrack *_stochasticTrack; }; diff --git a/src/apps/sequencer/ui/pages/UserScalePage.cpp b/src/apps/sequencer/ui/pages/UserScalePage.cpp index 2c5eb71b..874e5020 100644 --- a/src/apps/sequencer/ui/pages/UserScalePage.cpp +++ b/src/apps/sequencer/ui/pages/UserScalePage.cpp @@ -49,6 +49,8 @@ void UserScalePage::draw(Canvas &canvas) { void UserScalePage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); + functionShortcuts(event); + if (key.isContextMenu()) { contextShow(); event.consume(); @@ -215,3 +217,24 @@ void UserScalePage::loadUserScaleFromSlot(int slot) { _engine.resume(); }); } + +void UserScalePage::functionShortcuts(KeyPressEvent event) { + { + const auto &key = event.key(); + if (key.isFunction() && key.is(Key::F0) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Project); + } + + if (key.isFunction() && key.is(Key::F1) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Layout); + } + + if (key.isFunction() && key.is(Key::F2) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::Routing); + } + + if (key.isFunction() && key.is(Key::F3) && event.count() == 2) { + _manager.pages().top.setMode(TopPage::Mode::MidiOutput); + } + } +} \ No newline at end of file diff --git a/src/apps/sequencer/ui/pages/UserScalePage.h b/src/apps/sequencer/ui/pages/UserScalePage.h index 74e72651..6ca3dd28 100644 --- a/src/apps/sequencer/ui/pages/UserScalePage.h +++ b/src/apps/sequencer/ui/pages/UserScalePage.h @@ -32,6 +32,8 @@ class UserScalePage : public ListPage { void saveUserScaleToSlot(int slot); void loadUserScaleFromSlot(int slot); + void functionShortcuts(KeyPressEvent event); + int _selectedIndex; UserScale *_userScale; diff --git a/src/apps/sequencer/ui/painters/SequencePainter.cpp b/src/apps/sequencer/ui/painters/SequencePainter.cpp index c15fc0cf..47181954 100644 --- a/src/apps/sequencer/ui/painters/SequencePainter.cpp +++ b/src/apps/sequencer/ui/painters/SequencePainter.cpp @@ -1,6 +1,7 @@ #include "SequencePainter.h" #include "core/gfx/Canvas.h" #include "model/NoteSequence.h" +#include "model/StochasticSequence.h" #include void SequencePainter::drawLoopStart(Canvas &canvas, int x, int y, int w) { @@ -158,6 +159,53 @@ void SequencePainter::drawStageRepeatMode(Canvas &canvas, int x, int y, int w, i } } +void SequencePainter::drawStageRepeatMode(Canvas &canvas, int x, int y, int w, int h, StochasticSequence::StageRepeatMode mode) { + canvas.setBlendMode(BlendMode::Set); + canvas.setColor(Bright); + int bottom = y + h - 1; + std::bitset<4> enabled; + x += (w - 8) / 2; + + switch (mode) { + case StochasticSequence::StageRepeatMode::Each: + enabled = 0xf; + break; + case StochasticSequence::StageRepeatMode::First: + enabled = 0x1; + break; + case StochasticSequence::StageRepeatMode::Middle: + enabled = 0x1 << 2; + break; + case StochasticSequence::StageRepeatMode::Last: + enabled = 0x8; + break; + case StochasticSequence::StageRepeatMode::Odd: + enabled = 0x5; + break; + case StochasticSequence::StageRepeatMode::Even: + enabled = 0x5 << 1; + break; + case StochasticSequence::StageRepeatMode::Triplets: + enabled = 0x9; + break; + case StochasticSequence::StageRepeatMode::Random: + enabled = 0xf; + break; + } + + for (int i = 0; i < 4; i++) { + if (mode == StochasticSequence::StageRepeatMode::Random) { + canvas.drawText(x-1, y+4, "????"); + } else { + if (((enabled >> i) & mask) == 1) { + canvas.vline(x + 2 * i, y, h); + } else { + canvas.hline(x + 2 * i, bottom, 1); + } + } + } +} + void SequencePainter::drawSequenceProgress(Canvas &canvas, int x, int y, int w, int h, float progress) { if (progress < 0.f) { return; diff --git a/src/apps/sequencer/ui/painters/SequencePainter.h b/src/apps/sequencer/ui/painters/SequencePainter.h index 9e0d9f5c..e95746f8 100644 --- a/src/apps/sequencer/ui/painters/SequencePainter.h +++ b/src/apps/sequencer/ui/painters/SequencePainter.h @@ -2,6 +2,8 @@ #include "core/gfx/Canvas.h" #include "model/NoteSequence.h" +#include "model/StochasticSequence.h" + class SequencePainter { public: @@ -17,6 +19,8 @@ class SequencePainter { static void drawBypassScale(Canvas &canvas, int x, int y, int w, int h, bool active); static void drawStageRepeatMode(Canvas &canvas, int x, int y, int w, int h, NoteSequence::StageRepeatMode mode); + static void drawStageRepeatMode(Canvas &canvas, int x, int y, int w, int h, StochasticSequence::StageRepeatMode mode); + static void drawSequenceProgress(Canvas &canvas, int x, int y, int w, int h, float progress); }; diff --git a/src/platform/sim/sim/TargetTrace.cpp b/src/platform/sim/sim/TargetTrace.cpp index 1175541b..652c3685 100644 --- a/src/platform/sim/sim/TargetTrace.cpp +++ b/src/platform/sim/sim/TargetTrace.cpp @@ -5,6 +5,7 @@ #include "tinyformat.h" #include +#include namespace sim {