Search guides and the APICtrl K
  1. API reference
  2. Plugin model

audio.h

Core audio types. Our own, dependency-free equivalents of the framework types generated DSP code is written against. The authoring contract stays what it has always been:

#include <sonore/audio.h>
On this page

Design notes

struct SonoreDsp {
  void prepare(const sonore::ProcessSpec& spec);
  void process(sonore::AudioBlock<float>& io, const float* p);            // effect
  void process(sonore::AudioBlock<float>& io, const float* p,
               sonore::MidiBuffer& midi);                                 // instrument
  void reset();                                       // optional, see below
};

prepare() is where buffers are sized, but it is not only called from the main thread. A host's "start over" -- CLAP's reset(), an Audio Unit's Reset -- comes on the AUDIO thread with the rate and block size unchanged. Give the DSP a reset() that forgets what is sounding and keeps what prepare() sized, and those resets call it instead. Without one they call prepare() again, which must then neither allocate nor free when nothing it sizes from has changed. An LV2 host's switch into or out of freewheeling also comes on the audio thread, and calls prepare() whether or not there is a reset(), because spec.offline is what changed and LV2 offers no other moment to say so; the same rule holds there. A DSP taking ProcessContext also has ctx.offline, block by block.

Everything here is allocation-free and real-time safe: AudioBlock is a view over host-owned channel pointers, MidiBuffer is a fixed-capacity inline array. The same header compiles under MSVC/clang/gcc AND emscripten, so the browser preview and the shipped native plugin run the identical DSP source.

Types 12

What prepare() learns about the host: rate, worst-case block, channels.

template <typename SampleType> class AudioBlock

A non-owning view over planar channel data. The host owns the buffers; the DSP reads and writes through the channel pointers in place.

One short MIDI message -- a status byte and up to two data bytes -- as a DSP reads it out of a MidiBuffer.

Mirrors the reference's MidiMessage for the channel messages a plugin acts on, and only those: SysEx travels beside these in MidiBuffer's arena, never inside one. A plain value of three bytes, free to make and copy on the audio thread.

The channel numbering is not symmetric, and the README says so as well: the factories (noteOn, controlChange and the rest) take a channel from 0 to 15, while getChannel() answers 1 to 16, as MIDI is written -- noteOn(m.getChannel() - 1, ...) is the round trip. Data bytes are masked to 7 bits on the way in, so an out-of-range note number wraps instead of spilling into the status byte. A note-on with velocity 0 is a note-off, to isNoteOff() as on the wire.

Fixed-capacity, sample-ordered event list for one processing block. Iterate with a range-for; each entry carries the message + frame offset.

One block's expression events, sample-ordered like MidiBuffer.

A DSP correlates these with its voices by KEY plus CHANNEL: MPE guarantees one note per member channel, and CLAP hosts fill both fields even when they also carry a note id. Matching on the pair is what works everywhere.

One Registered or Non-Registered Parameter, once it has been assembled.

Assembles RPN and NRPN messages out of the control changes that carry them.

There is no such thing as an "RPN message" on the wire. A controller spells one across up to four separate control changes -- 101 and 100 select the parameter (99 and 98 for the non-registered kind), then 6 and 38 carry the value -- and a plugin that only looks at individual CCs sees four unrelated knob movements instead of one instruction.

This matters here for one reason above all others: RPN 0 is PITCH BEND SENSITIVITY, and it is how an MPE controller announces the bend range it is using. MpeDecoder assumed 48 semitones, which is the MPE default and is simply wrong for any player who has configured their instrument differently -- a controller set to +/-2 would have been bent twenty-four times too far, silently, with every note.

The value is reported TWICE where a controller sends both halves: once when the MSB arrives (7-bit, because that may be all there is) and again when the LSB follows (14-bit). A caller that only wants the precise one checks is14Bit; one that wants to respond immediately uses the first.

Which host is running this plugin.

