aax
無料Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.
日本語の概要は準備中です。原文の説明を表示しています。
LV2 format adapter for Pulp — the Turtle manifest the build emits by asking the module to describe itself, port indices as a saved-session wire format with one shared layout, host transport arriving as a time:Position atom on the MIDI port, the optional buf-size feature that is a hint and not a guarantee, state:interface versus control ports, and the real-time rules run() has to keep.
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
Use this when touching Pulp's LV2 adapter, when answering "what does a Pulp plugin look like to Ardour / Carla / Qtractor", or when an LV2 bundle behaves in a way the other adapters do not. LV2 is Pulp's Linux-native format and the only one whose host-facing contract is RDF text generated by the plugin rather than a C++ or C interface the compiler checks. That single difference is the source of most of what follows.
LV2 is experimental on Linux only in docs/status/support-matrix.yaml. Read
that file's formats: and format_limitations.lv2 before you promise anybody
anything — it is the canonical list of what the adapter does and does not do,
and this page deliberately does not restate it so the two cannot drift.
core/format/src/lv2_adapter.cpp or either LV2 header.state, time, buf-size, options, worker,
patch, ui, …) or changing a port..lv2 bundle.| Role | Path |
|---|---|
Turtle generation (generate_plugin_ttl, generate_manifest_ttl) | core/format/src/lv2_adapter.cpp |
| Instance struct, URID cache, port pointers | core/format/include/pulp/format/lv2_adapter.hpp |
instantiate / connect_port / run / cleanup + PULP_LV2_PLUGIN | core/format/include/pulp/format/lv2_entry.hpp |
Bundle build rule (_pulp_add_lv2) | tools/cmake/PulpPluginFormats.cmake |
| Scaffolded per-plugin entry | tools/templates/{gain,effect,instrument}/lv2_entry.cpp.template |
| Shared over-long-block clamp | core/format/include/pulp/format/max_block_contract.hpp |
Shared transport → ProcessContext mapper | core/format/include/pulp/format/adapter_boundary.hpp |
| Host side (Pulp loading an LV2 plugin) | core/host/src/plugin_slot_lv2.cpp, core/host/src/scanner.cpp — see the hosting skill |
| Tests | test/test_lv2_adapter.cpp, test/test_lv2_rt.cpp, test/test_lv2_host_discovery.cpp |
_pulp_add_lv2 emits manifest.ttl and <binary>.ttl into the bundle as a
POST_BUILD step. Before that existed, generate_plugin_ttl() and
generate_manifest_ttl() had no caller outside the test suite: the build
compiled the shared object into <name>.lv2/ and stopped, while the comment
above it claimed the directory held "the .so and .ttl files". A host discovers
plugins by reading manifest.ttl, so every bundle this tree produced was not a
plugin that loaded badly — it was not a plugin at all, silently.
The description comes from the module, not from CMake. The port layout is
the plugin descriptor's and the control ports are its parameters', so a build
script that emitted Turtle itself would be a second source of truth for a wire
format the host and connect_port() must agree on exactly. Instead the
PULP_LV2_PLUGIN macro exports pulp_lv2_write_bundle_ttl, and
tools/lv2-ttlgen dlopens the module that was just built and asks it. One
driver serves every target because it links nothing from the plugin.
Consequences worth knowing:
_pulp_add_lv2 then emits a loud CMake warning rather than a bundle that
looks finished. A bundle without a manifest is indistinguishable from a
working one until a host fails to list it.generate_manifest_ttl() points rdfs:seeAlso at <binary-stem>.ttl.
The description has to land on exactly that name, or a host reads the
manifest, follows the pointer and finds nothing — which looks to a user
exactly like a plugin that does not exist.lv2:minimumSize in the TTL"
while the generator emitted no such property for months, so hosts used their
own default. When the artifact is generated text, the only honest check is
one that reads the generated text.FORMATS declarations under examples/ and
none names it, so no ordinary build here exercises _pulp_add_lv2. That is
why the end-to-end coverage is a dedicated fixture
(test/native_components/lv2_ttl_fixture_plugin.cpp) that runs the same
POST_BUILD invocation and is read back through pulp::host's own discovery —
a test that called the generator directly would prove the generator works and
nothing about whether the build ever runs it.pulp create still offers LV2. On a non-macOS, non-Windows host the
default format list in experimental/pulp-rs/src/cmd/create_formats.rs
includes it and the scaffold drops in an lv2_entry.cpp.Lv2PortLayout (core/format/include/pulp/format/lv2_adapter.hpp) is the
single definition. generate_plugin_ttl() builds one to emit lv2:index N,
and connect_port() classifies a host-supplied index through the same type's
kind_of() / slot_of(). Do not re-derive the order anywhere else.
The order is: every audio input channel, every audio output channel, one
control port per parameter, the atom input port if accepts_midi, the atom
output port if produces_midi, then the latency output control port, always
last. It was genuinely written twice once, with nothing comparing them; the
failure that shape produces is the host handing a float* to a slot the
adapter reads as an LV2_Atom_Sequence — a type confusion that compiles, links
and loads. test_lv2_bundle_discovery_e2e.cpp is what now compares the emitted
indices against the reader, on a real built bundle.
kind_of() checks both ends of every range and answers None for a negative
or past-the-end index, so connect_port() cannot land outside an array;
instantiate() additionally refuses a descriptor wider than kMaxChannels
rather than dropping ports one at a time, because a dropped connection renders
as silence and reads as a DSP fault instead of a declaration fault.
Worse, the index is the host's saved wire format. Hosts store a session's
port connections by index, not by symbol. Inserting a port renumbers every port
after it, so a session saved against the old layout silently reconnects to the
wrong things. Adding a port to an already-shipped plugin is a compatibility
event, not a feature — which is why "just give every plugin an atom input port"
is not an available answer to the transport problem below. Treat that as a
decision rather than an oversight, and state it in format_limitations.lv2
rather than in a comment somewhere, so the next reader finds it.
One related sharp edge, and the reason the channel ceiling must be enforced at
admission rather than at connection. audio_in_ports / audio_out_ports
are float*[kMaxChannels] with kMaxChannels = 8. A descriptor whose buses sum
past eight per direction writes past the array — and the two arrays are adjacent
members, so index 8 and 9 land on audio_out_ports[0..1] and silently mis-wire
outputs rather than crashing. Casting the host's port number to int is the
second half of the same hazard: a port past INT_MAX narrows to a negative
index that an upper-bound check still accepts.
Do not assume run() is the safe half. It clamps its pointer-gathering
loops, then hands BufferView the unclamped channel count over that same
eight-pointer array — and BufferView::channel() does not bound-check, so the
Processor reads stack garbage as a float*.
Enforce the ceiling in instantiate(), before any allocation, and return
nullptr the way the missing-urid:map refusal already does. Clamping inside
connect_port() is the wrong shape: a silently dropped connection is a
declaration fault wearing a DSP fault's symptoms, and it leaves the run() half
unfixed.
LV2 has no transport API. A host publishes transport by writing a
time:Position object into an atom input port's sequence — the same
sequence that carries MIDI events. That has three consequences that surprise
people arriving from CLAP or VST3:
accepts_midi = false has no
atom input port, so there is nowhere for a time:Position to arrive. A
tempo-synced effect that takes no MIDI is blind under LV2 by construction.
See the port-renumbering paragraph above for why the obvious fix is not one.body.type is atom:Object — or
atom:Blank, which pre-1.8 hosts still send for the same thing. Match both
or you will decode nothing on an older host and see no error anywhere.time:Position is latched, not per-cycleNothing in the spec requires a host to send a Position every cycle, and real hosts send one only when something changes. So a decoder that reads the port and nothing else sees the same sample position on block after block — which downstream reads as the transport seeking back to the same spot every buffer, continuously resetting anything phase- or tempo-locked. The symptom is an LFO or arpeggiator that will not advance while the host plays normally.
Retain the last Position, advance it by the block length while speed is
non-zero, and let a freshly arrived Position overwrite the extrapolation
outright — that way a host which does send one per cycle never accumulates
drift, and one which does not keeps moving.
time:time:beat counts beats of time:beatUnit, not quarter notes. Pulp's
ProcessContext counts quarter notes. Scale by 4 / beatUnit or every
reading from a 6/8 host is wrong by a factor of two.time:framesPerSecond is the audio sample rate. It is not an SMPTE
frame rate, despite the name; there is nothing to map a FrameRate from.time:speed is a rate, not a boolean. Zero is stopped, one is normal
play, and anything else is a scrub or a varispeed.time:frame
and time:speed and nothing musical. Carry presence per field so a consumer
can tell "the host did not say" from "the host said 120". LV2's time
extension models no record-arm, no cycle range and no host clock at all, so
those stay unavailable rather than defaulted.Do the decode into LV2's own units first and project onto the shared
boundary::HostTransport second, the way the other adapters do — the mapper in
adapter_boundary.hpp owns bar derivation and the change flags, and a decoder
that computes those itself will disagree with every other format.
instantiate() receives a sample rate and no block size. The only way to learn
one is bufsz:maxBlockLength, delivered through the options feature.
Both of those are optional features, and that is the whole trap.
bufsz:boundedBlockLength is something a host may support; options is
something a host may provide. Reading a maxBlockLength therefore tells you
what a cooperative host intends, and promises nothing about what run() will
actually be handed. A host that supports neither may legitimately call run()
with any n_samples, and JACK-family period sizes are user-configurable at
runtime. A plugin that sizes scratch from that number and trusts it overruns
the first time somebody changes the period.
Follow the rule CLAP and VST3 follow: whatever ceiling prepare() was given,
run() must clamp n_samples to it and zero-fill the tail
[max, requested) on every output channel, so the host reads clean silence
instead of stale buffer contents. clamp_block_to_prepared_max() in
max_block_contract.hpp is the shared decision. Its header comment enumerates
the wired adapters and that list is itself stale — it omits LV2 even though
lv2_entry.hpp calls clamp_block_to_prepared_max(). Verify against the call
sites, not the comment; the comment is the thing that drifted.
Use the same discipline for the atom output buffer. The host allocates it; the
plugin's only influence is lv2:minimumSize in the TTL, and its actual
capacity arrives in atom.size on entry to run(). Read the capacity you were
given, and drop events that do not fit rather than writing past it.
Control ports are the host's state. It saves and restores them itself, and
it writes them back into the ports before the next run(). Nothing the plugin
does at save time can change that.
Everything else — sampler buffers, file references, any blob a Processor
returns from serialize_plugin_state() — has exactly one route home, and that
is state:interface. Without it a session reload restores the knobs and loses
the content, which reads to a user as "the plugin forgot my sample" rather than
as a missing feature.
Wiring it has two halves and both are load-bearing:
LV2_Descriptor::extension_data must return the interface for
LV2_STATE__interface.lv2:extensionData state:interface.A host checks the Turtle to decide whether to ask. Return the interface without declaring it and it is never called; declare it without returning it and the manifest advertises a capability the binary does not have. Given that nothing currently writes the Turtle at all (see above), the declaration half is the one that will bite.
Further notes that cost time to rediscover:
plugin_state_io envelope carries both the StateStore
payload and the plugin-owned payload. Under LV2 the control ports overwrite
the parameter half on the next run() regardless of what was restored, so a
restore is authoritative only for the plugin-owned part. Do not debug a
"parameter did not restore" report on this path without checking the port
value first.restore() runs in LV2's Instantiation threading class, so no run()
is in flight and a deserialize gets the non-concurrent context it documents.
This is a guarantee worth relying on and worth not breaking.state:mapPath and state:makePath are
unused, so an absolute path inside a saved payload does not survive the
bundle or the project moving to another machine.run() may not dorun() is the audio thread and nothing tells you otherwise. The adapter holds
the line with ScopedNoAlloc around the whole render — MIDI parse, process(),
MIDI serialise — plus ScopedFlushDenormals for the same reason every other
adapter has it. The MIDI scratch buffers are reserved once in instantiate()
with set_realtime_capacity_limit(true), so an overflowing block drops events
instead of growing a vector under the guard.
That means anything you add inside run() must be allocation-, lock- and
syscall-free, including indirectly. The two that catch people:
LV2_URID_Map::map() is not real-time safe. It is a host callback that
may take a lock and intern a string. Map every URI you will ever need once,
in instantiate(), and compare integers on the hot path. A URID of zero then
means "the host gave us no map", and every comparison against it fails
closed, which is the behaviour you want.instantiate() refuses without urid:map and returns nullptr so the
host reports a clear error, rather than instantiating something that silently
cannot decode an atom. Keep that shape for any feature you make mandatory.
lv2:requiredFeature does tell a conformant host not to load you without it,
but it is a line of Turtle — it protects you only as far as the host is
conformant and the manifest actually reached it, which is a second reason the
runtime check has to exist.Hosts that care about real-time behaviour (Ardour, Reaper) read
lv2:optionalFeature lv2:hardRTCapable out of the Turtle and treat its absence
as "not safe for the real-time thread". There is no other channel for that
claim — which makes it one more thing riding on a manifest that has to be
written first.
Parse the Turtle; do not grep it. The existing TTL tests are
ContainsSubstring assertions, which is why a property promised in two header
comments for months and never actually emitted went unnoticed: a substring test
can only fail on text it was told to look for. A substring also cannot tell you
that a triple landed on the right subject, that a port index is attached to the
port you meant, or that a URI resolves. Load the generated text with an RDF
parser (rdflib reads Turtle directly), then assert over triples: this subject
is an lv2:Plugin, it has N ports, the port with lv2:index 4 is a
lv2:ControlPort, every prefix used is declared. That catches the whole class
of "the generator emits plausible-looking text that means nothing".
If the host tools are available, lv2lint and sord_validate will grade a
real bundle far more harshly than any test here, and loading it in Ardour or
Carla is the only check that covers discovery, port wiring and session reload
end to end.
Two harness traps bit this work specifically; both are general, and both live with the rest of the CTest selection traps in docs/guides/test-lanes.md:
No tests ran, and exits 2. The
expensive part is what happens next: confirm_failure.sh reports that exit
as INCONCLUSIVE — the test already fails before any edit, so a perfectly
good test reads as one that does not cover its code, and the obvious response
is to go rewrite something that was never broken. Name new cases without
commas; escaping them (\,) recovers an existing one.ctest -N -R treats a literal ( in a name as a regex group, so a case
whose name contains run() is selected zero times until the parens are
escaped — and a selection that matches nothing still exits 0.Both are the same failure: a measurement aimed slightly wrong returns a clean, confident, empty answer. Pair every zero with a control on the same instrument that must return non-zero.
format_limitations.lv2 in docs/status/support-matrix.yaml is the canonical
list; read it rather than trusting a summary. The shape of what is missing, so
you know what you are looking at:
lv2:ui surface, so a plugin's view is
unreachable under LV2 and a host renders the generic control-port strip.patch: nor the pset: vocabulary is
implemented, so PresetManager is unreachable from an LV2 host.LV2_Worker is not wired, so there is no sanctioned way to do
non-real-time work on behalf of run().set_mpe_input) is wired on VST3 and AUv3.
The UMP sidecar (set_ump_input) is CLAP-only. Neither is wired here, so
expressive input degrades to plain MIDI 1.0 on this path.LatencyCompensatedBypass is wired into
CLAP, VST3, AU and AAX and not into LV2, so run() always calls process()
and a bypassed latent plugin is not delay-compensated the way it is
elsewhere.Each of those is a real gap rather than a hidden feature. If you find this page or the matrix disagreeing with the code, the code wins — fix the matrix in the same change and say so.
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Optional AAX support for Pulp, including developer-supplied Avid SDK setup, CMake enablement, DigiShell/AAX Validator workflows, and local AAX builds on macOS or Windows.
日本語の概要は準備中です。原文の説明を表示しています。
Configure, implement, and test Pulp's optional desktop Ableton Link tempo-sync adapter while preserving the developer-supplied SDK, licensing, realtime, latency-compensation, and no-install boundaries.
日本語の概要は準備中です。原文の説明を表示しています。
Maintain Pulp's installed design-time agent capability manifest and public-surface ledger. Use when adding, removing, renaming, or materially changing public audio, MIDI, signal, timebase, or sequence APIs; registering a new algorithm for generators; changing capability support or deprecation state; or repairing agent-capabilities freshness, schema, fingerprint, tombstone, or installed-SDK tests.
日本語の概要は準備中です。原文の説明を表示しています。
Android platform development for Pulp — NDK cross-compilation, Oboe audio, Dawn/Skia GPU rendering, JNI bridge, touch interaction, emulator workflows, and end-to-end smoke validation. Covers build, deploy, debug, and the gotchas discovered during bringup.
日本語の概要は準備中です。原文の説明を表示しています。
Optional ARA support for Pulp, including developer-supplied ARA SDK setup, CMake enablement, adapter companion APIs, validation, and ARA-aware plugin implementation guidance.
日本語の概要は準備中です。原文の説明を表示しています。
The measurement surface for ALL Pulp DSP and audio-pipeline work — read it BEFORE writing or gating DSP, not only when something already sounds wrong. Covers the C++ harness (signal generators, metrics, assertions, RenderScenario, contracts), the offline Audio Doctor (magnitude/frequency response, THD/THD+N, phase/group delay), and their Python sibling the Audio Quality Lab (tools/audio/quality-lab — null residual + alignment, LTAS log-spectral distance, spectral flux/centroid, HNR, Theil-Sen drift slope, Kaiser-sinc resampling, license-guarded corpus, regression-net ratchet). TRIGGER on AUTHORING work — "build/design an oscillator/filter/synth/effect", "add a DSP module", "what should the acceptance gate be", "how do I measure aliasing / anti-aliasing / alias floor", "null against a reference", "is this DSP correct", "choose a tolerance", "golden/regression corpus for audio", "measure drift or jitter", "A/B two renders" — AND on DEBUGGING work — "is there sound / no audio / I hear nothing", "does this filter/compressor/synth/delay produce the right signal", "prove the DSP / prove the contract", "measure the frequency response", "what's the THD / is it distorting", "what's the group delay / phase response / measured latency", "magnitude response curve", "render a test tone and assert", "audio regression", "64-frame works but 128 is silent", "sample-rate change pitch-shifted it", "describe what's in this buffer", "audio doctor", "compare before/after a DSP refactor". Reach for this BEFORE hand-rolling any FFT, null test, alias measurement, pitch tracker, or golden-render script — most of it already exists in one of the two lanes. Test/tool layer over HeadlessHost — deterministic, no audio device, no speakers. Off the realtime thread entirely.
日本語の概要は準備中です。原文の説明を表示しています。