Module Reference¶
Pulp is organized into independent subsystems under core/. Each is a separate CMake library (pulp::runtime, pulp::audio, etc.) that you link as needed.
platform¶
Platform detection and operating-system services that higher-level subsystems use without hard-coding macOS, Windows, Linux, iOS, or Web behavior.
Link: pulp::platform · Include prefix: <pulp/platform/...>
| Feature | Header | What It Does |
|---|---|---|
| Platform detection | detect.hpp |
Compile-time and runtime platform checks |
| Native handles | native_handle.hpp |
Typed OS window/device handles for adapters |
| Clipboard | clipboard.hpp |
Cross-platform text clipboard access where implemented |
| File dialogs | file_dialog.hpp |
Native open/save panels where implemented |
| Popup menus | popup_menu.hpp |
Native menu affordances for UI surfaces |
runtime¶
Core utilities — the foundation everything else builds on.
Link: pulp::runtime · Include prefix: <pulp/runtime/...>
SIMD — Portable vectorized math¶
Hardware-accelerated math via Google Highway. Dispatches to the best instruction set at runtime (SSE2, AVX2, NEON). Use for inner-loop DSP where every cycle counts.
#include <pulp/runtime/simd.hpp>
using namespace pulp::runtime;
float a[256], b[256], result[256];
simd_add(a, b, result, 256); // result[i] = a[i] + b[i]
simd_scale(a, 0.5f, result, 256); // result[i] = a[i] * 0.5
float peak = simd_reduce_max(a, 256);
XML — Parse and generate XML¶
Wraps pugixml (MIT). Parse from string or file, query with XPath, generate documents.
#include <pulp/runtime/xml.hpp>
XmlDocument doc;
doc.parse(R"(<plugin name="MyPlugin"><param>Gain</param></plugin>)");
auto name = doc.root_attribute("name"); // "MyPlugin"
auto params = doc.xpath_strings("//param"); // ["Gain"]
doc.save_file("settings.xml");
Streams — Unified byte I/O¶
One interface (pulp::runtime::Stream) for files, memory, pipes, TCP, and HTTP. See docs/reference/streams.md for the full API.
#include <pulp/runtime/stream.hpp>
#include <pulp/runtime/async_stream.hpp>
// Synchronous file I/O via the common interface.
FileStream f("preset.bin", FileStream::Mode::Read);
std::uint8_t buf[512]{};
auto r = f.read(buf, sizeof(buf)); // StreamResult{bytes, error}
// Wrap any Stream in AsyncStream for backpressure + cancellation.
AsyncStream async(std::make_unique<FileStream>("large.wav"));
async.on_data([](auto* data, auto n) { /* on worker thread */ });
async.start();
Budget Policy — Graceful degradation¶
pulp::runtime::evaluate_runtime_budget() gives background analysis, cache
refresh, validation helpers, and game-audio-style optional work a shared
run/defer/shed/bypass decision. Critical audio work always runs; interactive
work can defer to preserve reserve; background and opportunistic work can shed
or bypass when overload is active or budget is exhausted.
HTTP — Network requests¶
GET, POST, and file download via cpp-httplib (MIT). Use for license checks, cloud presets, update notifications.
#include <pulp/runtime/http.hpp>
auto response = http_get("https://api.example.com/presets");
if (response.ok())
process_presets(response.body);
http_download("https://example.com/ir.wav", "/tmp/impulse.wav");
Crypto — Hashing and encryption¶
SHA-256, MD5, AES-256-CBC via Mbed TLS (Apache 2.0). Use for license validation, integrity checks, secure preset storage.
#include <pulp/runtime/crypto.hpp>
auto hash = sha256_hex("my data"); // 64-char hex string
auto id = machine_id(); // Deterministic hardware fingerprint
auto encrypted = aes_encrypt(data, size, key32, iv16);
auto decrypted = aes_decrypt(encrypted->data(), encrypted->size(), key32, iv16);
Licensing — Plugin copy protection¶
Current license keys are v2 AES-256-GCM payloads validated with a 32-byte shared secret. Legacy v1 RSA-signed keys remain supported for migration and compatibility. Online activation returns the license key string that your plugin then validates locally.
#include <pulp/runtime/license.hpp>
LicenseValidator validator;
validator.set_shared_secret(shared_secret_32, 32);
auto status = validator.validate(license_key_string);
if (status == LicenseStatus::Valid) { /* unlocked */ }
if (status == LicenseStatus::Expired) { /* show renewal dialog */ }
auto info = validator.validate_and_parse(key);
// info->product_id, info->user_email, info->edition
// Legacy v1 keys can still be validated with the RSA public key.
validator.set_public_key(my_rsa_public_key_pem);
i18n — String translation¶
Load translations from .strings (Apple), .po (gettext), or .json files. Positional argument substitution with {0}, {1}.
#include <pulp/runtime/i18n.hpp>
LocalisedStrings::instance().load_json_file("lang/de.json");
auto text = tr("hello_user", {{"Max"}}); // "Hallo, Max!"
auto locale = LocalisedStrings::system_locale(); // "de"
Expression — Math evaluator¶
Parse and evaluate math expressions at runtime. Supports variables, 15+ functions, constants (pi, e). Use for user-defined formulas, automation curves.
#include <pulp/runtime/expression.hpp>
auto result = evaluate("sin(pi / 2)"); // 1.0
auto freq = evaluate("base * 2^(note/12)", {{"base", 440.0}, {"note", 7.0}});
ExpressionEvaluator eval;
eval.set("x", 0.5);
eval.evaluate("x * 100 + 10"); // 60.0
Other runtime utilities¶
| Feature | Header | Description |
|---|---|---|
| Analytics | analytics.hpp |
Thread-safe Analytics::instance().log_event("preset_load", {{"name", "Init"}}) |
| Base64 | base64.hpp |
base64_encode(data) / base64_decode(text) |
| BigInteger | big_integer.hpp |
Arbitrary-precision math for RSA — a.mod_pow(exp, modulus) |
| Child Process | child_process.hpp |
run_process("/usr/bin/auval", {"-a"}) with stdout capture |
| Dynamic Library | dynamic_library.hpp |
lib.open("plugin.dylib"); lib.find_symbol("entry") |
| Identity | identity.hpp |
Uuid::generate(), typed SessionId/ObjectId/RunId |
| IP Address | ip_address.hpp |
local_ipv4_address(), hostname(), is_valid_ipv4(addr) |
| IPC Lock | inter_process_lock.hpp |
Cross-process mutex via file locks |
| Memory Map | memory_mapped_file.hpp |
Zero-copy large file access via mmap |
| Named Pipes | named_pipe.hpp |
Cross-platform IPC (directional paired POSIX FIFOs / CreateNamedPipe) |
| Primes | primes.hpp |
is_prime(97), generate_prime(32), sieve_primes(1000) |
| Range | range.hpp |
Range<float>(0, 1).contains(0.5), intersection, union |
| Scope Guard | scope_guard.hpp |
PULP_ON_SCOPE_EXIT(file.close()) |
| Sockets | socket.hpp |
TCP/UDP plus credential-bearing OS-local streams; local endpoints require an owner-private parent and expose kernel peer credentials |
| System Info | system.hpp |
CPU model, core count, RAM, OS, SIMD features (runtime detected) |
| Temp File | temporary_file.hpp |
Auto-deleting temp file — TemporaryFile tmp(".wav") |
| Text Diff | text_diff.hpp |
Line-by-line diff with formatted +/- output |
| Timer | high_resolution_timer.hpp |
Sub-millisecond periodic callback on a dedicated thread |
| ZIP/GZIP | zip.hpp |
Compress/decompress data and archives (miniz) |
events¶
Event loop, timers, IPC, and process management.
Link: pulp::events · Include prefix: <pulp/events/...>
IPC — Inter-process communication¶
Length-prefixed messages over named pipes, TCP sockets, or OS-local sockets. Use
named pipes for existing worker protocols, TCP only where a network carrier is
actually intended, and LocalSocket when a local security boundary needs
kernel-observed peer credentials. Local endpoints require an absolute path in
an owner-owned 0700 directory with no extended ACL, refuse to replace an
existing filesystem object, are created 0600, and are removed with the owning
listener. macOS additionally exposes a peer audit-token process generation;
unsupported identity-verification platforms fail closed.
#include <pulp/events/interprocess_connection.hpp>
// Server side
InterprocessConnection server;
server.on_text_message = [](std::string_view msg) { handle(msg); };
server.create_server("my_pipe", IpcTransport::NamedPipe);
// Client side
InterprocessConnection client;
client.connect("my_pipe", IpcTransport::NamedPipe);
client.send_message("scan_plugin:/path/to/plugin.vst3");
LocalSocket does not make a peer authorized. It supplies carrier evidence
only. The capability-control verifier combines accepted-socket UID, GID, PID,
and macOS audit-token PID generation with the live process's validated code
signature, identifier, Team ID or per-artifact ad-hoc CDHash. The broker must
still exact-match that observation against launcher- or policy-owned expected
identity before minting a verified peer. Named-pipe and TCP peers cannot be
passed to that verifier. The installed pulp::inspect-control component owns
the resulting identity, registration, grant, typed admission, durable receipt,
and artifact state in ControlBroker. Installed ControlService and
ControlClient types expose the same typed protocol used by the canonical
macOS LocalSocket endpoint and ControlClientConnection. A
ControlClientTransport represents one authenticated connection and owns its
client lineage, including artifact reads. The optional
pulp-control-broker executable owns the per-user endpoint and currently
serves bounded health requests only; authority-bearing admission remains
fail-closed until signed launcher, host registration, consent, and execution
adapters are connected. Socket, singleton, and liveness state remain in the
temporary owner-private runtime directory. Durable receipts and artifacts live
separately under ~/.pulp/state/control-broker/v1; stopping or uninstalling the
service does not remove that state. The legacy Inspector session/server is not
exposed as a compatibility transport or second authority path.
Each service session must negotiate its own protocol version and mandatory
receipt support. The broker validates operation input JSON before admission and
successful output JSON before terminal completion. A started operation that
misses its response deadline remains durably running and occupies its active
quota slot until deferred completion settles it, even though the immediate
response reports unknown-needs-refresh.
The Phase 3b artifact store is a minimal lineage-bound persistence primitive, not the Phase 7 artifact lifecycle. Broker reads reauthorize the original grant and exact producer/receipt lineage, but only per-blob and per-chunk size limits exist; aggregate quota, retention collection, deletion audit, redaction, and generalized ACL policy are later work. Owner-private filesystem permissions exclude other OS users, not a malicious same-UID process, so they do not claim at-rest secrecy from every local process running as that user.
Child Process Pool — Crash-isolated workers¶
Launch child processes with IPC channels. If a child crashes (e.g., while loading a broken plugin), the host survives.
#include <pulp/events/child_process_manager.hpp>
ChildProcessManager pool;
auto* worker = pool.launch("/usr/bin/my-scanner", {"--plugin", path});
worker->send_message("scan");
worker->on_message = [](std::string_view result) { update_list(result); };
Other event utilities¶
| Feature | Header | Description |
|---|---|---|
| Action Broadcaster | coalesced_updater.hpp |
broadcaster.send_action("file_open") to all listeners |
| Async Updater | coalesced_updater.hpp |
Coalesce rapid cross-thread triggers into one callback |
| Event Loop | event_loop.hpp |
EventLoop loop; loop.dispatch([]{...}); loop.dispatch_after(100ms, []{...}) |
| Service Discovery | service_discovery.hpp |
ServicePublisher / ServiceBrowser use a platform default backend when available; use NetworkServiceDiscovery::install_backend() for custom or test backends |
| Timer | timer.hpp |
Timer timer(loop, 100ms, []{...}); timer.start(); — periodic or one-shot |
| Volume Detector | volume_detector.hpp |
Poll for USB drive mount/unmount events |
audio¶
Device I/O, file formats, channel layouts, and offline processing.
Link: pulp::audio · Include prefix: <pulp/audio/...>
Reading and writing audio files¶
The FormatRegistry handles codec dispatch. Pass a file path — it picks the right decoder from the extension.
#include <pulp/audio/format_registry.hpp>
// Read any supported format
auto data = FormatRegistry::instance().read("drums.flac");
// data->channels[0] = left channel floats, data->sample_rate = 44100
// Write to WAV
FormatRegistry::instance().write("output.wav", *data);
// Read just the metadata (no audio decode)
auto info = FormatRegistry::instance().read_info("song.mp3");
// info->duration_seconds, info->num_channels, info->sample_rate
For an already-resident RIFF/WAVE byte span, inspect_wav() and decode_wav()
in wav_decoder.hpp provide a no-file-I/O, no-exceptions path with caller-set
frame, channel, and decoded-byte ceilings. RF64, W64, and RIFX containers are
outside this byte-span API. Keep parsing and allocation off the audio callback.
Streaming write — large files without loading into memory¶
#include <pulp/audio/streaming_writer.hpp>
StreamingWriter writer;
writer.open("recording.wav", 48000, 2, 24); // 48kHz stereo 24-bit
while (recording) {
writer.write_frames(buffer, num_frames); // Write in chunks
}
writer.close(); // Finalizes WAV header
Audio device I/O¶
#include <pulp/audio/device.hpp>
auto system = create_audio_system(); // Platform-appropriate backend
auto devices = system->enumerate_devices();
DeviceConfig config;
config.sample_rate = 48000;
config.buffer_size = 256;
auto device = system->create_device(devices[0].id);
device->open(config);
if (auto timing = query_audio_io_timing(*device)) {
// Consume the graph's already-computed PDC total; this does not recompute it.
auto latency = make_latency_snapshot(
*timing, graph.latency_samples(), device->sample_rate());
if (latency && latency->output_scheduling_offset_frames) {
schedule_output_early_by(*latency->output_scheduling_offset_frames);
}
}
device->start([](const auto& input, auto& output, const auto& ctx) {
// Real-time audio callback — no allocation, no locks
process(input, output, ctx.buffer_size);
});
Device backends: CoreAudio (macOS), WASAPI (Windows), ALSA + JACK (Linux), Web Audio (browser)
AudioIoTiming reports present input/output latency, safety-offset, and I/O
buffer properties in device-rate sample frames, together with the exact sample rate, timestamp
provenance, confidence, and a calibration generation. LatencySnapshot is a
control-thread value that composes this with the graph's reported total latency
for input placement, monitoring, and output scheduling. Each directional result
is optional: an output-only route still supports output scheduling, while live
monitoring requires complete input and output properties. Composition refuses a
graph/device rate mismatch. CoreAudio publishes this contract today; other
desktop and mobile backends currently return no timing value rather than guessed
magnitudes.
Snapshot composition fails closed when timestamp domain/source is unspecified or confidence is unavailable; provenance-free values are not scheduling data.
For CoreAudio, a complete directional presentation path is device latency plus the directional safety offset plus one I/O buffer. Input placement and output scheduling each count that buffer once; live monitoring composes both paths and therefore counts it twice.
Audio file format support¶
| Format | Read | Write | Backend |
|---|---|---|---|
| AAC | macOS | ✓* | Read through ExtAudioFile on macOS; optional FDK AAC adds writing (pulp add fdk-aac --accept-license FDK-AAC) |
| AIFF / AIFF-C | ✓ | ✓ | Native (8/16/24/32-bit big-endian) |
| ALAC | macOS | ✓* | Read through ExtAudioFile on macOS; optional Apple ALAC adds writing (pulp add alac) |
| CAF | macOS | — | Read through ExtAudioFile on macOS |
| FLAC | ✓ | ✓* | dr_flac / libflac (pulp add libflac) |
| MP3 | ✓ | ✓* | dr_mp3 / LAME (pulp add lame --accept-license LGPL-2.0) |
| OGG Vorbis | ✓ | — | stb_vorbis |
| WAV | ✓ | ✓ | CHOC + bounded in-memory decoder + StreamingWriter |
*Write via optional pulp add packages. Those packages do not add portable
AAC/ALAC readers. Permissive (libflac, ALAC) packages install freely; copyleft
(LAME, fdk-aac) packages require --accept-license.
Sampler, looper, and analysis primitives¶
Reusable low-level pieces for building samplers, generated-audio freeze/loop workflows, waveform displays, and offline/background sample analysis. These are primitives, not a full sampler UI. Callback-safe operations are documented in rt_safety_contract.hpp; import/export, analysis, waveform thumbnail build, publication writes, and materialization stay off the audio callback.
unison_voice_stack.hpp adds the logical-note ownership layer above
InstrumentVoiceAllocator. It exclusively owns the allocator, preflights a
complete child stack, steals only complete oldest stacks, and requires exact
voice IDs when renderer tails finish recycled slots.
For a runnable integration of the asset, streaming, interpolation, starvation, and synthetic-heritage primitives, see the PulpSampler example. Start with the sampler playback chooser: the sequential source, shared page service, and resident publication path have different ownership and traversal contracts.
The Heritage Kit adds a character-processing and profile layer; it does not replace the existing sampler foundation. Keep these boundaries when composing the APIs:
| Existing surface | Relationship to Sample Heritage | Consolidation guidance |
|---|---|---|
SampleAsset, resident publication, stream service, and voice readers |
Own source storage, generations, page demand, and logical forward/reverse traversal | Reuse them unchanged; feed each voice's ordered samples into its prepared Heritage voice engine |
LoopPlaybackCursor, LoopReader, and LoopRenderer |
Own one-shot/loop regions, wraps, reverse traversal, and loop crossfades | They do not perform Heritage cyclic resynthesis. Resolve traversal first, then run live cyclic stretch in the per-voice character chain |
sample_interpolation.hpp and sinc kernels |
Reconstruct samples at playback-rate positions and protect high-rate playback | Heritage converter/hold blocks model the variable machine frame; Heritage reconstruction is the fixed per-voice frame after return conversion. They are complementary, not alternate names for playback interpolation |
sample_asset_io.hpp and edit/bounce metadata |
Import, export, and describe audio assets | Heritage JSON imports/exports profiles; record commit emits its own content-addressed audio/provenance envelope. Do not merge these formats |
| Onset, slice, key/tempo, transient, and loop-point analyzers | Derive content metadata and suggested regions | Analysis can inform authoring or zone selection but does not select or mutate Heritage blocks at runtime |
signal::OfflineStretch and realtime pitch/time processors |
Conventional high-quality tempo/pitch processing | Use these when transparency or ordinary tempo matching is the goal. Use Heritage fixed/adaptive cyclic stretch only when cyclic resynthesis is the intended character |
In short: consolidate on the existing storage, traversal, interpolation, and analysis primitives; let Heritage own only its typed profile, per-voice/bus character path, live cyclic stage, and offline record-commit transaction.
| Feature | Headers | Description |
|---|---|---|
| Stream handoff and rolling capture | audio_stream_handoff.hpp, planar_audio_ring_buffer.hpp, rolling_audio_capture_buffer.hpp, realtime_sample_recorder.hpp |
Bridge generated/live/model audio into host-paced processing, keep bounded rolling history, freeze stable windows, and materialize captures off the audio thread |
| Resident publication and shared paged storage | published_sample_store.hpp, sample_slot_bank.hpp, sample_slot_materializer.hpp, sample_pool.hpp, sample_asset.hpp, sample_stream_window.hpp, sample_stream_scheduler.hpp, sample_stream_service.hpp, sample_stream_async_service.hpp, sample_stream_decode_pool.hpp, sample_memory_governor.hpp, sample_preload_contract.hpp, sample_stream_voice_reader.hpp, sample_stream_loop_voice_reader.hpp |
Publish resident generations or build immutable assets over a shared page cache; bounded commands, fixed-scratch decode, shared memory budgeting, and narrow or loop-aware voice readers keep the two ownership models explicit |
| Sample interpolation | sample_interpolation.hpp, sample_sinc_kernel.hpp |
Share hold, nearest, linear, Hermite, Lagrange, and prepared ratio-tracking sinc footprints across resident and paged playback; build normalized immutable cutoff tables off the callback |
| Sampler octave mips | sample_mip_builder.hpp, sample_mip_sidecar.hpp |
Build resident or persisted octave levels with the sampler decimator; authenticate source and payload identities, then publish sidecar manifests transactionally for strict runtime admission |
| Sample Heritage profiles and processing | sample_heritage.hpp, sample_heritage_schema.hpp, sample_heritage_engine.hpp, sample_heritage_pitch.hpp, sample_heritage_live_cyclic.hpp, sample_heritage_bus_dsp.hpp, sample_heritage_record_commit.hpp, sample_heritage_src.hpp, sample_heritage_json.hpp, sample_heritage_runtime_state.hpp |
Define strict schema-v3 voice/bus/record-commit profiles; prepare per-voice variable machine-frame character and fixed-frame reconstruction/color, pitch families, converter/hold, live cyclic stretch, post-mix bus color, deterministic state, neutral offline cycle estimation, and content-addressed offline commits without claiming emulation of named hardware |
| Sequential streaming source | streaming_sample_source.hpp, streaming_sample_source_file.hpp |
Play a preload head plus background-filled SPSC tail; WAV and uncompressed AIFF use immutable private mapped snapshots for ranged reads while fallback codec capability remains explicit |
| Stream starvation envelope | sample_starvation_envelope.hpp |
Supply equal-power fade gains for valid low-water and recovered frames with explicit predicted, insufficient-lead, and emergency telemetry; source-position advancement remains voice-renderer policy |
| Looping and playback | loop_types.hpp, loop_playback_cursor.hpp, sample_interpolation.hpp, loop_reader.hpp, loop_renderer.hpp, loop_point_analyzer.hpp, sample_voice_renderer.hpp, voice_sum_mixer.hpp |
Share LoopRegion traversal, cursor plans, prepared interpolation footprints, and tap mapping across resident and paged storage; LoopRenderer is the resident rich-loop orchestrator, while SampleVoiceRenderer remains a compatible pool/envelope/fade adapter |
| Mapping and instrument policy | sample_zone_map.hpp, sample_key_map.hpp, instrument_runtime.hpp, instrument_voice_allocator.hpp, instrument_envelope.hpp, voice_modulation_buffer.hpp |
Represent key/velocity zones, chromatic/fixed-pitch/slice mappings, pool-backed trigger resolution, voice allocation, AHDSR envelopes, and per-voice modulation lanes without requiring products to adopt generic allocator or envelope policy |
| Editing, import/export, and bounce metadata | sample_edit_document.hpp, sample_asset_io.hpp, wav_metadata.hpp |
Track non-destructive edit intent, import/export policy, drop classification, and WAV metadata/interchange outside realtime paths |
| Onset, slice, key/tempo, and transient analysis | onset_detector.hpp, slice_point_analyzer.hpp, slice_map.hpp, sample_key_map.hpp, analyzer_provider.hpp, built_in_key_tempo_analyzer.hpp, built_in_transient_classifier.hpp |
Provide package-free fallback analysis, timeline or strongest-confidence slice selection, near-zero or sign-transition snapping, shared note mapping, and neutral provider/provenance metadata |
| Time/pitch extension point | analyzer_provider.hpp, signalsmith_time_pitch_processor.hpp |
Optional package-backed time-stretch/pitch-shift processor contract; availability and licensing stay explicit |
| Waveform summaries and render backends | waveform_overview.hpp, waveform_gpu_primitives.hpp, waveform_gpu_render_controller.hpp, waveform_headless_render_backend.hpp |
Build/cache serialized CPU waveform summaries, plan generation-keyed static layer uploads, exercise backend resource lifecycle in CPU/headless paths, and keep future GPU-assisted analysis/rendering off live audio-thread waits |
| Realtime contract labels | rt_safety_contract.hpp |
Machine-checkable sampler/looper RT-safety labels for representative hot paths and off-thread helpers |
Other audio features¶
| Feature | Header | Description |
|---|---|---|
| Buffering Reader | buffering_reader.hpp |
Ring buffer with background read thread for streaming |
| Channel Sets | channel_set.hpp |
ChannelSet::surround_5_1(), mono through 7.1.4 Atmos |
| Load Measurer | load_measurer.hpp |
Track CPU usage of your audio callback; evaluate_audio_runtime_overload() classifies process-load/xrun telemetry into nominal, watch, overloaded, or critical validation states with explicit shed/bypass guidance |
| Memory-Mapped Reader | mmap_reader.hpp |
Zero-copy access for large sample libraries |
| Offline Processor | offline_processor.hpp |
offline_process(input, callback, 512) for simple batch render; offline_render(input, callback, options) for deterministic block schedules, absolute sample positions, transport timeline, state generation, render-speed hints, render seeds, and explicit tail policy; offline_render_stems() extracts named channel groups; compare_offline_render_audio() reports golden/null residuals; create_offline_render_manifest() records artifact hashes, render-plan hashes, chunk boundaries, staged resource hashes, and cache-reuse metadata for reproducible offline/distributed renders; evaluate_offline_render_compute_policy() keeps GPU-assisted analysis out of live audio-thread scopes and makes CPU fallback explicit |
| Subsection Reader | subsection_reader.hpp |
Read frame range without copying — reader.sample(ch, frame) |
| System Volume | system_volume.hpp |
get_system_volume() / set_system_volume(0.8f) |
Offline render manifests intentionally separate artifact identity from render
plan identity. Equivalent renders with different chunk schedules can have the
same audio_sha256 and a zero residual while still carrying different
render_plan_sha256 values and chunk metadata for distributed reproduction.
music¶
Dependency-light 12-tone equal-temperament theory values for sharing musical intent across audio, MIDI, timeline, and product code. The module does not own a clock, sequencer, transform chain, event ledger, or tuning system.
Link: pulp::music · Include prefix: <pulp/music/...>
PitchClassSet is an arbitrary checked 12-bit set. Scale adds a root and
supports degree lookup, signed octave-spanning degrees, transposition, and mode
rotation. NamedScale retains the existing ten Pulp signal selector values and
appends the scale set currently needed by Forge. The explicit
kPulpSignalScales, kForgeRuntimeScales, and kForgePrimitiveScales tables
carry each existing stored index and spelling; consumers should map through the
matching table rather than cast between product enums.
Pulp's existing signal harmonizer scale table and Timeline chord/scale wire codec delegate through these compatibility maps. Their public enum ordinals and stored names remain unchanged while the interval and identity data has one owner.
ChordFormula accepts fixed-capacity ascending semitone formulas. Its
with_extension(), with_suspension(), and with_alteration() transforms add
the common ninth/eleventh/thirteenth vocabulary without allocating or changing
the stored identities of named formulas. kPulpTimelineChordQualities and
kForgeChordQualities map the two existing stored identities onto the shared
named qualities. Chord::construct() builds bounded MIDI pitches and
deterministic inversions, failing when a root, inversion, formula, or resulting
pitch is outside its legal domain.
Pitch spelling is policy-driven: callers select prefer_sharps,
prefer_flats, or deterministic minimize_accidentals. spell_chord() keeps
the formula's diatonic letter roles, so a C-sharp major third is E-sharp while
the same pitch-class root under the flat policy is spelled D-flat/F/A-flat.
minimize_accidentals chooses a natural spelling when one exists and resolves
equal-cost single-accidental ties toward sharps; it is deterministic rather
than key-signature contextual.
voice_chord() applies closed, open, drop-2, drop-3, or spread constraints and
fits the result into an explicit MIDI range. It returns no value when the
spacing or range is impossible. minimum_motion_voice_leading() searches the
bounded MIDI domain across every assignment of the formula's fixed tone multiset,
including compound and duplicated pitch classes, for the global minimum summed
motion without voice crossing. Equal-cost answers use ascending pitch order as
the stable tie-break.
diatonic_chord() constructs scale-degree third stacks directly from an
arbitrary Scale. recognize_chord() ranks every stable named quality and root
by missing and extra pitch classes. best_equivalent_count() and ambiguous()
make symmetric or otherwise tied analyses explicit instead of selecting one
silently; MIDI-note input additionally reports a recognized inversion when the
bass identifies exactly one formula degree. inversion_match_count and
inversion_match_mask expose duplicate-degree matches; inversion remains
empty when a pitch-class bass cannot distinguish them. This inversion evidence
participates in ranking and best-equivalence grouping. The candidate catalog is
deliberately the 12 stable named qualities. Extended or altered input is ranked
against that catalog by its missing and extra tones; recognition does not invent
an extension identity.
The music APIs are pure and deterministic and own no mutable processing state,
so they have no prepare() or reset() lifecycle. Pitch-class and scale
operations, formula transforms, chord construction and spelling, and diatonic
construction are fixed-capacity, allocation-free, bounded value operations that
may be used on a real-time path. voice_chord(),
minimum_motion_voice_leading(), and recognize_chord() are control/offline
algorithms, not audio-callback operations: they perform bounded searches or
candidate ranking and use comparatively large temporary result/work tables.
#include <pulp/music/music.hpp>
using namespace pulp::music;
const auto scale = Scale::named(PitchClass::d, NamedScale::dorian);
const auto base = ChordFormula::for_quality(ChordQuality::minor7);
const auto formula = base->with_extension(ChordExtension::ninth);
const auto first_inversion = Chord::construct(62, *formula, 1);
const auto spelling = spell_chord(*first_inversion, AccidentalPolicy::prefer_flats);
VoicingConstraints constraints;
constraints.mode = VoicingMode::drop2;
constraints.range = {48, 84};
const auto voiced = voice_chord(62, *formula, constraints);
const auto analyses = recognize_chord(first_inversion->pitch_classes());
The named collection is a 12-TET compatibility vocabulary, not a claim of microtonal support. More tuning systems belong in the provider-neutral MIDI tuning APIs rather than in this representation.
Generative pattern kernels¶
BinaryPattern<MaxSteps> defaults to a 64-step capacity and reports overflow
instead of truncating. The following operations are bounded, constexpr, and
allocation-free during evaluation:
euclidean_pattern()returns a deterministic onset-first canonical rotation, accepts a signed rotation, and rejects zero steps, excess pulses, and capacity overflow. Positive rotation delays onsets and negative rotation advances them; for example, E(3,8) changes from10010010to01001001at rotation+1and00100101at rotation-1. A silent zero-pulse pattern is valid.EuclideanPatternRecipegives integrations a versioned named-field contract for steps, pulses, and rotation without treating the C++ object bytes as a wire format. Conventional E-notation orders arguments(pulses, steps).PatternWalkersupports forward, reverse, ping-pong, and random traversal.reset()restores index zero for forward/ping-pong and the final index for reverse. Random traversal consumes a caller-supplied deterministic random word and has no internal stream to rewind; callingnext()without a word in random mode fails explicitly.PreparedMarkovModel<MaxStates>defaults to 16 states.prepare()consumes a row-major table of unsigned integer weights on the control thread and rejects empty, oversized, malformed, or zero-total rows. Its fixed state capacity and 32-bit weights make the 64-bit cumulative row total non-overflowing.next()is a const bounded lookup from a caller-supplied random word, with no allocation or failure except an invalid state.cellular_evolve()applies an elementary 8-bit cellular rule with explicit wrapping or fixed-off edges.looping_shift_register()rotates the last bit to the first and optionally flips the copied bit using an integer numerator/denominator mutation chance. Mutation consumes a caller-supplied random word. Empty input and invalid probabilities fail without changing the input.derive_rhythm_relationship()creates one lane from another using coincident, complementary, or independent candidates. Wrap and proportional length mapping, signed target-grid phase, source-collision filtering, and exact-onset density are explicit policies. Exact-density selection is a pure coordinate-keyed decision over seed, cycle, lane, and step, so evaluation order cannot change the result.
Random words are mapped to bounded choices with full-domain multiply-high reduction rather than remainder reduction.
Construction and transformation results carry explicit errors rather than silently truncating or repairing invalid input. None of these APIs owns mutable randomness, transport, event ordering, a transform chain, or a callback accumulator. APIs that consume random words take them from the caller; relationship density instead names its complete stateless draw coordinate, so callback partition never enters these kernels.
The theory surface does not provide pitch spelling, chord recognition, voicing
constraints, or minimum-motion voice leading; those are not implied
capabilities of ChordFormula.
midi¶
MIDI I/O, file handling, MIDI 2.0 support, and provider-neutral note tuning.
Link: pulp::midi · Include prefix: <pulp/midi/...>
MIDI message sequence — editing and offline processing¶
#include <pulp/midi/midi_message_sequence.hpp>
MidiMessageSequence seq;
seq.add_note_on(0.0, 0, 60, 100); // C4 at time 0
seq.add_note_off(0.5, 0, 60); // Release after 0.5s
seq.add_cc(0.25, 0, 1, 64); // Modulation at 0.25s
auto events = seq.events_in_range(0.0, 1.0); // All events in first second
auto off = seq.find_note_off(0); // Find matching note-off
MIDI CI — Device discovery and profiles¶
#include <pulp/midi/midi_ci.hpp>
CiDiscovery ci;
ci.on_device_discovered = [](const CiDeviceInfo& device) {
log("Found: MUID=" + std::to_string(device.muid.value));
};
auto inquiry = ci.create_discovery_inquiry();
send_sysex(inquiry); // Send over MIDI port
Other MIDI features¶
| Feature | Header | Description |
|---|---|---|
| Buffer | midi_buffer.hpp |
Timestamped event buffer for process() callbacks |
| Device I/O | platform/ | CoreMIDI (macOS), WinMIDI (Windows), ALSA (Linux); Web MIDI scaffold is not wired into the shipped WASM build |
| Files | midi_file.hpp |
Read/write Standard MIDI Files |
| Messages | via CHOC | ShortMessage::noteOn(0, 60, 100) |
| Tuning | tuning.hpp, mts_esp_tuning.hpp, scala_tuning.hpp |
Provider-neutral note-to-frequency API with 12-TET default, optional MTS-ESP session/SysEx provider, and optional Scala SCL/KBM local-file provider |
| UMP | ump.hpp |
MIDI 2.0 Universal MIDI Packets, MPE zones |
| MPE | mpe_voice_tracker.hpp, mpe_buffer.hpp, mpe_synth_voice.hpp |
Per-note pitch bend / pressure / timbre tracking, opt-in sidecar buffer, and voice/allocator helpers. See docs/guides/mpe.md |
| Utility kernels | utility_kernels.hpp umbrella; routing_utility_kernels.hpp, note_utility_kernels.hpp, controller_utility_kernels.hpp |
Fixed-capacity channel routing, note-range filtering, keyboard splitting, balanced note-length shaping, low/high/last monophonic priority with legato/glide state, CC mapping/smoothing, and scale-aware MPE bend/glide |
Every utility kernel publishes a MidiUtilityContract describing maximum event
amplification, fixed state capacity, overflow behavior, same-sample ordering,
and transport requirements. Stateful note kernels retain release debt when an
output buffer fills: stop, seek, loop, reset, and spec replacement call
flush()/reset() until complete is true, so capacity pressure cannot strand
a downstream note. Routing-kernel flush() and output-bearing replace_spec()
close downstream notes while retaining enough input ownership to consume their
later physical releases; reset() closes the notes and then discards that input
ownership for a lifecycle boundary that also resets the source stream. Scheduling
accepts timebase::SamplePosition; the kernels do not own or advance another
clock. Realtime calls require output buffers that
were reserved for the contract's worst case and pinned with
set_realtime_capacity_limit(true); an unpinned output is rejected instead of
silently allocating in add() or stable sort(). Inputs and outputs must be
distinct buffers (as must both split outputs), including their attached UMP
storage; aliased calls are rejected before any block is cleared.
Channel routing, note-range filtering, and keyboard splitting apply the same
channel/note decisions to native MIDI 1.0 and MIDI 2.0 UMP channel-voice
packets. Note-addressed expression (poly pressure plus MIDI 2 per-note
controllers, bend, and management) follows the addressed note through a range
or split. A split duplicates channel-wide voice messages onto both configured
output channels. Other UMP message types and SysEx retain their exact payloads.
audio/midi_voice_modulation_adapter.hpp projects a caller-selected voice slot's
note/MPE state into the existing VoiceModulationBuffer. Voice ownership stays
with the instrument's allocator, preserving the MIDI-to-audio dependency
direction and avoiding a second voice-allocation policy. Expression and release
updates must carry a nonzero note_id: event releases match channel, note, and
generation, while index-based releases match the generation explicitly. A stale
generation therefore cannot mutate or release a reused voice slot. The tracker
uses the 64-bit MpeNoteGeneration type, never recycles a generation across
reset(), and fails closed after issuing its final nonzero value: further
note-ons are consumed but create no voice or callback. Check
note_generation_exhausted() and refused_note_on_count() to surface that
terminal condition. flush() and
reset() are the deliberate identity-free lifecycle clears. Destinations are
preflighted for four lanes and the complete frame capacity before any lane is
written.
MIDI effects¶
Pulp's format layer hosts MIDI-only processors. The SDK supplies the bounded utility kernels above, while Forge supplies a bounded, hot-swappable ordered transform chain with 20 transforms, fixed host macros, pattern/chord data, note-balance enforcement, and realtime-safe publication. See the MIDI FX guide for the complete transform parameter reference, structured authoring schema, cookbook, C++ method API, and host-validation contract.
signal¶
Real-time-safe DSP processors, generators, analysis helpers, and composition
primitives. Process methods operate on single samples or buffers and are safe
for the audio thread after the helper's documented
construction/configuration/prepare() step. Setup methods that allocate
storage must run off the audio thread.
Link: pulp::signal · Include prefix: <pulp/signal/...>
For synthesized percussion, including the complete voice API, recipes, provenance, and Forge bake-layer controls, see Percussion synthesis.
Using a processor¶
Every processor follows the same pattern: configure, set sample rate, process.
#include <pulp/signal/compressor.hpp>
Compressor comp;
comp.set_params({.threshold_db = -20, .ratio = 4, .attack_ms = 5, .release_ms = 100});
comp.set_sample_rate(48000);
for (int i = 0; i < num_samples; ++i)
buffer[i] = comp.process(buffer[i]);
Advanced DSP guides¶
The advanced processors are documented by authoring responsibility. Each guide includes selection advice, lifecycle and real-time boundaries, and a focused composition example. The advanced DSP API is the exhaustive public-method inventory across these families.
| Guide | Algorithms covered |
|---|---|
| Dynamics processors | Feedforward, VCA, diode-bridge, and FET compressors |
| Nonlinear and tone processors | Saturation, circuit clippers, fuzz, tape, and speaker modeling |
| Modulation effects | Phaser, three vibrato mechanisms, chorus, flanger, SSB shifting, rotary, and scanner |
| Pitch, time, and granular | Pitch shifting, YIN tracking, harmony, cyclic stretch, and granular clouds |
| Synthesis and sequencing | Additive synthesis, vocoding, stage/grid/rungler sequencing, scale quantization, and gate utilities |
| Nonlinear space and convolution | Gated/reverse ambience and zero-latency multilevel convolution |
Applying a mono processor to stereo¶
#include <pulp/signal/processor_duplicator.hpp>
ProcessorDuplicator<Compressor> stereo_comp;
stereo_comp.prepare(2, 48000);
stereo_comp.for_each([](Compressor& c) { c.set_params({...}); });
stereo_comp.process(channels, 2, num_samples);
Dry/wet mixing with latency compensation¶
#include <pulp/signal/dry_wet_mixer.hpp>
DryWetMixer mixer;
mixer.set_mix(0.7f); // 70% wet
mixer.set_curve(MixCurve::EqualPower); // Constant-power crossfade
mixer.set_wet_latency(512); // Compensate 512 samples of plugin latency
mixer.prepare(2, 1024);
mixer.push_dry(input_channels, 2, num_frames);
// ... run your effect on the wet path ...
mixer.mix_wet(output_channels, 2, num_frames);
Routing and parallel-path alignment¶
The signal module owns shared routing math so fixed-topology processors and runtime graphs use the same gain and latency contracts:
| Primitive | Header | Contract |
|---|---|---|
| Orthonormal mid/side | mid_side.hpp |
Self-inverse, mono-compatible, energy-preserving stereo transform plus mono-safe width |
| Signed audio matrix | audio_matrix_mixer.hpp |
Fixed 16×16 default capacity, sample-continuous cell automation, explicit raw or peak-normalized headroom |
| N-way crossfade | nway_crossfade.hpp |
Adjacent-path cosine/sine weights with unit summed power |
| Click-free path switcher | path_switcher.hpp |
Fixed-capacity, retargetable smoothstep transitions with block-split deterministic weights |
| Path latency aligner | path_latency_aligner.hpp |
Prepared N-path/channel delay storage with maximum-path latency reporting and impulse-exact alignment |
All process paths are allocation-free after preparation. The matrix copies its inputs to bounded scratch and therefore supports input/output aliasing. The path switcher requires disjoint mono source/destination buffers. The latency aligner supports only exact corresponding in-place pairs, not partial or cross-path aliases. Overlap checks cover the full processed byte ranges. Latency-set changes clear alignment history; matrix and switch automation preserve their sample trajectory across host block boundaries.
Convolution — load an impulse response¶
PartitionedConvolver is partitioned for one fixed block size, and process()
must be handed exactly that many samples. load_ir() rounds its block_size
argument up to the next power of two (its FFT is radix-2), so the size you
must feed is conv.block_size() — not necessarily the value you passed in.
#include <pulp/signal/convolver.hpp>
PartitionedConvolver conv;
conv.load_ir(impulse_response.data(), ir_length, block_size); // may round up
// In your process callback — num_samples MUST equal conv.block_size():
conv.process(input, output, conv.block_size());
If your host delivers blocks of any other size (variable blocks, or a size that
is not a power of two), re-block the audio into conv.block_size() chunks
yourself and report the added delay from Processor::latency_samples().
A loaded convolver handed the wrong block size fails closed: it emits
silence and increments conv.block_size_violations(). It deliberately does not
pass the input through, because a pass-through is audibly indistinguishable from
a working convolution and would hide the bug. Assert
conv.block_size_violations() == 0 in your tests.
Available processors¶
Filters¶
| Processor | Header | Description |
|---|---|---|
| Biquad | biquad.hpp |
Second-order IIR filter — low/high/band-pass, notch, shelf, peaking EQ |
| Six-band EQ | six_band_eq.hpp |
Allocation-free low-shelf/four-peak/high-shelf cascade with optional stable cascade crossfades and endpoint response inspection |
| SOS Cascade | sos_cascade.hpp |
Fixed-capacity transactional runtime executor for stable normalized biquad cascades |
| Filter Design | filter_design.hpp |
Generate Butterworth and Chebyshev coefficient sets for arbitrary order |
| FIR | fir_filter.hpp |
Finite impulse response filter with arbitrary tap count for linear-phase EQ |
| Analog VCF | analog_vcf.hpp / ota_cascade_filter.hpp |
Four measured Juno, Jupiter-8, Prophet-5, and Minimoog panel voicings over a shared zero-delay nonlinear four-pole cascade |
| Ladder | ladder_filter.hpp |
Four-pole nonlinear resonant ladder filter with saturation |
| Linkwitz-Riley | linkwitz_riley.hpp |
Phase-aligned crossover filter for splitting audio into frequency bands |
| State Variable (TPT) | svf.hpp / tpt_filter.hpp |
Topology-preserving transform filter — simultaneous LP/HP/BP/notch outputs |
Effects¶
| Processor | Header | Description |
|---|---|---|
| Character Delay | character_delay.hpp |
Wet-only stereo delay with clean, vintage-digital, tape, BBD, and diffusion feedback-loop characters — see the dedicated guide |
| Chorus | chorus.hpp |
Modulated delay for stereo widening and detuning effects |
| Convolver | convolver.hpp |
Partitioned frequency-domain convolution for reverb impulse responses |
| Delay Line | delay_line.hpp |
Sample-accurate delay with linear, cubic, or sinc interpolation |
| Fractional Delay | fractional_delay.hpp |
Prepared Thiran-1/Lagrange delay lines plus bounded shared history with stateless multitap Lagrange-3/5 heads, explicit causal ranges, and typed fault recovery |
| Dither Quantizer | dither.hpp |
Deterministic TPDF dither with opt-in bounded first- or second-order error-feedback noise shaping; zero latency and allocation-free |
| Lo-Fi Chain | lofi_chain.hpp |
Bit-depth reduction, sample-and-hold rate reduction, and dead-zone saturation; dither/noise shaping are opt-in so the legacy default remains exact |
| Oversampling | oversampling.hpp |
2x/4x/8x/16x realtime up/downsampling; minimum-phase IIR and 96/140 dB-prototype linear-phase FIR tiers with exact latency reporting |
| Phaser | phaser.hpp |
All-pass filter chain with LFO modulation for sweeping comb effects |
| FDN Reverb | fdn_reverb.hpp |
16-line feedback delay network with a selectable internal tank sample rate (16-96 kHz), Jot decay law, granular shimmer, and a provably bounded loop gain; wet-only |
| Reverb | reverb.hpp |
Algorithmic stereo reverb with room size, damping, and width controls |
| Waveshaper | waveshaper.hpp |
Static nonlinear distortion via transfer function (tanh, soft clip, custom) |
Dynamics¶
| Processor | Header | Description |
|---|---|---|
| Envelope Follower | dynamics_contract.hpp |
Exact peak/RMS envelope timing, stereo detector linking, and canonical gain-reduction telemetry; BallisticsFilter retains its legacy nominal timing for render compatibility |
| Transient Designer | transient_designer.hpp |
Zero-latency fast/slow-envelope shaper with independent attack and sustain gain; positive values enhance and negative values attenuate |
| Dynamic EQ Band | dynamic_eq.hpp |
Zero-latency internally keyed band with threshold activity, signed boost/cut range, detector telemetry, and fixed-state RT processing |
| Compressor | compressor.hpp |
Soft-knee downward compressor with threshold, ratio, attack, release |
| True-peak limiter | true_peak_limiter.hpp |
Stereo look-ahead limiter with 8x intersample detection, a fixed 64-sample gain-scheduling horizon plus optional user lookahead, explicit channel linking, latency, tail, and gain-reduction telemetry; larger channel capacities require an explicit template specialization |
| DryWetMixer | dry_wet_mixer.hpp |
Parallel mix with latency compensation — equal-power or linear crossfade |
| Gain | gain.hpp |
Scalar gain stage; pair with smoothed_value.hpp, log_ramped_value.hpp, or audio apply_gain_ramp() when transitions need de-clicking |
| Noise Gate | noise_gate.hpp |
Silence signals below threshold with hysteresis to avoid chatter |
Generators and analysis¶
| Processor | Header | Description |
|---|---|---|
| ADSR | adsr.hpp |
Attack-decay-sustain-release envelope generator for amplitude or filter modulation |
| FFT | fft.hpp |
Fast Fourier Transform — uses vDSP on Apple, fallback on other platforms |
| Multi-Channel Meter | multi_channel_meter.hpp |
Sample peak, RMS, stereo correlation, and channel-based BS.1770-5 K-weighted momentary plus gated integrated loudness; not true-peak, short-term, LRA, or a complete EBU Mode meter |
| Oscillator | oscillator.hpp |
Legacy polyBLEP oscillator with sine, saw, square, triangle waveforms (float phase, integrated triangle) |
Oscillator suite (osc/) |
osc/va.hpp, osc/vco.hpp, osc/dco.hpp, osc/wt.hpp, osc/wt_lofi.hpp, osc/minblep.hpp |
Newer VA/VCO/DCO/wavetable family plus shared phase, polynomial BLEP/BLAMP, and fixed-capacity causal minBLEP primitives — see the oscillators guide |
| Velvet Noise Grid | velvet_noise.hpp |
Coordinate-keyed jitter/sign draws for sparse velvet-noise tap grids; full and incremental builders produce identical draws |
| Spectrogram | spectrogram.hpp |
Rolling time-frequency analysis for visual display of spectral content |
| STFT | stft.hpp |
Short-time Fourier Transform for visualization (analysis-only; for processing use spectral_frame_engine.hpp) |
Physical modeling¶
| Processor | Header | Description |
|---|---|---|
| Modal Bank | modal_bank.hpp |
SIMD-friendly bank of coupled-form modes with contact-pulse excitation, independent strike/pickup weights, amplitude-preserving retuning, and up to eight pickup outputs |
| Modal Specification | modal_spec.hpp |
Versioned JSON interchange for modal frequencies, T60 values, amplitudes, and optional shapes, with bounded validation before allocation |
| Bridged-T Resonator | bridged_t_resonator.hpp |
Trapezoidally integrated two-state model of the published TR-808 bridged-T network, exposing physical component values and circuit nodes; it is a resonator primitive, not a complete drum voice |
| Square Oscillator Bank | square_osc_bank.hpp |
Allocation-free-after-prepare bank of independently tunable, weighted, band-limited square oscillators for inharmonic metallic excitation and other clustered sources |
Modulation and utility toolkit¶
The library layer complex DSP composes from instead of re-implementing inline — sources, wires, events, envelopes, and the routing between them. Full guide with worked patches: the modulation toolkit.
| Processor | Header | Description |
|---|---|---|
| Deterministic randomness | rng.hpp |
Xorshift32 with a Box-Muller Gaussian, a stateless purpose-keyed hash, and the Ornstein-Uhlenbeck walk (OuWalkT, DriftT) behind every analog-drift effect |
| LFO | lfo.hpp |
Seven waveforms plus a continuous shape morph, pulse width, triangle bias, a random blend, stereo and quadrature output, and a delay/fade-in/repeat/fade-out lifecycle |
| Control-signal tools | mod_tools.hpp |
SlewLimiterT, SampleHoldT, AttenuverterT, RectifierT, ComparatorT, QuantizerT, CurveT, and the shared stage-curve law |
| Trigger and gate kit | trigger.hpp |
TriggerDetectT, GateGenT, ClockDividerT, ClockMultT, BurstGenT, TrigDelayT — the event domain the modular world calls triggers and gates |
| Envelope family | envelope.hpp |
ArT, AdT, AhdT, DahdsrT, ModEnvT with per-stage curves and looping, plus the level-independent TransientDetectorT |
| VCA | vca.hpp |
Control-driven gain with linear or ~40 dB exponential response and a built-in de-clicking control lag; exactly unity at full |
| Low-pass gate | lpg.hpp |
Vactrol-modelled Buchla gate — loudness and brightness move together, and a re-strike mid-decay accumulates the way a real roll does |
| Mod matrix | mod_matrix.hpp |
Fixed-capacity source-to-destination routing with depth and a via slot; trivially copyable, so it hot-swaps through a TripleBuffer |
| Unit conversions | units.hpp |
dB, MIDI pitch, cents, one-pole and T60 coefficients, tapers, and compatibility spellings for the canonical timebase musical divisions (straight, dotted, triplet) |
| Chaos | chaos.hpp |
LogisticMapT — one control-rate source that runs from periodic to chaotic on a single knob |
ModalBank::prepare() allocates its fixed-capacity storage and therefore runs
off the audio thread. After preparation, process_add(), reset(), and pickup
updates are allocation-free; set_modes() is allocation-free but evaluates
transcendentals and should remain control-rate for large banks. Load and
validate modal-spec JSON away from the audio callback, then pass the resulting
mode span into a prepared bank. Link pulp::signal-modal-spec in addition to
pulp::signal when calling parse_modal_spec() or to_json().
BridgedTResonator::prepare() and process() allocate nothing.
Spectral processing¶
| Processor | Header | Description |
|---|---|---|
| Spectral Frame Engine | spectral_frame_engine.hpp |
Streaming STFT analysis + overlap-add synthesis with coherent multichannel frame groups and variable synthesis hop |
| Realtime Pitch/Time | realtime_pitch_time_processor.hpp |
Phase-vocoder pitch shifting (fixed duration, exact reported latency) and independent time stretching, with transient preservation, formant follow/preserve, and freeze |
| Phase Coordinator | multichannel_phase_coordinator.hpp |
Laroche-Dolson phase propagation with identity peak locking, applied as one rotation per bin across a channel group — preserves inter-channel phase exactly |
| Source-filter Analysis | source_filter_analysis.hpp |
Prepared cepstral and true-envelope analysis plus safely scaled autocorrelation LPC, reflection coefficients, Schur stability, and all-pole response; formant extraction remains explicitly unsupported |
| Envelope Shifter | spectral_envelope_shifter.hpp |
Cepstral spectral-envelope estimation (true-envelope refinement) and formant warping with exact unity bypass |
| Transient Policy | transient_phase_policy.hpp |
Spectral-flux transient detection (median + energy-relative gates) driving phase reset at onsets |
| Freeze Hold | freeze_hold.hpp |
Spectral freeze / infinite hold with de-looped phase evolution, click-free engage/release, and a no-mute latch policy |
| Pitched Feedback Delay | pitched_feedback_delay.hpp |
Delay with a latency-bearing processor inside the feedback loop, tempo sync, freeze-aware feedback gating, and a computed minimum delay |
| Control Smoother | latency_aware_control_smoother.hpp |
Closed-form one-pole smoothing with attack/release asymmetry, semitone/ratio domains, block-size-independent trajectories |
| Windowing | windowing.hpp |
Hann, Hamming, Blackman, Blackman-Harris, Blackman-Nuttall, flat-top, and Kaiser windows for FFT analysis |
Math and utilities¶
| Processor | Header | Description |
|---|---|---|
| Bias | bias.hpp |
Shift a signal's DC offset — useful for asymmetric waveshaping |
| Fast Math | fast_math.hpp |
Scalar DSP math helpers; approximations are explicitly labeled, while exp2 follows standard float edge semantics and exact representable integer powers under the ambient FP mode (audio-callback FTZ/FZ may flush subnormals) |
| Interpolator | interpolator.hpp |
Lagrange and Hermite interpolation for fractional-sample delay and resampling |
| Log Ramped Value | log_ramped_value.hpp |
Logarithmic smoothing for perceptually linear parameter transitions |
| Lookup Table | lookup_table.hpp |
Pre-computed function table for fast repeated evaluation of expensive functions |
| Matrix | matrix.hpp |
2×2 through 4×4 matrix math for mid/side encoding, rotation, spatial processing |
| Routing matrix | audio_matrix_mixer.hpp |
Fixed-capacity signed audio routing with continuous gain automation and explicit headroom policy |
| Mid/side | mid_side.hpp |
Orthonormal stereo encode/decode and mono-safe width |
| N-way routing | nway_crossfade.hpp, path_switcher.hpp, path_latency_aligner.hpp |
Constant-power path morphing, click-free selection, and exact latency alignment |
| Mirrored History Buffer | mirrored_history_buffer.hpp |
Fixed-capacity single-thread sample history with a contiguous oldest-to-newest view and deterministic wrap cost |
| Panner | panner.hpp |
Stereo and surround panning with equal-power or linear law |
| Polynomial Math | poly_math.hpp |
Polynomial evaluation and Horner's method for waveshaper transfer functions |
| Processor Chain | processor_chain.hpp |
Connect multiple processors in series — automatic prepare/process forwarding |
| SIMD Buffer | simd_buffer.hpp |
Aligned memory buffer for SIMD-safe block processing |
| Smoothed Value | smoothed_value.hpp |
Linear parameter ramps for zipper-noise reduction; use log_ramped_value.hpp for multiplicative/log smoothing |
| Special Functions | special_functions.hpp |
sinc, Bessel, dB↔linear, MIDI note↔frequency conversions |
dsl¶
External DSP language lanes that generate or adapt source into ordinary Pulp processors while keeping the third-party toolchain developer-supplied.
Link: pulp::dsl · Include prefix: <pulp/dsl/...>
| Lane | What It Does | Guide |
|---|---|---|
| FAUST | Offline code generation into checked-in C++ headers | FAUST guide |
| Cmajor | External Cmajor toolchain validation and generation | Cmajor guide |
| JSFX | Bounded source-only JSFX subset parsing and validation | JSFX guide |
All DSL lanes use the same contract: Pulp owns the processor wrapper and build integration; the external compiler/runtime remains opt-in and outside the public repository unless its license allows redistribution.
state¶
Parameters, state trees, presets, and settings.
Link: pulp::state · Include prefix: <pulp/state/...>
Plugin parameters — the core state system¶
#include <pulp/state/store.hpp>
StateStore store;
constexpr ParamID kGainId = 1;
constexpr ParamID kMixId = 2;
store.add_parameter({.id = kGainId, .name = "Gain", .unit = "dB",
.range = {-60.0f, 12.0f, 0.0f}});
store.add_parameter({.id = kMixId, .name = "Mix",
.range = {0.0f, 1.0f, 1.0f}});
// Audio thread reads atomically (no locks)
float gain = store.get_value(kGainId);
// UI thread writes with gesture grouping for undo
store.begin_gesture(kGainId);
store.set_value(kGainId, -6.0f);
store.end_gesture(kGainId);
StateTree — reactive hierarchical state¶
Like a JSON document that notifies you when anything changes. Use for complex plugin state beyond flat parameters.
#include <pulp/state/state_tree.hpp>
auto root = StateTree::create("synth");
root->set("name", std::string("My Patch"));
root->set("polyphony", int64_t(8));
auto osc = StateTree::create("oscillator");
osc->set("waveform", std::string("saw"));
osc->set("detune", 7.0);
root->add_child(osc);
root->add_listener([](StateTree& node, std::string_view prop, auto&, auto& new_val) {
log(std::string(prop) + " changed");
});
std::string json = root->to_json(); // Serialize
auto restored = StateTree::from_json(json); // Deserialize
PropertyValue supports scalar values (std::monostate, bool, int64_t,
double, std::string) plus provider-neutral structured leaves:
root->set("macroState", make_property_object({
{"name", std::string("Brightness")},
{"points", make_property_array({0.0, 0.5, 1.0})},
{"metadata", make_property_object({{"enabled", true}, {"revision", int64_t(2)}})},
}));
Use arrays/objects for structured leaf data that belongs to a property
(JSON-like arrays, dictionaries, custom state records, imported var-style
values). Use child StateTree nodes for owned tree structure: a ValueTree-like
record with child records should become a StateTree parent with child nodes,
while each node's non-structural payload can use scalar/array/object
properties. Pulp intentionally does not store StateTree nodes inside
PropertyValue: node ownership lives in the parent/child graph, which keeps
deep_copy(), clone_synced(), and StateTreeSynchroniser free from hidden
aliasing and cycles.
Existing scalar typed getters and std::get_if checks keep working; exhaustive
std::visit handlers over PropertyValue should add Array/Object cases.
Persistent settings¶
#include <pulp/state/properties_file.hpp>
PropertiesFile settings;
settings.load("~/.config/MyPlugin/settings.json");
settings.set_string("theme", "dark");
settings.set_int("buffer_size", 512);
settings.save();
// Or use platform-standard paths automatically:
ApplicationProperties app("MyPlugin");
app.load();
app.user_settings().set_bool("first_run", false);
app.save();
Other state features¶
| Feature | Header | Description |
|---|---|---|
| Binding | binding.hpp |
Connect UI widget ↔ parameter with undo gesture grouping |
| Cached Property | cached_property.hpp |
CachedProperty<double> freq(tree, "freq", 440.0) — auto-updates |
| Preset Manager | preset_manager.hpp |
Factory/user presets, next/prev navigation, import/export |
| StateTree Sync | state_tree_sync.hpp |
Binary delta sync over IPC for multi-process state |
| Undo Manager | undo_manager.hpp |
undo_mgr.perform(action) / undo_mgr.undo() |
timebase¶
Musical and media time primitives for scheduling without accumulated floating-point
drift. CompiledTempoMap maps strong integer tick and sample positions across
constant tempos and BPM-linear-in-tick ramps. Tempo-point boundaries use exact
integer sample anchors; samples_to_ticks() returns a canonical tick so
sample-to-tick-to-sample conversion is exact while the tick grid is at least as
dense as the sample grid. Arbitrary ticks map monotonically to the integer sample
grid and may canonicalize to a neighboring tick. On a sparser grid,
resolve_sample() returns the nearest representable tick plus its exact sample
error; for example, at 48 kHz and 1 BPM, ticks 0 and 1 map to samples 0 and 4, so
sample 2 cannot have an exact integer-tick inverse.
The installed design-time capability timebase.tempo-map exposes exactly the
CompiledTempoMap type, its validating compile() factory,
ticks_to_samples(), and resolve_sample(). Compilation is a control-thread
operation that owns and validates the authored map. The two published lookup
operations are allocation-free and may run on the audio thread while the
immutable compiled value remains alive. Cursor and fractional interpolation
APIs remain available C++ APIs but are intentionally outside this capability's
v1.0 contract. TempoCursor::tempo_at_tick() is observational: inspecting a
future tempo does not change the sample-streaming position used by the next
advance() or advance_fractional() call.
The module depends only on pulp::runtime for typed results. Tick and sample
positions use their full signed 64-bit ranges; tick-position, duration, and MonotonicBeat
arithmetic saturates at the nearest endpoint rather than overflowing.
ticks_to_samples() likewise saturates when its mathematical result exceeds the
sample domain. When a requested sample lies outside the image of the tick
domain, resolve_sample() reports exact == false, a nearest canonical edge
representation, and the actual sample error.
project_ratchet_interval() subdivides two adjacent clock boundaries into a
bounded number of exact integer-tick hits. Its hit count includes the onset and
the later boundary is excluded, so adjacent intervals neither duplicate nor
orphan a clock-edge event. A count that would collapse multiple hits onto one
integer tick is rejected. Projection into half-open windows is allocation-free
and callback-partition invariant, including across non-divisible spans and the
full signed tick domain.
Link: pulp::timebase · Include prefix: <pulp/timebase/...>
#include <pulp/timebase/compiled_tempo_map.hpp>
using namespace pulp::timebase;
const TempoPoint points[] = {
{{0}, 120.0, TempoCurve::LinearInTicks},
{{8 * kTicksPerQuarter}, 160.0, TempoCurve::Constant},
};
const auto compiled = CompiledTempoMap::compile(points, {48'000, 1});
if (!compiled)
return handle_tempo_map_error(compiled.error());
const CompiledTempoMap& tempo = compiled.value();
const auto sample = tempo.ticks_to_samples({4 * kTicksPerQuarter});
MonotonicBeat is the strong type for the transport's non-looping musical
clock; the transport owns advancement while timeline positions may seek or
wrap. CompiledMeterMap provides the corresponding validated meter lookup.
BeatDivision is an append-only persisted vocabulary for straight, dotted, and
triplet values from whole notes through sixty-fourth notes. beat_fraction()
returns the reduced rational quarter-note value and division_ticks() converts
it to the exact document lattice, failing explicitly if a future division is
invalid, out of range, or not exactly representable. The older
signal::units::Division spellings retain their public names and persisted
ordinals, but convert to BeatDivision and derive beat values from this table;
new divisions must be appended to both vocabularies with exhaustive parity
coverage.
project_grid() projects those divisions through explicit
GridProjectionRange values. Each range carries its document sample/tick
anchors and its independent monotonic anchors, matching the clock domains a
transport publishes without introducing a dependency on playback. A pre-loop
range uses its ordinary document interval; every loop pass reuses the loop's
document sample interval and advances only the monotonic anchor; a seek may
replace the document anchor without resetting the monotonic clock.
Timeline-anchored grids retain global phase and bar-anchored grids restart at
exact bar boundaries. A stopped request emits no points. Callers provide output
storage; insufficient capacity reports the required count without modifying it,
and malformed ranges or sample/tick overflow fail explicitly. A block is bounded
to 65,536 candidate and projected points, with an overflow-safe preflight before
enumeration. Host-beat-mapped ranges retain their precise fractional tick
endpoints and project ticks proportionally into output frames, so session tempo
may differ from the document tempo without silently falling back to the document
sample map. Their HostGridAnchor names one normalized source tick at one
absolute output frame plus the source-ticks-per-frame slope; loop ranges add
their document-to-source pass offset. Callers initialize that coordinate from
the first resolved range in a normalization epoch, not from an absolute host
beat that the transport has already wrapped, and reset it when the epoch or
slope changes. Reusing that anchor across callbacks keeps a rounded loop split
from moving a grid tick by one frame when callback partitioning changes. For
document-clock ranges the rounded tick end is
inclusive only as a candidate search bound; the half-open document sample
interval is authoritative. This preserves a grid point in a one-frame range
even when a sparse tick map rounds both range endpoints to the same tick,
without duplicating it in the next range.
OrderPreservingGrooveKernel is not the canonical, named, sequence-owned
timeline::GrooveTemplate. It is a fixed-capacity realtime projection kernel
for the stricter non-reordering subset of that model. Its independent swing and
table grids, 0..1000 strengths, 0..4000 velocity accents, and 1024-step ceiling
match the timeline value domains so callers can adapt existing authored values
without inventing another format. Timing strength scales both swing and table,
making zero a complete identity. Construction checks the combined
configured transform over a bounded joint period and rejects reorder or a
period too large to validate. Application reports range failure rather than
saturating a document-visible tick.
coordinate_random() and coordinate_chance() derive deterministic values
from seed, tick, lane, loop cycle, and stream, rather than callback-local mutable
RNG state.
LoopRegion is the loop bounds a transport honours, in document ticks. It lives
here rather than beside either consumer because that is all it is — two document
positions and whether they are in force — so the rung that runs the transport
(playback::LoopRegion) and the rung that draws the ruler
(timeline_editor::UiPlayhead::loop) name one type instead of two structurally
identical ones. A disabled loop keeps its bounds, so turning looping off and back
on returns the user to the region they set up and a view keeps drawing it
meanwhile.
TriggerGrid is the allocation-free authored rhythm counterpart: fixed-capacity
track×step cells carry velocity, exact rational probability, and bounded
microtiming. Projection is a pure mapping of one caller-supplied cycle into a
half-open tick window. The caller also supplies one stable random word per grid
coordinate, so probability decisions do not depend on audio callback partition.
The grid deliberately does not own transport advancement, groove or swing,
mutable RNG state, generative pattern algorithms, or note lifetime.
#include <pulp/timebase/trigger_grid.hpp>
#include <array>
#include <cstdint>
pulp::timebase::TriggerGrid<8, 16> grid;
grid.configure(2, 16, {pulp::timebase::kTicksPerQuarter / 4});
grid.set_cell(0, 0, {.enabled = true, .velocity = 112});
std::array<std::uint64_t, 32> coordinate_draws{};
std::array<pulp::timebase::TriggerEvent, 32> events{};
const auto projected = grid.project_window({0}, {0},
{pulp::timebase::kTicksPerQuarter},
coordinate_draws, events);
timeline¶
Immutable document-model foundations and a bounded typed editing core for musical timelines. Project,
Sequence, Track, and Clip are cheap copyable snapshots whose construction
factories validate identities, ranges, references, and non-overlapping sparse
arrangement lanes. Tracks retain persistent AVL indexes for both
(anchor, start, ItemId) timeline order and ItemId lookup. replace_clip()
path-copies only affected search paths while older snapshots share untouched
subtrees.
Clip insertion, removal, movement, playback-property changes, note-velocity
edits, and tempo/meter map replacement apply only through atomic transactions.
DocumentSession serializes multiple control-thread
writers, publishes pinned immutable snapshots, rejects stale revisions and
typed precondition failures without partial application, and records a precise
dirty set. Its bounded journal rejects when full rather than losing replay
history; inverse-command undo/redo append ordinary new transactions.
Link: pulp::timeline · Include prefix: <pulp/timeline/...>
For subsystem contracts and ownership, see the Creative Timeline Engine SDK guide. For compile-backed project, transaction, persistence, playback, capture, launch, interchange, CLI, and MCP tasks, use the Timeline cookbook.
#include <pulp/timeline/model.hpp>
using namespace pulp::timeline;
auto empty = Clip::create({3}, {0}, {705'600}, EmptyContent{});
if (!empty)
return;
auto track = Track::create({2}, "Notes", {std::move(empty).value()});
Every owned object uses a nonzero monotonic ItemId; a Project stores the
next never-used value; UINT64_MAX is the explicit exhausted allocator state
and is valid project state after ownership reaches UINT64_MAX - 1.
Project::locate() returns an ItemLocation whose kind and immediate
parent_id are the canonical ownership key. Its sequence, track, and clip IDs
are ancestor-navigation caches, not additional ownership axes. Snapshot decode
derives parent_id for older identity records that predate the field, while
canonical output writes it explicitly.
ClipTimeAnchor distinguishes tempo-following musical tick ranges from fixed
absolute ranges expressed as SamplePosition, integer sample count, and a
normalized RationalRate. ClipPlaybackProperties carries nonnegative linear
gain plus fade-in and fade-out lengths in the clip anchor's native unit:
canonical ticks for musical clips and timeline samples for absolute clips.
Construction checks both fades against clip duration; compilation maps musical
fades through the tempo map to exact frame counts. Mixed-anchor clips within
one Track are rejected until a context-owned projection can compare those
domains; a Sequence can still contain separate musical and absolute Tracks and
bound both domains.
remap_ids() performs two passes: it allocates every destination identity
before rebuilding the immutable hierarchy. Clip, Track, and Sequence subtree
overloads distinguish owned IDs from external media-asset references and accept
an atomic ExternalIdFixup; closure-wide duplicate owned IDs are rejected
before allocator state changes. MidiContent is a flat POD array sorted by
(start, ItemId), alongside a sparse companion array of per-note playback
modifiers sorted by note ID and an authored seed. A modifier carries a
probability, a loop-pass condition, and a ratchet count; notes that play
unconditionally and once carry no entry. Evaluation is a pure function of the
seed, the note identity, and the loop-pass index, so an identical document and
transport trace always reproduces the same sounding decisions. The same content
carries the clip's controller and expression lanes, one lane per addressed
stream, ordered by address with each lane's points ordered by
(position, ItemId); a lane address is the MIDI wire's own group, channel,
status nibble, controller bank, and controller index, and a point value is the
32-bit channel-voice data width. Clips authoring no controllers carry no lanes.
Lane storage is complete, but playback does not yet emit lane values: compiling a
clip that carries lanes fails with a named error rather than producing a program
that plays the notes and drops the controllers.
Fallible construction uses
pulp::runtime::Result and reports ModelError without exceptions.
automation_curve.hpp provides immutable, position-ordered automation points
with stable IDs, Hold or Continuous interpolation, and bounded monotonic
curvature. Its random-access evaluator is a control/compile-time API. Timeline
lane targeting remains document-model state, while audio-thread cursors and
per-block event coalescing belong to pulp::playback; neither concern is folded
into the curve container.
automation_lane.hpp provides an immutable binding from one curve to a
format-neutral device-placement identity and opaque 32-bit parameter ID.
Standalone construction validates the lane and placement IDs via
ItemId::valid() (neither zero nor the exhausted UINT64_MAX sentinel);
Track attachment additionally proves the placement exists, enforces unique
placement/parameter targets, and registers lane and point identities in the
Project. Lanes persist in snapshots and are reachable through typed commands
and DocumentSession. pulp::playback compiles attached lanes into immutable
cursor programs, while host-graph parameter delivery remains outside Timeline.
parameter_target.hpp holds the format-neutral vocabulary for naming a
document parameter — a placed device parameter, or one of the owning track's own
mixer controls. One vocabulary serves every consumer that addresses a parameter,
because "which parameter" is addressing rather than a property of what writes
there. AutomationTarget and ModulationTarget are both names for it.
modulation.hpp provides modulators, macro controls, and modulation routes as
Track-owned document entities distinct from automation. The difference is what
they write: an automation lane authors a parameter's base value over time,
while a modulation route contributes a depth-scaled relative offset on top of
whatever base is in force, which is CLAP's param_value/param_mod separation.
Two consequences the document preserves: several routes may reach one parameter
and their offsets sum, where two automation lanes on one parameter is a
contradiction the model rejects; and depth belongs to the connection rather than
to the source, so one macro reaches many parameters with a different amount for
each. A route names its source by identity and kind, so a macro can never
stand in for a modulator that shared its ID. Track attachment proves the source
is a modulator or macro of the matching kind on the same track and that any
referenced placement exists in that track's chain. No modulator runtime ships
yet; the schema exists so routing authored now survives to the phase that adds
one.
device_placement.hpp defines the durable identity of one logical placement in
a Track-owned device chain. The chain preserves authored processing order
through immutable clip edits, persistence, and ID remapping. A placement is
identity/order-only: runtime instances, graph nodes, plugin formats, paths, and
platform metadata stay outside Timeline. Project snapshots persist the
placement identity and authored order, but do not persist a runtime device
definition or configuration payload.
Take and TakeLane keep recorded source identity and comp intent in the
document. Takes reference sealed assets in absolute sample time; comp segments
select normalized, non-overlapping ranges from those takes. Tracks retain all
lanes but select at most one active lane for playback, with zero meaning the
original arrangement. InsertTakeLane, InsertTake, SetRecordArm,
SetActiveTakeLane, and SetTakeComp use the same transactional
precondition/journal/undo machinery as clip and automation edits. Active lanes
and selected takes cannot be removed until their references are cleared.
TrackFreeze is an optional immutable selection of a sealed audio asset and
the content hash of the render plan that produced it. It supersedes arrangement
or comp playback without deleting authored clips, takes, automation, or device
placements. Publish CreateAsset before SetTrackFreeze in one transaction;
clear the freeze before removing the asset. A journal replay selects the sealed
artifact and never performs a hidden render.
TrackMixer is the track's own level and stereo placement: a linear
gain_linear and a pan balance in [-1, 1], edited with SetTrackMixer.
AutomationTarget accordingly names either a placed device parameter or one of
these mixer controls, so volume and pan automation is expressible without
inventing an out-of-band convention for which device is the fader. Sends, mute,
solo, and routing are not modeled. Playback applies the mixer where the track's
audio is accumulated; the graph binding uses a stable post-device node so
instrument output, effects, and tails are governed too. Hosted chains with a
nontransparent mixer must identify their post-device source and post-mixer
destination in TimelineTrackGraphRoute; the binding replaces and later
restores the exact direct edge. A lane supersedes the authored constant rather
than multiplying with it, and pan attenuates the opposite side without ever
boosting.
assets.hpp separates durable SHA-256 content identity from optional resolution
hints and alternate representations. An audio asset may also carry typed
AudioLoopInfo: musical length and meter, one-shot intent, MIDI root note,
half-open in/out markers, manual or analyzer-suggested loop points, and tags.
Tempo is intentionally derived from musical length, frame count, and sample
rate instead of being stored as a second value that can drift. Loop metadata is
canonicalized at project construction and remains asset-description state; it
does not make Timeline responsible for sample traversal or rendering.
schema_registry.hpp provides an explicit
immutable registry with typed extension codecs and bounded per-version
migrations. schema_release.hpp exposes release-labeled structural version
maps, and serialize_project_for_release() uses them to produce canonical
snapshots for v0.736.0, v0.744.0, v0.748.0, or v0.750.0. The latter
records the historical Track-v4 schema set; it predates SequenceRef content
and sequence mutation commands. Release export fails
when it would discard populated device, automation, take, audio-loop, or
extension state, including nested-sequence content unsupported by the target.
It removes inactive identity tombstones for kinds the target release cannot
name while preserving next_item_id as the durable no-reuse boundary.
serialize.hpp reads and writes deterministic JSON snapshots: 64-bit values are
canonical decimal strings, malformed or oversized input is rejected under
DecodeLimits, and unknown extension envelopes retain their exact validated
bytes for lossless ordinary re-save. SerializedSnapshot flags those opaque
objects so callers can surface compatibility risk.
peek_project_summary() uses the caller's load SchemaRegistry, validates the
complete structural envelope, and reports project identity, name, root, and
supported-object counts without constructing a Project or resolving
references; it is the bounded project-browser and load-admission tier. This is
snapshot JSON only; it does not read or write ZIP/package containers.
journal.hpp defines the optional JournalSink persistence seam. A session
publishes a transaction only after the sink reports its complete batch durable,
and installs a checkpoint before discarding the covered in-memory entries.
Because a failed write can have reached storage before its error is observable,
any sink error poisons that session for subsequent durable writes. Sink
callbacks run under the session writer lock and must not call lock-taking APIs
on the originating DocumentSession.
file_journal.hpp provides the native crash-consistent implementation. It
writes canonical snapshots as versioned, checksummed frames and completes each
append only after a platform durability fence. Recovery accepts only a valid
frame prefix, discards and reports a torn trailing frame, and fails closed on
earlier corruption. A checkpoint at the current durable revision replaces the
file through a synced temporary sibling and atomic rename; checkpointing an
older prefix preserves the newer durable frames already on disk. Recovered
sessions resume at their stored nonzero DocumentRevision only after the sink
validates an exact canonical-serialization/revision match without mutating
durable state. Symlink paths share one canonical lock identity. Multiply linked
journal files are rejected because atomic checkpoint replacement cannot
preserve hard-link identity. Package containers remain outside this module
surface.
SequenceRef clips place another project-owned sequence without transferring
ownership. Project construction rejects missing references, cycles, and depth
greater than eight. Sequence mutation, eager copy-on-edit divergence, and
reference retargeting use typed commands and the ordinary journal/undo path.
Playback expands supported child note/audio content into immutable root-track
programs before publication and fans child dirtiness out through every
transitive placement; unsupported child processing state fails compilation
closed.
This surface intentionally excludes package I/O, playback delivery, runtime launch arbitration, device implementation and routing, and UI. Authored scenes and launch slots are durable document state. The compiler accepts Arrangement only; the embedding application owns runtime launcher interpretation and scene-to-track arbitration.
project_package¶
Crash-consistent publication for stable project-package roots and generic
files or directories. AtomicPublisher::create() creates a private sibling
directory stage, accepts only safe package-relative paths through write(),
and publishes it with commit_directory(). File publication instead uses
create_file(), which returns one pre-created staging_file() that an external
producer may truncate and fill before commit_file(). Both modes publish a
previously absent destination without replacing it. PackageWriter instead maintains one
stable package root: stage_blob() hash-verifies and fences content-addressed
blobs before publishing them no-replace, while publish() validates every
package-relative asset reference before atomically replacing the root's
project.json generation and fencing the root directory. An interrupted stage
remains unreachable and cannot expose a durable reference to unfenced content;
package-wide abandoned-stage recovery and reachability GC belong to the
follow-on recovery layer.
Writer exclusion is cooperative. All package writers must honor the package
lock, and callers must not concurrently rename or replace package or private
staging entries out of band from another process running as the same account.
Any external producer using staging_directory() or staging_file() must
finish and release the stage before commit or cancellation begins. On Windows,
staged objects retain a private DACL until the rename succeeds; the still-open
published handle then adopts the destination parent's inheritance. A crash
before or during that adoption can leave the published object or some
descendants owner-private instead of broadly inherited; callers must treat
final permissions as incomplete.
The implementation pins and revalidates identities to reject detected
rebinding, but POSIX does not provide a portable operation that renames an
already-open directory by identity.
Link: pulp::project-package · Include prefix:
<pulp/project_package/...>
#include <pulp/project_package/atomic_publisher.hpp>
using namespace pulp::project_package;
auto publisher = AtomicPublisher::create(destination);
if (!publisher)
return;
auto staged = publisher->write("render-manifest.json", manifest_json);
if (!staged || !staged.value())
return;
auto outcome = publisher->commit_directory();
if (!outcome || outcome.value() != AtomicPublishOutcome::PublishedDurably)
return;
Use PackageWriter when the destination is a stable project-package root.
Stage each referenced blob first, then publish the Project that references it;
opening the package revalidates both the generation and its references:
#include <pulp/project_package/project_package.hpp>
using namespace pulp::project_package;
using pulp::timeline::make_builtin_timeline_registry;
auto registry = make_builtin_timeline_registry();
if (!registry)
return;
auto writer = PackageWriter::create(package_root, registry.value());
if (!writer)
return;
auto blob = writer.value().stage_blob(BlobStore::Media, media_hash, media_bytes);
if (!blob)
return;
// `project` contains the matching package-relative asset reference.
auto outcome = writer.value().publish(project);
if (!outcome || outcome.value() != AtomicPublishOutcome::PublishedDurably)
return;
auto opened = open_package(package_root, registry.value());
if (!opened)
return;
PublishedDurabilityUncertain means the new project.json may already be
visible even though final permission adoption or the directory fence did not
complete; callers must not report that outcome as a definite rollback.
Source builds may set PULP_ENABLE_PROJECT_PACKAGE=OFF to omit this component
and the dependent Timeline authoring tools. The resulting installed SDK does
not export Pulp::project-package, so requesting the project-package
component with find_package(Pulp REQUIRED COMPONENTS ...) fails.
The module owns publication and bounded exact-prefix cleanup of its private staging files, not package-wide recovery, reachability GC, a project schema, or an archive format. Timeline remains the authority for canonical project JSON; DAWproject ZIP admission and interchange loss accounting stay in their format and tooling layers rather than entering this dependency floor.
Depends on: pulp::timeline, pulp::runtime
interchange¶
Format-neutral interchange machinery shared by every exporter and importer.
A capability table declares, per concept, what a given format can represent;
plan_export() walks a Project against that table and returns an
ExportPlan carrying a LossManifest of everything the format cannot hold.
The seam is deliberately consent-based and fail-closed: run_export()
refuses a plan whose losses the caller has not accepted concept by concept.
There is no force flag, so an export cannot silently discard authored material.
New adapters plug in as format-bound FormatBoundExportWriter handles whose
callable is private to run_export(). The released callable ExportWriter
alias remains available for source compatibility. The plan owns the exact
immutable Project snapshot the writer receives, so consent measured on one
revision cannot authorize bytes
captured from another. run_export() centrally appends the versioned
pulp-loss-manifest.json artifact and reserves that name from adapters.
A census pass records which concepts a project actually uses, so a document that never uses a lossy concept is reported lossless rather than being flagged on the format's theoretical limits.
Depends on: timeline, timebase, runtime
dawproject¶
Reader and writer for the DAWproject
interchange format, implemented directly from the published specification over
pugixml. Ships as two libraries so a consumer can take only what it needs:
pulp::dawproject-import and pulp::dawproject-export.
Both sides cover the same bounded linear subset — flat tracks, beats-timed
clips, inline notes, referenced audio, one tempo and one meter — and both fail
closed outside it. The importer refuses nested groups, warps, seconds-timed
lanes, and unknown elements rather than dropping them. The exporter routes every
unrepresentable concept through the interchange consent seam and writes an
in-band loss manifest into the package alongside project.xml, so the losses
travel with the file instead of scrolling past in a console.
A track's <Channel> is admitted on import only when neutral, and refused by its
own concept when it states a volume or pan. On export a neutral channel is
written so a receiving DAW registers the track at all; that is a structural
requirement of the format, not an export of authored mixer state, which the loss
manifest still reports as dropped.
Media identity is sealed by content hash, and package-relative asset paths are confined to the package at two layers — a lexical check in the model and a canonicalizing beneath-the-base check in the loader, which is what catches a symlink that points outside.
Depends on: interchange, timeline, audio, runtime
smf¶
Standard MIDI File import and export, plus a consent-gated interchange adapter.
Links: pulp::smf-interop / pulp::smf-interchange · Include prefixes:
<pulp/timeline/smf.hpp> / <pulp/smf/interchange.hpp>
The raw codec library is separate so a consumer can leave SMF out, but that raw
API lives with the timeline surface it operates on. The consent adapter has its
own <pulp/smf/...> include prefix.
Import accepts format 0 and 1 with a metrical division, note on/off, and the tempo, time-signature, track-name, and end-of-track meta events. SMPTE divisions, format 2, and any other event fail the import unless the caller explicitly opts into ignoring non-note events.
The raw export API strictly rejects unsupported clip/event/time-grid shapes it
visits, but it does not census unrelated project-wide state. The separate
pulp::smf-interchange adapter binds SMF to plan_export() / run_export().
With exact per-concept consent it can omit unsupported concepts, strip note
modifiers, and approximate tempo ramps as discrete authored-point steps; the
central loss manifest records every such decision. Tick rounding remains under
the raw SMF export option and is never implied by concept consent.
Interchange planning is concept-level. representable() says that a concept
kind has format support in isolation; it is not an instance survivor list. For
example, notes inside an accepted dropped absolute clip remain a supported note
concept even though the container loss explains why those instances are
omitted. Only the census-backed adapter provides project-wide loss consent; the
raw codec is not a full-project loss audit.
Depends on: pulp::timeline, pulp::runtime; adapter additionally depends
on pulp::interchange
graph¶
Graph runtime scaffolding shared by the host binding and the sequence adapter.
Link: pulp::graph · Include prefix: <pulp/graph/...>
Levelization orders nodes into dependency levels, buffer assignment allocates and reuses the minimum set of intermediate buffers, and the runtime queue drives execution. The plan is computed on the control thread and consumed as an immutable snapshot by the audio thread, so the render path performs no allocation or graph traversal decisions of its own.
Depends on: pulp::midi, pulp::runtime
scene¶
Optional 3D scene surface (Scene3D).
Link: pulp::scene · Include prefix: <pulp/scene/...>
Loads glTF 2.0 assets, resolves material keys, and emits render packets for the GPU path. Bake preflight validates a scene before it reaches the renderer. This module is independent of the 2D view/canvas stack.
Depends on: pulp::runtime
sample_bank_manifest¶
Sample-bank manifest parsing, split out so a consumer can link it without the whole audio module.
Link: pulp::sample-bank-manifest · Include prefix: <pulp/audio/...>
It has no sources of its own — it compiles core/audio/src/sample_bank.cpp and
exposes the matching core/audio/include headers. pulp-audio PUBLIC-links it,
so every consumer of the audio module already has it in their link closure; the
separate target exists for consumers that want manifest handling alone.
Depends on: pulp::runtime
timeline_agent_view¶
Bounded, versioned read projections for agents and other context-limited
consumers. AgentView pins an immutable timeline::DocumentView; every read
requires the caller's expected revision and refuses mismatches. The outline is
project/sequence/track/clip sized. Rows carry Merkle content commitments, while
each omitted count and SHA-256 covers only the directly omitted authored rows,
so omission counts form a non-overlapping partition of the structural census.
Region pages select clip starts in a half-open window and order them by
(start, id). A cursor is accepted only when its version, revision, sequence,
anchor, exact window bounds, and key identify a member of that same window.
DirtySet projection additionally requires an adjacent before/after revision
range ending at the pinned view. That range rejects stale and multi-commit
projections, but it cannot authenticate DirtySet origin because the public set
type carries no session-issued provenance token; callers must pair it with the
CommitResult that produced it. Removed identities map to their tombstoned
nearest outline owner.
Link: pulp::timeline-agent-view · Include prefix:
<pulp/timeline_agent_view/...>
Depends on: pulp::timeline, pulp::runtime
timeline_editor¶
Interfaces for building a timeline editor over the document model, plus the edit vocabulary a gesture speaks.
The module exists so an editor view can show a moving playhead, let a user hear
what they are editing, and hand out the edits a gesture produced — without
linking playback. SequencerUiHost is that entire coupling: a view holds one,
and whoever owns audio implements it. A plugin that draws a piano roll over its
own engine implements it itself and never acquires a transport.
Two properties make the split hold, and both are enforced rather than asserted.
Everything crosses the interface by value, so a playhead reading a view is
holding cannot be invalidated when the engine adopts a different compiled
program — it goes stale, never dangling, and UiPlayhead::program_generation
is how a view tells the difference. UiPlayhead::continuity_epoch answers the
separate question of whether the position moved continuously between two
readings: a loop wrap, a seek, and a scrub anchor all break continuity without
recompiling anything, so a view that smooths motion between readings must hold
the newer position outright across a change in this value rather than
interpolating into it. And the vocabulary is document-side:
positions are timebase ticks, subjects are timeline::ItemIds. Nothing here
describes how audio is produced.
tools/scripts/timeline_engine_dependency_floor_check.py holds the module to
that closure over both its includes and its CMake links.
Link: pulp::timeline-editor · Include prefix: <pulp/timeline_editor/...>
#include <pulp/timeline_editor/sequencer_ui_host.hpp>
using namespace pulp::timeline_editor;
// A view repaints its ruler from a value it owns outright.
const UiPlayhead reading = host.playhead();
if (reading.moving())
draw_playhead_at(reading.position);
// Smoothing between publishes stops at a continuity break, so a loop wrap
// never draws the playhead sliding backwards through the whole timeline.
if (reading.continuity_epoch == previous.continuity_epoch)
draw_playhead_at(interpolate(previous.position, reading.position, alpha));
else
draw_playhead_at(reading.position);
// Clicking a note asks to hear it, in document terms only.
AuditionRequest request;
request.track = track_id;
request.pitch = 60; // pitch, velocity, channel as timeline::NoteEvent spells them
const AuditionResult result = host.begin_audition(request);
if (result.handle.valid())
host.end_audition(result.handle); // on mouse-up
Edit intents are the editor's own vocabulary, so intent submission lives on
SequencerUiHostT<Intent>, a thin templated shim over the same interface. The
non-template base carries the two duties that do not depend on how edits are
expressed.
EditIntent is that vocabulary, and lower_edit_intent turns one intent into the
ordinary timeline::Transaction that performs it — Draw to InsertClip, Erase
to RemoveClip, Move and Resize both to MoveClip, since a resize is a move
whose replacement range changes extent. An intent carries no coordinates, no
button, and no pointer id: a front-end resolves those against its hit metrics
first, so mouse, touch, and pen produce identical transactions by construction.
EditIntentHost names the concrete SequencerUiHostT<EditIntent> a front-end
submits to. The verbs live at this rung rather than in the document model so the
floor check can reject a reducer or serializer that reaches for one; the model's
floor excludes this module, which is the only direction in which the two rungs
differ.
TrackEditIntent is a second channel beside it, for arranging tracks rather than
editing clips, and lower_track_edit_intent turns one into the MoveTrack that
performs it. It is a separate type rather than added fields on EditIntent
because the two name different subjects: a clip intent names a clip inside a
track and carries clip time ranges, while a track intent names a track inside a
sequence and carries an insertion point. Folded together, every clip intent would
carry track-destination fields that are always empty and vice versa, with nothing
in the type able to say which combination is meaningful. TrackEditIntentHost is
the matching SequencerUiHostT<TrackEditIntent>, so a view that only rearranges
tracks never acquires the clip vocabulary.
Insertion is expressed as "before this track", matching the command, so a
front-end that resolved a drop position to a neighbour need not convert it to an
index. A std::nullopt destination means last position — a request, not an
omission, and deliberately not a separate append verb.
#include <pulp/timeline_editor/track_edit_intent.hpp>
TrackEditIntent intent; // drag `moved` above `neighbour`
intent.sequence_id = sequence_id;
intent.track_id = moved;
intent.expected_before_track_id = current_neighbour_of(moved);
intent.replacement_before_track_id = neighbour;
auto transaction = lower_track_edit_intent(intent, identity);
Piano-roll gestures use the sibling NoteEditIntent vocabulary. Insert carries
only a replacement note, erase carries only the expected note, and move, resize,
and velocity edits carry both snapshots with the same note identity.
ValidatedNoteEditIntent::create checks that shape and the note domain, and
NoteEditIntentHost accepts only the validated wrapper. Note intents deliberately
have no transaction lowerer yet: granular
note commands own that later boundary, so this editor API does not disguise an
O(clip) ReplaceNoteContent rewrite as an interactive note edit.
undo_gesture_budget answers how long a gesture can stream before the document
refuses it, which an editor needs before it opens one. A gesture coalesces
every Update into a single undo group, that group stays open until its End,
and only closed groups are evictable — so the charge accumulates with nothing
able to reclaim it, and past UndoLimits::max_retained_bytes the next step comes
back ConflictCode::UndoFull: a drag that stops responding rather than one that
degrades. The answer is max_retained_bytes / step_bytes and does not depend on
what the undo stack already holds, because a session has one open gesture at a
time, so every group present when one opens is closed and evictable.
#include <pulp/timeline_editor/gesture_budget.hpp>
// Priced before the gesture opens, from the step the drag is about to repeat.
const auto budget = undo_gesture_budget(limits.undo, forward, inverse);
if (budget.steps < expected_frames)
hold_the_edit_locally_and_commit_one_single_on_release();
else
stream_updates_into_one_open_group();
A GesturePhase::Single edit is closed on admission and therefore immediately
evictable, so the fallback branch has no step ceiling at all — only undo depth.
This is the undo ceiling alone: JournalLimits bounds the same gesture
independently, has no automatic eviction, and can bind first.
ScriptedUiHost<Intent> is a host whose playhead is written by the caller and
which keeps what a view emitted, so an editor is testable with no audio and no
mocking framework. It is also a legitimate deployment: an editor embedded in a
tool that only writes files gets a stopped playhead and Unsupported
auditions, and stays fully usable.
Depends on: pulp::timeline, pulp::timebase
timeline_view¶
Views that draw the document and turn a gesture into an edit intent — the first consumer of the editor rung rather than another declaration of it.
The rung exists so a view can be built, tested and reasoned about without an
engine. Its floor row admits timeline_editor, timeline, timebase, view,
canvas, platform and runtime, and deliberately omits playback: that
omission is the contract, not an oversight. A view's only coupling toward audio
stays the SequencerUiHost interface, so an arranger drawn over somebody else's
engine acquires no transport. Wanting to widen the row to reach playback means
wanting a host that implements the seam. It also omits project_package, which
keeps storage a sibling rung rather than a base: an editor is proven against a
serialize_project round trip, and re-hosting it on a package protocol later is
adapter work above the row.
A view takes resolved values — an origin tick, a px_per_tick, a resolved
tolerance_px, or a TickProjection/PitchProjection that is already a pair of
scalars with a name. What it does not take is viewport policy: an object
owning DPI, zoom or scroll state that the view must keep in sync. That line is
what lets a viewport slice and a view slice proceed concurrently without racing,
and it keeps a view testable headlessly with plain numbers.
Link: pulp::timeline-view · Include prefix: <pulp/timeline_view/...>
playback¶
The master timeline transport publishes integer-authoritative block snapshots.
Normal blocks contain one TransportRange; a block crossing a loop boundary
contains exactly two, with the second range marked as a discontinuity. Timeline
ticks wrap while MonotonicBeat remains continuous, so launch and scheduling
intent can use a clock that does not repeat. Forward, backward, and stopped
seeks reanchor only the timeline; they never jump the monotonic clock.
Control-thread changes are published as one coherent SeqLock state. The audio
thread consumes that state through allocation-free begin_block() calls. A
stopped block still covers the callback's frames while holding both musical
clocks. Loops shorter than the configured maximum block are rejected, which
guarantees one block can cross at most one loop boundary.
Snapshots carry first-block-safe transport and meter change flags, while each
range carries its own tempo and tempo_changed flag. This keeps a loop split
across tempo regions faithful when projected into per-range ProcessContexts.
Link: pulp::playback · Include prefix: <pulp/playback/...>
#include <pulp/playback/transport.hpp>
pulp::playback::MasterTransport transport;
transport.prepare(tempo, {
.max_buffer_size = 1024,
.initially_playing = true,
});
pulp::playback::TransportSnapshot block;
transport.begin_block(512, block);
for (std::uint8_t i = 0; i < block.range_count; ++i) {
schedule(block.ranges[i]);
}
Plugin/host adapters include
<pulp/format/playback_context_projection.hpp> to project a range into the
public ProcessContext. That header is the only lossy tick-to-double boundary;
core/playback does not depend on format, host, or view code.
The optional backend-neutral TempoSyncSource lets MasterTransport consume a
session tempo/beat mapping at the output boundary without putting device state
in the timeline document. Its host-time overload accepts an opaque timestamp
tagged by the producing source, preserves precise fractional ticks, and fails
closed when the token belongs to another clock domain or the source is
unavailable. The desktop adapter's realtime-safe output_host_time() reads
Link's platform clock and adds device/output latency before producing that
token. A developer can compile the desktop pulp::ableton-link adapter from
source with PULP_ENABLE_ABLETON_LINK=ON and an out-of-tree
PULP_ABLETON_LINK_SDK_DIR; that SDK-backed target is never installed or exported.
The module also compiles immutable PlaybackProgram snapshots off the audio
thread. A request carries one immutable Project snapshot, its external document
revision, a precompiled tempo map, and an explicit dirty-track set. Clean
TrackProgram objects retain shared-pointer identity while dirty tracks receive
a newer generation; revisions skipped by request coalescing are valid.
Sparse per-track policy deltas select an available provider and whether a stable
shell carries state by ItemId or resets it on stateless adoption. Omitted tracks
retain their published policy, and coalescing merges deltas with latest-track
wins before publication.
The compiler currently accepts arrangement payloads only: launcher/external-input
selections and availability bits are rejected until those provider programs exist.
DeferredCompileExecutor advances bounded slices from an idle/UI pump and
WorkerCompileExecutor supplies the native background lane, with an explicit
unsupported stub in threadless builds.
AutomationProgram separately compiles one immutable automation lane against
the exact shared tempo map. It retains tick endpoints so curved values stay in
musical time across tempo ramps while also storing sample-domain knots for
cursor traversal. A nonzero compile-time instance token distinguishes immutable
programs even when a caller accidentally reuses a generation.
AutomationCursor is an allocation-free, single-audio-thread
renderer over the transport's one or two half-open ranges. It emits immediate
or linear-ramp plain-domain control points into a caller-owned span. Selection
work and output are bounded by that explicit capacity. Range seeds and unique
in-range authored knots are mandatory; remaining capacity deterministically
refines continuous spans without erasing authored topology. The cursor reseeds
on loop/seek/adoption and rejects tempo-map or monotonic-generation mismatches.
TrackAutomationProgram validates one compiler-supplied track grouping and
retains its exact tempo-map and lane-program owners in lane-ItemId order. The
grouping rejects duplicate lane identities and duplicate device-parameter
targets while allowing unchanged lanes to retain older generations and instance
tokens. It is compiled playback data, not proof of Timeline document
attachment; PlaybackProgramCompiler owns authored-lane dirty tracking and
reuse decisions.
This layer deliberately does not know graph nodes or ParameterEventQueue; a
host binding must aggregate all lanes for a device, apply one global queue
budget, and inject one batch.
MediaRef clips use an immutable DecodedAudioAssetPool. Complete WAV bytes can
be decoded into this pool through the bounded no-file-I/O decoder, then the
ordinary incremental compiler lowers source ranges, musical or absolute clip
placement, gain, and fades into each TrackProgram. The renderer uses bounded
64-tap, 512-phase Kaiser-windowed sinc sample-rate conversion so media retains
its native wall-clock speed while strongly suppressing out-of-band content
before downsampling. Program compilation builds and shares one immutable
kernel per source/target rate pair; equal-rate media keeps an exact bypass.
Musical anchoring uses the tempo map for placement and extent but does not
silently introduce warp or time-stretch. Missing media, metadata mismatches,
invalid ranges, and capacity excesses reject compilation.
ArrangementAudioRenderer::process() consumes the same pinned
PlaybackProgram and the transport's complete zero/one-wrap snapshot. It
clears output, mixes arrangement-selected tracks in deterministic ItemId order,
zero-fills stops and source EOF, applies gain and linear fades sample-exactly,
and performs no allocation or lock on the audio thread. Mono sources duplicate
to wider output; multichannel sources average to mono or map like-numbered
channels. Float sums are not clipped or normalized by the engine.
At an audio callback boundary, one PlaybackProgramBlockLatch pins the whole
program for the block. Every StableRendererShell consults that same pin,
adopts only a newer generation for the same ItemId, and carries its small cursor
snapshot through a SeqLock.
Dirty arrangement tracks also compile their note clips into immutable,
tempo-map-resolved event streams. ArrangementNoteRenderer consumes the same
block pin plus the transport's one or two half-open ranges and emits
sample-offset MIDI without a graph audio node. Call prepare() off the audio
thread to reserve its bounded output buffer; process() is guarded by
ScopedNoAlloc. The output preserves authored 16-bit note velocity in a native
MIDI-2 UMP sidecar and provides a MIDI-1 compatibility mirror; capacity is
preflighted so a physical event appears in both lanes or neither. Note-offs
precede note-ons at equal samples. Program adoption,
seek, loop wrap, and stop release active notes and reset the cursor; Phase 1 does
not chase a note whose onset precedes the new range. The transport snapshot and
program must name the same compiled tempo-map identity, and overlapping logical
notes on one channel/pitch are reference-counted so the physical note-off waits
for the last overlap. Timeline commands and persistence preserve the clip gain
and fade properties consumed by the audio compiler.
capture_engine.hpp is the bounded realtime recording boundary. prepare()
allocates fixed take slots, audio storage, MIDI storage, and command/event
queues from caller-supplied limits; process() only mutates those prepared
buffers. Completed handles stay immutable until an explicit ReleaseTake.
Control-side code drains events, copies captured data, and surfaces queue or
capacity failures from CaptureEngineStats.
recording_commit.hpp seals captured or retrospective audio into WAV bytes, a
content-hashed MediaAsset, a Take, and ordered ordinary Timeline commands.
midi_capture_materializer.hpp converts captured MIDI frames through the exact
capture-rate CompiledTempoMap into note content while retaining MPE expression
as a sidecar. Applications submit the resulting commands through
DocumentSession and publish media bytes separately; the callback never edits
the project or journal.
sequence¶
Format-facing integration for publishing an immutable timeline playback program as a Pulp processor.
Link: pulp::sequence · Include prefix: <pulp/sequence/...>
SequenceProcessor adapts a caller-owned PlaybackProgramStore to
pulp::format::Processor. It projects host transport into timeline ticks and
executes the compiled track graph for VST3, AU, or CLAP adapters. It does not
own project editing, playback compilation, media resolution, a plugin host,
device I/O, or an editor.
#include <pulp/sequence/sequence_processor.hpp>
pulp::playback::PlaybackProgramStore programs;
pulp::sequence::SequenceProcessor processor(programs);
This is deliberately a heavier boundary than the engine-only
pulp::timebase/pulp::timeline/pulp::playback stack: linking it also brings
the format, graph, and state integration needed by a plugin processor. Publish
compatible immutable programs from a control or worker thread; the audio
callback only consumes the current program and host transport.
format¶
Plugin format adapters — write your plugin once, deploy to 9 formats.
Link: pulp::format · Include prefix: <pulp/format/...>
Writing a plugin¶
#include <pulp/format/processor.hpp>
class MyPlugin : public Processor {
public:
PluginDescriptor descriptor() const override {
return {
.name = "MyGain",
.manufacturer = "MyCompany",
.bundle_id = "com.myco.gain",
.version = "1.0.0",
.category = PluginCategory::Effect,
};
}
void define_parameters(state::StateStore& store) override {
store.add_parameter({.id = gain_id_, .name = "Gain", .unit = "dB",
.range = {-60.0f, 12.0f, 0.0f}});
}
void prepare(const PrepareContext& context) override {}
void process(audio::BufferView<float>& audio_output,
const audio::BufferView<const float>& audio_input,
midi::MidiBuffer& midi_in, midi::MidiBuffer& midi_out,
const ProcessContext& ctx) override {
float gain = db_to_linear(state().get_value(gain_id_));
for (std::size_t ch = 0;
ch < audio_output.num_channels() && ch < audio_input.num_channels();
++ch) {
auto in = audio_input.channel(ch);
auto out = audio_output.channel(ch);
for (std::size_t i = 0; i < out.size(); ++i) {
out[i] = in[i] * gain;
}
}
}
private:
state::ParamID gain_id_ = 1;
};
This single class automatically works as VST3, AU, CLAP, LV2, AAX, Standalone, WAM, and WCLAP.
Supported formats¶
| Format | Status | Notes |
|---|---|---|
| AAX | ✓ Usable | Requires developer-supplied Avid SDK |
| AU v2 | ✓ Usable | macOS only, via AudioUnitSDK |
| AU v3 | ✓ Usable | macOS + iOS (always runs sandboxed as .appex extension) |
| CLAP | ✓ Stable | First-class — modulation, WebView, note expressions |
| LV2 | ✓ Usable | Linux plugin format |
| Standalone | ✓ Stable | Desktop app with audio settings, test signal |
| VST3 | ✓ Stable | Full parameter sync, state, editor resize |
| WAM | ✓ Experimental | Web Audio Module (browser) |
| WCLAP | ✓ Experimental | Web CLAP (browser) |
Other format features¶
| Feature | Header | Description |
|---|---|---|
| ARA | ara.hpp |
Audio Random Access document-controller scaffold and SDK-gated companion-factory hooks |
| Host Detection | host_type.hpp |
detect_host_type() → Logic, Reaper, Ableton, etc. |
| Settings Panel | settings_panel.hpp |
Audio/MIDI device selector with test signal and meters |
| ViewBridge | view_bridge.hpp |
Editor-view lifecycle: create_view(), on_view_{opened,closed,resized}, multi-view attach (editor + inspector + remote). See docs/guides/view-bridge.md. |
The SDK-facing Processor and host-side PluginSlot surfaces share the
node ABI policy: virtual methods are append-only within a node
ABI generation, and new optional behavior should prefer additive descriptor
capabilities over new virtual methods.
host¶
Plugin hosting — the mirror of format. Load VST3 / AU / CLAP / LV2
plug-ins, wire them into a DAG, and process audio through the chain.
| Feature | Header | Description |
|---|---|---|
| Scanner | pulp/host/scanner.hpp |
Walk system plug-in paths; return PluginInfo |
| PluginSlot | pulp/host/plugin_slot.hpp |
Uniform load/prepare/process interface over every format |
| SignalGraph | pulp/host/signal_graph.hpp |
DAG topology + topological sort |
| Bake | pulp/host/baked_graph_processor.hpp |
Freeze a lowerable SignalGraph into an optimized BakedGraphProcessor (bit-identical to the live graph). bake() is the in-process (trusted) path |
| Baked codec | pulp/host/baked_codec.hpp |
Signed on-disk .pulpbake artifact: write_baked_signed + verify-before-parse load_baked (Ed25519 trust-set, bounded parse). See signal-graph |
All four format loaders (CLAP, VST3, AU, LV2) are implemented in
core/host/src/plugin_slot_*: each opens the bundle, resolves the
format's factory/descriptor, and wires the host-side PluginSlot onto
the format's real processing entry point. Feature coverage varies per
format (parameter automation, MIDI routing, editor views, and state
serialization are each at different stages); see the per-format
source files for the current scope. If an SDK is not compiled in at
configure time (for example, AU on Linux), the matching case in
PluginSlot::load() logs a warning and returns nullptr. See
hosting guide and
signal-graph reference.
canvas¶
2D drawing with GPU acceleration and smart text layout.
Link: pulp::canvas · Include prefix: <pulp/canvas/...>
Drawing¶
void MyWidget::paint(Canvas& canvas) {
// Background
canvas.set_fill_color(Color::rgba8(30, 30, 40));
canvas.fill_rounded_rect(0, 0, width, height, 8);
// Gradient fill
const Color colors[] = {Color::rgba8(80, 120, 255), Color::rgba8(40, 60, 180)};
const float positions[] = {0.0f, 1.0f};
canvas.set_fill_gradient_linear(0, 0, 0, height, colors, positions, 2);
canvas.fill_rect(10, 10, width - 20, 4);
canvas.clear_fill_gradient();
// Text
canvas.set_fill_color(Color::rgba8(220, 220, 230));
canvas.set_font("Inter", 14);
canvas.fill_text("Hello Pulp", 10, 30);
// Image
canvas.draw_image_from_file("icon.png", x, y, 32, 32);
}
Backends: Skia Graphite (GPU — Metal/Vulkan/D3D12) or CoreGraphics (macOS/iOS native).
TextShaper — measure once, reflow forever¶
Inspired by Cheng Lou's PreText. The expensive text measurement runs once; resizing uses just arithmetic.
#include <pulp/canvas/text_shaper.hpp>
TextShaper shaper;
auto prepared = shaper.prepare("Long text that wraps...", "Inter", 14);
// On every resize — pure arithmetic, no font calls:
auto layout = shaper.layout(prepared, container_width);
// layout.line_count, layout.total_height, layout.lines[i].text
float height = shaper.measure_height(prepared, 300.0f); // Fast
CMake option: PULP_TEXT_SHAPING — ON with GPU (real HarfBuzz metrics via Skia), OFF without (character-width estimation). Same API either way.
Other canvas features¶
| Feature | Header | Description |
|---|---|---|
| Attributed String | attributed_string.hpp |
Rich text spans — mixed font, color, weight per range |
| View effects | view_effect.hpp |
Per-view GPU post-processing: blur, bloom, vignette, chromatic aberration, EffectChain |
| Image Convolution | image_convolution.hpp |
ImageConvolutionKernel::gaussian_blur_5().apply(pixels, w, h) |
| Rectangle List | rectangle_list.hpp |
Clip regions with add/subtract/intersect for dirty tracking |
| SVG | svg.hpp |
Load and render SVG vector graphics via nanosvg |
| SDF text | sdf_atlas.hpp |
Single-channel signed distance field glyph atlas for resolution-independent GPU text. See docs/reference/sdf-text.md. |
| MSDF text | msdf_atlas.hpp |
Multi-channel SDF atlas scaffold with median(r,g,b) sampler plumbing; current generator emits equal RGB placeholder distances until msdfgen is wired. |
| PSDF text | psdf_atlas.hpp |
Pseudo-SDF variant with vector-fallback helper for extreme zoom. |
| SDF effects | sdf_effects.hpp |
Host-side design-token presets for outline / shadow / glow / bevel; visible rendering waits for the SkSL-backed SDF text draw path. |
| SDF atlas cache | sdf_atlas_cache.hpp |
Frame-based LRU glyph sharing across fill_text_sdf call-sites with dirty-rect upload hints. |
| Path → SDF | path_to_sdf.hpp |
Runtime EDT of a binary mask to produce an SDF for procedural shapes. |
view¶
Full widget toolkit with CSS-inspired layout and JS scripting.
Link: pulp::view · Include prefix: <pulp/view/...>
pulp::view is the full compatibility target and links both native widgets and
the JS runtime bridge. Baked/native UI code that constructs View trees
directly and does not evaluate JS can link pulp::view-core; code that uses
ScriptEngine, WidgetBridge, scripted UIs, or runtime import should link
pulp::view-script or the full pulp::view target.
Creating a UI¶
#include <pulp/view/widgets.hpp>
auto root = std::make_unique<Panel>();
root->set_background_token("bg.surface");
auto knob = std::make_unique<Knob>();
knob->set_label("Gain");
knob->set_value(0.5f);
knob->on_change = [&](float v) { store.set_value(gain_id, v); };
root->add_child(std::move(knob));
auto meter = std::make_unique<Meter>();
meter->set_orientation(Meter::Orientation::vertical);
root->add_child(std::move(meter));
Available widgets (30+)¶
Controls¶
| Widget | Description |
|---|---|
| Checkbox | Boolean on/off control rendered as a checkmark box |
| ComboBox | Drop-down menu for selecting one option from a list |
| Fader | Vertical or horizontal slider for continuous parameter control |
| Knob | Rotary control for parameters like gain, frequency, resonance |
| TextButton | Clickable button with a text label — supports toggle mode |
| TextEditor | Single or multi-line text input with native keyboard movement, selection, copy/paste, undo, IME, and grapheme-safe UTF-8 editing |
| Toggle | Two-state switch control for enabling/disabling features |
TextEditor Behavior¶
TextEditor is the SDK-level text-entry control used by native views, imported
HTML <input>, and imported <textarea> controls. It implements platform-style
caret movement and selection by default: character, word, line, document, page,
and Shift-selection variants; word/line delete shortcuts; double-click word
selection with word-granular drag extension; triple-click line selection in
multi-line mode; standard Cut/Copy/Paste/Select All context menus; and
mouse/trackpad scrolling for multi-line fields.
Text positions are stored as UTF-8 byte offsets for host/IME compatibility, but editing commands snap those offsets to grapheme-cluster boundaries. This keeps emoji, combining marks, flags, and ZWJ sequences from being split by arrow keys, Backspace/Delete, hit testing, or selection expansion.
Applications can tune text-field policy without forking key handling:
read_only allows focus, navigation, selection, and copy while blocking
mutation; View::set_enabled(false) disables interaction entirely;
tab_behavior chooses focus traversal, literal tab insertion, commit callback,
or consume/ignore behavior;
multi_line_return_behavior chooses Return/Shift-Return behavior; max_length
counts grapheme clusters; paste_sanitizer handles paste-only cleanup;
input_filter sanitizes typed and pasted insertion text; and validator
accepts or rejects a whole-buffer candidate before an edit lands.
line_ending_policy normalizes, strips, or preserves inserted line endings
where the control shape allows it. clipboard_policy can disable clipboard
traffic entirely or explicitly allow password contents to leave the field.
Password fields mask display text and disable selected-text export, copy, and
cut by default unless allow_password_clipboard or
ClipboardPolicy::allow_password_contents is enabled.
Programmatic set_text() is a host/state-sync operation, so it clears the
editor undo stack instead of recording a user-edit undo entry. Use
set_caret_pos(), set_selection(), selection_anchor(),
selection_active(), and selection_range() when a host, importer, IME, or
test needs explicit caret/selection control; all public offsets are clamped to
grapheme boundaries.
Containers¶
| Widget | Description |
|---|---|
| ConcertinaPanel | Accordion-style stacked panels — expand one section, collapse others |
| Panel | Basic container with optional background, border, and layout |
| ScrollView | Scrollable viewport for content larger than the visible area |
| SplitView | Resizable split between two child views with a draggable divider |
| TabPanel | Tabbed container — switch between child views via tab bar |
| Toolbar | Horizontal or vertical bar of buttons, toggles, separators, and custom views |
Data display¶
| Widget | Description |
|---|---|
| Breadcrumb | Navigation trail showing the current location in a hierarchy |
| FileBrowser | File system browser with navigation, filtering, and selection |
| FileTree | Hierarchical file/folder tree with expand/collapse |
| Label | Static text display with font, color, and alignment options |
| ListBox | Scrollable list of selectable items with virtual rendering for large datasets |
| PropertyList | Two-column key/value editor for inspector-style property panels |
| TableListBox | Sortable, scrollable table with column headers and row selection |
| TreeView | Hierarchical data display with expand/collapse and lazy loading |
Audio visualization¶
| Widget | Description |
|---|---|
| CorrelationMeter | Displays stereo phase correlation from -1 (out of phase) to +1 (mono) |
| EqCurveView | Interactive frequency response curve for parametric EQ — drag handles to edit bands |
| Meter | Peak/RMS level meter with configurable ballistics and clip indicators |
| MultiMeter | Multiple level meters side-by-side for multi-channel monitoring |
| SpectrogramView | Rolling time-frequency heatmap showing spectral content over time |
| SpectrumView | Real-time frequency spectrum analyzer with configurable FFT size |
| WaveformView | Audio waveform display with zoom, selection, and playhead |
| XYPad | Two-dimensional control surface for parameters like pan/width or filter freq/res |
Specialized¶
| Widget | Description |
|---|---|
| CanvasWidget | Custom-drawn view — override paint() to draw anything with the Canvas API |
| CodeEditor | Syntax-highlighted text editor with line numbers, designed for script editing |
| ColorPicker | HSV color selector with hue ring, saturation/value square, and hex input |
| FileDropZone | Drop target that accepts files dragged from the OS file manager |
| ImageView | Display raster images (PNG, JPEG) with optional scaling and aspect ratio |
| LassoComponent | Rubber-band marquee selection tool for selecting multiple items by dragging |
| LiveConstantEditor | In-app slider overlay for tweaking magic numbers during development |
| MidiKeyboard | Interactive piano keyboard — click to play notes, highlight active voices |
| PresetBrowser | Factory/user preset list with next/prev navigation and search |
| SplashScreen | Timed overlay window for branding or loading screens on app startup |
| SpriteStrip | Filmstrip-based control rendered from a sprite sheet image |
| WaveformEditor | Editable waveform display for drawing custom oscillator shapes |
Layout — Yoga flexbox + CSS Grid¶
// Flexbox
root->style().set_flex_direction(FlexDirection::Row);
root->style().set_gap(8);
knob->style().set_flex_grow(1);
meter->style().set_width(40);
meter->style().set_height_percent(100);
Theming — design tokens¶
Theme theme;
theme.colors["bg.surface"] = Color::rgba8(25, 25, 35);
theme.colors["control.accent"] = Color::rgba8(80, 130, 255);
theme.colors["text.primary"] = Color::rgba8(220, 220, 230);
// Widgets resolve tokens automatically:
canvas.set_fill_color(resolve_color("bg.surface"));
Design import¶
Per-source parsers under core/view/src/design_import_*.cpp translate
external designs into Pulp's import IR for pulp import-design.
design_import_designmd.cpp parses Google's DESIGN.md format
(YAML-frontmatter + Markdown body) into a DTCG tokens.json;
yaml-cpp (MIT, vendored via CMake FetchContent) provides the
frontmatter parse. See reference/imports/designmd.md.
JS-scripted UI¶
Write your plugin UI in JavaScript. Live hot reload is validated in the macOS
standalone development host; plugin targets can load the same UI_SCRIPT, but
live reload is not yet guaranteed across hosts:
// plugin-ui.js
const knob = new Knob({ label: "Gain", param: "gain" });
const meter = new Meter({ orientation: "vertical" });
document.body.append(knob, meter);
Engines: QuickJS (default, lightweight), JavaScriptCore (Apple, fast JIT), V8 (full ES2024)
Accessibility¶
VoiceOver (macOS + iOS), UIA (Windows), AT-SPI (Linux). Widgets declare their role and Pulp maps to platform accessibility APIs.
knob->set_access_role(AccessRole::slider);
knob->set_access_label("Gain");
knob->set_access_value("-6 dB");
osc¶
Open Sound Control messaging for networked audio control.
Link: pulp::osc · Include prefix: <pulp/osc/...>
#include <pulp/osc/osc.hpp>
#include <pulp/osc/bundle.hpp>
// Send
pulp::osc::Sender sender;
sender.connect("192.168.1.100", 9000);
sender.send(pulp::osc::Message("/synth/freq").add(440.0f));
sender.send(pulp::osc::Message("/synth/gate").add(1));
// Receive
pulp::osc::Receiver receiver;
receiver.listen(9000, [](const pulp::osc::Message& msg) {
if (msg.address == "/synth/freq")
set_frequency(msg.get_float(0, 440.0f));
});
// Bundle
pulp::osc::Bundle bundle;
bundle.add(pulp::osc::Message("/synth/freq").add(880.0f));
sender.send(bundle);
Supports bundles with timetags, the optional OSC RGBA colour tag (r), raw datagram sends, and address pattern matching (*, ?, [...], {a,b}) through receiver routes.
Binding OSC addresses to parameters¶
OscParameterMap binds incoming OSC addresses to plugin parameters, with a
learn mode — the OSC counterpart of pulp::state::MidiParameterMap. A mapping
targets a pulp::state::ParamID, the same id a timeline DeviceParameterTarget
carries, so OSC, MIDI, and authored automation address one parameter space.
#include <pulp/osc/osc_parameter_map.hpp>
pulp::osc::OscParameterMap map;
map.set_mapping("/track/1/fader", kGain); // literal address
map.set_mapping("/track/*/mix", kMix, {0.0f, 1.0f, 0.25f, 0.75f}); // pattern + window
map.arm_learn(kCutoff); // the next incoming address binds to kCutoff
// On the thread dispatching OSC messages:
map.pump();
map.handle_message(store, msg);
An OscMapScale declares both the range the surface sends (in_min..in_max)
and the normalized [0, 1] window it drives (out_min..out_max); input is
clamped to the incoming range and out_min > out_max inverts. Mappings live in
fixed-capacity storage, and a literal address dispatches by byte compare without
touching the heap — wildcard patterns route through address_matches, which
carries no such guarantee and must not be driven from the audio thread.
Because the binding lives in the interface of pulp::osc, the module links
pulp::state publicly. The dependency runs protocol → parameter model, keeping
pulp::state free of socket code.
native-components¶
The language-neutral C ABI for opt-in native-language audio components (Rust
first, also C / Zig / generated DSP). A C++ Processor adapter owns a
source-built native DSP core through this private, C-shaped FFI.
Link: pulp::native-components · Include prefix: <pulp/native_components/...>
#include <pulp/native_components/native_core.h> // the canonical C contract
#include <pulp/native_components/native_core.hpp> // optional C++ sugar (hash, asserts)
The header carries POD structs with leading size/abi_version, opaque
instance handles, status codes, host-owned borrowed buffers, a sorted
parameter-event view, an opaque versioned state span, and explicit
suspend/resume/reset lifecycle — embodying the twelve forward-compatibility
decisions in native-components reference. It has no
Rust dependency: the module builds on every platform, and the opt-in Rust
staticlib lane lives behind the PULP_BUILD_NATIVE_COMPONENT_RUST_TESTS CMake
option (OFF by default).
This is the Processor-level FFI, deliberately independent of SignalGraph.
The module also ships the public pulp_node_v1 node ABI (pulp_node_v1.h) for
custom graph nodes; its NativeCoreProcessor adapter lives in pulp::format, and
signed dynamic node-pack loading lives in pulp::host (node_pack.hpp). See
node-abi for the pulp_node_v1 contract and node packs.
render¶
GPU surface management — you rarely use this directly. Canvas and View handle it.
| Feature | What It Does |
|---|---|
| Dawn/WebGPU | Cross-platform GPU abstraction (Metal, Vulkan, D3D12, OpenGL) |
| GPU Compute | Experimental batch processing for >64K element workloads |
| Skia Graphite | 2D rendering on top of Dawn — what Canvas uses internally |
gpu_audio¶
Experimental. A fixed-latency bridge that lets a real-time process() block
offload heavy DSP to the GPU without blocking the audio thread. GpuAudioTransport
owns lock-free input/output rings and a polling worker; a GpuAudioNode submits
whole blocks to render::GpuCompute and reads results back one block later, so the
audio thread never waits on the device. When the GPU can't keep up (an offline
bounce running faster than real time, or a cold pipeline), a configurable miss
policy falls back to CPU or silence rather than glitching.
Link: pulp::gpu-audio · Include prefix: <pulp/gpu_audio/...> ·
Depends on: audio, runtime, signal (always) · render (GPU builds only)
| Node / primitive | What It Does |
|---|---|
GpuAudioTransport |
Fixed-latency RT↔non-RT bridge: lock-free rings + polling worker over GpuCompute |
GpuConvolver |
GPU-resident fused / batched partitioned convolution (long IRs, many instances) |
GpuMultiConvolver |
Batched multi-IR / multi-room convolution — one GPU submit per block across N IRs |
GpuStft |
GPU STFT / ISTFT primitive — the spectral toolkit's analysis/synthesis stage |
GpuSpectralFreeze / GpuSpectralMorph / GpuSpectralStack |
Capture-and-render spectral engines (single freeze, A/B morph, N-layer stack/cloud) |
The node boundary is not real-time-safe at the device level by design — the
GPU round-trip is amortized across a block of fixed latency, not paid per sample.
Only the GPU node implementations are gated on pulp::render; the
GpuAudioTransport bridge and the public node classes still compile and link in
a build without the GPU stack, report gpu_available() == false, and route the
signal::* CPU fallback.
Example plugins built on it (in-tree, examples/):
| Example | Uses | Docs |
|---|---|---|
| SuperConvolver | GpuConvolver (single IR) / GpuMultiConvolver (many rooms) — convolution reverb with live IR swap; GPU carries very long IRs / many rooms |
super-convolver |
| Spectral Lab | GPU spectral stack — N-layer freeze / morph "cloud" | spectral-lab |
| GPU NAM | render::GpuCompute::wavenet_forward neural-inference primitive (own repo) |
gpu-nam |
SDK guide: GPU Audio SDK — building nodes, the transport contract, and the validation checklist.
ship¶
Packaging and distribution — from code signing to installer to update feed.
Link: pulp::ship · Include prefix: <pulp/ship/...>
# Sign all plugin bundles
pulp ship sign --identity "Developer ID Application: My Company"
# Package — creates .pkg/.dmg (macOS), NSIS (Windows), or .deb/.tar.gz (Linux)
pulp ship package --version 1.2.0
# Check signing status
pulp ship check
| Feature | What It Does |
|---|---|
| Code Signing | macOS codesign + Windows signtool |
| DMG / PKG | macOS installer creation |
| Linux Packaging | .deb and .tar.gz |
| Notarization | macOS notarization with notarytool |
| Signing Check | Verify signing status of all built plugin bundles |
| Windows Installer | NSIS-based with optional Authenticode |