SDK Version 3 Documentation

The SynthEdit Music Plugin Standard (SEM for short) is an advanced API for MIDI and Audio plugins.  SEM Version 3 is a clean modern API designed for power and simplicity. SEM is easy to use yet more powerful than other plugin standards.

Download SEM V3 SDK

Support

For help with the SDK see the SDK Mailing List

For free synthesis and effects source code see the Music DSP Source Code Archive

For signal processing discussions see the KVR Forum - DSP and Plug-in Development

Code generator

To get you started quickly (or convert your old modules to the new SDK), SE includes a simple code generator. On your existing module... Right-click->Build Code Skeleton. This creates the template code for your module in the folder "Documents\SynthEdit Projects\".

The template doesn't do any processing, it's just a non-functioning module with the pins and module-name etc. setup for you, to save you typing the boring bits. NOTE - SE SDK3 does not support GUI DT_ENUM pins. Avoid upgrading any modules with those.

Plugin Properties

To save typing too much code, most of a SynthEdit module is described in an XML file. There are many example modules included in the SDK download to illustrate. Below are the supported fields.

  • id
  • name
  • category
  • helpUrl
  • graphicsApi{HWND,composited,none}
  • polyphonicSource{ true, false }
  • polyphonicAggregator{ true, false } - What it means is if you use say the Voltmeter on a polyphonic signal you will see the sum of all the voices. You will not get many voltmeters, 1 per voice.
  • cloned{ true, false }
  • voiceMonitorIgnore{ true, false } - SynthEdit 'sleeps' voice that are inactive (not playing a note). SynthEdit figures this out by monitoring all 'terminating' modules ( modules with inputs, but no outputs ). This is a confusing way of saying SynthEdit looks at the signals going to your I/O mod or sound-out or wave-recorder modules, when the voice goes silent, it's shut-down. Some modules like the Scope seem similar to a audio-output, it has inputs but no outputs. However this module is an exception to the rule, when you put the Scope on an Oscillator you never intend SynthEdit to treat that as a 'terminating' module (otherwise the voice would never ever sleep). So the Scope is flagged 'voiceMonitorIgnore' - i.e. don't monitor this module.
  • GUI
  • Parameters
  • Pins
Pin Properties
  • id
  • name
  • datatype {float, int, text, blob, midi, bool, enum (DSP only. not fully supported)}
  • default. For Audio pins default is not Volts, it's divided by 10. e.g. default of 0.4 represents 4 Volts.
  • direction {in, out}
  • rate {audio} - float pins only. Distinguishes streag audio pins from event-driven 'control signals' (float pins, int pins etc.).
  • private{ true, false }
  • autoRename{ true, false }
  • isFilename{ true, false } - Connecting a list-entry to this pin will show the 'browse' button.
  • linearInput{ true, false } SynthEdit can save CPU in some cases by sending two signals through the same module. For example imagine a polyphonic synth with a master volume control at the end of the signal chain. There are two possible topologies: A- One volume control per voice, then sum the voices together. B- Sum the voices, then apply one volume control. Option B uses less CPU. SynthEdit will use this optimization only if the module input is linear. Hence the 'linearInput' flag. A distortion or clipper module is not linear because summing the voices first results in a louder signal (and more clipping). These types of module can't use the optimisation because the end result is not the same.
  • ignorePatchChange{ true, false }
  • autoDuplicate{ true, false } - A pin that will replicate as needed (e.g. the Switch modules). Quite difficult to use on a GUI module. See project "Gui Autoduplicating Example" for details.
  • isMinimised{ true, false } - Show pin only on properties pane, not as a connectable pin.
  • isPolyphonic{ true, false }
  • autoConfigureParameter{ true, false } This goes hand in hand with the RangeMinimum/Maximum settings. Except this flag goes on the Sliders output pin. It indicates to SynthEdit that when you connect the Slider's output pin, the slider should be initialised to the destination pin's default value, and the Minimum and maximum range should be set to suit the destination pin.
  • parameterId
  • parameterField{ Value, writeable only in SynthEdit environment -ShortName , MenuItems,MenuSelection,RangeMinimum,RangeMaximum,EnumList,FileExtension
  • IgnoreProgramChange,Private,Automation,Automation Sysex,Default,Grab,Normalized }
  • metadata - For audio pins the format is ",,40,0". When user connects a slider, the sliders maximum and minimum Voltage is automatically set. In this example max=40, min = 0. SynthEdit does not enforce these limits, the user can override them. Only valid on input pins. The first two blank values are depreciated and ignored.
  • hostConnect{ PatchCommands, MidiChannelIn, ProgramNamesList, Program, ProgramName, Voice/Trigger, Voice/Gate, Voice/Pitch, Voice/VelocityKeyOn, Voice/VelocityKeyOff, Voice/Aftertouch, Voice/VirtualVoiceId, Voice/Active, VoiceAllocationMode, Bender, HoldPedal, Channel Pressure, Time/BPM, Time/SongPosition, Time/TransportPlaying }

