Our Community Plugin Format

DSP, Plugin and Host development discussion.
Locked New Topic

Poll 1 - Let's give it a name (Acronym)

HOT Plugins
0
No votes
WAR Plugins
0
No votes
TOP Plugins
0
No votes
OUR Plugins
0
No votes
PRO Plugins +1 Point
1
3%
EVE Plugins
3
10%
ION Plugins
3
10%
IVY Plugins
2
7%
MAN Plugins +1 Point
0
No votes
WTF Plugins +2 Points
1
3%
KVR Plugins (permission issue?)
2
7%
DIY Plugins
1
3%
COP Plugins
1
3%
API Plugins (Amazing Plugin Interface)
0
No votes
TPIA Plugins (This Plugin is Amazing)
0
No votes
OPI Plugins (Open Plugin Interface)
8
27%
OPS Plugins (Open Plugin Standard)
8
27%
 
Total votes: 30

RELATED
PRODUCTS

Post

The main issues that come to mind with a sample rate change are having, potentially, lots of plugins simultaneously recalculating their internal states at the same time. Plus the execution time for relocating memory is unpredictable and you may have a number of plugins wanting to resize buffers. As long as this is done outside of playback it's not a problem. I think setSampleRate() like VST makes sense and doesn't require the tearing down and rebuilding of all the plugins. Plus as someone already mentioned most devs are probably already comfortable with that way of doing things

Post

camsr wrote: Tue Jul 07, 2020 6:31 pm I wonder how sampling rate changes might be handled. Would plugins have to be reloaded, or could it be done with minimal interference.
Tell plugin to stop processing, give it a new I/O configuration, tell plugin to start processing again. The whole suspend/resume cycle isn't necessarily important for the average software plugin, but it could be useful when some external hardware or another plugin API is involved.

IMHO setting the whole I/O configuration in bulk also makes more sense than having a bunch of different methods for setting different options, since more often than not the plugin ends up having to reconfigure everything anyway, whether it's the sampling rate or the block size or the number of I/O channels that changed.

Post

Community.h has been added to the repository.

Github:
https://github.com/The-Originator/CommunityPlugin

Please contribute changes and add your own ideas to the code.

Also,
I added the branding poll to create intrigue. Three-letter naming acronyms, I went through every word I could find and thought how it would look on a plugin sales page. I figured 3 letters is best, considering ease of remembering, file extension, and imagery and emotion it creates for customers. For example, if someone asked if you have "BIG plugins", it creates an impression of size, awe, stability, and strength. Whereas if it were " POP Plugins", it might sound fresh, but could also signify pops and clicks. What's in a name?
SLH - Yes, I am a woman, deal with it.

Post

DIY Plugins :P

Post

COP - Community Owned Plugin Format

Post

ThisPluginIsAmazing (TPIA)
Free MIDI plugins and other stuff:
https://jstuff.wordpress.com
"MIDI 2.0 is an extension of MIDI 1.0. It does not replace MIDI 1.0(...)"

Post

Great you're already dealing with marketing names and icons and whatsoever. But there's only 1 line of code right now. Maybe start fixing this first :lol:

Post

umd wrote: Wed Jul 08, 2020 10:59 am ThisPluginIsAmazing (TPIA)
Let's call it Amazing Plugin Interface (API). I'm sure this would not cause any confusion whatsoever.

Post

RobinWood wrote: Wed Jul 08, 2020 11:11 am Great you're already dealing with marketing names and icons and whatsoever. But there's only 1 line of code right now. Maybe start fixing this first :lol:
The name is required to name things. E.g. adding prefixes to structs, enums, etc.

Post

RobinWood wrote: Wed Jul 08, 2020 11:11 am Great you're already dealing with marketing names and icons and whatsoever. But there's only 1 line of code right now. Maybe start fixing this first :lol:
I don't think that there is a lack of ideas about what to put in that header.

What is missing most is the momentum, a feel that something is taking off. A remarkable name and a fancy logo is a good way to visualize that this new format is indeed a real thing.

