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 client and server for networked audio |
| 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 or TCP sockets. Use for crash-isolated plugin scanning, multi-process architectures, standalone↔plugin communication.
#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");
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);
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)
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.
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.
| 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 |
| Synthetic sampler heritage | sample_heritage.hpp, sample_heritage_schema.hpp, sample_heritage_engine.hpp, sample_heritage_src.hpp, sample_heritage_json.hpp, sample_heritage_runtime_state.hpp |
Define validated synthetic coloration/clock profiles, processing and rate conversion, strict JSON, and versioned runtime state 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.
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 |
signal¶
30+ real-time-safe DSP processors. 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/...>
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]);
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);
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 |
| 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 |
| Ladder | ladder_filter.hpp |
4-pole Moog-style resonant filter with saturation — classic analog synth sound |
| 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 |
|---|---|---|
| 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 |
| 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 |
| 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 |
|---|---|---|
| Ballistics Filter | ballistics_filter.hpp |
Envelope follower with configurable attack/release for meter and dynamics |
| Compressor | compressor.hpp |
Soft-knee downward compressor with threshold, ratio, attack, release |
| 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 |
Peak and RMS level measurement across multiple channels |
| 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 |
Newer VA/VCO/DCO/wavetable family sharing a phase accumulator and BLEP/BLAMP kernels — see the oscillators guide |
| 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 |
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 |
| 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, Kaiser window functions 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 |
Approximations of sin, cos, tanh, exp for inner loops where precision is traded for speed |
| 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 |
| 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 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.
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.
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/...>
#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.
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. NoteContent is a flat POD array sorted by
(start, ItemId). 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 unattached immutable binding from one curve
to a format-neutral device identity and opaque 32-bit parameter ID. Construction
validates only the lane and device IDs via ItemId::valid() (neither zero nor
the exhausted UINT64_MAX sentinel): it does not prove that the device exists
in a Project, register global identity, clamp plain-domain values, or consult
plugin metadata. The lane is not yet attached to a Track, persisted, reachable
through commands or DocumentSession, or delivered to a host graph.
pulp::playback can compile and exercise this standalone value without
implying document attachment.
assets.hpp separates durable SHA-256 content identity from optional resolution
hints and alternate representations. schema_registry.hpp provides an explicit
immutable registry with typed extension codecs and bounded per-version
migrations. 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 re-save. SerializedSnapshot flags those
opaque objects so callers can surface compatibility risk. This is snapshot JSON
only; it does not read or write ZIP/package containers.
This initial surface intentionally excludes durable journal sinks, package I/O, playback, document-attached automation lanes and delivery, launch slots, takes, nesting, devices, routing, and UI.
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 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;
the future track compiler 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
stateless linear sample-rate conversion so media retains its native wall-clock
speed; musical anchoring uses the tempo map for placement and extent but does
not silently introduce warp or time-stretch. A later quality pass decides
whether linear SRC should be upgraded. 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.
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.
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 |