Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
crossfade_plugin_slot.hpp
Go to the documentation of this file.
1#pragma once
2
3// A PluginSlot wrapper that crossfades the outgoing instance into the incoming one
4// for a short window after a live swap, so replacing a hosted plugin instance is
5// click-free (not just gap-free). It holds both instances + a TransitionMixer and
6// blends old->new in process() via the shared signal::blend_fade_out primitive — the
7// same crossfade law the reload hot-swap slot uses. Every non-process method delegates
8// to the new (surviving) instance, so the graph executor renders it as an ordinary
9// PluginSlot with no executor-side changes.
10//
11// Lifetime: the wrapper OWNS the retained old instance. Once fade_done() is true the
12// blend is a no-op (output is the pure new render), and the control thread collapses
13// the node back to the bare new slot + drops the wrapper (freeing the old instance on
14// the control thread, after RCU readers have drained) — never on the audio thread.
15
18#include <pulp/midi/buffer.hpp>
20
21#include <algorithm>
22#include <atomic>
23#include <cstddef>
24#include <memory>
25#include <vector>
26
27namespace pulp::host {
28
29class CrossfadePluginSlot final : public PluginSlot {
30public:
31 // fade_samples <= 0 or a null old instance means "no fade" — the wrapper still
32 // renders the new instance but blend is skipped (done from the start). out_channels
33 // / max_block size the scratch that the old render targets before the blend.
34 CrossfadePluginSlot(std::shared_ptr<PluginSlot> new_slot,
35 std::shared_ptr<PluginSlot> old_slot,
36 std::size_t fade_samples,
38 int out_channels,
39 int max_block)
40 : new_slot_(std::move(new_slot)),
41 old_slot_(std::move(old_slot)),
42 scratch_channels_(std::max(0, out_channels)),
43 scratch_frames_(std::max(0, max_block)) {
44 mixer_.configure(fade_samples, curve);
45 if (fade_samples == 0 || !old_slot_) {
46 done_.store(true, std::memory_order_release);
47 }
48 scratch_storage_.assign(
49 static_cast<std::size_t>(scratch_channels_) * static_cast<std::size_t>(scratch_frames_),
50 0.0f);
51 scratch_ptrs_.resize(static_cast<std::size_t>(scratch_channels_));
52 for (int c = 0; c < scratch_channels_; ++c) {
53 scratch_ptrs_[static_cast<std::size_t>(c)] =
54 scratch_storage_.data() + static_cast<std::size_t>(c) * static_cast<std::size_t>(scratch_frames_);
55 }
56 // Pre-size the fading-out instance's MIDI scratch and cap it to reserved
57 // capacity, so a fade-out slot that emits MIDI (arp, MIDI-FX, note-expression)
58 // never heap-allocates on the audio thread — mirrors the reload hot-swap slot.
59 old_midi_in_.reserve(64, 0);
60 old_midi_out_.reserve(64, 0);
61 old_midi_in_.set_realtime_capacity_limit(true);
62 old_midi_out_.set_realtime_capacity_limit(true);
63 }
64
65 // True once the crossfade has fully completed (output is the pure new render).
66 // Read on the control thread to collapse the wrapper back to the bare new slot.
67 bool fade_done() const noexcept { return done_.load(std::memory_order_acquire); }
68
69 // The surviving instance, for the control thread to reinstate once the fade is done.
70 const std::shared_ptr<PluginSlot>& new_slot() const noexcept { return new_slot_; }
71
72 // ── process: blend old -> new ────────────────────────────────────────────
75 const midi::MidiBuffer& midi_in,
76 midi::MidiBuffer& midi_out,
77 const ParameterEventQueue& param_events,
78 int num_samples) override {
80 new_slot_->process(output, input, midi_in, midi_out, param_events, num_samples);
81 blend_old_into_(output, input, num_samples);
82 }
83
84 void begin_process() noexcept override {
85 new_slot_->begin_process();
86 if (old_slot_)
87 old_slot_->begin_process();
88 }
89
90 ProcessStatus process_status() const noexcept override {
91 if (new_slot_->process_status() == ProcessStatus::failed ||
92 (old_slot_ && old_slot_->process_status() == ProcessStatus::failed))
94 return ProcessStatus::ok;
95 }
96
98 const midi::MidiBuffer& midi_in,
99 midi::MidiBuffer& midi_out,
100 const ParameterEventQueue& param_events,
101 int num_samples) override {
103 if (skip_blend_(audio, num_samples)) {
104 new_slot_->process(audio, midi_in, midi_out, param_events, num_samples);
105 return;
106 }
107 // Render OLD into the main output, snapshot it to scratch, then render NEW over
108 // the top and blend scratch(old) -> output(new). Rendering both through the real
109 // ProcessBuffers keeps the surviving instance's bus routing intact.
110 old_midi_in_.clear();
111 old_midi_out_.clear();
112 old_slot_->process(audio, old_midi_in_, old_midi_out_, empty_params_, num_samples);
113 blend_after_old_(audio, [&] {
114 new_slot_->process(audio, midi_in, midi_out, param_events, num_samples);
115 }, num_samples);
116 }
117
119 const midi::MidiBuffer& midi_in,
120 midi::MidiBuffer& midi_out,
121 const ParameterEventQueue& param_events,
122 int num_samples,
123 const format::ProcessContext& transport) override {
125 if (skip_blend_(audio, num_samples)) {
126 new_slot_->process(audio, midi_in, midi_out, param_events, num_samples, transport);
127 return;
128 }
129 // Both instances see transport: a transport-sensitive fading-out instance must
130 // keep its playhead/tempo across the swap or it would step at fade start.
131 old_midi_in_.clear();
132 old_midi_out_.clear();
133 old_slot_->process(audio, old_midi_in_, old_midi_out_, empty_params_, num_samples, transport);
134 blend_after_old_(audio, [&] {
135 new_slot_->process(audio, midi_in, midi_out, param_events, num_samples, transport);
136 }, num_samples);
137 }
138
139 // ── delegate everything else to the surviving instance ───────────────────
140 const PluginInfo& info() const override { return new_slot_->info(); }
141 bool is_loaded() const override { return new_slot_->is_loaded(); }
142 bool prepare(double sr, int block) override { return new_slot_->prepare(sr, block); }
143 void release() override { new_slot_->release(); }
144 std::vector<HostParamInfo> parameters() const override { return new_slot_->parameters(); }
145 float get_parameter(uint32_t id) const override { return new_slot_->get_parameter(id); }
146 void set_parameter(uint32_t id, float v) override { new_slot_->set_parameter(id, v); }
147 void set_bypass(bool b) override { new_slot_->set_bypass(b); }
148 bool is_bypassed() const override { return new_slot_->is_bypassed(); }
149 BypassSurface bypass_surface() const override { return new_slot_->bypass_surface(); }
150 std::vector<uint8_t> save_state() const override { return new_slot_->save_state(); }
151 bool restore_state(const std::vector<uint8_t>& d) override { return new_slot_->restore_state(d); }
152 bool has_editor() const override { return new_slot_->has_editor(); }
153 void* create_editor_view() override { return new_slot_->create_editor_view(); }
154 void destroy_editor_view() override { new_slot_->destroy_editor_view(); }
155 std::unique_ptr<HostedEditor> create_hosted_editor(void* parent) override {
156 return new_slot_->create_hosted_editor(parent);
157 }
158 void destroy_hosted_editor(std::unique_ptr<HostedEditor> ed) override {
159 new_slot_->destroy_hosted_editor(std::move(ed));
160 }
161 int latency_samples() const override { return new_slot_->latency_samples(); }
162 int tail_samples() const override { return new_slot_->tail_samples(); }
163 void accept(NativeHandleVisitor& v) const override { new_slot_->accept(v); }
164 bool wants_transport() const override { return new_slot_->wants_transport(); }
165
166private:
167 // True when there is nothing to blend for this ProcessBuffers block (fade already
168 // done, no old instance, no main bus, or a block larger than the scratch — which
169 // snaps the fade to complete). The caller then just renders the new instance.
170 bool skip_blend_(format::ProcessBuffers& audio, int num_samples) {
171 if (done_.load(std::memory_order_relaxed) || !old_slot_) return true;
172 auto* main_out = audio.main_output();
173 if (main_out == nullptr) return true;
174 const int ch = std::min<int>(scratch_channels_, static_cast<int>(main_out->num_channels()));
175 if (num_samples > scratch_frames_ || ch <= 0) {
176 done_.store(true, std::memory_order_release);
177 return true;
178 }
179 return false;
180 }
181
182 // The old instance has already rendered into the main output; snapshot it to
183 // scratch, render the new instance over the top, then blend scratch(old) into the
184 // output(new) under the mixer. RT-safe: no alloc/lock. skip_blend_ has guaranteed a
185 // main bus, ch>0, and num_samples <= scratch_frames_.
186 template <class RenderNew>
187 void blend_after_old_(format::ProcessBuffers& audio, RenderNew&& render_new, int num_samples) {
188 auto* main_out = audio.main_output();
189 const int ch = std::min<int>(scratch_channels_, static_cast<int>(main_out->num_channels()));
190 for (int c = 0; c < ch; ++c) {
191 std::copy_n(main_out->channel_ptr(static_cast<std::size_t>(c)),
192 static_cast<std::size_t>(num_samples),
193 scratch_ptrs_[static_cast<std::size_t>(c)]);
194 }
195 render_new();
196 audio::BufferView<float> scratch(scratch_ptrs_.data(), static_cast<std::size_t>(ch),
197 static_cast<std::size_t>(num_samples));
198 if (signal::blend_fade_out(mixer_, *main_out, scratch)) {
199 done_.store(true, std::memory_order_release);
200 }
201 }
202
203 // Render the fading-out instance into scratch (same input, no MIDI/param events —
204 // it is on its way out) and blend it into `output` under the mixer. RT-safe: no
205 // alloc/lock (scratch + mixer are pre-sized at construction). Used by the
206 // transport-less BufferView overload.
207 void blend_old_into_(audio::BufferView<float>& output,
208 const audio::BufferView<const float>& input,
209 int num_samples) {
210 if (done_.load(std::memory_order_relaxed) || !old_slot_) return;
211 const int ch = std::min<int>(scratch_channels_, static_cast<int>(output.num_channels()));
212 if (num_samples > scratch_frames_ || ch <= 0) {
213 // Block larger than the scratch (misdeclared block size): snap to full-new.
214 done_.store(true, std::memory_order_release);
215 return;
216 }
217 audio::BufferView<float> scratch(scratch_ptrs_.data(), static_cast<std::size_t>(ch),
218 static_cast<std::size_t>(num_samples));
219 old_midi_in_.clear();
220 old_midi_out_.clear();
221 old_slot_->process(scratch, input, old_midi_in_, old_midi_out_, empty_params_, num_samples);
222 if (signal::blend_fade_out(mixer_, output, scratch)) {
223 done_.store(true, std::memory_order_release);
224 }
225 }
226
227 std::shared_ptr<PluginSlot> new_slot_;
228 std::shared_ptr<PluginSlot> old_slot_;
230 std::vector<float> scratch_storage_;
231 std::vector<float*> scratch_ptrs_;
232 int scratch_channels_ = 0;
233 int scratch_frames_ = 0;
234 std::atomic<bool> done_{false};
235 midi::MidiBuffer old_midi_in_;
236 midi::MidiBuffer old_midi_out_;
237 ParameterEventQueue empty_params_;
238};
239
240} // namespace pulp::host
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
ProcessStatus process_status() const noexcept override
Definition crossfade_plugin_slot.hpp:90
void set_parameter(uint32_t id, float v) override
Definition crossfade_plugin_slot.hpp:146
void * create_editor_view() override
Definition crossfade_plugin_slot.hpp:153
const PluginInfo & info() const override
Definition crossfade_plugin_slot.hpp:140
bool restore_state(const std::vector< uint8_t > &d) override
Definition crossfade_plugin_slot.hpp:151
std::vector< HostParamInfo > parameters() const override
Definition crossfade_plugin_slot.hpp:144
bool is_loaded() const override
Definition crossfade_plugin_slot.hpp:141
int latency_samples() const override
Definition crossfade_plugin_slot.hpp:161
float get_parameter(uint32_t id) const override
Definition crossfade_plugin_slot.hpp:145
bool fade_done() const noexcept
Definition crossfade_plugin_slot.hpp:67
int tail_samples() const override
Definition crossfade_plugin_slot.hpp:162
void destroy_editor_view() override
Definition crossfade_plugin_slot.hpp:154
std::vector< uint8_t > save_state() const override
Definition crossfade_plugin_slot.hpp:150
bool prepare(double sr, int block) override
Definition crossfade_plugin_slot.hpp:142
std::unique_ptr< HostedEditor > create_hosted_editor(void *parent) override
Create the hosted editor.
Definition crossfade_plugin_slot.hpp:155
void set_bypass(bool b) override
Definition crossfade_plugin_slot.hpp:147
void destroy_hosted_editor(std::unique_ptr< HostedEditor > ed) override
Tear down a hosted editor. Default routes to destroy_editor_view().
Definition crossfade_plugin_slot.hpp:158
const std::shared_ptr< PluginSlot > & new_slot() const noexcept
Definition crossfade_plugin_slot.hpp:70
void accept(NativeHandleVisitor &v) const override
Definition crossfade_plugin_slot.hpp:163
void process(format::ProcessBuffers &audio, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples, const format::ProcessContext &transport) override
Definition crossfade_plugin_slot.hpp:118
void release() override
Definition crossfade_plugin_slot.hpp:143
bool has_editor() const override
Definition crossfade_plugin_slot.hpp:152
BypassSurface bypass_surface() const override
Where this plugin keeps its own bypass control.
Definition crossfade_plugin_slot.hpp:149
bool wants_transport() const override
Definition crossfade_plugin_slot.hpp:164
CrossfadePluginSlot(std::shared_ptr< PluginSlot > new_slot, std::shared_ptr< PluginSlot > old_slot, std::size_t fade_samples, signal::TransitionCurve curve, int out_channels, int max_block)
Definition crossfade_plugin_slot.hpp:34
bool is_bypassed() const override
Definition crossfade_plugin_slot.hpp:148
void process(audio::BufferView< float > &output, const audio::BufferView< const float > &input, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples) override
Definition crossfade_plugin_slot.hpp:73
void begin_process() noexcept override
Definition crossfade_plugin_slot.hpp:84
void process(format::ProcessBuffers &audio, const midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ParameterEventQueue &param_events, int num_samples) override
Definition crossfade_plugin_slot.hpp:97
Definition native_handle_visitor.hpp:116
Definition plugin_slot.hpp:94
ProcessStatus
Definition plugin_slot.hpp:101
@ failed
Definition plugin_slot.hpp:101
@ ok
Definition plugin_slot.hpp:101
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Definition analyzer_provider.hpp:12
ProcessBuffersT< float > ProcessBuffers
Definition process_block.hpp:422
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
::pulp::state::ParameterEventQueue ParameterEventQueue
Definition parameter_event_queue.hpp:8
BypassSurface
Definition plugin_slot.hpp:64
TransitionMixerT< float > TransitionMixer
Definition transition_mixer.hpp:83
TransitionCurve
Definition transition_mixer.hpp:34
bool blend_fade_out(Mixer &mixer, OutView &out, const OldView &old_render) noexcept
Blend an already-rendered fading-out buffer into the live output under a TransitionMixer's old->new g...
Definition transition_mixer.hpp:97
STL namespace.
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Definition scanner.hpp:35
Shared click-free crossfade primitive.