Our Community Plugin Format
-
- KVRian
- 1273 posts since 9 Jan, 2006
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
- KVRAF
- 7892 posts since 12 Feb, 2006 from Helsinki, Finland
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.
- Banned
- Topic Starter
- 697 posts since 29 Oct, 2016
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?
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.
-
- KVRian
- 537 posts since 23 Jan, 2008 from Hamburg, Germany
COP - Community Owned Plugin Format
-
- KVRian
- 1143 posts since 26 Feb, 2006 from Fartland
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(...)"
https://jstuff.wordpress.com
"MIDI 2.0 is an extension of MIDI 1.0. It does not replace MIDI 1.0(...)"
- KVRAF
- 7892 posts since 12 Feb, 2006 from Helsinki, Finland
Let's call it Amazing Plugin Interface (API). I'm sure this would not cause any confusion whatsoever.
- KVRist
- 243 posts since 24 Aug, 2014
-
- KVRian
- 537 posts since 23 Jan, 2008 from Hamburg, Germany
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.
- KVRAF
- 7892 posts since 12 Feb, 2006 from Helsinki, Finland
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.
Errata: String conversions should be limited to [UI] context for memory management clarity. The struct for editor size was missing.
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
Last edited by mystran on Wed Jul 08, 2020 2:08 pm, edited 1 time in total.
- KVRist
- 243 posts since 24 Aug, 2014
Nice work @mystran. I didn't look at it thoroughly but few things that come to my mind:
I feel like number of channels should also be a part of this struct.
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.
There is a problem with that lifetime rule since the dispatcher can be called from different threads.
- KVRAF
- 7892 posts since 12 Feb, 2006 from Helsinki, Finland
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.
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."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.
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.
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.
-
- KVRAF
- 3388 posts since 29 May, 2001 from New York, NY
@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
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
- KVRist
- 384 posts since 28 Nov, 2013 from Germany
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.