Introduction
Note Expression is a new way of event controller editing in host supporting this VST 3.5 feature (like Cubase 6/7/8).
With VST 3 Note Expression, the Plug-in is able to break free from the limitations of MIDI controller events by providing access to new VST 3 controller events that circumvent the laws of MIDI and provide articulation information for each individual note (event) in a polyphonic arrangement according to its noteId.
A major limitation of MIDI is the nature of controller information; controllers are only channel messages (Pitch Bend, Modulation,...) and could not be assigned to a specific playing note, with the exception of poly pressure (polyphonic aftertouch) which allows change only for a given pitch (not a given note!).
Articulating each note in a chord individually creates a much more natural feel, just like multiple players playing the same instrument at the same time but each adding his own personality to the notes played.
For example Cubase 6 introduces the first VST 3 Note Expression compatible virtual instrument: HALion Sonic SE. This Plug-in HALion Sonic SE does not only supports "standard" note expression control for Tuning (Pitch), Volume and Pan, it also offers additional custom pre-assigned note expression types of event (kCustomStart in Steinberg::Vst::NoteExpressionTypeIDs).
How does it work ?
The best way to understand how to support note expression from the Plug-in side, is to check out the step by step implementation example below. For more details, check out the Note Expression Synth example included in the SDK.
Step by Step:
We want a mono-timbral (1 channel) instrument Plug-in with 1 event bus and support for the detune (kTuningTypeID) note expression:
- The instrument Plug-in must have at least one input event bus.
tresult PLUGIN_API MyExampleProcessor::initialize (FUnknown* context)
{
tresult result = AudioEffect::initialize (context);
if (result == kResultTrue)
{
addEventInput (STR16 ("Event Input"), 1);
}
return result;
}
- The controller must provide the Steinberg::Vst::INoteExpressionController interface, like below:
class MyExampleController: public EditController, public INoteExpressionController
{
public:
...
virtual int32 PLUGIN_API getNoteExpressionCount (
int32 busIndex,
int16 channel);
virtual tresult PLUGIN_API getNoteExpressionInfo (
int32 busIndex,
int16 channel,
int32 noteExpressionIndex, NoteExpressionTypeInfo& info);
virtual tresult PLUGIN_API getNoteExpressionStringByValue (
int32 busIndex,
int16 channel, NoteExpressionTypeID
id, NoteExpressionValue valueNormalized ,
String128 string);
virtual tresult PLUGIN_API getNoteExpressionValueByString (
int32 busIndex,
int16 channel, NoteExpressionTypeID
id,
const TChar*
string, NoteExpressionValue& valueNormalized);
...
OBJ_METHODS (MyExampleController, EditController)
DEFINE_INTERFACES
DEF_INTERFACE (INoteExpressionController)
END_DEFINE_INTERFACES (EditController)
REFCOUNT_METHODS(EditController)
...
};
- Now we have to implement the Steinberg::Vst::INoteExpressionController interface, in our example Steinberg::Vst::INoteExpressionController::getNoteExpressionCount should return 1 as we only want to support tuning:
int32 MyExampleController::getNoteExpressionCount (
int32 busIndex,
int16 channel)
{
if (busIndex == 0 && channel == 0)
return 1;
return 0;
}
- Then we have to implement Steinberg::Vst::INoteExpressionController::getNoteExpressionInfo which describes what note expression the Plug-in supports:
tresult PLUGIN_API MyExampleController::getNoteExpressionInfo (
int32 busIndex,
int16 channel,
int32 noteExpressionIndex,
NoteExpressionTypeInfo& info)
{
if (busIndex == 0 && channel == 0 && noteExpressionIndex == 0)
{
memset (&info, 0, sizeof (NoteExpressionTypeInfo));
info.typeId = kTuningTypeID;
USTRING (
"Tuning").copyTo (info.title, 128);
USTRING (
"Tun").copyTo (info.shortTitle, 128);
USTRING (
"Half Tone").copyTo (info.units, 128);
info.unitID = -1;
info.associatedParameterID = -1;
info.flags = NoteExpressionTypeInfo::kIsBipolar;
double kNormTuningOneOctave = 12.0 / 240.0;
info.valueDesc.minimum = 0.5 - kNormTuningOneOctave;
info.valueDesc.maximum = 0.5 + kNormTuningOneOctave;
info.valueDesc.defaultValue = 0.5;
info.valueDesc.stepCount = 0;
}
}
- For displaying note expression values, we have to implement the conversion functions:
- Last step, in the processor component we have to adapt the process call to interpret the note expression event (Steinberg::Vst::NoteExpressionValueEvent) send from the host to the Plug-in:
tresult MyExampleProcessor::process (ProcessData& data)
{
....
IEventList* inputEvents = data.inputEvents;
if (inputEvents)
{
Event e;
int32 numEvents = inputEvents->getEventCount ();
for (
int32 i = 0; i < numEvents; i++)
{
{
switch (e.type)
{
case Event::kNoteOnEvent:
{
break;
}
case Event::kNoteOffEvent:
{
break;
}
case Event::kNoteExpressionValueEvent:
{
if (e.noteExpressionValue.typeId == kTuningTypeID)
{
VoiceClass* voice = findVoice (e.noteExpressionValue.noteId);
if (voice)
{
voice->setNoteExpressionValue (e.noteExpressionValue.typeId, e.noteExpressionValue.value);
}
}
break;
}
}
}
}
}
...
}
That is it!
Back to Contents