Plugins


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.  SEM V3 is supported in SynthEdit Version 1.1. 

Download SynthEdit 1.1 Currently in beta testing.

Download SEM V3 SDK

SDK Version 3 Documentation

Getting a Compiler

3 Compilers have been tested with the SDK:

Microsoft Visual C++

http://msdn.microsoft.com/vstudio/

The SDK was written using this compiler.
The Visual C++ Express version is free, but requires you to download Windows Platform SDK yourself.
   Also cheap for Students is the Academic version. Shop around to get the best price.

Gnu C++

Code::Blocks integrated development environment.

This is good, free, modern compiler. See Using Code::Blocks with SynthEdit tutorial.

Borland free compiler

This compiler is too old now, it does not work with this SDK.

Code generator

To help convert your 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 "C:\SE\My Projects\modules_source" - you need to create that folder first.

  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 yet support GUI DT_ENUM pins.  Avoid upgrading any modules with those.

Processing

// Process audio.
void Gain::subProcess( int bufferOffset, int sampleFrames )
{
	// assign pointers to your in/output buffers. Each buffer is an array of float samples.
	float* in1  = bufferOffset + pinInput1.getBuffer();
	float* in2  = bufferOffset + pinInput2.getBuffer();
	float* out1 = bufferOffset + pinOutput1.getBuffer();
 
	forint s = sampleFrames; s > 0; --s ) // sampleFrames = how many samples to process (can vary). repeat (loop) that many times
	{
		float input1 = *in1;	// get the sample 'POINTED TO' by in1.
		float input2 = *in2;
 
		// Multiplying the two input's samples together.
		float result = input1 * input2;
 
		// store the result in the output buffer.
		*out1 = result;
 
		// increment the pointers (move to next sample in buffers).
		++in1;
		++in2;
		++out1;
	}
}

Like VST, your module processes 'blocks' of audio samples. These samples arrive in an array/buffer. You process around 100 samples during each call to subProcess(). In this example the audio is retrieved via a pointer ( *in1 ), then after each individual sample is processed, we advance the pointer to the next sample (++in1).

Advanced note: If your code needs to change the floating point flags - set the flags at the start of subProcess(), then restore the original flags at the end.

SEM XML

Every module needs some meta-data to describe the module, it's parameters, and it's input and output pins.

SDK3 introduces an easy, compact format based on XML. This example is called "Gain3", it has 2 inputs and one output.

<?xml version="1.0" encoding="utf-8" ?>
 
<PluginList>
  <Plugin id="SynthEdit Gain example V3" name="Gain3" category="SDK Examples" helpUrl="gain.htm">
    <Audio>
      <Pin id="0" name="Input1" direction="in"  datatype="float" rate="audio" default="0.8" />
      <Pin id="1" name="Input2" direction="in"  datatype="float" rate="audio" default="0.8" />
      <Pin id="2" name="Output" direction="out" datatype="float" rate="audio" />
    </Audio>
  </Plugin>
</PluginList>

Plugin Properties

id
name
category
helpUrl
graphicsApi{HWND,composited,none}
polyphonicSource{ true, false }
polyphonicAggregator{ true, false }
cloned{ true, false }
voiceMonitorIgnore{ true, false }
GUI
Parameters
Pins

Pin Properties

id
name
datatype {float, int, text, blob, midi, bool, enum (DSP only. not fully supported)}
default
direction {in, out}
rate {audio} - float pins only. Distinguishes streaming audio pins from event-driven 'control signals' (float pins, int pins etc.).
private{ true, false }
autoRename{ true, false }
isFilename{ true, false }

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 optimisation 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 }
isMinimised{ true, false }
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 minimum and maximum travel is automatically set. In this example min=0, max=40. 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.

notes

notes
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 minimises artefacts.

 

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.

 

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;
}

Types of Windows

SynthEdit supports two types of windows.

1 - Standard Microsoft Window (Native)


These are the plain regular windows as used in most applications.  They are always rectangular and always obscure any window 'behind' them.

Advantages

Disadvantages
2 - SynthEdit Composited Windows

Created to support non-rectangular shapes, these are still actually rectangular but support transparent pixels. Here the red circle is partially transparent.

Advantages:

Disadvantages

Specify which type by deriving your from the appropriate base class. In your graphics class header file, and also in your XML file...

For a native window...

MyPluginGui.h

class MyGui : public SeGuiWindowsGfxBase

{

MyPlugin.xml

<Plugin id="SynthEdit Scope3" graphicsApi="HWND">


 
For a composited window...

class MyGui : public SeGuiCompositedGfxBase

{

 

<Plugin id="SynthEdit Scope3" graphicsApi="Composited">

Customizing the popup context menu

You can customize SynthEdit's right-click context menu.

In your .h file...

    virtual int32_t MP_STDCALL onCreateContextMenu();

