Deluge Firmware 1.3.0
Build date: 2025.09.27
Loading...
Searching...
No Matches
filter_container.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
20#include "gui/menu_item/horizontal_menu_container.h"
21#include "hid/led/indicator_leds.h"
22#include "param.h"
23#include "util/comparison.h"
24
25namespace deluge::gui::menu_item::filter {
26
27using namespace deluge::hid::display;
28
29class FilterContainer final : public HorizontalMenuContainer {
30public:
31 FilterContainer(std::initializer_list<MenuItem*> items, FilterParam* morph_item)
32 : HorizontalMenuContainer(items), morph_item_{morph_item} {}
33 FilterContainer(std::initializer_list<MenuItem*> items, UnpatchedFilterParam* morph_item)
34 : HorizontalMenuContainer(items), morph_item_unpatched_{morph_item} {}
35
36 void render(int32_t start_x, int32_t width, int32_t start_y, int32_t height, const MenuItem* selected_item,
37 HorizontalMenu* parent, bool* halt_remaining_rendering) override {
38 oled_canvas::Canvas& image = OLED::main;
39
40 const auto [freq_raw, reso_raw, morph_raw, filter_mode, is_hpf] = getFilterValues();
41 const float freq_value = freq_raw / 50.f;
42 const float reso_value = sigmoidLikeCurve(reso_raw, 50.f, 15.f);
43 const float morph_value = [&] {
44 float result = 0.f;
45 if (util::one_of(filter_mode, {FilterMode::SVF_BAND, FilterMode::SVF_NOTCH})) {
46 result = morph_raw / 50.f;
47 }
48 if (is_hpf) {
49 result = 1.0f - result; // treat HPF as fully morphed LPF visually
50 }
51 return result;
52 }();
53
54 constexpr uint8_t reso_segment_width = 5;
55 constexpr uint8_t freq_slope_width = 5;
56 constexpr uint8_t padding_x = 3;
57 const uint8_t total_width = width - 4 - padding_x * 2;
58 const uint8_t base_width = total_width - freq_slope_width - reso_segment_width;
59
60 uint8_t min_x = start_x + padding_x;
61 uint8_t max_x = min_x + total_width;
62 uint8_t reso_x0 = min_x - reso_segment_width + base_width * freq_value;
63 uint8_t reso_x1 = reso_x0 + reso_segment_width;
64 uint8_t reso_x2 = reso_x1 + reso_segment_width;
65 int8_t slope0_x0 = reso_x0 - base_width - freq_slope_width;
66 int8_t slope0_x1 = slope0_x0 + freq_slope_width;
67 uint8_t slope1_x0 = reso_x2;
68 uint8_t slope1_x1 = slope1_x0 + freq_slope_width;
69
70 if (morph_value > 0) {
71 constexpr uint8_t padding = padding_x - 1; // reduce movement range a bit for better looking
72 const uint8_t slope_shift = std::lerp(0, total_width + padding, morph_value);
73 const uint8_t reso_shift = std::lerp(0, freq_slope_width + reso_segment_width + padding, morph_value);
74 const uint8_t base_shift = std::lerp(0, padding, morph_value);
75 min_x += base_shift;
76 max_x += base_shift;
77 slope0_x0 += slope_shift;
78 slope0_x1 += slope_shift;
79 slope1_x0 += slope_shift;
80 slope1_x1 += slope_shift;
81 reso_x0 += reso_shift;
82 reso_x1 += reso_shift;
83 reso_x2 += reso_shift;
84 }
85
86 constexpr uint8_t padding_y = 2;
87 const uint8_t peak_y = start_y + padding_y;
88 const uint8_t floor_y = start_y + height - 3;
89 const uint8_t full_reso_y = start_y + (height >> 1) + 1;
90 const uint8_t body_y = std::lerp(start_y + padding_y, full_reso_y, reso_value);
91
92 auto draw_segment = [&](int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
93 oled_canvas::Point last_point{-1, -1};
94 auto draw_fill_pattern = [&](oled_canvas::Point point) {
95 if (last_point.x != point.x && point.x % 3 == 0) {
96 for (int32_t y = point.y; y <= floor_y + 2; y++) {
97 if (y % 3 == 1) {
98 image.drawPixel(point.x, y);
99 }
100 }
101 }
102 last_point = point;
103 };
104 image.drawLine(x0, y0, x1, y1, {.min_x = min_x, .max_x = max_x, .point_callback = draw_fill_pattern});
105 return last_point;
106 };
107
108 // Slope 0
109 auto slope0_last_point = slope0_x1 <= min_x ? oled_canvas::Point{min_x, body_y}
110 : draw_segment(slope0_x0, floor_y, slope0_x1, body_y);
111
112 // Body
113 draw_segment(slope0_x1, body_y, reso_x0, body_y);
114
115 // Resonance
116 draw_segment(reso_x0, body_y, reso_x1, peak_y);
117 draw_segment(reso_x1, peak_y, reso_x2, body_y);
118 draw_segment(reso_x2, body_y, slope1_x0, body_y);
119
120 // Slope 1
121 auto slope1_last_point = slope1_x0 >= max_x ? oled_canvas::Point{max_x, body_y}
122 : draw_segment(slope1_x0, body_y, slope1_x1, floor_y);
123
124 // Body level dashed line
125 constexpr uint8_t line_offset = 3;
126 constexpr uint8_t line_interval = 5;
127 for (uint8_t x = min_x + line_offset; x <= max_x - line_offset; x += line_interval) {
128 if (x < slope0_x1 - line_offset || x > slope1_x0 + line_offset) {
129 image.drawPixel(x, body_y);
130 }
131 }
132
133 // Freq / reso indicator squares
134 auto freq_point = pickFreqPoint(slope0_last_point, slope1_last_point, min_x, max_x);
135 drawIndicatorSquare(freq_point.x, freq_point.y, selected_item == items_[0]);
136 drawIndicatorSquare(reso_x1, peak_y, selected_item == items_[1]);
137
138 syncIndicatorsPositionWithLEDs(freq_point == slope1_last_point, selected_item, parent,
139 halt_remaining_rendering);
140 };
141
142private:
143 FilterParam* morph_item_{nullptr};
144 UnpatchedFilterParam* morph_item_unpatched_{nullptr};
145
147 int32_t freq_value{0};
148 int32_t reso_value{0};
149 int32_t morph_value{0};
150 FilterMode mode{FilterMode::OFF};
151 bool is_hpf{false};
152 };
153
154 [[nodiscard]] FilterValues getFilterValues() const {
155 if (morph_item_ != nullptr) {
156 // Get from patched params
157 auto freq_item = static_cast<FilterParam*>(items_[0]);
158 auto reso_item = static_cast<FilterParam*>(items_[1]);
159 FilterMode filter_mode = morph_item_->getFilterInfo().getMode();
160 bool is_hpf = freq_item->getP() == params::LOCAL_HPF_FREQ;
161 return {freq_item->getValue(), reso_item->getValue(), morph_item_->getValue(), filter_mode, is_hpf};
162 }
163
164 // Get from unpatched params
165 auto freq_item = static_cast<UnpatchedFilterParam*>(items_[0]);
166 auto reso_item = static_cast<UnpatchedFilterParam*>(items_[1]);
167 FilterMode filter_mode = morph_item_unpatched_->getFilterInfo().getMode();
168 bool is_hpf = freq_item->getP() == params::UNPATCHED_HPF_FREQ;
169 return {freq_item->getValue(), reso_item->getValue(), morph_item_unpatched_->getValue(), filter_mode, is_hpf};
170 }
171
172 static oled_canvas::Point pickFreqPoint(const oled_canvas::Point& slope0_last_point,
173 const oled_canvas::Point& slope1_last_point, uint8_t min_x, uint8_t max_x) {
174 if (slope0_last_point.x > min_x && slope0_last_point.x < max_x) {
175 return slope0_last_point;
176 }
177 if (slope1_last_point.x > min_x && slope1_last_point.x < max_x) {
178 return slope1_last_point;
179 }
180 return slope0_last_point.y > slope1_last_point.y ? slope0_last_point : slope1_last_point;
181 }
182
183 static void drawIndicatorSquare(int32_t center_x, int32_t center_y, bool is_selected) {
184 oled_canvas::Canvas& image = OLED::main;
185
186 for (int32_t x = center_x - 1; x <= center_x + 1; x++) {
187 for (int32_t y = center_y - 1; y <= center_y + 1; y++) {
188 image.clearPixel(x, y);
189 }
190 }
191 if (is_selected) {
192 image.invertArea(center_x - 1, 3, center_y - 1, center_y + 1);
193 }
194 image.drawRectangle(center_x - 2, center_y - 2, center_x + 2, center_y + 2);
195 };
196
197 void syncIndicatorsPositionWithLEDs(bool freq_is_on_right_side, const MenuItem* selected_item,
198 HorizontalMenu* parent, bool* halt_remaining_rendering) const {
199 MenuItem* freq = items_[0];
200 MenuItem* reso = items_[1];
201
202 uint8_t freq_index = 1, reso_index = 2;
203 if (freq_is_on_right_side) {
204 std::swap(freq_index, reso_index);
205 }
206
207 auto& parent_items = parent->getItems();
208 if (parent_items[freq_index] != freq) {
209 parent_items[freq_index] = freq;
210 parent_items[reso_index] = reso;
211
212 // We can be inside a horizontal menu group or a plain horizontal menu
213 auto host_menu = parent->parent != nullptr ? parent->parent : parent;
214
215 // Reset the current item iterator, it points to incorrect item now after items order was changed
216 host_menu->setCurrentItem(selected_item);
217
218 // Should re-render the whole menu to apply new items order
219 OLED::clearMainImage();
220 host_menu->renderOLED();
221 *halt_remaining_rendering = true;
222 }
223 }
224};
225} // namespace deluge::gui::menu_item::filter
Base class for all menu items.
Definition menu_item.h:43
Definition horizontal_menu.h:28