FAQ
Questions and Answers
As a start on proper SDK documentation, I am listing any frequently asked questions here. This info will gradually become organized into structured documentation. This documentation is provided online and included with the SDK in the sdk_docs folder.
Controlling how modules responds to polyphony ( i.e. module gets duplicated for each note) or not.
This is quite complicated.....
The delay is sometimes polyphonic, and sometime monophonic...SE decides automatically...
MONOPHONIC USE
Image you have a delay in your synth, fixed at a 1 second delay.
[MIDI to CV]->[OSC]->[VCA]->[Delay]->[Speaker]
There is no audible difference between:
- two voices going through the one shared delay....and
- two voices going though two delays (one per voice)
So SynthEdit uses just one Delay module.
How does SE 'know' this?...The "Audio In" plug of the delay has a special flag, IO LINEAR INPUT, this flag means it's safe to put two voices through the one delay. The two voices won't affect each other.
RULE 1: If a module is downstream from the MIDI CV, it is polyphonic (cloned)
EXCEPTION: If the module connected via a 'Linear" input, it isn't cloned.
POLYPHONIC USE
Now imagine you want the delay time to vary depending on the note pitch (higher pitch = less delay). So you connect the MIDI CV Pitch to the Delay Modulation...
[MIDI to CV]->[OSC]->[VCA]->[Delay]->[Speaker]
|______________________/
In this example the Delay is cloned. When you play two notes, they need to go through separate delays because each will need a different delay time. How does SE know?
Here SynthEdit sees that the MIDI CV is connected to the Delay's "Modulation" plug. This plug is not flagged as "Linear", SE then knows that this module needs to be cloned to play polyphonically.
Hope that made some sense?.
There's only a few modules that have 'linear' inputs..
the Level Adjust, the VCA, the delay.
To set a plug as 'Linear, you set a flag in getPinProperties()...
properties->flags = IO LINEAR INPUT;
If you don't do this, SE assume it must clone your module to play a new note.
POLYPHONIC USE 2
Here's a 3rd example
[MIDI to CV]->[OSC]->[Delay]->[VCA]->[Speaker]
\-->[ADSR]---------------/
Here the delay is in the middle of a voice structure. It's fairly obvious that SE will clone the Oscillator (you need one per voice), it's also apparent that it will clone the ADSR and VCA (you also need 1 of each per voice). The delay is also cloned because it is 'sandwiched' between polyphonic modules.
Question: Which pins should use the IO_LINEAR_INPUT flag?, would adding it to an ENV Gate input be any use? - or even a bad move?
very bad move.
IO_LINEAR_INPUT is a hint to SE to make a module monophonic because putting two (or more) voices though that pin won't change the final sound..
for example, The LAST module in your synth is a level adjust. Used with a slider as a simple volume control...
[MIDICV]->[OSC]->[FILTER]->[VCA]--->[Level Adjust]-->[SPEAKER]
L----------------------------------------^
Now normally all the modules get cloned, so you would get one level adjust per voice....but adjusting the level is a linear operation...i.e. there's no harm sending several voices though a single volume control (saving CPU).
Setting IO_LINEAR_INPUT informs SE that it can do so.
On the other hand, say the last module is a distortion module, you can't combine several voices into one distortion without affecting the sound (it would be more distorted, there would be interaction between the voices). So you would NOT use IO_LINEAR_INPUT on a distortion module.
an IO_LINEAR_INPUT applies only linear processing, in general this means adjusting the volume only, another example would be a delay unit, two voices into one delay unit sounds the same as two voices into two delay units, so SE can save some CPU there too.
So a "Gate" input is not a linear device, you can't share it between voices. If you had a module that simply gated audio on/off, like some kind of switch, the audio input would have the IO_LINEAR_INPUT flag, but the "Gate" input would not.
note in general the IO_LINEAR_INPUT is only relevant when that module is last in the signal chain, usually after the VCA...
[MIDICV]->[OSC]-->[LEVEL ADJ]->[Filter]-->[VCA]
L-------------------------------------------------------------^
In this example the level adjust is used 'inside' the voice (to adjust an osc level), SE knows that it needs one osc per voice, and one filter per voice, so anything 'in between' must also be polyphonic (IO_LINEAR_INPUT is ignored).
Communicating Directly With The VST Host
You can bypass the SynthEdit SDK, and talk directly to the VST Host (Cubase or whatever). This example gets the Host Vendor name.
// testing direct call to VST Host (getting host vendor in this example)
se_call_vst_host_params params;
memset( ¶ms, 0, sizeof(params) ); // clear structure
char host_name[100] = ""; // a string to store the result of this query
params.opcode = 32; // audioMasterGetVendorString;
params.index = 0;
params.value = 0;
params.ptr = host_name;
params.opt = 0;
// when running in SynthEdit, you get a result -1, indicates no VST Host
long res = CallHost(seaudioMasterCallVstHost, 0,0, ¶ms);
Normally you use the opcode name (audioMasterGetVendorString) directly, I have used it's value (32) instead, to allow this example to compile without the VST SDK.
The Opcodes available are listed in the VST SDK. Get it at.
http://ygrabit.steinberg.de/users/ygrabit/public_html/
(or Google it). The available opcodes are listed in files aeffect.h and aeffectx.h
For questions on the VST SDK sign up to the VST Mailing List
Getting a filename's full path
When getting a filename from a Text-Entry, SE may pass only a short filename. e.g."wavefile", this indicates the file is in SE's default wave folder. To get the full path use seaudioMasterResolveFilename2 (DSP code) or seGuiHostResolveFilename (GUI code). SynthEdit will return the full path, e.g. "C:/Program Files/SynthEdit/audio/wavefile.wav".
When running within a VST plug-in, SE will return the VST Plug-in path plus the plug-in’s private foldername, e.g. "C:/VST plugins/MyReverb/wavefile.wav".
const int MAX_FILENAME_LENGTH = 300;
// Both source and destination are UNICODE (two-byte) character strings
unsigned short *source = L"test.txt";
unsigned short dest[MAX_FILENAME_LENGTH];
CallHost( seaudioMasterResolveFilename2 , (long) source, MAX_FILENAME_LENGTH, &dest);
You may want to convert the result to ASCII (8-bit) characters...
// to convert to ascii (optional)
#include <windows.h> //for WideCharToMultiByte
char ascii_filename[MAX_FILENAME_LENGTH];
WideCharToMultiByte(CP_ACP, 0, dest, -1, ascii_filename, MAX_FILENAME_LENGTH, NULL, NULL);
Setting a text output pin
To send text data out of a pin...
CallHost(seaudioMasterSetPinOutputText, PN_TEXT_OUT, 0, (void *) "duck" );
where PN_TEXT_OUT is the index of the pin, and "duck" is the text to send. SynthEdit will copy the string, so you don't have to retain it.
Sharing Data between Modules - CreateSharedLookup
// in module.h header file...
float *m_lookup_table;
// in module.cpp ...
#define TABLE_SIZE 100;
char table_name[40];
sprintf(table_name, "SE wave shaper %x curve", Handle() );
bool need_initialise = CreateSharedLookup( table_name, &m_lookup_table, -1, TABLE_SIZE );
if( need_initialise == true )
{
for( int i = 0 ; i < TABLE_SIZE ; i++ )
{
m_lookup_table[i] = whatever; // fill your table with some data
}
}
CreateSharedLookup() 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, specified in 'floats' ( a float variable is 4 bytes in size). i.e. a table size of 1 = 4 bytes. For example to create a table size of 100 bytes specify size of 25.
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 se_waveshaper for an example of a shared table.
Regular Events to The GUI - OnIdle
You may need to update your graphics even when the user is not dragging the mouse. VST provides a regular timed event called OnIdle.
In GuiModule.h ...
virtual bool OnIdle(void);
Also, if not already ...
void Initialize(bool loaded_from_file);
~GuiModule(void);
In GuiModule.cpp
void GuiModule::Initialize(bool loaded_from_file)
{
SEGUI_base::Initialize(loaded_from_file);
CallHost(seGuiHostSetIdle, 1 ); // request OnIdle events
}
GuiModule::~GuiModule(void)
{
CallHost(seGuiHostSetIdle, 0 ); // cancel OnIdle events
}
bool GuiModule::OnIdle(void)
{
// do something here
return true; // or return false if you don't require more events
}
The second parameter to seGuiHostSetIdle is a switch: 1 = On (enable Idle Events), 0 = Off (Disable Idle Events).
OnIdle is called approximately once every 50ms, equivalent to 20 Hz. It does vary between hosts though.
Modifying se_gain to make your own plugin
Copy se_gain folder
open in DevStudio project settings, post build step, change the copy-to filename e.g. se_delay.sep (both debug and release builds. SE plug-ins go in the SynthEdit/modules folder.
put your algorithm in module.cpp sub_process() function
There is little documentation as yet, please feel free to email the SynthEdit SDK Group if you have any questions, I give top priority to questions posted there.
What format is the audio data?
DT_FSAMPLE - Streaming float data in range -1 to 1 ( displayed as +/- 10 Volt ). DR_IN, DR_OUT Allowed
DT_ENUM - Integer data ranging 0-num options. use datatype_extra field to specify e.g. " Sawtooth, Square, Sine" would give 3 possible input values [0, 1, 2]. You can customize the values too e.g. datatype_extra = "ten=10,eleven,twelve,ni ju=20" would give 4 possible input values [10,11,12,20] (the use gets a list of 4 options "ten", " eleven","twelve","ni ju" DR_IN, DR_OUT, DR_PARAMETER Allowed
DT_TEXT - A pointer to a string. DR_IN, DR_OUT, DR_PARAMETER Allowed
DT_MIDI2 - MIDI data. MIDI will arrive as events. DR_IN, DR_OUT Allowed.
DT_DOUBLE - obsolete, don't use.
DT_BOOL - 'bool' datatype, true or false. DR_IN, DR_OUT, DR_PARAMETER Allowed.
DT_FLOAT - A field that accepts any number typed by the user. DR_IN, DR_OUT, DR_PARAMETER Allowed.
DT_INT - A field that accepts any integer number typed by the user. DR_IN, DR_OUT, DR_PARAMETER Allowed.
Events described in se_datatypes.h
UET_STAT_CHANGE - Indicates either user has changed a parameter, or audio data has started/stopped streaming to a pin ST_STATIC, ST_RUN
UET_SUSPEND - SE audio engine has suspended processing on this module to save CPU.
UET_MIDI - MIDI data has arrived on a pin
UET_RUN_FUNCTION - used to schedule a timed event for your module (not avail yet)
UET_IO_FUNC - reserved SE internal use
UET_UI_NOTIFY - general purpose event signals user has clicked/dragged your User Interface (not avail yet)
UET_PROG_CHANGE - user has changed program (patch)
UET_NOTE_ON - reserved for use by "MIDI to CV"
UET_NOTE_OFF - reserved for use by "MIDI to CV"
UET_NOTE_MUTE - reserved for use by "MIDI to CV"
UET_PITCH_BEND - reserved for use by "MIDI to CV"
UET_AFTERTOUCH - reserved for use by "MIDI to CV"
UET_START_PORTAMENTO - reserved for use by "MIDI to CV"
UET_WS_TABLE_CHANGE - reserved for use by "Waveshaper"
UET_DELAYED_GATE - reserved for use by "MIDI to CV"
UET_PARAM_AUTOMATION - reserved for use by "MIDI to CV"
UET_NOTE_OFF_HELD - reserved for use by "MIDI to CV"
UET_HELD_NOTES_OFF - reserved for use by "MIDI to CV"
Recommended Plug order
The recommended order to list plugs is inputs first, then outputs, then parameters. Auto-duplicate plugs are usually last (input or output). If you need to upgrade your module's plugs after it's initial release, you can only add plugs to the bottom of the list. This keeps the numbering consistent when loading older .se1 files. You can't remove pins, but you can make them private (hidden from user). If you really need to remove pins or change the pin ordering, make a new module.
Module Calling sequence
When SynthEdit starts, it scans your Module folder for .sep modules. Each is loaded and it's details queried..
plugin is loaded.
constructor is called: Module::Module()
getModuleProperties()
getPinProperties() called once per plug
plug is destroyed.....
destructor is called Module::~Module()
So when SE first loads, it creates your plugin, to query it's abilities. But it doesn’t use it. So best not to do anything intensive in Module:: Module()
later, user pushes "PLAY"
plug-in is loaded,
constructor is called: Module::Module()
Open() called just once, good place to initialize data etc.
Audio starts streaming...
sub_process() called repeatedly....
OnPlugStateChange() called as needed in between sub_process() calls
user pushes "STOP"
Close()
plug is destroyed.....
destructor is called Module::~Module()
Close() is the place to release any resources allocated in Open(). Use the destructor, Module::~Module() to release resources allocated in the constructor, Module::Module(). Remember also that several instances of your plug-in may be used at once, therefore your module might allocate memory several times.
Once your module is running, SynthEdit will never change the samplerate. To change the samplerate, SynthEdit closes, destroys, then re-creates your module.
Scheduled function calls - RUN_AT macro
Your module can schedule a future action. For example, I am working on a 'joystick input' module, it needs to check the PC's joystick at regular intervals, say 20 times per second, so I add a function to my plug-in..
void Module::QueryJoyStick()
{
JOYINFO ji;
joyGetPos(0,&ji);
// etc...
RUN_AT(SampleClock() + 2000, &Module::QueryJoyStick );
}
the last line there is scheduling a call to QueryJoyStick() roughly 1/20 second into the future (2000 samples to be precise). So the SDK is taking care of the timing for you. You can have as many of these scheduled events as you need. In this example, the Module::Open() function needs to call QueryJoyStick() once, to 'kick start' the repeating calls.
Power Saving - plug 'status'
Each module is aware of it's input plug's 'state'..
ST_RUN - Constantly changing audio (Oscillator output, moving knob)
ST_STATIC - Fixed 'flat line' signal (silence or a slider output 'at rest').
SynthEdit notifies your module whenever the state of a pin changes. To do this, SE calls your OnPlugStateChange(...). These messages can occur part-way though an audio buffer. For example the first half of a buffer may contain silence, and the second half a waveform.
This information can help your module reduce CPU use by bypassing it's internal processing when it's input is 'quiet'.
NOTE: Such 'power saving' code is entirely optional. The code required is fairly complicated. I recommend you first create a simple, straight-forward module, then add power saving later.
Example: Take a module with one input / one output, it converts the output of a slider to a non-linear scale (decibels perhaps). Most of the time the slider is 'at rest', your plug-in can save a lot of CPU if it realizes it has no work to do.
Even if you ignore the input pin status, you still need to set your output pins status correctly. At a minimum, set all output pins to ST_RUN at startup.
To set your output plug's status..
getPin(PN_OUTPUT1)->TransmitStatusChange( SampleClock(), ST_RUN);
If you don't correct setup your output pins, downstream modules will assume they have no work to do, and ignore your plugin. If you forget to do so, you typically hear the first buffer of your module's output, followed by silence or at best, clicks and pops
For an example of OnPlugStateChange() and 'power saving' in action, Check out the se_gain_mk2 example module.
Included in the SDK is a handy prefab diagnostic module "monitor.se1", connect to your module's output plug to display it's output status (STATIC/RUN).
Multiple process functions optimized for common usage scenarios
Most module inputs can be categorized into two types: audio inputs and control inputs. Control inputs are usually connected to a knob that doesn't change often.
In a modular environment these categories are fairly loose. You need to plan for the possibility someone might connect an audio signal to your control input.
Most of the time, your module's control inputs will not be changing. So it is inefficient to be checking them repeatedly in sub_process(). On the other hand, if you ignore those parameters in sub_process, you will get zipper noise (steps and clicks) when the user moves a knob.
The solution is to create two sub_process functions. One is the 'fast' sub_process ( optimized for when no knobs are moving), the other is the 'smooth' sub_process (uses more CPU but handles knob movements smoothly)...
void Module::sub_process_fast(..)
{
// assume controller is not changing
}
void Module::sub_process_smooth(..)
{
// update control every sample float
*ctrl = buffer_offset + controller_buffer;
// ...etc.
}
The gain_mk2 example has two sub_process functions.
SynthEdit informs you when a knob starts moving (the pin's state changes to ST_RUN ) .. and when it stops moving (the pin's state changes to ST_STATIC). In OnPluginStateChange, check the status of the control pins, if any are ST_RUN use your 'smooth' sub_process, else use your 'fast' sub_process...
void Module::OnPlugStateChange( SEPin *pin)
{
if( getPin(PN_CONTROLLER)-> getStatus() == ST_RUN )
{
SET_PROCESS_FUNC( Module::sub_process_smooth );
}
else
{
SET_PROCESS_FUNC( Module::sub_process_fast );
}
// ....etc.
The downside of this approach is that you may end up with many sub_process functions, each optimized for a different situation. The most difficult modules to optimize this way are ones with many inputs. It's best to decide what the most common uses of your module are, and write optimized sub_process routines for only those few cases.
Power Saving - putting a module to sleep so that it consumes no CPU when its idle
Many modules have the potential to reduce CPU consumption in certain situations, like when the input signal is silence often the output is also silence. In these cases a module can enter sleep mode.
The advanced gain example /se_gain_mk2 shows how to do this. The command that sends a module to sleep is...
CallHost(seaudioMasterSleepMode);
Note: There are some extra complications. Your module can't just stop processing at any time. It must meet 2 conditions:
-All outputs are inactive. (status ST_STATIC).
-All samples in it's output buffers are the same value.
Remember 'downstream' modules may continue to loop through your output buffer. The easiest way to achieve this is: once your module's output is inactive and stable:
1) your output pin should send an ST_STATIC message.
2) process an additional 96 samples. before sleeping. ( there are 96 samples in your output buffer ). That's the purpose of the static_count variable in the example code, to count out an extra 96 samples.
3) put your module to sleep.
NOTE: The size of your output buffer may vary depending on the user's soundcard settings. Always use the SDK function getBlockSize() to determine the actual figure.
// call the regular sub-process routine, and counts down the 96 samples...
void Module::sub_process_static(long buffer_offset, long sampleFrames )
{
sub_process( buffer_offset, sampleFrames );
static_output_count -= sampleFrames; // add: int static_output_count. to your header file...
if( static_output_count <= 0 )
{
CallHost(seaudioMasterSleepMode);
}
}
Now, you just need to choose which sub-process to use...
void Module::OnPlugStateChange( SEPin *pin)
{
// first the usual stuff...
state_type out_stat = getPin(PN_INPUT1)->getStatus();
getPin(PN_OUTPUT1)->TransmitStatusChange( SampleClock(), out_stat);
// now set the appropriate sub-process
if( out_stat == ST_RUN )
{
SET_PROCESS_FUNC(Module::sub_process); // business as usual
}
else // start counting how many samples of silence we have sent
{
static_output_count = getBlockSize(); // usually 96 samples, but never assume it.
SET_PROCESS_FUNC( Module::sub_process_static);
}
}
Feel free to ask questions about this on the SDK group. It takes some thinking to understand fully. Remember, while you're developing your plug-in, don't worry too much about this 'power saving' stuff. It is optional.
Note: seaudioMasterSleepMode should be the last host call. Calling TransmitStatusChange after that can crash SynthEdit.
Suspend mode
Similar to sleep mode is suspend mode (forced-sleep). This applies only to polyphonic modules in a synthesizer.
When playing a synth voice, SynthEdit monitors the note's audio output. Once the note is finished and silent all modules in the voice are suspended (forced to sleep). Your module has no control over this.
Any signal sent to a suspended module (from the GUI class, or from another module) is held until the module wakes again due to a note-on.
Voice Reset
When a new note is played your module is woken up, at this time some modules need to reset themselves e.g. Envelope modules need to restart at zero.
This is done by monitoring SynthEdit's "Voice Active" signal. This signal is 1 when a note starts playing, and 0 during note-off.
Add a pin to your module like so. The pin name must be exactly as shown...
#define IO_HOST_CONTROL 0x200000
case PN_VOICE_ACTIVE:
properties->name = "Voice/Active";
properties->variable_address = &m_voice_active;
properties->direction = DR_IN;
properties->datatype = DT_FLOAT;
properties->flags = IO_HIDE_PIN|IO_HOST_CONTROL;
break;
You can detect a voice reset situation like so...
void Module::OnPlugStateChange(SEPin *pin)
{
if( pin->getPinID() == PN_VOICE_ACTIVE )
{
if( m_voice_active == 1.0f )
{
// SynthEdit has reset this voice.
// Envelopes now reset to zero.
_RPT0(_CRT_WARN, "Voice Reset\n" );
}
}
Auto-Duplicating Pins
Some modules like the Switch modules have 'spare' pins that automatically add new pins as you use them. These are called Autoduplicating pins.
To make a pin Autoduplicate add flag IO_AUTODUPLICATE. If you want it's name to change when you connect it, use IO_RENAME...
case 0:
properties->name = "Spare";
properties->direction = DR_IN;
properties->datatype = DT_FSAMPLE;
properties->flags = IO_AUTODUPLICATE|IO_RENAME;
If you want a List plug to reflect the names of the autoduplicating plugs...
case 1:
properties->name = "Choice";
properties->direction = DR_IN;
properties->datatype = DT_ENUM;
properties->datatype_extra = "{AUTO}";
break;
To access the variables on Autoduplicate plugs ( as on SE's Many-to-One switch) you need to allocate some memory to hold pointers to the variables...
in header module.h ... h ...
int dynamic_plugs_count;
float **dynamic_plugs;
In module.cpp
void Module::open()
{
int normal_out_plugs = 3; // however many 'normal' output plugs
int num_in_plugs = CallHost(seaudioMasterGetInputPinCount);
int num_out_plugs = CallHost(seaudioMasterGetOutputPinCount);
dynamic_plugs_count = num_out_plugs - normal_out_plugs;
dynamic_plugs = new float *[ dynamic_plugs_count];
for(int i = 0 ; i < dynamic plugs count ; i++ )
{
dynamic_plugs[i] = (float *)CallHost(seaudioMasterGetPinVarAddress, normal_out_plugs+i);
}
//etc...
}
void Module::sub_process(long buffer_offset, long sampleFrames )
{
for(int i = 0 ; i < dynamic_plugs_count; i ++ )
{
float *out = buffer_offset + dynamic_plugs[i];
for(int s = sampleFrames ; s > 0 ; s-- )
{
*out++ = (float) i; // just testing, different voltage out each one
}
}
}
Module::~Module()
{
// This is where you free any memory/resources your module has created
delete [] dynamic_plugs;
}
Copy protection. Preventing other people using your module
void Module::open() // How to prevent other people using your custom module.
{
// query the current registered user's name
char host_registered_to[100];
CallHost( seaudioMasterGetRegisteredName, 0,0, host_registered_to );
bool registered_to_me = strcmp(host_registered_to, "Jeff McClintock") == 0;
if( !registered_to_me )
{
// do something appropriate here if you don't want other people using your module
}
}
Will the SynthEdit-built plugin's sem folder become embedded in the single VST dll.
In future SynthEdit might be enhanced so that embedded sems do not need unloading to disk, But this feature is technically difficult and non-portable to Mac or Linux.
My general opinion is that the main reason for this is to give the modules good copy-protection... But there may be easier ways to provide that without complicated code. For example SDK V3 modules strip out the module 'meta-data' (name, unique-id, plug list) during save-as-vst. That makes embedded sems unusable, and if loaded into SE show a blank name and zero plugs. The module is be of no use.
I am not so concerned about the unloaded sems being 'messy' because the end user, using perhaps Cubase, doesn't see these files. Many non-SynthEdit plugins also have external support files.
Also I've had complaints that embedding files in the VST plugin wastes RAM (embedded files need to be copied to either disk, or to (additional) RAM memory before use), so for example a 100MB Soundfont ends up using 200MB when loaded.
Merging several modules into one SEM file
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.
Here's the code in SeModuleMain.cpp that informs SynthEdit what modules are in the dll...
int getModuleProperties(int p_index, SEModuleProperties* properties)
{
properties->sdk_version = SDK_VERSION;
switch( p_index )
{
case 0:
{
// describe the plugin, this is the name the end-user will see.
properties->name = "Int To List2";
properties->id = "SynthEdit Int To List2";
.. The bit where it says 'case 0'.. that's the first module in the dll. Add more cases like 'case 1:' etc.
... It makes sense to name your module classes clearly, like JM_Flanger, JM_Reverb instead of the generic 'Module', 'Module2'. Same with makeModule() function, add extra cases.
Iterating through all clones
In a polyphonic synth, your module may be 'cloned', if you need to step though your module's clones...
// iterate through all clones (example code)
SEMod_struct_base2 *clone_struct;
// get first one
CallHost(seaudioMasterGetFirstClone,0,0,&clone_struct);
while( clone_struct != 0 ) // convert host's clone pointer to a 'Module' object
{
*clone = ((Module *)(clone_struct->object));
// Access each clone here
// step to next clone
clone->CallHost(seaudioMasterGetNextClone,0,0,&clone_struct);
}
Displaying a pop-up menu
void GuiModule::OnLButtonDown( SEWndInfo *wi, UINT nFlags, sepoint point )
{
//* testing pop up menu...
// get parent HWND
long parent_context = wi-> context_handle;
HWND h = 0;
while( h == 0 )
{
parent_context = CallHost(seGuiHostGetParentContext, parent_context );
h = (HWND) CallHost( seGuiHostGetWindowHandle, parent_context );
}
sepoint offset(point);
CallHost(seGuiHostMapClientPointToScreen, wi->context_handle, 0, &offset, 0 );
// create a pop-up menu
HMENU hm = CreatePopupMenu();
// add some items to it..
AppendMenu( hm, MF_STRING , 1, "Cat" );
AppendMenu( hm, MF_STRING , 2, "Dog" );
// show the menu
int selection = TrackPopupMenu( hm, TPM_LEFTALIGN|TPM_NONOTIFY|TPM_RETURNCMD,offset.x ,offset.y,0, h, 0);
// clean up
DestroyMenu(hm);
Patch-store pins
The Patch Parameter modules store data in the VST chunk. You can store your own data too (int, float, or text, at present) by using a 'Patch Store' pin.
To create a patch-store pin, flag an input pin with IO_PATCH_STORE. SynthEdit will add a matching VST parameter to your plug-in.
A patch-store pin's value will be stored in the patch memory, it will be loaded/saved with VST bank and patch files, and it will appear on the synth's automation page.
Useful flags:
IO_PAR_PRIVATE - Do NOT expose to the end-user as a tweakable parameter on the automation screen.
IO_MINIMISED - Show the pin on the right-click Properties screen (but not the structure view)
IO_PRIVATE - Do NOT show on the structure view. Do not show on Property screen either. Keeps the pin's value secret from the end-user.
For an example of this see the Waveshaper example project. The waveshaper stores it's 'shape' X-Y co-ordinates in a string "[0,0],[1,1] .. etc.". These are displayed graphically and can be manipulated with the mouse. The pin is marked private so the end-user can't manipulate the string directly.
SynthEdit tags each Parameter in the patch memory with a unique 32 bit identifier. SynthEdit chooses the identifier at random and ensures no two parameters have the same ID. The purpose of the ID is to ensure when you save/load a bank to disk, SE can restore each parameter correctly, even if you have since re-named the parameter, even if you have deleted or added some parameters, even if two parameters have the same name.
Communication between DSP and GUI classes
SynthEdit maintains a clear separation between your audio (DSP) and graphics (GUI) code. This is not only good programming practice, it allows SynthEdit to provide automation and patch storage with very little effort on your part (just set a flag).
There are 3 ways to send data between your modules GUI and DSP classes:
1) Patch Store pins
2) Dual pins
3) Sending raw string data
1 - Patch Store pins
This is the recommended method. GUI and DSP classes each need a matching patch-store pin (use flag IO_PATCH_STORE). Both pins must be the same datatype. There are two possible configurations: DSP->GUI or GUI->DSP. Communication is one-way, to send data both directions requires two sets of pins.
To send data from GUI to DSP you need:
- an input pin on the DSP Module
- an input pin on the GUI Module
Flag both pins IO_PATCH_STORE, this tells SynthEdit to connect the two. Flag the DSP pin as IO_HIDE_PIN because DSP pins can accept only one input. This prevents the DSP pin being accessible to the end-user.
Note GUI pins are bi-directional, you can send data 'out' a GUI 'input' pin. SynthEdit sends the data to the DSP input pin.
To send data from DSP to GUI you need:
- an output pin on the DSP Module
- an input pin on the GUI Module
Flag both pins IO_PATCH_STORE, this tells SynthEdit to connect the two.
If the communication is private, i.e. sending some data you don't need the end user to see, also flag the GUI pin IO_HIDE_PIN. That makes the pins not visible on the structure view.
Example code: sending a floating point value from the DSP Module to the GUI Module...
Describing the pins:
bool Module::getPinProperties (long index, SEPinProperties* properties)
{
switch( index )
{
case 0: // hidden pin that sends data from DSP to the GUI module
properties->name = "gui_com_pin";
properties-> variable_address = &out_val;
properties->direction = DR_OUT;
properties->datatype = DT_FLOAT;
properties->flags = IO_PATCH_STORE|IO_HIDE_PIN;
break;
// GUI PIN. Appears only on GUI object
case 1: // this GUI pin receives data from the DSP Module
properties->name = "Val from DSP";
properties->direction = DR_IN;
properties-> datatype = DT_FLOAT;
properties->flags = IO_PATCH_STORE|IO_UI_COMMUNICATION;
break;
default:
return false;
};
return true;
}
Sending the data:
out_val = 23.75;
// inform SE pin's value has changed. SE will send the new value to the GUI
getPin(PN_OUT)->TransmitStatusChange( SampleClock(), ST_STATIC );
Receiving the data:
void GuiModule::OnGuiPinValueChange( SeGuiPin *p_pin )
{
float val_from_dsp = getPin(1)->getValueFloat();
}
The same technique works on other datatypes too like DT_INT, DT_BOOL, DT_TEXT, DT_ENUM. It does not work on audio or MIDI data (DT_FSAMPLE, DT_MIDI).
2 - Dual pins
This technique preceded the patch-store pins, but works exactly the same way. It works only for GUI to DSP communication, not the other direction.
The difference with Dual pins is: You list only one pin and mark it with the IO_UI_COMMUNICATION_DUAL flag. This type of pin appears on both the GUI and DSP class. This is exactly the same as declaring two regular pins (one GUI, one DSP). The pin direction must be DR_IN.
There is no advantage to this technique over IO_PATCH_STORE pins, both techniques function the same. Do not mix IO_UI_COMMUNICATION_DUAL and IO_PATCH_STORE flags. Do not use both in the same module. Doing so confuses SynthEdit's pin numbering.
3 - Raw string data
Before patch-store pins, there was only one communication method, sending a string between GUI and DSP. This is still supported, and is useful for custom data, and large amounts of data. Note this technique can send any binary data not only strings. You are limited to about 1000 bytes total though.
See the se_scope example module for an example of how to use the SendStringToGui() and SendStringToAudio() functions to send raw binary data.
This technique is due to be superseded by true binary data pins in SynthEdit Version 1.1
Multiple Patch-Store pins
When using 2 or more sets of linked pins it's important to list GUI and DSP pins in the same order. For example both following configurations are OK, in both cases SE connects GUI pin A to DSP pin A, and GUI pin B to DSP pin B:
GUI and DSP pins in separate groups (recommended)
pin
0 - DSP INT A
1 - DSP INT B
2 - GUI INT A
3 - GUI INT B
GUI and DSP pins interleaved
0 - DSP INT A
1 - GUI INT A
2 - DSP INT B
3 - GUI INT B
Does the connection between GUI->DSP have to be a dedicated pair of pins only used for that purpose?
Putting IO_PATCH_STORE on a pin connects it (invisibly) to the Patch-Change module and therefore indirectly to your other class (GUI or DSP).
DSP inputs accept only one connection, so if it's used for Patch-Store, it can’t have additional connections (hence IO-Private being needed).
GUI inputs accept multiple connections, so no problem with having PATCH-STORE and user connections.
DSP outputs can have multiple connections, so again PATCH-STORE plus user-connection is OK.
Patch-store pins Summary:
Type Can be visible/connected by end-user
DSP-IN NO
DSP-OUT YES
GUI-IN YES
GUI-OUT NO (PATCH-STORE not supported at all)
GUI vs. DSP Pin Numbering
Very important point: GUI Pins are not visible to the DSP class.
For example here is the description of two pins..
bool Module::getPinProperties (long index, SEPinProperties* properties)
{
switch( index )
{
case PN_GUI_FLOAT_PIN:
properties->name = "GUI Float Pin";
properties->direction = DR_IN;
properties->datatype = DT_FLOAT;
properties->flags = IO_UI_COMMUNICATION;
break;
case PN_DSP_INT_PIN:
properties->name = "DSP Int Pin";
properties->direction = DR_IN;
properties->datatype = DT_INT;
break;
default:
return false;
};
return true;
}
Q: What pin number is the first pin?
A: Zero
Q: What pin number is the second pin?
A: It depends..
To access the second pin from the DSP class, it's index is zero. The first pin don't appear on the DSP Module because it's a GUI pin...
#define PN_DSP_INT_PIN 0
Another way of looking at it: Ignore GUI pins when calculating the DSP pin index.
..but in the GUI Class (GUIModule) it's pin number 1 ...(all pins appear on the GUI class)
#define PN_GUI_FLOAT_PIN 0
#define PN_DSP_INT_PIN 1
getPin( PN_DSP_INT_PIN )->DoSomething();
Simplify your life by listing all your DSP pins first...
bool Module::getPinProperties (long index, SEPinProperties* properties)
{
switch( index )
{
case PN_DSP_INT_PIN:
properties->name = "DSP Int Pin";
properties->direction = DR_IN;
properties->datatype = DT_INT;
break;
case PN_GUI_FLOAT_PIN:
properties->name = "GUI Float Pin";
properties->direction = DR_IN;
properties->datatype = DT_FLOAT;
properties->flags = IO_UI_COMMUNICATION;
break;
default:
return false;
};
return true;
}
Now both GUI and DSP classes use the same numbering...
#define PN_DSP_INT_PIN 0
#define PN_GUI_FLOAT_PIN 1
Including your modules in the official SynthEdit?
Currently Jeff does not distribute 3rd-party modules in the official SynthEdit release.
Modules shipped with SynthEdit are perceived as being part-of SynthEdit. Users rightly expect Jeff to support them, supply bug-fixes, answering support emails etc.
Much of my time is already spent on administrative tasks such as replying to emails, processing orders, keeping the web page up to date. I prefer to spend time on improving SynthEdit.
Also there are some moral issues. Imagine you donate your modules to SynthEdit. Later Jeff sells SynthEdit to Microsoft for 2 million dollars. In hindsight would you donate to make Microsoft richer?, Don't you deserve some of that money? These questions show the need for a good legal contract. At this time I prefer to avoid the complication.
I am happy to host any modules on the SynthEdit website because that makes it clear who wrote the module, allows me to give you due credit, and allows you to withdraw it at any time. If you prefer a link to your website, no problem.
It would be great to have a "SynthEdit expansion pack" available. Much more convenient to get a whole bunch of modules at once. The main challenge I see is ensuring everyone involved was fairly compensated. Also the various licenses of several authors need be reconciled. It would require some diplomacy.
Module won't load on some systems
Often modules (dlls) have dependencies on other system files. Like if you use MS C++ it might need the C Library runtime dlls to function. There's a utility called depends.exe that tells you what files a module depends on to load, and shows if you have those files installed or not.