SDK Version 3 Documentation

The SynthEdit SDK allows you to make your own modules for SynthEdit.

The SynthEdit Music Plugin Standard (SEM for short) is an 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 SynthEdit 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 SynthEdit includes a code generator. On any existing module... <right-click>More->Build Code Skeleton... This creates a starter template code for your module. You will find the generated code in the folder "Documents\new_module\".

The template doesn't do any processing, it's just a non-functioning module with the pins and module-name, etc setup for you. This saves you from typing the boring parts.

Plugin Properties

The properties of a SynthEdit module are described in an XML file.

ModuleXml

 

You can find many example modules in the SDK download.
Below are the supported fields.

  • id
  • name
  • category
  • helpUrl
  • graphicsApi{GmpiGui,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.
  • AlwaysExport{ true, false } - During Export-Plugin, Exports the module regardless of if it is used in the project or not. Used only for critical SynthEdit factory modules. 
  • 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 } - hide the pin, the user won't see it. Useful for 'removing' a pin without breaking existing projects.
  • 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, 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 - one of:
    • 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
    • Polyphony
    • ReserveVoices
    • Oversampling/Rate ( 0 - Off, 2, 4, 8 etc)
    • Oversampling/Filter ( [3,5,7,9] Elliptic IIR Filter, [13-low, 14-med, 15-hi, 16-ultra] FIR Adaptive Quality, [20+] FIR actual taps) 
    • User/Int0
    • Time/BarStartPosition
    • Time/Timesignature/Numerator
    • Time/Timesignature/Denominator
    • Voice/Volume
    • Voice/Pan
    • Voice/Tuning
    • Voice/Vibrato
    • Voice/Expression
    • Voice/Brightness
    • Voice/UserControl0
    • Voice/UserControl1
    • Voice/UserControl2
    • Voice/PortamentoEnable
    • Portamento
    • Voice/GlideStartPitch
    • BenderRange
    • SubPatchCommands
    • Processor/OfflineRenderMode - integer. 0=Realtime rendering, 2=Offline rendering (SynthEdit editor only)
    • User/Int1
    • User/Int2
    • User/Int3
    • User/Int4
    • PatchCables
  • simdOverwritesBufferEnd - Single-Instruction-Multiple-Data instructions (SIMD) allow software to process samples in "bunches", usually 4 or 8 samples at a time. This results in faster code and lower CPU. SIMD can be fiddly however when there is an odd number of samples to process (e.g. 5 samples). With SIMD it's actually easier to process 8 samples than 5, and takes no more CPU because SIMD can process 4 samples at a time. Setting this flag instructs SynthEdit to add extra unused samples to "round up" audio buffers to the nearest convenient size. You can then write code that "overwrites" off the end of your audio buffer without any bad consequences. This helps to simplify your SIMD code. You can see an example of a module that takes advantage of this feature in the "Unit Converter - Volts" module (included in the SDK).

Parameter properties

  • persistant - set persistant="false" to prevent the parameter being saved in presets. Useful for stuff like meters that don't need to retain their position between sessions.

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 - its 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, its 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)

SynthEdit plugins are built from two main parts, the audio (DSP) side and the GUI (user interface side). These are quite isolated from each other. Communicating values between GUI and DSP is done using a Parameter object.

The sender and receiver connect to the same parameter. The parameter provides the connection between the two objects.

Example: Send a value of type 'float' from the Audio to the Graphics class.

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