Voice/Active

To save CPU SynthEdit suspends processing on unused voices. This can lead to a problem when the module is 'woken' to play a new note - it's inputs may have changed while the module was suspended. This can be due to the user changing patch, or because an LFO or envelope feeding the module has changed while the module was inactive. From the module's point of view, it's input has 'spiked' or 'stepped' suddenly, any kind of filter in this situation will ring for a short time before it settles. This is quite noticeable in SE synths with resonant filters as a click on the next few note-ons after changing patch.  SE has a specific signal for this situation, called Voice/Active. This is a reset signal that you can use to reset your module to an as-new state...to zero any filter history variables etc. This minimizes artifacts.  This reset signal is sent only on new notes when the voice has been suspended. It's NOT used in situations like mono-mode when one note is gliding into the next, because since the voice is not interrupted there's no need to reset any module. This avoids clicks.  This signal is not needed for most modules, it's mostly for modules with feedback, like filters or delays.

Values of Voice-Active signal:
1.0 - Voice playing a note.
0.0 - Voice shutting off (20ms fade).
0.5 - Voice has been 'overlapped' by another playing the same key.
-1.0 - Voice has been stolen and is shutting-off rapidly (5ms).

You can see it in use in the ADSR2 source code...

XML Pin Information
<Pin id="8" name="VoiceReset" direction="in" datatype="float" hostConnect="Voice/Active" isPolyphonic="true" />
.cpp file
void Envelope::onSetPins(void)
{
	bool forcedReset = pinVoiceReset.isUpdated() && pinVoiceReset != 0.0f;
	if( forcedReset )
	{
		// envelope must reset to zero.
	}
}

Sending data between Graphics (GUI) and Audio (DSP)

Communicating values between GUI and DSP is much the same as SDK2, except SDK3 requires less code.

The sender needs an output pin. The receiver needs an input. A Parameter provides the connection.

[DSP]->[PatchParameter]->[GUI]

An 'output' parameter sends from DSP->GUI. An 'input' parameter from GUI->DSP.

Example: Send a float from Audio to Graphics class.

Add one audio pin, one GUI pin, and one parameter to you XML:

<Plugin id="SE PatchMemory Float Out" name="PatchMemory Float Out3"category="Sub-Controls" >
<Parameters>
<Parameter id="0"datatype="float" direction="out"/>
</Parameters>
<Audio>
<Pin id="0"name="PM Value Out" direction="out" datatype="float" parameterId="0" />
</Audio>
<GUI>
<Pin id="0"name="PM Value In" direction="in" datatype="float" parameterId="0" />
</GUI>
</Plugin>

In you C++ code make the pins as per usual..(relevant lines only shown)..

DSP.

Declare the pin as usual.

class PatchMemoryFloatOut: public MpBase
{
FloatOutPin pinValueOut;
};

Initialize it as usual.

PatchMemoryFloatOut::PatchMemoryFloatOut(IMpUnknown* host) : MpBase(host)
{
initializePin( 0, pinValueOut );
}

Anytime you want to send a value to the GUI simply assign to the output pin....

pinValueOut = 123.0;
GUI.

Declare the output float pin.

class PatchMemoryFloatOutGui : public MpGuiBase
{
FloatGuiPin pinValueIn;
};

Initialize it in the constructor. Giving a member function to call when updates arrive.

PatchMemoryFloatOutGui::PatchMemoryFloatOutGui( IMpUnknown* host ) : MpGuiBase( host )
{
initializePin( 0, ValueIn, static_cast<MpGuiBaseMemberPtr>( PatchMemoryFloatOutGui::onValueInChanged) );
}

Your function is called to notify the GUI each time the DSP sends an updated value.

void PatchMemoryFloatOutGui::onValueInChanged()
{
float new_value = pinValueIn;
}

Sending data between Audio and GUI classes using the message 'pipe'

