Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
osc_parameter_map.hpp File Reference

Bind incoming OSC messages to plugin parameters, with a "learn" mode — the OSC counterpart of pulp::state::MidiParameterMap. More...

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string_view>
#include <variant>
#include <pulp/osc/bundle.hpp>
#include <pulp/osc/osc.hpp>
#include <pulp/runtime/spsc_queue.hpp>
#include <pulp/state/store.hpp>

Go to the source code of this file.

Classes

struct  pulp::osc::OscMapScale
 The incoming value range an OSC surface sends, and the normalized [0, 1] window it drives. More...
class  pulp::osc::OscParameterMap

Namespaces

namespace  pulp
namespace  pulp::osc

Variables

constexpr std::size_t pulp::osc::kOscPatternCapacity = 96
 Longest OSC address pattern a mapping can hold, including the terminator.

Detailed Description

Bind incoming OSC messages to plugin parameters, with a "learn" mode — the OSC counterpart of pulp::state::MidiParameterMap.

A tablet control surface (TouchOSC, Lemur, Open Stage Control) or any OSC-speaking script reaches the same parameter space a MIDI CC does, with no per-surface driver code.

Target identity: a mapping stores a pulp::state::ParamID. That is the same stable 32-bit id a timeline DeviceParameterTarget carries in its param_id field, so an address bound here lands on exactly the parameter the timeline would automate — OSC, MIDI, and authored automation share one target space.

Thread model: mappings are added / armed from the UI thread and consumed on the thread that dispatches OSC messages (typically the receiver's socket thread). UI calls go through a lock-free command queue; the dispatching thread drains it with pump() and then routes each message through handle_message(). The parameter write itself lands as an atomic store the audio thread picks up on its next read, and the format adapter's post-process diff reports the change to the host as automation.

Which store write: handle_message() / handle_value() use the non-RT set_normalized(), because an OSC receiver runs on its own thread and StateStore's RT path (set_normalized_rt) feeds a queue with a single producer — the audio thread. A processor that would rather apply inside process() (draining its own OSC queue there) uses route() and calls set_normalized_rt from the audio thread itself, keeping that contract.

Address matching: a mapping's pattern is either a literal OSC address or an OSC 1.0 address pattern (*, ?, [...], {...}). Literal patterns are matched by direct byte compare and allocate nothing; wildcard patterns go through pulp::osc::address_matches, which is not allocation-free and must not be driven from the audio thread. Patterns and mappings are held in fixed-capacity storage, so installing and dispatching a binding never grows the heap.

Value scaling: OSC arguments are unbounded floats, so a mapping carries an OscMapScale describing both the incoming range the surface sends (in_min..in_max) and the normalized [0, 1] window it drives (out_min..out_max). Input outside the incoming range is clamped, so a misconfigured sender can never push a parameter past its bounds, and out_min > out_max inverts the response. The default is the identity map for the conventional 0..1 fader: 0 → 0, 1 → 1.

Usage: osc_map_.set_mapping("/track/1/fader", kGain); osc_map_.set_mapping("/track/*&zwj;/mix", kMix, {0.0f, 1.0f, 0.25f, 0.75f}); osc_map_.arm_learn(kCutoff); // next message binds to kCutoff and on the dispatching thread: osc_map_.pump(); osc_map_.handle_message(state(), msg);