Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the kit velocity keyboard view's zoom levels behavior #3132

Merged
merged 32 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8f3b545
Update kit velocity keyboard zoom levels behavior
Metamere Dec 24, 2024
0e6ce02
Refinement
Metamere Dec 24, 2024
3b0461c
pad color calculation changes, refinement.
Metamere Dec 25, 2024
f48b2e4
New pad color selection method
Metamere Dec 25, 2024
f4fc678
Adjusted color scheme
Metamere Dec 26, 2024
a1c65dd
Added level 13 zoom, changed dimming behavior
Metamere Dec 26, 2024
6c19849
Efficiency improvements
Metamere Dec 26, 2024
2de4389
Color shift enabled, formatted
Metamere Dec 26, 2024
08fc0c7
comment cleanup
Metamere Dec 26, 2024
2120bbd
Finding ways to make it more efficient, trying to debug issue #3168 t…
Metamere Dec 28, 2024
4a993ec
Uneven pad sizes on levels where the pad width is odd
Metamere Dec 31, 2024
7864822
Updated pad color calculations and loop structure
Metamere Jan 1, 2025
2e0c1e0
Update kit velocity keyboard zoom levels behavior
Metamere Dec 24, 2024
675104d
Refinement
Metamere Dec 24, 2024
34ecce3
pad color calculation changes, refinement.
Metamere Dec 25, 2024
e98973a
New pad color selection method
Metamere Dec 25, 2024
d12a2bd
Adjusted color scheme
Metamere Dec 26, 2024
c32d9f3
Added level 13 zoom, changed dimming behavior
Metamere Dec 26, 2024
4c4ecd2
Efficiency improvements
Metamere Dec 26, 2024
55a4e73
Color shift enabled, formatted
Metamere Dec 26, 2024
4c66226
comment cleanup
Metamere Dec 26, 2024
99b11af
Finding ways to make it more efficient, trying to debug issue #3168 t…
Metamere Dec 28, 2024
052b55e
Uneven pad sizes on levels where the pad width is odd
Metamere Dec 31, 2024
ea5dba1
Updated pad color calculations and loop structure
Metamere Jan 1, 2025
763475a
Updated code based on review comments
Metamere Jan 2, 2025
a0a98d5
Merge branch 'Metamere-Zoom' of https://github.com/Metamere/DelugeFir…
Metamere Jan 2, 2025
0ce8396
remove precalculations
Metamere Jan 2, 2025
33f82cf
Merge branch 'community' into Metamere-Zoom
Metamere Jan 3, 2025
9e0e397
Updated changelog.md for this PR
Metamere Jan 3, 2025
06a078f
properly removed precalculate() and stubbed in the header file.
Metamere Jan 3, 2025
60fb6b1
Merge branch 'community' into Metamere-Zoom
Metamere Jan 3, 2025
aca4d24
Final nits, get the CI running
stellar-aria Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,13 @@ at velocity 0 it would look the same as its tail (but you can't have 0 velocity)
#### <ins>Keyboard View</ins>

##### Kits
- Added ability to change the pad size in the `KIT VELOCITY KEYBOARD VIEW` using the Zoom In/Out shortcut by `Pressing + Turning <>`
- `KIT VELOCITY KEYBOARD VIEW` Changes:
- Additional shortcut of `Pressing + Turning <>` to change the pad size using the Zoom In/Out.
- Went from 8 zoom levels to 13, with mostly smaller jumps in size and number of drum pads between levels.
- Rectangular pads of various sizes are used where needed to fully cover the screen without any partial pads. This means you can now have six or four pads, or even the entire screen as a single pad.
- Zoom level 1 pads play the system-level default velocity (64 by default out of a maximum 127, but it is user defineable), and levels 2 and 3 have a slightly lowered max velocity of 100 to make them more useable.
- Pad colors are calculated to avoid having adjacent pads of the same color as much as possible.
- Default pad brightness is set to match the default brightness value, so they will be more visible if needed. They will dim when pressed instead of getting brighter. The brightness gradient over the drum pads, going from dim on the low velocity pads to bright on the highest, is now much more apparent, due to the max brightness being higher, the lowest brightness being lower, and using a quadratic curve for the brightness ratio rather than linear. The amount that the drum pads dim when pressed depends on the pad area, so that smaller pad presses will be more visible, and larger pad presses won't be TOO visible, i.e. they won't have a potentially unpleasant strobe light flashing behavior.

##### Layout
- Added the classic piano keyboard layout.
Expand Down
5 changes: 1 addition & 4 deletions src/deluge/gui/ui/keyboard/keyboard_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,8 @@ ActionResult KeyboardScreen::padAction(int32_t x, int32_t y, int32_t velocity) {
// Pad was already active
if (pressedPads[idx].active && pressedPads[idx].x == x && pressedPads[idx].y == y) {
pressedPads[idx].active = false;
pressedPads[idx].padPressHeld = false;
markDead = idx;
if ((AudioEngine::audioSampleTimer - pressedPads[idx].timeLastPadPress) > FlashStorage::holdTime) {
pressedPads[idx].padPressHeld = true;
}
break;
}
}
Expand Down Expand Up @@ -638,7 +636,6 @@ ActionResult KeyboardScreen::buttonAction(deluge::hid::Button b, bool on, bool i
}

else {
requestRendering();
ActionResult result = InstrumentClipMinder::buttonAction(b, on, inCardRoutine);
if (result != ActionResult::NOT_DEALT_WITH) {
return result;
Expand Down
244 changes: 152 additions & 92 deletions src/deluge/gui/ui/keyboard/layout/velocity_drums.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,121 +24,181 @@
namespace deluge::gui::ui::keyboard::layout {

void KeyboardLayoutVelocityDrums::evaluatePads(PressedPad presses[kMaxNumKeyboardPadPresses]) {
uint8_t noteIdx = 0;

currentNotesState = NotesState{}; // Erase active notes

static_assert(kMaxNumActiveNotes < 32, "We use a 32-bit integer to represent note active state");
uint32_t activeNotes = 0;
std::array<int32_t, kMaxNumActiveNotes> noteOnTimes{};

for (int32_t idxPress = 0; idxPress < kMaxNumKeyboardPadPresses; ++idxPress) {
if (presses[idxPress].active && presses[idxPress].x < kDisplayWidth) {
uint8_t note = noteFromCoords(presses[idxPress].x, presses[idxPress].y);
uint8_t velocity = (intensityFromCoords(presses[idxPress].x, presses[idxPress].y) >> 1);
auto noteOnIdx = currentNotesState.enableNote(note, velocity);

// exceeded maximum number of active notes, ignore this note-on
if (noteOnIdx == kMaxNumActiveNotes) {
continue;
}

if ((activeNotes & (1 << noteOnIdx)) == 0) {
activeNotes |= 1 << noteOnIdx;
noteOnTimes[noteOnIdx] = presses[idxPress].timeLastPadPress;
}
else {
// this is a retrigger on the same note, see if we should update the velocity
auto lastOnTime = noteOnTimes[noteOnIdx];
auto thisOnTime = presses[idxPress].timeLastPadPress;
if (util::infinite_a_lt_b(lastOnTime, thisOnTime)) {
// this press is more recent, use its velocity.
currentNotesState.notes[noteOnIdx].velocity = velocity;
noteOnTimes[noteOnIdx] = thisOnTime;
}
uint32_t active_notes = 0;
std::array<int32_t, kMaxNumActiveNotes> note_on_times{};
uint32_t offset = getState().drums.scroll_offset;
uint32_t zoom_level = getState().drums.zoom_level;
uint32_t edge_size_x = zoom_arr[zoom_level][0];
uint32_t edge_size_y = zoom_arr[zoom_level][1];
uint32_t pads_per_row = kDisplayWidth / edge_size_x;
uint32_t highest_clip_note = getHighestClipNote();
bool odd_pad = (edge_size_x % 2 == 1 && edge_size_x > 1);
for (int32_t idx_press = 0; idx_press < kMaxNumKeyboardPadPresses; ++idx_press) {
if (!presses[idx_press].active) {
continue;
}
uint32_t x = presses[idx_press].x;
if (x >= kDisplayWidth) {
continue; // prevents the sidebar from being able to activate notes
}
bool x_adjust_note = (odd_pad && x == kDisplayWidth - 1);
uint32_t y = presses[idx_press].y;
uint32_t note = (x / edge_size_x) - x_adjust_note + ((y / edge_size_y) * pads_per_row) + offset;
if (note > highest_clip_note) {
continue; // save calculation time if press was on an unlit pad. Is there a way to prevent
// re-rendering?
}
uint32_t velocity =
(velocityFromCoords(x, y, edge_size_x, edge_size_y) >> 1); // uses bitshift to divide by two, to get 0-127
// D_PRINTLN("note, velocity: %d, %d", note, velocity);
auto note_on_idx = currentNotesState.enableNote(note, velocity);

if ((active_notes & (1 << note_on_idx)) == 0) {
active_notes |= 1 << note_on_idx;
note_on_times[note_on_idx] = presses[idx_press].timeLastPadPress;
}
else {
// this is a retrigger on the same note, see if we should update the velocity
auto last_on_time = note_on_times[note_on_idx];
auto this_on_time = presses[idx_press].timeLastPadPress;
if (util::infinite_a_lt_b(last_on_time, this_on_time)) {
// this press is more recent, use its velocity.
currentNotesState.notes[note_on_idx].velocity = velocity;
note_on_times[note_on_idx] = this_on_time;
}
}

// if this note was recently pressed, set it as the selected drum
if (isShortPress(noteOnTimes[noteOnIdx])) {
InstrumentClip* clip = getCurrentInstrumentClip();
Kit* thisKit = (Kit*)clip->output;
Drum* thisDrum = thisKit->getDrumFromNoteCode(clip, note);
bool shouldSendMidiFeedback = false;
instrumentClipView.setSelectedDrum(thisDrum, true, nullptr, shouldSendMidiFeedback);
}
// if this note was recently pressed, set it as the selected drum
if (isShortPress(note_on_times[note_on_idx])) {
InstrumentClip* clip = getCurrentInstrumentClip();
Kit* thisKit = (Kit*)clip->output;
Drum* thisDrum = thisKit->getDrumFromNoteCode(clip, note);
bool shouldSendMidiFeedback = false;
instrumentClipView.setSelectedDrum(thisDrum, true, nullptr, shouldSendMidiFeedback);
}
}
}

void KeyboardLayoutVelocityDrums::handleVerticalEncoder(int32_t offset) {
PressedPad pressedPad{};
handleHorizontalEncoder(offset * (kDisplayWidth / getState().drums.edgeSize), false, &pressedPad, false);
uint32_t edge_size_x = zoom_arr[getState().drums.zoom_level][0];
handleHorizontalEncoder(offset * (kDisplayWidth / edge_size_x), false, &pressedPad, false);
}

void KeyboardLayoutVelocityDrums::handleHorizontalEncoder(int32_t offset, bool shiftEnabled,
PressedPad presses[kMaxNumKeyboardPadPresses],
bool encoderPressed) {
KeyboardStateDrums& state = getState().drums;
bool zoom_level_changed = false;
if (shiftEnabled || Buttons::isButtonPressed(hid::button::X_ENC)) { // zoom control
if (state.zoom_level + offset >= k_min_zoom_level && state.zoom_level + offset <= k_max_zoom_level) {
state.zoom_level += offset;
zoom_level_changed = true;
}
else
return;

if (shiftEnabled || Buttons::isButtonPressed(hid::button::X_ENC)) {
state.edgeSize += offset;
state.edgeSize = std::clamp(state.edgeSize, kMinDrumPadEdgeSize, kMaxDrumPadEdgeSize);

char buffer[13] = "Pad size: ";
auto displayOffset = (display->haveOLED() ? 10 : 0);
intToString(state.edgeSize, buffer + displayOffset, 1);
display->displayPopup(buffer);

offset = 0; // Reset offset variable for processing scroll calculation without actually shifting
}

// Calculate highest possible displayable note with current edgeSize
int32_t displayedfullPadsCount = ((kDisplayHeight / state.edgeSize) * (kDisplayWidth / state.edgeSize));
int32_t highestScrolledNote = std::max<int32_t>(0, (getHighestClipNote() + 1 - displayedfullPadsCount));

// Make sure current value is in bounds
state.scrollOffset = std::clamp(state.scrollOffset, getLowestClipNote(), highestScrolledNote);

// Offset if still in bounds (check for verticalEncoder)
int32_t newOffset = state.scrollOffset + offset;
if (newOffset >= getLowestClipNote() && newOffset <= highestScrolledNote) {
state.scrollOffset = newOffset;
}

precalculate();
}

void KeyboardLayoutVelocityDrums::precalculate() {
KeyboardStateDrums& state = getState().drums;

// Pre-Buffer colours for next renderings
int32_t displayedfullPadsCount = ((kDisplayHeight / state.edgeSize) * (kDisplayWidth / state.edgeSize));
for (int32_t i = 0; i < displayedfullPadsCount; ++i) {
noteColours[i] = getNoteColour(state.scrollOffset + i);
DEF_STACK_STRING_BUF(buffer, 16);
if (display->haveOLED()) {
buffer.append("Zoom Level: ");
}
buffer.appendInt(state.zoom_level + 1);
display->displayPopup(buffer.c_str());

offset = 0; // Since the offset variable can be used for scrolling or zooming,
// we reset it to 0 before calculating scroll offset
} // end zoom control
// scroll offset control - need to run to adjust to new max position if zoom level has changed

// Calculate highest possible displayable note with current edge size
uint32_t edge_size_x = zoom_arr[state.zoom_level][0];
uint32_t edge_size_y = zoom_arr[state.zoom_level][1];
int32_t displayed_full_pads_count = ((kDisplayHeight / edge_size_y) * (kDisplayWidth / edge_size_x));
int32_t highest_scrolled_note = std::max<int32_t>(0, (getHighestClipNote() + 1 - displayed_full_pads_count));

// Make sure scroll offset value is in bounds.
// For example, if zoom level has gone down and scroll offset was at max, it will be reduced some.
int32_t new_offset = std::clamp(state.scroll_offset + offset, getLowestClipNote(), highest_scrolled_note);

// may need to update the scroll offset if zoom level has changed
if (new_offset != state.scroll_offset || zoom_level_changed) {
state.scroll_offset = new_offset;
}
}

void KeyboardLayoutVelocityDrums::renderPads(RGB image[][kDisplayWidth + kSideBarWidth]) {
uint8_t highestClipNote = getHighestClipNote();

for (int32_t y = 0; y < kDisplayHeight; ++y) {
for (int32_t x = 0; x < kDisplayWidth; x++) {
uint8_t note = noteFromCoords(x, y);
if (note > highestClipNote) {
image[y][x] = colours::black;
continue;
// D_PRINTLN("render pads");
uint32_t highest_clip_note = getHighestClipNote();
uint32_t offset = getState().drums.scroll_offset;
uint32_t offset2 = getCurrentInstrumentClip()->colourOffset + 60;
uint32_t zoom_level = getState().drums.zoom_level;
uint32_t edge_size_x = zoom_arr[zoom_level][0];
uint32_t edge_size_y = zoom_arr[zoom_level][1];
uint32_t pad_area_1 = edge_size_x * edge_size_y;
uint32_t pad_area_2 = pad_area_1 + edge_size_y;
bool odd_pad = (edge_size_x % 2 == 1 && edge_size_x > 1); // check if the view has odd width pads
// Dims more for smaller pads, less for bigger ones.
// Changing brightness too much on large areas is unpleasant to the eyes.
float dim_brightness = std::min(0.25 + 0.65 * pad_area_1 / 128, 0.75);
// fine tune the curve for different pad sizes.
float initial_intensity = pad_area_1 == 2 ? 0.25 : pad_area_1 < 6 ? 0.045 : 0.015;
float intensity_increment_1 = edge_size_x > 1 ? std::exp(-std::log(initial_intensity) / (pad_area_1 - 1)) : 1;
float intensity_increment_2 = odd_pad ? std::exp(-std::log(initial_intensity) / (pad_area_2 - 1)) : 1;
uint32_t pads_per_row = kDisplayWidth / edge_size_x;
uint32_t pads_per_col = kDisplayHeight / edge_size_y;
uint32_t note = offset;
for (uint32_t padY = 0; padY < pads_per_col; ++padY) {
uint32_t y = padY * edge_size_y;
for (uint32_t padX = 0; padX < pads_per_row; ++padX) {
uint32_t x = padX * edge_size_x;
RGB note_colour = RGB::fromHue((note * 14 + (note % 2 == 1) * 107 + offset2) % 192);
bool note_enabled = currentNotesState.noteEnabled(note);
// Dim the active notes
// Brighter by default makes the pads more visible in daylight and allows for the full gradient.
float brightness_factor = note_enabled ? dim_brightness : 1;

if (edge_size_x > 1) {
bool disabled_pad = (note > highest_clip_note);
bool x_adjust = (odd_pad && padX == pads_per_row - 1);
uint32_t edge_size_x2 = edge_size_x + x_adjust;
uint32_t pad_area = x_adjust ? pad_area_2 : pad_area_1;
float intensity_increment = x_adjust ? intensity_increment_2 : intensity_increment_1;
float colour_intensity = initial_intensity;
uint32_t x2 = 0;
uint32_t y2 = 0;
for (int32_t i = 0; i < pad_area; ++i) {
if (disabled_pad)
image[y + y2][x + x2] = colours::black;
else {
image[y + y2][x + x2] =
note_colour.transform([brightness_factor, colour_intensity](uint8_t chan) {
return (chan * brightness_factor * colour_intensity);
});
}
x2++;
if (x2 == edge_size_x2) {
x2 = 0;
y2++;
}
if (disabled_pad || i == pad_area - 1)
colour_intensity = 1;
else
colour_intensity *= intensity_increment;
}
}

RGB noteColour = noteColours[note - getState().drums.scrollOffset];

uint8_t colourIntensity = intensityFromCoords(x, y);

// Highlight active notes
uint8_t brightnessDivider = currentNotesState.noteEnabled(note) ? 1 : 3;

image[y][x] = noteColour.transform([colourIntensity, brightnessDivider](uint8_t chan) {
return ((chan * colourIntensity / 255) / brightnessDivider);
});
else {
if (note > highest_clip_note)
image[y][x] = colours::black;
else if (note_enabled)
image[y][x] =
note_colour.transform([brightness_factor](uint8_t chan) { return (chan * brightness_factor); });
else
image[y][x] = note_colour;
}
note++;
}
}
}
Expand Down
57 changes: 35 additions & 22 deletions src/deluge/gui/ui/keyboard/layout/velocity_drums.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@

namespace deluge::gui::ui::keyboard::layout {

constexpr int32_t kMinDrumPadEdgeSize = 1;
constexpr int32_t kMaxDrumPadEdgeSize = 8;
constexpr int32_t k_min_zoom_level = 0;
constexpr int32_t k_max_zoom_level = 12;

// The zoom_arr is used to set the edge sizes of the pads {x size, y size} on each zoom level.
const int32_t zoom_arr[13][2] = {{1, 1}, {2, 1}, {3, 1}, {2, 2}, {3, 2}, {4, 2}, {5, 2},
{3, 4}, {4, 4}, {5, 4}, {8, 4}, {8, 8}, {16, 8}};

class KeyboardLayoutVelocityDrums : KeyboardLayout {
public:
KeyboardLayoutVelocityDrums() {}
~KeyboardLayoutVelocityDrums() override {}
KeyboardLayoutVelocityDrums() = default;
~KeyboardLayoutVelocityDrums() override = default;
void precalculate() override {}

void evaluatePads(PressedPad presses[kMaxNumKeyboardPadPresses]) override;
void handleVerticalEncoder(int32_t offset) override;
void handleHorizontalEncoder(int32_t offset, bool shiftEnabled, PressedPad presses[kMaxNumKeyboardPadPresses],
bool encoderPressed = false) override;
void precalculate() override;

void renderPads(RGB image[][kDisplayWidth + kSideBarWidth]) override;

Expand All @@ -42,25 +46,34 @@ class KeyboardLayoutVelocityDrums : KeyboardLayout {
bool supportsKit() override { return true; }

private:
inline uint8_t noteFromCoords(int32_t x, int32_t y) {
uint8_t edgeSize = (uint32_t)getState().drums.edgeSize;
uint8_t padsPerRow = kDisplayWidth / edgeSize;
return (x / edgeSize) + ((y / edgeSize) * padsPerRow) + getState().drums.scrollOffset;
}

inline uint8_t intensityFromCoords(int32_t x, int32_t y) {
uint8_t edgeSize = getState().drums.edgeSize;
uint8_t localX = (x % edgeSize);
uint8_t localY = (y % edgeSize);
uint8_t position = localX + (localY * edgeSize) + 1;
inline uint8_t velocityFromCoords(int32_t x, int32_t y, uint32_t edge_size_x, uint32_t edge_size_y) {
uint32_t velocity = 0;
if (edge_size_x == 1) {
// No need to do a lot of calculations or use max velocity for only one option.
velocity = FlashStorage::defaultVelocity * 2;
}
else {
bool odd_pad = (edge_size_x % 2 == 1); // check if the view has odd width pads
uint32_t x_limit = kDisplayWidth - 2 - edge_size_x; // end of second to last pad in a row (the regular pads)
bool x_adjust = (odd_pad && x > x_limit);
uint32_t localX = x_adjust ? x - x_limit : x % (edge_size_x);

// We use two bytes to increase accuracy and shift it down to one byte later
uint32_t stepSize = 0xFFFF / (edgeSize * edgeSize);

return (position * stepSize) >> 8;
if (edge_size_y == 1) {
velocity = (localX + 1) * 200 / (edge_size_x + x_adjust); // simpler, more useful, easier on the ears.
}
else {
if (edge_size_x % 2 == 1 && x > kDisplayWidth - 2 - edge_size_x)
edge_size_x += 1;
uint32_t position = localX + 1;
position += ((y % edge_size_y) * (edge_size_x + x_adjust));
// We use two bytes to keep the precision of the calculations high,
// then shift it down to one byte at the end.
uint32_t stepSize = 0xFFFF / ((edge_size_x + x_adjust) * edge_size_y);
velocity = (position * stepSize) >> 8;
}
}
return velocity; // returns an integer value 0-255, which will then be divided by 2 to get 0-127
}

RGB noteColours[kDisplayHeight * kDisplayWidth];
};

}; // namespace deluge::gui::ui::keyboard::layout
2 changes: 1 addition & 1 deletion src/deluge/gui/ui/keyboard/notes_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct PressedPad : Cartesian {
uint32_t timeLastPadPress;
bool padPressHeld;
bool active;
// all evaluatePads wil be called at least once with pad.active == false on release. Following
// all evaluatePads will be called at least once with pad.active == false on release. Following
// that, dead will be set to true to avoid repeatedly processing releases.
// exception - if the pad is "used up" by switching keyboard columns it will be set dead immediately to prevent
// processing its release, while still being set as active
Expand Down
Loading
Loading