Skip to content

Commit

Permalink
Adds support for IMidiLearn, fixes bool marshalling
Browse files Browse the repository at this point in the history
  • Loading branch information
azeno committed Oct 28, 2024
1 parent 0b35a01 commit b869a9c
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 34 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ From https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentat
- UI Snapshots - no
- NoteExpression Physical UI Mapping - no
- Legacy MIDI CC Out Event - no
- MIDI Learn - no
- MIDI Learn - yes
- Host Query Interface support - no
- MPE support for Wrappers - no
- Parameter Function Name - no
Expand Down
48 changes: 33 additions & 15 deletions src/EffectHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,10 @@ public void Update()

private void HandleMidiMessage(IMidiMessage message)
{
var midiMapping = controller as IMidiMapping;
if (message is ChannelMessage channelMessage)
{
var midiChannel = (short)channelMessage.MidiChannel;

// TODO: Midi Stop All see https://forums.steinberg.net/t/vst3-velocity-clarification/908883
if (channelMessage.IsNoteOn())
{
Expand All @@ -296,27 +297,44 @@ private void HandleMidiMessage(IMidiMessage message)
noteId: channelMessage.Data1);
inputEventQueue.Add(Event.New(e, busIndex: 0, sampleOffset: 0, ppqPosition: 0, isLive: false));
}
else if (channelMessage.Command == ChannelCommand.Controller)
else
{
if (midiMapping is null)
return;
// IMidiMapping and IMidiLearn expect to be called on main thread
synchronizationContext?.Post(m => HandleMidiMessageOnMainThread((ChannelMessage)m!), channelMessage);
}
}
}

