Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
aax_editor.hpp
Go to the documentation of this file.
1#pragma once
2
9
10#include <cstdint>
11#include <functional>
12#include <optional>
13#include <string>
14#include <string_view>
15#include <unordered_map>
16#include <unordered_set>
17
18// Editor-support logic for the AAX adapter: parameter-gesture routing and the
19// editor sizing contract. Both are expressed without any AAX SDK type so they
20// are testable on every platform, including builds with no SDK configured. The
21// thin shell that talks to `AAX_CEffectGUI` lives in `aax_effect_gui.cpp`.
22//
23// AAX records automation from an explicit touch/release pair: a custom editor
24// must tell the host "the user grabbed this control" before writing values and
25// "the user let go" afterwards. Pulp's editors already emit that intent through
26// `state::Binding::begin_gesture()` / `end_gesture()`, which fan out via
27// `StateStore::set_gesture_callbacks`. This router is the adapter between the
28// two: it resolves a `state::ParamID` to the AAX parameter-ID string the
29// EffectParameters object registered, and enforces the pairing AAX requires.
30
32
37inline bool param_id_for_aax_id(const PluginDefinition& definition,
38 std::string_view aax_id,
39 state::ParamID& out_id) {
40 for (const auto& binding : definition.parameters) {
41 if (binding.aax_id == aax_id) {
42 out_id = binding.id;
43 return true;
44 }
45 }
46 return false;
47}
48
49// ── The parameter taper, in one place ──────────────────────────────────────
50//
51// AAX reaches a parameter's real value through an `AAX_ITaperDelegate<T>`; the
52// adapter's delegate is these three functions over a `state::ParamRange`. They
53// live here, free of AAX types, because `ParameterMirror` below has to model
54// what AAX does with a write — and a second, drifting copy of this math is
55// exactly how the mirror's rule came apart from the taper it was reasoning
56// about. `FloatTaperDelegate` in the adapter calls straight through to these,
57// so the SDK shell, the mirror, and the tests cannot disagree about the map.
58
62inline float taper_constrain_real(const state::ParamRange& range, float value) {
63 return range.denormalize(range.normalize(value));
64}
65
67inline float taper_normalized_to_real(const state::ParamRange& range, double normalized) {
68 return range.denormalize(static_cast<float>(normalized));
69}
70
76inline double taper_real_to_normalized(const state::ParamRange& range, float value) {
77 return range.normalize(taper_constrain_real(range, value));
78}
79
84inline float aax_value_after_write(const state::ParamRange& range, float value) {
85 return taper_normalized_to_real(range, taper_real_to_normalized(range, value));
86}
87
103public:
104 virtual ~EditorHost() = default;
105
109
113
118 virtual std::optional<ViewSize> editor_view_size() = 0;
119
121 virtual const PluginDefinition& editor_definition() const = 0;
122
126 virtual void release_editor_gestures() = 0;
127
133 virtual void set_editor_invalidation_handler(std::function<void()> handler) {
134 (void)handler;
135 }
136};
137
153 uint32_t width = 0;
154 uint32_t height = 0;
155 uint32_t min_width = 0;
156 uint32_t min_height = 0;
157 bool resizable = false;
158 bool pin_viewport = false;
165 uint32_t design_width = 0;
166 uint32_t design_height = 0;
167 float aspect_ratio = 0.0f;
168};
169
178constexpr uint32_t aax_host_dimension(uint32_t logical, float display_scale) {
179 (void)display_scale;
180 return logical;
181}
182
192 EditorSizePlan plan;
193 plan.width = hints.preferred_width;
194 plan.height = hints.preferred_height;
195 plan.resizable = hints.min_width > 0 && hints.min_height > 0;
196
198 if (plan.pin_viewport) {
201 }
202 plan.aspect_ratio = static_cast<float>(native_view_aspect_ratio(hints));
203
204 // A fixed-size editor's floor is its design size: reporting a smaller one
205 // would invite the host to shrink a surface that cannot reflow.
206 plan.min_width = plan.resizable ? hints.min_width : plan.width;
207 plan.min_height = plan.resizable ? hints.min_height : plan.height;
208 return plan;
209}
210
216public:
217 explicit ParameterIdMap(const PluginDefinition& definition) {
218 ids_.reserve(definition.parameters.size());
219 for (const auto& binding : definition.parameters) {
220 ids_.emplace(binding.id, binding.aax_id);
221 }
222 }
223
226 const std::string* find(state::ParamID id) const {
227 const auto it = ids_.find(id);
228 return it == ids_.end() ? nullptr : &it->second;
229 }
230
231private:
232 std::unordered_map<state::ParamID, std::string> ids_;
233};
234
304public:
307 using ValueReader = std::function<bool(const char* aax_param_id, float& out_value)>;
308
311 using ValueWriter = std::function<bool(const char* aax_param_id, float value)>;
312
320 static constexpr std::uint32_t kCorrectionFuseLimit = 8;
321
322 ParameterMirror(const PluginDefinition& definition, ValueReader reader, ValueWriter writer)
323 : reader_(std::move(reader))
324 , writer_(std::move(writer))
325 {
326 params_.reserve(definition.parameters.size());
327 for (const auto& binding : definition.parameters) {
328 params_.emplace(binding.id, Entry{binding.aax_id, binding.range});
329 }
330 }
331
334 bool on_store_value_changed(state::ParamID id, float store_value) {
335 const auto it = params_.find(id);
336 if (it == params_.end()) {
337 return false; // not an AAX-registered parameter
338 }
339 Entry& entry = it->second;
340
341 float aax_value = 0.0f;
342 if (!reader_ || !reader_(entry.aax_id.c_str(), aax_value)) {
343 return false;
344 }
345 const bool already_agrees =
346 taper_real_to_normalized(entry.range, store_value) ==
347 taper_real_to_normalized(entry.range, aax_value);
348 const bool write_is_a_no_op =
349 aax_value_after_write(entry.range, store_value) == aax_value;
350 if (already_agrees || write_is_a_no_op) {
351 ++suppressed_count_;
352 entry.consecutive_corrections = 0; // settled — re-arm the fuse
353 return false;
354 }
355
356 // A new value is a new correction, however the last one ended.
357 if (!entry.has_last_written || entry.last_written != store_value) {
358 entry.last_written = store_value;
359 entry.has_last_written = true;
360 entry.consecutive_corrections = 0;
361 }
362
363 // The backstop. The rule above is what makes the loop terminate; this is
364 // what keeps a host that breaks the rule anyway from wedging Pro Tools.
365 // Stopping leaves AAX holding a value at most one taper round trip from
366 // the store's — the host stays authoritative and correct to within that
367 // epsilon, which is strictly better than an unbounded touch/post/release
368 // stream at the host's dispatch rate. The next distinct value re-arms
369 // it, so a fused parameter is not deaf for the rest of the session.
370 if (entry.consecutive_corrections >= kCorrectionFuseLimit) {
371 ++refused_count_;
372 if (!entry.fuse_reported) {
373 entry.fuse_reported = true; // report the parameter once, not the stream
375 "AAX mirror: parameter '{}' did not converge after {} corrective "
376 "writes of {}; AAX holds {}. Leaving the host authoritative.",
377 entry.aax_id, kCorrectionFuseLimit, store_value, aax_value);
378 }
379 return false;
380 }
381 ++entry.consecutive_corrections;
382
383 return writer_ && writer_(entry.aax_id.c_str(), store_value);
384 }
385
388 std::uint64_t suppressed_count() const { return suppressed_count_; }
389
392 std::uint64_t refused_count() const { return refused_count_; }
393
396 bool is_fused(state::ParamID id) const {
397 const auto it = params_.find(id);
398 return it != params_.end() &&
399 it->second.consecutive_corrections >= kCorrectionFuseLimit;
400 }
401
402private:
403 struct Entry {
404 std::string aax_id;
405 state::ParamRange range;
406 float last_written = 0.0f;
407 bool has_last_written = false;
408 std::uint32_t consecutive_corrections = 0;
409 bool fuse_reported = false;
410 };
411
412 std::unordered_map<state::ParamID, Entry> params_;
413 ValueReader reader_;
414 ValueWriter writer_;
415 std::uint64_t suppressed_count_ = 0;
416 std::uint64_t refused_count_ = 0;
417};
418
437public:
439 using Sink = std::function<void(const char* aax_param_id)>;
440
441 GestureRouter(const PluginDefinition& definition, Sink touch, Sink release)
442 : ids_(definition)
443 , touch_(std::move(touch))
444 , release_(std::move(release))
445 {}
446
450 const auto* aax_id = lookup(id);
451 if (!aax_id) {
452 ++unknown_count_;
453 return false;
454 }
455 if (!touched_.insert(id).second) {
456 return false; // already touched — keep the record single
457 }
458 if (touch_) touch_(aax_id->c_str());
459 return true;
460 }
461
465 const auto* aax_id = lookup(id);
466 if (!aax_id) {
467 ++unknown_count_;
468 return false;
469 }
470 if (touched_.erase(id) == 0) {
471 return false; // never touched — nothing to release
472 }
473 if (release_) release_(aax_id->c_str());
474 return true;
475 }
476
480 void release_all() {
481 // Copy: end() mutates touched_.
482 const std::unordered_set<state::ParamID> held = touched_;
483 for (const auto id : held) {
484 end(id);
485 }
486 }
487
489 bool is_touched(state::ParamID id) const { return touched_.count(id) != 0; }
490
492 std::size_t touched_count() const { return touched_.size(); }
493
496 std::uint64_t unknown_count() const { return unknown_count_; }
497
499 const std::string* aax_id_for(state::ParamID id) const { return lookup(id); }
500
501private:
502 const std::string* lookup(state::ParamID id) const { return ids_.find(id); }
503
504 ParameterIdMap ids_;
505 Sink touch_;
506 Sink release_;
507 std::unordered_set<state::ParamID> touched_;
508 std::uint64_t unknown_count_ = 0;
509};
510
511} // namespace pulp::format::aax
Definition processor.hpp:174
The editor-facing surface of the AAX data model.
Definition aax_editor.hpp:102
virtual state::StateStore * editor_store()=0
The store the editor's Bindings read and write.
virtual void set_editor_invalidation_handler(std::function< void()> handler)
Install the callback the model fires when the Processor behind editor_processor() is about to go away...
Definition aax_editor.hpp:133
virtual std::optional< ViewSize > editor_view_size()=0
The plugin's declared ViewSize hints, or nullopt when it has no editor to size.
virtual ~EditorHost()=default
virtual const PluginDefinition & editor_definition() const =0
The parameter/bus model this plugin registered with AAX.
virtual void release_editor_gestures()=0
Release any automation gesture still open.
virtual Processor * editor_processor()=0
The model-side Processor whose create_view() builds the editor.
const std::string * aax_id_for(state::ParamID id) const
The AAX parameter-ID string for id, or nullptr when unregistered.
Definition aax_editor.hpp:499
std::size_t touched_count() const
Number of parameters with an open gesture.
Definition aax_editor.hpp:492
std::function< void(const char *aax_param_id)> Sink
Invoked with the AAX parameter-ID string for the gesture's parameter.
Definition aax_editor.hpp:439
std::uint64_t unknown_count() const
Gestures dropped because the parameter has no AAX registration.
Definition aax_editor.hpp:496
bool begin(state::ParamID id)
Route Binding::begin_gesture() to AAX_IEffectParameters::TouchParameter.
Definition aax_editor.hpp:449
bool end(state::ParamID id)
Route Binding::end_gesture() to AAX_IEffectParameters::ReleaseParameter.
Definition aax_editor.hpp:464
bool is_touched(state::ParamID id) const
Whether a gesture is currently open on id.
Definition aax_editor.hpp:489
void release_all()
Release every parameter still held.
Definition aax_editor.hpp:480
GestureRouter(const PluginDefinition &definition, Sink touch, Sink release)
Definition aax_editor.hpp:441
const std::string * find(state::ParamID id) const
The AAX parameter-ID string for id, or nullptr when Pulp knows the parameter but AAX never registered...
Definition aax_editor.hpp:226
ParameterIdMap(const PluginDefinition &definition)
Definition aax_editor.hpp:217
std::function< bool(const char *aax_param_id, float &out_value)> ValueReader
Reads the value the AAX parameter manager currently holds.
Definition aax_editor.hpp:307
bool on_store_value_changed(state::ParamID id, float store_value)
Editor store → AAX parameter manager.
Definition aax_editor.hpp:334
ParameterMirror(const PluginDefinition &definition, ValueReader reader, ValueWriter writer)
Definition aax_editor.hpp:322
bool is_fused(state::ParamID id) const
Whether id's fuse is currently open, i.e.
Definition aax_editor.hpp:396
std::function< bool(const char *aax_param_id, float value)> ValueWriter
Requests value for aax_param_id — AAX_IParameter::SetValueWithFloat.
Definition aax_editor.hpp:311
std::uint64_t suppressed_count() const
Echoes absorbed.
Definition aax_editor.hpp:388
static constexpr std::uint32_t kCorrectionFuseLimit
Consecutive corrective writes of the same store value before the fuse opens on a parameter.
Definition aax_editor.hpp:320
std::uint64_t refused_count() const
Corrective writes the fuse refused.
Definition aax_editor.hpp:392
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
Definition aax_editor.hpp:31
float aax_value_after_write(const state::ParamRange &range, float value)
The value an AAX parameter ends up holding once SetValueWithFloat(value) has been through the host an...
Definition aax_editor.hpp:84
float taper_constrain_real(const state::ParamRange &range, float value)
AAX_ITaperDelegate::ConstrainRealValue — the nearest value the range can actually represent,...
Definition aax_editor.hpp:62
double taper_real_to_normalized(const state::ParamRange &range, float value)
AAX_ITaperDelegate::RealToNormalized.
Definition aax_editor.hpp:76
EditorSizePlan plan_editor_size(const ViewSize &hints)
Derive the sizing plan from a plugin's declared ViewSize hints.
Definition aax_editor.hpp:191
bool param_id_for_aax_id(const PluginDefinition &definition, std::string_view aax_id, state::ParamID &out_id)
Resolve an AAX parameter-ID string back to the state::ParamID it was registered under.
Definition aax_editor.hpp:37
float taper_normalized_to_real(const state::ParamRange &range, double normalized)
AAX_ITaperDelegate::NormalizedToReal.
Definition aax_editor.hpp:67
constexpr uint32_t aax_host_dimension(uint32_t logical, float display_scale)
Convert Pulp's logical editor coordinates to AAX host units.
Definition aax_editor.hpp:178
@ release
Release only: voices enter their normal release and tails ring out (MIDI CC 123 semantic).
Definition panic.hpp:39
constexpr double native_view_aspect_ratio(const ViewSize &hints)
Resolve the native host constraint while preserving the legacy fixed-editor contract.
Definition plugin_descriptor.hpp:128
constexpr bool should_pin_design_viewport(const ViewSize &hints)
Editor-host resize contract shared by the format adapters (VST3 PulpPlugView, the AU v2 Cocoa view; m...
Definition plugin_descriptor.hpp:106
constexpr uint32_t design_viewport_width(const ViewSize &hints)
Definition plugin_descriptor.hpp:54
constexpr uint32_t design_viewport_height(const ViewSize &hints)
Definition plugin_descriptor.hpp:58
void log_error(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:195
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
STL namespace.
Editor size hints (in logical pixels).
Definition plugin_descriptor.hpp:27
uint32_t preferred_height
Definition plugin_descriptor.hpp:29
uint32_t preferred_width
Definition plugin_descriptor.hpp:28
uint32_t min_width
Definition plugin_descriptor.hpp:30
uint32_t min_height
Definition plugin_descriptor.hpp:31
How the editor surface reacts to the size AAX gives it.
Definition aax_editor.hpp:152
float aspect_ratio
host aspect lock; 0 = none
Definition aax_editor.hpp:167
uint32_t width
size reported to the host from GetViewSize
Definition aax_editor.hpp:153
bool resizable
host may drive a different size
Definition aax_editor.hpp:157
uint32_t min_height
Definition aax_editor.hpp:156
bool pin_viewport
pin design viewport + lock aspect
Definition aax_editor.hpp:158
uint32_t min_width
reported from GetMinimumViewSize
Definition aax_editor.hpp:155
uint32_t height
Definition aax_editor.hpp:154
uint32_t design_width
The coordinate space the root lays out in while pinned.
Definition aax_editor.hpp:165
uint32_t design_height
Definition aax_editor.hpp:166
Definition aax_model.hpp:59
std::vector< ParameterBinding > parameters
Definition aax_model.hpp:62
Defines the numeric range of a parameter, including normalization.
Definition parameter.hpp:131
float denormalize(float normalized) const
Map a normalized [0, 1] value back to the real range.
Definition parameter.hpp:188
float normalize(float value) const
Map a real value to the normalized [0, 1] range.
Definition parameter.hpp:168