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.
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();
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¶
RSA-signed license keys with online activation. The public key lives in your plugin; the private key stays on your server.
#include <pulp/runtime/license.hpp>
LicenseValidator validator;
validator.set_public_key(my_rsa_public_key_pem);
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
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("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 (mkfifo / 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 | async_updater.hpp |
broadcaster.send_action("file_open") to all listeners |
| Async Updater | async_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 | volume_detector.hpp |
mDNS/Bonjour API surface (experimental — requires host-supplied backend via install_backend; no built-in backend yet, see #302) |
| Timer | timer.hpp |
Timer t; t.start(100ms, []{...}) — 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
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 | ✓ | ✓* | ExtAudioFile (macOS) / FDK AAC (pulp add fdk-aac --accept-license FDK-AAC) |
| AIFF / AIFF-C | ✓ | ✓ | Native (8/16/24/32-bit big-endian) |
| ALAC | ✓ | ✓* | ExtAudioFile (macOS) / Apple ALAC (pulp add alac) |
| 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 + StreamingWriter |
*Write via optional pulp add packages. Permissive (libflac, ALAC) install freely. Copyleft (LAME, fdk-aac) require --accept-license.
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 |
| Memory-Mapped Reader | mmap_reader.hpp |
Zero-copy access for large sample libraries |
| Offline Processor | offline_processor.hpp |
offline_process(input, callback, 512) — batch render |
| 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) |
midi¶
MIDI I/O, file handling, and MIDI 2.0 support.
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 |
| Files | midi_file.hpp |
Read/write Standard MIDI Files |
| Messages | via CHOC | ShortMessage::noteOn(0, 60, 100) |
| 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. All operate on single samples or buffers. All are safe for 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¶
#include <pulp/signal/convolver.hpp>
PartitionedConvolver conv;
conv.init(impulse_response.data(), ir_length, block_size);
// In your process callback:
conv.process(input, output, block_size);
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 up/downsampling with anti-alias filtering for nonlinear processing |
| 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 |
Smoothed gain stage that ramps between values to avoid clicks |
| 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 |
Wavetable oscillator with sine, saw, square, triangle waveforms |
| Spectrogram | spectrogram.hpp |
Rolling time-frequency analysis for visual display of spectral content |
| STFT | stft.hpp |
Short-time Fourier Transform for overlap-add spectral processing |
| 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 or exponential parameter smoothing to prevent zipper noise |
| Special Functions | special_functions.hpp |
sinc, Bessel, dB↔linear, MIDI note↔frequency conversions |
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;
auto gain_id = store.add_param({.name = "Gain", .min = -60, .max = 12, .default_val = 0});
auto mix_id = store.add_param({.name = "Mix", .min = 0, .max = 1, .default_val = 1});
// Audio thread reads atomically (no locks)
float gain = store.get(gain_id);
// UI thread writes with gesture grouping for undo
store.begin_gesture(gain_id);
store.set(gain_id, -6.0f);
store.end_gesture(gain_id);
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
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() |
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", .vendor = "MyCompany", .uid = "com.myco.gain"};
}
void define_parameters(state::StateStore& store) override {
gain_id_ = store.add_param({.name = "Gain", .min = -60, .max = 12});
}
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(store().get(gain_id_));
for (int ch = 0; ch < audio_output.num_channels(); ++ch)
for (int i = 0; i < audio_output.num_frames(); ++i)
audio_output[ch][i] = audio_input[ch][i] * gain;
}
};
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 (stub) |
| 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. |
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 |
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::rgba(30, 30, 40));
canvas.fill_rounded_rect(0, 0, width, height, 8);
// Gradient fill
canvas.set_fill_gradient(LinearGradient{0, 0, 0, height,
Color::rgba(80, 120, 255), Color::rgba(40, 60, 180)});
canvas.fill_rect(10, 10, width - 20, 4);
// Text
canvas.set_fill_color(Color::rgba(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 |
| Effects | effects.hpp |
Drop shadow, bloom, blur, color adjustment |
| 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 with median(r,g,b) sampler for sharp corners at extreme zoom (in-house median-of-three generator). |
| PSDF text | psdf_atlas.hpp |
Pseudo-SDF variant with vector-fallback helper for extreme zoom. |
| SDF effects | sdf_effects.hpp |
Design-token presets for outline / shadow / glow / bevel over any SDF atlas (SkSL effect module). |
| 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 (Phase 5). |
view¶
Full widget toolkit with CSS-inspired layout and JS scripting.
Link: pulp::view · Include prefix: <pulp/view/...>
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(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 selection, copy/paste, undo |
| Toggle | Two-state switch control for enabling/disabling features |
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.set("bg.surface", Color::rgba(25, 25, 35));
theme.set("control.accent", Color::rgba(80, 130, 255));
theme.set("text.primary", Color::rgba(220, 220, 230));
// Widgets resolve tokens automatically:
canvas.set_fill_color(resolve_color("bg.surface"));
JS-scripted UI¶
Write your entire plugin UI in JavaScript with hot-reload during development:
// 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>
// Send
Sender sender;
sender.connect("192.168.1.100", 9000);
sender.send("/synth/freq", 440.0f);
sender.send("/synth/gate", 1);
// Receive
Receiver receiver;
receiver.bind(9000);
receiver.on_message = [](const Message& msg) {
if (msg.address == "/synth/freq")
set_frequency(msg.args[0].as_float());
};
Supports bundles with timetags and address pattern matching (*, ?, [...], {a,b}).
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 |
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 (macOS) or NSIS installer (Windows)
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 |