Deluge Firmware 1.3.0
Build date: 2025.11.26
Loading...
Searching...
No Matches
eq_menu.h
1/*
2 * Copyright (c) 2025 Leonid Burygin
3 *
4 * This file is part of The Synthstrom Audible Deluge Firmware.
5 *
6 * The Synthstrom Audible Deluge Firmware is free software: you can redistribute it and/or modify it under the
7 * terms of the GNU General Public License as published by the Free Software Foundation,
8 * either version 3 of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12 * See the GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along with this program.
15 * If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#pragma once
19#include "gui/menu_item/horizontal_menu.h"
20#include "hid/display/oled.h"
21#include "hid/display/oled_canvas/canvas.h"
22
23#include <util/comparison.h>
24
25using namespace deluge::hid::display;
26
27namespace deluge::gui::menu_item::eq {
28class EqMenu final : public HorizontalMenu {
29public:
30 EqMenu(l10n::String newName, std::initializer_list<MenuItem*> newItems) : HorizontalMenu(newName, newItems) {}
31
32 void renderMenuItems(std::span<MenuItem*> items, const MenuItem* currentItem) override {
33 const auto [bass, treble, bass_freq, treble_freq, order_changed] = ensureCorrectItemsOrderAndGetValues();
34 if (order_changed) {
35 renderOLED();
36 return;
37 }
38
39 constexpr uint8_t start_y = OLED_MAIN_TOPMOST_PIXEL + kTextTitleSizeY + 5;
40 constexpr uint8_t end_y = OLED_MAIN_HEIGHT_PIXELS - 6;
41 constexpr uint8_t center_y = start_y + (end_y - start_y) / 2;
42 constexpr uint8_t height = end_y - start_y;
43
44 constexpr uint8_t padding_x = 4;
45 constexpr uint8_t start_x = padding_x - 1;
46 constexpr uint8_t end_x = OLED_MAIN_WIDTH_PIXELS - padding_x;
47 constexpr uint8_t slope_width = 12;
48 constexpr uint8_t bass_band_travel_width = (end_x - start_x) / 2 - slope_width;
49 constexpr uint8_t treble_band_travel_width = (end_x - start_x) * 0.75f;
50
51 constexpr uint8_t bass_x0 = start_x;
52 uint8_t bass_x1 = std::lerp(bass_x0, bass_x0 + bass_band_travel_width, bass_freq);
53 uint8_t bass_x2 = bass_x1 + slope_width;
54 uint8_t bass_y1 = std::lerp(end_y, end_y - height, bass);
55 uint8_t bass_y2 = center_y;
56
57 constexpr uint8_t treble_x0 = end_x;
58 uint8_t treble_x1 = std::lerp(end_x - treble_band_travel_width, end_x, treble_freq);
59 uint8_t treble_x2 = treble_x1 - slope_width;
60 uint8_t treble_y1 = std::lerp(end_y, end_y - height, treble);
61 uint8_t treble_y2 = center_y;
62
63 // Treble EQ also can affect mid & bass frequencies, and it has higher priority,
64 // so we allow to move the treble band to the bass' territory
65 if (bass_x2 > treble_x2) {
66 const uint8_t diff = bass_x2 - treble_x2;
67 bass_x2 -= diff;
68 bass_x1 -= diff;
69 }
70
71 // If bass freq and treble freq adjustment points are close to each other, smoothly adjust their y positions
72 // to morph between a slope line and a straight line
73 auto center_between = [](uint8_t a, uint8_t b) { return std::min(a, b) + std::abs(b - a) / 2; };
74 const float morph = 1.0f - (treble_x2 - bass_x2) / 14.f;
75 if (morph > 0.f) {
76 const uint8_t target_y = center_between(bass_y1, treble_y1);
77 bass_y2 = std::lerp(bass_y2, target_y, morph);
78 treble_y2 = std::lerp(treble_y2, target_y, morph);
79 }
80
81 oled_canvas::Canvas& image = OLED::main;
82 image.drawLine(bass_x0, bass_y1, bass_x1, bass_y1);
83 image.drawLine(bass_x1, bass_y1, bass_x2, bass_y2);
84 image.drawLine(bass_x2, bass_y2, treble_x2, treble_y2);
85 image.drawLine(treble_x2, treble_y2, treble_x1, treble_y1);
86 image.drawLine(treble_x1, treble_y1, treble_x0, treble_y1);
87
88 // Draw reference lines
89 if (std::abs(center_y - bass_y1) > 1) {
90 for (uint8_t x = 0; x <= bass_x2; x++) {
91 if (x % 6 == 3 && std::abs(x - bass_x2) > 1 && std::abs(x - treble_x2) > 1) {
92 image.drawPixel(x, center_y);
93 }
94 }
95 }
96 if (std::abs(center_y - treble_y1) > 1) {
97 for (uint8_t x = 0; x <= end_x; x++) {
98 if (x % 6 == 3 && std::abs(x - bass_x2) > 1 && std::abs(x - treble_x2) > 1) {
99 image.drawPixel(x, center_y);
100 }
101 }
102 }
103 for (uint8_t y = start_y - 1; y <= end_y + 1; y += 4) {
104 image.drawPixel(bass_x2, y);
105 image.drawPixel(treble_x2, y);
106 }
107
108 // Draw control indicators
109 selected_x_ = -1, selected_y_ = -1;
110 drawControlIndicator(center_between(bass_x0, bass_x1), bass_y1, currentItem == items[0]);
111 drawControlIndicator(bass_x2, bass_y2, currentItem == items[1]);
112 drawControlIndicator(treble_x2, treble_y2, currentItem == items[2]);
113 drawControlIndicator(center_between(treble_x1, treble_x0), treble_y1, currentItem == items[3]);
114 }
115
116private:
117 int32_t selected_x_, selected_y_;
118
120 float bass{0.f};
121 float treble{0.f};
122 float bass_freq{0.f};
123 float treble_freq{0.f};
124 bool order_changed{false};
125 };
126
127 EqualizerValues ensureCorrectItemsOrderAndGetValues() {
128 using namespace deluge::modulation;
129
130 const uint8_t current_item_pos = std::distance(items.begin(), current_item_);
131 UnpatchedParam* desired_order_items[4] = {nullptr, nullptr, nullptr, nullptr};
132 EqualizerValues result{};
133
134 for (auto* i : items) {
135 switch (const auto as_unpatched = static_cast<UnpatchedParam*>(i); as_unpatched->getP()) {
136 case params::UNPATCHED_BASS:
137 desired_order_items[0] = as_unpatched;
138 result.bass = as_unpatched->getValue() / 50.f;
139 break;
140 case params::UNPATCHED_BASS_FREQ:
141 desired_order_items[1] = as_unpatched;
142 result.bass_freq = as_unpatched->getValue() / 50.f;
143 break;
144 case params::UNPATCHED_TREBLE_FREQ:
145 desired_order_items[2] = as_unpatched;
146 // Treble boost has no effect on treble freq's values above 32
147 result.treble_freq = std::clamp<int32_t>(as_unpatched->getValue(), 0, 32) / 32.f;
148 break;
149 case params::UNPATCHED_TREBLE:
150 desired_order_items[3] = as_unpatched;
151 result.treble = as_unpatched->getValue() / 50.f;
152 break;
153 default:
154 break;
155 }
156 }
157
158 for (int idx = 0; idx < items.size(); ++idx) {
159 if (items[idx] != desired_order_items[idx] && desired_order_items[idx]) {
160 items[idx] = desired_order_items[idx];
161 result.order_changed = true;
162 }
163 }
164
165 if (result.order_changed) {
166 current_item_ = items.begin() + current_item_pos;
167 lastSelectedItemPosition = kNoSelection;
168 }
169
170 return result;
171 }
172
173 void drawControlIndicator(const float center_x, const float center_y, const bool is_selected) {
174 oled_canvas::Canvas& image = OLED::main;
175
176 const int32_t ix = static_cast<int32_t>(center_x);
177 const int32_t iy = static_cast<int32_t>(center_y);
178
179 if (!is_selected && ix == selected_x_ && iy == selected_y_) {
180 // Overlap occurred, skip drawing
181 return;
182 }
183
184 // Clear region inside
185 constexpr int32_t square_size = 2;
186 constexpr int32_t innerSquareSize = square_size - 1;
187 for (int32_t x = ix - innerSquareSize; x <= ix + innerSquareSize; x++) {
188 for (int32_t y = iy - innerSquareSize; y <= iy + innerSquareSize; y++) {
189 image.clearPixel(x, y);
190 }
191 }
192
193 if (is_selected) {
194 // Invert region inside to highlight selection
195 selected_x_ = ix, selected_y_ = iy;
196 image.invertArea(ix - innerSquareSize, square_size * 2 - 1, iy - innerSquareSize, iy + innerSquareSize);
197 }
198
199 // Draw a transition square
200 image.drawRectangle(ix - square_size, iy - square_size, ix + square_size, iy + square_size);
201 }
202};
203
204} // namespace deluge::gui::menu_item::eq
void renderOLED() override
Root rendering routine for OLED.
Definition horizontal_menu.cpp:87
Definition unpatched_param.h:28