Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
controller_utility_kernels.hpp
Go to the documentation of this file.
1#pragma once
2
4
5namespace pulp::midi {
6
7using ControllerCurve = float (*)(float normalized, float amount) noexcept;
8
9inline float linear_controller_curve(float normalized, float) noexcept {
10 return std::clamp(normalized, 0.0f, 1.0f);
11}
12
14 std::uint8_t input_channel = 0;
15 std::uint8_t input_cc = 1;
16 std::uint8_t output_channel = 0;
17 std::uint8_t output_cc = 1;
18 float minimum = 0.0f;
19 float maximum = 1.0f;
20 float curve_amount = 0.0f;
21 float smoothing_seconds = 0.0f;
23 bool enabled = false;
24};
25
26template <std::size_t MaximumMappings = 32> class ControllerMapper {
27 public:
28 static_assert(MaximumMappings > 0);
34
35 bool set_rule(std::size_t index, ControllerMapRule rule) noexcept {
36 if (index >= MaximumMappings)
37 return false;
38 if (rule.input_channel > 15 || rule.output_channel > 15 || rule.input_cc > 127 ||
39 rule.output_cc > 127)
40 return false;
41 if (!std::isfinite(rule.minimum) || !std::isfinite(rule.maximum) ||
42 !std::isfinite(rule.curve_amount) || !std::isfinite(rule.smoothing_seconds))
43 return false;
44 if (rule.minimum > rule.maximum)
45 std::swap(rule.minimum, rule.maximum);
46 if (rule.smoothing_seconds < 0.0f)
47 rule.smoothing_seconds = 0.0f;
48 if (rule.curve == nullptr)
49 rule.curve = linear_controller_curve;
50 rules_[index] = rule;
51 states_[index] = {};
52 return true;
53 }
54
56 double sample_rate,
57 timebase::SamplePosition block_start = {}) noexcept {
58 if (utility_detail::blocks_alias(input, output))
59 return {0, input.size(), 0, false};
61 MidiUtilityProcessReport report;
62 if (!utility_detail::ready(output))
63 return {0, input.size(), 0, false};
64 if (!std::isfinite(sample_rate))
65 return {0, input.size(), 0, false};
66 for (const auto& event : input) {
67 if (!event.is_cc()) {
68 utility_detail::emit(output, event, report);
69 continue;
70 }
71 bool mapped = false;
72 for (std::size_t i = 0; i < MaximumMappings; ++i) {
73 const auto& rule = rules_[i];
74 if (!rule.enabled || rule.input_channel != event.channel() ||
75 rule.input_cc != event.cc_number())
76 continue;
77 mapped = true;
78 auto& state = states_[i];
79 const float normalized = static_cast<float>(event.cc_value()) / 127.0f;
80 const float raw_curve = rule.curve(normalized, rule.curve_amount);
81 if (!std::isfinite(raw_curve)) {
82 ++report.dropped;
83 report.complete = false;
84 continue;
85 }
86 const float curved = std::clamp(raw_curve, 0.0f, 1.0f);
87 const double target =
88 std::lerp(static_cast<double>(rule.minimum), static_cast<double>(rule.maximum),
89 static_cast<double>(curved));
90 if (!std::isfinite(target)) {
91 ++report.dropped;
92 report.complete = false;
93 continue;
94 }
95 double current = target;
96 if (!state.initialized || rule.smoothing_seconds <= 0.0f || sample_rate <= 0.0) {
97 current = target;
98 } else {
99 const auto event_sample = utility_detail::saturating_sample_add(
100 block_start.value, event.sample_offset);
101 const auto elapsed =
102 nonnegative_sample_distance(event_sample, state.last_sample);
103 const double coefficient =
104 std::exp(-static_cast<double>(elapsed) /
105 (sample_rate * static_cast<double>(rule.smoothing_seconds)));
106 current = std::lerp(target, state.current, coefficient);
107 }
108 if (!std::isfinite(current)) {
109 ++report.dropped;
110 report.complete = false;
111 continue;
112 }
113 state.target = target;
114 state.current = current;
115 state.initialized = true;
116 state.last_sample =
117 utility_detail::saturating_sample_add(block_start.value, event.sample_offset);
118 const int value =
119 static_cast<int>(std::lround(std::clamp(current, 0.0, 1.0) * 127.0));
120 auto mapped_event = MidiEvent::cc(rule.output_channel, rule.output_cc,
121 static_cast<std::uint8_t>(value));
122 mapped_event.sample_offset = event.sample_offset;
123 utility_detail::emit(output, mapped_event, report);
124 }
125 if (!mapped)
126 utility_detail::emit(output, event, report);
127 }
128 utility_detail::copy_sidecars(input, output, report);
129 return report;
130 }
131
132 float value(std::size_t index) const noexcept {
133 return index < MaximumMappings && states_[index].initialized
134 ? static_cast<float>(states_[index].current)
135 : 0.0f;
136 }
137
138 void reset() noexcept {
139 states_.fill({});
140 }
141 void flush() noexcept {
142 reset();
143 }
144 void hot_swap_reset() noexcept {
145 reset();
146 }
147
148 private:
149 static constexpr std::uint64_t nonnegative_sample_distance(std::int64_t later,
150 std::int64_t earlier) noexcept {
151 if (later <= earlier)
152 return 0;
153 return static_cast<std::uint64_t>(later) - static_cast<std::uint64_t>(earlier);
154 }
155
156 struct State {
157 double current = 0.0;
158 double target = 0.0;
159 std::int64_t last_sample = 0;
160 bool initialized = false;
161 };
162 std::array<ControllerMapRule, MaximumMappings> rules_{};
163 std::array<State, MaximumMappings> states_{};
164};
165
171
173 public:
179
180 explicit constexpr ScaleAwareMpePitch(ScaleAwareMpeSpec spec = {}) noexcept
181 : spec_(sanitize(spec)) {}
182
183 float target_cents(const MpeNoteState& note, const music::Scale& scale) noexcept {
184 const int source_note = std::min<int>(note.note, 127);
185 const int anchor = utility_detail::nearest_scale_note(source_note, scale);
186 const float finite_bend =
187 std::isfinite(note.pitch_bend_semitones) ? note.pitch_bend_semitones : 0.0f;
188 const float normalized =
189 std::clamp(finite_bend / spec_.input_bend_range_semitones, -1.0f, 1.0f);
190 const float degree_position = normalized * spec_.bend_range_degrees;
191 const int low_degree = static_cast<int>(std::floor(degree_position));
192 const int high_degree = static_cast<int>(std::ceil(degree_position));
193 const float fraction = degree_position - static_cast<float>(low_degree);
194 const int anchor_degree = nearest_degree(anchor, scale);
195 const auto low = scale.degree_to_semitones(anchor_degree + low_degree).value_or(0) -
196 scale.degree_to_semitones(anchor_degree).value_or(0);
197 const auto high = scale.degree_to_semitones(anchor_degree + high_degree).value_or(0) -
198 scale.degree_to_semitones(anchor_degree).value_or(0);
199 const double scale_bend =
200 static_cast<double>(low) + static_cast<double>(high - low) * fraction;
201 const double computed_cents =
202 static_cast<double>((anchor - source_note) * 100) + scale_bend * 100.0;
203 target_cents_ = std::isfinite(computed_cents) &&
204 std::abs(computed_cents) <=
205 static_cast<double>(std::numeric_limits<float>::max())
206 ? static_cast<float>(computed_cents)
207 : 0.0f;
208 if (!initialized_) {
209 current_cents_ = target_cents_;
210 initialized_ = true;
211 }
212 return target_cents_;
213 }
214
215 float advance(std::uint32_t samples, double sample_rate) noexcept {
216 if (!initialized_)
217 return 0.0f;
218 if (spec_.glide_seconds <= 0.0f || sample_rate <= 0.0 || !std::isfinite(sample_rate)) {
219 current_cents_ = target_cents_;
220 return current_cents_;
221 }
222 const double coefficient =
223 std::exp(-static_cast<double>(samples) /
224 (sample_rate * static_cast<double>(spec_.glide_seconds)));
225 const double next = static_cast<double>(target_cents_) +
226 (static_cast<double>(current_cents_) - target_cents_) * coefficient;
227 current_cents_ = std::isfinite(next) ? static_cast<float>(next) : target_cents_;
228 return current_cents_;
229 }
230
231 float current_cents() const noexcept {
232 return current_cents_;
233 }
234 void reset() noexcept {
235 initialized_ = false;
236 current_cents_ = target_cents_ = 0.0f;
237 }
238 void replace_spec(ScaleAwareMpeSpec spec) noexcept {
239 spec_ = sanitize(spec);
240 reset();
241 }
242
243 private:
244 static constexpr ScaleAwareMpeSpec sanitize(ScaleAwareMpeSpec spec) noexcept {
245 if (!(spec.input_bend_range_semitones > 0.0f) ||
246 !std::isfinite(spec.input_bend_range_semitones))
247 spec.input_bend_range_semitones = MpeVoiceTracker::kDefaultMemberBendSemitones;
248 if (!(spec.bend_range_degrees >= 0.0f) || !std::isfinite(spec.bend_range_degrees))
249 spec.bend_range_degrees = 0.0f;
250 spec.bend_range_degrees = std::min(spec.bend_range_degrees, 128.0f);
251 if (!(spec.glide_seconds >= 0.0f) || !std::isfinite(spec.glide_seconds))
252 spec.glide_seconds = 0.0f;
253 return spec;
254 }
255
256 static int nearest_degree(int note, const music::Scale& scale) noexcept {
257 const int root = static_cast<int>(scale.root());
258 const int approximate_octave = (note - root) / 12;
259 const int center = approximate_octave * static_cast<int>(scale.degree_count());
260 int best_degree = center;
261 int best_distance = std::numeric_limits<int>::max();
262 for (int degree = center - static_cast<int>(scale.degree_count());
263 degree <= center + static_cast<int>(scale.degree_count()); ++degree) {
264 const auto semitones = scale.degree_to_semitones(degree);
265 if (!semitones)
266 continue;
267 const int candidate = root + *semitones;
268 const int distance = std::abs(candidate - note);
269 if (distance < best_distance || (distance == best_distance && candidate < note)) {
270 best_degree = degree;
271 best_distance = distance;
272 }
273 }
274 return best_degree;
275 }
276
277 ScaleAwareMpeSpec spec_{};
278 float current_cents_ = 0.0f;
279 float target_cents_ = 0.0f;
280 bool initialized_ = false;
281};
282
283} // namespace pulp::midi
Definition controller_utility_kernels.hpp:26
void reset() noexcept
Definition controller_utility_kernels.hpp:138
void hot_swap_reset() noexcept
Definition controller_utility_kernels.hpp:144
bool set_rule(std::size_t index, ControllerMapRule rule) noexcept
Definition controller_utility_kernels.hpp:35
void flush() noexcept
Definition controller_utility_kernels.hpp:141
float value(std::size_t index) const noexcept
Definition controller_utility_kernels.hpp:132
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, double sample_rate, timebase::SamplePosition block_start={}) noexcept
Definition controller_utility_kernels.hpp:55
static constexpr MidiUtilityContract contract() noexcept
Definition controller_utility_kernels.hpp:29
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
std::size_t size() const
Definition buffer.hpp:123
static constexpr float kDefaultMemberBendSemitones
Default member-channel pitch-bend range (MPE spec).
Definition mpe_voice_tracker.hpp:73
float current_cents() const noexcept
Definition controller_utility_kernels.hpp:231
static constexpr MidiUtilityContract contract() noexcept
Definition controller_utility_kernels.hpp:174
void replace_spec(ScaleAwareMpeSpec spec) noexcept
Definition controller_utility_kernels.hpp:238
float advance(std::uint32_t samples, double sample_rate) noexcept
Definition controller_utility_kernels.hpp:215
float target_cents(const MpeNoteState &note, const music::Scale &scale) noexcept
Definition controller_utility_kernels.hpp:183
constexpr ScaleAwareMpePitch(ScaleAwareMpeSpec spec={}) noexcept
Definition controller_utility_kernels.hpp:180
void reset() noexcept
Definition controller_utility_kernels.hpp:234
Definition pitch.hpp:266
void copy_sidecars(const MidiBuffer &input, MidiBuffer &output, MidiUtilityProcessReport &report) noexcept
Definition utility_contract.hpp:96
void clear_output(MidiBuffer &output) noexcept
Definition utility_contract.hpp:82
bool blocks_alias(const MidiBuffer &first, const MidiBuffer &second) noexcept
Definition utility_contract.hpp:92
constexpr std::int64_t saturating_sample_add(std::int64_t position, std::int64_t offset) noexcept
Definition utility_contract.hpp:59
int nearest_scale_note(int note, const music::Scale &scale) noexcept
Definition utility_contract.hpp:165
bool ready(const MidiBuffer &output) noexcept
Definition utility_contract.hpp:86
bool emit(MidiBuffer &output, const MidiEvent &event, MidiUtilityProcessReport &report)
Definition utility_contract.hpp:67
Definition custom_node_events.hpp:26
@ MonotonicSamples
Definition utility_contract.hpp:32
@ DropUnstarted
Definition utility_contract.hpp:20
float(*)(float normalized, float amount) noexcept ControllerCurve
Definition controller_utility_kernels.hpp:7
@ InputStable
Definition utility_contract.hpp:26
float linear_controller_curve(float normalized, float) noexcept
Definition controller_utility_kernels.hpp:9
@ i
Definition formant_filter_bank.hpp:20
Definition controller_utility_kernels.hpp:13
float minimum
Definition controller_utility_kernels.hpp:18
std::uint8_t output_cc
Definition controller_utility_kernels.hpp:17
float smoothing_seconds
Definition controller_utility_kernels.hpp:21
std::uint8_t input_channel
Definition controller_utility_kernels.hpp:14
ControllerCurve curve
Definition controller_utility_kernels.hpp:22
bool enabled
Definition controller_utility_kernels.hpp:23
std::uint8_t output_channel
Definition controller_utility_kernels.hpp:16
float maximum
Definition controller_utility_kernels.hpp:19
float curve_amount
Definition controller_utility_kernels.hpp:20
std::uint8_t input_cc
Definition controller_utility_kernels.hpp:15
static MidiEvent cc(uint8_t channel, uint8_t controller, uint8_t value)
Create a Control Change event.
Definition message.hpp:64
Definition utility_contract.hpp:36
Definition utility_contract.hpp:46
Per-note state maintained by MpeVoiceTracker.
Definition mpe_voice_tracker.hpp:44
Definition controller_utility_kernels.hpp:166
float input_bend_range_semitones
Definition controller_utility_kernels.hpp:167
float bend_range_degrees
Definition controller_utility_kernels.hpp:168
float glide_seconds
Definition controller_utility_kernels.hpp:169
Definition rational_time.hpp:9