    virtual int32_t MP_STDCALL onContextMenu( int32_t selection );

In your .cpp file...

// Add custom items to right-click menu.

int32_t MyGui::onCreateContextMenu()

{

    getHost()->addContextMenuItem( L"Cat", 0, 0 );

    getHost()->addContextMenuItem( L"Dog", 1, 0 );

    return gmpi::MP_OK;

}

 

// act on user selecting right-click item.

int32_t MyGui::onContextMenu( int32_t selection )

{

    switch( selection )

    {

    case 0:

        // 'Cat' selected

        break;

    case 1:

        // 'Dog' selected

        break;

    };

 

    return gmpi::MP_OK;

}

Right-click handler

To override SynthEdit's right-click handler.  NOTE: If possible please avoid doing this as it disables SynthEdit's pop up menu system.

In your .h file...

    // This handles the Windows message loop.

    // Add here to override right-click behaviour.

    virtual LRESULT MsgProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

    int32_t onRButtonDown( UINT flags, POINT point );

    int32_t onRButtonUp( UINT flags, POINT point );

In your .cpp file...

// Test overriding right-click handling.

// WARNING: This will disable SynthEdit's right-click menu.  If possible please avoid taking

// right-click for your own use.

LRESULT MyGui::MsgProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )

{

    switch (message)

    {

        case (WM_RBUTTONDOWN):

            {

                POINT p;

                p.x = MAKEPOINTS(lParam).x;

                p.y = MAKEPOINTS(lParam).y;

 

                onRButtonDown( (UINT) wParam, p );

            }

            return 1;

            break;

        case (WM_RBUTTONUP):

            {

                POINT p;

                p.x = MAKEPOINTS(lParam).x;

                p.y = MAKEPOINTS(lParam).y;

 

                onRButtonUp( (UINT) wParam, p );

            }

            return 1;

            break;

        default:

            return SeGuiWindowsGfxBase::MsgProc( hwnd, message, wParam, lParam );

            break;

    }

}

 

int32_t MyGui::onRButtonDown( UINT flags, POINT point )

{

    return gmpi::MP_OK;

}

 

int32_t MyGui::onRButtonUp( UINT flags, POINT point )

{

    return gmpi::MP_OK;

}

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:

 Release 1:

[-input    ]  ID=0

[   output-]  ID=1

Release 2:

[-input    ]  ID=0

[-input2   ]  ID=1

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    ]  ID=0

[   output-]  ID=1

Release 2:

[-input    ]  ID=0

[   output-]  ID=1 pins. It *looks* like they're deleted, but they are still [-input2   ]  ID=2

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    ]  ID=0

[   output-]  ID=1

Release 2:

[-input    ]  ID=0

