diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eecbb5a..84dccb7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: with: submodules: recursive - name: Install Dependencies - run: sudo apt install -y libsdl2-dev libasound2-dev mesa-common-dev python3.7 python3.7-dev + run: sudo apt update && sudo apt install -y libsdl2-dev libasound2-dev mesa-common-dev python3.7 python3.7-dev - name: Setup run: make setup_sim - name: Build diff --git a/README.md b/README.md index 1db2046c..f256e08f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ -![Build Status](https://github.com/westlicht/performer/actions/workflows/ci.yml/badge.svg?branch=master) +Build Status: ![Build Status](https://github.com/jackpf/performer/actions/workflows/ci.yml/badge.svg?branch=master) +Upstream Build Status: ![Upstream Build Status](https://github.com/westlicht/performer/actions/workflows/ci.yml/badge.svg?branch=master) + +# Improvements + +This is a fork of the [original repository](https://github.com/westlicht/performer), with some improvements that I personally find essential. + +To find out more about improvements changes, check the table below. + +| Change | Documentation | +|-------------------- |-------------------------------------------------- | +| Noise reduction | [docs](./doc/improvements/noise-reduction.md) | +| Shape improvements | [docs](./doc/improvements/shape-improvements.md) | +| MIDI improvements | [docs](./doc/improvements/midi-improvements.md) | + +--- original documentation below --- # PER|FORMER diff --git a/doc/improvements/midi-improvements.md b/doc/improvements/midi-improvements.md new file mode 100644 index 00000000..df7ecea5 --- /dev/null +++ b/doc/improvements/midi-improvements.md @@ -0,0 +1,22 @@ +# Improvement: MIDI Improvements + +This change improves MIDI integration of the performer. + +## Program Change Messages + +The performer can now send and receive program change messages. + +Note: + +- Program change messages must be enabled in the project settings +- Program change messages are only sent when changing all patterns to the same pattern (changing patterns individually will not trigger program change messages) +- Program change messages are currently always sent on channel 0 +- Program change messages are sent slightly before the end of the sequence in sync mode (this allows the receiving hardware some time to acknowledge the program change request) + +## Always Sync Patterns + +Enabled in project settings. This isn't strictly a MIDI improvement, but it works well with program changes. + +When enabled, pattern changes are synced by default, rather than having to press **SYNC** before selecting a pattern. + +Not very exciting, but very useful for live performance when you want things to always be synced. \ No newline at end of file diff --git a/doc/improvements/noise-reduction.md b/doc/improvements/noise-reduction.md new file mode 100644 index 00000000..6d211911 --- /dev/null +++ b/doc/improvements/noise-reduction.md @@ -0,0 +1,30 @@ +# Improvement: Noise Reduction + +The OLED screen that the performer uses is inherently noisy. It generates noise on the power bus, and also on its outputs, +meaning any other module that is connected to the performer will pick up this noise (particularly VCOs & filters). + +It's not very noticeable, unless you amplify, compress or overdrive the output, but it's definitely there. + +The issue with some further information on the original original repository is [here](https://github.com/westlicht/performer/issues/304). + +The problem is not entirely solvable by software (since it's a hardware issue), but basically the amount of noise is directly +related to the pixels that are lit up on the OLED screen. + +This change provides some settings to reduce the noise as much as possible (I think I achieved around 20dB reduction). + +## Guide + +1. Open up the **System** page (**PAGE** + **SYSTEM**) +2. Press **SETTINGS** (**F5**) + +## Settings + +| Setting | Description | +|-------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Brightness | Reduces the overall brightness of the display. Lower brightness will reduce noise. | +| Screensaver | Turns the screen off after the specified time, or "off" to disable. When the screen is off, the noise is completely gone. This is useful in a studio setting, when you'd like to record something completely noise-free. | +| Wake Mode | Determines when the screen should "wake up" from the screensaver. "always" will wake the screen on any key press. "required" allows the screen to stay off when performing tasks that don't really require the screen, e.g. modifying a sequence (since the LEDs describe the current sequence). This is also a pseudo "screen-less" mode for the sequencer, which can be nice :) | +| Dim Sequence | The sequence page is particularly noisy if a lot of steps are enabled from the steps currently displayed on-screen. With dim sequence on, the squares are slightly darker than normal, which reduces noise on the sequence page substantially. | + +One change that is not added as a setting is changing the footer UI elements (the text relating the the **F** keys) +so that the active function is bold instead of highlighted. This reduces noise drastically. \ No newline at end of file diff --git a/doc/improvements/shape-improvements.md b/doc/improvements/shape-improvements.md new file mode 100644 index 00000000..d1c470d5 --- /dev/null +++ b/doc/improvements/shape-improvements.md @@ -0,0 +1,36 @@ +# Improvement: Shape Improvements + +This change improves curve sequencing. + +## Curves spanning multiple steps + +Adds the ability to make a curve span over multiple steps. + +### Guide + +1. Open a curve track +2. Select multiple steps (**SHIFT** + **Sx**) +3. Hold **SHIFT** and turn the encoder to select a shape + +The shape will be spread across the selected steps. + +Not holding **SHIFT** will result in the original behaviour - all selected steps are set to the same shape. + +### Extra features + +- While holding **SHIFT** with multiple steps selected, pressing the encoder will reverse the shape across the selected steps + +## Extra shapes + +Adds more shapes. + +Added shapes: + +- Half ramp up: same as ramp up, but half a step +- Half ramp down: same as ramp down, but half a step +- Half exponential up: same as exponential up, but half a step +- Half exponential down: same as exponential down, but half a step +- Half log up: same as log up, but half a step +- Half log down: same as log down, but half a step +- Half smooth up: same as smooth up, but half a step +- Half smooth down: same as smooth down, but half a step \ No newline at end of file diff --git a/src/apps/sequencer/CMakeLists.txt b/src/apps/sequencer/CMakeLists.txt index a45c4fc3..23de6069 100644 --- a/src/apps/sequencer/CMakeLists.txt +++ b/src/apps/sequencer/CMakeLists.txt @@ -48,6 +48,7 @@ set(sources model/Track.cpp model/Types.cpp model/UserScale.cpp + model/UserSettings.cpp # ui ui/Controller.cpp ui/ControllerManager.cpp @@ -56,6 +57,7 @@ set(sources ui/Page.cpp ui/PageManager.cpp ui/Ui.cpp + ui/Screensaver.cpp # ui/controllers/launchpad ui/controllers/launchpad/LaunchpadController.cpp ui/controllers/launchpad/LaunchpadDevice.cpp diff --git a/src/apps/sequencer/Sequencer.cpp b/src/apps/sequencer/Sequencer.cpp index 00f63208..273f2000 100644 --- a/src/apps/sequencer/Sequencer.cpp +++ b/src/apps/sequencer/Sequencer.cpp @@ -80,7 +80,7 @@ static CCMRAM_BSS Profiler profiler; static Model model; static CCMRAM_BSS Engine engine(model, clockTimer, adc, dac, dio, gateOutput, midi, usbMidi); -static CCMRAM_BSS Ui ui(model, engine, lcd, blm, encoder); +static CCMRAM_BSS Ui ui(model, engine, lcd, blm, encoder, model.settings()); static constexpr uint32_t TaskAliveCount = 4; diff --git a/src/apps/sequencer/SequencerApp.h b/src/apps/sequencer/SequencerApp.h index 31b11c8c..5bdc9827 100644 --- a/src/apps/sequencer/SequencerApp.h +++ b/src/apps/sequencer/SequencerApp.h @@ -52,7 +52,7 @@ struct SequencerApp { SequencerApp() : volume(sdCard), engine(model, clockTimer, adc, dac, dio, gateOutput, midi, usbMidi), - ui(model, engine, lcd, blm, encoder) + ui(model, engine, lcd, blm, encoder, model.settings()) { MidiMessage::setPayloadPool(midiMessagePayloadPool, sizeof(midiMessagePayloadPool)); diff --git a/src/apps/sequencer/asteroids/Asteroids.cpp b/src/apps/sequencer/asteroids/Asteroids.cpp index 661b0d23..d0c3b749 100644 --- a/src/apps/sequencer/asteroids/Asteroids.cpp +++ b/src/apps/sequencer/asteroids/Asteroids.cpp @@ -214,7 +214,7 @@ void Player::draw(Canvas &canvas) { if (_killed) { return; } - canvas.setColor(0xf); + canvas.setColor(Color::Bright); Mat3 transform = Mat3::transform2D(_position, _angle, 1.f); drawShape(canvas, transform, playerShape); } @@ -237,7 +237,7 @@ void Asteroid::update(float dt) { } void Asteroid::draw(Canvas &canvas) { - canvas.setColor(0x7); + canvas.setColor(Color::Medium); Mat3 transform = Mat3::transform2D(_position, _angle, _scale); drawShape(canvas, transform, asteroidShapes[_shape]); } @@ -263,7 +263,7 @@ void Projectile::update(float dt) { void Projectile::draw(Canvas &canvas) { const Vec2 &a = _position; Vec2 b = a + _direction * 3.f; - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.line(a.x, a.y, b.x, b.y); } @@ -293,7 +293,7 @@ void Particle::draw(Canvas &canvas) { const Vec2 &a = _position; Vec2 b = a + _direction * 3.f; uint8_t color = static_cast((1.f - _time / _params->lifeTime) * 0xf); - canvas.setColor(color); + canvas.setColorValue(color); canvas.line(a.x, a.y, b.x, b.y); } @@ -456,7 +456,7 @@ void Game::update(float dt, Inputs &inputs, Outputs &outputs) { void Game::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); canvas.setBlendMode(BlendMode::Add); @@ -551,25 +551,25 @@ void Game::divideAsteroid(Asteroid &asteroid) { void Game::drawTexts(Canvas &canvas, const char *title, const char *msg) { canvas.setFont(Font::Small); - drawShadowText(canvas, (ScreenWidth - canvas.textWidth(title)) / 2, 30, 0xf, title); + drawShadowText(canvas, (ScreenWidth - canvas.textWidth(title)) / 2, 30, Color::Bright, title); canvas.setFont(Font::Tiny); - drawShadowText(canvas, (ScreenWidth - canvas.textWidth(msg)) / 2, 44, 0x7, msg); + drawShadowText(canvas, (ScreenWidth - canvas.textWidth(msg)) / 2, 44, Color::Medium, msg); } void Game::drawHUD(Canvas &canvas) { canvas.setFont(Font::Tiny); FixedStringBuilder<16> level("Level %d", _level); - drawShadowText(canvas, 2, 7, 0xf, level); + drawShadowText(canvas, 2, 7, Color::Bright, level); FixedStringBuilder<16> score("Score %d", _player.score()); - drawShadowText(canvas, ScreenWidth - 2 - canvas.textWidth(score), 7, 0xf, score); + drawShadowText(canvas, ScreenWidth - 2 - canvas.textWidth(score), 7, Color::Bright, score); } -void Game::drawShadowText(Canvas &canvas, int x, int y, uint8_t color, const char *str) { +void Game::drawShadowText(Canvas &canvas, int x, int y, Color color, const char *str) { canvas.setBlendMode(BlendMode::Sub); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); for (int dx = -1; dx <= 1; dx += 1) { for (int dy = -1; dy <= 1; dy += 1) { diff --git a/src/apps/sequencer/asteroids/Asteroids.h b/src/apps/sequencer/asteroids/Asteroids.h index e7072a07..65e464eb 100644 --- a/src/apps/sequencer/asteroids/Asteroids.h +++ b/src/apps/sequencer/asteroids/Asteroids.h @@ -285,7 +285,7 @@ class Game { private: void drawTexts(Canvas &canvas, const char *title, const char *msg); void drawHUD(Canvas &canvas); - void drawShadowText(Canvas &canvas, int x, int y, uint8_t color, const char *str); + void drawShadowText(Canvas &canvas, int x, int y, Color color, const char *str); static void initAsteroidShapes(); diff --git a/src/apps/sequencer/engine/Engine.cpp b/src/apps/sequencer/engine/Engine.cpp index 13af90f8..57a00484 100644 --- a/src/apps/sequencer/engine/Engine.cpp +++ b/src/apps/sequencer/engine/Engine.cpp @@ -314,6 +314,21 @@ bool Engine::trackEnginesConsistent() const { return true; } +bool Engine::trackPatternsConsistent() const { + auto playState = _project.playState(); + auto firstTrackState = playState.trackState(0); + + for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { + auto trackState = playState.trackState(trackIndex); + + if (trackState.pattern() != firstTrackState.pattern() + || trackState.requestedPattern() != firstTrackState.requestedPattern()) { + return false; + } + } + return true; +} + bool Engine::sendMidi(MidiPort port, uint8_t cable, const MidiMessage &message) { switch (port) { case MidiPort::Midi: @@ -327,6 +342,33 @@ bool Engine::sendMidi(MidiPort port, uint8_t cable, const MidiMessage &message) return false; } +bool Engine::midiProgramChangesEnabled() { + return _project.midiIntegrationProgramChangesEnabled() + && trackPatternsConsistent() + && !_project.playState().snapshotActive(); +} + +void Engine::sendMidiProgramChange(int programNumber) { + if (_project.midiIntegrationMalekkoEnabled()) { + _midiOutputEngine.sendMalekkoSelectHandshake(0); + } + if (_project.midiIntegrationProgramChangesEnabled()) { + _midiOutputEngine.sendProgramChange(0, _project.midiProgramOffset() + programNumber); + } + if (_project.midiIntegrationMalekkoEnabled()) { + _midiOutputEngine.sendMalekkoSelectReleaseHandshake(0); + } +} + +void Engine::sendMidiProgramSave(int programNumber) { + if (_project.midiIntegrationMalekkoEnabled()) { + _midiOutputEngine.sendMalekkoSaveHandshake(0); + } + if (_project.midiIntegrationProgramChangesEnabled()) { + _midiOutputEngine.sendProgramChange(0, _project.midiProgramOffset() + programNumber); + } +} + void Engine::showMessage(const char *text, uint32_t duration) { if (_messageHandler) { _messageHandler(text, duration); @@ -445,6 +487,22 @@ void Engine::updatePlayState(bool ticked) { bool handleSyncedRequests = _tick % syncDivisor() == 0; bool handleSongAdvance = ticked && _tick > 0 && _tick % measureDivisor() == 0; + bool withinPreHandleRange = (_tick + 192) % syncDivisor() < 192; + if (withinPreHandleRange && _pendingPreHandle == PreHandleNone) { + _pendingPreHandle = PreHandlePending; + } else if (!withinPreHandleRange && _pendingPreHandle != PreHandleNone) { + _pendingPreHandle = PreHandleNone; + } + + // send initial program change if we haven't sent it already + // means that when the sequencer initially starts, it will sync connected devices to the same pattern + // only works when all patterns are equal + // we also send a program change if the midi program offset setting is updated + if (midiProgramChangesEnabled() && (!_midiHasSentInitialPgmChange || _project.midiProgramOffset() != _midiLastInitialProgramOffset)) { + sendMidiProgramChange(playState.trackState(0).pattern()); + _midiHasSentInitialPgmChange = true; + _midiLastInitialProgramOffset = _project.midiProgramOffset(); + } // handle mute & pattern requests @@ -476,6 +534,15 @@ void Engine::updatePlayState(bool ticked) { // clear requests trackState.clearRequests(muteRequests | patternRequests); } + + bool shouldSendPgmChange = !_preSendMidiPgmChange && changedPatterns; + bool shouldPreSendPgmChange = _preSendMidiPgmChange && ((changedPatterns && !playState.hasSyncedRequests()) + || (_pendingPreHandle == PreHandlePending && playState.hasSyncedRequests())); + + if (midiProgramChangesEnabled() && (shouldSendPgmChange || shouldPreSendPgmChange)) { + sendMidiProgramChange(playState.trackState(0).requestedPattern()); + _pendingPreHandle = PreHandleComplete; + } } // handle song requests @@ -535,28 +602,55 @@ void Engine::updatePlayState(bool ticked) { } // handle song slot change - - if (songState.playing() && handleSongAdvance) { + if (songState.playing()) { const auto &slot = song.slot(songState.currentSlot()); int currentSlot = songState.currentSlot(); int currentRepeat = songState.currentRepeat(); - if (currentRepeat + 1 < slot.repeats()) { - // next repeat - songState.setCurrentRepeat(currentRepeat + 1); - } else { - // next slot - songState.setCurrentRepeat(0); - if (currentSlot + 1 < song.slotCount()) { - songState.setCurrentSlot(currentSlot + 1); - } else { - songState.setCurrentSlot(0); + // send program changes when advancing pattern in song mode + if (ticked && ((_preSendMidiPgmChange && _pendingPreHandle == PreHandlePending) || (!_preSendMidiPgmChange && handleSongAdvance))) { + if (currentRepeat + 1 >= slot.repeats()) { + auto nextSlot = song.slot(currentSlot + 1 < song.slotCount() ? currentSlot + 1 : 0); + bool nextSlotPatternsEqual = true; + int firstPattern = nextSlot.pattern(0); + + for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { + if (nextSlot.pattern(trackIndex) != firstPattern) { + nextSlotPatternsEqual = false; + break; + } + } + + if (midiProgramChangesEnabled() && nextSlotPatternsEqual) { + sendMidiProgramChange(firstPattern); + } } - // update patterns - activateSongSlot(song.slot(songState.currentSlot())); - for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { - _trackEngines[trackIndex]->restart(); + // TODO There is a possible race condition with synced patterns here + // If song mode is active, we pre-send the program change, + // and a user syncs a pattern change after it was pre-sent + // we will not send the program change for the synced pattern change + _pendingPreHandle = PreHandleComplete; + } + + if (handleSongAdvance) { + if (currentRepeat + 1 < slot.repeats()) { + // next repeat + songState.setCurrentRepeat(currentRepeat + 1); + } else { + // next slot + songState.setCurrentRepeat(0); + if (currentSlot + 1 < song.slotCount()) { + songState.setCurrentSlot(currentSlot + 1); + } else { + songState.setCurrentSlot(0); + } + + // update patterns + activateSongSlot(song.slot(songState.currentSlot())); + for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { + _trackEngines[trackIndex]->restart(); + } } } } @@ -657,6 +751,22 @@ void Engine::receiveMidi(MidiPort port, uint8_t cable, const MidiMessage &messag return; } + // handle program changes + if (message.isProgramChange() && _project.midiIntegrationProgramChangesEnabled()) { + auto &playState = _project.playState(); + // if requested pattern > 16, we wrap around and start from the beginning + // this allows other gear that may send fixed program changes based on the pattern + // to still select some sequence on the performer on patterns > 16 + int requestedPattern = message.programNumber() % CONFIG_PATTERN_COUNT; + + for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { + playState.selectTrackPattern(trackIndex, requestedPattern, PlayState::ExecuteType::Immediate); + } + + // Forward program changes to output + _midiOutputEngine.sendProgramChange(0, requestedPattern); + } + // let midi learn inspect messages (except from virtual CV/Gate messages) if (port != MidiPort::CvGate) { _midiLearn.receiveMidi(port, message); diff --git a/src/apps/sequencer/engine/Engine.h b/src/apps/sequencer/engine/Engine.h index 8ac6d9ef..7f2be066 100644 --- a/src/apps/sequencer/engine/Engine.h +++ b/src/apps/sequencer/engine/Engine.h @@ -144,11 +144,15 @@ class Engine : private Clock::Listener { MidiLearn &midiLearn() { return _midiLearn; } bool trackEnginesConsistent() const; + bool trackPatternsConsistent() const; bool sendMidi(MidiPort port, uint8_t cable, const MidiMessage &message); void setMidiReceiveHandler(MidiReceiveHandler handler) { _midiReceiveHandler = handler; } void setUsbMidiConnectHandler(UsbMidiConnectHandler handler) { _usbMidiConnectHandler = handler; } void setUsbMidiDisconnectHandler(UsbMidiDisconnectHandler handler) { _usbMidiDisconnectHandler = handler; } + bool midiProgramChangesEnabled(); + void sendMidiProgramChange(int programNumber); + void sendMidiProgramSave(int programNumber); // message handling void showMessage(const char *text, uint32_t duration = 1000); @@ -242,6 +246,20 @@ class Engine : private Clock::Listener { int8_t lastTrack = -1; } _midiMonitoring; + // TODO Could be a setting if needed + static const bool _preSendMidiPgmChange = true; + bool _midiHasSentInitialPgmChange = false; + int _midiLastInitialProgramOffset = -1; + // State machine for when to pre-handle (midi) events + // Allows us to handle pre-handle events even if they are submitted after the pre-handle tick + // (then we process them immediately) + enum PreHandle { + PreHandleNone, + PreHandlePending, + PreHandleComplete, + }; + PreHandle _pendingPreHandle = PreHandleNone; + // gate output overrides bool _gateOutputOverride = false; uint8_t _gateOutputOverrideValue = 0; diff --git a/src/apps/sequencer/engine/MidiOutputEngine.cpp b/src/apps/sequencer/engine/MidiOutputEngine.cpp index d9c5b53f..e7781de8 100644 --- a/src/apps/sequencer/engine/MidiOutputEngine.cpp +++ b/src/apps/sequencer/engine/MidiOutputEngine.cpp @@ -148,6 +148,26 @@ void MidiOutputEngine::sendCv(int trackIndex, float cv) { } } +void MidiOutputEngine::sendProgramChange(int channel, int programNumber) { + auto pgmChangeMessage = MidiMessage::makeProgramChange(channel, programNumber); + + sendMidi(MidiPort::Midi, pgmChangeMessage); + sendMidi(MidiPort::UsbMidi, pgmChangeMessage); +} + +void MidiOutputEngine::sendMalekkoSelectHandshake(int channel) { + sendMidi(MidiPort::Midi, MidiMessage::makeControlChange(channel, 20, 127)); + sendMidi(MidiPort::Midi, MidiMessage::makeControlChange(channel, 16, 64)); +} + +void MidiOutputEngine::sendMalekkoSelectReleaseHandshake(int channel) { + sendMidi(MidiPort::Midi, MidiMessage::makeControlChange(channel, 20, 0)); +} + +void MidiOutputEngine::sendMalekkoSaveHandshake(int channel) { + sendMidi(MidiPort::Midi, MidiMessage::makeControlChange(channel, 16, 127)); +} + void MidiOutputEngine::resetOutput(int outputIndex) { auto &outputState = _outputStates[outputIndex]; diff --git a/src/apps/sequencer/engine/MidiOutputEngine.h b/src/apps/sequencer/engine/MidiOutputEngine.h index fd4b30e4..50460cf3 100644 --- a/src/apps/sequencer/engine/MidiOutputEngine.h +++ b/src/apps/sequencer/engine/MidiOutputEngine.h @@ -24,6 +24,12 @@ class MidiOutputEngine { void sendGate(int trackIndex, bool gate); void sendSlide(int trackIndex, bool slide); void sendCv(int trackIndex, float cv); + void sendProgramChange(int channel, int programNumber); + + // Malekko integration + void sendMalekkoSelectHandshake(int channel); + void sendMalekkoSelectReleaseHandshake(int channel); + void sendMalekkoSaveHandshake(int channel); private: struct OutputState { diff --git a/src/apps/sequencer/intro/Intro.cpp b/src/apps/sequencer/intro/Intro.cpp index d671ea99..626b3b34 100644 --- a/src/apps/sequencer/intro/Intro.cpp +++ b/src/apps/sequencer/intro/Intro.cpp @@ -51,9 +51,9 @@ void Intro::update(float dt) { void Intro::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); Vec3 eye(0.f, 0.f, -15.f); Vec3 target(0.f); @@ -65,7 +65,7 @@ void Intro::draw(Canvas &canvas) { Vec2 positions[8]; canvas.setBlendMode(BlendMode::Add); - canvas.setColor(0xa); + canvas.setColor(Color::MediumBright); for (int instance = -2; instance <= 2; ++instance) { Mat4 modelMatrix = Mat4::translate(Vec3(instance * 6.f, 0.f, 0.f)) * Mat4::rotXYZ(Vec3((_time + instance) * 0.3, (_time + instance) * 0.7, (_time + instance) * 1.3)); diff --git a/src/apps/sequencer/model/Curve.cpp b/src/apps/sequencer/model/Curve.cpp index 63322a68..56a7b328 100644 --- a/src/apps/sequencer/model/Curve.cpp +++ b/src/apps/sequencer/model/Curve.cpp @@ -15,14 +15,6 @@ static float high(float x) { return 1.f; } -static float stepUp(float x) { - return x < 0.5f ? 0.f : 1.f; -} - -static float stepDown(float x) { - return x < 0.5f ? 1.f : 0.f; -} - static float rampUp(float x) { return x; } @@ -55,6 +47,38 @@ static float smoothDown(float x) { return 1.f - x * x * (3.f - 2.f * x); } +static float rampUpHalf(float x) { + return x < 0.5f ? rampUp(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float rampDownHalf(float x) { + return x < 0.5f ? rampDown(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float expUpHalf(float x) { + return x < 0.5f ? expUp(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float expDownHalf(float x) { + return x < 0.5f ? expDown(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float logUpHalf(float x) { + return x < 0.5f ? logUp(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float logDownHalf(float x) { + return x < 0.5f ? logDown(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float smoothUpHalf(float x) { + return x < 0.5f ? smoothUp(std::fmod(x * 2.f, 1.f)) : 0.f; +} + +static float smoothDownHalf(float x) { + return x < 0.5f ? smoothDown(std::fmod(x * 2.f, 1.f)) : 0.f; +} + static float triangle(float x) { return (x < 0.5f ? x : 1.f - x) * 2.f; } @@ -63,6 +87,14 @@ static float bell(float x) { return 0.5f - 0.5f * std::cos(x * TwoPi); } +static float stepUp(float x) { + return x < 0.5f ? 0.f : 1.f; +} + +static float stepDown(float x) { + return x < 0.5f ? 1.f : 0.f; +} + static float expDown2x(float x) { return x < 1.f ? expDown(std::fmod(x * 2.f, 1.f)) : 0.f; } @@ -76,23 +108,31 @@ static float expDown4x(float x) { } static Curve::Function functions[] = { - &low, - &high, - &stepUp, - &stepDown, - &rampUp, - &rampDown, - &expUp, - &expDown, - &logUp, - &logDown, - &smoothUp, - &smoothDown, - &triangle, - &bell, - &expDown2x, - &expDown3x, - &expDown4x, + &low, + &high, + &rampUp, + &rampDown, + &expUp, + &expDown, + &logUp, + &logDown, + &smoothUp, + &smoothDown, + &rampUpHalf, + &rampDownHalf, + &expUpHalf, + &expDownHalf, + &logUpHalf, + &logDownHalf, + &smoothUpHalf, + &smoothDownHalf, + &triangle, + &bell, + &stepUp, + &stepDown, + &expDown2x, + &expDown3x, + &expDown4x, }; Curve::Function Curve::function(Type type) { @@ -101,4 +141,4 @@ Curve::Function Curve::function(Type type) { float Curve::eval(Type type, float x) { return functions[type](x); -} +} \ No newline at end of file diff --git a/src/apps/sequencer/model/Curve.h b/src/apps/sequencer/model/Curve.h index e81a5c07..8b27d976 100644 --- a/src/apps/sequencer/model/Curve.h +++ b/src/apps/sequencer/model/Curve.h @@ -7,8 +7,6 @@ class Curve { enum Type { Low, High, - StepUp, - StepDown, RampUp, RampDown, ExpUp, @@ -17,8 +15,18 @@ class Curve { LogDown, SmoothUp, SmoothDown, + RampUpHalf, + RampDownHalf, + ExpUpHalf, + ExpDownHalf, + LogUpHalf, + LogDownHalf, + SmoothUpHalf, + SmoothDownHalf, Triangle, Bell, + StepUp, + StepDown, ExpDown2x, ExpDown3x, ExpDown4x, @@ -29,4 +37,3 @@ class Curve { static float eval(Type type, float x); }; - diff --git a/src/apps/sequencer/model/Project.cpp b/src/apps/sequencer/model/Project.cpp index 8118849d..e8d0fffd 100644 --- a/src/apps/sequencer/model/Project.cpp +++ b/src/apps/sequencer/model/Project.cpp @@ -33,11 +33,14 @@ void Project::clear() { setSwing(50); setTimeSignature(TimeSignature()); setSyncMeasure(1); + setAlwaysSyncPatterns(false); setScale(0); setRootNote(0); setMonitorMode(Types::MonitorMode::Always); setRecordMode(Types::RecordMode::Overdub); setMidiInputMode(Types::MidiInputMode::All); + setMidiIntegrationMode(Types::MidiIntegrationMode::None); + setMidiProgramOffset(0); setCvGateInput(Types::CvGateInput::Off); setCurveCvInput(Types::CurveCvInput::Off); @@ -105,11 +108,14 @@ void Project::write(VersionedSerializedWriter &writer) const { writer.write(_swing.base); _timeSignature.write(writer); writer.write(_syncMeasure); + writer.write(_alwaysSyncPatterns); writer.write(_scale); writer.write(_rootNote); writer.write(_monitorMode); writer.write(_recordMode); writer.write(_midiInputMode); + writer.write(_midiIntegrationMode); + writer.write(_midiProgramOffset); _midiInputSource.write(writer); writer.write(_cvGateInput); writer.write(_curveCvInput); @@ -145,6 +151,9 @@ bool Project::read(VersionedSerializedReader &reader) { _timeSignature.read(reader); } reader.read(_syncMeasure); + if (reader.dataVersion() >= ProjectVersion::Version32) { + reader.read(_alwaysSyncPatterns); + } reader.read(_scale); reader.read(_rootNote); reader.read(_monitorMode, ProjectVersion::Version30); @@ -153,6 +162,10 @@ bool Project::read(VersionedSerializedReader &reader) { reader.read(_midiInputMode); _midiInputSource.read(reader); } + if (reader.dataVersion() >= ProjectVersion::Version32) { + reader.read(_midiIntegrationMode); + reader.read(_midiProgramOffset); + } reader.read(_cvGateInput, ProjectVersion::Version6); reader.read(_curveCvInput, ProjectVersion::Version11); diff --git a/src/apps/sequencer/model/Project.h b/src/apps/sequencer/model/Project.h index 7a088ede..1f0f5f54 100644 --- a/src/apps/sequencer/model/Project.h +++ b/src/apps/sequencer/model/Project.h @@ -124,6 +124,22 @@ class Project { str("%d %s", syncMeasure(), syncMeasure() > 1 ? "bars" : "bar"); } + // always sync + + bool alwaysSyncPatterns() const { return _alwaysSyncPatterns; } + void setAlwaysSyncPatterns(bool alwaysSync) { + _alwaysSyncPatterns = alwaysSync; + } + + void editAlwaysSyncPatterns(int value, bool shift) { + _alwaysSyncPatterns = value == 1; + } + + void printAlwaysSyncPatterns(StringBuilder &str) const { + if (_alwaysSyncPatterns) str("Always"); + else str("Default"); + } + // scale int scale() const { return _scale; } @@ -219,6 +235,47 @@ class Project { const MidiSourceConfig &midiInputSource() const { return _midiInputSource; } MidiSourceConfig &midiInputSource() { return _midiInputSource; } + // midiIntegrationMode + + void editMidiIntegrationMode(int value, bool shift) { + _midiIntegrationMode = ModelUtils::adjustedEnum(_midiIntegrationMode, value); + } + + void printMidiIntegrationMode(StringBuilder &str) const { + str(Types::midiIntegrationModeName(_midiIntegrationMode)); + } + + void setMidiIntegrationMode(Types::MidiIntegrationMode midiIntegrationMode) { + _midiIntegrationMode = midiIntegrationMode; + } + + bool midiIntegrationProgramChangesEnabled() const { + return _midiIntegrationMode == Types::MidiIntegrationMode::ProgramChanges + || _midiIntegrationMode == Types::MidiIntegrationMode::Malekko; + } + + bool midiIntegrationMalekkoEnabled() const { + return _midiIntegrationMode == Types::MidiIntegrationMode::Malekko; + } + + // midiProgramOffset + + void editMidiProgramOffset(int value, bool shift) { + _midiProgramOffset += value; + } + + void printMidiProgramOffset(StringBuilder &str) const { + str("%d", _midiProgramOffset); + } + + void setMidiProgramOffset(int midiProgramOffset) { + _midiProgramOffset = midiProgramOffset; + } + + int midiProgramOffset() { + return _midiProgramOffset; + } + // cvGateInput Types::CvGateInput cvGateInput() const { return _cvGateInput; } @@ -428,12 +485,15 @@ class Project { Routable _swing; TimeSignature _timeSignature; uint8_t _syncMeasure; + bool _alwaysSyncPatterns; uint8_t _scale; uint8_t _rootNote; Types::RecordMode _recordMode; Types::MonitorMode _monitorMode; Types::MidiInputMode _midiInputMode; MidiSourceConfig _midiInputSource; + Types::MidiIntegrationMode _midiIntegrationMode; + uint8_t _midiProgramOffset; Types::CvGateInput _cvGateInput; Types::CurveCvInput _curveCvInput; diff --git a/src/apps/sequencer/model/ProjectVersion.h b/src/apps/sequencer/model/ProjectVersion.h index 393d395f..30ea5d02 100644 --- a/src/apps/sequencer/model/ProjectVersion.h +++ b/src/apps/sequencer/model/ProjectVersion.h @@ -90,6 +90,9 @@ enum ProjectVersion { // changed MidiCvTrack::VoiceConfig to 8-bit value Version31 = 31, + // added Project::midiIntegrationMode, Project::midiProgramOffset, Project::alwaysSync + Version32 = 32, + // automatically derive latest version Last, Latest = Last - 1, diff --git a/src/apps/sequencer/model/Settings.cpp b/src/apps/sequencer/model/Settings.cpp index de3a7be1..91bdd647 100644 --- a/src/apps/sequencer/model/Settings.cpp +++ b/src/apps/sequencer/model/Settings.cpp @@ -10,10 +10,12 @@ Settings::Settings() { void Settings::clear() { _calibration.clear(); + _userSettings.clear(); } void Settings::write(VersionedSerializedWriter &writer) const { _calibration.write(writer); + _userSettings.write(writer); writer.writeHash(); } @@ -22,6 +24,7 @@ bool Settings::read(VersionedSerializedReader &reader) { clear(); _calibration.read(reader); + _userSettings.read(reader); bool success = reader.checkHash(); if (!success) { diff --git a/src/apps/sequencer/model/Settings.h b/src/apps/sequencer/model/Settings.h index 129a885a..50a90d42 100644 --- a/src/apps/sequencer/model/Settings.h +++ b/src/apps/sequencer/model/Settings.h @@ -2,6 +2,7 @@ #include "Calibration.h" #include "Serialize.h" +#include "UserSettings.h" class Settings { public: @@ -14,6 +15,9 @@ class Settings { const Calibration &calibration() const { return _calibration; } Calibration &calibration() { return _calibration; } + const UserSettings &userSettings() const { return _userSettings; } + UserSettings &userSettings() { return _userSettings; } + void clear(); void write(VersionedSerializedWriter &writer) const; @@ -24,4 +28,5 @@ class Settings { private: Calibration _calibration; + UserSettings _userSettings; }; diff --git a/src/apps/sequencer/model/Types.h b/src/apps/sequencer/model/Types.h index 89884bb8..c66ab758 100644 --- a/src/apps/sequencer/model/Types.h +++ b/src/apps/sequencer/model/Types.h @@ -56,6 +56,24 @@ class Types { Last }; + // MidiIntegrationMode + enum class MidiIntegrationMode : uint8_t { + None, + ProgramChanges, + Malekko, + Last + }; + + static const char *midiIntegrationModeName(MidiIntegrationMode midiIntegrationMode) { + switch (midiIntegrationMode) { + case MidiIntegrationMode::None: return "None"; + case MidiIntegrationMode::ProgramChanges: return "Program Changes"; + case MidiIntegrationMode::Malekko: return "Malekko"; + case MidiIntegrationMode::Last: break; + } + return nullptr; + } + // CvGateInput enum class CvGateInput : uint8_t { diff --git a/src/apps/sequencer/model/UserSettings.cpp b/src/apps/sequencer/model/UserSettings.cpp new file mode 100644 index 00000000..fb33bf2a --- /dev/null +++ b/src/apps/sequencer/model/UserSettings.cpp @@ -0,0 +1,44 @@ +#include "UserSettings.h" + +void UserSettings::set(int key, int value) { + _settings[key]->setValue(value); +} + +void UserSettings::shift(int key, int shift) { + _settings[key]->shiftValue(shift); +} + +BaseSetting *UserSettings::_get(const std::string& key) { + for (auto &setting : _settings) { + if (setting->getKey() == key) { + return setting; + } + } + return nullptr; +} + +BaseSetting *UserSettings::get(int key) { + return _settings[key]; +} + +std::vector UserSettings::all() { + return _settings; +} + +void UserSettings::clear() { + for (auto &setting : _settings) { + setting->reset(); + } +} + +void UserSettings::write(VersionedSerializedWriter &writer) const { + for (auto &setting : _settings) { + setting->write(writer); + } +} + +void UserSettings::read(VersionedSerializedReader &reader) { + for (auto &setting : _settings) { + setting->read(reader); + } +} \ No newline at end of file diff --git a/src/apps/sequencer/model/UserSettings.h b/src/apps/sequencer/model/UserSettings.h new file mode 100644 index 00000000..5a9ccbc3 --- /dev/null +++ b/src/apps/sequencer/model/UserSettings.h @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#define SettingBrightness "brightness" +#define SettingScreensaver "screensaver" +#define SettingWakeMode "wakemode" +#define SettingDimSequence "dimsequence" + +class BaseSetting { +public: + virtual std::string getKey() = 0; + virtual void shiftValue(int shift) = 0; + virtual void setValue(int value) = 0; + virtual std::string getMenuItem() = 0; + virtual std::string getMenuItemKey() = 0; + virtual void read(VersionedSerializedReader &writer) = 0; + virtual void write(VersionedSerializedWriter &writer) = 0; + virtual void reset() = 0; +}; + +template +class Setting : public BaseSetting { +public: + Setting( + std::string key, + std::string menuItem, + std::vector menuItemKeys, + std::vector menuItemValues, + T defaultValue + ) : + _value(defaultValue), + _key(std::move(key)), + _menuItem(std::move(menuItem)), + _menuItemKeys(std::move(menuItemKeys)), + _menuItemValues(std::move(menuItemValues)), + _defaultValue(defaultValue) + {} + + std::string getKey() override { + return _key; + } + + std::string getMenuItem() override { + return _menuItem; + } + + std::string getMenuItemKey() override { + return _menuItemKeys[getCurrentIndex()]; + } + + void setValue(int index) override { + unsigned int validIndex = (index >= 0) ? index : 0; + if (validIndex > _menuItemValues.size() - 1) validIndex = _menuItemValues.size() - 1; + _value = _menuItemValues[validIndex]; + }; + + void shiftValue(int shift) override { + setValue(getCurrentIndex() + shift); + }; + + T &getValue() { + return _value; + }; + + const T &getValue() const { + return _value; + } + + void reset() override { + _value = _defaultValue; + }; + + void read(VersionedSerializedReader &reader) override { + reader.read(getValue()); + }; + + void write(VersionedSerializedWriter &writer) override { + writer.write(getValue()); + }; + +private: + int getCurrentIndex() { + auto it = std::find(_menuItemValues.begin(), _menuItemValues.end(), _value); + return std::distance(_menuItemValues.begin(), it); + } + + T _value; + + std::string _key; + std::string _menuItem; + std::vector _menuItemKeys; + std::vector _menuItemValues; + T _defaultValue; +}; + +class BrightnessSetting : public Setting { +public: + BrightnessSetting() : Setting( + SettingBrightness, + "Brightness", + {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, + {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}, + 1.0 + ) {} +}; + +class ScreensaverSetting : public Setting { +public: + ScreensaverSetting() : Setting( + SettingScreensaver, + "Screensaver", + {"off", "3s", "5s", "10s", "30s", "1m", "10m", "30m"}, + {0, 3000, 5000, 10000, 30000, 60000, 600000, 1800000}, + 0 + ) {} +}; + +class WakeModeSetting : public Setting { +public: + WakeModeSetting() : Setting( + SettingWakeMode, + "Wake Mode", + {"always", "required"}, + {0, 1}, + 0 + ) {} +}; + +class DimSequenceSetting : public Setting { +public: + DimSequenceSetting() : Setting( + SettingDimSequence, + "Dim Sequence", + {"off", "on"}, + {false, true}, + false + ) {} +}; + +class UserSettings { +public: + UserSettings() { + addSetting(new BrightnessSetting()); + addSetting(new ScreensaverSetting()); + addSetting(new WakeModeSetting()); + addSetting(new DimSequenceSetting()); + } + + //---------------------------------------- + // Methods + //---------------------------------------- + + void set(int key, int value); + void shift(int key, int shift); + BaseSetting *get(int key); + template + S *get(std::string key) { return dynamic_cast(_get(key)); } + std::vector all(); + + void clear(); + void write(VersionedSerializedWriter &writer) const; + void read(VersionedSerializedReader &reader); + +private: + std::vector _settings; + + template + void addSetting(Setting *setting) { + _settings.push_back(setting); + } + BaseSetting *_get(const std::string &key); +}; diff --git a/src/apps/sequencer/ui/MessageManager.cpp b/src/apps/sequencer/ui/MessageManager.cpp index cd9a4f0b..fb788c68 100644 --- a/src/apps/sequencer/ui/MessageManager.cpp +++ b/src/apps/sequencer/ui/MessageManager.cpp @@ -35,6 +35,6 @@ void MessageManager::draw(Canvas &canvas) { WindowPainter::drawFrame(canvas, 16, 16, 224, 32); canvas.setFont(Font::Tiny); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(16, 16, 224, 32, _text); } diff --git a/src/apps/sequencer/ui/PageManager.cpp b/src/apps/sequencer/ui/PageManager.cpp index 2da5f749..708fef7e 100644 --- a/src/apps/sequencer/ui/PageManager.cpp +++ b/src/apps/sequencer/ui/PageManager.cpp @@ -74,16 +74,16 @@ int PageManager::fps() const { void PageManager::dispatchEvent(Event &event) { // handle modal page - if (top()->isModal()) { + if (top()->isModal() && !event.consumed()) { top()->dispatchEvent(event); return; } // handle top to bottom for (int i = _pageStackPos; i >= 0; --i) { - _pageStack[i]->dispatchEvent(event); if (event.consumed()) { break; } + _pageStack[i]->dispatchEvent(event); } } diff --git a/src/apps/sequencer/ui/Screensaver.cpp b/src/apps/sequencer/ui/Screensaver.cpp new file mode 100644 index 00000000..fd44bf28 --- /dev/null +++ b/src/apps/sequencer/ui/Screensaver.cpp @@ -0,0 +1,80 @@ +#include "Screensaver.h" + +void Screensaver::on() { + _screenSaved = true; + _canvas.screensaver(); +} + +void Screensaver::off() { + _screenSaved = false; + setScreenOnTicks(0); +} + +bool Screensaver::shouldBeOn() { + // TODO Is tick comparison correct? Seems roughly correct... + return _screenOffAfter > 0 && !_buttonPressed && _screenOnTicks > _screenOffAfter; +} + +void Screensaver::consumeKey(KeyEvent &event) { + consumeKey(event, event.key()); +} + +void Screensaver::consumeKey(KeyPressEvent &event) { + consumeKey(event, event.key()); +} + +void Screensaver::consumeKey(Event &event, Key key) { + if (_screenSaved && key.code() == Key::Code::Encoder) { + event.consume(); + } + + if (_wakeMode == 1 && _screenSaved) { // required + switch(key.code()) { + case Key::Code::Play: + case Key::Code::Shift: + case Key::Code::Left: + case Key::Code::Right: + case Key::Code::Track0 ... Key::Code::Track7: + case Key::Code::Step0 ... Key::Code::Step7: + case Key::Code::Step8 ... Key::Code::Step15: + return; + } + } + + // Don't turn on screensaver if button is held + if (event.type() == Event::Type::KeyDown) { + _buttonPressed = true; + } else if (event.type() == Event::Type::KeyUp) { + _buttonPressed = false; + } + + if (event.type() == Event::Type::KeyPress) { + off(); + } +} + +void Screensaver::consumeEncoder(EncoderEvent &event) { + if (_screenSaved) { + // Don't really want this since it disables + // setting on all steps with the encoder when the screen is saved + // Downside is that we could modify menu items for example + // when the screen is off/first comes on +// event.consume(); + + if (_wakeMode == 1) { // required + return; + } + } + + off(); +} + +void Screensaver::setScreenOnTicks(uint32_t ticks) { + _screenOnTicks = ticks; +} + +void Screensaver::incScreenOnTicks(uint32_t ticks) { + if (_screenOffAfter > 0) { // Prevent flickering when setting screensaver in settings + _screenOnTicks += ticks; + } +} \ No newline at end of file diff --git a/src/apps/sequencer/ui/Screensaver.h b/src/apps/sequencer/ui/Screensaver.h new file mode 100644 index 00000000..d8064843 --- /dev/null +++ b/src/apps/sequencer/ui/Screensaver.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include "Key.h" +#include "Event.h" + +class Screensaver { +public: + Screensaver(Canvas &canvas, uint32_t &screenOffAfter, int &wakeMode) : + _canvas(canvas), + _screenOffAfter(screenOffAfter), + _wakeMode(wakeMode) + {} + + void on(); + void off(); + bool shouldBeOn(); + + void setScreenOnTicks(uint32_t ticks); + void incScreenOnTicks(uint32_t ticks); + + void consumeKey(KeyEvent &event); + void consumeKey(KeyPressEvent &event); + void consumeEncoder(EncoderEvent &event); + +private: + void consumeKey(Event &event, Key key); + + Canvas &_canvas; + bool _screenSaved; + bool _buttonPressed; + uint32_t _screenOnTicks; + uint32_t &_screenOffAfter; + int &_wakeMode; +}; \ No newline at end of file diff --git a/src/apps/sequencer/ui/StepSelection.h b/src/apps/sequencer/ui/StepSelection.h index 950d1ef8..99dfa018 100644 --- a/src/apps/sequencer/ui/StepSelection.h +++ b/src/apps/sequencer/ui/StepSelection.h @@ -130,6 +130,25 @@ class StepSelection { return _first; } + int firstSetIndex() const { + for (size_t i = 0; i < _selected.size(); ++i) { + if (_selected[i]) { + return i; + } + } + return -1; + } + + int lastSetIndex() const { + int last = -1; + for (size_t i = 0; i < _selected.size(); ++i) { + if (_selected[i]) { + last = i; + } + } + return last; + } + bool none() const { return _selected.none(); } @@ -169,15 +188,6 @@ class StepSelection { return other; } - int firstSetIndex() const { - for (size_t i = 0; i < _selected.size(); ++i) { - if (_selected[i]) { - return i; - } - } - return -1; - } - enum class Mode : uint8_t { Immediate, Persist, diff --git a/src/apps/sequencer/ui/Ui.cpp b/src/apps/sequencer/ui/Ui.cpp index 6341a230..67a411be 100644 --- a/src/apps/sequencer/ui/Ui.cpp +++ b/src/apps/sequencer/ui/Ui.cpp @@ -9,18 +9,24 @@ #include "model/Model.h" -Ui::Ui(Model &model, Engine &engine, Lcd &lcd, ButtonLedMatrix &blm, Encoder &encoder) : - _model(model), - _engine(engine), - _lcd(lcd), - _blm(blm), - _encoder(encoder), - _frameBuffer(CONFIG_LCD_WIDTH, CONFIG_LCD_HEIGHT, _frameBufferData), - _canvas(_frameBuffer), - _pageManager(_pages), - _pageContext({ _messageManager, _pageKeyState, _globalKeyState, _model, _engine }), - _pages(_pageManager, _pageContext), - _controllerManager(model, engine) +Ui::Ui(Model &model, Engine &engine, Lcd &lcd, ButtonLedMatrix &blm, Encoder &encoder, Settings &settings) : + _model(model), + _engine(engine), + _lcd(lcd), + _blm(blm), + _encoder(encoder), + _frameBuffer(CONFIG_LCD_WIDTH, CONFIG_LCD_HEIGHT, _frameBufferData), + _canvas(_frameBuffer, settings.userSettings().get(SettingBrightness)->getValue()), + _pageManager(_pages), + _pageContext({ _messageManager, _pageKeyState, _globalKeyState, _model, _engine }), + _pages(_pageManager, _pageContext), + _controllerManager(model, engine), + // TODO pass as arg + _screensaver(Screensaver( + _canvas, + settings.userSettings().get(SettingScreensaver)->getValue(), + settings.userSettings().get(SettingWakeMode)->getValue() + )) { } @@ -84,9 +90,14 @@ void Ui::update() { uint32_t currentTicks = os::ticks(); uint32_t intervalTicks = os::time::ms(1000 / _pageManager.fps()); if (currentTicks - _lastFrameBufferUpdateTicks >= intervalTicks) { - _pageManager.draw(_canvas); - _messageManager.update(); - _messageManager.draw(_canvas); + if (!_screensaver.shouldBeOn()) { + _pageManager.draw(_canvas); + _messageManager.update(); + _messageManager.draw(_canvas); + _screensaver.incScreenOnTicks(intervalTicks); + } else { + _screensaver.on(); + } _lcd.draw(_frameBuffer.data()); _lastFrameBufferUpdateTicks += intervalTicks; } @@ -101,10 +112,10 @@ void Ui::update() { } void Ui::showAssert(const char *filename, int line, const char *msg) { - _canvas.setColor(0); + _canvas.setColor(Color::None); _canvas.fill(); - _canvas.setColor(0xf); + _canvas.setColor(Color::Bright); _canvas.setFont(Font::Small); _canvas.drawText(4, 10, "FATAL ERROR"); @@ -128,10 +139,13 @@ void Ui::handleKeys() { _pageKeyState[event.value()] = isDown; _globalKeyState[event.value()] = isDown; Key key(event.value(), _globalKeyState); + KeyEvent keyEvent(isDown ? Event::KeyDown : Event::KeyUp, key); + _screensaver.consumeKey(keyEvent); _pageManager.dispatchEvent(keyEvent); if (isDown) { KeyPressEvent keyPressEvent = _keyPressEventTracker.process(key); + _screensaver.consumeKey(keyPressEvent); _pageManager.dispatchEvent(keyPressEvent); } } @@ -141,26 +155,29 @@ void Ui::handleEncoder() { Encoder::Event event; while (_encoder.nextEvent(event)) { switch (event) { - case Encoder::Left: - case Encoder::Right: { - EncoderEvent encoderEvent(event == Encoder::Left ? -1 : 1, _pageKeyState[Key::Encoder]); - _pageManager.dispatchEvent(encoderEvent); - break; - } - case Encoder::Down: - case Encoder::Up: { - bool isDown = event == Encoder::Down; - _pageKeyState[Key::Encoder] = isDown ? 1 : 0; - _globalKeyState[Key::Encoder] = isDown ? 1 : 0; - Key key(Key::Encoder, _globalKeyState); - KeyEvent keyEvent(isDown ? Event::KeyDown : Event::KeyUp, key); - _pageManager.dispatchEvent(keyEvent); - if (isDown) { - KeyPressEvent keyPressEvent = _keyPressEventTracker.process(key); - _pageManager.dispatchEvent(keyPressEvent); + case Encoder::Left: + case Encoder::Right: { + EncoderEvent encoderEvent(event == Encoder::Left ? -1 : 1, _pageKeyState[Key::Encoder]); + _screensaver.consumeEncoder(encoderEvent); + _pageManager.dispatchEvent(encoderEvent); + break; + } + case Encoder::Down: + case Encoder::Up: { + bool isDown = event == Encoder::Down; + _pageKeyState[Key::Encoder] = isDown ? 1 : 0; + _globalKeyState[Key::Encoder] = isDown ? 1 : 0; + Key key(Key::Encoder, _globalKeyState); + KeyEvent keyEvent(isDown ? Event::KeyDown : Event::KeyUp, key); + _screensaver.consumeKey(keyEvent); + _pageManager.dispatchEvent(keyEvent); + if (isDown) { + KeyPressEvent keyPressEvent = _keyPressEventTracker.process(key); + _screensaver.consumeKey(keyPressEvent); + _pageManager.dispatchEvent(keyPressEvent); + } + break; } - break; - } } } } diff --git a/src/apps/sequencer/ui/Ui.h b/src/apps/sequencer/ui/Ui.h index 37292d5c..eee9aa6a 100644 --- a/src/apps/sequencer/ui/Ui.h +++ b/src/apps/sequencer/ui/Ui.h @@ -23,12 +23,13 @@ #include "engine/Engine.h" #include "model/Model.h" +#include "Screensaver.h" class Key; class Ui { public: - Ui(Model &model, Engine &engine, Lcd &lcd, ButtonLedMatrix &blm, Encoder &encoder); + Ui(Model &model, Engine &engine, Lcd &lcd, ButtonLedMatrix &blm, Encoder &encoder, Settings &settings); void init(); void update(); @@ -72,4 +73,6 @@ class Ui { ControllerManager _controllerManager; uint32_t _lastControllerUpdateTicks; + + Screensaver _screensaver; }; diff --git a/src/apps/sequencer/ui/model/ProjectListModel.h b/src/apps/sequencer/ui/model/ProjectListModel.h index f6c70983..cbcd295c 100644 --- a/src/apps/sequencer/ui/model/ProjectListModel.h +++ b/src/apps/sequencer/ui/model/ProjectListModel.h @@ -52,11 +52,14 @@ class ProjectListModel : public RoutableListModel { Swing, TimeSignature, SyncMeasure, + AlwaysSync, Scale, RootNote, MonitorMode, RecordMode, MidiInput, + MidiIntegrationMode, + MidiProgramOffset, CvGateInput, CurveCvInput, Last @@ -64,19 +67,22 @@ class ProjectListModel : public RoutableListModel { static const char *itemName(Item item) { switch (item) { - case Name: return "Name"; - case Tempo: return "Tempo"; - case Swing: return "Swing"; - case TimeSignature: return "Time Signature"; - case SyncMeasure: return "Sync Measure"; - case Scale: return "Scale"; - case RootNote: return "Root Note"; - case MonitorMode: return "Monitor Mode"; - case RecordMode: return "Record Mode"; - case MidiInput: return "MIDI Input"; - case CvGateInput: return "CV/Gate Input"; - case CurveCvInput: return "Curve CV Input"; - case Last: break; + case Name: return "Name"; + case Tempo: return "Tempo"; + case Swing: return "Swing"; + case TimeSignature: return "Time Signature"; + case SyncMeasure: return "Sync Measure"; + case AlwaysSync: return "Sync Patterns"; + case Scale: return "Scale"; + case RootNote: return "Root Note"; + case MonitorMode: return "Monitor Mode"; + case RecordMode: return "Record Mode"; + case MidiInput: return "MIDI Input"; + case MidiIntegrationMode: return "MIDI Integr."; + case MidiProgramOffset: return "MIDI Pgm Off."; + case CvGateInput: return "CV/Gate Input"; + case CurveCvInput: return "Curve CV Input"; + case Last: break; } return nullptr; } @@ -102,6 +108,9 @@ class ProjectListModel : public RoutableListModel { case SyncMeasure: _project.printSyncMeasure(str); break; + case AlwaysSync: + _project.printAlwaysSyncPatterns(str); + break; case Scale: _project.printScale(str); break; @@ -117,6 +126,12 @@ class ProjectListModel : public RoutableListModel { case MidiInput: _project.printMidiInput(str); break; + case MidiIntegrationMode: + _project.printMidiIntegrationMode(str); + break; + case MidiProgramOffset: + _project.printMidiProgramOffset(str); + break; case CvGateInput: _project.printCvGateInput(str); break; @@ -144,6 +159,9 @@ class ProjectListModel : public RoutableListModel { case SyncMeasure: _project.editSyncMeasure(value, shift); break; + case AlwaysSync: + _project.editAlwaysSyncPatterns(value, shift); + break; case Scale: _project.editScale(value, shift); break; @@ -159,6 +177,12 @@ class ProjectListModel : public RoutableListModel { case MidiInput: _project.editMidiInput(value, shift); break; + case MidiIntegrationMode: + _project.editMidiIntegrationMode(value, shift); + break; + case MidiProgramOffset: + _project.editMidiProgramOffset(value, shift); + break; case CvGateInput: _project.editCvGateInput(value, shift); break; diff --git a/src/apps/sequencer/ui/model/SettingsListModel.h b/src/apps/sequencer/ui/model/SettingsListModel.h new file mode 100644 index 00000000..f9177855 --- /dev/null +++ b/src/apps/sequencer/ui/model/SettingsListModel.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Config.h" + +#include "ListModel.h" + +#include "model/UserSettings.h" + +class SettingsListModel : public ListModel { +public: + SettingsListModel(UserSettings &userSettings) : + _userSettings(userSettings) + {} + + int rows() const override { + return _userSettings.all().size(); + } + + int columns() const override { + return 2; + } + + void cell(int row, int column, StringBuilder &str) const override { + if (column == 0) { + str("%s", _userSettings.get(row)->getMenuItem().c_str()); + } else if (column == 1) { + str("%s", _userSettings.get(row)->getMenuItemKey().c_str()); + } + } + + void edit(int row, int column, int value, bool shift) override { + if (column == 1) { + _userSettings.shift(row, value); + } + } + +private: + UserSettings &_userSettings; +}; diff --git a/src/apps/sequencer/ui/pages/BusyPage.cpp b/src/apps/sequencer/ui/pages/BusyPage.cpp index 98cb8a22..df7bacd4 100644 --- a/src/apps/sequencer/ui/pages/BusyPage.cpp +++ b/src/apps/sequencer/ui/pages/BusyPage.cpp @@ -4,7 +4,7 @@ static void drawProgressBar(Canvas &canvas, int x, int y, int w, int h, int stripeLength, int stripeOffset) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); int sx = (stripeOffset % stripeLength) - stripeLength; while (sx < w + stripeLength) { @@ -35,7 +35,7 @@ void BusyPage::draw(Canvas &canvas) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(0, 32 - 16, Width, 8, _text); diff --git a/src/apps/sequencer/ui/pages/ConfirmationPage.cpp b/src/apps/sequencer/ui/pages/ConfirmationPage.cpp index 99d105e1..1c3c0515 100644 --- a/src/apps/sequencer/ui/pages/ConfirmationPage.cpp +++ b/src/apps/sequencer/ui/pages/ConfirmationPage.cpp @@ -32,7 +32,7 @@ void ConfirmationPage::draw(Canvas &canvas) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(0, 32 - 4, Width, 8, _text); } diff --git a/src/apps/sequencer/ui/pages/ContextMenuPage.cpp b/src/apps/sequencer/ui/pages/ContextMenuPage.cpp index 86c74779..098599ce 100644 --- a/src/apps/sequencer/ui/pages/ContextMenuPage.cpp +++ b/src/apps/sequencer/ui/pages/ContextMenuPage.cpp @@ -20,10 +20,10 @@ void ContextMenuPage::draw(Canvas &canvas) { const int BarHeight = 12; - canvas.setColor(0x0); + canvas.setColor(Color::None); canvas.fillRect(0, Height - BarHeight - 1, Width, BarHeight + 1); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.hline(0, Height - BarHeight, Width); for (int i = 1; i < 5; ++i) { canvas.vline((Width * i) / 5, Height - BarHeight, BarHeight); @@ -43,7 +43,7 @@ void ContextMenuPage::draw(Canvas &canvas) { // canvas.drawRect(x + (w - iconSize) / 2, 32 + 4, iconSize, iconSize); if (item.title) { - canvas.setColor(enabled ? 0xf : 0x7); + canvas.setColor(enabled ? Color::Bright : Color::Medium); canvas.drawText(x + (w - canvas.textWidth(item.title) + 1) / 2, Height - 4, item.title); } } diff --git a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp index 05a435a2..4f24d3f2 100644 --- a/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp +++ b/src/apps/sequencer/ui/pages/CurveSequenceEditPage.cpp @@ -78,11 +78,30 @@ static void drawGatePattern(Canvas &canvas, int x, int y, int w, int h, int gate int gs = w / 4; int gw = w / 8; for (int i = 0; i < 4; ++i) { - canvas.setColor((gate & (1 << i)) ? 0xf : 0x7); + canvas.setColor((gate & (1 << i)) ? Color::Bright : Color::Medium); canvas.fillRect(x + i * gs, y, gw, h); } } +static std::pair calculateMultiStepShapeMinMax(size_t stepsSelected, + size_t multiStepsProcessed, + int shape, + bool reverse) { + // If shift is pressed, reverse ascension + int m = !reverse ? multiStepsProcessed : stepsSelected - multiStepsProcessed - 1; + + int min, max; + if (shape == 0) { + min = CurveSequence::Min::Min; + max = CurveSequence::Max::Max; + } else { + min = std::ceil(float(m) * CurveSequence::Min::Max / stepsSelected); + max = std::ceil(float(m + 1) * CurveSequence::Max::Max / stepsSelected); + } + + return std::make_pair(min, max); +} + CurveSequenceEditPage::CurveSequenceEditPage(PageManager &manager, PageContext &context) : BasePage(manager, context) { @@ -126,13 +145,13 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { // draw loop points canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); SequencePainter::drawLoopStart(canvas, (sequence.firstStep() - stepOffset) * stepWidth + 1, loopY, stepWidth - 2); SequencePainter::drawLoopEnd(canvas, (sequence.lastStep() - stepOffset) * stepWidth + 1, loopY, stepWidth - 2); // draw grid if (!drawShapeVariation) { - canvas.setColor(0x3); + canvas.setColor(Color::Low); for (int stepIndex = 1; stepIndex < StepCount; ++stepIndex) { int x = stepIndex * stepWidth; for (int y = 0; y <= curveHeight; y += 2) { @@ -142,7 +161,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { } // draw curve - canvas.setColor(0xf); + canvas.setColor(Color::Bright); float lastY = -1.f; float lastYVariation = -1.f; for (int i = 0; i < StepCount; ++i) { @@ -158,13 +177,13 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { // loop if (stepIndex > sequence.firstStep() && stepIndex <= sequence.lastStep()) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.point(x, loopY); } // step index { - canvas.setColor(_stepSelection[stepIndex] ? 0xf : 0x7); + 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); } @@ -173,7 +192,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { { const auto function = Curve::function(Curve::Type(std::min(Curve::Last - 1, step.shape()))); - canvas.setColor(drawShapeVariation ? 0x5 : 0xf); + canvas.setColor(drawShapeVariation ? Color::MediumLow : Color::Bright); canvas.setBlendMode(BlendMode::Add); drawCurve(canvas, x, curveY, stepWidth, curveHeight, lastY, function, min, max); @@ -182,7 +201,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { if (drawShapeVariation) { const auto function = Curve::function(Curve::Type(std::min(Curve::Last - 1, step.shapeVariation()))); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.setBlendMode(BlendMode::Add); drawCurve(canvas, x, curveY, stepWidth, curveHeight, lastYVariation, function, min, max); @@ -203,7 +222,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { case Layer::Min: case Layer::Max: { bool functionPressed = globalKeyState()[MatrixMap::fromFunction(activeFunctionKey())]; - canvas.setColor(0x5); + canvas.setColor(Color::MediumLow); canvas.setBlendMode(BlendMode::Add); if (layer() == Layer::Min || functionPressed) { drawMinMax(canvas, x, curveY, stepWidth, curveHeight, min); @@ -214,7 +233,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { break; } case Layer::Gate: - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.setBlendMode(BlendMode::Set); drawGatePattern(canvas, x, bottomY, stepWidth, 2, step.gate()); break; @@ -232,7 +251,7 @@ void CurveSequenceEditPage::draw(Canvas &canvas) { // draw cursor if (isActiveSequence) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); int x = ((trackEngine.currentStep() - stepOffset) + trackEngine.currentStepFraction()) * stepWidth; canvas.vline(x, curveY, curveHeight); } @@ -310,6 +329,26 @@ void CurveSequenceEditPage::keyPress(KeyPressEvent &event) { return; } + if (key.isEncoder() && layer() == Layer::Shape && globalKeyState()[Key::Shift] && _stepSelection.count() > 1) { + auto firstStep = sequence.step(_stepSelection.firstSetIndex()); + auto lastStep = sequence.step(_stepSelection.lastSetIndex()); + bool isReversed = firstStep.max() > lastStep.max(); + + for (size_t stepIndex = 0, multiStepsProcessed = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + if (_stepSelection[stepIndex]) { + auto &step = sequence.step(stepIndex); + + float min, max; + std::tie(min, max) = calculateMultiStepShapeMinMax(_stepSelection.count(), multiStepsProcessed, sequence.step(_stepSelection.firstSetIndex()).shape(), !isReversed); + + step.setMin(int(min)); + step.setMax(int(max)); + + multiStepsProcessed++; + } + } + } + _stepSelection.keyPress(event, stepOffset()); updateMonitorStep(); @@ -346,13 +385,25 @@ void CurveSequenceEditPage::encoder(EncoderEvent &event) { return; } - for (size_t stepIndex = 0; stepIndex < sequence.steps().size(); ++stepIndex) { + for (size_t stepIndex = 0, multiStepsProcessed = 0; stepIndex < sequence.steps().size(); ++stepIndex) { if (_stepSelection[stepIndex]) { auto &step = sequence.step(stepIndex); bool shift = globalKeyState()[Key::Shift]; switch (layer()) { case Layer::Shape: - step.setShape(step.shape() + event.value()); + if (_stepSelection.count() > 1 && shift) { // Create a multi-step shape + auto &firstStep = sequence.step(_stepSelection.firstSetIndex()); + int firstStepShape = multiStepsProcessed == 0 ? std::max(firstStep.shape() + event.value(), 0) : firstStep.shape(); + step.setShape(firstStepShape); + + int min, max; + std::tie(min, max) = calculateMultiStepShapeMinMax(_stepSelection.count(), multiStepsProcessed, firstStepShape, false); + + step.setMin(min); + step.setMax(max); + } else { + step.setShape(step.shape() + event.value()); + } break; case Layer::ShapeVariation: step.setShapeVariation(step.shapeVariation() + event.value()); @@ -388,6 +439,8 @@ void CurveSequenceEditPage::encoder(EncoderEvent &event) { case Layer::Last: break; } + + multiStepsProcessed++; } } @@ -490,7 +543,7 @@ void CurveSequenceEditPage::drawDetail(Canvas &canvas, const CurveSequence::Step WindowPainter::drawFrame(canvas, 64, 16, 128, 32); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(64 + 32, 16, 32); canvas.setFont(Font::Small); @@ -514,7 +567,7 @@ void CurveSequenceEditPage::drawDetail(Canvas &canvas, const CurveSequence::Step ); str.reset(); str("%.1f%%", 100.f * step.shapeVariationProbability() / 8.f); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Min: @@ -528,7 +581,7 @@ void CurveSequenceEditPage::drawDetail(Canvas &canvas, const CurveSequence::Step ); str.reset(); str("%.1f%%", 100.f * (step.gateProbability() + 1.f) / CurveSequence::GateProbability::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Last: diff --git a/src/apps/sequencer/ui/pages/GeneratorPage.cpp b/src/apps/sequencer/ui/pages/GeneratorPage.cpp index 114bc3a5..a706195b 100644 --- a/src/apps/sequencer/ui/pages/GeneratorPage.cpp +++ b/src/apps/sequencer/ui/pages/GeneratorPage.cpp @@ -54,7 +54,7 @@ void GeneratorPage::draw(Canvas &canvas) { canvas.setFont(Font::Small); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); auto drawValue = [&] (int index, const char *str) { int w = Width / 5; @@ -185,10 +185,10 @@ void GeneratorPage::drawEuclideanGenerator(Canvas &canvas, const EuclideanGenera int y = Height / 2 - stepHeight / 2; for (int i = 0; i < steps; ++i) { - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.drawRect(x + 1, y + 1, stepWidth - 2, stepHeight - 2); if (pattern[i]) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(x + 1, y + 1, stepWidth - 2, stepHeight - 2); } x += stepWidth; @@ -207,9 +207,9 @@ void GeneratorPage::drawRandomGenerator(Canvas &canvas, const RandomGenerator &g for (int i = 0; i < steps; ++i) { int h = stepHeight - 2; int h2 = (h * pattern[i]) / 255; - canvas.setColor(0x3); + canvas.setColor(Color::Low); canvas.drawRect(x + 1, y + 1, stepWidth - 2, h); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.hline(x + 1, y + 1 + h - h2, stepWidth - 2); // canvas.fillRect(x + 1, y + 1 + h - h2 , stepWidth - 2, h2); x += stepWidth; diff --git a/src/apps/sequencer/ui/pages/ListPage.cpp b/src/apps/sequencer/ui/pages/ListPage.cpp index 6000afbf..19a784aa 100644 --- a/src/apps/sequencer/ui/pages/ListPage.cpp +++ b/src/apps/sequencer/ui/pages/ListPage.cpp @@ -101,7 +101,7 @@ void ListPage::drawCell(Canvas &canvas, int row, int column, int x, int y, int w _listModel->cell(row, column, str); canvas.setFont(Font::Small); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(column == int(_edit) && row == _selectedRow ? 0xf : 0x7); + canvas.setColor(column == int(_edit) && row == _selectedRow ? Color::Bright : Color::Medium); canvas.drawText(x, y + 7, str); } diff --git a/src/apps/sequencer/ui/pages/MonitorPage.cpp b/src/apps/sequencer/ui/pages/MonitorPage.cpp index 0362fade..b8da1c94 100644 --- a/src/apps/sequencer/ui/pages/MonitorPage.cpp +++ b/src/apps/sequencer/ui/pages/MonitorPage.cpp @@ -92,7 +92,7 @@ void MonitorPage::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); canvas.setFont(Font::Tiny); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); switch (_mode) { case Mode::CvIn: diff --git a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp index 7472f480..dc616d88 100644 --- a/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp +++ b/src/apps/sequencer/ui/pages/NoteSequenceEditPage.cpp @@ -89,7 +89,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { // draw loop points canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); SequencePainter::drawLoopStart(canvas, (sequence.firstStep() - stepOffset) * stepWidth + 1, loopY, stepWidth - 2); SequencePainter::drawLoopEnd(canvas, (sequence.lastStep() - stepOffset) * stepWidth + 1, loopY, stepWidth - 2); @@ -102,31 +102,31 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { // loop if (stepIndex > sequence.firstStep() && stepIndex <= sequence.lastStep()) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.point(x, loopY); } // step index { - canvas.setColor(_stepSelection[stepIndex] ? 0xf : 0x7); + 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 ? 0xf : 0x7); + canvas.setColor(stepIndex == currentStep ? Color::Bright : Color::Medium); canvas.drawRect(x + 2, y + 2, stepWidth - 4, stepWidth - 4); if (step.gate()) { - canvas.setColor(0xf); + 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() ? 0x0 : 0xf); + canvas.setColor(step.gate() ? Color::None : Color::Bright); canvas.fillRect(x + 6, y + 6, stepWidth - 12, stepWidth - 12); - canvas.setColor(0x7); + 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); @@ -187,7 +187,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { break; case Layer::Note: { int rootNote = sequence.selectedRootNote(_model.project().rootNote()); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); FixedStringBuilder<8> str; scale.noteName(str, step.note(), rootNote, Scale::Short1); canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 20, str); @@ -197,7 +197,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { break; } case Layer::NoteVariationRange: { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); FixedStringBuilder<8> str("%d", step.noteVariationRange()); canvas.drawText(x + (stepWidth - canvas.textWidth(str) + 1) / 2, y + 20, str); break; @@ -217,7 +217,7 @@ void NoteSequenceEditPage::draw(Canvas &canvas) { ); break; case Layer::Condition: { - canvas.setColor(0xf); + 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); @@ -568,7 +568,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & WindowPainter::drawFrame(canvas, 64, 16, 128, 32); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(64 + 32, 16, 32); canvas.setFont(Font::Small); @@ -592,7 +592,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.gateProbability() + 1.f) / NoteSequence::GateProbability::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::GateOffset: @@ -603,7 +603,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * step.gateOffset() / float(NoteSequence::GateOffset::Max + 1)); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Retrigger: @@ -614,7 +614,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%d", step.retrigger() + 1); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::RetriggerProbability: @@ -625,7 +625,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.retriggerProbability() + 1.f) / NoteSequence::RetriggerProbability::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Length: @@ -636,7 +636,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.length() + 1.f) / NoteSequence::Length::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::LengthVariationRange: @@ -647,7 +647,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.lengthVariationRange()) / NoteSequence::Length::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::LengthVariationProbability: @@ -658,7 +658,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.lengthVariationProbability() + 1.f) / NoteSequence::LengthVariationProbability::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Note: @@ -681,7 +681,7 @@ void NoteSequenceEditPage::drawDetail(Canvas &canvas, const NoteSequence::Step & ); str.reset(); str("%.1f%%", 100.f * (step.noteVariationProbability() + 1.f) / NoteSequence::NoteVariationProbability::Range); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawTextCentered(64 + 32 + 64, 32 - 4, 32, 8, str); break; case Layer::Condition: diff --git a/src/apps/sequencer/ui/pages/OverviewPage.cpp b/src/apps/sequencer/ui/pages/OverviewPage.cpp index bc4de4d4..ebc6777b 100644 --- a/src/apps/sequencer/ui/pages/OverviewPage.cpp +++ b/src/apps/sequencer/ui/pages/OverviewPage.cpp @@ -17,15 +17,15 @@ static void drawNoteTrack(Canvas &canvas, int trackIndex, const NoteTrackEngine int x = 64 + i * 8; if (trackEngine.currentStep() == stepIndex) { - canvas.setColor(step.gate() ? 0xf : 0xa); + canvas.setColor(step.gate() ? Color::Bright : Color::MediumBright); canvas.fillRect(x + 1, y + 1, 6, 6); } else { - canvas.setColor(step.gate() ? 0x7 : 0x3); + canvas.setColor(step.gate() ? Color::Medium : Color::Low); canvas.fillRect(x + 1, y + 1, 6, 6); } // if (trackEngine.currentStep() == stepIndex) { - // canvas.setColor(0xf); + // canvas.setColor(Color::Bright); // canvas.drawRect(x + 1, y + 1, 6, 6); // } } @@ -55,7 +55,7 @@ static void drawCurve(Canvas &canvas, int x, int y, int w, int h, float &lastY, static void drawCurveTrack(Canvas &canvas, int trackIndex, const CurveTrackEngine &trackEngine, const CurveSequence &sequence) { canvas.setBlendMode(BlendMode::Add); - canvas.setColor(0xa); + canvas.setColor(Color::MediumBright); int stepOffset = (std::max(0, trackEngine.currentStep()) / 16) * 16; int y = trackIndex * 8; @@ -77,7 +77,7 @@ static void drawCurveTrack(Canvas &canvas, int trackIndex, const CurveTrackEngin if (trackEngine.currentStep() >= 0) { int x = 64 + ((trackEngine.currentStep() - stepOffset) + trackEngine.currentStepFraction()) * 8; canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(x, y + 1, 7); } } @@ -98,7 +98,7 @@ void OverviewPage::draw(Canvas &canvas) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.vline(64 - 3, 0, 64); canvas.vline(64 - 2, 0, 64); @@ -111,22 +111,22 @@ void OverviewPage::draw(Canvas &canvas) { const auto &trackEngine = _engine.trackEngine(trackIndex); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); int y = 5 + trackIndex * 8; // track number / pattern number - canvas.setColor(trackState.mute() ? 0x7 : 0xf); + canvas.setColor(trackState.mute() ? Color::Medium : Color::Bright); canvas.drawText(2, y, FixedStringBuilder<8>("T%d", trackIndex + 1)); canvas.drawText(18, y, FixedStringBuilder<8>("P%d", trackState.pattern() + 1)); // gate output bool gate = _engine.gateOutput() & (1 << trackIndex); - canvas.setColor(gate ? 0xf : 0x7); + canvas.setColor(gate ? Color::Bright : Color::Medium); canvas.fillRect(256 - 48 + 1, trackIndex * 8 + 1, 6, 6); // cv output - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(256 - 32, y, FixedStringBuilder<8>("%.2fV", _engine.cvOutput().channel(trackIndex))); switch (track.trackMode()) { diff --git a/src/apps/sequencer/ui/pages/PatternPage.cpp b/src/apps/sequencer/ui/pages/PatternPage.cpp index 6fca8b4e..e728a488 100644 --- a/src/apps/sequencer/ui/pages/PatternPage.cpp +++ b/src/apps/sequencer/ui/pages/PatternPage.cpp @@ -22,6 +22,7 @@ enum class ContextAction { Copy, Paste, Duplicate, + Save, Last }; @@ -30,6 +31,7 @@ static const ContextMenuModel::Item contextMenuItems[] = { { "COPY" }, { "PASTE" }, { "DUP"}, + { "SAVE PR."}, }; @@ -80,32 +82,32 @@ void PatternPage::draw(Canvas &canvas) { x += 2; - canvas.setColor(trackSelected ? 0xf : 0x7); + canvas.setColor(trackSelected ? Color::Bright : Color::Medium); canvas.drawTextCentered(x, y - 2, w, 8, FixedStringBuilder<8>("T%d", trackIndex + 1)); y += 11; - canvas.setColor(trackEngine.activity() ? 0xf : 0x7); + canvas.setColor(trackEngine.activity() ? Color::Bright : Color::Medium); canvas.drawRect(x, y, w, h); for (int p = 0; p < 16; ++p) { int px = x + (p % 8) * 3 + 2; int py = y + (p / 8) * 3 + 2; if (p == trackState.pattern()) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(px, py, 3, 3); } else if (trackState.hasPatternRequest() && p == trackState.requestedPattern()) { - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(px, py, 3, 3); } else { - canvas.setColor(0x3); + canvas.setColor(Color::Low); canvas.point(px + 1, py + 1); } } y += 5; - canvas.setColor(trackSelected ? 0xf : 0x7); + canvas.setColor(trackSelected ? Color::Bright : Color::Medium); canvas.drawTextCentered(x, y + 10, w, 8, snapshotActive ? "S" : FixedStringBuilder<8>("P%d", trackState.pattern() + 1)); if (trackState.hasPatternRequest() && trackState.pattern() != trackState.requestedPattern()) { @@ -114,7 +116,7 @@ void PatternPage::draw(Canvas &canvas) { } if (playState.hasSyncedRequests() && hasRequested) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.hline(0, 10, _engine.syncFraction() * Width); } } @@ -265,8 +267,11 @@ void PatternPage::keyPress(KeyPressEvent &event) { // use immediate by default // use latched when LATCH is pressed - // use synced when SYNC is pressed - PlayState::ExecuteType executeType = _latching ? PlayState::Latched : (_syncing ? PlayState::Synced : PlayState::Immediate); + // use synced when SYNC is pressed or project set to always sync + PlayState::ExecuteType executeType; + if (_latching) executeType = PlayState::Latched; + else if (_syncing || (_project.alwaysSyncPatterns() && _engine.state().running())) executeType = PlayState::Synced; + else executeType = PlayState::Immediate; bool globalChange = true; for (int trackIndex = 0; trackIndex < CONFIG_TRACK_COUNT; ++trackIndex) { @@ -320,6 +325,9 @@ void PatternPage::contextAction(int index) { case ContextAction::Duplicate: duplicatePattern(); break; + case ContextAction::Save: + sendMidiProgramSave(); + break; case ContextAction::Last: break; } @@ -329,6 +337,8 @@ bool PatternPage::contextActionEnabled(int index) const { switch (ContextAction(index)) { case ContextAction::Paste: return _model.clipBoard().canPastePattern(); + case ContextAction::Save: + return _engine.midiProgramChangesEnabled() && _project.midiIntegrationMalekkoEnabled(); default: return true; } @@ -358,3 +368,10 @@ void PatternPage::duplicatePattern() { showMessage("PATTERN DUPLICATED"); } } + +void PatternPage::sendMidiProgramSave() { + if (_engine.midiProgramChangesEnabled()) { + _engine.sendMidiProgramSave(_project.playState().trackState(0).pattern()); + showMessage("SENT MIDI PROGRAM SAVE"); + } +} diff --git a/src/apps/sequencer/ui/pages/PatternPage.h b/src/apps/sequencer/ui/pages/PatternPage.h index 22bda8ae..5ea30f44 100644 --- a/src/apps/sequencer/ui/pages/PatternPage.h +++ b/src/apps/sequencer/ui/pages/PatternPage.h @@ -30,6 +30,7 @@ class PatternPage : public BasePage { void copyPattern(); void pastePattern(); void duplicatePattern(); + void sendMidiProgramSave(); bool _modal = false; bool _latching = false; diff --git a/src/apps/sequencer/ui/pages/PerformerPage.cpp b/src/apps/sequencer/ui/pages/PerformerPage.cpp index 3b1b80b1..97cb4137 100644 --- a/src/apps/sequencer/ui/pages/PerformerPage.cpp +++ b/src/apps/sequencer/ui/pages/PerformerPage.cpp @@ -59,17 +59,17 @@ void PerformerPage::draw(Canvas &canvas) { x += 8; // draw track number (highlight when fill is active) - canvas.setColor(trackState.fill() ? 0xf : 0x7); + canvas.setColor(trackState.fill() ? Color::Bright : Color::Medium); canvas.drawTextCentered(x, y - 2, w, 8, FixedStringBuilder<8>("T%d", trackIndex + 1)); y += 8; // draw outer rectangle (track activity) - canvas.setColor(trackEngine.activity() ? 0xf : 0x7); + canvas.setColor(trackEngine.activity() ? Color::Bright : Color::Medium); canvas.drawRect(x, y, w, h); // draw mutes and mute requests - canvas.setColor(0xf); + canvas.setColor(Color::Bright); if (trackState.hasMuteRequest() && trackState.mute() != trackState.requestedMute()) { hasRequested = true; canvas.fillRect(x + BorderRequested, y + BorderRequested, w - 2 * BorderRequested, h - 2 * BorderRequested); @@ -82,14 +82,14 @@ void PerformerPage::draw(Canvas &canvas) { // draw fill & fill amount amount bool pressed = pageKeyState()[MatrixMap::fromStep(trackIndex)]; - canvas.setColor(pressed ? 0x7 : 0x3); + canvas.setColor(pressed ? Color::Medium : Color::Low); canvas.fillRect(x, y + h + 6, w, 4); - canvas.setColor(pressed ? 0xf : 0x7); + canvas.setColor(pressed ? Color::Bright : Color::Medium); canvas.fillRect(x, y + h + 6, (trackState.fillAmount() * w) / 100, 4); } if (playState.hasSyncedRequests() && hasRequested) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.hline(0, 10, _engine.syncFraction() * Width); } } diff --git a/src/apps/sequencer/ui/pages/QuickEditPage.cpp b/src/apps/sequencer/ui/pages/QuickEditPage.cpp index 66f7aeb7..cc5fda34 100644 --- a/src/apps/sequencer/ui/pages/QuickEditPage.cpp +++ b/src/apps/sequencer/ui/pages/QuickEditPage.cpp @@ -27,7 +27,7 @@ void QuickEditPage::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); canvas.setFont(Font::Small); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); FixedStringBuilder<16> str; _listModel->cell(_row, 0, str); diff --git a/src/apps/sequencer/ui/pages/RoutingPage.cpp b/src/apps/sequencer/ui/pages/RoutingPage.cpp index 540b862d..4889f008 100644 --- a/src/apps/sequencer/ui/pages/RoutingPage.cpp +++ b/src/apps/sequencer/ui/pages/RoutingPage.cpp @@ -126,7 +126,7 @@ void RoutingPage::drawCell(Canvas &canvas, int row, int column, int x, int y, in ) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(edit() && row == selectedRow() ? 0xf : 0x7); + canvas.setColor(edit() && row == selectedRow() ? Color::Bright : Color::Medium); uint8_t tracks = _editRoute.tracks(); for (int i = 0; i < CONFIG_TRACK_COUNT; ++i) { diff --git a/src/apps/sequencer/ui/pages/SongPage.cpp b/src/apps/sequencer/ui/pages/SongPage.cpp index 0b0a47f9..5c22a8f4 100644 --- a/src/apps/sequencer/ui/pages/SongPage.cpp +++ b/src/apps/sequencer/ui/pages/SongPage.cpp @@ -67,7 +67,7 @@ void SongPage::draw(Canvas &canvas) { int y = tableOriginY; canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); auto isHighlighted = [isShift, selectedTracks] (int colIndex) { return @@ -81,7 +81,7 @@ void SongPage::draw(Canvas &canvas) { { int x = tableOriginX; for (int colIndex = 0; colIndex < 10; ++colIndex) { - canvas.setColor(isHighlighted(colIndex) ? 0xf : 0x7); + canvas.setColor(isHighlighted(colIndex) ? Color::Bright : Color::Medium); canvas.drawTextCentered(x, y, colWidth[colIndex], rowHeight, colHeader[colIndex]); x += colWidth[colIndex]; } @@ -98,13 +98,13 @@ void SongPage::draw(Canvas &canvas) { // draw play cursor if (songState.playing() && slotIndex == songState.currentSlot()) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); SongPainter::drawArrowRight(canvas, x - 4, y, 4, rowHeight); } // draw table cells for (int colIndex = 0; colIndex < 10; ++colIndex) { - canvas.setColor(slotIndex == _selectedSlot && isHighlighted(colIndex) ? 0xf : 0x7); + canvas.setColor(slotIndex == _selectedSlot && isHighlighted(colIndex) ? Color::Bright : Color::Medium); FixedStringBuilder<8> str; if (colIndex == 0) { str("%d", slotIndex + 1); @@ -145,7 +145,7 @@ void SongPage::draw(Canvas &canvas) { uint32_t beat = _engine.tick() / _engine.noteDivisor(); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.setFont(Font::Tiny); canvas.drawTextCentered(8, 10, 32, 10, FixedStringBuilder<16>("%d.%d", beat / beatsPerMeasure + 1, beat % beatsPerMeasure + 1)); @@ -158,7 +158,7 @@ void SongPage::draw(Canvas &canvas) { } if (playState.hasSyncedRequests() && songState.hasPlayRequests()) { - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.hline(0, 10, _engine.syncFraction() * Width); } } diff --git a/src/apps/sequencer/ui/pages/StartupPage.cpp b/src/apps/sequencer/ui/pages/StartupPage.cpp index fdf11c27..4b595a98 100644 --- a/src/apps/sequencer/ui/pages/StartupPage.cpp +++ b/src/apps/sequencer/ui/pages/StartupPage.cpp @@ -28,10 +28,10 @@ void StartupPage::draw(Canvas &canvas) { close(); } - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.setFont(Font::Small); canvas.drawTextCentered(0, 0, Width, 32, "PERFORMER"); diff --git a/src/apps/sequencer/ui/pages/SystemPage.cpp b/src/apps/sequencer/ui/pages/SystemPage.cpp index 9ab510ec..3b13b599 100644 --- a/src/apps/sequencer/ui/pages/SystemPage.cpp +++ b/src/apps/sequencer/ui/pages/SystemPage.cpp @@ -11,11 +11,12 @@ enum Function { Calibration = 0, - Utilities = 3, - Update = 4, + Utilities = 2, + Update = 3, + Settings = 4, }; -static const char *functionNames[] = { "CAL", nullptr, nullptr, "UTILS", "UPDATE" }; +static const char *functionNames[] = { "CAL", nullptr, "UTILS", "UPDATE", "SETTINGS" }; enum CalibrationEditFunction { Auto = 0, @@ -40,7 +41,8 @@ static const ContextMenuModel::Item contextMenuItems[] = { SystemPage::SystemPage(PageManager &manager, PageContext &context) : ListPage(manager, context, _cvOutputListModel), - _settings(context.model.settings()) + _settings(context.model.settings()), + _settingsListModel(_settings.userSettings()) { setOutputIndex(0); } @@ -90,7 +92,7 @@ void SystemPage::draw(Canvas &canvas) { WindowPainter::drawActiveFunction(canvas, "UPDATE"); WindowPainter::drawFooter(canvas, functionNames, pageKeyState(), int(_mode)); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(4, 24, "CURRENT VERSION:"); FixedStringBuilder<16> str("%d.%d.%d", CONFIG_VERSION_MAJOR, CONFIG_VERSION_MINOR, CONFIG_VERSION_REVISION); canvas.drawText(100, 24, str); @@ -103,6 +105,12 @@ void SystemPage::draw(Canvas &canvas) { #endif break; } + case Mode::Settings: { + WindowPainter::drawActiveFunction(canvas, "SETTINGS"); + WindowPainter::drawFooter(canvas, functionNames, pageKeyState(), int(_mode)); + ListPage::draw(canvas); + break; + } } } @@ -151,7 +159,7 @@ void SystemPage::keyPress(KeyPressEvent &event) { const auto &key = event.key(); if (key.isContextMenu()) { - if (_mode == Mode::Calibration) { + if (_mode == Mode::Calibration || _mode == Mode::Settings) { contextShow(); event.consume(); return; @@ -175,6 +183,9 @@ void SystemPage::keyPress(KeyPressEvent &event) { case Function::Update: setMode(Mode::Update); break; + case Function::Settings: + setMode(Mode::Settings); + break; } } } @@ -199,6 +210,9 @@ void SystemPage::keyPress(KeyPressEvent &event) { break; case Mode::Update: break; + case Mode::Settings: + ListPage::keyPress(event); + break; } } @@ -213,6 +227,9 @@ void SystemPage::encoder(EncoderEvent &event) { break; case Mode::Update: break; + case Mode::Settings: + ListPage::encoder(event); + break; } } @@ -225,6 +242,9 @@ void SystemPage::setMode(Mode mode) { case Mode::Utilities: setListModel(_utilitiesListModel); break; + case Mode::Settings: + setListModel(_settingsListModel); + break; default: break; } diff --git a/src/apps/sequencer/ui/pages/SystemPage.h b/src/apps/sequencer/ui/pages/SystemPage.h index 48c9e86a..b80c0205 100644 --- a/src/apps/sequencer/ui/pages/SystemPage.h +++ b/src/apps/sequencer/ui/pages/SystemPage.h @@ -4,6 +4,7 @@ #include "ui/model/CalibrationCvOutputListModel.h" #include "ui/model/UtilitiesListModel.h" +#include "ui/model/SettingsListModel.h" #include "model/Settings.h" @@ -25,8 +26,9 @@ class SystemPage : public ListPage { private: enum class Mode : uint8_t { Calibration = 0, - Utilities = 3, - Update = 4, + Utilities = 2, + Update = 3, + Settings = 4, }; void setMode(Mode mode); @@ -55,6 +57,7 @@ class SystemPage : public ListPage { int _outputIndex; CalibrationCvOutputListModel _cvOutputListModel; UtilitiesListModel _utilitiesListModel; + SettingsListModel _settingsListModel; uint32_t _encoderDownTicks; }; diff --git a/src/apps/sequencer/ui/pages/TempoPage.cpp b/src/apps/sequencer/ui/pages/TempoPage.cpp index 365a5567..c0fcb5d3 100644 --- a/src/apps/sequencer/ui/pages/TempoPage.cpp +++ b/src/apps/sequencer/ui/pages/TempoPage.cpp @@ -23,7 +23,7 @@ void TempoPage::draw(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); canvas.setFont(Font::Small); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); switch (_mode) { case Mode::Tempo: { diff --git a/src/apps/sequencer/ui/pages/TextInputPage.cpp b/src/apps/sequencer/ui/pages/TextInputPage.cpp index 2c56d74d..3567a105 100644 --- a/src/apps/sequencer/ui/pages/TextInputPage.cpp +++ b/src/apps/sequencer/ui/pages/TextInputPage.cpp @@ -53,7 +53,7 @@ void TextInputPage::draw(Canvas &canvas) { WindowPainter::drawFooter(canvas, functionNames, pageKeyState()); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.setFont(Font::Small); const int titleX = 28; @@ -78,7 +78,7 @@ void TextInputPage::draw(Canvas &canvas) { } if (os::ticks() % os::time::ms(300) < os::time::ms(150)) { - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(titleX + titleWidth + offset, titleY - 8, width - 1, 12); const char str[2] = { _text[_cursorIndex], '\0' }; canvas.setBlendMode(BlendMode::Sub); @@ -87,16 +87,16 @@ void TextInputPage::draw(Canvas &canvas) { } canvas.setFont(Font::Tiny); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); int ix = 0; int iy = 0; for (int i = 0; i < int(sizeof(characterSet)); ++i) { canvas.drawTextCentered(charsX + ix * 10, charsY + iy * 10, 10, 10, FixedStringBuilder<2>("%c", characterSet[i])); if (_selectedIndex == i) { - canvas.setColor(pageKeyState()[Key::Encoder] ? 0xf : 0x7); + canvas.setColor(pageKeyState()[Key::Encoder] ? Color::Bright : Color::Medium); canvas.drawRect(charsX + ix * 10, charsY + iy * 10 + 1, 9, 9); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); } ++ix; if (ix % 20 == 0) { diff --git a/src/apps/sequencer/ui/painters/SequencePainter.cpp b/src/apps/sequencer/ui/painters/SequencePainter.cpp index 5ae1785a..52e80e2d 100644 --- a/src/apps/sequencer/ui/painters/SequencePainter.cpp +++ b/src/apps/sequencer/ui/painters/SequencePainter.cpp @@ -18,13 +18,13 @@ void SequencePainter::drawOffset(Canvas &canvas, int x, int y, int w, int h, int canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(x, y, w, h); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.vline(x + remap(0), y, h); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(x + remap(offset), y, h); } @@ -34,7 +34,7 @@ void SequencePainter::drawRetrigger(Canvas &canvas, int x, int y, int w, int h, int bw = w / maxRetrigger; x += (w - bw * retrigger) / 2; - canvas.setColor(0xf); + canvas.setColor(Color::Bright); for (int i = 0; i < retrigger; ++i) { canvas.fillRect(x, y, bw / 2, h); @@ -47,10 +47,10 @@ void SequencePainter::drawProbability(Canvas &canvas, int x, int y, int w, int h int pw = (w * probability) / maxProbability; - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(x, y, pw, h); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(x + pw, y, w - pw, h); } @@ -59,7 +59,7 @@ void SequencePainter::drawLength(Canvas &canvas, int x, int y, int w, int h, int int gw = ((w - 1) * length) / maxLength; - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(x, y, h); canvas.hline(x, y, gw); @@ -73,21 +73,21 @@ void SequencePainter::drawLengthRange(Canvas &canvas, int x, int y, int w, int h int gw = ((w - 1) * length) / maxLength; int rw = ((w - 1) * std::max(0, std::min(maxLength, length + range))) / maxLength; - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.vline(x, y, h); canvas.hline(x, y, gw); canvas.vline(x + gw, y, h); canvas.hline(x + gw, y + h - 1, w - gw); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(x + std::min(gw, rw), y + 2, std::max(gw, rw) - std::min(gw, rw) + 1, h - 4); } void SequencePainter::drawSlide(Canvas &canvas, int x, int y, int w, int h, bool active) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); if (active) { canvas.line(x, y + h, x + w, y); @@ -102,8 +102,8 @@ void SequencePainter::drawSequenceProgress(Canvas &canvas, int x, int y, int w, } canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(x, y, w, h); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(x + int(std::floor(progress * w)), y, h); } diff --git a/src/apps/sequencer/ui/painters/SongPainter.cpp b/src/apps/sequencer/ui/painters/SongPainter.cpp index f8a2f352..3b5b4318 100644 --- a/src/apps/sequencer/ui/painters/SongPainter.cpp +++ b/src/apps/sequencer/ui/painters/SongPainter.cpp @@ -24,8 +24,8 @@ void SongPainter::drawArrowRight(Canvas &canvas, int x, int y, int w, int h) { void SongPainter::drawProgress(Canvas &canvas, int x, int y, int w, int h, float progress) { int pw = w * progress; - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.fillRect(x + pw, y, w - pw, h); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(x, y, pw, h); } diff --git a/src/apps/sequencer/ui/painters/WindowPainter.cpp b/src/apps/sequencer/ui/painters/WindowPainter.cpp index 4ca61cb1..abe5aba4 100644 --- a/src/apps/sequencer/ui/painters/WindowPainter.cpp +++ b/src/apps/sequencer/ui/painters/WindowPainter.cpp @@ -7,7 +7,7 @@ static void drawInvertedText(Canvas &canvas, int x, int y, const char *text, bool inverted = true) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); if (inverted) { canvas.fillRect(x - 1, y - 5, canvas.textWidth(text) + 1, 7); @@ -19,21 +19,21 @@ static void drawInvertedText(Canvas &canvas, int x, int y, const char *text, boo void WindowPainter::clear(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); } void WindowPainter::drawFrame(Canvas &canvas, int x, int y, int w, int h) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fillRect(x, y, w, h); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawRect(x, y, w, h); } void WindowPainter::drawFunctionKeys(Canvas &canvas, const char *names[], const KeyState &keyState, int highlight) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.hline(0, PageHeight - FooterHeight - 1, PageWidth); for (int i = 0; i < FunctionKeyCount; ++i) { @@ -44,9 +44,9 @@ void WindowPainter::drawFunctionKeys(Canvas &canvas, const char *names[], const } canvas.setFont(Font::Tiny); - canvas.setColor(0xf); for (int i = 0; i < FunctionKeyCount; ++i) { + canvas.setColor(Color::Medium); if (names[i]) { bool pressed = keyState[Key::F0 + i]; @@ -58,11 +58,8 @@ void WindowPainter::drawFunctionKeys(Canvas &canvas, const char *names[], const int x1 = (PageWidth * (i + 1)) / FunctionKeyCount; int w = x1 - x0 + 1; - canvas.setBlendMode(BlendMode::Set); - if (pressed) { - canvas.fillRect(x0, PageHeight - FooterHeight, w, FooterHeight); - canvas.setBlendMode(BlendMode::Sub); + canvas.setColor(Color::Bright); } canvas.drawText(x0 + (w - canvas.textWidth(names[i])) / 2, PageHeight - 3, names[i]); @@ -77,14 +74,14 @@ void WindowPainter::drawClock(Canvas &canvas, const Engine &engine) { drawInvertedText(canvas, 2, 8 - 2, name); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(10, 8 - 2, FixedStringBuilder<8>("%.1f", engine.tempo())); } void WindowPainter::drawActiveState(Canvas &canvas, int track, int playPattern, int editPattern, bool snapshotActive, bool songActive) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); // draw selected track canvas.drawText(40, 8 - 2, FixedStringBuilder<8>("T%d", track + 1)); @@ -103,14 +100,14 @@ void WindowPainter::drawActiveState(Canvas &canvas, int track, int playPattern, void WindowPainter::drawActiveMode(Canvas &canvas, const char *mode) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(PageWidth - canvas.textWidth(mode) - 2, 8 - 2, mode); } void WindowPainter::drawActiveFunction(Canvas &canvas, const char *function) { canvas.setFont(Font::Tiny); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(100, 8 - 2, function); } @@ -127,13 +124,13 @@ void WindowPainter::drawHeader(Canvas &canvas, const Model &model, const Engine drawActiveMode(canvas, mode); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.hline(0, HeaderHeight, PageWidth); } void WindowPainter::drawFooter(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.hline(0, PageHeight - FooterHeight - 1, PageWidth); } @@ -147,11 +144,11 @@ void WindowPainter::drawScrollbar(Canvas &canvas, int x, int y, int w, int h, in } canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0x7); + canvas.setColor(Color::Medium); canvas.drawRect(x, y, w, h); int bh = (visibleRows * h) / totalRows; int by = (displayRow * h) / totalRows; - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(x, y + by, w, bh); } diff --git a/src/apps/tester/Tester.cpp b/src/apps/tester/Tester.cpp index d214f529..db8c5615 100644 --- a/src/apps/tester/Tester.cpp +++ b/src/apps/tester/Tester.cpp @@ -73,7 +73,7 @@ class TesterTask { Last }; - enum class Color : uint8_t { + enum class LEDColor : uint8_t { Off, Red, Green, @@ -88,7 +88,7 @@ class TesterTask { TesterTask() : _frameBuffer(256, 64, _frameBufferData), - _canvas(_frameBuffer) + _canvas(_frameBuffer, _brightness) { _cvOutputs.fill(0); _gateOutputs.fill(false); @@ -186,15 +186,15 @@ class TesterTask { case Mode::Leds: if (keyDown) { if (index < 32) { - _leds[index] = Color((int(_leds[index]) + 1) % int(Color::Last)); + _leds[index] = LEDColor((int(_leds[index]) + 1) % int(LEDColor::Last)); } else if (index == 32) { - fillLeds(Color::Off); + fillLeds(LEDColor::Off); } else if (index == 33) { - fillLeds(Color::Red); + fillLeds(LEDColor::Red); } else if (index == 34) { - fillLeds(Color::Green); + fillLeds(LEDColor::Green); } else if (index == 35) { - fillLeds(Color::Orange); + fillLeds(LEDColor::Orange); } } break; @@ -369,24 +369,24 @@ class TesterTask { void drawClear(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); } void drawTitle(Canvas &canvas, const char *title) { canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.fillRect(0, 0, 256, 10); canvas.setBlendMode(BlendMode::Sub); canvas.setFont(Font::Tiny); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.drawText(2, 7, title); } void drawLog(Canvas &canvas) { canvas.setBlendMode(BlendMode::Set); canvas.setFont(Font::Tiny); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); int x = 2; int y = 18; @@ -415,19 +415,19 @@ class TesterTask { _logBuffer.print(text); } - void fillLeds(Color color = Color::Off) { + void fillLeds(LEDColor color = LEDColor::Off) { _leds.fill(color); } - void setLed(int index, Color color) { + void setLed(int index, LEDColor color) { _leds[index] = color; } void updateLeds() { for (size_t i = 0; i < _leds.size(); ++i) { - Color c = _leds[i]; - uint8_t red = (c == Color::Red || c == Color::Orange) ? 0xff : 0; - uint8_t green = (c == Color::Green || c == Color::Orange) ? 0xff : 0; + LEDColor c = _leds[i]; + uint8_t red = (c == LEDColor::Red || c == LEDColor::Orange) ? 0xff : 0; + uint8_t green = (c == LEDColor::Green || c == LEDColor::Orange) ? 0xff : 0; blm.setLed(i, red, green); } } @@ -461,7 +461,7 @@ class TesterTask { LogBuffer<5, 64> _logBuffer; - std::array _leds; + std::array _leds; bool _clockOutput = false; bool _resetOutput = false; @@ -471,6 +471,7 @@ class TesterTask { uint8_t _frameBufferData[256 * 64]; FrameBuffer8bit _frameBuffer; Canvas _canvas; + float _brightness = 1.0; }; static TesterTask tester; diff --git a/src/core/gfx/Canvas.cpp b/src/core/gfx/Canvas.cpp index 6f0f3f9c..5548dde2 100644 --- a/src/core/gfx/Canvas.cpp +++ b/src/core/gfx/Canvas.cpp @@ -36,6 +36,10 @@ void Canvas::fill() { _frameBuffer.fill(_color); } +void Canvas::screensaver() { + _frameBuffer.fill(0x0); +} + void Canvas::point(int x, int y) { switch (_blendMode) { case BlendMode::Set: point(x, y); break; diff --git a/src/core/gfx/Canvas.h b/src/core/gfx/Canvas.h index 300830ae..f322ef9b 100644 --- a/src/core/gfx/Canvas.h +++ b/src/core/gfx/Canvas.h @@ -13,6 +13,15 @@ enum class BlendMode { Sub, }; +enum Color { + None = 0, + Low = 0x3, + MediumLow = 0x5, + Medium = 0x7, + MediumBright = 0xa, + Bright = 0xf +}; + enum class Font { Tiny, Small, @@ -33,15 +42,17 @@ enum class VerticalAlign { class Canvas { public: - Canvas(FrameBuffer8bit &frameBuffer) : + Canvas(FrameBuffer8bit &frameBuffer, float &brightness) : _frameBuffer(frameBuffer), _right(frameBuffer.width() - 1), - _bottom(frameBuffer.height() - 1) + _bottom(frameBuffer.height() - 1), + _brightness(brightness) { } uint8_t color() const { return _color; } - void setColor(uint8_t color) { _color = color; } + void setColorValue(uint8_t color) { _color = color * _brightness; } + void setColor(Color color) { setColorValue(color); } BlendMode blendMode() const { return _blendMode; } void setBlendMode(BlendMode blendMode) { _blendMode = blendMode; } @@ -50,6 +61,7 @@ class Canvas { void setFont(Font font) { _font = font; } void fill(); + void screensaver(); void point(int x, int y); @@ -263,4 +275,5 @@ class Canvas { uint8_t _color = 0xf; BlendMode _blendMode = BlendMode::Set; Font _font = Font::Default; + float &_brightness; }; diff --git a/src/tests/integration/drivers/TestLcd.cpp b/src/tests/integration/drivers/TestLcd.cpp index 4e0539d1..3ffac2cb 100644 --- a/src/tests/integration/drivers/TestLcd.cpp +++ b/src/tests/integration/drivers/TestLcd.cpp @@ -12,7 +12,7 @@ class TestLcd : public IntegrationTest { public: TestLcd() : frameBuffer(256, 64, frameBufferData), - canvas(frameBuffer) + canvas(frameBuffer, brightness) {} void init() override { @@ -32,10 +32,10 @@ class TestLcd : public IntegrationTest { timer.reset(); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); canvas.vline(frame % 256, 0, 64); canvas.hline(0, frame % 64, 256); @@ -50,6 +50,7 @@ class TestLcd : public IntegrationTest { Timer timer; MovingAverage frameInterval; int lastFrame = -1; + float brightness = 1.0; }; INTEGRATION_TEST(TestLcd, "Lcd", true) diff --git a/src/tests/unit/sequencer/TestCurve.cpp b/src/tests/unit/sequencer/TestCurve.cpp index 68641322..06d269f3 100644 --- a/src/tests/unit/sequencer/TestCurve.cpp +++ b/src/tests/unit/sequencer/TestCurve.cpp @@ -14,6 +14,7 @@ const int Width = 32; const int Height = 64; const int Padding = 4; +float brightness = 1.0; UNIT_TEST("Curve") { @@ -24,14 +25,14 @@ UNIT_TEST("Curve") { auto drawCurve = [] (int index, const char *filename) { uint8_t data[Width * Height]; FrameBuffer8bit framebuffer(Width, Height, data); - Canvas canvas(framebuffer); + Canvas canvas(framebuffer, brightness); canvas.setBlendMode(BlendMode::Set); - canvas.setColor(0); + canvas.setColor(Color::None); canvas.fill(); canvas.setBlendMode(BlendMode::Add); - canvas.setColor(0xf); + canvas.setColor(Color::Bright); const int Steps = Width * 2;