Add one audio pin, one GUI pin, and one parameter to your 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 your C++ code make the pins as per usual..(only relevant lines 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 supports a low-level mechanism for communicating between audio and GUI, the message-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 disadvantage of this method is that there is no protection against 'flooding' the message pipe with too much data, which will make SynthEdit or your DAW unresponsive, and make other modules GUI's unresponsive too. Avoid using this method. The parameter-based method is far more sophisticated at regulating the flow of data and maintaining high frame rates on screen.
For a full example see the 'QueLoader' example project in the SDK.

Notes about communication between DSP and GUI

There are two buffers for communication between the audio and GUI parts. These buffers are 5MB in size each (5242880 bytes). These buffers are fixed in size to avoid the need to allocate memory on the DSP thread (Allocating memory on the DSP side may take a significant amount of time which causes audio glitches and dropouts). The size of these buffers places a limit on the maximum size of any data that needs to pass from the GUI to the DSP side. e.g. the biggest BLOB or string that can be transmitted is 5MB (minus a small header of 12 bytes). Note that there is no limit on the size of the number of BLOBs traveling down wires between modules, the limit is only in how large a BLOB can be saved in a parameter (patch-mem pins). 

The communication channel between the DSP and GUI parts of your module operates at 60Hz. If a module sends updates at a faster rate, some updates will be dropped. If the communications channel becomes overloaded because too many modules are sending data at the same time, some updates will be dropped. SynthEdit will never drop the most recent update. For example, if you have a LED on screen and the DSP sends three updates in rapid succession [ On, Off, On ] SynthEdit might drop the first and/or second update, but will never drop the last one. This ensures that even if the LED misses a few updates, it always ends up in the correct state.

If you use patch Memory modules to hold BLOBs, they are smart enough to share the buffers with other patch-mems by waiting patiently when the buffer is full. i.e. you can have multiple BLOBS of 5MB using patch-memories and you will never lose data.

On the other hand, if a module uses the low-level communication system, e.g. ‘sendMessageToAudio()’ then SynthEdit will discard messages if they are too big to fit in the free space within the buffer. i.e. if another module places a large amount of data in the buffer, then other modules will have to either wait a while (if using parameters) or have their message discarded (if using sendMessageToAudio).

With objects larger than 5MB it is more practical to share them indirectly. In this case the message buffer is used only to send a compact ‘pointer’ or ‘handle’ that represents the large object. This avoids the need to copy large amounts of memory into and out of the buffer, which can cause audio dropouts. This is how the SE Sample-Oscillator2 works.
Nowadays sample sets have become even larger, often Gigabytes of samples (which is often too much to fit into RAM all at the same time). In this situation it is necessary to use disk-streaming, whereby each sample is read off disk only when it is needed to be played). This is how the SFZ Player module works.

“Why don’t you just increase the fixed buffer size to 5GB so we can load any sample in BLOBs?”

Note that every BLOB in a SE Patch-Memory module is held on both the GUI side and also on the DSP side, in addition, each of the two communication buffers needs to be large enough to hold the largest BLOB, these requirements place a theoretical maximum BLOB size at ¼ your RAM. In addition to this, any modules which pass around the BLOB (via wires) need to hold an additional copy of the BLOB, which might represent several more copies in total. So even if the SE communication buffers were increased to the maximum possible size, a user could utilize only a fraction of their RAM for samples.

e.g. If your PC had 8GB of RAM, using BLOBs SynthEdit could load at absolute maximum 2GB of samples, more likely as little as ½ GB of samples. This is not very efficient and is likely to frustrate end-users because they wouldn’t understand why they can’t utilize all their RAM for samples. This is why I don’t encourage the use of BLOBs for large general-purpose sampling (BLOBs are fine for small sample sets). For large sample sets, I recommend the SFZ module.

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 a 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.

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

Considerations when releasing 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 an 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.
 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 its 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 a 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;

Power Saving - Using Sleep-mode to reduce CPU when idle

Many modules have the potential to reduce CPU consumption in certain situations, like when processing silence. In these cases, a module can enter Sleep-mode.

Sleep-mode is when your module is effectively turned-off, SynthEdit does not call any processing function. Your module output will be 'static'. This usually means silence, but can also be any fixed unchanging 'DC' signal.

How Sleep-mode works is complicated, but the SDK now handles most of the hard work for you. The Gain example module (int the 'Gain' folder) shows how this works.

'Static' vs 'Streaming'

In SynthEdit the signals that flow between modules can in one of two states: 'Static' or 'Streaming'. A Streaming signal is simply a normal audio signal containing some type of sound. A Static signal is one that is not changing, like silence. SynthEdit communicates changes to the state of your module's audio pins via your SetPins() method. Likewise, your module can communicate the state of its output signal via its pin's SetStreaming() method.

Note that It is not necessary to implement Sleep-mode functionality in your module, Sleep mode is an optional enhancement that takes advantage of the pins streaming state information to avoid wasting CPU when the module's input signal is silent.

The easiest way to implement Sleep mode is simply to use your output pins SetStreaming() method to communicate when your output signal is silent, the SDKs default implementation of Sleep-mode will then automatically sleep your module whenever its audio pins are all 'static' (silent).

The Gain example shows how it determines if its output is silent. The first rule is that if the input is silent - then the output will be silent. The second rule is that if the gain is zero - the output will be silent (regardless of if the input is silent or not). You can see this logic in the Gain example...