SynthEdit support a low-level mechanism for communicating beween audio and GUI, the mesage-pipe. Example: sending some data from the GUI to the audio process.

GUI code
int messageId = 1; // optional identifier for your message. Allows you to support several different messages.
int myData = 1234; // some data to send, any kind of variable, including strings and arrays etc.
int totalDataBytes = sizeof( myData ); // Data is sent as unformatted bytes, calculate the total number of bytes you need to send.

getHost()->sendMessageToAudio( messageId, totalDataBytes, &myData  );
Audio process side code

in your class declaration (*.h)

virtual int32_t MP_STDCALL receiveMessageFromGui( int32_t id, int32_t size, void* messageData );

in your *.cpp file

int32_t MyModule::receiveMessageFromGui( int32_t id, int32_t size, void* messageData )
{
    if( id == 1 )
    {
        int myData;
        memcpy( &myData, messageData, sizeof(myData) );
    }
}

The disadvantages of this method is that there is no protection against 'flooding' the message pipe with too much data, which will make SynthEdit unresponsive, and make other modules GUI's unresponsive.
For a full example see the 'QueLoader' example project in the SDK. The maximim message size is 1MB (as-of Jan 2015).

Controlling Bank Load and Save

SDK Version 3 is designed to control the host in a very natural way. You put a special pin on your GUI Module class, and specify what you want to connect it to (on the host).
Think of the host itself being like a SEM with pins exposed for various features...

For example the keyboard2 module connects to the host's "Voice/Pitch" pin like so...

<Pin id="0" name="Pitch" direction="in" datatype="float" hostConnect="Voice/Pitch" />

..the difference between this and a normal pin is the 'hostConnect' part. When the keyboard2 wants to set a voice's pitch, it transmits the new pitch out that pin. The host receives that signal and acts on it. Another example - loading a fxb bank:

- Put an DT_INT pin on your GUI Module.
- specify - hostConnect="PatchCommands".
- Valid commands are:
0 null
1 CopyPatch
2 LoadPatch
3 SavePatch
4 LoadBank
5 SaveBank

To initiate a bank load. Set your pin to 4, then back to zero (ready for the next command). SynthEdit will display the Bank-Load dialog box, then load whatever bank the user selects.

Windows 2000 users

To enable runtime support for Visual Studio V8, you may need to download these dlls: MFC80U.DLL MSVCR80.DLL MSVCP80.DLL. Put them in C:\Program Files\SynthEdit\

DEV C++ Compiler Notes
Install DEV C++, Install gcc V4.1 (or better) from Tools menu. 3.4 don't support templates very well.

File, New Project, DLL, Add module files (Gain.cpp, Gain.h)
Project-Project Options - Directories - Include Directories - Add se_sdk3 folder

Release updates to your module

You may need to release an updated version of your module with more features. Your users will have SynthEdit projects created with the older version. Changes to your module can cause those existing projects to crash. There are some guidelines you need to follow when adding or removing pins from an existing module.

RULE 1 - The worst thing you can do is:

  • Remove a pin.
  • Add a new pin using the same ID, but a different type or direction. E.g.

Release 1:

[-input ]
[ output-]

Release 2:

[-input ]
[-input2 ]

When the user updates the module, then reloads his project, a wire that was previously going to an output is suddenly going to an input, or a pin of the wrong type. This is likely to crash. Likewise renumbering the existing pin Ids will cause serious problems.

RULE 2 - It is OK to add new pins at the end of the list:

Release 1:

[-input ]
[ output-]

Release 2:

[-input ]
[ output-]
[-input2 ]

The original pins are exactly the same, only the last pin is new. This can't ever cause weird wiring because the old project didn't have wires to the new pin.

RULE 3 - You can hide pins. It *looks* like they're deleted, but they are still there. This gives very good backward compatibility.

Release 1:

[-input ]
[ output-]

Release 2:

