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>- 12types
- 17other declarations
- Guides: Getting started, Parameters and state, Presets
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
- fn
inline bool isDataByte(int status)Anything below 0x80: a payload byte, not a message. Named because the parsers ask this constantly and a bareaudio.h:227< 0x80is the kind of comparison that gets copied with its meaning left behind. - fn
inline bool isChannelMessage(int status)0x80..0xEF -- note on, note off, control change, pitch bend and the rest. The low nibble is the channel.audio.h:231 - fn
inline bool isSystemRealtime(int status)0xF8..0xFF -- clock, start, continue, stop, active sensing, reset.audio.h:239 - fn
inline bool isSystemCommon(int status)0xF1..0xF7 -- song position, song select, MTC quarter frame, tune request, and the SysEx terminator.audio.h:243 - fn
inline bool isSysexStart(int status)0xF0 -- the start of a System Exclusive message.audio.h:251 - fn
inline int shortMessageLength(int status)audio.h:253 - fn
inline bool deliverableToDsp(int status)Whether a three-byte MIDI event should be handed to a DSP.audio.h:265 - fn
inline void forwardSysex(const MidiBuffer& in, MidiBuffer& out)Copy every SysEx message from one block's MIDI to another's.audio.h:636 - fn
inline void sendRpn(MidiBuffer& out, int channel, int parameterNumber, int value, bool isNrpn = false, bool is14Bit = false, int samplePosition = 0)The other direction: an RPN or NRPN, spelled out as the control changes that carry it.audio.h:843 - fn
inline void sendPitchBendRange(MidiBuffer& out, int channel, int semitones, int cents = 0, int samplePosition = 0)Announce a pitch bend range, which is RPN 0. The message an instrument sends to say how far its bend wheel travels -- and the one MpeDecoder reads on the way in.audio.h:870 - fn
inline uint64_t defaultChannelMask(uint32_t width)The layout the industry means by "N channels", as a role bitmask. Hosts may propose others; this is only what WE offer when nobody said otherwise.audio.h:1412 - fn
inline uint32_t rolesFromMask(uint64_t mask, uint8_t* out, uint32_t capacity)Channel order follows ASCENDING role: channel 0 is the lowest set bit. Both CLAP and VST3 order their channels this way, so one rule serves both. Writes at mostaudio.h:1442capacityroles and returns how many it wrote.
Aliases 1
- using
using ProcessContext = ProcessContextT<float>The ProcessContext of 32-bit float processing, the one a DSP declares inaudio.h:1545process(sonore::ProcessContext& ctx, const float* params).
Enums 2
- enum
enum NoteExpression : uint8_tPer-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 - enum
enum ChannelRole : uint8_tWhat 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
- const
constexpr float kPi = 3.14159265358979323846fPi, as a float: half a turn in radians.audio.h:46 - const
constexpr float kTwoPi = 6.28318530717958647692fTwo pi, as a float: one whole turn in radians, the factor that turns a frequency in cycles into a phase increment in radians.audio.h:49