Nobody wants to need this and everybody ends up needing it. Hosts have bugs and quirks that a plugin cannot fix and has to work around -- one that calls process() before activate, one whose automation lane cannot show a stepped control, one that reports a block size it then exceeds. A plugin that cannot tell them apart has to apply every workaround to everybody, and each workaround is a small wrongness inflicted on the hosts that never needed it.

It is also the honest way to say "this needs a newer host" instead of failing in a way the user has to guess at.

Empty strings are a normal answer. LV2 has no way for a host to introduce itself, and a plugin that assumes a name will find one missing.

The track this instance was dropped onto.

The most visible use is cosmetic and the reason people ask for it: a plugin that tints its own face to the colour of the track it sits on, so a rack of eight identical compressors is eight distinguishable compressors. It is worth more than that at CREATION time, though, which is when a host normally sends it -- a reverb landing on a return track wants to start at 100% wet, and one landing on a channel does not, and guessing wrong means every user's first action is to undo the default.

Every field is optional, separately. hasName and hasColour exist because "the host did not say" is a different answer from "" and from black, and a plugin that cannot tell them apart paints itself black on hosts that simply never mention colour.

Not every format can carry this. CLAP has an extension for it, VST3 has IInfoListener, and LV2 has nothing at all -- so an LV2 build is told nothing, forever, which is the reason its fields stay false rather than a bug in the host.

An MPE zone: which channel is the MASTER, and how many members follow it.

MPE splits a controller's sixteen channels into a zone with one master channel and a run of member channels. A note lives on a member channel and its bend, pressure and timbre are that finger's alone. The master channel carries the same three things for the ZONE -- the player's whole hand pushing, a global bend, the sustain pedal -- and they apply to every note sounding in it.

That distinction is not decoration. A controller sending a global bend on its master channel has no note on that channel, so a decoder that treats every channel alike drops the message on the floor.

A zone is announced with the MPE Configuration Message: RPN 6 on channel 1 for the lower zone or channel 16 for the upper, valued with the number of member channels. Zero turns the zone off, which is also how a controller says "I am an ordinary MIDI instrument now".

Turns MPE's channel-per-note MIDI into expression events.

MPE spells per-note control as ordinary MIDI on a member channel: pitch bend is that note's tuning, channel pressure is its pressure, CC74 its brightness. The decoder tracks which notes sound on which channel, because the bend arrives with no note number attached -- that mapping IS the protocol. A channel message reaches EVERY note sounding on its channel: one under MPE, a whole chord from a plain keyboard. Pitch bend range defaults to MPE's own ±48 semitones, not a keyboard's ±2.

template <typename SampleType> struct ProcessContextT

Everything one process() call is given, in one object.

The simple signatures -- process(block, params), (block, params, midi), (block, sidechain, params) -- stay the common case and are unchanged. A DSP that needs EXTRA OUTPUT BUSES (a drum sampler routing pads to their own channels, a band splitter, a synth emitting stems) declares

void process(sonore::ProcessContext& ctx, const float* params);

instead, and gets the main bus, the aux buses, the sidechain and MIDI together. One signature rather than eight: aux-outs times MIDI times sidechain would otherwise be a combinatorial explosion of traits.

Aux buses are ALWAYS present in the object -- a host that left one unconnected gets a zero-width block rather than a null pointer, so a DSP writing to it needs no host-specific checks. Check getNumSamples() before writing if the DSP cares.

Functions 12

Aliases 1

Enums 2

  • enumenum NoteExpression : uint8_t
    Per-NOTE expression. A keyboard sends one pitch bend for the whole instrument; an MPE controller (Osmose, LinnStrument, Seaboard) bends, presses and brightens each finger independently. The ids are CLAP's.
    audio.h:645
  • enumenum ChannelRole : uint8_t
    What a channel MEANS. Width alone is not enough: a limiter that ducks the LFE like a full-range channel, or a panner that treats side as back, is wrong in a way no amount of testing at stereo reveals.
    audio.h:1386

Constants 2

↑↓ to move↵ to openEvery public name in the headers, and every guide