Post

Yesterday I figured I'd try to draft a quick interface that would do the basics of what I would usually want to support and called it "OPI" for "Open Plugin Interface" since the whole naming discussion hadn't even started yet.

I'm not going to suggest this is any good. I'm not going to suggest it isn't missing stuff (since it most definite is missing a bunch of stuff). I'm not going to suggest you actually implement this (since I didn't even try to implement it myself yet), but in theory it should probably work and I highly doubt anyone will be able to come up with anything significantly more simple.

I don't have high hopes for the community effort (ie. part of why I did this is just because I need a private API), but if you want to use this as a base, then feel free to do so. There might be some errata required with regards to things like struct-packing, but I'm reasonably confident I should have caught at least most of the hazards. If something seems odd, feel free to ask for more rationale.

Code: Select all


#pragma once

#include <stdint.h>

#ifdef __cplusplus__
extern "C" {
#endif

enum OpiEventType
{
    opiEventMidi,
    opiEventAutomation
};

// event-type support flags, for future extensibility
static const uint32_t   opiEventWantMidi        = 1<<0;
static const uint32_t   opiEventWantAutomation  = 1<<1;

// common fields for all events
struct OpiEvent
{
    uint32_t    type;   // OpiEventType
    uint32_t    delta;  // delta frames from start of block
};

// This is for simple MIDI commands like note-on/off, CC etc.
// Most current plugins (or frameworks) have to support this stuff
// anyway and higher-level note control could be added as separate
// event types at a later point in time (rather than bloating this).
struct OpiEventMidi
{
    uint32_t    type;   // OpiEventType
    uint32_t    delta;  // delta frames from start of block
    
    uint8_t     data[4];
};

struct OpiEventAutomation
{
    uint32_t    type;   // OpiEventType
    uint32_t    delta;  // delta frames from start of block

    uint32_t    paramIndex;
    float       targetValue;
    
    uint32_t    smoothFrames;   // frames to interpolate over (0 = snap)
};

// OpiTimeInfo flags: bitwise OR together
static const uint32_t   opiTimeTransportPlaying = 1<<0;
static const uint32_t   opiTimeSamplePosValid   = 1<<1;
static const uint32_t   opiTimePpqPosValid      = 1<<2;
static const uint32_t   opiTimeTempoValid       = 1<<3;

struct OpiTimeInfo
{
    uint32_t    infoSize;       // = sizeof(OpiTimeInfo) for future extensions
    uint32_t    flags;
    
    uint64_t    samplePos;      // sample position
    
    double      ppqPos;         // song position in quarter notes
    double      tempo;          // tempo in beats per minute
    
    // FIXME: time signature info?
};

// Prototype for the host and plugin dispatcher callbacks.
//
// While the general idea is to pass any parameters as an operation specific struct
// many operations required an index and promoting this to an explicit argument
// reduces the amount of special case structures that are required.
//
// For operations where the index doesn't make sense, it should be set to 0.
typedef intptr_t (*OpiCallback)(struct OpiPlugin *, int32_t op, int32_t idx, void *);

// The main plugin struct can be very simple; plugins can "derive" from this
// structure by adding whatever fields they need after the ones defined here.
struct OpiPlugin
{
    OpiCallback dispatchToHost;     // host dispatcher callback
    OpiCallback dispatchToPlugin;   // plugin dispatcher callback

    void *  ptrHost;                // host private pointer
};

// opcodes for dispatchToHost (void* parameter type in parenthesis)
//
enum OpiHostOps
{
    opiHostParamState,  // set parameter automation state, 1 = editing (uint32_t *)
    opiHostParamValue,  // send parameter automation, normalized [0,1] (float *)

    opiHostPatchChange, // notify the host that all past state should be flushed
    opiHostResizeEdit,  // resize editor, call once at init (struct OpiEditSize *)

    opiHostSetLatency,  // request that host refresh opiPlugGetLatency
};

struct OpiEditSize
{
    uint32_t    w;  // width in pixels (logical pixels for Retina, etc)
    uint32_t    h;  // height in pixels (logical pixels for Retina, etc)
};

// opcodes for dispatchToPlugin (parameters in parenthesis)
//
// The plugin should always return 0 for unknown or unimplemented opcodes
// and 1 for those it implements, unless otherwise indicated below.
//
// Operations marked [RT] are considered part of the "real-time context" and
// must not be called concurrently with each other, but ONLY opiPlugProcess
// is required to be "real-time safe" in the usual sense.
//
// Operations marked [UI] are considered part of the "interactive context" and
// must not be called concurrently with each other.
//
// Operations marked [ANY] can be called concurrently with everything else
// (eg. two opiSetParam calls to the same parameter concurrently are valid)
//
// Finally opiPlugDestroy must never be called while any other call active.
//
// opiPlugConfig is only valid while a plugin is in disable state (the default)
// and opiPlugProcess/opiPlugReset are only valid while a plugin is in enable state.
//
// opiPlugEnable implies a full reset, so opiPlugReset is only indended for use when
// the host wants the plugin to reset, but processing will continue immediately
//
enum OpiPlugOps
{
    opiPlugProcess,     // [RT] process (struct OpiProcessInfo)
    opiPlugDestroy,     // plugin should deallocate itself

    opiPlugNumInputs,   // [ANY] return the number of input busses
    opiPlugNumOutputs,  // [ANY] return the number of output busses

    opiPlugMaxChannels, // [ANY] return the maximum  number of channels (per bus)
    opiPlugInEventMask, // [ANY] return mask of event-types the plugin wants
    opiPlugOutEventMask,// [ANY] return mask of event-types the plugin will generate

    opiPlugGetLatency,  // [ANY] return current latency
    
    opiPlugConfig,      // [RT] configure processing parameters (struct OpiConfig)
    opiPlugReset,       // [RT] reset plugin state, but continue processing
    
    opiPlugEnable,      // [RT] start processing
    opiPlugDisable,     // [RT] stop processing

    opiPlugOpenEdit,    // [UI] open editor (platform HWND, NSView, etc)
    opiPlugCloseEdit,   // [UI] close editor

    opiPlugSaveChunk,   // [UI] save state into a chunk (struct OpiChunk*)
    opiPlugLoadChunk,   // [UI] load state from a chunk (struct OpiChunk*)

    opiPlugNumParam,    // [ANY] return number of parameters
    opiPlugGetParam,    // [ANY] get the parameter value (float *)
    opiPlugSetParam,    // [ANY] set the parameter value (float *)

    opiPlugGetParamName,    // [UI] get the name of the parameter (struct OpiString *)
    
    opiPlugValueToString,   // [UI] convert value to string (struct OpiParamString *)
    opiPlugStringToValue,   // [UI] convert string to value (struct OpiParamString *)

    opiPlugGetPatchName,    // [UI] get current patch name (struct OpiString *)
    opiPlugSetPatchName,    // [UI] set current patch (struct OpiString *)
};

// each logical bus is a collection of channels
// the number of channels must be set by calling opiPlugConfig (see below)
//
// All the buffers are allocated by the host.
//
// If a channel is completely silent (all zeroes) then a bit in the silenceMask
// can be set (by host for inputs and plugin for outputs) to indicate that
// processing this channel is not necessary (ie. soft-bypass is possible).
//
// Note that the buffer must still be cleared (ie. the flag is just a hint).
//
// The host can also set the silenceMask for outputs before a process call to
// indicate that the contents are already zero and the plugin need not clear
// them explicitly if it only wants to output silence.
//
// RATIONALE: The ability (of both host and plugin) to skip completely zero
// buffers gives most of the performance benefits of other soft-bypass schemes,
// while still leaving the plugin in full control over it's own processing and
// the additional complexity is very minor (eg. the plugin can just always set
// silenceMask to zero for all the outputs if it doesn't care about any of this).
//
struct OpiBusChannels
{
    uint64_t    silenceMask;    // bitmask of fully silent channels (1 = silent)
    float       *channels[];
};

// The host must provide the events in order sorted by delta-time and in case of
// identical delta times, by logical ordering: the event placed first in the queue
// is considered to happen "first" even though both happen during the same frame.
//
// RATIONALE: We require the list of events to be sorted, because it is typically
// much easier to maintain such a list in sorted order (or merge multiple sorted
// lists), rather than to explicitly sort when no such guarantees are provided.
//
// If plugin wants to send outbound events, it should set the outEvents pointer
// and the number of events to non-zero values.
//
struct OpiProcessInfo
{
    uint32_t    processInfoSize; // = sizeof(OpiProcessInfo) for future extensions
    uint32_t    nFrames;
    
    struct OpiTimeInfo * timeInfo;  // pointer to time info

    OpiBusChannels  *inputs;        // pointer to array of OpiBusChannels
    OpiBusChannels  *outputs;       // pointer to array of OpiBusChannels

    OpiEvent        **inEvents;     // pointer to an array of pointers to events
    OpiEvent        **outEvents;    // pointer to an array of pointers to events
    
    uint32_t        nInEvents;      // number of input events
    uint32_t        nOutEvents;     // number of output events
};

struct OpiBusConfig
{
    uint32_t    nChannels;      // number of channels (0 = disconnected)
};

// the plugin must be in disabled (initial) state when opiPlugConfig is called
//
// it is NOT required that a plugin supports any given channel configuration,
// but at bare minimum all plugins should support nChannels == 2 for each bus
//
// if the plugin returns 0, then the host must retry with another configuration
//
// RATIONALE: Since different busses can potentially have dependencies on each
// other in terms of number of channels, yet the plugin might still support a
// large number of different configurations (only few of which are likely to be
// relevant in any given situation) it seems to make the most sense to just let
// the host try the applicable configurations one by one in order of preference.
//
struct OpiConfig
{
    uint32_t    configSize; // = sizeof(OpiConfig) for future extensions
    
    uint32_t    blocksize;
    float       samplerate;

    uint32_t    busConfigSize;  // = sizeof(OpiBusConfig) for future extensions

    OpiBusConfig *inBusChannels;    // array of bus configurations
    OpiBusConfig *outBusChannels;   // array of bus configurations
};

// This is passed by host to both opiPlugSaveChunk and opiPlugLoadChunk
// but for opiPlugSaveChunk the plugin sets the pointer and the data size and
// the buffer remains valid until next [UI] context call to dispatcher.
struct OpiChunk
{
    void *      data;
    uint32_t    size;
};

// This is used for operations that get or set strings. The pointer and size
// are always filled by the party that provides the contents and must remain
// valid until the next [UI) context dispatcher call.
// All strings must use UTF-8 encoding.
struct OpiString
{
    char *      data;
    uint32_t    size;
};

// This is used for converting between parameter values and strings.
// Buffer must remain valid until next [UI] context dispatcher call.
//
// RATIONALE: There are various situations where the host might want to convert
// between parameter values and strings, eg. when editing automation curves
// without modifying the actual parameter state of the plugin. As such, it makes
// the most sense to simply specify such conversions as operations separate from
// everything else.
struct OpiParamString
{
    char *      data;
    uint32_t    size;
    float       value;
};

// Plugin entry point
#ifndef DLLEXPORT
# ifdef _WIN32
#  define DLLEXPORT __declspec(dllexport)
# else
#  define DLLEXPORT __attribute__((visibility("default")))
# endif
#endif

DLLEXPORT OpiPlugin * OpiPluginEntrypoint(OpiCallback hostCallback)
{
    return 0;
}

#ifdef __cplusplus__
} // extern "C"
#endif
Errata: String conversions should be limited to [UI] context for memory management clarity. The struct for editor size was missing.
Last edited by mystran on Wed Jul 08, 2020 2:08 pm, edited 1 time in total.

Post

Nice work @mystran. I didn't look at it thoroughly but few things that come to my mind:
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiBusChannels
I feel like number of channels should also be a part of this struct.
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiBusConfig
When nChannels is more than 2, it is not clear what channel each channel buffer should represent. There have to be some extra layout type marker. VST3 does not operate with number of channels, speaker arrangement only, from which the number of channels can be figured out.
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiChunk
the buffer must remain valid until the next call to the plugin dispatcher
There is a problem with that lifetime rule since the dispatcher can be called from different threads.

Post

Vokbuz wrote: Wed Jul 08, 2020 2:06 pm Nice work @mystran. I didn't look at it thoroughly but few things that come to my mind:
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiBusChannels
I feel like number of channels should also be a part of this struct.
That was my initial thought as well, but the rationale for removing it was that this creates a redundancy and then you basically end up with "undefined behaviour" (unless you somehow specify it) when the channel counts from the previous configuration don't match the channel counts provided for processing. IMHO it is simply more clear that the information is provided exactly once, in exactly one place and there is no ambiguity whatsoever. It is also unreasonable to expect a plugin to deal with the channel counts suddenly changing without warning.
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiBusConfig
When nChannels is more than 2, it is not clear what channel each channel buffer should represent. There have to be some extra layout type marker. VST3 does not operate with number of channels, speaker arrangement only, from which the number of channels can be figured out.
You are correct, this is somewhat of a "placeholder" design. One possibility would be to specify an enumeration of all the known channel layouts and let the plugin return a list of those layouts that it is aware of, then replace the "nChannel" field with values from said enumeration. On the other hand, as far as I can see there is also some value in being able to have "generic" multi-channel busses with no particular layout. In any case, that particular aspect of my design is "intentionally underspecified."

edit: I want to clarify that there are some other aspects in the draft that could also use some work, but my goal was not to come up with a perfect design, but rather just a design that would be complete enough that you could theoretically implement it right now if you wanted to.

edit2: I feel like the sensible thing to do would be to add a separate field that specifies how the channel are to be interpreted (eg. with value 0 reserved for "unspecified") such that both arbitrary channel bundles and specific layouts would be possible, so that plugins that don't care could still choose to process arbitrary channel counts, yet the information would be there for when it is important.
mystran wrote: Wed Jul 08, 2020 1:43 pm OpiChunk
the buffer must remain valid until the next call to the plugin dispatcher
There is a problem with that lifetime rule since dispatcher can be called from different threads.
I already kinda fixed that problem above, before reading your reply. The intended specification (for both chunks and strings) is that the buffers remain valid until the next [UI] context dispatch call (and not [ANY] as it was by accident). While there is no mention of "threads" as such, there is an explicit requirement that no two calls are made to the same "context" concurrently (ie. even if you had 2 "UI threads" only one of them is allowed to call into the "UI" context at a time; same with those specified as RT).

edit: Beyond simple "getters" only the parameter (raw value) access is specified as [ANY] and this is because forcing serialization of parameter access would potentially introduce a lot of complexity on the host side, yet dealing with thread-safe here is reasonably easy for a plugin (and often necessary internally anyway).

The intention is that you can output your chunks and strings into something like std::vector<char> (defined as a fixed member of the higher-level plugin wrapper) and just fill in the .size() and .data() into the struct, then reuse the same vector (or whatever) on the next call that needs to return such data. If the host wants to keep a copy, it should make one explicitly. As far as I can see, this sort of memory management shouldn't be particularly hard in any other (non-C++) language either.

Post

@mystran, great effort. The sad thing though, is that most of this could have been done with extension to the perfectly working vst2 api.
At least thank you for proving that a working plugin interface doesn't need to have a million files like the insanities that are VST3, AU or AAX

Post

RobinWood wrote: Wed Jul 08, 2020 11:11 am Great you're already dealing with marketing names and icons and whatsoever. But there's only 1 line of code right now. Maybe start fixing this first :lol:
While the plugin format does not have a name yet there's at least a name for that: it's called bikeshedding. ;)
Passed 303 posts. Next stop: 808.

Locked

Return to “DSP and Plugin Development”