VST 3 Interfaces VST 3.6.11 SDK for developing VST Plug-in

- Main Page
- Related Pages
- Modules
- Namespaces
- Classes
- Files

All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Groups Pages
Note Expression Support

A new way to control / modify / change a specific played note during the playback.

# Introduction

Note Expression is a new way of event controller editing in host supporting this VST 3.5 feature (in Cubase since version 6).
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:

1. The instrument Plug-in must have at least one input event bus.
   //------------------------------------------------------------------------
   tresult PLUGIN_API MyExampleProcessor::initialize (FUnknown* context)
   {
   //---always initialize the parent-------
   tresult result = AudioEffect::initialize (context);
   if (result == kResultTrue)
   {
   // we want a Stereo Output
   addAudioOutput (STR16 ("Stereo Output"), SpeakerArr::kStereo);
   // create Event In bus (1 bus with only 1 channel)
   addEventInput (STR16 ("Event Input"), 1);
   }
   return result;
   }
   //------------------------------------------------------------------------
2. The controller must provide the Steinberg::Vst::INoteExpressionController interface, like below:
   //-----------------------------------------------------------------------------
   class MyExampleController: public EditController, public INoteExpressionController
   {
   public:
   ...
   //---from INoteExpressionController
   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)
   ...
   };
   //-----------------------------------------------------------------------------

1. 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)
   {
   // we accept only the first bus and 1 channel
   if (busIndex == 0 && channel == 0)
   return 1;
   return 0;
   }
   //------------------------------------------------------------------------

1. 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)
   {
   // we accept only the first bus and 1 channel and only 1 Note Expression (tuning)
   if (busIndex == 0 && channel == 0 && noteExpressionIndex == 0)
   {
   memset (&info, 0, sizeof (NoteExpressionTypeInfo));
   // set the tuning type
   info.typeId = kTuningTypeID;
   // set some strings
   USTRING ("Tuning").copyTo (info.title, 128);
   USTRING ("Tun").copyTo (info.shortTitle, 128);
   USTRING ("Half Tone").copyTo (info.units, 128);
   info.unitID = -1; // no unit wanted
   info.associatedParameterID = -1; // no associated parameter wanted
   info.flags = NoteExpressionTypeInfo::kIsBipolar; // event is bipolar (centered)
   // for Tuning the convert functions are : plain = 240 * (norm - 0.5); norm = plain / 240 + 0.5;
   // we want to support only +/- one octave
   double kNormTuningOneOctave = 12.0 / 240.0;
   info.valueDesc.minimum = 0.5 - kNormTuningOneOctave;
   info.valueDesc.maximum = 0.5 + kNormTuningOneOctave;
   info.valueDesc.defaultValue = 0.5; // middle of [0, 1] => no detune (240 * (0.5 - 0.5) = 0)
   info.valueDesc.stepCount = 0; // we want continuous (no step)
   return kResultTrue;
   }
   return kResultFalse;
   }
   //------------------------------------------------------------------------

1. For displaying note expression values, we have to implement the conversion functions:
   - Steinberg::Vst::INoteExpressionController::getNoteExpressionStringByValue : normalized value -> string
     //------------------------------------------------------------------------
     tresult PLUGIN_API MyExampleController::getNoteExpressionStringByValue (int32 busIndex, int16 channel, NoteExpressionTypeID id,
     NoteExpressionValue valueNormalized , String128 string);
     {
     // here we use the id (not the index)
     if (busIndex == 0 && channel == 0 && id == kTuningTypeID)
     {
     // here we have to convert a normalized value to a Tuning string representation
     UString128 wrapper;
     valueNormalized = (240 * valueNormalized) - 120; // compute half Tones
     wrapper.printFloat (valueNormalized, 2);
     wrapper.copyTo (string, 128);
     return kResultTrue;
     }
     return kResultFalse;
     }
     //------------------------------------------------------------------------

- Steinberg::Vst::INoteExpressionController::getNoteExpressionValueByString : string -> normalized value
  //-----------------------------------------------------------------------------
  tresult PLUGIN_API MyExampleController::getNoteExpressionValueByString (int32 busIndex, int16 channel, NoteExpressionTypeID id,
  const TChar* string, NoteExpressionValue& valueNormalized);
  {
  // here we use the id (not the index)
  if (busIndex == 0 && channel == 0 && id == kTuningTypeID)
  {
  // here we have to convert a given tuning string (half Tone) to a normalized value
  String wrapper ((TChar*)string);
  ParamValue tmp;
  if (wrapper.scanFloat (tmp))
  {
  valueNormalized = (tmp + 120) / 240;
  return kResultTrue;
  }
  }
  return kResultFalse;
  }
  //------------------------------------------------------------------------

1. 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)
   {
   ....
   // get the input event queue
   IEventList* inputEvents = data.inputEvents;
   if (inputEvents)
   {
   Event e;
   int32 numEvents = inputEvents->getEventCount ();
   // for each events check it..
   for (int32 i = 0; i < numEvents; i++)
   {
   if (inputEvents->getEvent (i, e) == kResultTrue)
   {
   switch (e.type)
   {
   //-----------------------
   case Event::kNoteOnEvent:
   {
   // here a note On, we may need to play something a keep a trace of the e.noteOn.noteId
   break;
   }
   //-----------------------
   case Event::kNoteOffEvent:
   {
   // here we have to release the voice associated to this id : e.noteOff.noteId
   // Note that kNoteExpressionValueEvent event could be send after the note is in released
   break;
   }
   //-----------------------
   case Event::kNoteExpressionValueEvent:
   {
   // here are the Note Expression interpretation
   // we check and use only tuning expression
   if (e.noteExpressionValue.typeId == kTuningTypeID)
   {
   // we have to find the voice which should be change (the note could be in released state)
   VoiceClass* voice = findVoice (e.noteExpressionValue.noteId);
   if (voice)
   {
   // we apply to it the wanted value (for a given type of note expression (detune, volume....)
   voice->setNoteExpressionValue (e.noteExpressionValue.typeId, e.noteExpressionValue.value);
   }
   // if the associated id is not anymore marked as playing voice (end of release reached) we ignore the Note Expression Event
   }
   break;
   }
   }
   }
   }
   }
   ...
   }
   //------------------------------------------------------------------------

That is it!

Back to Contents

Empty

Copyright ©2018 Steinberg Media Technologies GmbH. All Rights Reserved. This documentation is under this license.