private void HandleMidiMessageOnMainThread(ChannelMessage channelMessage)
{
var midiChannel = (short)channelMessage.MidiChannel;
var midiMapping = controller as IMidiMapping;

if (midiMapping.getMidiControllerAssignment(0, (short)channelMessage.MidiChannel, (ControllerNumbers)channelMessage.Data1, out var paramId))
if (channelMessage.Command == ChannelCommand.Controller)
{
var midiLearn = controller as IMidiLearn;
if (midiLearn != null)
{
if (midiLearn.onLiveMIDIControllerInput(0, midiChannel, (ControllerNumbers)channelMessage.Data1))
{
var value = MessageUtils.MidiIntToFloat(channelMessage.Data2);
synchronizationContext?.Post(s => SetParameter(paramId, value), null);
// Plugin did map the controller to one of its parameters
}
}
else if (channelMessage.Command == ChannelCommand.PitchWheel)
if (midiMapping != null && midiMapping.getMidiControllerAssignment(0, midiChannel, (ControllerNumbers)channelMessage.Data1, out var paramId))
{
if (midiMapping is null)
return;
var value = MessageUtils.MidiIntToFloat(channelMessage.Data2);
SetParameter(paramId, value);
}
}
else if (channelMessage.Command == ChannelCommand.PitchWheel)
{
if (midiMapping is null)
return;

if (midiMapping.getMidiControllerAssignment(0, (short)channelMessage.MidiChannel, ControllerNumbers.kPitchBend, out var paramId))
{
var value = channelMessage.GetPitchWheel();
synchronizationContext?.Post(s => SetParameter(paramId, value), null);
}
if (midiMapping.getMidiControllerAssignment(0, midiChannel, ControllerNumbers.kPitchBend, out var paramId))
{
var value = channelMessage.GetPitchWheel();
SetParameter(paramId, value);
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/VL.Audio.VST.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VL.Audio.VST", "VL.Audio.VST.csproj", "{140ABA84-F20C-4851-BDC4-A16C45E1153D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{05EC4CAB-1E18-4BA4-BB68-E5EB7D76B77A}"
ProjectSection(SolutionItems) = preProject
..\README.md = ..\README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
84 changes: 84 additions & 0 deletions src/VST3/IMidiLearn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace VST3;

/// <summary>
/// MIDI Learn interface: Vst::IMidiLearn
/// </summary>
/// <remarks>
/// <para>
/// If this interface is implemented by the edit controller, the host will call this method whenever
/// there is live MIDI-CC input for the plug-in. This way, the plug-in can change its MIDI-CC parameter
/// mapping and inform the host via the IComponentHandler::restartComponent with the
/// kMidiCCAssignmentChanged flag.
/// </para>
/// <para>
/// Use this if you want to implement custom MIDI-Learn functionality in your plug-in.
/// </para>
/// <example>
/// <code>
/// //------------------------------------------------
/// // in MyController class declaration
/// class MyController : public Vst::EditController, public Vst::IMidiLearn
/// {
/// // ...
/// //--- IMidiLearn ---------------------------------
/// tresult PLUGIN_API onLiveMIDIControllerInput (int32 busIndex, int16 channel,
/// CtrlNumber midiCC) SMTG_OVERRIDE;
/// // ...
///
/// OBJ_METHODS (MyController, Vst::EditController)
/// DEFINE_INTERFACES
/// // ...
/// DEF_INTERFACE (Vst::IMidiLearn)
/// END_DEFINE_INTERFACES (Vst::EditController)
/// //...
/// }
///
/// //------------------------------------------------
/// // in mycontroller.cpp
/// #include "pluginterfaces/vst/ivstmidilearn.h"
///
/// namespace Steinberg {
/// namespace Vst {
/// DEF_CLASS_IID (IMidiLearn)
/// }
/// }
///
/// //------------------------------------------------------------------------
/// tresult PLUGIN_API MyController::onLiveMIDIControllerInput (int32 busIndex,
/// int16 channel, CtrlNumber midiCC)
/// {
/// // if we are not in doMIDILearn (triggered by a UI button for example)
/// // or wrong channel then return
/// if (!doMIDILearn || busIndex != 0 || channel != 0 || midiLearnParamID == InvalidParamID)
/// return kResultFalse;
///
/// // adapt our internal MIDICC -> parameterID mapping
/// midiCCMapping[midiCC] = midiLearnParamID;
///
/// // new mapping then inform the host that our MIDI assignment has changed
/// if (auto componentHandler = getComponentHandler ())
/// {
/// componentHandler->restartComponent (kMidiCCAssignmentChanged);
/// }
/// return kResultTrue;
/// }
/// </code>
/// </example>
/// </remarks>
[GeneratedComInterface(Options = ComInterfaceOptions.ComObjectWrapper)]
[Guid("6b2449cc-4197-40b5-ab3c-79dac5fe5c86")]
partial interface IMidiLearn
{
/// <summary>
/// Called on live input MIDI-CC change associated to a given bus index and MIDI channel.
/// </summary>
/// <param name="busIndex">The bus index.</param>
/// <param name="channel">The MIDI channel.</param>
/// <param name="midiCC">The MIDI control change number.</param>
[PreserveSig]
[return: MarshalUsing(typeof(VstBoolMarshaller))]
bool onLiveMIDIControllerInput(int busIndex, short channel, ControllerNumbers midiCC);
}
32 changes: 16 additions & 16 deletions src/VST3/IMidiMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ namespace VST3;

/// <summary>
/// MIDI Mapping interface: Vst::IMidiMapping
/// \ingroup vstIPlug vst301
/// - [plug imp]
/// - [extends IEditController]
/// - [released: 3.0.1]
/// - [optional]
///
/// </summary>
/// <remarks>
/// <para>
/// MIDI controllers are not transmitted directly to a VST component. MIDI as hardware protocol has
/// restrictions that can be avoided in software. Controller data in particular come along with unclear
/// and often ignored semantics. On top of this they can interfere with regular parameter automation and
/// the host is unaware of what happens in the plug-in when passing MIDI controllers directly.
///
/// </para>
/// <para>
/// So any functionality that is to be controlled by MIDI controllers must be exported as regular parameter.
/// The host will transform incoming MIDI controller data using this interface and transmit them as regular
/// parameter change. This allows the host to automate them in the same way as other parameters.
/// CtrlNumber can be a typical MIDI controller value extended to some others values like pitchbend or
/// aftertouch (see \ref ControllerNumbers).
/// aftertouch (see <see cref="ControllerNumbers"/>).
/// If the mapping has changed, the plug-in must call IComponentHandler::restartComponent (kMidiCCAssignmentChanged)
/// to inform the host about this change.
/// </summary>
/// </para>
/// </remarks>
/// <example>
/// <code>
/// //--------------------------------------
Expand Down Expand Up @@ -62,11 +61,12 @@ partial interface IMidiMapping
/// <summary>
/// Gets an (preferred) associated ParamID for a given Input Event Bus index, channel and MIDI Controller.
/// </summary>
/// <param name="busIndex">index of Input Event Bus</param>
/// <param name="channel">channel of the bus</param>
/// <param name="midiControllerNumber">see \ref ControllerNumbers for expected values (could be bigger than 127)</param>
/// <param name="id">return the associated ParamID to the given midiControllerNumber</param>
/// <returns>True if the assignment is successful, otherwise false</returns>
[return: MarshalAs(UnmanagedType.Bool)]
/// <param name="busIndex">Index of Input Event Bus.</param>
/// <param name="channel">Channel of the bus.</param>
/// <param name="midiControllerNumber">See <see cref="ControllerNumbers"/> for expected values (could be bigger than 127).</param>
/// <param name="id">Returns the associated ParamID to the given midiControllerNumber.</param>
/// <returns>True if the assignment is successful, otherwise false.</returns>
[return: MarshalUsing(typeof(VstBoolMarshaller))]
[PreserveSig]
bool getMidiControllerAssignment(int busIndex, short channel, ControllerNumbers midiControllerNumber, out ParamID id);
};
}
6 changes: 4 additions & 2 deletions src/VST3/IUnitInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ partial interface IUnitInfo
void getProgramInfo(ProgramListID listId, int programIndex,
[MarshalAs(UnmanagedType.LPStr)] string attributeId /*in*/, String128 attributeValue /*out*/);

/** Returns kResultTrue if the given program index of a given program list ID supports PitchNames. */
[return: MarshalAs(UnmanagedType.Bool)] bool hasProgramPitchNames(ProgramListID listId, int programIndex);
/** Returns kResultTrue if the given program index of a given program list ID supports PitchNames. */
[PreserveSig]
[return: MarshalUsing(typeof(VstBoolMarshaller))]
bool hasProgramPitchNames(ProgramListID listId, int programIndex);

/** Gets the PitchName for a given program list ID, program index and pitch.
If PitchNames are changed the plug-in should inform the host with IUnitHandler::notifyProgramListChange. */
Expand Down
22 changes: 22 additions & 0 deletions src/VST3/VstBoolMarshaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace VST3;

[CustomMarshaller(typeof(bool), MarshalMode.Default, typeof(VstBoolMarshaller))]
internal static unsafe class VstBoolMarshaller
{
public static bool ConvertToManaged(int unmanaged)
{
switch (unmanaged)
{
case 0:
return true;
case 1:
return false;
default:
Marshal.ThrowExceptionForHR(unmanaged);
return false;
}
}
}

0 comments on commit b869a9c

Please sign in to comment.