Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
adapter_boundary.hpp
Go to the documentation of this file.
1#pragma once
2
61
63#include <pulp/format/detail/playhead_diff.hpp>
66#include <pulp/state/store.hpp>
67
68#include <array>
69#include <cmath>
70#include <cstddef>
71#include <cstdint>
72#include <cstring>
73#include <span>
74#include <vector>
75
77
83inline constexpr std::size_t kBoundaryMaxChannels = 8;
84
85// ---------------------------------------------------------------------------
86// 1. f64 (double) marshalling
87//
88// Raw-pointer block copies for the double-precision host boundary. A host that
89// runs the graph in `double` hands the adapter `double*` buffers; the shared
90// `Processor` renders in `float`, so the adapter converts in and out. These are
91// the exact loops that were duplicated in three TUs. `count` is the per-block
92// frame count; all three are branch-light and vectorise.
93// ---------------------------------------------------------------------------
94
96inline void copy_f32_to_f64(const float* src, double* dst,
97 std::uint32_t count) noexcept {
98 if (!src || !dst) return;
99 for (std::uint32_t i = 0; i < count; ++i)
100 dst[i] = static_cast<double>(src[i]);
101}
102
104inline void copy_f64_to_f32(const double* src, float* dst,
105 std::uint32_t count) noexcept {
106 if (!src || !dst) return;
107 for (std::uint32_t i = 0; i < count; ++i)
108 dst[i] = static_cast<float>(src[i]);
109}
110
112inline void zero_f64(double* dst, std::uint32_t count) noexcept {
113 if (!dst) return;
114 for (std::uint32_t i = 0; i < count; ++i) dst[i] = 0.0;
115}
116
117// ---------------------------------------------------------------------------
118// 2. transport → ProcessContext
119// ---------------------------------------------------------------------------
120
128
129 bool is_playing = false;
130 bool is_recording = false;
131 bool is_looping = false;
132
133 double tempo_bpm = 120.0;
134
135 double position_beats = 0.0;
136
137 std::int64_t position_samples = 0;
138
141
142 double loop_start_beats = 0.0;
143 double loop_end_beats = 0.0;
144
147 std::int64_t host_bar = 0;
148
149 std::int64_t host_time_ns = 0;
151};
152
163inline void apply_host_transport(ProcessContext& ctx, const HostTransport& transport,
164 detail::PlayheadSnapshot& snapshot) noexcept {
165 ctx.transport_validity = transport.validity;
166 if (transport.validity.has(TransportField::Playing))
167 ctx.is_playing = transport.is_playing;
168 if (transport.validity.has(TransportField::Recording))
169 ctx.is_recording = transport.is_recording;
170 if (transport.validity.has(TransportField::Looping))
171 ctx.is_looping = transport.is_looping;
172 if (transport.validity.has(TransportField::Tempo))
173 ctx.tempo_bpm = transport.tempo_bpm;
174 if (transport.validity.has(TransportField::BeatPosition))
175 ctx.position_beats = transport.position_beats;
176 if (transport.validity.has(TransportField::SamplePosition))
177 ctx.position_samples = transport.position_samples;
178 if (transport.validity.has(TransportField::TimeSignature)) {
179 ctx.time_sig_numerator = transport.time_sig_numerator;
180 ctx.time_sig_denominator = transport.time_sig_denominator;
181 }
182 if (transport.validity.has(TransportField::LoopRange)) {
183 ctx.loop_start_beats = transport.loop_start_beats;
184 ctx.loop_end_beats = transport.loop_end_beats;
185 }
186 if (transport.validity.has(TransportField::HostTime))
187 ctx.host_time_ns = transport.host_time_ns;
188 if (transport.validity.has(TransportField::FrameRate))
189 ctx.frame_rate = transport.frame_rate;
190 if (transport.validity.has(TransportField::Bar)) {
191 ctx.bar = transport.host_bar;
192 } else if (transport.validity.has(TransportField::BeatPosition) &&
193 transport.validity.has(TransportField::TimeSignature) &&
194 std::isfinite(ctx.position_beats) &&
195 ctx.time_sig_numerator > 0 && ctx.time_sig_denominator > 0) {
196 detail::derive_bar_from_beats(ctx);
197 ctx.transport_validity.set(TransportField::Bar);
198 }
199 detail::compute_playhead_changes(
200 ctx, snapshot, detail::TransportDiffMode::FieldValidity);
201}
202
203// ---------------------------------------------------------------------------
204// 3. latency-compensated bypass
205// ---------------------------------------------------------------------------
206
220template <std::size_t MaxChannels = kBoundaryMaxChannels>
222public:
228 void prepare(int latency_samples) {
229 delay_samples_ = latency_samples > 0 ? latency_samples : 0;
230 if (delay_samples_ > 0) {
231 for (auto& line : lines_) line.prepare(delay_samples_);
232 }
233 }
234
236 void reset() {
237 for (auto& line : lines_) line.reset();
238 }
239
241 bool is_latency_compensated() const noexcept { return delay_samples_ > 0; }
242
244 int delay_samples() const noexcept { return delay_samples_; }
245
247 static constexpr std::size_t channel_capacity() noexcept { return MaxChannels; }
248
257 float process_sample(float in, std::size_t ch) {
258 if (delay_samples_ <= 0 || ch >= lines_.size()) return in;
259 return lines_[ch].process(in, static_cast<float>(delay_samples_));
260 }
261
266 void process_channel(float* out, const float* in, std::uint32_t count,
267 std::size_t ch) {
268 if (!out) return;
269 if (delay_samples_ <= 0 || ch >= lines_.size()) {
270 // Uncompensated: straight dry passthrough (or silence with no input).
271 if (in) {
272 if (out != in) std::memcpy(out, in, sizeof(float) * count);
273 } else {
274 std::memset(out, 0, sizeof(float) * count);
275 }
276 return;
277 }
278 auto& line = lines_[ch];
279 const float delay = static_cast<float>(delay_samples_);
280 if (in) {
281 for (std::uint32_t i = 0; i < count; ++i)
282 out[i] = line.process(in[i], delay);
283 } else {
284 for (std::uint32_t i = 0; i < count; ++i)
285 out[i] = line.process(0.0f, delay);
286 }
287 }
288
289private:
290 std::array<signal::DelayLine, MaxChannels> lines_{};
291 int delay_samples_ = 0;
292};
293
295
316template <std::size_t MaxChannels>
319 float* const* out_ptrs, int out_channels,
320 const float* const* in_ptrs, int in_channels,
321 std::uint32_t count) {
322 const bool delayed = bypass.is_latency_compensated() &&
323 out_channels <= static_cast<int>(bypass.channel_capacity());
324 for (int ch = 0; ch < out_channels; ++ch) {
325 float* dst = out_ptrs ? out_ptrs[ch] : nullptr;
326 if (dst == nullptr) continue;
327 const float* src =
328 (in_ptrs && ch < in_channels) ? in_ptrs[ch] : nullptr;
329 if (delayed) {
330 // process_channel emits `src[n - latency]`, or delayed silence when
331 // src is null — every channel takes the same delayed path.
332 bypass.process_channel(dst, src, count,
333 static_cast<std::size_t>(ch));
334 } else if (src != nullptr) {
335 std::memcpy(dst, src, sizeof(float) * count);
336 } else {
337 std::memset(dst, 0, sizeof(float) * count);
338 }
339 }
340}
341
342// ---------------------------------------------------------------------------
343// 4. parameter dual-write
344// ---------------------------------------------------------------------------
345
363 state::StateStore& store,
365 std::int32_t sample_offset,
366 float value) {
367 const bool queued = queue.push(state::ParameterEvent{id, sample_offset, value});
368 store.set_value_rt(id, value);
369 return queued;
370}
371
372// ---------------------------------------------------------------------------
373// 5. output-parameter publication
374//
375// The mirror of (4): values the *plugin* changed during process() have to reach
376// the host so it can record automation. The host-ABI emit differs per format
377// (VST3 `IParamValueQueue::addPoint`, CLAP `out_events->try_push`), but the
378// bookkeeping around it does not: snapshot every parameter before process(),
379// diff after, and skip any parameter that already reported sample-accurate
380// explicit events. These are the pieces of that bookkeeping which carry no
381// format currency. Consumed by `clap_adapter.cpp` and `vst3_adapter.cpp`.
382// ---------------------------------------------------------------------------
383
385inline constexpr std::size_t kParamIndexNotFound = static_cast<std::size_t>(-1);
386
399inline std::size_t find_param_index(std::span<const state::ParamInfo> all_params,
400 state::ParamID id) noexcept {
401 for (std::size_t i = 0; i < all_params.size(); ++i) {
402 if (all_params[i].id == id) return i;
403 }
404 return kParamIndexNotFound;
405}
406
415 std::span<const state::ParamInfo> all_params,
416 std::vector<float>& snapshot) {
417 snapshot.resize(all_params.size());
418 for (std::size_t i = 0; i < all_params.size(); ++i) {
419 snapshot[i] = store.get_value(all_params[i].id);
420 }
421}
422
426 std::uint64_t generation = 0;
427 bool valid = false;
428};
429
439template <typename AfterRead = void (*)()>
441 const state::StateStore& store, std::span<const state::ParamInfo> all_params,
442 std::vector<float>& snapshot, ParamSnapshotState& state, std::uint64_t generation,
443 AfterRead&& after_read = [] {}) {
444 if (state.valid && state.generation == generation && snapshot.size() == all_params.size())
445 return;
446 const bool quiet_before = store.state_snapshot_is_current(generation);
447 snapshot_param_values(store, all_params, snapshot);
448 after_read();
449 state.generation = generation;
450 state.valid = quiet_before && store.state_snapshot_is_current(generation);
451}
452
461inline bool changed_since_snapshot(float current, float snapshotted) noexcept {
462 return std::memcmp(&current, &snapshotted, sizeof(float)) != 0;
463}
464
465// ---------------------------------------------------------------------------
466// 6. MPE per-note-expression sidecar
467//
468// A format that carries per-note expression feeds a Processor that opted into
469// MPE (PluginDescriptor::supports_mpe) through Pulp's MPE model: inbound MIDI
470// runs an MpeVoiceTracker whose callbacks append per-note deltas to an
471// MpeBuffer, and that buffer is handed to the Processor for exactly the duration
472// of one process() call. CLAP and VST3 each open-coded the identical enable /
473// bind / per-block clear-run-attach dance around their own event decode;
474// MpeSidecar is the single owner of it. The per-format expression *decode*
475// (VST3 kNoteExpressionValueEvent, CLAP clap_event_note_expression_t) stays in
476// the adapter — it synthesizes channel-wide MIDI via mpe_expression.hpp into the
477// shared MidiBuffer this consumes.
478// ---------------------------------------------------------------------------
479
495 std::int32_t current_sample_offset = 0;
497 bool enabled = false;
500
501 MpeSidecar() = default;
502 MpeSidecar(const MpeSidecar&) = delete;
503 MpeSidecar& operator=(const MpeSidecar&) = delete;
506
511 void configure(bool supports_mpe) {
512 enabled = supports_mpe;
513 if (enabled) {
516 }
517 }
518
523 void reserve(std::size_t event_capacity) {
524 buffer.reserve(event_capacity);
525 buffer.set_realtime_capacity_limit(true);
526 }
527
535 void reset() noexcept {
536 tracker.reset();
537 buffer.clear();
540 }
541
553 template <typename MidiRange>
554 [[nodiscard]] bool run(Processor& processor, const MidiRange& midi_in) {
555 if constexpr (requires { midi_in.dropped_event_count(); }) {
556 if (midi_in.dropped_event_count() != 0) {
557 // Source-stream completeness is required even when the
558 // processor did not opt into MPE: a retained attack followed
559 // by a dropped release can stick an ordinary MIDI voice too.
560 reset();
561 processor.set_mpe_input(enabled ? &buffer : nullptr);
562 return false;
563 }
564 }
565 if (enabled) {
566 buffer.clear();
568 // Releases that could not fit at the end of the previous block
569 // must lead this block before any new starts. The tracker blocks
570 // note-ons while this fixed queue is nonempty, so no voice can be
571 // orphaned and the queue remains bounded by tracker polyphony.
573 tracker.flush_pending_note_offs();
574 for (const auto& ev : midi_in) {
575 current_sample_offset = ev.sample_offset;
576 tracker.process(ev);
577 }
579 // Expression callbacks observe tracker state after mutation. If
580 // any fixed-capacity append fails, discard the partial stream
581 // and tracker state together; the adapter requests a processor
582 // reset so neither side retains a newer voice snapshot.
583 reset();
584 processor.set_mpe_input(&buffer);
585 return false;
586 }
587 processor.set_mpe_input(&buffer);
588 } else {
589 processor.set_mpe_input(nullptr);
590 }
591 return true;
592 }
593};
594
595} // namespace pulp::format::boundary
Definition processor.hpp:174
void set_mpe_input(const midi::MpeBuffer *mpe)
Definition processor.hpp:1032
Compact validity mask for the transport values carried by ProcessContext.
Definition transport_fields.hpp:29
Per-channel dry-signal delay used by an adapter's bypass path.
Definition adapter_boundary.hpp:221
static constexpr std::size_t channel_capacity() noexcept
Number of per-channel delay lines this instance owns.
Definition adapter_boundary.hpp:247
int delay_samples() const noexcept
The compensation delay in samples (0 when uncompensated).
Definition adapter_boundary.hpp:244
void prepare(int latency_samples)
Size the per-channel delay to latency_samples (the processor's reported latency, i....
Definition adapter_boundary.hpp:228
bool is_latency_compensated() const noexcept
True when a non-zero reported latency is being compensated.
Definition adapter_boundary.hpp:241
float process_sample(float in, std::size_t ch)
Push one dry sample through channel ch's delay and return the compensated output (in[n - delay] in st...
Definition adapter_boundary.hpp:257
void process_channel(float *out, const float *in, std::uint32_t count, std::size_t ch)
Emit in[n - delay] into out for channel ch (steady state), or a straight copy of in when uncompensate...
Definition adapter_boundary.hpp:266
void reset()
Clear the delay history without resizing (e.g. on a transport reset).
Definition adapter_boundary.hpp:236
A time-ordered list of per-note expression events for one block.
Definition mpe_buffer.hpp:51
Tracks MPE voice state across channels.
Definition mpe_voice_tracker.hpp:67
Definition parameter_event_queue.hpp:82
bool push(const ParameterEvent &e)
Definition parameter_event_queue.hpp:88
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
float get_value(ParamID id) const
Read a parameter's base value (lock-free).
Definition store.hpp:186
void set_value_rt(ParamID id, float value)
Real-time-safe parameter write for use from the audio thread.
bool state_snapshot_is_current(std::uint64_t generation) const noexcept
True when no writer is in flight and none has advanced the generation past generation.
Definition store.hpp:330
Definition adapter_boundary.hpp:76
LatencyCompensatedBypassT< kBoundaryMaxChannels > LatencyCompensatedBypass
Definition adapter_boundary.hpp:294
void copy_f64_to_f32(const double *src, float *dst, std::uint32_t count) noexcept
Narrow count doubles src → dst as floats. No-op on a null pointer.
Definition adapter_boundary.hpp:104
void refresh_param_snapshot(const state::StateStore &store, std::span< const state::ParamInfo > all_params, std::vector< float > &snapshot, ParamSnapshotState &state, std::uint64_t generation, AfterRead &&after_read=[] {})
Brings snapshot up to date for a block whose store generation is generation, re-reading every value o...
Definition adapter_boundary.hpp:440
bool apply_param_value(state::ParameterEventQueue &queue, state::StateStore &store, state::ParamID id, std::int32_t sample_offset, float value)
Apply one incoming host parameter-value event, the way every adapter must.
Definition adapter_boundary.hpp:362
bool changed_since_snapshot(float current, float snapshotted) noexcept
True when current differs from snapshotted, compared bitwise.
Definition adapter_boundary.hpp:461
void snapshot_param_values(const state::StateStore &store, std::span< const state::ParamInfo > all_params, std::vector< float > &snapshot)
Capture every parameter's current value into snapshot, positionally aligned with all_params,...
Definition adapter_boundary.hpp:414
void apply_host_transport(ProcessContext &ctx, const HostTransport &transport, detail::PlayheadSnapshot &snapshot) noexcept
Write transport into ctx's transport fields, derive the bar when the host did not supply one,...
Definition adapter_boundary.hpp:163
std::size_t find_param_index(std::span< const state::ParamInfo > all_params, state::ParamID id) noexcept
Position of id within all_params, or kParamIndexNotFound.
Definition adapter_boundary.hpp:399
void copy_f32_to_f64(const float *src, double *dst, std::uint32_t count) noexcept
Widen count floats src → dst as doubles. No-op on a null pointer.
Definition adapter_boundary.hpp:96
constexpr std::size_t kBoundaryMaxChannels
Channel ceiling — the single source of truth for the per-bus channel count every adapter caps at.
Definition adapter_boundary.hpp:83
constexpr std::size_t kParamIndexNotFound
Index returned by find_param_index when the id is not registered.
Definition adapter_boundary.hpp:385
void render_bypass_passthrough(LatencyCompensatedBypassT< MaxChannels > &bypass, float *const *out_ptrs, int out_channels, const float *const *in_ptrs, int in_channels, std::uint32_t count)
Render one bypassed block: copy main input → output per channel, latency- compensated through bypass ...
Definition adapter_boundary.hpp:317
void zero_f64(double *dst, std::uint32_t count) noexcept
Zero count doubles. No-op on a null pointer.
Definition adapter_boundary.hpp:112
FrameRate
SMPTE video frame rate.
Definition process_context.hpp:24
@ unknown
Definition process_context.hpp:25
@ BeatPosition
Definition transport_fields.hpp:14
@ FrameRate
Definition transport_fields.hpp:20
@ HostTime
Definition transport_fields.hpp:19
@ LoopRange
Definition transport_fields.hpp:17
@ Looping
Definition transport_fields.hpp:12
@ TimeSignature
Definition transport_fields.hpp:16
@ Recording
Definition transport_fields.hpp:11
@ Playing
Definition transport_fields.hpp:10
@ Tempo
Definition transport_fields.hpp:13
@ Bar
Definition transport_fields.hpp:18
@ SamplePosition
Definition transport_fields.hpp:15
void bind_tracker_to_buffer(MpeVoiceTracker &tracker, MpeBuffer &out, int32_t &current_sample_offset, bool *expression_event_dropped=nullptr)
Convenience: install tracker callbacks that forward events to out with the given sample offset.
Definition mpe_buffer.hpp:163
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Format-neutral transport snapshot, already decoded from a host playhead into canonical units (quarter...
Definition adapter_boundary.hpp:126
double loop_start_beats
meaningful only when is_looping
Definition adapter_boundary.hpp:142
TransportValidity validity
Definition adapter_boundary.hpp:127
std::int64_t position_samples
Definition adapter_boundary.hpp:137
std::int64_t host_bar
A host that publishes a precomputed bar sets TransportField::Bar; otherwise the mapper derives it fro...
Definition adapter_boundary.hpp:147
double loop_end_beats
Definition adapter_boundary.hpp:143
double tempo_bpm
Definition adapter_boundary.hpp:133
bool is_looping
Definition adapter_boundary.hpp:131
bool is_recording
Definition adapter_boundary.hpp:130
double position_beats
quarter notes
Definition adapter_boundary.hpp:135
FrameRate frame_rate
Definition adapter_boundary.hpp:150
std::int64_t host_time_ns
Definition adapter_boundary.hpp:149
int time_sig_denominator
Definition adapter_boundary.hpp:140
int time_sig_numerator
Definition adapter_boundary.hpp:139
bool is_playing
Definition adapter_boundary.hpp:129
midi::MpeBuffer buffer
Definition adapter_boundary.hpp:492
void reserve(std::size_t event_capacity)
Reserve the expression buffer to event_capacity and pin it to realtime-capacity mode so run() never a...
Definition adapter_boundary.hpp:523
bool run(Processor &processor, const MidiRange &midi_in)
Per-block: when enabled, clear the buffer, run midi_in through the tracker (stamping each emitted exp...
Definition adapter_boundary.hpp:554
void configure(bool supports_mpe)
Enable the sidecar per the descriptor opt-in and, when enabled, bind the tracker's callbacks to buffe...
Definition adapter_boundary.hpp:511
MpeSidecar(const MpeSidecar &)=delete
bool enabled
Mirrors PluginDescriptor::effective_capabilities().supports_mpe.
Definition adapter_boundary.hpp:497
MpeSidecar(MpeSidecar &&)=delete
bool expression_event_dropped
Set by the bound expression callbacks when fixed storage is exhausted.
Definition adapter_boundary.hpp:499
void reset() noexcept
Drop adapter-owned per-note state so a re-activation / processor reset cannot route a stale noteId.
Definition adapter_boundary.hpp:535
std::int32_t current_sample_offset
Sample offset stamped onto every expression event the tracker emits for the MIDI event currently bein...
Definition adapter_boundary.hpp:495
midi::MpeVoiceTracker tracker
Definition adapter_boundary.hpp:491
MpeSidecar & operator=(MpeSidecar &&)=delete
MpeSidecar & operator=(const MpeSidecar &)=delete
Generation-gated pre-process snapshot: which generation it was taken at and whether it can be trusted...
Definition adapter_boundary.hpp:425
bool valid
Definition adapter_boundary.hpp:427
std::uint64_t generation
Definition adapter_boundary.hpp:426
Definition parameter_event_queue.hpp:34