diff --git a/docs/community_features.md b/docs/community_features.md
index c38731eda2..0f1546cd62 100644
--- a/docs/community_features.md
+++ b/docs/community_features.md
@@ -409,6 +409,10 @@ Here is a list of features that have been added to the firmware as a list, group
- Track color can be changed by holding any populated clip in a column and rotating `▼︎▲︎`. For fine changes
to the color press `▼︎▲︎` while turning it.
- Section pads (left sidebar column) will allow changing repeat count while held
+ - `Purple mode`
+ - [Song macros](#419---song-macros) can be set up. First select a macro slot in the left sidebar and then press a clip to
+ put it in the slot. press the same clip multiple times to switch between macro kinds (i e affect the entire
+ output or section for the clip)
- ([#970]) Streamline recording new clips while Deluge is playing
- This assumes the Deluge is in Grid mode, you are in Green mode, the Deluge is Playing, and Recording is enabled.
- To use this feature you will need to enable it in the menu:
@@ -480,6 +484,23 @@ 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 macros
+
+Macros are a way to quickly switch playing clips without needing to go into song view.
+Within grid view, purple mode is used to edit macros. There are 8 macro slots
+shown in the left sidebar. 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: Launch or mute the clip
+- output macro: cycle though all clips for this particular output
+- section macro: Launch all clips for this section
+
+Inside a clip timeline view, hold SONG button and press the left sidebar to launch a macro.
+In keyboard view, macros are available as a sidebar control.
+SHIFT makes the launch immediate just like in song view. AFFECT ENTIRE + clip macro can be
+used to jump to edit the clip.
+
### 4.2 - Clip View - General Features (Instrument and Audio Clips)
#### 4.2.1 - Filters
@@ -950,6 +971,8 @@ to each individual note onset. ([#1978])
pads will default to the first 7 scale modes, but you can change any pad to any scale by
holding it down and turning the vertical encoder. If the scale that is going to be set
can't fit/transpose the existing notes from your clips, screen will show `Can't`.
+ - **`Song Macro Mode (SONG - various):`** Activate [Song macros](#419---song-macros).
+
- ([#2174]) With the addition of the new Keyboard Sidebar Controls, the default behaviour of being able to immediately exit the menu by pressing a sidebar pad while in Keyboard View was removed. To accomodate users that still wish to be able to exit the menus immediately by pressing a sidebar pad, a new community feature toggle has been added (`Enable KB View Sidebar Menu Exit (EXIT)`) which will enable you to immediately exit the menu using the top left sidebar pad if you are in the `SETTINGS` or `SOUND` menu for `KEYBOARD VIEW`.
#### 4.4.2 - Scales
diff --git a/src/definitions_cxx.hpp b/src/definitions_cxx.hpp
index 15ca925202..4b01794f63 100644
--- a/src/definitions_cxx.hpp
+++ b/src/definitions_cxx.hpp
@@ -997,7 +997,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..b36081eddd
--- /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 |= view.renderMacros(column, y, -1, 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;
+ m.clip = nullptr;
+ m.output = nullptr;
+ m.section = 0;
+
+ switch (m.kind) {
+ case CLIP_LAUNCH:
+ m.clip = getCurrentClip();
+ break;
+
+ case OUTPUT_CYCLE:
+ 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) {
+
+ if (pad.active) {}
+ else {
+ view.activateMacro(pad.y);
+ }
+ 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..76b992999f 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 */ {"SONG", "song 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 fc729b1470..eaa3f3cd02 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 72d4036ff6..8caada6e82 100644
--- a/src/deluge/gui/views/audio_clip_view.cpp
+++ b/src/deluge/gui/views/audio_clip_view.cpp
@@ -200,9 +200,18 @@ bool AudioClipView::renderSidebar(uint32_t whichRows, RGB image[][kDisplayWidth
return true;
}
+ int32_t macroColumn = kDisplayWidth;
+ bool armed = false;
for (int32_t y = 0; y < kDisplayHeight; y++) {
RGB* const start = &image[y][kDisplayWidth];
std::fill(start, start + kSideBarWidth, colours::black);
+
+ if (isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) {
+ armed |= view.renderMacros(macroColumn, y, -1, image, occupancyMask);
+ }
+ }
+ if (armed) {
+ view.flashPlayEnable();
}
return true;
@@ -282,10 +291,27 @@ 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);
+ 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;
+ }
uiTimerManager.unsetTimer(TimerName::UI_SPECIFIC);
@@ -502,6 +528,17 @@ ActionResult AudioClipView::padAction(int32_t x, int32_t y, int32_t on) {
}
}
}
+ else if (x == kDisplayWidth) {
+ if (isUIModeActive(UI_MODE_HOLDING_SONG_BUTTON)) {
+ if (sdRoutineLock) {
+ return ActionResult::REMIND_ME_OUTSIDE_CARD_ROUTINE;
+ }
+ if (!on) {
+ view.activateMacro(y);
+ }
+ return ActionResult::DEALT_WITH;
+ }
+ }
return ActionResult::DEALT_WITH;
}
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 f9952f312f..ccbf194531 100644
--- a/src/deluge/gui/views/instrument_clip_view.cpp
+++ b/src/deluge/gui/views/instrument_clip_view.cpp
@@ -308,10 +308,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();
@@ -1589,6 +1607,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..
+ view.activateMacro(y);
+ }
+ return ActionResult::DEALT_WITH;
+ }
else if (getCurrentOutputType() == OutputType::KIT && lastAuditionedYDisplay == y
&& isUIModeActive(UI_MODE_AUDITIONING) && getNumNoteRowsAuditioning() == 1) {
if (velocity) {
@@ -4412,12 +4440,23 @@ bool InstrumentClipView::renderSidebar(uint32_t whichRows, RGB image[][kDisplayW
return true;
}
+ int32_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 |= view.renderMacros(macroColumn, i, -1, 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 334c7a0e3c..00addec4e8 100644
--- a/src/deluge/gui/views/instrument_clip_view.h
+++ b/src/deluge/gui/views/instrument_clip_view.h
@@ -241,6 +241,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 f899496819..1c0c916f71 100644
--- a/src/deluge/gui/views/session_view.cpp
+++ b/src/deluge/gui/views/session_view.cpp
@@ -2151,7 +2151,7 @@ void SessionView::graphicsRoutine() {
}
/// display number of bars or quarter notes remaining until a launch event
-int32_t SessionView::displayLoopsRemainingPopup() {
+int32_t SessionView::displayLoopsRemainingPopup(bool ephemeral) {
int32_t sixteenthNotesRemaining = session.getNumSixteenthNotesRemainingTilLaunch();
if (sixteenthNotesRemaining > 0) {
DEF_STACK_STRING_BUF(popupMsg, 40);
@@ -2169,7 +2169,7 @@ int32_t SessionView::displayLoopsRemainingPopup() {
}
popupMsg.appendInt(quarterNotesRemaining);
}
- if (display->haveOLED()) {
+ if (display->haveOLED() && !ephemeral) {
deluge::hid::display::OLED::clearMainImage();
deluge::hid::display::OLED::drawPermanentPopupLookingText(popupMsg.c_str());
deluge::hid::display::OLED::sendMainImage();
@@ -2878,24 +2878,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) {
+ view.renderMacros(sectionColumnIndex, y, selectedMacro, 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();
+ }
}
}
}
@@ -2924,6 +2929,11 @@ void SessionView::gridRenderActionModes(int32_t y, RGB image[][kDisplayWidth + k
modeColour = colours::blue; // Blue
break;
}
+ case GridMode::MAGENTA: {
+ modeActive = (gridModeActive == SessionGridModeMacros);
+ modeColour = colours::magenta_full; // Magenta
+ break;
+ }
case GridMode::PINK: {
modeActive = performanceSessionView.gridModeActive;
modeColour = colours::magenta; // Pink
@@ -3041,6 +3051,26 @@ RGB SessionView::gridRenderClipColor(Clip* clip) {
}
RGB resultColour = RGB::fromHue(clip->output->colour);
+ bool macroActive = false;
+ 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);
+ }
+ }
// If we are not in record arming mode make this clip full color for being soloed
if ((clip->soloingInSessionMode || clip->armState == ArmState::ON_TO_SOLO) && !viewingRecordArmingActive) {
@@ -3049,7 +3079,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) {
@@ -3455,6 +3486,10 @@ ActionResult SessionView::gridHandlePads(int32_t x, int32_t y, int32_t on) {
gridModeActive = SessionGridModeEdit;
break;
}
+ case GridMode::MAGENTA: {
+ gridModeActive = SessionGridModeMacros;
+ break;
+ }
case GridMode::PINK: {
performanceSessionView.gridModeActive = true;
performanceSessionView.timeGridModePress = AudioEngine::audioSampleTimer;
@@ -3491,6 +3526,10 @@ ActionResult SessionView::gridHandlePads(int32_t x, int32_t y, int32_t on) {
modeHandleResult = gridHandlePadsLaunch(x, y, on, clip);
break;
}
+ case SessionGridModeMacros: {
+ modeHandleResult = gridHandlePadsMacros(x, y, on, clip);
+ break;
+ }
}
if (modeHandleResult == ActionResult::DEALT_WITH) {
@@ -3825,6 +3864,43 @@ void SessionView::gridHandlePadsLaunchToggleArming(Clip* clip, bool immediate) {
}
}
+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;
+ }
+ 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 061f2ba1b6..24d60c766b 100644
--- a/src/deluge/gui/views/session_view.h
+++ b/src/deluge/gui/views/session_view.h
@@ -32,6 +32,7 @@ class ModelStackWithTimelineCounter;
enum SessionGridMode : uint8_t {
SessionGridModeEdit,
SessionGridModeLaunch,
+ SessionGridModeMacros,
SessionGridModeMaxElement // Keep as boundary
};
@@ -64,7 +65,7 @@ class SessionView final : public ClipNavigationTimelineView {
bool renderRow(ModelStack* modelStack, uint8_t yDisplay, RGB thisImage[kDisplayWidth + kSideBarWidth],
uint8_t thisOccupancyMask[kDisplayWidth + kSideBarWidth], bool drawUndefinedArea = true);
void graphicsRoutine();
- int32_t displayLoopsRemainingPopup();
+ int32_t displayLoopsRemainingPopup(bool ephemeral = false);
void potentiallyRenderClipLaunchPlayhead(bool reallyNoTickSquare, int32_t sixteenthNotesRemaining);
void requestRendering(UI* ui, uint32_t whichMainRows = 0xFFFFFFFF, uint32_t whichSideRows = 0xFFFFFFFF);
@@ -114,6 +115,7 @@ class SessionView final : public ClipNavigationTimelineView {
// action to set this to false
uint8_t sectionPressed;
uint8_t masterCompEditMode;
+ int8_t selectedMacro = -1;
Clip* getClipForLayout();
@@ -179,6 +181,7 @@ class SessionView final : public ClipNavigationTimelineView {
ActionResult gridHandlePadsLaunch(int32_t x, int32_t y, int32_t on, Clip* clip);
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 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 e94c366fb5..872611cc23 100644
--- a/src/deluge/gui/views/view.cpp
+++ b/src/deluge/gui/views/view.cpp
@@ -2644,6 +2644,18 @@ 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 {
+ // TODO: sidebar might not actually be visible, flash song button in that case?
+ uiNeedsRendering(getCurrentUI(), 0x00000000, 0xFFFFFFFF);
+ }
+}
+
void View::flashPlayEnable() {
uiTimerManager.setTimer(TimerName::PLAY_ENABLE_FLASH, kFastFlashTime);
}
@@ -2663,6 +2675,119 @@ void View::flashPlayDisable() {
#endif
}
+bool View::renderMacros(int32_t column, uint32_t y, int32_t selectedMacro, RGB image[][kDisplayWidth + kSideBarWidth],
+ uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth]) {
+ uint8_t brightness = 1;
+ uint8_t otherChannels = 0;
+
+ bool is_active = selectedMacro == y;
+ bool is_other_active = 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 View::activateMacro(uint32_t y) {
+ if (y > 8) {
+ return;
+ }
+
+ SessionMacro& m = currentSong->sessionMacros[y];
+ switch (m.kind) {
+ case CLIP_LAUNCH:
+ if (Buttons::isButtonPressed(deluge::hid::button::AFFECT_ENTIRE)) {
+ if (getCurrentClip() != m.clip) {
+ sessionView.transitionToViewForClip(m.clip);
+ }
+ }
+ else {
+ session.toggleClipStatus(m.clip, nullptr, Buttons::isShiftButtonPressed(), kInternalButtonPressLatency);
+ }
+ break;
+
+ case OUTPUT_CYCLE: {
+ Clip* nextClip = findNextClipForOutput(m.output);
+ if (nextClip) {
+ session.toggleClipStatus(nextClip, nullptr, Buttons::isShiftButtonPressed(), kInternalButtonPressLatency);
+ }
+ break;
+ }
+
+ case SECTION:
+ session.armSection(m.section, kInternalButtonPressLatency);
+ break;
+
+ default:
+ break;
+ }
+}
+
+Clip* View::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;
+}
+
/*
char modelStackMemory[MODEL_STACK_MAX_SIZE];
ModelStackWithThreeMainThings* modelStack = setupModelStack(modelStackMemory);
diff --git a/src/deluge/gui/views/view.h b/src/deluge/gui/views/view.h
index 7eae5918c0..177f63b349 100644
--- a/src/deluge/gui/views/view.h
+++ b/src/deluge/gui/views/view.h
@@ -96,6 +96,12 @@ class View {
ActionResult clipStatusPadAction(Clip* clip, bool on, int32_t yDisplayIfInSessionView = -1);
void flashPlayEnable();
void flashPlayDisable();
+ void flashPlayRoutine();
+
+ void activateMacro(uint32_t y);
+ Clip* findNextClipForOutput(Output* output);
+ bool renderMacros(int32_t column, uint32_t y, int32_t selectedMacro, RGB image[][kDisplayWidth + kSideBarWidth],
+ uint8_t occupancyMask[][kDisplayWidth + kSideBarWidth]);
// MIDI learn stuff
MidiLearn thingPressedForMidiLearn = MidiLearn::NONE;
diff --git a/src/deluge/model/song/song.cpp b/src/deluge/model/song/song.cpp
index 7b5a57b495..172bb1cc05 100644
--- a/src/deluge/model/song/song.cpp
+++ b/src/deluge/model/song/song.cpp
@@ -1237,6 +1237,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");
}
@@ -1253,6 +1302,9 @@ 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!
@@ -1655,6 +1707,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())) {
@@ -3237,6 +3345,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();
@@ -5082,6 +5197,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 d6c1eac95c..ed7cf82e73 100644
--- a/src/deluge/model/song/song.h
+++ b/src/deluge/model/song/song.h
@@ -84,6 +84,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();
@@ -222,6 +237,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 155875304a..73b1a131b2 100644
--- a/src/deluge/playback/mode/session.cpp
+++ b/src/deluge/playback/mode/session.cpp
@@ -1232,6 +1232,10 @@ void Session::armingChanged() {
}
}
}
+ else {
+ // TODO: only if sidebar visible!
+ uiNeedsRendering(getCurrentUI(), 0x00000000, 0xFFFFFFFF);
+ }
}
void Session::scheduleOverdubToStartRecording(Clip* overdub, Clip* clipAbove) {