From 664b48e3f30520fb1583e8e9622e2c9e6eb411cc Mon Sep 17 00:00:00 2001 From: bfredl Date: Tue, 19 Mar 2024 22:11:00 +0100 Subject: [PATCH] feature: session macro sidebar --- docs/community_features.md | 14 ++ src/definitions_cxx.hpp | 2 +- .../ui/keyboard/column_controls/session.cpp | 86 +++++++ .../gui/ui/keyboard/column_controls/session.h | 42 ++++ .../ui/keyboard/layout/column_control_state.h | 3 + .../ui/keyboard/layout/column_controls.cpp | 8 + src/deluge/gui/ui/ui.h | 1 + src/deluge/gui/ui_timer_manager.cpp | 5 +- src/deluge/gui/views/audio_clip_view.cpp | 13 +- src/deluge/gui/views/audio_clip_view.h | 1 + src/deluge/gui/views/instrument_clip_view.cpp | 41 +++- src/deluge/gui/views/instrument_clip_view.h | 1 + src/deluge/gui/views/session_view.cpp | 226 ++++++++++++++++-- src/deluge/gui/views/session_view.h | 10 + src/deluge/gui/views/view.cpp | 10 + src/deluge/gui/views/view.h | 1 + src/deluge/model/song/song.cpp | 123 ++++++++++ src/deluge/model/song/song.h | 17 ++ src/deluge/playback/mode/session.cpp | 3 + 19 files changed, 584 insertions(+), 23 deletions(-) create mode 100644 src/deluge/gui/ui/keyboard/column_controls/session.cpp create mode 100644 src/deluge/gui/ui/keyboard/column_controls/session.h diff --git a/docs/community_features.md b/docs/community_features.md index 689f923d4a..774c43f234 100644 --- a/docs/community_features.md +++ b/docs/community_features.md @@ -461,6 +461,20 @@ Here is a list of features that have been added to the firmware as a list, group - y1 = -26.4 to -22.1 - y0 = -30.8 to -26.5 +### 4.1.9 - Song part macros + +Macros are a way to quickly switch playing clips without needing to go into song view. +Within grid view, a fourth (purple) mode is used to edit macros. There are 8 macro slots +shown in the left sidebar (the section colors are hidden in this mode). To assign a macro, +first select a macro slot and then press a clip in the grid. Pressing the same clip multiple +time cycles though different modes: + +- clip macro: Turn individual clip on/off +- output macro: cycle all clips for this particular clip +- section macro: activate all clips for this section + +These macros than then be accessed in keyboard view of any part by selecting the "CLIP MACROS" sidebar. + ### 4.2 - Clip View - General Features (Instrument and Audio Clips) #### 4.2.1 - Filters diff --git a/src/definitions_cxx.hpp b/src/definitions_cxx.hpp index 664cfcb363..5b13b9d74b 100644 --- a/src/definitions_cxx.hpp +++ b/src/definitions_cxx.hpp @@ -989,7 +989,7 @@ enum GridMode : uint8_t { Unassigned1, Unassigned2, Unassigned3, - Unassigned4, + MAGENTA, YELLOW, BLUE, GREEN, diff --git a/src/deluge/gui/ui/keyboard/column_controls/session.cpp b/src/deluge/gui/ui/keyboard/column_controls/session.cpp new file mode 100644 index 0000000000..eea6a28fd1 --- /dev/null +++ b/src/deluge/gui/ui/keyboard/column_controls/session.cpp @@ -0,0 +1,86 @@ +/* + * Copyright © 2016-2024 Synthstrom Audible Limited + * + * This file is part of The Synthstrom Audible Deluge Firmware. + * + * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + */ + +#include "session.h" +#include "gui/ui/keyboard/layout/column_controls.h" +#include "gui/views/session_view.h" +#include "gui/views/view.h" +#include "hid/buttons.h" +#include "playback/mode/session.h" + +using namespace deluge::gui::ui::keyboard::layout; + +namespace deluge::gui::ui::keyboard::controls { + +void SessionColumn::renderColumn(RGB image[][kDisplayWidth + kSideBarWidth], int32_t column) { + bool armed = false; + for (int32_t y = 0; y < kDisplayHeight; ++y) { + armed |= sessionView.gridRenderMacros(column, y, false, image, nullptr); + } + if (armed) { + view.flashPlayEnable(); + } +} + +bool SessionColumn::handleVerticalEncoder(int8_t pad, int32_t offset) { + SessionMacro& m = currentSong->sessionMacros[pad]; + int kindIndex = (int32_t)m.kind + offset; + if (kindIndex >= SessionMacroKind::NUM_KINDS) { + kindIndex = 0; + } + else if (kindIndex < 0) { + kindIndex = SessionMacroKind::NUM_KINDS - 1; + } + + m.kind = (SessionMacroKind)kindIndex; + + switch (m.kind) { + case CLIP_LAUNCH: + m.clip = getCurrentClip(); + break; + + case OUTPUT_CYCLE: + m.clip = nullptr; + m.output = getCurrentOutput(); + break; + + case SECTION: + m.section = getCurrentClip()->section; + + default: + break; + } + + return true; +}; + +void SessionColumn::handleLeavingColumn(ModelStackWithTimelineCounter* modelStackWithTimelineCounter, + KeyboardLayout* layout){}; + +void SessionColumn::handlePad(ModelStackWithTimelineCounter* modelStackWithTimelineCounter, PressedPad pad, + KeyboardLayout* layout) { + + SessionMacro& m = currentSong->sessionMacros[pad.y]; + if (pad.active) {} + else { + sessionView.activateMacro(pad.y, pad.padPressHeld); + } + view.flashPlayEnable(); +}; + + +} // namespace deluge::gui::ui::keyboard::controls diff --git a/src/deluge/gui/ui/keyboard/column_controls/session.h b/src/deluge/gui/ui/keyboard/column_controls/session.h new file mode 100644 index 0000000000..ae90b4935b --- /dev/null +++ b/src/deluge/gui/ui/keyboard/column_controls/session.h @@ -0,0 +1,42 @@ +/* + * Copyright © 2016-2024 Synthstrom Audible Limited + * + * This file is part of The Synthstrom Audible Deluge Firmware. + * + * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . + */ + +#pragma once + +#include "gui/ui/keyboard/column_controls/control_column.h" +#include "model/song/song.h" + +namespace deluge::gui::ui::keyboard::controls { + +class SessionColumn : public ControlColumn { +public: + SessionColumn() = default; + + void renderColumn(RGB image[][kDisplayWidth + kSideBarWidth], int32_t column) override; + bool handleVerticalEncoder(int8_t pad, int32_t offset) override; + void handleLeavingColumn(ModelStackWithTimelineCounter* modelStackWithTimelineCounter, + KeyboardLayout* layout) override; + void handlePad(ModelStackWithTimelineCounter* modelStackWithTimelineCounter, PressedPad pad, + KeyboardLayout* layout) override; + + void handleOutput(SessionMacro& m, PressedPad pad); + Clip* findNextClipForOutput(SessionMacro& m, PressedPad pad); + +private: +}; + +} // namespace deluge::gui::ui::keyboard::controls diff --git a/src/deluge/gui/ui/keyboard/layout/column_control_state.h b/src/deluge/gui/ui/keyboard/layout/column_control_state.h index b8a3a4fed9..3e4931f6e5 100644 --- a/src/deluge/gui/ui/keyboard/layout/column_control_state.h +++ b/src/deluge/gui/ui/keyboard/layout/column_control_state.h @@ -22,6 +22,7 @@ #include "gui/ui/keyboard/column_controls/dx.h" #include "gui/ui/keyboard/column_controls/mod.h" #include "gui/ui/keyboard/column_controls/scale_mode.h" +#include "gui/ui/keyboard/column_controls/session.h" #include "gui/ui/keyboard/column_controls/song_chord_mem.h" #include "gui/ui/keyboard/column_controls/velocity.h" @@ -35,6 +36,7 @@ enum ColumnControlFunction : int8_t { CHORD_MEM, SCALE_MODE, DX, + SESSION, // BEAT_REPEAT, COL_CTRL_FUNC_MAX, }; @@ -50,6 +52,7 @@ struct ColumnControlState { ChordMemColumn chordMemColumn{}; ScaleModeColumn scaleModeColumn{}; DXColumn dxColumn{}; + SessionColumn sessionColumn{}; ColumnControlFunction leftColFunc = VELOCITY; ColumnControlFunction rightColFunc = MOD; diff --git a/src/deluge/gui/ui/keyboard/layout/column_controls.cpp b/src/deluge/gui/ui/keyboard/layout/column_controls.cpp index 025f33b383..39735a9b55 100644 --- a/src/deluge/gui/ui/keyboard/layout/column_controls.cpp +++ b/src/deluge/gui/ui/keyboard/layout/column_controls.cpp @@ -44,6 +44,7 @@ const char* functionNames[][2] = { /* CHORD_MEM */ {"CCME", "Clip Chord Memory"}, /* SCALE_MODE */ {"SMOD", "Scales"}, /* DX */ {"DX", "DX operators"}, + /* SESSION */ {"SESS", "clip macros"}, /* BEAT_REPEAT */ {"BEAT", "Beat Repeat"}, }; @@ -204,6 +205,8 @@ ControlColumn* ColumnControlState::getColumnForFunc(ColumnControlFunction func) return &scaleModeColumn; case DX: return &dxColumn; + case SESSION: + return &sessionColumn; } return nullptr; } @@ -224,6 +227,8 @@ const char* columnFunctionToString(ColumnControlFunction func) { return "scale_mode"; case DX: return "dx"; + case SESSION: + return "session"; } return ""; } @@ -250,6 +255,9 @@ ColumnControlFunction stringToColumnFunction(char const* string) { else if (!strcmp(string, "dx")) { return DX; } + else if (!strcmp(string, "session")) { + return SESSION; + } else { return VELOCITY; // unknown column, just pick the default } diff --git a/src/deluge/gui/ui/ui.h b/src/deluge/gui/ui/ui.h index 463deba02b..78257ace54 100644 --- a/src/deluge/gui/ui/ui.h +++ b/src/deluge/gui/ui/ui.h @@ -75,6 +75,7 @@ extern bool pendingUIRenderingLock; #define UI_MODE_HOLDING_STATUS_PAD 62 #define UI_MODE_IMPLODE_ANIMATION 63 #define UI_MODE_STEM_EXPORT 64 +#define UI_MODE_HOLDING_SONG_BUTTON 65 #define EXCLUSIVE_UI_MODES_MASK ((uint32_t)255) diff --git a/src/deluge/gui/ui_timer_manager.cpp b/src/deluge/gui/ui_timer_manager.cpp index a74656b50d..71fd6ecc39 100644 --- a/src/deluge/gui/ui_timer_manager.cpp +++ b/src/deluge/gui/ui_timer_manager.cpp @@ -85,10 +85,7 @@ void UITimerManager::routine() { break; case TimerName::PLAY_ENABLE_FLASH: { - RootUI* rootUI = getRootUI(); - if ((rootUI == &sessionView) || (rootUI == &performanceSessionView)) { - sessionView.flashPlayRoutine(); - } + view.flashPlayRoutine(); break; } case TimerName::DISPLAY: diff --git a/src/deluge/gui/views/audio_clip_view.cpp b/src/deluge/gui/views/audio_clip_view.cpp index 231fe416f2..f8ff1d0b00 100644 --- a/src/deluge/gui/views/audio_clip_view.cpp +++ b/src/deluge/gui/views/audio_clip_view.cpp @@ -282,10 +282,21 @@ ActionResult AudioClipView::buttonAction(deluge::hid::Button b, bool on, bool in // Song view button if (b == SESSION_VIEW) { - if (on && currentUIMode == UI_MODE_NONE) { + if (on) { + if (currentUIMode == UI_MODE_NONE) { + currentUIMode = UI_MODE_HOLDING_SONG_BUTTON; + timeSongButtonPressed = AudioEngine::audioSampleTimer; + indicator_leds::setLedState(IndicatorLED::SESSION_VIEW, true); + } + + } else { + if (!isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) { + return ActionResult::DEALT_WITH; + } if (inCardRoutine) { return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; } + exitUIMode(UI_MODE_HOLDING_SONG_BUTTON); uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC); diff --git a/src/deluge/gui/views/audio_clip_view.h b/src/deluge/gui/views/audio_clip_view.h index 862ab3df0e..713ffd1539 100644 --- a/src/deluge/gui/views/audio_clip_view.h +++ b/src/deluge/gui/views/audio_clip_view.h @@ -62,6 +62,7 @@ class AudioClipView final : public ClipView, public ClipMinder { const char* getName() { return "audio_clip_view"; } private: + uint32_t timeSongButtonPressed; void needsRenderingDependingOnSubMode(); int32_t lastTickSquare; bool mustRedrawTickSquares; diff --git a/src/deluge/gui/views/instrument_clip_view.cpp b/src/deluge/gui/views/instrument_clip_view.cpp index 3e0cd0d8e0..660863c619 100644 --- a/src/deluge/gui/views/instrument_clip_view.cpp +++ b/src/deluge/gui/views/instrument_clip_view.cpp @@ -244,10 +244,28 @@ ActionResult InstrumentClipView::buttonAction(deluge::hid::Button b, bool on, bo // Song view button else if (b == SESSION_VIEW) { - if (on && currentUIMode == UI_MODE_NONE) { + if (on) { + if (currentUIMode == UI_MODE_NONE) { + currentUIMode = UI_MODE_HOLDING_SONG_BUTTON; + timeSongButtonPressed = AudioEngine::audioSampleTimer; + indicator_leds::setLedState(IndicatorLED::SESSION_VIEW, true); + uiNeedsRendering(this, 0, 0xFFFFFFFF); + } + + } else { + if (!isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) { + return ActionResult::DEALT_WITH; + } if (inCardRoutine) { return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; } + exitUIMode(UI_MODE_HOLDING_SONG_BUTTON); + + if ((int32_t)(AudioEngine::audioSampleTimer - timeSongButtonPressed) > kShortPressTime) { + uiNeedsRendering(this, 0, 0xFFFFFFFF); + indicator_leds::setLedState(IndicatorLED::SESSION_VIEW, false); + return ActionResult::DEALT_WITH; + } if (currentSong->lastClipInstanceEnteredStartPos != -1 || getCurrentClip()->isArrangementOnlyClip()) { bool success = arrangerView.transitionToArrangementEditor(); @@ -1523,6 +1541,16 @@ ActionResult InstrumentClipView::padAction(int32_t x, int32_t y, int32_t velocit } view.noteRowMuteMidiLearnPadPressed(velocity, noteRow); } + else if (isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) { + if (sdRoutineLock) { + return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE; + } + if (!velocity) { + // TODO: long press.. + sessionView.activateMacro(y, false); + } + return ActionResult::DEALT_WITH; + } else if (getCurrentOutputType() == OutputType::KIT && lastAuditionedYDisplay == y && isUIModeActive(UI_MODE_AUDITIONING) && getNumNoteRowsAuditioning() == 1) { if (velocity) { @@ -4347,12 +4375,21 @@ bool InstrumentClipView::renderSidebar(uint32_t whichRows, RGB image[][kDisplayW return true; } + uint32_t macroColumn = kDisplayWidth; + bool armed = false; for (int32_t i = 0; i < kDisplayHeight; i++) { if (whichRows & (1 << i)) { - drawMuteSquare(getCurrentInstrumentClip()->getNoteRowOnScreen(i, currentSong), image[i], occupancyMask[i]); + if (isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) { + armed |= sessionView.gridRenderMacros(macroColumn, i, true, image, occupancyMask); + } else { + drawMuteSquare(getCurrentInstrumentClip()->getNoteRowOnScreen(i, currentSong), image[i], occupancyMask[i]); + } drawAuditionSquare(i, image[i]); } } + if (armed) { + view.flashPlayEnable(); + } return true; } diff --git a/src/deluge/gui/views/instrument_clip_view.h b/src/deluge/gui/views/instrument_clip_view.h index 87ad3abf8c..7d3ec51026 100644 --- a/src/deluge/gui/views/instrument_clip_view.h +++ b/src/deluge/gui/views/instrument_clip_view.h @@ -224,6 +224,7 @@ class InstrumentClipView final : public ClipView, public InstrumentClipMinder { uint8_t yDisplayOfNewNoteRow; int32_t quantizeAmount; + uint32_t timeSongButtonPressed; std::array rowColour; std::array rowTailColour; diff --git a/src/deluge/gui/views/session_view.cpp b/src/deluge/gui/views/session_view.cpp index 53f4528772..1d7b679d35 100644 --- a/src/deluge/gui/views/session_view.cpp +++ b/src/deluge/gui/views/session_view.cpp @@ -2797,24 +2797,29 @@ bool SessionView::gridRenderSidebar(uint32_t whichRows, RGB image[][kDisplayWidt // Section column uint32_t sectionColumnIndex = kDisplayWidth; for (int32_t y = (kGridHeight - 1); y >= 0; --y) { - occupancyMask[y][sectionColumnIndex] = 64; + if (gridModeActive == SessionGridModeMacros) { + gridRenderMacros(sectionColumnIndex, y, true, image, occupancyMask); + } + else { + occupancyMask[y][sectionColumnIndex] = 64; - auto section = gridSectionFromY(y); - RGB& ptrSectionColour = image[y][sectionColumnIndex]; + auto section = gridSectionFromY(y); + RGB& ptrSectionColour = image[y][sectionColumnIndex]; - ptrSectionColour = RGB::fromHue(defaultClipGroupColours[gridSectionFromY(y)]); - ptrSectionColour = ptrSectionColour.adjust(255, 2); + ptrSectionColour = RGB::fromHue(defaultClipGroupColours[gridSectionFromY(y)]); + ptrSectionColour = ptrSectionColour.adjust(255, 2); - if (view.midiLearnFlashOn && gridModeActive == SessionGridModeLaunch) { - // MIDI colour if necessary - if (currentSong->sections[section].launchMIDICommand.containsSomething()) { - ptrSectionColour = colours::midi_command; - } + if (view.midiLearnFlashOn && gridModeActive == SessionGridModeLaunch) { + // MIDI colour if necessary + if (currentSong->sections[section].launchMIDICommand.containsSomething()) { + ptrSectionColour = colours::midi_command; + } - else { - // If user assigning MIDI controls and has this section selected, flash to half brightness - if (currentSong && view.learnedThing == ¤tSong->sections[section].launchMIDICommand) { - ptrSectionColour = ptrSectionColour.dim(); + else { + // If user assigning MIDI controls and has this section selected, flash to half brightness + if (currentSong && view.learnedThing == ¤tSong->sections[section].launchMIDICommand) { + ptrSectionColour = ptrSectionColour.dim(); + } } } } @@ -2848,6 +2853,11 @@ void SessionView::gridRenderActionModes(int32_t y, RGB image[][kDisplayWidth + k modeColour = colours::yellow; // Yellow break; } + case GridMode::MAGENTA: { + modeActive = (gridModeActive == SessionGridModeMacros); + modeColour = colours::magenta_full; // Magenta + break; + } case GridMode::PINK: { modeActive = performanceSessionView.gridModeActive; modeColour = colours::magenta; // Pink @@ -2862,6 +2872,123 @@ void SessionView::gridRenderActionModes(int32_t y, RGB image[][kDisplayWidth + k image[y][actionModeColumnIndex] = modeColour.adjust(255, (modeActive ? 1 : 8)); } +bool SessionView::gridRenderMacros(int32_t column, uint32_t y, bool in_view, RGB image[][kDisplayWidth + kSideBarWidth], + uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth]) { + uint8_t brightness = 1; + uint8_t otherChannels = 0; + + bool is_active = in_view && selectedMacro == y; + bool is_other_active = in_view && selectedMacro >= 0 && !is_active; + uint8_t dark = is_active ? 32 : 0; + uint8_t light = is_other_active ? 208 : 255; + + bool armed = view.clipArmFlashOn; + + SessionMacro& m = currentSong->sessionMacros[y]; + switch (m.kind) { + case CLIP_LAUNCH: + if (m.clip->activeIfNoSolo) { + image[y][column] = {0, light, 0}; + } + else { + image[y][column] = {light, 0, 0}; + } + if (m.clip->armState != ArmState::OFF) { + armed = true; + if (view.clipArmFlashOn) { + image[y][column] = {0, 0, 0}; + } + } + + break; + case OUTPUT_CYCLE: + image[y][column] = {0, 64, light}; + break; + case SECTION: + image[y][column] = {light, 0, 128}; + break; + case NO_MACRO: + image[y][column] = {dark, dark, dark}; + break; + } + + if (occupancyMask) { + occupancyMask[y][column] = true; + } + + return armed; +} + +void SessionView::activateMacro(uint32_t y, bool long_press) { + if (y > 8) { + return; + } + + SessionMacro& m = currentSong->sessionMacros[y]; + switch (m.kind) { + case CLIP_LAUNCH: + session.toggleClipStatus(m.clip, nullptr, Buttons::isShiftButtonPressed(), kInternalButtonPressLatency); + break; + + case OUTPUT_CYCLE: + if (!long_press) { + Clip* nextClip = findNextClipForOutput(m.output); + if (nextClip) { + session.toggleClipStatus(nextClip, nullptr, Buttons::isShiftButtonPressed(), kInternalButtonPressLatency); + } + } + else { + // long press: mute if possible, otherwise do the same. + Clip* curClip = currentSong->getClipWithOutput(m.output, true, nullptr); + if (!curClip) { + curClip = findNextClipForOutput(m.output); + } + if (curClip) { + session.toggleClipStatus(curClip, nullptr, Buttons::isShiftButtonPressed(), kInternalButtonPressLatency); + } + } + break; + + case SECTION: + // TODO: we can do really fancy stuff like registering a section+output combo + session.armSection(m.section, kInternalButtonPressLatency); + + default: + break; + } +} + +Clip* SessionView::findNextClipForOutput(Output *output) { + int last_active = -1; + for (int i = 0; i < currentSong->sessionClips.getNumElements(); i++) { + Clip* clip = currentSong->sessionClips.getClipAtIndex(i); + if (clip->output == output) { + if (last_active == -1) { + if (clip->activeIfNoSolo) { + last_active = i; + } + } + else { + return clip; + } + } + } + + if (last_active == -1) { + last_active = currentSong->sessionClips.getNumElements(); + } + + // might need to cycle around to find the next clip + for (int i = 0; i < last_active; i++) { + Clip* clip = currentSong->sessionClips.getClipAtIndex(i); + if (clip->output == output) { + return clip; + } + } + + return nullptr; +} + bool SessionView::gridRenderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth + kSideBarWidth], uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth], bool drawUndefinedArea) { @@ -2961,9 +3088,29 @@ RGB SessionView::gridRenderClipColor(Clip* clip) { } RGB resultColour; + bool macroActive = false; if (gridModeActive == SessionGridModeConfig) { resultColour = view.getClipMuteSquareColour(clip, resultColour, true, false); } + else if (gridModeActive == SessionGridModeMacros && selectedMacro >= 0) { + auto& macro = currentSong->sessionMacros[selectedMacro]; + if (macro.kind == SessionMacroKind::CLIP_LAUNCH) { + macroActive = (macro.clip == clip); + } + else if (macro.kind == SessionMacroKind::OUTPUT_CYCLE) { + macroActive = (macro.output == clip->output); + } + else if (macro.kind == SessionMacroKind::SECTION) { + macroActive = (macro.section == clip->section); + } + + if (macroActive) { + resultColour = RGB(255, 255, 255); + } + else { + resultColour = RGB::fromHue(clip->output->colour); + } + } else { resultColour = RGB::fromHue(clip->output->colour); } @@ -2975,7 +3122,8 @@ RGB SessionView::gridRenderClipColor(Clip* clip) { // If clip is not active or grayed out - dim it else if (!clip->activeIfNoSolo) { - resultColour = resultColour.transform([](auto chan) { return ((float)chan / 255) * 10; }); + resultColour = + resultColour.transform([macroActive](auto chan) { return ((float)chan / 255) * (macroActive ? 64 : 10); }); } if (greyout) { @@ -3313,6 +3461,10 @@ ActionResult SessionView::gridHandlePads(int32_t x, int32_t y, int32_t on) { gridModeActive = SessionGridModeConfig; break; } + case GridMode::MAGENTA: { + gridModeActive = SessionGridModeMacros; + break; + } case GridMode::PINK: { performanceSessionView.gridModeActive = true; performanceSessionView.timeGridModePress = AudioEngine::audioSampleTimer; @@ -3353,6 +3505,10 @@ ActionResult SessionView::gridHandlePads(int32_t x, int32_t y, int32_t on) { modeHandleResult = gridHandlePadsConfig(x, y, on, clip); break; } + case SessionGridModeMacros: { + modeHandleResult = gridHandlePadsMacros(x, y, on, clip); + break; + } } if (modeHandleResult == ActionResult::DEALT_WITH) { @@ -3688,6 +3844,46 @@ ActionResult SessionView::gridHandlePadsConfig(int32_t x, int32_t y, int32_t on, return ActionResult::ACTIONED_AND_CAUSED_CHANGE; } +ActionResult SessionView::gridHandlePadsMacros(int32_t x, int32_t y, int32_t on, Clip* clip) { + if (x < kDisplayWidth) { + if (selectedMacro == -1 || !on) { + return ActionResult::DEALT_WITH; + } + auto& macro = currentSong->sessionMacros[selectedMacro]; + if (gridFirstPressedX != x || gridFirstPressedY != y) { + if (clip == nullptr) { + // TODO: be smart and assign output or section if can be determined + gridFirstPressedX = -1; + return ActionResult::ACTIONED_AND_CAUSED_CHANGE; + } + gridFirstPressedX = x; + gridFirstPressedY = y; + macro.kind = SessionMacroKind::CLIP_LAUNCH; + macro.clip = clip; + macro.output = clip->output; + macro.section = clip->section; + } + else { + int kindIndex = (int32_t)macro.kind + 1; + if (kindIndex == SessionMacroKind::NUM_KINDS) { + kindIndex = 0; + } + macro.kind = (SessionMacroKind)kindIndex; + // char txt[] = "bluff 0"; + // txt[6] = '0' + kindIndex; + // display->displayPopup(txt); + // TODO: popup kind! + } + return ActionResult::ACTIONED_AND_CAUSED_CHANGE; + } + else { + if (on) { + selectedMacro = (selectedMacro == y) ? -1 : y; + gridFirstPressedX = -1; + } + } + return ActionResult::ACTIONED_AND_CAUSED_CHANGE; +} ActionResult SessionView::gridHandleScroll(int32_t offsetX, int32_t offsetY) { if (currentUIMode == UI_MODE_CLIP_PRESSED_IN_SONG_VIEW && offsetY != 0) { auto track = gridTrackFromX(gridFirstPressedX, gridTrackCount()); diff --git a/src/deluge/gui/views/session_view.h b/src/deluge/gui/views/session_view.h index 7fae1e66a5..a49a3e63dd 100644 --- a/src/deluge/gui/views/session_view.h +++ b/src/deluge/gui/views/session_view.h @@ -31,6 +31,7 @@ enum SessionGridMode : uint8_t { SessionGridModeEdit, SessionGridModeLaunch, SessionGridModeConfig, + SessionGridModeMacros, SessionGridModeMaxElement // Keep as boundary }; @@ -95,6 +96,8 @@ class SessionView final : public ClipNavigationTimelineView { Clip* getClipOnScreen(int32_t yDisplay); void modEncoderAction(int32_t whichModEncoder, int32_t offset); ActionResult verticalScrollOneSquare(int32_t direction); + void activateMacro(uint32_t y, bool long_press); + Clip* findNextClipForOutput(Output *output); void renderOLED(deluge::hid::display::oled_canvas::Canvas& canvas) override; @@ -111,6 +114,7 @@ class SessionView final : public ClipNavigationTimelineView { // action to set this to false uint8_t sectionPressed; uint8_t masterCompEditMode; + int8_t selectedMacro = -1; Clip* getClipForLayout(); @@ -160,6 +164,11 @@ class SessionView final : public ClipNavigationTimelineView { bool gridRenderMainPads(uint32_t whichRows, RGB image[][kDisplayWidth + kSideBarWidth], uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth], bool drawUndefinedArea = true); +public: + bool gridRenderMacros(int32_t column, uint32_t y, bool in_view, RGB image[][kDisplayWidth + kSideBarWidth], + uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth]); + +private: RGB gridRenderClipColor(Clip* clip); ActionResult gridHandlePadsEdit(int32_t x, int32_t y, int32_t on, Clip* clip); @@ -167,6 +176,7 @@ class SessionView final : public ClipNavigationTimelineView { ActionResult gridHandlePadsLaunchImmediate(int32_t x, int32_t y, int32_t on, Clip* clip); ActionResult gridHandlePadsLaunchWithSelection(int32_t x, int32_t y, int32_t on, Clip* clip); ActionResult gridHandlePadsConfig(int32_t x, int32_t y, int32_t on, Clip* clip); + ActionResult gridHandlePadsMacros(int32_t x, int32_t y, int32_t on, Clip* clip); void gridHandlePadsLaunchToggleArming(Clip* clip, bool immediate); ActionResult gridHandleScroll(int32_t offsetX, int32_t offsetY); diff --git a/src/deluge/gui/views/view.cpp b/src/deluge/gui/views/view.cpp index 5ccb006be6..1270da724d 100644 --- a/src/deluge/gui/views/view.cpp +++ b/src/deluge/gui/views/view.cpp @@ -2678,6 +2678,16 @@ ActionResult View::clipStatusPadAction(Clip* clip, bool on, int32_t yDisplayIfIn return ActionResult::DEALT_WITH; } +void View::flashPlayRoutine() { + view.clipArmFlashOn = !view.clipArmFlashOn; + RootUI* rootUI = getRootUI(); + if ((rootUI == &sessionView) || (rootUI == &performanceSessionView)) { + sessionView.flashPlayRoutine(); + } else { + uiNeedsRendering(getCurrentUI(), 0x00000000, 0xFFFFFFFF); + } +} + void View::flashPlayEnable() { uiTimerManager.setTimer(TimerName::PLAY_ENABLE_FLASH, kFastFlashTime); } diff --git a/src/deluge/gui/views/view.h b/src/deluge/gui/views/view.h index 3f7bbcc5f9..a5150de0e7 100644 --- a/src/deluge/gui/views/view.h +++ b/src/deluge/gui/views/view.h @@ -97,6 +97,7 @@ class View { ActionResult clipStatusPadAction(Clip* clip, bool on, int32_t yDisplayIfInSessionView = -1); void flashPlayEnable(); void flashPlayDisable(); + void flashPlayRoutine(); // MIDI learn stuff MidiLearn thingPressedForMidiLearn = MidiLearn::NONE; diff --git a/src/deluge/model/song/song.cpp b/src/deluge/model/song/song.cpp index 6d84abffc4..687c15c3fa 100644 --- a/src/deluge/model/song/song.cpp +++ b/src/deluge/model/song/song.cpp @@ -1376,6 +1376,55 @@ void Song::writeToFile(StorageManager& bdsm) { writer.writeClosingTag("chordMem"); } + // macros + int maxSessionMacroToSave = 0; + for (int32_t y = 0; y < kDisplayHeight; y++) { + if (sessionMacros[y].kind != SessionMacroKind::NO_MACRO) { + maxSessionMacroToSave = y + 1; + } + } + if (maxSessionMacroToSave > 0) { + // some macros to save + writer.writeOpeningTag("sessionMacros"); + for (int32_t y = 0; y < maxSessionMacroToSave; y++) { + auto& m = sessionMacros[y]; + writer.writeOpeningTagBeginning("macro"); + switch (m.kind) { + case CLIP_LAUNCH: { + int32_t index = sessionClips.getIndexForClip(m.clip); + if (index >= 0) { + writer.writeAttribute("kind", "clip_launch"); + writer.writeAttribute("clip", index); + } + break; + } + case OUTPUT_CYCLE: { + int32_t i = 0; + Output* thisOutput; + for (thisOutput = firstOutput; thisOutput; thisOutput = thisOutput->next) { + if (thisOutput == m.output) { + break; + } + i++; + } + if (thisOutput != nullptr) { + writer.writeAttribute("kind", "output_cycle"); + writer.writeAttribute("output", i); + } + break; + } + case SECTION: + writer.writeAttribute("kind", "section"); + writer.writeAttribute("section", m.section); + break; + case NO_MACRO: + break; + } + writer.closeTag(); + } + writer.writeClosingTag("sessionMacros"); + } + writer.writeClosingTag("song"); } @@ -1392,6 +1441,10 @@ Error Song::readFromFile(Deserializer& reader) { for (int32_t s = 0; s < kMaxNumSections; s++) { sections[s].numRepetitions = -1; } + for (int32_t y = 0; y < 8; y++) { + sessionMacros[y].kind = NO_MACRO; + } + uint64_t newTimePerTimerTick = (uint64_t)1 << 32; // TODO: make better! @@ -1794,6 +1847,62 @@ Error Song::readFromFile(Deserializer& reader) { reader.exitTag("chordMem"); } + else if (!strcmp(tagName, "sessionMacros")) { + int slot_index = 0; + while (*(tagName = reader.readNextTagOrAttributeName())) { + if (!strcmp(tagName, "macro")) { + int y = slot_index++; + if (y >= 8) { + reader.exitTag("macro"); + continue; + } + auto& m = sessionMacros[y]; + while (*(tagName = reader.readNextTagOrAttributeName())) { + if (!strcmp(tagName, "kind")) { + const char* kind = reader.readTagOrAttributeValue(); + if (!strcmp(kind, "clip_launch")) { + m.kind = CLIP_LAUNCH; + } + else if (!strcmp(kind, "output_cycle")) { + m.kind = OUTPUT_CYCLE; + } + else if (!strcmp(kind, "section")) { + m.kind = SECTION; + } + } + else if (!strcmp(tagName, "clip")) { + int32_t index = reader.readTagOrAttributeValueInt(); + if (index >= 0 && index < sessionClips.getNumElements()) { + m.clip = sessionClips.getClipAtIndex(index); + } + } + else if (!strcmp(tagName, "output")) { + int32_t index = reader.readTagOrAttributeValueInt(); + Output* thisOutput; + for (thisOutput = firstOutput; thisOutput; thisOutput = thisOutput->next) { + if (index == 0) { + break; + } + index--; + } + m.output = thisOutput; + } + else if (!strcmp(tagName, "section")) { + m.section = reader.readTagOrAttributeValueInt(); + } + } + + if ((m.kind == CLIP_LAUNCH && m.clip == nullptr) + || (m.kind == OUTPUT_CYCLE && m.output == nullptr)) { + m.kind = NO_MACRO; + } + } + else { + reader.exitTag(); + } + } + reader.exitTag("chordMem"); + } else if (!strcmp(tagName, "sections")) { // Read in all the sections while (*(tagName = reader.readNextTagOrAttributeName())) { @@ -3512,6 +3621,13 @@ void Song::deleteOrHibernateOutput(Output* output) { } void Song::deleteOutput(Output* output) { + for (int y = 0; y < 8; y++) { + auto& m = sessionMacros[y]; + if (m.kind == OUTPUT_CYCLE && m.output == output) { + m.kind = NO_MACRO; + } + } + output->deleteBackedUpParamManagers(this); void* toDealloc = dynamic_cast(output); output->~Output(); @@ -5357,6 +5473,13 @@ void Song::removeSessionClipLowLevel(Clip* clip, int32_t clipIndex) { clip->activeIfNoSolo = false; } + for (int y = 0; y < 8; y++) { + auto& m = sessionMacros[y]; + if (m.kind == CLIP_LAUNCH && m.clip == clip) { + m.kind = NO_MACRO; + } + } + sessionClips.deleteAtIndex(clipIndex); } diff --git a/src/deluge/model/song/song.h b/src/deluge/model/song/song.h index a764c68f09..f24214fc71 100644 --- a/src/deluge/model/song/song.h +++ b/src/deluge/model/song/song.h @@ -82,6 +82,21 @@ struct BackedUpParamManager { #define MAX_NOTES_CHORD_MEM 10 +enum SessionMacroKind : int8_t { + NO_MACRO = 0, + CLIP_LAUNCH, + OUTPUT_CYCLE, + SECTION, + NUM_KINDS, +}; + +struct SessionMacro { + SessionMacroKind kind; + Clip* clip; + Output* output; + uint8_t section; +}; + class Song final : public TimelineCounter { public: Song(); @@ -221,6 +236,8 @@ class Song final : public TimelineCounter { String dirPath; + SessionMacro sessionMacros[8]; + bool getAnyClipsSoloing() const; Clip* getCurrentClip(); void setCurrentClip(Clip* clip) { diff --git a/src/deluge/playback/mode/session.cpp b/src/deluge/playback/mode/session.cpp index d9f04fc829..1c9136b358 100644 --- a/src/deluge/playback/mode/session.cpp +++ b/src/deluge/playback/mode/session.cpp @@ -1217,6 +1217,9 @@ void Session::armingChanged() { view.flashPlayEnable(); } } + } else { + // TODO: only if sidebar visible! + uiNeedsRendering(getCurrentUI(), 0x00000000, 0xFFFFFFFF); } }