void Gain::onSetPins(void)
{
	// Determin if output is silent or active, then notify downstream modules.
	// Downstream modules can then 'sleep' (save CPU) when processing silence.
 
	// If either input is active, output will be active. ( "||" means "or" ).
	bool OutputIsActive = pinInput1.isStreaming() || pinInput2.isStreaming();
 
	// Exception...
	// If either input zero, output is silent.
	if( !pinInput1.isStreaming() && pinInput1 == 0.0f )
	{
		OutputIsActive = false;
	}
 
	if( !pinInput2.isStreaming() && pinInput2 == 0.0f )
	{
		OutputIsActive = false;
	}
 
	// Transmit new output state to modules 'downstream'.
	pinOutput1.setStreaming( OutputIsActive );

Implementing the SetStreaming() logic will now allow other modules 'downstream' to sleep when they are notified that the signal status is 'Static'. This will also allow the SDK to automatically sleep the gain module when its inputs and outputs are Static.

Taking it further

So far, the Gain module will Sleep when all its inputs are Static. However, we can enhance this a little further by recognizing that when the Gain's volume is zero the output will be silent - even if the input signal is Streaming. In this situation, it is safe to manually override the default behavior and force the module to Sleep.

// control sleep mode manually.
setSleep( !OutputIsActive ); // '!' means 'not'.

Modules with a 'tail'

The Gain example shows how to handle a simple case of Sleep-mode. However, some modules, like Reverb, are more sophisticated because they may output audio even after their input signal has gone silent. This extra audio output is called a 'tail'.

Modules with a tail that wish to use Sleep-mode typically need to monitor their output signal and wait until that signal is silent before sending a 'Static' status out of the output pin and calling the setSleep() method. An example of this is the Low Pass Butterworth Filter in the Filters/ButterworthLp.h file, which is based on the 'FilterBase' class. Read file shared/FilterBase.h for an overview of how to handle Sleep mode for any module with a tail (not only filters).

Updating pins from the process function

All signals in SynthEdit are sample-accurate. However, SE process methods operate on blocks of samples. For example, your subProcess() method may operate typically on 96 samples each time it's called (the exact number can vary). A block is like a small 'slice' of time. So anytime your module updates the status of a pin with a method like SetStreaming() or transmits a value out of an event-based pin, SynthEdit needs to know when during the current time-slice to send that event. This time information is called a 'time-stamp' or 'block-position'.

In SynthEdit modules you specify time-stamps relative to the block start. So an event that happens right at the start of the current time-slice would have a timestamp of 0 (zero). An event happening at the end of a  96-sample time-slice would have a timestamp of 95. Do not specify timestamps outside the range of the current time-slice. The second argument to setStreaming() is the time-stamp.

pinOutput.setStreaming( true, 65 );

Much of the time, you will update output pins from the OnSetPins() method. This is a special case. In this method, the SynthEdit SDK has enough information to automatically calculate the current timestamp. To use the automatically calculated timestamp, use the special value -1 to indicate 'current timestamp' or just leave out the timestamp altogether.

pinOutput1.setStreaming( OutputIsActive, -1 ); // use 'current' timestamp.
pinOutput1.setStreaming( OutputIsActive ); // use 'current' timestamp.

If you wish to update a pin during your process method, you will need to specify the timestamp. Calculate the timestamp value from the current block-position plus the number of samples you have processed. An example:

	for (int s = 0; s < sampleFrames; ++s)
	{
		if( whatever )
		{
			int blockRelativeSamplePosition = getBlockPosition() + s;
 			pinOutput1.setStreaming(false, blockRelativeSamplePosition);
		}
// normal processing stuff... }

Drawing

SynthEdit's drawing API is copied from Direct2D and DirectWrite, so all the concepts are near-identical. You should be able to adapt D2D examples fairly easily, plus all the SynthEdit graphical modules are included in the SDK download as examples for you to draw on.

For example drawing a rectangle in D2D, vs SynthEdit ( AKA "GMPI Drawing" )

// Direct-2D
{
	D2D1::ColorF black(D2D1::ColorF::Black);
 
	ID2D1SolidColorBrush* pBlackBrush;
 
	pRT->CreateSolidColorBrush(
		black,
		&pBlackBrush);
 
	pRT->DrawRectangle(
		D2D1::RectF(0, 0, 10, 10),
		pBlackBrush);
}
 
// GMPI Drawing API
{
	GmpiDrawing::Color black(GmpiDrawing::Color::Black);
 
	GmpiDrawing_API::IMpSolidColorBrush* pBlackBrush;
 
	pDC->CreateSolidColorBrush(
		&black,
		&pBlackBrush);
 
	GmpiDrawing_API::MP1_RECT rc{ 0, 0, 10, 10 };
	pDC->DrawRectangle(
		&rc,
		pBlackBrush);
}

In addition, SynthEdit has "wrappers" that manage all the pointers for you (no need to remember to "release" everything later). This is more concise and friendly than raw Direct2D, while retaining all the concepts.

// GMPI Drawing (wrappers)
{
	Color black(Color::Black);
 
	auto BlackBrush = g.CreateSolidColorBrush(black);
 
	g.DrawRectangle(
		Rect(0, 0, 10, 10),
		BlackBrush);
}

Reporting latency

Some modules have unwanted latency. A module can report this latency in order to have it 'canceled'.

Example: Some kinds of filters like FIR filters (Finite Impulse Response) often introduce a delay into the audio signal.

sdk latency1
Latency - the green trace is the filter input signal, yellow the output. The output signal is delayed relative to the input.

Your module can report this latency to SynthEdit to enable PDC (Plugin Delay Compensation). PDC hides the effect of latency through the clever use of delay lines.

sdk latency2

PDC enabled. The signal exits the filter nicely aligned with the input signal. It's like the filter never had any latency.

There are two ways to report latency.

Reporting a fixed latency

If your module has a fixed latency that never changes, you can specify this in the module's XML. Latency is specified in sample frames.

sdk latency3
Reporting a variable latency

If its latency depends on the settings of a module, you can calculate and report the latency at run-time from the module's 'Processor' object. Here is an example from the FIR filter.

The SynthEdit "SINC Lowpass filter" has a pin called "Taps" which the user can set to control the quality of the filter (more taps is better). The latency of the module also depends on the number of taps. In this example, the module is watching for any change to the default value of the 'Taps' pin. The module then uses this value to calculate how much latency compensation it requires and passes that value to the host via the 'SetLatency' method. Latency is measured in sample frames.

sdk latency4

 WARNING: Changing latency will interrupt and restart the plugin, and possibly other plugins running in the DAW. It is a disruptive operation. Please minimize the chance of this happening. For example, it's best not to expose to the DAW for automation any parameter which might change the latency. If you can get away with a fixed-latency (in the XML), prefer that option.

Latency reporting to the DAW is currently supported in VST3 plugins. How it works is the plugin adds up the cumulative latency of all its modules and reports the total latency to the DAW.

Live coding

In SynthEdit 1.5 and later, you can work on and test your modules without leaving SynthEdit. This is called 'live coding' and it can save a lot of time and hassle.

In Visual Studio, add a Post-Build Event to your module project:

xcopy /c /y "$(OutDir)$(TargetName)$(TargetExt)" "C:\Program Files\Common Files\SynthEdit\modules-staged\"

live coding post build

Anytime you build your module it will get copied to SynthEdit's 'staging area'. SynthEdit will then take that as a hint to reload that module.

MIDI 2.0

SynthEdit MIDI pins can handle MIDI 1.0 and/or MIDI 2.0.
From Version 1.5 many SynthEdit modules accept either MIDI 1 or MIDI 2 but emit MIDI 2.

SynthEdit also provides a MIDI converter module that can convert MIDI 1 to MIDI 2 and vice versa. This is useful for maintaining compatibility with MIDI 1 only modules.

MIDI 2.0 is the default, because MIDI 1, MIDI MPE, and Steinberg Note-Expression can all be converted losslessly to MIDI 2, but not always the other way round.

The SynthEdit SDK now provides helper classes that convert MIDI for you. This allows you to write your MIDI code without having to handle all the different types of MIDI.

We recommend writing your module to use MIDI 2. The SDK contains the 'MIDI to Gate' module that shows how to write a MIDI 2 module that also accepts MIDI 1 transparently.

Converting your module to MIDI 2.0

For an example of a module that handles both MIDI 1.0 and MIDI 2.0, check out the MidiToGate module (it's in the MidiPlayer2 folder).
 How it works is the module uses a converter object to convert every message to MIDI 2.0.

Add the midi converter as a member of your module.

#include "../se_sdk3/mp_midi.h"
class MidiToGate : public MpBase2
{
gmpi::midi_2_0::MidiConverter2 midiConverter;
public:

init the midi converter in your constructor (all this does is send converted MIDI to the onMidi2Message method)

MidiToGate() :
	// init the midi converter
midiConverter(
// provide a lambda to accept converted MIDI 2.0 messages
[this](const midi::message_view& msg, int offset)
{ onMidi2Message(msg); }
)
{
initializePin( pinMIDIIn ); // etc....

Alter your MIDI handler to forward all MIDI to the converter...

// passes all MIDI to the converter.
void onMidiMessage(int pinunsigned charmidiMessageint size)
{
	midi::message_view msg((const uint8_t*)midiMessagesize);
 
	// convert everything to MIDI 2.0
	midiConverter.processMidi(msg, -1);
}

finally, add a method to handle the MIDI 2.0 data...

// put your midi handling code in here.
void onMidi2Message(const midi::message_viewmsg)
{
	const auto header = gmpi::midi_2_0::decodeHeader(msg);
 
	// only 8-byte messages supported.
	if (header.messageType != gmpi::midi_2_0::ChannelVoice64)
		return;
 
	switch (header.status)
	{
 
	case gmpi::midi_2_0::NoteOn:
	{
		const auto note = gmpi::midi_2_0::decodeNote(msg);

...etc