Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
mpe_synth_voice.hpp
Go to the documentation of this file.
1#pragma once
2
13
17#include <algorithm>
18#include <array>
19#include <atomic>
20#include <cmath>
21#include <cstddef>
22#include <cstdint>
23#include <limits>
24#include <vector>
25
26namespace pulp::midi {
27
31public:
32 virtual ~MpeSynthVoice() = default;
33
35 virtual void on_note_on(const MpeNoteState& note) {
36 note_ = note;
37 active_ = true;
38 // Jump targets to initial state; smoothers catch up naturally.
39 pitch_bend_target_ = note.pitch_bend_semitones;
40 pressure_target_ = note.pressure;
41 timbre_target_ = note.timbre;
42 }
43
45 virtual void on_note_off() { releasing_ = true; }
46
49 pitch_bend_ = smooth(pitch_bend_, pitch_bend_target_);
50 pressure_ = smooth(pressure_, pressure_target_);
51 timbre_ = smooth(timbre_, timbre_target_);
52 }
53
58 virtual void render(float* out, int num_samples) = 0;
59
61 virtual void reset() {
62 active_ = false;
63 releasing_ = false;
64 pitch_bend_ = pitch_bend_target_ = 0.0f;
65 pressure_ = pressure_target_ = 0.0f;
66 timbre_ = timbre_target_ = 0.0f;
67 note_ = {};
68 }
69
70 // Expression setters — advance_smoothers() ramps current → target.
71 void set_pitch_bend(float semitones) { pitch_bend_target_ = semitones; }
72 void set_pressure(float p) { pressure_target_ = p; }
73 void set_timbre(float t) { timbre_target_ = t; }
74
76 void set_smoothing(float coeff) {
77 if (coeff < 0.0f) coeff = 0.0f;
78 if (coeff >= 1.0f) coeff = 0.9999f;
79 smoothing_ = coeff;
80 }
81
82 // Getters for subclasses and the allocator.
83 bool active() const { return active_; }
84 bool releasing() const { return releasing_; }
85 MpeNoteGeneration note_id() const { return note_.note_id; }
86 uint8_t channel() const { return note_.channel; }
87 uint8_t note_number() const { return note_.note; }
88 uint8_t velocity() const { return note_.velocity; }
89 float pitch_bend() const { return pitch_bend_; }
90 float pressure() const { return pressure_; }
91 float timbre() const { return timbre_; }
92
93protected:
94 void finish_release() { active_ = false; releasing_ = false; }
95
96private:
97 float smooth(float current, float target) const {
98 return smoothing_ * current + (1.0f - smoothing_) * target;
99 }
100
101 MpeNoteState note_{};
102 bool active_ = false;
103 bool releasing_ = false;
104 float pitch_bend_ = 0.0f, pitch_bend_target_ = 0.0f;
105 float pressure_ = 0.0f, pressure_target_ = 0.0f;
106 float timbre_ = 0.0f, timbre_target_ = 0.0f;
107 float smoothing_ = 0.99f;
108};
109
117
126
137
141public:
144 bool observe_note_on(const MpeNoteState& note) {
145 const bool glide = channel_refcount_[note.channel] > 0;
146 ++channel_refcount_[note.channel];
147 return glide;
148 }
149 void observe_note_off(const MpeNoteState& note) {
150 if (channel_refcount_[note.channel] > 0) --channel_refcount_[note.channel];
151 }
152 void reset() { channel_refcount_.fill(0); }
153 bool channel_held(uint8_t ch) const { return channel_refcount_[ch] > 0; }
154private:
155 std::array<uint8_t, 16> channel_refcount_{};
156};
157
160template<typename Voice>
162 static_assert(std::is_base_of_v<MpeSynthVoice, Voice>,
163 "Voice must derive from MpeSynthVoice");
164public:
165 explicit MpeVoiceAllocator(std::size_t polyphony)
166 : voices_(polyphony) {}
167
168 std::size_t polyphony() const { return voices_.size(); }
169 Voice& voice(std::size_t i) { return voices_[i]; }
170 const Voice& voice(std::size_t i) const { return voices_[i]; }
171
172 void set_steal_mode(MpeVoiceStealMode m) { steal_mode_ = m; }
173 MpeVoiceStealMode steal_mode() const { return steal_mode_; }
174
177 switch (e.kind) {
179 const bool glide = glide_detector_.observe_note_on(e.state);
180 last_was_glide_ = glide;
181 Voice* v = pick_free_voice();
182 if (!v) {
183 v = steal_voice();
184 // Stealing ends the stolen note's lifetime; decrement
185 // the glide refcount so later note-ons on its channel
186 // aren't misclassified as legato. Only a still-HELD
187 // voice (active && !releasing) still owns its channel
188 // refcount here — a releasing voice already had its
189 // NoteOff processed (refcount decremented in the
190 // NoteOff case), so decrementing again would
191 // double-count and zero the count the just-placed
192 // note bumped.
193 if (v && v->active() && !v->releasing()) {
194 MpeNoteState released{};
195 released.channel = v->channel();
196 released.note = v->note_number();
197 released.note_id = v->note_id();
198 glide_detector_.observe_note_off(released);
199 }
200 }
201 if (v) {
202 v->reset();
203 v->on_note_on(e.state);
204 age_[static_cast<std::size_t>(v - voices_.data())] = ++age_counter_;
205 } else {
206 // No voice could be placed (zero polyphony). Balance
207 // the unconditional observe_note_on() increment above
208 // so the channel doesn't read glide-held forever, and
209 // report no glide since nothing actually sounds.
210 glide_detector_.observe_note_off(e.state);
211 last_was_glide_ = false;
212 }
213 break;
214 }
216 // Only decrement the glide refcount if the note is still
217 // tracked. Stolen notes were already retired (and their
218 // channel refcount decremented) in the NoteOn steal path;
219 // decrementing here again would double-count.
220 if (Voice* v = find_voice_by_id(e.state.note_id)) {
221 glide_detector_.observe_note_off(e.state);
222 v->on_note_off();
223 }
224 break;
225 }
227 if (Voice* v = find_voice_by_id(e.state.note_id))
228 v->set_pitch_bend(e.state.pitch_bend_semitones);
229 break;
231 if (Voice* v = find_voice_by_id(e.state.note_id))
232 v->set_pressure(e.state.pressure);
233 break;
235 if (Voice* v = find_voice_by_id(e.state.note_id))
236 v->set_timbre(e.state.timbre);
237 break;
238 }
239 }
240
242 void dispatch_all(const MpeBuffer& buf) {
243 for (const auto& e : buf) dispatch(e);
244 }
245
246 std::size_t active_count() const {
247 std::size_t n = 0;
248 for (const auto& v : voices_) if (v.active()) ++n;
249 return n;
250 }
251
252 std::size_t releasing_count() const {
253 std::size_t n = 0;
254 for (const auto& v : voices_) if (v.active() && v.releasing()) ++n;
255 return n;
256 }
257
258 std::uint64_t steal_count() const {
259 return steal_count_.load(std::memory_order_relaxed);
260 }
261
263 steal_count_.store(0, std::memory_order_relaxed);
264 }
265
271 return {
272 .polyphony = polyphony(),
273 .active_voice_count = active_count(),
274 .releasing_voice_count = releasing_count(),
275 .steal_count = steal_count(),
276 .steal_mode = steal_mode_,
277 .last_was_glide = last_was_glide_,
278 };
279 }
280
281 std::uint64_t estimate_optional_runtime_cost() const {
282 const auto t = telemetry();
283 std::uint64_t cost = 0;
284 cost = saturating_add_u64_(
285 cost, saturating_mul_u64_(
286 static_cast<std::uint64_t>(t.polyphony), 4));
287 cost = saturating_add_u64_(
288 cost, saturating_mul_u64_(
289 static_cast<std::uint64_t>(t.active_voice_count), 64));
290 cost = saturating_add_u64_(
291 cost, saturating_mul_u64_(
292 static_cast<std::uint64_t>(t.releasing_voice_count), 32));
293 return cost;
294 }
295
299 bool required = false) const {
300 const auto t = telemetry();
301 const auto cost = estimate_optional_runtime_cost();
302 const auto decision = frame.evaluate(lane, cost, required);
303 return {
304 .decision = decision,
305 .frame_stats = frame.stats(),
306 .telemetry = t,
307 .estimated_cost = cost,
308 };
309 }
310
311 bool last_was_glide() const { return last_was_glide_; }
312
313 void reset_all() {
314 for (auto& v : voices_) v.reset();
315 glide_detector_.reset();
316 last_was_glide_ = false;
317 age_counter_ = 0;
318 }
319
320private:
321 static std::uint64_t saturating_add_u64_(std::uint64_t a,
322 std::uint64_t b) {
323 const auto max = std::numeric_limits<std::uint64_t>::max();
324 return b > max - a ? max : a + b;
325 }
326
327 static std::uint64_t saturating_mul_u64_(std::uint64_t a,
328 std::uint64_t b) {
329 const auto max = std::numeric_limits<std::uint64_t>::max();
330 if (a == 0 || b == 0) return 0;
331 return a > max / b ? max : a * b;
332 }
333
334 Voice* pick_free_voice() {
335 for (auto& v : voices_) if (!v.active()) return &v;
336 return nullptr;
337 }
338
339 Voice* steal_voice() {
340 if (voices_.empty()) return nullptr;
341 auto cmp = [this](const Voice& a, const Voice& b) -> bool {
342 switch (steal_mode_) {
344 return age_[&a - voices_.data()] < age_[&b - voices_.data()];
346 return a.velocity() < b.velocity();
348 return a.note_number() < b.note_number();
350 return a.note_number() > b.note_number();
351 }
352 return false;
353 };
354 Voice* target = &voices_[0];
355 for (auto& v : voices_) if (cmp(v, *target)) target = &v;
356 steal_count_.fetch_add(1, std::memory_order_relaxed);
357 return target;
358 }
359
360 Voice* find_voice_by_id(MpeNoteGeneration id) {
361 for (auto& v : voices_) {
362 if (v.active() && v.note_id() == id) return &v;
363 }
364 return nullptr;
365 }
366
367 std::vector<Voice> voices_;
368 std::vector<uint64_t> age_ = std::vector<uint64_t>(voices_.size(), 0);
369 uint64_t age_counter_ = 0;
371 MpeGlideDetector glide_detector_;
372 bool last_was_glide_ = false;
373 std::atomic<std::uint64_t> steal_count_{0};
374};
375
376} // namespace pulp::midi
Small runtime policy helper for budgeted work and graceful degradation.
A time-ordered list of per-note expression events for one block.
Definition mpe_buffer.hpp:51
Glide/legato detector — observes MPE note-on events and reports when a new note on the same channel o...
Definition mpe_synth_voice.hpp:140
void observe_note_off(const MpeNoteState &note)
Definition mpe_synth_voice.hpp:149
void reset()
Definition mpe_synth_voice.hpp:152
bool channel_held(uint8_t ch) const
Definition mpe_synth_voice.hpp:153
bool observe_note_on(const MpeNoteState &note)
Returns true if note.channel already has an active note at the time of a new note-on,...
Definition mpe_synth_voice.hpp:144
Abstract base for a single MPE voice.
Definition mpe_synth_voice.hpp:30
void set_timbre(float t)
Definition mpe_synth_voice.hpp:73
float pitch_bend() const
Definition mpe_synth_voice.hpp:89
virtual void reset()
Hard-reset the voice (e.g. on allocation steal).
Definition mpe_synth_voice.hpp:61
uint8_t channel() const
Definition mpe_synth_voice.hpp:86
void set_smoothing(float coeff)
Smoothing coefficient in [0, 1). 0 = instant; 0.99 = ~100 sample TC.
Definition mpe_synth_voice.hpp:76
bool releasing() const
Definition mpe_synth_voice.hpp:84
float pressure() const
Definition mpe_synth_voice.hpp:90
void advance_smoothers()
Called once per sample before rendering. Advances smoothers.
Definition mpe_synth_voice.hpp:48
virtual void on_note_off()
Called by the allocator when this voice should release.
Definition mpe_synth_voice.hpp:45
bool active() const
Definition mpe_synth_voice.hpp:83
uint8_t note_number() const
Definition mpe_synth_voice.hpp:87
uint8_t velocity() const
Definition mpe_synth_voice.hpp:88
virtual void render(float *out, int num_samples)=0
Render num_samples of audio into out (additive or replacing — subclass decides).
virtual ~MpeSynthVoice()=default
virtual void on_note_on(const MpeNoteState &note)
Called by the allocator when this voice is activated.
Definition mpe_synth_voice.hpp:35
void set_pitch_bend(float semitones)
Definition mpe_synth_voice.hpp:71
float timbre() const
Definition mpe_synth_voice.hpp:91
void finish_release()
Definition mpe_synth_voice.hpp:94
void set_pressure(float p)
Definition mpe_synth_voice.hpp:72
MpeNoteGeneration note_id() const
Definition mpe_synth_voice.hpp:85
Voice & voice(std::size_t i)
Definition mpe_synth_voice.hpp:169
std::size_t active_count() const
Definition mpe_synth_voice.hpp:246
MpeVoiceAllocator(std::size_t polyphony)
Definition mpe_synth_voice.hpp:165
const Voice & voice(std::size_t i) const
Definition mpe_synth_voice.hpp:170
void dispatch_all(const MpeBuffer &buf)
Dispatch all events in a buffer (in the order they appear).
Definition mpe_synth_voice.hpp:242
bool last_was_glide() const
Definition mpe_synth_voice.hpp:311
std::uint64_t estimate_optional_runtime_cost() const
Definition mpe_synth_voice.hpp:281
MpeVoiceAllocatorTelemetry telemetry() const
Cheap owner-thread snapshot for voice-count telemetry.
Definition mpe_synth_voice.hpp:270
void reset_all()
Definition mpe_synth_voice.hpp:313
MpeVoiceAllocatorRuntimeBudgetReport evaluate_optional_runtime_budget(runtime::RuntimeBudgetFrame &frame, runtime::RuntimeWorkLane lane=runtime::RuntimeWorkLane::Background, bool required=false) const
Definition mpe_synth_voice.hpp:296
std::size_t releasing_count() const
Definition mpe_synth_voice.hpp:252
std::uint64_t steal_count() const
Definition mpe_synth_voice.hpp:258
void dispatch(const MpeExpressionEvent &e)
Dispatch a single MPE expression event to the voice pool.
Definition mpe_synth_voice.hpp:176
void reset_steal_count()
Definition mpe_synth_voice.hpp:262
void set_steal_mode(MpeVoiceStealMode m)
Definition mpe_synth_voice.hpp:172
MpeVoiceStealMode steal_mode() const
Definition mpe_synth_voice.hpp:173
std::size_t polyphony() const
Definition mpe_synth_voice.hpp:168
Definition budget_policy.hpp:101
RuntimeBudgetDecision evaluate(RuntimeWorkLane lane, std::uint64_t estimated_cost, bool required=false) noexcept
Definition budget_policy.hpp:112
const RuntimeBudgetFrameStats & stats() const noexcept
Definition budget_policy.hpp:130
Sample-accurate MPE expression event stream.
MPE (MIDI Polyphonic Expression) voice tracker.
Voice
The two registered voices.
Definition forge_synthesis_catalog.hpp:152
Definition custom_node_events.hpp:26
MpeVoiceStealMode
Voice-steal policy when the pool is full.
Definition mpe_synth_voice.hpp:111
@ HighestPitch
Steal the highest-note voice.
Definition mpe_synth_voice.hpp:115
@ Oldest
Steal the voice that started first.
Definition mpe_synth_voice.hpp:112
@ LowestPitch
Steal the lowest-note voice.
Definition mpe_synth_voice.hpp:114
@ LowestVelocity
Steal the quietest voice.
Definition mpe_synth_voice.hpp:113
std::uint64_t MpeNoteGeneration
Stable identity assigned to one accepted MPE note-on generation.
Definition mpe_voice_tracker.hpp:36
@ a
Definition pitch.hpp:29
@ b
Definition pitch.hpp:31
RuntimeWorkLane
Definition budget_policy.hpp:11
@ Background
Definition budget_policy.hpp:14
@ v
Definition input_events.hpp:41
A single per-note expression event.
Definition mpe_buffer.hpp:31
@ NoteOn
Definition mpe_buffer.hpp:33
@ NoteOff
Definition mpe_buffer.hpp:34
@ Pressure
Definition mpe_buffer.hpp:36
@ PitchBend
Definition mpe_buffer.hpp:35
@ Timbre
Definition mpe_buffer.hpp:37
Per-note state maintained by MpeVoiceTracker.
Definition mpe_voice_tracker.hpp:44
float timbre
CC 74 (0..1); same value as slide.
Definition mpe_voice_tracker.hpp:51
float pitch_bend_semitones
Current per-note pitch bend in semitones.
Definition mpe_voice_tracker.hpp:49
float pressure
Channel pressure (0..1).
Definition mpe_voice_tracker.hpp:50
uint8_t channel
MPE member channel the note lives on.
Definition mpe_voice_tracker.hpp:46
MpeNoteGeneration note_id
Monotonic generation assigned on note-on (never 0).
Definition mpe_voice_tracker.hpp:52
uint8_t note
MIDI note number (0-127).
Definition mpe_voice_tracker.hpp:47
Definition mpe_synth_voice.hpp:127
runtime::RuntimeBudgetFrameStats frame_stats
Definition mpe_synth_voice.hpp:129
MpeVoiceAllocatorTelemetry telemetry
Definition mpe_synth_voice.hpp:130
bool should_run_optional_work() const noexcept
Definition mpe_synth_voice.hpp:133
std::uint64_t estimated_cost
Definition mpe_synth_voice.hpp:131
runtime::RuntimeBudgetDecision decision
Definition mpe_synth_voice.hpp:128
Definition mpe_synth_voice.hpp:118
bool last_was_glide
Definition mpe_synth_voice.hpp:124
std::uint64_t steal_count
Definition mpe_synth_voice.hpp:122
std::size_t active_voice_count
Definition mpe_synth_voice.hpp:120
MpeVoiceStealMode steal_mode
Definition mpe_synth_voice.hpp:123
std::size_t releasing_voice_count
Definition mpe_synth_voice.hpp:121
std::size_t polyphony
Definition mpe_synth_voice.hpp:119
Definition budget_policy.hpp:39
Definition budget_policy.hpp:46