diff --git a/docs/community_features.md b/docs/community_features.md index 090f6d0cca..43275c16bf 100644 --- a/docs/community_features.md +++ b/docs/community_features.md @@ -397,6 +397,10 @@ Here is a list of features that have been added to the firmware as a list, group - Normal clips are dull grey. - Active clips are green, or whatever colour is set for active pads. - The colours can be changed in `SETTINGS > PADS > COLOURS > FILL/ONCE` + - `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: @@ -468,6 +472,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 @@ -938,6 +959,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 - New scales diff --git a/src/definitions_cxx.hpp b/src/definitions_cxx.hpp index d3bb434a09..9304d88d3e 100644 --- a/src/definitions_cxx.hpp +++ b/src/definitions_cxx.hpp @@ -990,7 +990,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..075360eeaa --- /dev/null +++ b/src/deluge/gui/ui/keyboard/column_controls/session.cpp @@ -0,0 +1,85 @@ +/* + * 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; + + 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 { + view.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..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..42a8dcd645 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, false); + } + 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 27dac23272..a2c205d7f4 100644 --- a/src/deluge/gui/views/instrument_clip_view.cpp +++ b/src/deluge/gui/views/instrument_clip_view.cpp @@ -245,10 +245,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(); @@ -1526,6 +1544,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, false); + } + return ActionResult::DEALT_WITH; + } else if (getCurrentOutputType() == OutputType::KIT && lastAuditionedYDisplay == y && isUIModeActive(UI_MODE_AUDITIONING) && getNumNoteRowsAuditioning() == 1) { if (velocity) { @@ -4364,12 +4392,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 0b57d81825..acb7b9ac87 100644 --- a/src/deluge/gui/views/instrument_clip_view.h +++ b/src/deluge/gui/views/instrument_clip_view.h @@ -223,6 +223,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 3d00d8ac3e..1c9d387f50 100644 --- a/src/deluge/gui/views/session_view.cpp +++ b/src/deluge/gui/views/session_view.cpp @@ -2864,24 +2864,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(); + } } } } @@ -2915,6 +2920,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 @@ -3028,9 +3038,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); } @@ -3042,7 +3072,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) { @@ -3381,6 +3412,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; @@ -3421,6 +3456,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) { @@ -3770,6 +3809,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 9760a18965..12e2d4b95b 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 }; @@ -113,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(); @@ -169,6 +171,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 7380044d6c..c048e456ea 100644 --- a/src/deluge/gui/views/view.cpp +++ b/src/deluge/gui/views/view.cpp @@ -2686,6 +2686,17 @@ 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); } @@ -2705,6 +2716,132 @@ 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, bool long_press) { + 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: + 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* 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 3f7bbcc5f9..233e24bafa 100644 --- a/src/deluge/gui/views/view.h +++ b/src/deluge/gui/views/view.h @@ -97,6 +97,12 @@ class View { ActionResult clipStatusPadAction(Clip* clip, bool on, int32_t yDisplayIfInSessionView = -1); void flashPlayEnable(); void flashPlayDisable(); + void flashPlayRoutine(); + + void activateMacro(uint32_t y, bool long_press); + 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 b7831b41e0..bdad2dd507 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,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! @@ -1794,6 +1846,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 +3620,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 +5472,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 8f6deef7df..531acd95a6 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 c9a1cc0d19..2daa96232d 100644 --- a/src/deluge/playback/mode/session.cpp +++ b/src/deluge/playback/mode/session.cpp @@ -1230,6 +1230,10 @@ void Session::armingChanged() { } } } + else { + // TODO: only if sidebar visible! + uiNeedsRendering(getCurrentUI(), 0x00000000, 0xFFFFFFFF); + } } void Session::scheduleOverdubToStartRecording(Clip* overdub, Clip* clipAbove) {