[-input ]
[ output-] (private="true" or IO_HIDE_PIN. User can't see this pin).

RULE 4 - If you want to release a module with major changes - consider releasing it with a updated module ID. The user will have to manually replace the old module, but there is no problem loading old projects. This is very safe.

Combining Several Modules into one .SEM

You can combine several modules into a single .SEM file. The advantage of having several modules in a single file are that you save memory because some of each SEM is the SDK code, which can be shared in a combined SEM. Another advantage is when shipping 'packs' of related SEMs, having a single file is easier to distribute. Lastly, if you have too many SEMs in a project your plugin may fail to load because it's attempting to open too many files. The limit is approximately 300 SEMs. This limit is shared between all SynthEdit plugins. e.g. 3 different plugins with 100 SEMS each could load, but no more. You can reduce this risk by merging related modules into one SEM file.
 Merging SEMs does not generally improve their CPU, although the shared SDK code will consume less precious CPU cache, which can improve code speed.

Easiest is to take one of your existing SEM projects, and add another module to it.

To merge two SEMS first combine their XML files into one. Here you can see two plugins listed in the same XML file.

<PluginList>
     <Plugin id="WV Wavetable Osc" name="Wavetable Osc" graphicsApi="none">
         <Audio>
             <Pin id="0" name="WaveBankId" direction="in" datatype="string" />
         </Audio>
     </Plugin>

     <Plugin id="WV Wavetable Loader" name="Wavetable Loader" >
         <Audio>
             <Pin id="0" name="Wave" direction="in" datatype="blob" />
         </Audio>
     </Plugin>
</PluginList>

Next add the other module's header and c++ source files to your project. Here you can see I've added the WaveTableLoader module's files to the WavetableOsc's project.SDK tutorial

That's it, remember to delete the old redundant SEM, then re-scan your modules to see the new combined SEM.

Sharing Data between Modules - allocateSharedMemory

// in module.h header file...

float* lookupTable;

// in module.cpp ...

// If the data is shared only by one module and it's clones (like the waveshaer's 'shape')
// Then pass the module's unique 'handle', otherwise pass -1.
int32_t handle;
getHost()->getHandle(handle);

// Create a name for the shared memory. Always use your initials, the module's name,
// and a short description.
wchar_t name[40];
swprintf(name, sizeof(name)/sizeof(wchar_t), L"SE wave shaper %x curve", handle);

int32_t sampleRate = -1;
int32_t tableSize = 100;
int32_t needInitialise;

getHost()->allocateSharedMemory( name, (void**) &lookupTable, sampleRate, tableSize * sizeof(float), needInitialise);

if( need_initialise == true )
{
for( int i = 0 ; i < TABLE_SIZE ; i++ )
   {
       m_lookup_table[i] = whatever; // fill your table with some data.
   }
}

 

allocateSharedMemory() is for when your module needs a large table (e.g. an oscillator wavetable), and you want to share that table between all instances of the module. This much more efficient than having each oscillator create it's own copy of the table.

Each table must have a unique name, if you look at the waveshaper example, you will see the name, e.g. "SE wave shaper curve 23". 23 is the handle of that instance of the waveshaper, it is unique for each waveshaper. i.e. if you have two different waveshapers, they each get their own table. For an Oscillator, you may want to share the table between all oscillators, for example, SE's internal Osc shares it's sine wave table between all oscillators including any PD Oscs. in this case don't use the handle, e.g. "my osc sine wave".

Your module needs a pointer to the table, SE will create the table for you. CreateSharedLookup's return value tells you if you're creating the table for the very first time or not. The idea being that the first module to use the table needs to initialize it, any subsequent modules can skip that step.

The 3rd parameter to CreateSharedLookup is the sample rate. If the sample rate changes SynthEdit will re-initialize your module and CreateSharedLookup returns true to indicate you should re-initialize the table. If your table doesn't depend on the samplerate, pass -1 as the samplerate.

The 4th parameter is the size of the table in bytes ( a float variable is 4 bytes in size). For example to create a table to hold 100 floats, specify size of 400.

Use any type of data you need in the table (int, float, whatever). It doesn't actually have to be a 'table', you could store a 'struct'. organize it any way you require.

See the Waveshapers source code for an example of a shared table.

GUI Enum (List) pins

SDK3 does not have GUI enum pins (as such). It is more flexible to separate out the “choice” (an integer) and the ‘Item List” (the strings representing the choices).

In the screenshot below “List Items” communicates the strings that populate the List-Entry “1x,2x,4x” etc. This data travels left-to-right. The “Choice” pin sends back whatever the user selects (right-to-left flow), or updates the list on preset changes (left-to-right flow).

GUI Enum Pins

 XML

     <Pin id="4" name="Oversampling" direction="out" datatype="int"/>
     <Pin id="5" name="List Items" direction="out" datatype="string"/>

Header

       IntGuiPin pinOversampling;
       StringGuiPin pinListItems;