From 374d3edd486303945393903018b51d3c91891f2f Mon Sep 17 00:00:00 2001 From: Marshall Date: Wed, 29 Jan 2025 14:38:51 -0700 Subject: [PATCH] NavigationView initial commit --- .../gui/ui/keyboard/keyboard_screen.cpp | 10 +- src/deluge/gui/views/arranger_view.cpp | 17 + src/deluge/gui/views/automation_view.cpp | 55 +- src/deluge/gui/views/navigation_view.cpp | 486 ++++++++++++++++++ src/deluge/gui/views/navigation_view.h | 58 +++ src/deluge/gui/views/performance_view.cpp | 61 ++- src/deluge/gui/views/session_view.cpp | 49 +- src/deluge/gui/views/timeline_view.cpp | 21 +- src/deluge/gui/views/view.cpp | 17 +- .../model/clip/instrument_clip_minder.cpp | 5 + src/deluge/playback/playback_handler.cpp | 12 +- 11 files changed, 750 insertions(+), 41 deletions(-) create mode 100644 src/deluge/gui/views/navigation_view.cpp create mode 100644 src/deluge/gui/views/navigation_view.h diff --git a/src/deluge/gui/ui/keyboard/keyboard_screen.cpp b/src/deluge/gui/ui/keyboard/keyboard_screen.cpp index e945cc082c..aa3566df0d 100644 --- a/src/deluge/gui/ui/keyboard/keyboard_screen.cpp +++ b/src/deluge/gui/ui/keyboard/keyboard_screen.cpp @@ -24,6 +24,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/session_view.h" #include "gui/views/view.h" #include "hid/buttons.h" @@ -727,8 +728,13 @@ void KeyboardScreen::selectLayout(int8_t offset) { } getCurrentInstrumentClip()->keyboardState.currentLayout = (KeyboardLayoutType)nextLayout; - if (getCurrentInstrumentClip()->keyboardState.currentLayout != lastLayout) { - display->displayPopup(l10n::get(layoutList[getCurrentInstrumentClip()->keyboardState.currentLayout]->name())); + const char* name = l10n::get(layoutList[getCurrentInstrumentClip()->keyboardState.currentLayout]->name()); + if (naviview.useNavigationView()) { + naviview.textBuffer = name; + naviview.drawDashboard(); + } + else if (getCurrentInstrumentClip()->keyboardState.currentLayout != lastLayout) { + display->displayPopup(name); } // Ensure scale mode is as expected diff --git a/src/deluge/gui/views/arranger_view.cpp b/src/deluge/gui/views/arranger_view.cpp index ac04fef888..c08efa5ff0 100644 --- a/src/deluge/gui/views/arranger_view.cpp +++ b/src/deluge/gui/views/arranger_view.cpp @@ -32,6 +32,7 @@ #include "gui/views/audio_clip_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/session_view.h" #include "gui/views/view.h" #include "gui/waveform/waveform_renderer.h" @@ -494,6 +495,9 @@ void ArrangerView::clearArrangement() { } uiNeedsRendering(this, 0xFFFFFFFF, 0); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } } bool ArrangerView::opened() { @@ -2910,6 +2914,9 @@ ActionResult ArrangerView::horizontalScrollOneSquare(int32_t direction) { } uiNeedsRendering(this, 0xFFFFFFFF, 0); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } reassessWhetherDoingAutoScroll(); } @@ -3136,6 +3143,9 @@ void ArrangerView::graphicsRoutine() { if (currentUIMode != UI_MODE_HORIZONTAL_ZOOM) { uiNeedsRendering(ui, 0xFFFFFFFF, 0); } + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } } } @@ -3174,6 +3184,10 @@ void ArrangerView::graphicsRoutine() { PadLEDs::setTickSquares(tickSquares, colours); lastTickSquare = newTickSquare; + + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } } } @@ -3231,6 +3245,9 @@ void ArrangerView::autoScrollOnPlaybackEnd() { sharpJump: currentSong->xScroll[NAVIGATION_ARRANGEMENT] = newScrollPos; uiNeedsRendering(this, 0xFFFFFFFF, 0); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } } } } diff --git a/src/deluge/gui/views/automation_view.cpp b/src/deluge/gui/views/automation_view.cpp index 511bb69726..3d73f744d3 100644 --- a/src/deluge/gui/views/automation_view.cpp +++ b/src/deluge/gui/views/automation_view.cpp @@ -37,6 +37,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/audio_clip_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/session_view.h" #include "gui/views/timeline_view.h" #include "gui/views/view.h" @@ -1276,6 +1277,12 @@ void AutomationView::renderAutomationOverviewDisplayOLED(deluge::hid::display::o deluge::hid::display::OLED::drawPermanentPopupLookingText(overviewText); } else { + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + naviview.drawMainboard(); + naviview.drawBaseboard(); + return; + } overviewText = l10n::get(l10n::String::STRING_FOR_AUTOMATION_OVERVIEW); canvas.drawStringCentred(overviewText, yPos, kTextSpacingX, kTextSpacingY); } @@ -1293,7 +1300,15 @@ void AutomationView::renderAutomationEditorDisplayOLED(deluge::hid::display::ole #else int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 3; #endif - canvas.drawStringCentredShrinkIfNecessary(parameterName.c_str(), yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.parameterName.clear(); + naviview.parameterName.append(parameterName.c_str()); + naviview.knobPosLeft = knobPosLeft; + naviview.knobPosRight = knobPosRight; + } + else { + canvas.drawStringCentredShrinkIfNecessary(parameterName.c_str(), yPos, kTextSpacingX, kTextSpacingY); + } // display automation status yPos = yPos + 12; @@ -1326,12 +1341,22 @@ void AutomationView::renderAutomationEditorDisplayOLED(deluge::hid::display::ole } } - canvas.drawStringCentred(isAutomated, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.isAutomated = isAutomated; + naviview.drawDashboard(); + naviview.drawMainboard(); + } + else { + canvas.drawStringCentred(isAutomated, yPos, kTextSpacingX, kTextSpacingY); + } // display parameter value yPos = yPos + 12; - if (knobPosRight != kNoSelection) { + if (naviview.useNavigationView()) { + naviview.drawBaseboard(); + } + else if (knobPosRight != kNoSelection) { char bufferLeft[10]; bufferLeft[0] = 'L'; bufferLeft[1] = ':'; @@ -1367,7 +1392,16 @@ void AutomationView::renderNoteEditorDisplayOLED(deluge::hid::display::oled_canv #else int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 3; #endif - canvas.drawStringCentredShrinkIfNecessary(parameterName.c_str(), yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.parameterName.clear(); + naviview.parameterName.append(parameterName.c_str()); + naviview.knobPosLeft = knobPosLeft; + naviview.knobPosRight = knobPosRight; + naviview.drawDashboard(); + } + else { + canvas.drawStringCentredShrinkIfNecessary(parameterName.c_str(), yPos, kTextSpacingX, kTextSpacingY); + } // display note / drum name yPos = yPos + 12; @@ -1407,12 +1441,21 @@ void AutomationView::renderNoteEditorDisplayOLED(deluge::hid::display::oled_canv } } - canvas.drawStringCentred(noteRowName, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + strncpy(naviview.noteRowName, noteRowName, 49); + naviview.drawMainboard(); + } + else { + canvas.drawStringCentred(noteRowName, yPos, kTextSpacingX, kTextSpacingY); + } // display parameter value yPos = yPos + 12; - if (automationParamType == AutomationParamType::NOTE_VELOCITY) { + if (naviview.useNavigationView()) { + naviview.drawBaseboard(); + } + else if (automationParamType == AutomationParamType::NOTE_VELOCITY) { if (knobPosRight != kNoSelection) { char bufferLeft[10]; bufferLeft[0] = 'L'; diff --git a/src/deluge/gui/views/navigation_view.cpp b/src/deluge/gui/views/navigation_view.cpp new file mode 100644 index 0000000000..81df4cfcf6 --- /dev/null +++ b/src/deluge/gui/views/navigation_view.cpp @@ -0,0 +1,486 @@ +/* + * Copyright © 2014-2023 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 "gui/views/navigation_view.h" +#include "definitions_cxx.hpp" +#include "extern.h" +#include "gui/l10n/l10n.h" +#include "gui/ui/root_ui.h" +#include "gui/ui/ui.h" +#include "gui/views/arranger_view.h" +#include "gui/views/automation_view.h" +#include "gui/views/performance_view.h" +#include "gui/views/session_view.h" +#include "hid/display/display.h" +#include "hid/display/oled.h" +#include "io/midi/midi_engine.h" +#include "lib/printf.h" +#include "model/clip/clip.h" +#include "model/instrument/cv_instrument.h" +#include "model/instrument/instrument.h" +#include "model/instrument/kit.h" +#include "model/instrument/melodic_instrument.h" +#include "model/instrument/midi_instrument.h" +#include "model/song/song.h" +#include "navigation_view.h" +#include "playback/mode/session.h" +#include "processing/audio_output.h" +#include "util/d_string.h" + +using namespace deluge; +using namespace gui; + +NavigationView naviview{}; + +NavigationView::NavigationView() { + knobPosLeft = 0; + knobPosRight = 0; +} + +// Returns true when In-Key Keyboard is set +// as default and the Deluge has an OLED display. +bool NavigationView::useNavigationView() { + return (display->haveOLED() && FlashStorage::defaultKeyboardLayout == KeyboardLayoutType::KeyboardLayoutTypeInKey); +} + +// Top Line Dashboard on the OLED screen +void NavigationView::drawDashboard() { + DEF_STACK_STRING_BUF(info, 25); + char buffer[25]; + const char* name; + + hasTempoBPM = false; + hasScale = false; + RootUI* rootUI = getRootUI(); + UIType rootUIType = rootUI->getUIType(); + bool isSessionView = (rootUI == &sessionView); + bool isPerformanceView = (rootUI == &performanceView); + bool isArrangerView = (rootUI == &arrangerView); + bool isAudioClipView = (rootUI == &audioClipView); + bool isAutomationView = (rootUI == &automationView); + bool isAutomationOverview = (isAutomationView && ((AutomationView*)rootUI)->onAutomationOverview()); + bool isKeyboardView = (rootUIType == UIType::KEYBOARD_SCREEN); + Clip* clip = getCurrentClip(); + Output* output = getCurrentOutput(); + OutputType outputType = output->type; + bool isKit = outputType == OutputType::KIT; + + int32_t channel = -1; + switch (outputType) { + case OutputType::MIDI_OUT: + case OutputType::CV: + channel = ((NonAudioInstrument*)output)->getChannel(); + break; + case OutputType::AUDIO: + channel = static_cast(((AudioOutput*)output)->mode); + break; + default: + break; + } + const char* outputTypeText = getOutputTypeName(outputType, channel); + + int32_t root = currentSong->key.rootNote; + Scale scale = currentSong->getCurrentScale(); + + int32_t navSysId; + uint32_t maxLength{0}; + + if (isArrangerView) { + navSysId = NAVIGATION_ARRANGEMENT; + } + else { + navSysId = NAVIGATION_CLIP; + } + + uint32_t xScroll = currentSong->xScroll[navSysId]; + uint32_t noteLength = currentSong->xZoom[navSysId]; + if (isArrangerView) { + maxLength = ((ArrangerView*)rootUI)->getMaxLength(); + } + else if (isSessionView) { + maxLength = ((SessionView*)rootUI)->getMaxLength(); + } + else if (clip) { + maxLength = clip->getMaxLength(); + } + + int32_t magnitude = getNoteMagnitudeFfromNoteLength(noteLength, currentSong->getInputTickMagnitude()); + // Positive magnitudes are bars, negative magnitudes are divisions of bars. + uint32_t numBars = (uint32_t)1 << magnitude; + uint32_t division = (uint32_t)1 << (0 - magnitude); + + // Get Number of Bars and Beats + uint32_t oneBar = currentSong->getBarLength(); + uint32_t totalBarsInClip = maxLength / oneBar; + uint32_t whichBar = xScroll / oneBar; + uint32_t posWithinBar = xScroll - whichBar * oneBar; + uint32_t whichBeat = posWithinBar / (oneBar >> 2); + uint32_t posWithinBeat = posWithinBar - whichBeat * (oneBar >> 2); + uint32_t whichSubBeat = posWithinBeat / (oneBar >> 4); + whichBar++; + whichBeat++; + whichSubBeat++; + + if (totalBarsInClip < whichBar) { // 1/0 or 3/2 appear when the last measure is less than 4 beats. + totalBarsInClip = whichBar; + } + + if (isArrangerView && magnitude < 0 && division >= 8) { + sprintf(buffer, "%d/%d:%d:%d", whichBar, totalBarsInClip, whichBeat, whichSubBeat); + } + else if (isArrangerView && magnitude < 0 && division >= 2) { + sprintf(buffer, "%d/%d:%d", whichBar, totalBarsInClip, whichBeat); + } + else if (magnitude < 0 && division == 128) { + sprintf(buffer, "%d/%d:%d:%d", whichBar, totalBarsInClip, whichBeat, whichSubBeat); + } + else if (magnitude < 0 && division >= 32) { + sprintf(buffer, "%d/%d:%d", whichBar, totalBarsInClip, whichBeat); + } + else { + sprintf(buffer, "%d/%d", whichBar, totalBarsInClip); + } + + if (isKeyboardView || isPerformanceView || isAutomationOverview) { + // do nothing, navigation is not available, label added below + } + else { + info.append(buffer); + } + + // Get Subdivision + if (magnitude < 0) { + sprintf(buffer, " 1/%d", division); + } + else { + sprintf(buffer, " %dB/C", numBars); + } + if (isKeyboardView || isPerformanceView || isAutomationOverview) { + // do nothing, navigation is not available, label added below + } + else { + info.append(buffer); + } + + if (isPerformanceView) { + name = l10n::get(l10n::String::STRING_FOR_PERFORM_VIEW); + info.append(name); + } + else if (isAutomationOverview) { + name = l10n::get(l10n::String::STRING_FOR_AUTOMATION_OVERVIEW); + info.append(name); + } + else if (isKeyboardView) { + info.append(textBuffer); + } + else if (isAutomationView) { + if (strcmp(isAutomated, l10n::get(l10n::String::STRING_FOR_AUTOMATION_ON)) == 0) { + info.append(" "); // Text added after navigation + info.append(isAutomated); + } + } + else if (isAudioClipView) { + info.append(" "); // text added after navigation + info.append(outputTypeText + 6); // Skip 'Audio ' + } + else if (!isKit) { // Key and Scale added after navigation + hasScale = true; + // Get Key and Scale + clearBuffer(buffer, 25); + int32_t isNatural = 1; // gets modified inside noteCodeToString to be 0 if sharp. + noteCodeToString(root, buffer, &isNatural, false); + // noteCodeToString(root, buffer, false, root, scale); + info.append(" "); + info.append(buffer); + NoteSet notes = currentSong->key.modeNotes; + bool moreMajor = (notes.majorness() >= 0); + if (!moreMajor) { + info.append("-"); + } + if (scale != MAJOR_SCALE && scale != MINOR_SCALE) { + sprintf(buffer, " %s", getScaleName(scale)); + for (int i = 1; i < 25 - 1; ++i) { + if (buffer[i] == 0) + break; + if (buffer[i] == ' ' && buffer[i + 1] == 'M') { // Strip off ' Minor' + buffer[i] = 0; + break; + } + } + info.append(buffer); + } + } + +#if OLED_MAIN_HEIGHT_PIXELS == 64 + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 12; +#else + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 3; +#endif + hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; + canvas.clearAreaExact(0, yPos, OLED_MAIN_WIDTH_PIXELS - 1, yPos + kTextSpacingY); + canvas.drawString(info.data(), 0, yPos, kTextSpacingX, kTextSpacingY); + if (!isAudioClipView && !isAutomationView + && (isSessionView || isArrangerView || isPerformanceView || scale == MAJOR_SCALE || scale == MINOR_SCALE)) { + drawTempoBPM(); + } + hid::display::OLED::markChanged(); +} + +// Middle Line of OLED screen +void NavigationView::drawMainboard(const char* nameToDraw) { + DEF_STACK_STRING_BUF(info, 25); + char buffer[12]; + const char* name; + RootUI* rootUI = getRootUI(); + UIType rootUIType = rootUI->getUIType(); + Clip* clip = getCurrentClip(); + Output* output = getCurrentOutput(); + OutputType outputType = output->type; + Instrument* instrument = (Instrument*)output; + + int32_t channel{0}; + int32_t channelSuffix{0}; + switch (outputType) { + case OutputType::MIDI_OUT: + channelSuffix = ((MIDIInstrument*)instrument)->channelSuffix; + // no break; + case OutputType::CV: + channel = ((NonAudioInstrument*)output)->getChannel(); + break; + case OutputType::AUDIO: + channel = static_cast(((AudioOutput*)output)->mode); + break; + default: + break; + } + const char* outputTypeText = getOutputTypeName(outputType, channel); + + switch (rootUIType) { + case UIType::INSTRUMENT_CLIP: + case UIType::AUDIO_CLIP: + case UIType::AUDIO_RECORDER: + case UIType::AUTOMATION: + case UIType::KEYBOARD_SCREEN: + if (outputType == OutputType::MIDI_OUT) { + if (strncmp(outputTypeText, "Int", 3) == 0) { + name = "Int."; + } + else { + name = outputTypeText; + } + } + else if (outputType == OutputType::CV) { + name = "CV"; + } + else { + if (nameToDraw != nullptr) { // current output is old, load_instrument_preset_ui + // calls drawOutputNameFromDetails before loading is complete + // so use the parameter instead. + name = nameToDraw; + } + else { + name = getCurrentOutput()->name.get(); + } + } + info.append(name); + break; + case UIType::SESSION: + case UIType::ARRANGER: + case UIType::PERFORMANCE: + case UIType::LOAD_SONG: + case UIType::NONE: + if (nameToDraw != nullptr) { // current output is old, load_instrument_preset_ui + // calls drawOutputNameFromDetails before loading is complete + // so use the parameter instead. + name = nameToDraw; + } + else if (currentSong->name.isEmpty()) { + name = "UNSAVED"; + } + else { + name = currentSong->name.get(); + } + info.append(name); + break; + default: + return; // nothing to draw + } + + if (outputType == OutputType::MIDI_OUT) { + info.append(" "); + if (channel < 16) { + slotToString(channel + 1, channelSuffix, buffer, 1); + info.append(buffer); + } + else if (channel == MIDI_CHANNEL_MPE_LOWER_ZONE || channel == MIDI_CHANNEL_MPE_UPPER_ZONE) { + info.append((channel == MIDI_CHANNEL_MPE_LOWER_ZONE) ? "Lower" : "Upper"); + } + else { + info.append("Transpose"); + } + } + if (outputType == OutputType::CV) { + info.append(" "); + if (channel < both) { + info.appendInt(channel + 1); + } + else { + info.append("1 and 2"); + } + } + + deluge::hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; +#if OLED_MAIN_HEIGHT_PIXELS == 64 + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 30; +#else + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 17; +#endif + canvas.clearAreaExact(0, yPos, OLED_MAIN_WIDTH_PIXELS - 1, yPos + kTextSpacingY); + int32_t stringLengthPixels = canvas.getStringWidthInPixels(name, kTextTitleSizeY); + if (stringLengthPixels <= OLED_MAIN_WIDTH_PIXELS) { + canvas.drawStringCentred(info.data(), yPos, kTextTitleSpacingX, kTextTitleSizeY); + } + else { + canvas.drawString(info.data(), 0, yPos, kTextTitleSpacingX, kTextTitleSizeY); + deluge::hid::display::OLED::setupSideScroller(0, name, 0, OLED_MAIN_WIDTH_PIXELS, yPos, yPos + kTextTitleSizeY, + kTextTitleSpacingX, kTextTitleSizeY, false); + } + hid::display::OLED::markChanged(); +} + +// Bottom Line of OLED screen +void NavigationView::drawBaseboard() { + DEF_STACK_STRING_BUF(info, 25); + char const* baseText; + RootUI* rootUI = getRootUI(); + UIType rootUIType = rootUI->getUIType(); + Clip* clip = getCurrentClip(); + Output* output = getCurrentOutput(); + OutputType outputType = output->type; + + switch (rootUIType) { + case UIType::PERFORMANCE: + if (!parameterName.empty()) { + info.append(parameterName.data()); + info.append(":"); + if (textBuffer) { + info.append(textBuffer); + } + else { + info.appendInt(knobPosLeft); + } + } + break; + case UIType::AUTOMATION: + if (((AutomationView*)rootUI)->inAutomationEditor()) { + // display parameter name + info.append(parameterName.data()); + if (knobPosRight != kNoSelection) { + info.append(":L"); + info.appendInt(knobPosLeft); + info.append("-R"); + info.appendInt(knobPosRight); + } + else { + info.append(":"); + info.appendInt(knobPosLeft); + } + } + else if (((AutomationView*)rootUI)->inNoteEditor()) { + if (((AutomationView*)rootUI)->automationParamType == AutomationParamType::NOTE_VELOCITY) { + info.append("Velocity: "); + info.append(noteRowName); + } + } + break; + case UIType::INSTRUMENT_CLIP: + case UIType::AUDIO_CLIP: + case UIType::AUDIO_RECORDER: + case UIType::KEYBOARD_SCREEN: + if (clip != nullptr) { + if (clip->name.isEmpty()) { + info.append("Section "); + info.appendInt(clip->section + 1); + } + else { + info.appendInt(clip->section + 1); + info.append(": "); + info.append(clip->name.get()); + } + } + break; + case UIType::SESSION: + case UIType::ARRANGER: + case UIType::LOAD_SONG: + case UIType::NONE: + default: + break; + } + + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 32; + hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; + canvas.clearAreaExact(0, yPos, OLED_MAIN_WIDTH_PIXELS - 1, yPos + kTextSpacingY); + canvas.drawString(info.data(), 0, yPos, kTextSpacingX, kTextSpacingY); + deluge::hid::display::OLED::setupSideScroller(1, info.data(), 0, OLED_MAIN_WIDTH_PIXELS, yPos, yPos + kTextSpacingY, + kTextSpacingX, kTextSpacingY, false); + hid::display::OLED::markChanged(); +} + +void NavigationView::drawRemainingCountdown(const char* msg) { + int32_t sixteenthNotesRemaining = session.getNumSixteenthNotesRemainingTilLaunch(); + int32_t barsRemaining = ((sixteenthNotesRemaining - 1) / 16) + 1; + int32_t quarterNotesRemaining = ((sixteenthNotesRemaining - 1) / 4) + 1; + int32_t rndQuarterNotesRemaining = ((quarterNotesRemaining - 1) % 4) + 1; + + DEF_STACK_STRING_BUF(buffer, 10); + buffer.append(" Q"); + buffer.appendInt(barsRemaining); + buffer.append(":"); + buffer.appendInt(rndQuarterNotesRemaining); + + int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 32; + hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; + + canvas.clearAreaExact(OLED_MAIN_WIDTH_PIXELS - (kTextSpacingX * buffer.length()), yPos, OLED_MAIN_WIDTH_PIXELS - 1, + yPos + kTextSpacingY); + if (strncmp(msg, "Bars ", 5) || strncmp(msg, "Beats", 5) || strncmp(msg, "Loops", 5)) { + canvas.drawStringAlignRight(buffer.data(), yPos, kTextSpacingX, kTextSpacingY); + } + else { + canvas.drawStringAlignRight(msg, yPos, kTextSpacingX, kTextSpacingY); + } + hid::display::OLED::markChanged(); +} + +void NavigationView::drawTempoBPM() { + hasTempoBPM = true; + DEF_STACK_STRING_BUF(tempoBPM, 10); + sessionView.lastDisplayedTempo = playbackHandler.calculateBPM(playbackHandler.getTimePerInternalTickFloat()); + playbackHandler.getTempoStringForOLED(sessionView.lastDisplayedTempo, tempoBPM); + hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; + sessionView.displayTempoBPM(canvas, tempoBPM, true); + deluge::hid::display::OLED::markChanged(); +} + +void NavigationView::clearBuffer(char* buffer, int8_t bufferLength) { + int8_t i; + for (i = 0; i < bufferLength; ++i) { + if (buffer[i] != 0) { + buffer[i] = 0; + } + } +} diff --git a/src/deluge/gui/views/navigation_view.h b/src/deluge/gui/views/navigation_view.h new file mode 100644 index 0000000000..6ddc54a929 --- /dev/null +++ b/src/deluge/gui/views/navigation_view.h @@ -0,0 +1,58 @@ +/* + * Copyright © 2014-2023 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 "definitions_cxx.hpp" +#include "util/d_string.h" +#include + +class InstrumentClip; +class AudioClip; +class UI; +class RootUI; +class Output; + +// Active view of current state of the Deluge +class NavigationView { +public: + NavigationView(); + + // AutomationView parameters + int32_t knobPosLeft; + int32_t knobPosRight; + const char* isAutomated; + DEF_STACK_STRING_BUF(parameterName, 25); + char noteRowName[50]; // Used by VELOCITY automation + + const char* textBuffer; // Used by KEYBOARD and PERFORMANCE as label to replace navigation + bool hasTempoBPM; // True when BPM is drawn on dashboard + bool hasScale; // True when Scale is drawn on dashboard + + bool useNavigationView(); + void drawDashboard(); + void drawMainboard(const char* nameToDraw = nullptr); + void drawBaseboard(); + void drawRemainingCountdown(const char* msg = nullptr); + void drawTempoBPM(); + +private: + void clearBuffer(char* buffer, int8_t bufferLength = 0); + const char* retrieveOutputTypeName(Output* output); +}; + +extern NavigationView naviview; diff --git a/src/deluge/gui/views/performance_view.cpp b/src/deluge/gui/views/performance_view.cpp index a7c84fb060..5341fc6d38 100644 --- a/src/deluge/gui/views/performance_view.cpp +++ b/src/deluge/gui/views/performance_view.cpp @@ -27,6 +27,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/session_view.h" #include "gui/views/view.h" #include "hid/button.h" @@ -535,10 +536,16 @@ void PerformanceView::renderViewDisplay() { yPos = yPos + 12; - // Render "Performance View" in the middle of the OLED screen - image.drawStringCentred(l10n::get(l10n::String::STRING_FOR_PERFORM_VIEW), yPos, kTextSpacingX, - kTextSpacingY); - + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + naviview.drawMainboard(); + naviview.drawBaseboard(); + } + else { + // Render "Performance View" in the middle of the OLED screen + image.drawStringCentred(l10n::get(l10n::String::STRING_FOR_PERFORM_VIEW), yPos, kTextSpacingX, + kTextSpacingY); + } deluge::hid::display::OLED::markChanged(); } else { @@ -565,7 +572,16 @@ void PerformanceView::renderFXDisplay(params::Kind paramKind, int32_t paramID, i #endif yPos = yPos + 12; - image.drawStringCentred(parameterName, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.parameterName.clear(); + naviview.parameterName.append(parameterName); + naviview.drawDashboard(); + naviview.drawMainboard(); + naviview.drawBaseboard(); + } + else { + image.drawStringCentred(parameterName, yPos, kTextSpacingX, kTextSpacingY); + } deluge::hid::display::OLED::markChanged(); } @@ -587,7 +603,16 @@ void PerformanceView::renderFXDisplay(params::Kind paramKind, int32_t paramID, i #else int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 3; #endif - image.drawStringCentred(parameterName, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.parameterName.clear(); + naviview.parameterName.append(parameterName); + naviview.drawDashboard(); + naviview.drawMainboard(); + naviview.drawBaseboard(); + } + else { + image.drawStringCentred(parameterName, yPos, kTextSpacingX, kTextSpacingY); + } // display parameter value yPos = yPos + 24; @@ -610,12 +635,28 @@ void PerformanceView::renderFXDisplay(params::Kind paramKind, int32_t paramID, i else { // 64ths stutter: all 4 leds turned on buffer = "64ths"; } - image.drawStringCentred(buffer, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.knobPosLeft = kNoSelection; + naviview.knobPosRight = kNoSelection; + naviview.textBuffer = buffer; + naviview.drawBaseboard(); + } + else { + image.drawStringCentred(buffer, yPos, kTextSpacingX, kTextSpacingY); + } } else { - char buffer[5]; - intToString(knobPos, buffer); - image.drawStringCentred(buffer, yPos, kTextSpacingX, kTextSpacingY); + if (naviview.useNavigationView()) { + naviview.knobPosLeft = knobPos; + naviview.knobPosRight = kNoSelection; + naviview.textBuffer = nullptr; + naviview.drawBaseboard(); + } + else { + char buffer[5]; + intToString(knobPos, buffer); + image.drawStringCentred(buffer, yPos, kTextSpacingX, kTextSpacingY); + } } deluge::hid::display::OLED::markChanged(); diff --git a/src/deluge/gui/views/session_view.cpp b/src/deluge/gui/views/session_view.cpp index 83e05f3258..420a854177 100644 --- a/src/deluge/gui/views/session_view.cpp +++ b/src/deluge/gui/views/session_view.cpp @@ -39,6 +39,7 @@ #include "gui/views/audio_clip_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/performance_view.h" #include "gui/views/view.h" #include "gui/waveform/waveform_renderer.h" @@ -1906,8 +1907,13 @@ void SessionView::renderOLED(deluge::hid::display::oled_canvas::Canvas& canvas) if (currentPlaybackMode == &session) { if (session.launchEventAtSwungTickCount) { intToString(session.numRepeatsTilLaunch, &loopsRemainingText[17]); - deluge::hid::display::OLED::clearMainImage(); - deluge::hid::display::OLED::drawPermanentPopupLookingText(loopsRemainingText); + if (naviview.useNavigationView()) { + naviview.drawRemainingCountdown(loopsRemainingText); + } + else { + deluge::hid::display::OLED::clearMainImage(); + deluge::hid::display::OLED::drawPermanentPopupLookingText(loopsRemainingText); + } } } @@ -2028,11 +2034,15 @@ void SessionView::renderViewDisplay() { #else int32_t yPos = OLED_MAIN_TOPMOST_PIXEL + 3; #endif - - DEF_STACK_STRING_BUF(tempoBPM, 10); - lastDisplayedTempo = playbackHandler.calculateBPM(playbackHandler.getTimePerInternalTickFloat()); - playbackHandler.getTempoStringForOLED(lastDisplayedTempo, tempoBPM); - displayTempoBPM(canvas, tempoBPM, false); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } + else { + DEF_STACK_STRING_BUF(tempoBPM, 10); + lastDisplayedTempo = playbackHandler.calculateBPM(playbackHandler.getTimePerInternalTickFloat()); + playbackHandler.getTempoStringForOLED(lastDisplayedTempo, tempoBPM); + displayTempoBPM(canvas, tempoBPM, false); + } #if OLED_MAIN_HEIGHT_PIXELS == 64 yPos = OLED_MAIN_TOPMOST_PIXEL + 30; @@ -2050,7 +2060,10 @@ void SessionView::renderViewDisplay() { int32_t stringLengthPixels = canvas.getStringWidthInPixels(name, kTextTitleSizeY); - if (stringLengthPixels <= OLED_MAIN_WIDTH_PIXELS) { + if (naviview.useNavigationView()) { + naviview.drawMainboard(); + } + else if (stringLengthPixels <= OLED_MAIN_WIDTH_PIXELS) { canvas.drawStringCentred(name, yPos, kTextTitleSpacingX, kTextTitleSizeY); } else { @@ -2061,9 +2074,14 @@ void SessionView::renderViewDisplay() { yPos = OLED_MAIN_TOPMOST_PIXEL + 32; - DEF_STACK_STRING_BUF(rootNoteAndScaleName, 40); - currentSong->getCurrentRootNoteAndScaleName(rootNoteAndScaleName); - displayCurrentRootNoteAndScaleName(canvas, rootNoteAndScaleName, false); + if (naviview.useNavigationView()) { + naviview.drawBaseboard(); + } + else { + DEF_STACK_STRING_BUF(rootNoteAndScaleName, 40); + currentSong->getCurrentRootNoteAndScaleName(rootNoteAndScaleName); + displayCurrentRootNoteAndScaleName(canvas, rootNoteAndScaleName, false); + } deluge::hid::display::OLED::markChanged(); } @@ -2075,7 +2093,8 @@ void SessionView::displayTempoBPM(deluge::hid::display::oled_canvas::Canvas& can int32_t metronomeIconSpacingX = 7 + 3; if (clearArea) { - canvas.clearAreaExact(OLED_MAIN_WIDTH_PIXELS - (kTextSpacingX * 6) - metronomeIconSpacingX, + canvas.clearAreaExact(OLED_MAIN_WIDTH_PIXELS - (kTextSpacingX * (tempoBPM.length() + 1)) + - metronomeIconSpacingX, OLED_MAIN_TOPMOST_PIXEL, OLED_MAIN_WIDTH_PIXELS - 1, yPos + kTextSpacingY); } @@ -2382,7 +2401,11 @@ int32_t SessionView::displayLoopsRemainingPopup(bool ephemeral) { } popupMsg.appendInt(quarterNotesRemaining); } - if (display->haveOLED() && !ephemeral) { + + if (naviview.useNavigationView()) { + naviview.drawRemainingCountdown(popupMsg.c_str()); + } + else if (display->haveOLED() && !ephemeral) { deluge::hid::display::OLED::clearMainImage(); deluge::hid::display::OLED::drawPermanentPopupLookingText(popupMsg.c_str()); deluge::hid::display::OLED::sendMainImage(); diff --git a/src/deluge/gui/views/timeline_view.cpp b/src/deluge/gui/views/timeline_view.cpp index 7cab2decbc..0f4580f553 100644 --- a/src/deluge/gui/views/timeline_view.cpp +++ b/src/deluge/gui/views/timeline_view.cpp @@ -18,6 +18,7 @@ #include "gui/views/timeline_view.h" #include "definitions_cxx.hpp" #include "extern.h" +#include "gui/views/navigation_view.h" #include "gui/views/view.h" #include "hid/button.h" #include "hid/buttons.h" @@ -36,6 +37,9 @@ void TimelineView::scrollFinished() { // Needed because sometimes we initiate a scroll before reverting an Action, so we need to // properly render again afterwards uiNeedsRendering(getRootUI(), 0xFFFFFFFF, 0); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } } // Virtual function @@ -118,10 +122,16 @@ ActionResult TimelineView::buttonAction(deluge::hid::Button b, bool on, bool inC } void TimelineView::displayZoomLevel(bool justPopup) { - DEF_STACK_STRING_BUF(text, 30); - currentSong->getNoteLengthName(text, currentSong->xZoom[getNavSysId()], "-notes", true); - display->displayPopup(text.data(), justPopup ? 3 : 0, true); + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } + else { + DEF_STACK_STRING_BUF(text, 30); + currentSong->getNoteLengthName(text, currentSong->xZoom[getNavSysId()], "-notes", true); + + display->displayPopup(text.data(), justPopup ? 3 : 0, true); + } } bool horizontalEncoderActionLock = false; @@ -253,7 +263,10 @@ void TimelineView::displayNumberOfBarsAndBeats(uint32_t number, uint32_t quantiz whichSubBeat++; } - if (display->haveOLED()) { + if (naviview.useNavigationView()) { + naviview.drawDashboard(); + } + else if (display->haveOLED()) { char buffer[15]; sprintf(buffer, "%d : %d : %d", whichBar, whichBeat, whichSubBeat); display->popupTextTemporary(buffer); diff --git a/src/deluge/gui/views/view.cpp b/src/deluge/gui/views/view.cpp index dd2dcfacfe..3b24f4c358 100644 --- a/src/deluge/gui/views/view.cpp +++ b/src/deluge/gui/views/view.cpp @@ -36,6 +36,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/performance_view.h" #include "gui/views/session_view.h" #include "hid/buttons.h" @@ -2007,7 +2008,11 @@ void View::drawOutputNameFromDetails(OutputType outputType, int32_t channel, int return; } - if (display->haveOLED()) { + if (naviview.useNavigationView()) { + hid::display::OLED::clearMainImage(); + naviview.drawDashboard(); + } + else if (display->haveOLED()) { deluge::hid::display::oled_canvas::Canvas& canvas = hid::display::OLED::main; hid::display::OLED::clearMainImage(); @@ -2037,7 +2042,10 @@ void View::drawOutputNameFromDetails(OutputType outputType, int32_t channel, int int32_t stringLengthPixels = canvas.getStringWidthInPixels(nameToDraw, kTextTitleSizeY); - if (stringLengthPixels <= OLED_MAIN_WIDTH_PIXELS) { + if (naviview.useNavigationView()) { + naviview.drawMainboard(nameToDraw); + } + else if (stringLengthPixels <= OLED_MAIN_WIDTH_PIXELS) { canvas.drawStringCentred(nameToDraw, yPos, kTextTitleSpacingX, kTextTitleSizeY); } else { @@ -2047,7 +2055,10 @@ void View::drawOutputNameFromDetails(OutputType outputType, int32_t channel, int kTextTitleSizeY, false); } - if (clip) { + if (naviview.useNavigationView()) { + naviview.drawBaseboard(); + } + else if (clip) { // "SECTION NN" is 10, "NN: " is 3 => 10 over current name is always enough. DEF_STACK_STRING_BUF(info, clip->name.getLength() + 10); if (clip->name.isEmpty()) { diff --git a/src/deluge/model/clip/instrument_clip_minder.cpp b/src/deluge/model/clip/instrument_clip_minder.cpp index 4d6728f73e..243d17e0c9 100644 --- a/src/deluge/model/clip/instrument_clip_minder.cpp +++ b/src/deluge/model/clip/instrument_clip_minder.cpp @@ -29,6 +29,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/view.h" #include "hid/button.h" #include "hid/buttons.h" @@ -589,6 +590,10 @@ bool InstrumentClipMinder::setScale(Scale newScale) { } void InstrumentClipMinder::displayScaleName(Scale scale) { + if (naviview.useNavigationView() && naviview.hasScale) { + naviview.drawDashboard(); + return; + } display->displayPopup(getScaleName(scale)); } diff --git a/src/deluge/playback/playback_handler.cpp b/src/deluge/playback/playback_handler.cpp index 7ebeecc5d4..b91d744acd 100644 --- a/src/deluge/playback/playback_handler.cpp +++ b/src/deluge/playback/playback_handler.cpp @@ -27,6 +27,7 @@ #include "gui/views/arranger_view.h" #include "gui/views/automation_view.h" #include "gui/views/instrument_clip_view.h" +#include "gui/views/navigation_view.h" #include "gui/views/performance_view.h" #include "gui/views/session_view.h" #include "gui/views/view.h" @@ -2150,7 +2151,9 @@ void PlaybackHandler::tempoEncoderAction(int8_t offset, bool encoderButtonPresse else { if (!isExternalClockActive()) { UI* currentUI = getCurrentUI(); - bool isOLEDSessionView = display->haveOLED() && (currentUI == &sessionView || currentUI == &arrangerView); + bool isOLEDSessionView = display->haveOLED() + && (currentUI == &sessionView || currentUI == &arrangerView + || (naviview.useNavigationView() && naviview.hasTempoBPM)); if (display->hasPopupOfType(PopupType::TEMPO) || isOLEDSessionView) { // Truth table for how we decide between adjusting coarse and fine tempo: // @@ -2327,8 +2330,11 @@ void PlaybackHandler::displayTempoBPM(float tempoBPM) { if (display->haveOLED()) { UI* currentUI = getCurrentUI(); // if we're currently in song or arranger view, we'll render tempo on the display instead of a popup - if ((currentUI == &sessionView || currentUI == &arrangerView) - && !deluge::hid::display::OLED::isPermanentPopupPresent()) { + if (naviview.useNavigationView() && naviview.hasTempoBPM) { + naviview.drawTempoBPM(); + } + else if ((currentUI == &sessionView || currentUI == &arrangerView) + && !deluge::hid::display::OLED::isPermanentPopupPresent()) { sessionView.lastDisplayedTempo = tempoBPM; getTempoStringForOLED(tempoBPM, text); sessionView.displayTempoBPM(deluge::hid::display::OLED::main, text, true);