[   output-]  ID=1 (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. id="My Module V2".  The user will have to manually replace the old module, but there is no problem loading old projects.  This is very safe.

Converting SDK Version 2 modules to Version 3

Porting SE-Gain

You should now be able to re-open SynthEdit and insert your new module. (It is not functional yet because it has an empty sub-process method).

Back in Visual Studio you can inspect the new project files.  There are a handful of SDK files, plus the actual module files: Gain.h, Gain.cpp, and Gain.xml.

Open Gain2.cpp, inside is the minimal code for your plugin, including a basic subProcess method.

void Gain2::subProcess( int bufferOffset, int sampleFrames )

{

    // get pointers to in/output buffers.

    float* input    = bufferOffset + pinInput.getBuffer();

    float* input2    = bufferOffset + pinInput2.getBuffer();

    float* output    = bufferOffset + pinOutput.getBuffer();

 

    for( int s = sampleFrames; s > 0; --s )

    {

        // TODO: Signal processing goes here.

 

        // Increment buffer pointers.

        ++input;

        ++input2;

        ++output;

    }

}

Note the name has changed from 'sub_process' to 'subProcess', this is an example of the updated coding conventions.  Also the buffer pointers are retrieved differently.  Otherwise the method is the same as before.

The TODO is a reminder to copy your signal processing code from your old module...

    for( int s = sampleFrames; s > 0; --s )

    {

        float in1 = *input;    // get the sample 'POINTED TO' by in1

        float in2 = *input2;

 

        // do the actual processing (multiplying the two input samples together)

        float result = in1 * in2;

 

        *output = result;    // store the result in the output buffer

 

        // Increment buffer pointers.

        ++input;

        ++input2;

        ++output;

    }

What happened to getModuleProperties() and getPinProperties()?

In the old SDK you had 2 methods to provide your module name and pin names and properties...

// describe your module

bool Module::getModuleProperties (SEModuleProperties* properties)

{

    // describe the plugin, this is the name the end-user will see.

    properties->name = "Gain Example";                               

 

    // return a unique string 32 characters max

    properties->id = "SynthEdit Gain Example";                       

 

    properties->about = "by Jeff M (MS)" ;

 

    return true;

}

 

// describe the pins (plugs)

bool Module::getPinProperties (long index, SEPinProperties* properties)

{

    switch( index )                               

    {

        // typical input plug (inputs are listed first)

    case 0:

        properties->name                = "Input";

        properties->variable_address    = &input1_buffer;

        properties->direction            = DR_IN;

        properties->datatype            = DT_FSAMPLE;

        properties->default_value        = "0";

        break;

    case 1:

        properties->name                = "Input";

        properties->variable_address    = &input2_buffer;

        properties->direction            = DR_IN;

        properties->datatype            = DT_FSAMPLE;

        properties->default_value        = "5";

        break;

        // typical output plug

    case 2:

        properties->name                = "Output";

        properties->variable_address    = &output1_buffer;

        properties->direction            = DR_OUT;

        properties->datatype            = DT_FSAMPLE;

        break;

    default:

        return false; // host will ask for plugs 0,1,2,3 etc. return false to signal when done

    };

 

    return true;

}

 

These methods are gone.  In their place is a simple text file - Gain2.xml.

<?xml version="1.0" encoding="utf-8" ?>

 

<PluginList>

  <Plugin id="My Gain2" name="Gain2" category="MyModules" graphicsApi="HWND" helpUrl="Gain2.htm">

    <Audio>

      <Pin id="0" name="Input" direction="in" datatype="float" rate="audio"/>

      <Pin id="1" name="Input 2" direction="in" datatype="float" rate="audio" default="0.5"/>

      <Pin id="2" name="Output" direction="out" datatype="float" rate="audio"/>

    </Audio>

  </Plugin>

</PluginList>

 

 

 As you can see this is less typing and easy to edit.  It also results in faster compilation, less code generated and smaller SEM files.

OnPlugStateChange

This method was called anytime a pin was updated.

void Module::OnPlugStateChange(SEPin *pin)

{

    state_type in_stat1 = getPin(PN_INPUT1)->getStatus();

 SDK3 replaces OnPlugStateChange() with onSetPins().

void Gain2::onSetPins(void)

{

    // Check which pins are updated.

    if( pinInput.isStreaming() )

    {

    }

 Rather than check inputs for a status of ST_RUN, you now use the isStreaming() method.

Rather than check non-audio pins for ST_ONE_OFF, you now use the isUpdated() method.

void Gain2::onSetPins(void)

{

    // Check which pins are updated.

    if( pinInput.isUpdated() && pinInput2.isUpdated() )

    {

        // They both changed at the same time.

    }

The functionality is the same as before, just a more readable syntax.

The only substantial difference is - When two pins are updated at the same time, onSetPins() is called only once (with both pins flagged).  It was very difficult before to detect simultaneous updates on two pins, now it's easy.

Likewise, when the audio engine starts SynthEdit flags all your input pins as 'updated' and calls onSetPins() just the once.  This results in less function call overhead and faster SEMs.

SET_PROCESS_FUNC

This has been updated. Search-and-Replace your old code with the new look.  Don't forget the '&' (address-of) symbol.

Old.

    SET_PROCESS_FUNC(Module::sub_process);

New.

    SET_PROCESS(&Gain2::subProcess);

TransmitStatusChange

TransmitStatusChange() has been replaced with setStreaming(). Less typing, more readable...

Old

    getPin(PN_OUTPUT1)->TransmitStatusChange( SampleClock(), ST_RUN );

New

    pinOutput.setStreaming(true);

Old

    getPin(PN_OUTPUT1)->TransmitStatusChange( SampleClock(), ST_STATIC );

New

    pinOutput.setStreaming(false);

Sleep Mode

Previously, after checking your input pins were silent, you implemented sleep mode with a special process method to filled the output buffers with silent samples. Once that was done you asked the host to put the module to sleep...

// every module has an output buffer of approx. 100 samples.

// before deactivating module, need to fill that buffer with silence.

// "static_count" is counting out those 100 samples.

void Module::sub_process_static(long buffer_offset, long sampleFrames )

{

    sub_process(buffer_offset, sampleFrames);

    static_count = static_count - sampleFrames;

    if( static_count <= 0 )

    {

        CallHost(seaudioMasterSleepMode);

    }

}

Although this is still necessary.  The new SDK does it for you. Your module no longer needs any special code. You don't need to port that code to SDK V3.

If you don't want automatic sleep mode, or need to handle it manually you can disable automatic sleep mode...

    setSleep(false);

Summary

That was the quick guide to porting to SDK3.  Any further questions please post to the SynthEdit SDK group at yahoo.

SEM Supported Features

 

SEM is an implementation of GMPI (Generalized Music Plugin Interface) with a few extensions for SynthEdit.  Some less crucial GMPI features are not supported yet.

GMPI requirements not supported