Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
wam_adapter.hpp
Go to the documentation of this file.
1#pragma once
2
3// WAMv2 (Web Audio Modules v2) format adapter for Pulp
4// Wraps a Pulp Processor as a WAMv2 plugin for browser-based DAWs.
5//
6// Architecture (WAMv2 spec: https://github.com/webaudiomodules/api):
7// WebAudioModule (JS, main thread)
8// ├── WamDescriptor — plugin metadata from Processor::descriptor()
9// ├── WamNode (AudioWorkletNode) — main-thread proxy
10// │ ├── parameter info/values bridge
11// │ ├── state save/load
12// │ └── event scheduling (MIDI, automation, transport)
13// └── WamProcessor (AudioWorkletProcessor) — audio thread
14// ├── Pulp Processor (WASM-compiled C++)
15// ├── StateStore parameter sync
16// └── MIDI event processing
17//
18// The C++ side provides:
19// - WamDescriptorData: serializable metadata for JS WamDescriptor
20// - WamProcessorBridge: audio-thread processing callable from AudioWorklet
21// - Emscripten exports for JS interop
22//
23// The JS side (wam-plugin.js) wraps this as a standard WAMv2 module.
24
26#include <pulp/audio/buffer.hpp>
27#include <pulp/midi/buffer.hpp>
28#include <pulp/state/store.hpp>
29#include <memory>
30#include <string>
31#include <vector>
32#include <atomic>
33#include <cstdint>
34
36
37// Serializable descriptor matching WAMv2 WamDescriptor schema
39 std::string name;
40 std::string vendor;
41 std::string version;
42 std::string api_version = "2.0.0";
43 std::string description;
44 std::string website;
45 std::string thumbnail;
46 std::vector<std::string> keywords;
47 bool is_instrument = false;
48 bool has_audio_input = true;
49 bool has_audio_output = true;
50 bool has_midi_input = false;
51 bool has_midi_output = false;
54 bool has_mpe_input = false;
55 bool has_mpe_output = false;
56
57 // Delay-compensation / tail hints surfaced to the web host so it can align
58 // the plugin like a native adapter would. latency_samples comes from
59 // Processor::latency_samples() (a method, not a descriptor field, so the
60 // bridge fills it after from_processor()); tail_samples is
61 // PluginDescriptor::tail_samples.
63 int tail_samples = 0;
64
65 // Non-empty only for a rack: the per-stage display names, emitted as a
66 // "stages" array. A single plugin leaves this empty, so to_json() produces
67 // the exact single-plugin object it always has. This is what lets the rack
68 // descriptor reuse the ONE emitter below instead of hand-rolling a second.
69 std::vector<std::string> stages;
70
71 // Build from a Pulp PluginDescriptor
73
74 // Serialize to JSON string for JS consumption. The single source of truth
75 // for the WAMv2 descriptor JSON — both the single-plugin bridge and the rack
76 // (which fills `stages`) emit through here so the two can never drift.
77 std::string to_json() const;
78};
79
80// WAMv2 parameter info matching WamParameterInfo schema
82 std::string id; // string ID (WAMv2 uses string IDs)
83 std::string label;
84 std::string type; // "float", "int", "boolean", "choice"
86 float min_value;
87 float max_value;
88 float step; // raw parameter step (e.g. 0.1); 0 = continuous
89 int discrete_step; // integer step for int/boolean params; 0 otherwise
90 std::string unit;
91 // Display names for each discrete value, when the parameter is stepped and
92 // declares a ParamInfo::to_string formatter (e.g. Sine/Saw/Square/Triangle).
93 // Empty for continuous parameters. Lets a generated web control render a
94 // <select> of real names instead of a bare 0..3 slider.
95 std::vector<std::string> value_labels;
96};
97
98// Host-supplied transport snapshot (Web Audio has no transport of its own).
99// Plain POD so a set_transport() call is an allocation-free field write; copied
100// into ProcessContext at the top of every process(). Shared by both the single-
101// plugin bridge and the chain rack.
103 bool valid = false;
104 bool is_playing = false;
105 double tempo_bpm = 120.0;
106 double position_beats = 0.0;
107 int64_t position_samples = 0;
110};
111
112// One processor + its StateStore + its audio-output and MIDI-output buffers:
113// the reusable unit shared by the single-plugin bridge and the chain rack. A
114// stage does NOT own host-facing scratch (interleave buffers, the inbound MIDI
115// queue, the drain scratch) — those are the bridge's concern. It receives its
116// audio input as a BufferView and its MIDI input as a MidiBuffer&, so a chain
117// can hand stage i's output buffer directly to stage i+1 as its input with no
118// copy and no sample-offset rewriting (same wasm memory, by reference).
119//
120// RT contract: initialize() reserves the output planar buffer and the midi_out_
121// buffer once (the latter with set_realtime_capacity_limit(true)); run() then
122// neither allocates nor locks.
123class WamStage {
124public:
125 WamStage() = default;
126
127 // Bind a processor, define its parameters, prepare its DSP, and size its
128 // output buffers. Called once, off the audio thread.
129 bool initialize(std::unique_ptr<Processor> processor,
130 double sample_rate, int max_block_size);
131
132 // Re-prepare for a sample-rate / block-size change. CONTROL THREAD ONLY —
133 // re-runs Processor::prepare() and grows the output buffer only when the
134 // block size increases (a same-or-smaller block never allocates).
135 void prepare(double sample_rate, int block_size);
136
137 // Run one block. `in_view` and `midi_in` are supplied by the bridge: host
138 // audio + inbound MIDI for stage 0, the previous stage's output buffer +
139 // midi_out for later stages. Writes this stage's audio into its own planar
140 // output and its MIDI into midi_out_ (cleared first). Allocation-free.
142 midi::MidiBuffer& midi_in, const ProcessContext& ctx,
143 int num_frames);
144
145 // A const view over this stage's output planar buffer, to feed the next
146 // stage as its audio input — zero copy, same wasm memory.
148
149 // Copy this stage's planar output into the host's planar output channel
150 // buffers (one pointer per channel — the same layout wam_process receives).
151 // A straight per-channel copy: the rack is planar end-to-end, no interleave.
152 void copy_output_into(float* const* outputs, int host_channels,
153 int num_frames) const;
154
155 // The MIDI this stage produced during the last run() — handed by reference
156 // to the next stage as its midi_in, or serialized for the host by the rack.
157 midi::MidiBuffer& midi_out() { return midi_out_; }
158 const midi::MidiBuffer& midi_out() const { return midi_out_; }
159
160 // Parameters, addressed by the plugin's own numeric ParamID within a stage.
161 //
162 // Writing the value only MARKS the stage's derived state dirty; the non-RT
163 // pass runs later, in service_non_realtime(). See that method for why the
164 // work is not done inline here.
165 void set_param(state::ParamID id, float value) {
166 store_.set_value(id, value);
167 tick_pending_ = true;
168 }
169 float get_param(state::ParamID id) const { return store_.get_value(id); }
170 const state::StateStore& store() const { return store_; }
171
172 // Mark this stage's derived state dirty without touching a parameter (a
173 // state restore names a different IR / sample / wavetable than the live one).
174 void mark_non_realtime_dirty() { tick_pending_ = true; }
175
176 // COALESCED non-realtime pass — at most ONE per render turn.
177 //
178 // A wasm module has no worker thread, so a processor that derives heavy
179 // state from a control change (SuperConvolver rebuilds its impulse response
180 // when `Size` moves: decode, resample, window, FFT re-partition) can only do
181 // it on the render thread. The worklet dispatches port.onmessage on THAT
182 // thread, and a single knob drag delivers a burst of param messages in one
183 // turn — so ticking inline per message did the whole rebuild once per
184 // message, N× the work with only the last result ever audible. That is an
185 // underrun while the user drags a knob.
186 //
187 // Instead the setters mark dirty and the bridge calls this ONCE per block,
188 // AFTER the render call, so the burst collapses into a single rebuild of the
189 // LATEST value. That is exactly the coalescing the native background worker
190 // gets for free from its poll loop. The cost is one block of latency before
191 // a control change is audible (~2.7 ms at 128 frames / 48 kHz).
192 //
193 // Also honours the processor's own `non_realtime_tick_pending()` so work a
194 // processor discovers for itself still gets serviced.
196 if (!processor_) return;
197 if (!tick_pending_ && !processor_->non_realtime_tick_pending()) return;
198 tick_pending_ = false;
199 processor_->on_non_realtime_tick();
200 }
201
202 // Bumped by an audio-thread listener on every parameter change in this
203 // stage, the plugin's own writes from inside process() included. See
204 // WamProcessorBridge::param_epoch for why a host needs this.
205 uint32_t param_epoch() const {
206 return param_epoch_.load(std::memory_order_relaxed);
207 }
208
209 // Bulk value read in all_params() declaration order. Writes
210 // min(count, capacity) floats; returns this stage's parameter count.
211 int read_param_values(float* dst, int capacity) const;
212
213 // Per-stage state: the same versioned "PWS1" container the single-plugin
214 // bridge writes, so one stage's blob is byte-identical to a standalone
215 // plugin's and a legacy blob still loads.
216 std::vector<uint8_t> get_state() const;
217 bool set_state(const uint8_t* data, size_t size);
218
220 return processor_ ? processor_->descriptor() : PluginDescriptor{};
221 }
222 int latency_samples() const {
223 return processor_ ? processor_->latency_samples() : 0;
224 }
225 int num_channels() const { return num_channels_; }
226 bool ready() const { return processor_ != nullptr; }
227
228private:
229 static constexpr std::size_t kMidiEventCapacity = 256;
230 static constexpr std::size_t kSysexCapacity = 16;
231 static constexpr std::size_t kSysexPayloadCapacity = 512;
232
233 // The store is declared before the Processor so it is destroyed after it.
234 // `Processor::state()` dereferences a pointer to this store, and a Processor
235 // may read it from its destructor or from a worker thread that destructor is
236 // about to join. Reversing these two lines hands that thread a freed store.
237 state::StateStore store_;
238 std::unique_ptr<Processor> processor_;
239
240 std::atomic<uint32_t> param_epoch_{0};
241 state::ListenerToken param_listener_;
242
243 std::vector<float> output_planar_;
244 std::vector<float*> output_ptrs_;
245 midi::MidiBuffer midi_out_;
246
247 // Set by a control write, cleared by service_non_realtime(). Plain bool: the
248 // worklet is single-threaded — the render call and port.onmessage run on the
249 // same thread.
250 bool tick_pending_ = false;
251
252 int num_channels_ = 2;
253 int block_size_ = 128;
254};
255
256// Audio-thread bridge: wraps a Processor for AudioWorkletProcessor calls
258public:
261
262 // Called once from AudioWorkletProcessor.constructor()
263 bool initialize(double sample_rate, int max_block_size);
264
265 // Re-run the processor's prepare() for a real sample-rate / block-size
266 // change (a Web Audio context can run at 44.1 kHz, not the 48 kHz assumed
267 // at construction). CONTROL THREAD ONLY — never call from process(): it
268 // re-runs Processor::prepare() and may resize the planar buffers, both of
269 // which allocate. In the worklet it arrives as a port message, which the
270 // audio thread services BETWEEN render quanta (never mid-block). The planar
271 // buffers are only resized when the block size actually grows, so a same-
272 // or-smaller block never allocates here.
273 void prepare(double sample_rate, int block_size);
274
275 // One-shot DSP-state reset. Processor has no reset() virtual; the reset
276 // contract is ProcessContext::reset_requested / should_reset_dsp_state().
277 // This sets an RT-safe flag (a single bool) that makes the NEXT process()
278 // raise ctx.reset_requested for exactly one block, then clears it — so a
279 // plugin like mpe-spreader drops its held-note channel map on that block
280 // and is normal again on the next. Safe to call from any thread; it only
281 // flips a bool.
282 void request_reset() noexcept { reset_pending_ = true; }
283
284 // Processor-reported delay-compensation latency in samples. Control thread.
285 int latency_samples() const;
286
287 // Host-supplied transport snapshot. Web Audio has no transport of its own,
288 // so a host that wants tempo/playhead-aware DSP pushes it here; the values
289 // are copied into ProcessContext at the top of each process(). POD copy,
290 // no allocation — safe from the control thread between render quanta.
291 void set_transport(bool is_playing, double bpm, double position_beats,
292 double position_samples, int tsig_num, int tsig_den) noexcept;
293
294 // Called each AudioWorkletProcessor.process() frame.
295 //
296 // PLANAR channel-pointer arrays — the SAME shape WCLAP and native CLAP use
297 // (clap_process_t audio buffers). `inputs`/`outputs` each point at an array
298 // of `num_channels` channel pointers, one contiguous float buffer per
299 // channel. Web Audio is already planar and the Pulp Processor is planar, so
300 // the processor renders straight from `inputs` into `outputs` with no
301 // interleave round-trip (the four transposes the interleaved single-pointer
302 // ABI used to do all cancelled, so removing them is behavior-preserving).
303 void process(const float* const* inputs, float* const* outputs,
304 int num_channels, int num_frames);
305
306 // Parameter access (WAMv2 uses string IDs)
307 std::vector<WamParamInfo> get_parameter_info() const;
308 // Parameter metadata as a JSON array for generated web controls:
309 // [{id,label,type,unit,defaultValue,minValue,maxValue,discreteStep}, ...]
310 std::string parameters_json() const;
311 float get_parameter_value(const std::string& id) const;
312 void set_parameter_value(const std::string& id, float value);
313
314 // MIDI events from WAMv2 event system
315 void schedule_midi(uint8_t status, uint8_t data1, uint8_t data2,
316 int sample_offset);
317
318 // Variable-length System Exclusive input (a full F0 .. F7 payload), which
319 // the 3-byte schedule_midi() cannot express. Copies into a pre-reserved
320 // payload pool; oversized or overflowing messages are dropped (and counted)
321 // rather than allocating. Returns false when the message was dropped.
322 bool schedule_sysex(const uint8_t* data, int size, int sample_offset);
323
324 // Drain the MIDI the processor produced during the LAST process() call.
325 //
326 // Copies up to `cap` bytes into `dst` and returns the number of bytes that
327 // were AVAILABLE — so a caller passing too small a buffer sees size > cap
328 // and knows the tail was truncated. Records are packed back-to-back:
329 //
330 // [int32 sample_offset][uint16 byte_len][byte_len raw MIDI bytes]
331 //
332 // Short messages carry 1..3 bytes; SysEx carries the full F0..F7 payload,
333 // so one drain call handles both and the reader dispatches on the first
334 // byte. Records are emitted short-messages-first, then SysEx; consumers
335 // that need strict time order should stable-sort on sample_offset.
336 //
337 // Called from the audio thread immediately after process(): it is a single
338 // memcpy out of a buffer serialized during process(), so it neither
339 // allocates nor locks.
340 int drain_midi_out(uint8_t* dst, int cap) const;
341
342 // A plugin can change its OWN parameters, and nothing told the host.
343 // synth-with-presets loads a factory preset into its timbre parameters when
344 // Program changes — inside process(), a block after the host wrote Program.
345 // A generated web UI therefore kept showing the previous knob values, since
346 // the web ABI is pull-only and the host had no reason to re-read.
347 //
348 // param_epoch() is bumped by an audio-thread StateStore listener on every
349 // value change (its own writes included). A host polls it once per block —
350 // one wasm call, no allocation, no lock — and re-reads values only when it
351 // moves.
352 uint32_t param_epoch() const;
353
354 // Bulk value read in all_params() declaration order, i.e. the same order
355 // wam_parameters() reports, so a host can zip the two. Avoids a
356 // string-keyed round trip per parameter. Writes min(count, capacity)
357 // floats and returns the total parameter count.
358 int read_param_values(float* dst, int capacity) const;
359
360 // State persistence
361 std::vector<uint8_t> get_state() const;
362 bool set_state(const uint8_t* data, size_t size);
363
364 // Descriptor
366 // The descriptor as a JSON string. Thin wrapper over descriptor().to_json()
367 // so the single-plugin and rack entry points call the SAME method name and
368 // the shared wam_* export layer needs no per-bridge special-casing.
369 std::string descriptor_json() const { return descriptor().to_json(); }
370
371private:
372 ProcessorFactory factory_;
373 // The store is declared before the Processor so it is destroyed after it.
374 // `Processor::state()` dereferences a pointer to this store, and a Processor
375 // may read it from its destructor or from a worker thread that destructor is
376 // about to join. Reversing these two lines hands that thread a freed store.
377 state::StateStore store_;
378 std::unique_ptr<Processor> processor_;
379
380 // Bumped by an audio-thread listener on every parameter change. Atomic
381 // because the counter is written on the audio thread and read from a
382 // wam_param_epoch() call that a host may make from either side.
383 std::atomic<uint32_t> param_epoch_{0};
384 state::ListenerToken param_listener_;
385
386 // No planar scratch: the processor renders directly from the host's planar
387 // input channel pointers into the host's planar output channel pointers
388 // (see process()), so there is nothing to de-interleave into.
389
390 // Bounds for the adapter-owned MIDI buffers. They are reserved once in
391 // initialize() and run with set_realtime_capacity_limit(true), so add()
392 // drops on overflow rather than reallocating on the audio thread.
393 static constexpr std::size_t kMidiEventCapacity = 256;
394 static constexpr std::size_t kSysexCapacity = 16;
395 static constexpr std::size_t kSysexPayloadCapacity = 512;
396
397 // Persistent across blocks — never reallocated during process().
398 // pending_midi_ is filled by schedule_midi() between render quanta and is
399 // handed to the processor as midi_in; midi_out_ receives what it emits.
400 midi::MidiBuffer pending_midi_;
401 midi::MidiBuffer midi_out_;
402
403 // Serialized midi_out_ records, written at the end of process() and copied
404 // out by drain_midi_out(). Sized once; `midi_out_bytes_` is the live length.
405 std::vector<uint8_t> midi_out_scratch_;
406 std::size_t midi_out_bytes_ = 0;
407
408 double sample_rate_ = 48000.0;
409 int num_channels_ = 2;
410 int block_size_ = 128;
411
412 // Set by request_reset(); consumed (and cleared) by the next process(), so
413 // exactly one block sees ctx.reset_requested = true. One bool: RT-safe.
414 bool reset_pending_ = false;
415
416 // Set by a control write (parameter / state restore), cleared by
417 // service_non_realtime() once per block. See WamStage::service_non_realtime
418 // for why the non-RT pass is coalesced instead of run inline per message.
419 bool tick_pending_ = false;
420 void service_non_realtime();
421
422 // Host-supplied transport, copied into ProcessContext every block. The same
423 // WamTransport POD the rack uses (was a byte-identical private twin) so
424 // set_transport() writes it through the one shared helper. Fields the web
425 // host cannot supply are left at ProcessContext's own defaults.
426 WamTransport transport_;
427};
428
429// ── WamChainBridge ──────────────────────────────────────────────────────
430//
431// N processors run as ONE rack inside ONE wasm module and ONE
432// AudioWorkletProcessor. Stage i's midi_out_ buffer is handed DIRECTLY (by
433// reference, no JS marshalling, no offset rewriting) to stage i+1 as its
434// midi_in, and stage i's audio output buffer becomes stage i+1's audio input —
435// all in the same wasm memory. This is why an in-worklet rack is not the same
436// as forwarding wam.onMidiOut into a second AudioWorkletNode's scheduleMidi():
437//
438// * that costs at least one render block of latency (the forward is serviced
439// on the next block), and
440// * it RE-RELATIVIZES sample offsets — an event at offset 37 of block N
441// arrives as offset 0 of block N+1 — and
442// * two AudioWorkletNodes do not share a wasm memory, so no C++ buffer can
443// pass between them.
444//
445// Here the whole rack advances within a single process() call: offsets are
446// preserved exactly and there is zero cross-thread hop between stages.
447//
448// The rack reuses the ENTIRE existing wam_* C ABI verbatim (no new export):
449// parameters are addressed "<stage>:<paramId>" (plain "6" still means stage 0),
450// state is a versioned "PWR1" container of per-stage "PWS1" blobs, and the
451// descriptor is the composite of the endpoints.
453public:
454 // Supplied by the rack entry TU. Returns the ordered stages of the rack.
455 using ChainFactory = std::vector<std::unique_ptr<Processor>> (*)();
456
457 explicit WamChainBridge(ChainFactory factory);
459
460 // Construct + prepare every stage. Off the audio thread.
461 bool initialize(double sample_rate, int max_block_size);
462
463 // Re-prepare every stage. CONTROL THREAD ONLY.
464 void prepare(double sample_rate, int block_size);
465
466 // One-shot DSP reset for the whole rack: the next process() raises
467 // ctx.reset_requested for every stage for exactly one block. RT-safe bool.
468 void request_reset() noexcept { reset_pending_ = true; }
469
470 // Sum of the stages' reported latencies (a host delay-compensates the rack
471 // as a unit). Control thread.
472 int latency_samples() const;
473
474 void set_transport(bool is_playing, double bpm, double position_beats,
475 double position_samples, int tsig_num, int tsig_den) noexcept;
476
477 // Run the whole rack for one block. See the class comment for the wiring.
478 // PLANAR channel-pointer arrays, same shape as WamProcessorBridge::process
479 // and native CLAP: stage 0 renders straight from `inputs`, the last stage's
480 // planar output is copied into `outputs` — no interleave round-trip.
481 void process(const float* const* inputs, float* const* outputs,
482 int num_channels, int num_frames);
483
484 // Every stage's parameters, stage-qualified. Each entry carries a "stage"
485 // field so a host can group them; ids are "<stage>:<paramId>".
486 std::string parameters_json() const;
487 // Accepts "<stage>:<paramId>" and, for stage 0, plain "<paramId>".
488 float get_parameter_value(const std::string& id) const;
489 void set_parameter_value(const std::string& id, float value);
490
491 // Sum of every stage's epoch: any stage rewriting a parameter moves it.
492 // See WamProcessorBridge::param_epoch for why a web host needs this.
493 uint32_t param_epoch() const;
494
495 // Every stage's values concatenated, in the order parameters_json() reports
496 // them (stage 0's parameters, then stage 1's, ...).
497 int read_param_values(float* dst, int capacity) const;
498
499 // Host MIDI enters STAGE 0 as its midi_in.
500 void schedule_midi(uint8_t status, uint8_t data1, uint8_t data2,
501 int sample_offset);
502 bool schedule_sysex(const uint8_t* data, int size, int sample_offset);
503
504 // Drain the LAST stage's MIDI output — same packed record layout as the
505 // single-plugin bridge. RT-safe memcpy out of the serialized scratch.
506 int drain_midi_out(uint8_t* dst, int cap) const;
507
508 // Rack state: "PWR1" [u32 stage_count] ( [u32 len][per-stage PWS1] )*.
509 // A legacy single-plugin blob (bare or "PWS1") loads into stage 0.
510 std::vector<uint8_t> get_state() const;
511 bool set_state(const uint8_t* data, size_t size);
512
513 // Composite descriptor JSON, incl. a "stages" name array.
514 std::string descriptor_json() const;
515
516private:
517 ChainFactory factory_;
518 // unique_ptr elements so a stage (which owns a StateStore and buffers whose
519 // pointers alias its own storage) never moves after initialize().
520 std::vector<std::unique_ptr<WamStage>> stages_;
521
522 // No host-facing input scratch: stage 0 renders directly from the host's
523 // planar input channel pointers (see process()).
524
525 // Inbound host MIDI → stage 0. Reserved once, RT-capacity-limited.
526 midi::MidiBuffer pending_midi_;
527
528 // Serialized last-stage MIDI output for drain_midi_out().
529 std::vector<uint8_t> midi_out_scratch_;
530 std::size_t midi_out_bytes_ = 0;
531
532 static constexpr std::size_t kMidiEventCapacity = 256;
533 static constexpr std::size_t kSysexCapacity = 16;
534 static constexpr std::size_t kSysexPayloadCapacity = 512;
535
536 double sample_rate_ = 48000.0;
537 int num_channels_ = 2; // host-facing bus width (stage 0 input width)
538 int block_size_ = 128;
539 bool reset_pending_ = false;
540 WamTransport transport_;
541};
542
543} // namespace pulp::format::wam
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
std::vector< std::unique_ptr< Processor > >(*)() ChainFactory
Definition wam_adapter.hpp:455
std::string parameters_json() const
void process(const float *const *inputs, float *const *outputs, int num_channels, int num_frames)
void request_reset() noexcept
Definition wam_adapter.hpp:468
std::vector< uint8_t > get_state() const
int drain_midi_out(uint8_t *dst, int cap) const
bool initialize(double sample_rate, int max_block_size)
void schedule_midi(uint8_t status, uint8_t data1, uint8_t data2, int sample_offset)
float get_parameter_value(const std::string &id) const
void set_transport(bool is_playing, double bpm, double position_beats, double position_samples, int tsig_num, int tsig_den) noexcept
std::string descriptor_json() const
WamChainBridge(ChainFactory factory)
bool schedule_sysex(const uint8_t *data, int size, int sample_offset)
void set_parameter_value(const std::string &id, float value)
int read_param_values(float *dst, int capacity) const
void prepare(double sample_rate, int block_size)
bool set_state(const uint8_t *data, size_t size)
int read_param_values(float *dst, int capacity) const
void schedule_midi(uint8_t status, uint8_t data1, uint8_t data2, int sample_offset)
float get_parameter_value(const std::string &id) const
std::vector< uint8_t > get_state() const
void set_parameter_value(const std::string &id, float value)
std::vector< WamParamInfo > get_parameter_info() const
bool set_state(const uint8_t *data, size_t size)
void set_transport(bool is_playing, double bpm, double position_beats, double position_samples, int tsig_num, int tsig_den) noexcept
WamDescriptorData descriptor() const
WamProcessorBridge(ProcessorFactory factory)
void process(const float *const *inputs, float *const *outputs, int num_channels, int num_frames)
bool schedule_sysex(const uint8_t *data, int size, int sample_offset)
bool initialize(double sample_rate, int max_block_size)
std::string descriptor_json() const
Definition wam_adapter.hpp:369
int drain_midi_out(uint8_t *dst, int cap) const
void request_reset() noexcept
Definition wam_adapter.hpp:282
void prepare(double sample_rate, int block_size)
int read_param_values(float *dst, int capacity) const
const midi::MidiBuffer & midi_out() const
Definition wam_adapter.hpp:158
audio::BufferView< const float > output_as_input(int num_frames) const
int latency_samples() const
Definition wam_adapter.hpp:222
bool set_state(const uint8_t *data, size_t size)
void run(const audio::BufferView< const float > &in_view, midi::MidiBuffer &midi_in, const ProcessContext &ctx, int num_frames)
const state::StateStore & store() const
Definition wam_adapter.hpp:170
void service_non_realtime()
Definition wam_adapter.hpp:195
bool ready() const
Definition wam_adapter.hpp:226
void copy_output_into(float *const *outputs, int host_channels, int num_frames) const
midi::MidiBuffer & midi_out()
Definition wam_adapter.hpp:157
uint32_t param_epoch() const
Definition wam_adapter.hpp:205
float get_param(state::ParamID id) const
Definition wam_adapter.hpp:169
void set_param(state::ParamID id, float value)
Definition wam_adapter.hpp:165
void prepare(double sample_rate, int block_size)
void mark_non_realtime_dirty()
Definition wam_adapter.hpp:174
int num_channels() const
Definition wam_adapter.hpp:225
bool initialize(std::unique_ptr< Processor > processor, double sample_rate, int max_block_size)
std::vector< uint8_t > get_state() const
PluginDescriptor plugin_descriptor() const
Definition wam_adapter.hpp:219
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
RAII handle for a registered parameter-change listener.
Definition listener_token.hpp:47
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
Definition wam_adapter.hpp:35
std::unique_ptr< Processor >(*)() ProcessorFactory
Factory function type — plugins provide this to create processor instances.
Definition processor.hpp:1416
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Plugin metadata — declared once, immutable.
Definition plugin_descriptor.hpp:245
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Definition wam_adapter.hpp:38
std::string name
Definition wam_adapter.hpp:39
std::vector< std::string > stages
Definition wam_adapter.hpp:69
bool has_audio_input
Definition wam_adapter.hpp:48
bool has_audio_output
Definition wam_adapter.hpp:49
bool is_instrument
Definition wam_adapter.hpp:47
bool has_midi_output
Definition wam_adapter.hpp:51
int latency_samples
Definition wam_adapter.hpp:62
bool has_midi_input
Definition wam_adapter.hpp:50
std::string api_version
Definition wam_adapter.hpp:42
bool has_mpe_input
Definition wam_adapter.hpp:54
std::string version
Definition wam_adapter.hpp:41
std::string vendor
Definition wam_adapter.hpp:40
bool has_mpe_output
Definition wam_adapter.hpp:55
std::vector< std::string > keywords
Definition wam_adapter.hpp:46
int tail_samples
Definition wam_adapter.hpp:63
bool has_automation_input
Definition wam_adapter.hpp:52
std::string description
Definition wam_adapter.hpp:43
std::string thumbnail
Definition wam_adapter.hpp:45
static WamDescriptorData from_processor(const PluginDescriptor &desc)
std::string website
Definition wam_adapter.hpp:44
bool has_automation_output
Definition wam_adapter.hpp:53
Definition wam_adapter.hpp:81
float max_value
Definition wam_adapter.hpp:87
std::vector< std::string > value_labels
Definition wam_adapter.hpp:95
float default_value
Definition wam_adapter.hpp:85
float min_value
Definition wam_adapter.hpp:86
float step
Definition wam_adapter.hpp:88
std::string id
Definition wam_adapter.hpp:82
int discrete_step
Definition wam_adapter.hpp:89
std::string type
Definition wam_adapter.hpp:84
std::string unit
Definition wam_adapter.hpp:90
std::string label
Definition wam_adapter.hpp:83
Definition wam_adapter.hpp:102
bool valid
Definition wam_adapter.hpp:103
double position_beats
Definition wam_adapter.hpp:106
bool is_playing
Definition wam_adapter.hpp:104
int64_t position_samples
Definition wam_adapter.hpp:107
int time_sig_denominator
Definition wam_adapter.hpp:109
double tempo_bpm
Definition wam_adapter.hpp:105
int time_sig_numerator
Definition wam_adapter.hpp:108