Cakewalk Plug-In Development Kit
DXi Software Synthesizers
DirectShow Plug-Ins
MIDI Effects Filters
January 10, 2002
Copyright © 1998- by Twelve Tone Systems, Inc.
All rights reserved.
Cakewalk and SONAR are registered trademarks of
Twelve Tone Systems, Inc.
Microsoft, Windows, DirectX, DirectShow, Visual Studio and Visual C++ are
registered trademarks of Microsoft, Inc.
If you are an experienced DirectShow, MFX or DXi developer, or otherwise seasoned and battle-scarred, this section will help you cut to the chase.
The bare minimum you probably need to read in this document are the chapters named DXi 2.0 Structures and Types and DXi 2.0 Base Classes. Armed with that information, you should next explore the TWONAR sample DXi synthesizer, particularly Twonar.cpp. This should give you a feel for the basic concepts required in building a DXi 2.0 synthesizer.
You should also read about IDeferZeroFill,
because knowing how this works can significantly improve the performance of
your plug-in.
DXi 2.0 has been extensively enhanced and reworked, both to provide new capabilities to SONAR 2 and other host applications, and to simplify the development of DXi software synthesizers and DirectX plug-ins.
Multiple audio output streams from a DXi
MIDI output from an MFX or DXi, to the host
Raw MIDI event processing (e.g. for MTC)
Flexible, high-resolution time-conversion
Per-channel patch, note, RPN and NRPN names
Efficient
processing of silent audio buffers
Pitch marker access
DXi 2.0 includes an overhauled App Wizard, for use with Microsoft Visual C++. With this wizard, the coding and deployment of plug-ins is easier than ever.
Fully editable source code
Single self-contained DLL
Simpler DXi coding model
This section describes the various kinds of audio and MIDI plug-ins available in Cakewalk products, and how they behave in those products. Other host applications may behave differently.
This section focuses on the real-time usage of various kinds of plug-ins, e.g., whether they are used as a “track insert” style effect, whether they can respond to live input “just in time”, etc. All of the effects listed below are also available for use as destructive effects in Cakewalk products, as well as in other host applications.
An MFX effect processes MIDI data in one of several ways.
MIDI events are sent to MFX processes in buffers, implying that processing happens in “near real-time.” The host application will work-ahead by some small amount of time, and this workaround time will determine the size of the MIDI data buffers fed to an MFX.
The following diagram shows how MFX are utilized in Cakewalk products. Each MIDI track has a patch point control, into which one or more MFX may be inserted into an MFX chain. Events from the track are periodically packaged into a buffer and fed to the first MFX in the track’s chain. The MFX produces then produces output buffer, usually based on the contents of the input buffer. This output buffer becomes the input buffer for the next MFX in the chain, and the process continues for all MFX. The output of the last MFX in the chain is sent to the MIDI output port
An MFX processor which generates MIDI data does so via a periodic signal from the host application. No data needs to be on the track for this to happen – the song doesn’t even need to be playing.
In contrast to audio effects, Cakewalk products currently support only track inserts for MIDI effects, not “aux busses” or “master effects”. During playback, MIDI events from each track are run through the chain of effects (if any) for the track.

A DirectX plug-in processes audio data in one of two ways.
The preferred data format for audio processing is 32-bit IEEE floating point. As a practical matter, every commercially available DirectX plug-in supports this format.
The signal flow model for audio in Cakewalk products varies product by product, and is beyond the scope of this document. Please refer to the user’s guide for your host application for the specifics.
For detailed
information about how to develop DirectX audio plug-ins, please refer to the Microsoft DirectShow SDK
documentation.
A DXi has the characteristics of both an MFX effect and a DirectX plug-in. As such, a DXi can behave either as software synthesizer, or as a MIDI controllable audio plug-in.
As an MFX effect, a DXi receives buffers of MIDI data, both streamed and just-in-time. For format of these data buffers is identical to the buffers provided to MFX plug-ins.
As a DirectX plug-in, a DXi can exist anywhere in a project where you would patch an audio effect. Furthermore, a DXi is fed audio data that may have been processed by upstream effects, or data coming out of an audio track. Thus a DXi some has flexibility in choosing how to process audio data:

MFX and DXi plug-ins are COM objects implemented as in-process servers (in DLLs).
You should make as few assumptions as possible about the
behavior of the host application. If you find some area to be ambiguous, rather
than code a specific product’s behavior, please alert Cakewalk so that the
issue can be resolved and/or better documented.
This document can’t provide you with an introduction to COM, but it can recommend some books that you should own:
· Rogerson, Dale, Inside COM. Microsoft Press 1997. This is a good introduction to the fundamentals of COM.
· Kruglinski, David J.; Shepherd, George; and Wingo, Scott. Programming Microsoft Visual C++. Microsoft Press 1998.
Most functions in this document return an HRESULT, as is the COM convention. Rather than listing possible return values with the documentation of each function, we will list several common return values here. Please consult the Windows Platform SDK Documentation for a complete list of values.
|
S_OK |
Method succeeded; or “Yes”, if the method returns a yes/no
answer. |
|
S_FALSE |
Method did not succeed; or “No”, if the method returns a
yes/no answer. |
|
E_INVALIDARG |
An invalid argument was supplied to the method. |
|
E_POINTER |
An invalid pointer value was supplied to the method. |
|
E_OUTOFMEMORY |
A memory allocation failed. |
|
E_FAIL |
An unspecified error occurred. |
All code for object
(de)registration is automatically generated by the App Wizard. What follows is a summary of the registry
keys that are actually written.
When they are installed, MFX filters must modify the Registry to make themselves available to applications.
HKEY_CLASS_ROOT\CLSID\{clsid} = <friendly
name>
HKEY_CLASS_ROOT\CLSID\{clsid}\InProcServer32 = <pathname to
DLL>
HKEY_LOCAL_MACHINE\Software\Cakewalk Music Software\MIDI Filters\{clsid} = <>
Note: The MidiFilter.h file
has a #define for the root key name:
#define
SZ_MIDI_FILTER_REGKEY "Software\\Cakewalk Music Software\\MIDI
Filters"
Because DXi’s are DirectShow filters, they already perform whatever registration is required by DirectShow. In order to be made available for use as DXi’s, there is additional registry key that must be written.
HKEY_LOCAL_MACHINE\Software\Classes\MfxSoftSynths\{clsid} = <>
Note: The MidiFilter.h file
has a #define for the root key name:
#define
SZ_SOFT_SYNTH_REGKEY "Software\\Classes\\MfxSoftSynths"
The declarations in MidiFilter.h have evolved over time, as newer capabilities have been added alongside new Cakewalk products. For backwards compatibility with Cakewalk products, the following table lists which Cakewalk products correspond to particular versions of MFX.
|
MFX_VERSION |
Supported Cakewalk
Product(s) |
|
< 8 |
Pro Audio, Home Studio and Professional, versions 6.x and 7.x; Guitar Studio 1.0 |
|
8 |
Pro Audio, Home Studio and Professional, version 8.x; Guitar Studio 2.0 |
|
9 |
Pro Audio and Home Studio, version 9.x |
|
10 |
SONAR and SONAR XL, version 1.x; Home Studio 01; Plasma; Club Tracks |
|
11 |
SONAR and SONAR Xl, version 2.x |
The App Wizard creates a DLL that is dynamically linked against the MFC DLLs, so any functions exported from this DLL which call into MFC must have the AFX_MANAGE_STATE macro added at the very beginning of the function. For example:
extern "C" BOOL PASCAL EXPORT ExportedFunction()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
// normal function body here
}
It is very important that this macro appear in each function, prior to any calls into MFC. This means that it must appear as the first statement within the function, even before any object variable declarations (because their constructors may generate calls into the MFC DLL). Failure to conform to this requirement can cause access violations in the host application. Please see MFC Technical Notes 33 and 58 for additional details.
Several methods in the DXi 2.0 SDK may be called in the context of the time critical thread:
IMfxEventFilter::OnInput
IMfxEventFilter::OnEvents
IMfxSoftSynth::OnInput
IMfxSoftSynth::OnEvents
CDXi::Process
CDXi::InitializeNoteEvent
CDXI::ExpireNoteEvent
These methods should be written to execute as quickly as
possible.
MFX, DXi 2.0 and DirectX objects may be used in real-time
during playback. In this case with
Cakewalk products, the methods listed above are called from a dedicated
thread. The thread may be running at time
critical thread priority. Such threads
must be careful about which APIs they call, to ensure smooth system performance.
To give one specific example, do not call functions that paint the screen, like InvalidateRect or SetWindowText. These are particularly bad in Windows 9x and ME, because such functions acquire the Win16Mutex. As a result, this time-critical thread can block on some other thread painting the screen. Instead, if you need to update your UI based on something happening in one of these methods, then a better approach would be to post yourself a message using PostMessage (not SendMessage!). This way, you will avoid causing the thread to block, and the UI update can occur at a more appropriate time.
As explained above, methods can be called from different
threads. The App Wizard will generate
code that uses critical section objects to synchronize access to data objects,
as required. For more information about
critical section objects, read about them in the Microsoft
Platform SDK. Also we recommend a good book like Jeffrey
Richter’s Advanced Windows. Finally, consult the sample code.
This class encapsulates a DirectShow media sample (IMediaSample), providing automatic support for deferred zero filling if the host application supports it. For DXi 2.0 synthesizers, AudioBuffer also contains the association of the buffer to the synthesizers output pin.
The
number of samples in the audio buffer.
Note that for stereo buffers, each pair of left/right values is counted
as one sample.
(This
data member is generated by the wizard only for DXi synthesizers.) The output pin assigned to this audio buffer,
suitable for use in CDXiSynthContext methods.
The
media sample underlying this audio data buffer.
A DXi or plug-in will seldom need to refer to this value.
Returns
a pointer to the audio data as a pointer to a 32-bit IEEE float. This call may also result in the data buffer
being explicitly filled with zeroes, if a previous call to SetZerofill(TRUE) was made.
Test
to see if this buffer was previous tagged as being zero-filled. If TRUE, then the buffer is all zeroes, and
processing can be optimized as a result.
Your plug-in code should take care to call this method first, before
calling GetPointer, to avoid
unnecessary memory fills.
Flag
this buffer as being zero-filled, or not.
This call doesn’t actually fill the buffer with any zeros. It simply sets a flag (using IDeferZeroFill if possible), to trigger
a “lazy” or deferred memory fill. Use
this method whenever your DSP code is producing an output buffer that is known
to be fully zeroes.
This structure encapsulates a MIDI event that is due to be processed by a DXi synthesizer. It extends the constructs of MfxEvent by providing a sample accurate timestamp (for rendering), and hooks for plug-in specific data such as the state of a voice’s oscillator.
The
MIDI event fed to the DXi from the host application.
The
virtual MFX_CHANNEL where the event is to be rendered. In SONAR 2.x, this corresponds to the track
number in the project, but this may change in future products or other host
applications.
The start time of the event, on a free-running synthesizer sample clock.
From
the DXi 2.0 synthesizer perspective, all MIDI events are rendered on an ever
increasing, sample-accurate timeline.
The DXi 2.0 class framework handles the ugly details of the event’s
origins, be it live MIDI input or track streaming, as well as looping playback.
Synthesizer
specific data for the event. Your DXi
code initializes this value via the CDXi::InitializeNoteEvent override, and frees it via the CDXi::ExpireNoteEvent override.
CDXi
is generated by the DXi 2.0 App Wizard.
It consists of a small number of “boilerplate” methods which are filled
in to provide the exact details of your DirectX plug-in or DXi synthesizer. In code generated by the wizard, you fill
find these functions in the module named {PROJECT}.cpp,
where {PROJECT} is the name you provided when first creating your plug-in with
the wizard.
This method is called by framework exactly once on the DXi object, when it first comes to life. Any and all non-trivial initialization of your plug-in object should be placed here, especially initializations which may fail (such as memory allocation.)
Never put initializations which could
possibly fail into your plug-in’s constructor!
Ask the plug-in or DXi if the specified wave format is acceptable as an input format. Note that the framework code will have already validated pwfx to ensure that it is 16-bit PCM or 32-bit float, 1 or 2 channels. The default implementation of this function, generated by the framework, will require that your plug-in accept only 32-bit floats. (This is not a very restrictive requirement, given that all modern DirectX host applications support floating point processing.)
Return
S_OK if the input format is acceptable, or VFW_E_TYPE_NOT_ACCEPTED if
unacceptable.
Ask the plug-in or DXi if the specified wave format is acceptable as an output format. Note that the framework code will have already validated pwfx to ensure that it is 16-bit PCM or 32-bit float, 1 or 2 channels. The default implementation of this function, generated by the framework, will require that your plug-in accept only 32-bit floats.
Return
S_OK if the output format is acceptable, or VFW_E_TYPE_NOT_ACCEPTED if
unacceptable.
Ask the plug-in or DXi if the conversion from input to output wave format is acceptable. Note that the framework code will have already validated pwfx to ensure that it is 16-bit PCM or 32-bit float, 1 or 2 channels, and that the input and output formats have the same sample rate. The default implementation of this function, generated by the framework, will require that your plug-in accept only 32-bit floats.
Return
S_OK if the transform is acceptable, or VFW_E_TYPE_NOT_ACCEPTED if
unacceptable.
Ask the plug-in or DXi to recommend a preferred output format, given a particular input format. Note that the framework code will have already validated pwfx to ensure that it is 16-bit PCM or 32-bit float, 1 or 2 channels. The default implementation of this function, generated by framework, simply returns S_OK. This means that suggested output format will be the same as the current input format.
This method is where your DirectX plug-in will be doing most of its work. Note that the wizard will declare Process differently, depending on whether you have chosen to create a DirectX plug-in or a DXi synthesizer. The form shown here is for a DirectX plug-in.
A DirectX plug-in may process its data in-place, or buffer-to-buffer. To detect whether your plug-in indeed operating in one mode versus the other, you can simply compare pbufIn and pbufOut. (The source code created by the App Wizard does this for you already.)
llSampTimestamp corresponds to the DirectShow “media time”, or time-stamp of the incoming media sample. Plug-ins which implement DirectX 8.0 automation (IMediaParams et al) would inspect this value to determine the timeline position for automation data.
This
method may be running on a time-critical
thread, and should be written to execute as
quickly as possible.
This method is where your DXi software synthesizer will be doing most of its work. Note that the wizard will declare Process differently, depending on whether you have chosen to create a DirectX plug-in or a DXi synthesizer. The form shown here is for a DXi software synthesizer.
llSampTimestamp is corresponds to the DirectShow “media time”, or time-stamp of the incoming media sample. Plug-ins which implement DirectX 8.0 automation (IMediaParams et al) would inspect this value to determine the timeline position for automation data.
llSampMidiClock is the current value of the free-running sample clock for use by the synthesizer. Its value is the time base for all MIDI events in the qMidi queue.
A typical DXi implementation of Process would take the following steps. (Please see the TWONAR sample for a working example.)
This method allows a DXi synthesizer to filter out MIDI events that don’t participate in synthesis. It basically “stands guard” between the host applications large MIDI data buffers and just-in-time rendering done by Process, to ensure that Process isn’t burdened with MIDI events that it can’t render.
For
example, if a user’s project contains a dense passage of NRPN events, and these
events aren’t recognized by the DXi, then the DXi’s Process method would be used to ensure that Process would never be asked to process a NRPN message.
This method allows a DXi synthesizer to initialize any state information pertaining to a MIDI note event. Most synthesizers are designed with a kind of “voice architecture”, and this method allows the synthesizer to initialize its “voice” for a newly sounding note. (Please see the TWONAR sample for a working example.)
This
method may be running on a time-critical
thread, and should be written to execute as
quickly as possible.
This method is called by the framework, to allow the DXi to test whether a note event has played to completion, and/or to free all memory allocated for the note event by InitializeNoteEvent.
This
method may be running on a time-critical
thread, and should be written to execute as
quickly as possible.
This
method is called at the start of streaming, specifically, when the DirectShow
filter graph is transitioned from a “stopped” state into a “paused” state. A DirectX plug-in or DXi will usually want to
allocate all audio format-dependent resources here, such as delay lines that
are sized based on the current sample rate, etc.
This
method is called at the end of streaming, specifically, when the DirectShow
filter graph is transitioned from a “running” state into a “paused” state. A DirectX plug-in or DXi will usually want to
release all audio format-dependent resources here, such as delay lines that are
sized based on the current sample rate, etc.
Returns number of bytes needs by the plug-in or DXi to persist (save or load) its state.
The code generated by the App Wizard will package all presets into a member variable struct named m_p, and the implementation of PersistGetSize will return the size of this struct.
Load the DXi state from pStream. The number of bytes read must equal the value returned from PersistGetSize.
The
code generated by the App Wizard will read bytes from pStream directly into m_p.
Save the DXi state into pStream. The number of bytes written must equal the value returned from PersistGetSize.
The
code generated by the App Wizard will write bytes directly from m_p into pStream.
This class is created by the App Wizard only for DXi 2.0 synthesizers. It exposes services to the DXi for creating, querying and destroying audio output pins. (For an explanation “pins,” “filters”, and other such terminology, please refer to the Microsoft DirectShow SDK.) It also exposes helper functions for conversion between musical time and sample time. Most DXi 2.0 implementations will not ever need to change this class.
Creates a new audio output pin for the DXi synthesizer. If pszName is NULL, then the framework will create a name for the pin for you. If pidCreated is not NULL, then the ID of the new pin will be returned to you.
Important: The framework will negotiate with host
application to determine whether a new pin may be created. This negotiation can easily fail, so be very
careful about checking the return value from this call. Some host applications may not support
filters having multiple output pins.
Also, some host applications may deny a request to create new pins if the
application is “busy” or in an otherwise unacceptable state.
Removes the specified audio output pin from the DXi synthesizer.
Important: The framework will negotiate with host
application to determine whether the pin may be destroyed. This negotiation can easily fail, so be very
careful about checking the return value from this call. Some host applications may not support
filters having multiple pins, and could reject a call to destroy a pin outright. Also, some host applications may deny a
request to destroy a pin if the application is “busy” or in an otherwise
unacceptable state.
Removes all output pins from the DXi synthesizer. A DXi would seldom need to do this, because one would expect always needing to keep at least a single audio output pin.
Important: The framework will negotiate with host
application to determine whether the pins may be destroyed. This negotiation can easily fail, so be very
careful about checking the return value from this call. Some host applications may require that a
filter have at least one pin. Also, some
host applications may deny a request to destroy pins if the application is
“busy” or in an otherwise unacceptable state.
Get the DirectShow CBasePin
object for the specified pin ID. Most
DXi’s will not ever need to manipulate output pins directly, but this method is
provided just in case.
Returns the number of audio pins currently
allocated for the DXi.
Returns the ID for the Nth output pin, as
given by nIndex.
Converts a musical tick time (in units of 960 PPQN) to samples.
This function is provided to simplify
backwards compatibility with DXi 1.0 host applications. DXi 1.0 did not provide IMfxTimeConverter,
which gives sample-accurate conversions between ticks and samples. The implementation of this function will
attempt to use IMfxTimeConverter if
one is available, otherwise it will fall back to the older (msec accurate) IMfxTempoMap.
Converts a sample time to a musical tick time (in units of 960 PPQN).
This
function is provided to simplify backwards compatibility with DXi 1.0 host
applications. DXi 1.0 did not provide IMfxTimeConverter, which gives sample-accurate conversions
between ticks and samples. The
implementation of this function will attempt to use IMfxTimeConverterif one is available, otherwise it will fall
back to the older (msec accurate) IMfxTempoMap.
This class is created by the App Wizard only
for DXi 2.0 synthesizers. It provides a
boilerplate implementation of IMfxInstrument
which you can fill in to provide the specifics for your synth. Please refer to the TWONAR sample to see how
you might modify CInstrument to suit your needs.
An MFX_CHANNEL identifies the origin of a MIDI data stream in the host application. For example, in SONAR 1.0, the MFX_CHANNEL is always the same as the track number where the MIDI data resides. In future products, the MFX_CHANNEL may identify the stream or “clip” where the data resides.
The purpose of this data type is to allow certain MIDI event properties to be changed “just in time.” For example, Cakewalk products allow all of the MIDI data on a track to be transposed by modifying a “Key+” entry in the track’s UI. These changes would be encoded in an MfxNotifyMsg structure (below) having an MFX_CHANNEL equal to the track number where the Key+ was changed.
The MFX_TIME struct is used to represent the position of events on a song’s timeline. MFX_TIME supports a large variety of time formats. MFX_TIME values can be converted from one unit to another via IMfxTimeConverter.
typedef enum MFX_TIME_FORMAT
{
TF_NULL, // no time value (used to clear a time value)
TF_SECONDS, // time in absolute seconds from song top (double)
TF_SAMPLES, // time in absolute samples from song top (LONGLONG)
TF_TICKS, // time in MIDI ticks @ 960 ppqn, from song top (LONGLONG)
TF_UTICKS, // time in hi-res MIDI ticks @ 960*(2^16) ppqn, from song top (LONGLONG)
TF_MBT, // time in measure:beat:ticks, from song top
TF_FRAMES, // time in SMPTE frames, including the projects SMPTE offset
TF_FRAMES_REL, // time in SMPTE frames, relative to song top (no offset applied)
TF_SMPTE, // time in SMPTE HMSF format, including the projects SMPTE offset
TF_SMPTE_REL // time in SMPTE HMSF format, relative to song top (no offset applied)
}
MFX_TIME_FORMAT;
typedef enum MFX_FPS
{
FPS_24, // 24 fps
FPS_25, // 25 fps
FPS_2997, // 29.97 fps
FPS_2997_DROP, // 29.97 fps, drop-frame
FPS_30, // 30 fps
FPS_30_DROP // 30 fps, drop-frame
}
MFX_FPS;
typedef struct MFX_TIME
{
MFX_TIME_FORMAT timeFormat;
union
{
double dSeconds; // timeFormat == TF_SECONDS
LONGLONG llSamples; // timeFormat == TF_SAMPLES
LONG lTicks; // timeFormat == TF_TICKS
LONGLONG llUTicks; // timeFormat == TF_UTICKS
struct
{
int nMeas;
short nBeat;
short nTick;
}
mbt; // timeFormat == TF_MBT
struct
{
short fps; // one of enum MFX_FPS
short nSub400; // units = 1/400 of a frame
char nFrame;
char nSec;
char nMin;
char nHour;
}
smpte; // timeFormat == TF_SMPTE, TF_SMPTE_REL
struct
{
short fps;
LONG lFrame;
}
frames; timeFormat == TF_FRAMES, TF_FRAMES_REL
};
}
MFX_TIME;
The MfxData struct is used to describe MIDI events at a low level. This is seen in the member function OnInput.
struct MfxData
{
// Musical ticks. For MfxData items passed to IMfxEventFilter::OnInput(),
// this will correspond to the current time. If the application isn't playing,
// the current time will not change.
LONG m_lTime;
union
{
DWORD m_dwData; // this is in midiOutShortMsg() format...
struct
{
BYTE m_byStatus;
BYTE m_byData1;
BYTE m_byData2;
};
};
};
The MfxEvent struct is used to describe a single MIDI event.
This is a slightly higher-level concept than raw MIDI voice messages. For example, Note, Patch, RPN, and NRPN MfxEvents consolidate multiple MIDI voice messages into a single logical MfxEvent, for easier processing.
The m_eType field identifies the type of event. New types of events may be defined in the future. As a result, in your IMfxEventFilter::OnEvents member you should be prepared for m_eType values that you do not recognize. The action you should take depends on your filter. Usually you should pass unknown events to IMfxEventQueue::Add. However, if the purpose of your filter were to delete all events except for those of a certain type, it might be reasonable to delete the unknown events (not pass them to Add).
The m_lTime field may only hold non-negative values. So why is it a signed LONG? Because time math often is convenient to do with signed integers, and this allows you to avoid excessive casting. But again, only values greater than or equal to zero are permitted for MfxEvent start times.
struct MfxEvent
{
// This describes a MIDI event.
// This is a bit higher-level than raw
MIDI voice messages. For example,
// Note, Patch, RPN and NRPN MfxEvents
consolidate multiple MIDI voice
// messages into a single logical
MfxEvent, for easier processing.
enum Type
{
Note,
KeyAft,
Control,
Patch,
ChanAft,
Wheel,
RPN,
NRPN
#if
(MFX_VERSION > 8)
Sysx,
Text,
Lyric,
#endif //
(MFX_VERSION > 8)
#if
(MFX_VERSION > 9)
MuteMask,
VelOfs,
VelTrim,
KeyOfs,
KeyTrim,
#endif //
(MFX_VERSION > 9)
#if
(MFX_VERSION > 10)
ShortMsg,
#endif //
(MFX_VERSION > 10)
} m_eType;
LONG m_lTime; // in musical ticks; must be >= 0L
union
{
struct
{
BYTE m_byPort; // MIDI port for this event
BYTE m_byChan; // MIDI channel for this event
};
struct
{
MfxMuteMask m_maskSet; // for MuteMask, bits to set in the mask
MfxMuteMask m_maskClear; // for MuteMask, bits to clear from the mask
};
char m_nOfs; // for VelOfs and KeyOfs
char m_nTrim; // for VelTrim and KeyTrim
};
Type m_eType;
union
{
struct // m_eType == Note
{
BYTE m_byKey; // 0..127; key number
BYTE m_byVel; // 0..127; attack velocity
BYTE m_byVelOff; // 0..127 release velocity (64 = default)
DWORD m_dwDuration; // 0..ULONG_MAX; in musical ticks
};
struct // m_eType == KeyAft
{
BYTE m_byKey; // 0..127
BYTE m_byAmt; // 0..127
};
struct // m_eType == Control
{
BYTE m_byNum; // 0..127; controller number
BYTE m_byVal; // 0..127; controller value
};
struct // m_eType == Patch
{
BYTE m_byPatch; // 0..127
BYTE m_byBankSelMethod; // 0==Normal, 1==Ctrl0, 2==Ctrl32, 3==Patch100
short m_nBank; // -1..16383 (-1 ==
"none")
};
struct // m_eType == ChanAft
{
BYTE m_nAmt; // 0..127
};
struct // m_eType == Wheel
{
short m_nVal; // -8191..+8191 (0 == center)
};
struct // m_eType == RPN or NRPN
{
WORD m_wNum; // 0..16383
WORD m_wVal; // 0..16383
};
#if
(MFX_VERSION > 8)
struct // m_eType == Sysx, Text,
or Lyric
{
// Pass the MFX_HBUFFER to
the IMfxFactory::GetPointer() method
// to cash it in for a
pointer and length.
// Note: Text and Lyric
events are wchar_t pointers -- i.e. Unicode
// strings -- following COM
convention.
MFX_HBUFFER m_hBuffer;
};
#endif //
(MFX_VERSION > 8)
#if
(MFX_VERSION > 9)
struct // m_eType == Mute, VelOfs,
VelTrim, KeyOfs, KeyTrim
{
MFX_CHANNEL m_mfxChannel;
};
#endif //
(MFX_VERSION > 9)
#if
(MFX_VERSION > 10)
struct // m_eType == ShortMsg
{
DWORD m_dwShortMsg;
};
#endif //
(MFX_VERSION > 9) };
};
The MfxMuteMask class is used to describe why a particular channel has been muted. Specifically, it encapsulates a bit-field, each bit of which represents one possible reason for a mute to occur.
struct MfxMuteMask
{
MfxMuteMask() { byte = 0; }
MfxMuteMask(int b) { byte = BYTE(b); }
BOOL AnySet() const { return byte; }
operator BYTE() const { return byte; }
BYTE operator=(int n) { byte = BYTE(n); return byte; }
union
{
BYTE byte;
struct
{
BYTE Manual:1;
BYTE Auto:1;
BYTE Solo:1;
BYTE Record:1;
BYTE Scrub:1;
};
};
};
|
Mask Bit |
Description |
|
Manual |
The user has clicked on a mute button, either in the application’s UI on an external control surface. |
|
Auto |
A mute automation envelope has changed the mute state of the channel. |
|
Solo |
This channel is being (un)muted because some other channel is being (un)soloed. |
|
Record |
The application has begun an “overwrite” recording pass on this channel. Any data being streamed out of the channel should be muted. |
|
Scrub |
Some other channel is being scrubbed. |
This struct is used by IMfxNotify. It encapsulates “just in time” changes sent from the host app to a DXi.
struct MfxNotifyMsg
{
enum Type
{
ChannelMidiChannel,
ChannelMuteMask,
ChannelVelOfs,
ChannelVelTrim,
ChannelKeyOfs,
ChannelKeyTrim,
};
Type m_type;
union
{
struct // ChannelMidiChannel
{
MFX_CHANNEL m_mfxChannel;
char m_nMidiChannel;
};
struct // ChannelMuteMask
{
MFX_CHANNEL m_mfxChannel;
MfxMuteMask m_maskSet;
MfxMuteMask m_maskClear;
};
struct // ChannelVelOfs, ChannelKeyOfs
{
MFX_CHANNEL m_mfxChannel;
char m_nOfs;
};
struct // ChannelVelTrim, ChannelKeyTrim
{
MFX_CHANNEL m_mfxChannel;
char m_nTrim;
};
};
};
|
Notification |
Description |
|
ChannelMidiChannel |
All of the data which came from channel m_mfxChannel should be rendered out a different synthesizer channel, m_nMidiChannel. |
|
ChannelMuteMask |
The user has muted/unmuted channel m_mfxChannel. Mutes are specified via 2 mute mask variables, one which specifies bits to clear, and another which specifies bits to set. In other words, Mute = (Mute | m_maskSet) & (~m_maskClear), and the channel is muted if “Mute” is nonzero. For example, a DXi will typically initialize every channel’s mute mask to 0x00. If a mute notification arrives with Set=0x01/Clear=0x00, then Mute=(0x00|0x01)&(~0x00)=0x01. |
|
ChannelVelOfs |
Add m_nOfs to the velocity of every MIDI note event which came from m_mfxChannel. |
|
ChannelVelTrim |
Add m_nTrim to the velocity of every MIDI note-on event which came from m_mfxChannel. |
|
ChannnelKeyOfs |
Add m_nOfs to the key (pitch) of every MIDI note-on event which came from m_mfxChannel. |
|
ChannelKeyTrim |
Add m_nTrim to the key (pitch) of every MIDI note-on event which came from m_mfxChannel. |
In the event of any discrepancy, the final authority is the header file MidiFilter.h from the SDK. Also, be sure to study the sample code provided.
A MFX filter must implement a COM interface defined for MFX:
Also, it must implement a standard COM interface that allows the effect to be persisted (allows its CLSID and its properties to be stored in files):
¨ IPersistStream, to support saving effects in project files, and to support the “Presets” feature.
If a MFX filter has any user interface (most will), it should also implement some standard COM interfaces:
¨ ISpecifyPropertyPages, to provide a user interface in the form of one or more IPropertyPage objects.
These last two interfaces are also implemented by DirectShow audio filters. In other words, for both MIDI and audio effects, PA handles user interface, persistence, and presets using the same mechanism.
A software synthesizer must implement a COM interface defined for software synthesizers:
It should implement the COM interface to
facilitate just-in-processing of mutes, key-transposition, etc.
Also, it must implement a standard COM interface that allows the effect to be persisted (allows its CLSID and its properties to be stored in files):
¨ IPersistStream, to support saving effects in project files, and to support the “Presets” feature.
If the DXi has any user interface (most will), it should also implement some standard COM interfaces:
¨ ISpecifyPropertyPages, to provide a user interface in the form of one or more IPropertyPage objects.
If the DXi wants to send MIDI data to the host “just in time,” for example, to record automation as MIDI from the DXi’s user interface, it should also implement
¨ IMfxInputPort, to accept an IMfxInputCallback from the host, through which MIDI messages can be sent.
The host application of your MFX filter, will implement some other special interfaces:
¨ IMfxEventQueue and IMfxEventQueue2, so that your IMfxEventFilter::OnEvents member can pass on MfxEvents that it modifies or creates.
¨ IMfxBufferFactory, so that you may work with MFX_HBUFFER handles in MfxEvents (for example, System Exclusive events).
¨ IMfxDataQueue, so that your IMfxEventFilter::OnInput member can pass on MfxDatas that it modifies or creates.
¨ IMfxTempoMap, so that your effect can convert between musical ticks and milliseconds, and get read-only access to the tempo map.
¨
IMfxMarkerMap and IMfxMarkerMap2, so that your
effect can access any markers and pitch markers in the project.
¨ IMfxMeterMap, so that your effect can convert between musical ticks and Measure:Beat:Tick format, and get read-only access to the meter map.
¨ IMfxKeySigMap, so that your effect can query the key signature in effect at any given time.
¨ IMfxInstruments, IMfxNameListSet, and IMfxNameList, so that your application can access the instrument definitions and lists of names for patches, notes, and controllers.
¨ IMfxSelection, so that your effect can find out if it’s being used as an offline edit command, and if so, what is the time span of the selection.
¨
IMfxInputPulse, so that
certain types of effects may have their IMfxEventFilter::OnInput member called
periodically, regardless of whether there’s any MIDI input.
¨
IMfxInputCallback, to allow the
MFX or DXi to send MIDI data back to the host application for recording, UI
automation, etc.
¨
IMfxTimeConverter, to perform
high-resolution time conversions between various time formats.
¨
IDeferZeroFill, to allow the
plug-in to efficiently manage silent audio buffer.
The purpose of IDeferZeroFill is to improve DSP performance by avoiding
unnecessary computation and/or memory access.
IDeferZeroFill is “discovered” by a plug-in via a query interface on a DirectShow media sample. What follows is an example of how a simple DSP routine might detect and use this capability.
HRESULT ScaleBuffer( IMediaSample* pms, double dGain )
{
// Check for possible silent buffer optimizations
IDeferZeroFill* pdzf = NULL;
if (S_OK == pms->QueryInterface( IID_IDeferZeroFill, (void**)&pdzf ))
{
if (pdzf->get_NeedsZerofill()) // silent buffer?
{
// Silent buffers are always silent, even after being scaled.
// Nothing more to do.
pdzf->Release();
return S_OK;
}
else if (0.0 == dGain) // multiply by zero?
{
// Scaling by zero yields a silent buffer. Do this efficiently
// without touching memory or CPU.
pdzf->put_NeedsZerofill( TRUE );
pdzf->Release();
return S_OK;
}
pdzf->Release();
}
// No optimizations possible. Process away . . .
}
Why go through all this work? In a word, performance. Modern CPUs are very efficient at floating point math. Yet in highly optimized DSP algorithms, the DSP code itself is often not the bottleneck! Instead, bandwidth between the CPU and RAM is the limiting factor. Even with a L1 and L2 processor caches, there is great benefit in avoiding unnecessary memory accesses. (The introduction of this IDeferZeroFill literally doubled the performance of Cakewalk’s audio engine!)
This is because of a key implementation detail that allows IDeferZeroFill to be effective, even when used with plug-ins that don’t know how to recognize it. Visualize the call to put_NeedsZerofill(TRUE) as putting a kind of tamper-proof seal on the data buffer. The seal gets “broken” when somebody calls IMediaSample::GetPointer – necessarily so, because this function gives the caller full access the audio data. Since there is not way to guess the caller’s intent in calling IMediaSample::GetPointer, any reasonable implementation of IDeferZeroFill must be pessimistic and assume that the buffer is about to populated with non zero data.
So the moral of the story is, always check
for deferred zero filling before you attempt to obtain a media sample’s
pointer. (Fortunately, the AudioBuffer
class provided with the DXi 2.0 App Wizard takes care of this for you
automatically.)
Reports if the associated media sample has
been flagged as being fully silent.
Flags a media sample as being fully silent.
Returns the data buffer pointed to by the IMediaSample,
bypassing the usual pessimistic side-effect of calling IMediaSample::GetPointer.
MFX buffers are used to support variable-length data associated with certain MfxEvent types like System Exclusive, Text, and Lyric. The MfxEvent doesn't contain a raw pointer to the extra memory. Instead it contains an MFX_HBUFFER handle. You use this IMfxBufferFactory interface to cash in the handle for a pointer to the data as well as its length. In addition if you are modifying or creating new MfxEvents of this type, the IMfxBufferFactory interface lets you create new buffers.
MFX buffers are valid only during the call to your IMfxEventFilter::OnEvents member. You should not retain the IMfxBufferFactory pointer or any MFX_HBUFFER handles across such calls and expect them to be valid. This enables the host application can do automatic garbage collection. This means that effects aren't responsible for freeing memory to prevent leaks, and by the same token effects can't corrupt the heap by freeing memory twice. Finally, because effects should check the return value of GetPointer using the SUCCEEDED macro, they should not get into trouble by using bad pointers: If the MFX_HBUFFER is no longer valid, GetPointer will return E_INVALIDARG.
For efficiency, MFX buffers may not be resized. If you need to resize a buffer, you must call CreateCopy to create a new buffer of the desired length and copy the contents of the old buffer to the new one. By contrast, if you need only to modify the buffer in-place (for example to modify the value of one byte in a System Exclusive message) you may do so efficiently with the read/write pointer you get from GetPointer.
This creates a new MFX buffer with the capacity to store dwLength bytes.
Return S_OK on success, or E_OUTOFMEMORY if there was not enough memory to create the buffer.
This creates a new MFX buffer with the capacity to store dwLength bytes. In addition, the contents of hbufOld will be copied to hbufNew (up to but not exceeding the length of the new buffer).
Return S_OK on success, E_INVALIDARG if hbufOld is an invalid handle, or E_OUTOFMEMORY if there was not enough memory to create the buffer.
This returns a read/write pointer to the data in ppvData. If pdwLength is not NULL, this return the number of bytes that the buffer contains.
Return S_OK on success or E_INVALIDARG if hbuf is an invalid handle.
IMfxDataQueue represents a collection of MfxEvents.
Some of your IMfxEventFilter members are called with a pointer to an IMfxDataQueue. This allows you to output zero or more MfxData.
Return the number of MfxData in the queue
Provides the MfxData at index 'ix'.
This method is called before any other. It gives your filter a chance to obtain interfaces provided by the host application. To do so, QueryInterface the pContext pointer.
Your filter may require a certain interface like IMfxTempoMap. If QueryInterface fails, you should return E_FAIL to indicate that your filter cannot work at all.
Return NOERROR (or S_OK) to indicate that your filter found what was required and that it can work.
Note: Your filter may want to use an interface if available, but be able to work without it (perhaps in some limited fashion). This is an important COM concept: An object can find out at runtime what capabilities another object supports. For example, the host application might add a new “ISuperDuperMap” interface. Subsequently, you might write a new filter for which ISuperDuperMap might be handy but not required. Your future filter could QueryInterface for ISuperDuperMap, and proceed accordingly. This way, your filter could take advantage of a specific host application, but remain compatible with all other host applications that don’t supply ISuperDuperMap.
If you do not retain a copy of pContext, be sure to call Release on it before returning.
This method is called before the final Release of your filter. At this time, you should Release any interface pointers that you obtained in Connect.
Called when playback (or other streaming session) starts, with the start time.
The IMfxEventQueue* pqOut is valid only for this call; do not retain a copy of it and do not call AddRef or Release.
Called when playback (or other streaming session) loops back to an earlier time without stopping and restarting.
The IMfxEventQueue* pqOut is valid only for this call; do not retain a copy of it and do not call AddRef or Release.
Called when playback (or other streaming session) stops, with the stop time.
The IMfxEventQueue* pqOut is valid only for this call; do not retain a copy of it and do not call AddRef or Release.
This is called when a period of time is being processed. The period is represented by timeFrom through timeThru.
This method may be running on a time-critical thread, and should be written to execute as quickly as possible.
This member will be called even if there are no events during the time period for you to process. This allows you to implement purely generative effects, or ones which need may need to generate output periodically even if no input is occurring (for example, an arpeggiator).
If there are any events to process, they are in the IMfxEventQueue* pqIn.
Both pqIn and pqOut are valid only for this call; do not retain a copy of them and do not call AddRef or Release.
For each event in pqIn:
¨ To pass it on unchanged, pass it to pqOut->AddRef().
¨ To modify it, pass a modified MfxEvent to pqOut->AddRef().
¨ To delete it, simply do not call pqOut->AddRef() at all.
¨ To create one or more additional events, call pqOut->AddRef() for each one.
The MfxEvent structure has members for the MIDI port and channel: m_byPort and m_byChan. The meaning depends on how the filter is being used.
¨ Playback. Each event has a MIDI channel stored with it. In addition, tracks have a forced channel property, which can be “none” or a specific channel from 1 to 16. If the forced channel is other than “none”, then each event is stamped with that channel during playback. This stamping process takes place before you get the MfxEvent in OnEvents. You may modify m_byPort and m_byChan to change where events are output during playback. For example, you could write an echo filter that would send the duplicated notes to a port and channel specified by the user, regardless of the original port and channel.
¨ Offline. When your filter is used as an offline edit command, the m_byPort field is meaningless and is ignored. The m_byChan field is the actual channel stored with the event (the track forced channel is not used). If you modify m_byChan, this will modify the channel when the event is stored back in the track.
This member is called to allow you to modify live input at the MIDI input ports, “on the fly”. Whenever a MIDI voice message is received, it is passed to this member.
This method may be running on a time-critical thread, and should be written to execute as quickly as possible.
Both pqIn and pqOut are valid only for this call; do not retain a copy of them and do not call AddRef or Release.
For each event in pqIn:
¨ To pass it on unchanged, pass it to pqOut->AddRef().
¨ To modify it, pass a modified MfxData to pqOut->AddRef().
¨ To delete it, simply do not call pqOut->AddRef() at all.
¨ To create one or more additional events, call pqOut->AddRef() for each one.
A limited class of effects needs to be able to execute periodically, even in the absence of MIDI input. For example, an arpeggiator effect might want to generate notes indefinitely. The user presses some keys on the MIDI keyboard: OnInput is called. The user releases all the keys: OnInput is called again. But in between, an indefinite amount of time may pass, during which the arpeggiator wants to generate output notes.
To meet this need, IMfxInputPulse may be used. You may obtain this interface by calling QueryInterface on the IUnknown* passed to your Connect member. Call IMfxInputPulse::BeginPulse to begin the periodic calls to OnInput. Call IMfxInputPulse::EndPulse to end them.
IMfxInputPulse::BeginPulse returns the period of the callbacks (how frequently your OnInput member will be called). Use this to generate notes with future timestamps, to cover that period of time, until the next call to OnInput.
Because this pulse consumes valuable CPU resources, it is a very good idea to have the pulse enabled only when you absolutely need it. In the arpeggiator example, you might call BeginPulse when the first key is pressed, and call EndPulse when the last note is released.
See IMfxInputPulse for additional information.
Keep in mind that most types of MIDI effects do not need to use IMfxInputPulse. A simple echo/delay, for instance, can go ahead and create all of the echoed notes when the original is received. This does not need, and should not use, IMfxBeginPulse. Instead it should use the capability to stamp output data with times in the future. That is the most efficient way to create data when the amount of data to be created is known in advance.
IMfxEventQueue represents a collection of MfxEvents.
Some of your IMfxEventFilter members are called with a pointer to an IMfxEventQueue. This allows you to output zero or more MfxEvents.
Return the number of MfxEvents in the queue
Provides the MfxEvent at index 'ix'.
IMfxEventQueue2 represents a collection of MfxEvents. It extends the original IMfxEventQueue interface by adding a GetBufferFactory method.
Some of your IMfxEventFilter members are called with a pointer to an IMfxEventQueue. This allows you to output zero or more MfxEvents.
Return the number of MfxEvents in the queue
Provides the MfxEvent at index 'ix'.
Use this to obtain the IMfxBufferFactory associated with this IMfxEventQueue.
Returns S_OK on success, or E_POINTER if the pointer argument is bad.
You must call Release on the IMfxBufferFactory pointer it returns, when you are done with it. Typically you will obtain this pointer in your IMfxEventFilter::OnEvents method. Note that for compatibility, OnEvents takes IMfxEventQueue pointers -- not IMfxEventQueue2. Therefore you should:
1. QueryInterface the IMfxEventQueue pointer for the IMfxEventQueue2 interface.
2. Call IMfxEventQueue2::GetBufferFactory() to get the IMfxBufferFactory pointer.
3. Call IMfxBufferFactory::GetPointer() to cash in the MFX_HBUFFER handle for a pointer and length.
4. Release the IMfxBufferFactory pointer.
5. Release the IMfxEventQueue2 pointer.
Here is a sample code fragment. In production code, you might prefer to obtain and hold the IMfxEventQueue2 and IMfxBufferFactory interface pointers outside the event iteration loop. Just be sure you call Release on both pointers when you are done.
HRESULT CMidiFilter::OnEvents( LONG timeFrom, LONG timeThru,
IMfxEventQueue* pqIn, IMfxEventQueue* pqOut )
{
HRESULT hr;
// Determine the number of events to be processed
int nCount;
hr = pqIn->GetCount( &nCount );
if (FAILED( hr ))
return hr;
for (int ix = 0; ix < nCount; ++ix)
{
// Get the next event
MfxEvent event;
hr = pqIn->GetAt( ix, &event );
if (FAILED( hr ))
return hr;
if (MfxEvent::Sysx == event.m_eType)
{
IMfxEventQueue2* pq2;
hr = pqIn->QueryInterface( IID_IMfxEventQueue2, (void**)&pq2 );
if (FAILED( hr ))
return hr;
IMfxBufferFactory* pBufFact;
hr = pq2->GetBufferFactory( &pBufFact );
if (SUCCEEDED( hr ))
{
void* pv;
DWORD cb;
hr = pBufFact->GetPointer( event.m_hBuffer, &pv, cb );
if (SUCCEEDED( hr ))
{
BYTE* pby = static_cast<BYTE*>(pv);
for ( ; cb--; ++pby)
{
TRACE( "%x ", *pby );
}
TRACE0( "\n" );
}
pBufFact->Release();
}
pq2->Release();
}
}
return hr;
}
This is implemented by the host application, working with IMfxInputPort to allow your filter to send MIDI data back to the host application.
Call this to “push” a MIDI event to the host
application, i.e., when the user has struck a note on your plug-ins virtual
keyboard, or moved a control in your UI which the host application should
record.
This allows your filter to send MIDI data back to the host application. This is useful if you are building a DXi that presents some kind of virtual keyboard interface. It is also useful for DXi or MFX filters having parameters that are controllable both from a UI and from MIDI. In this latter usage, IMfxInputCallback would facilitate the recording of user interactions on the plug-in, as MIDI in the host application.
This is called by the host application to establish a connection between it and your plug-in. Your plug-in should make calls on pCallback whenever it needs to send MIDI data to the host.
pCallback may be NULL, meaning that the
host no longer can accept MIDI data from your plug-in.
If
your plug-in intends to send MIDI, and will therefore be storing pCallback, it should obey standard COM
rules and AddRef the pointer before storing it.
Similarly, when your plug-in destroys itself, it should Release this
pointer.
This is called by the host application to obtain a pointer to any callback pointer stored in your plug-in.
If
your stored callback pointer is not NULL, this method should obey standard COM
rules and AddRef the pointer before storing it in ppCallback.
This allows your filter to request that its IMfxEventFilter::OnInput member be called periodically, even if there is no actual MIDI input.
Please see the documentation for IMfxEventFilter::OnInput for important information about how to use IMfxInputPulse correctly.
Call this to begin having your IMfxEventFilter::OnInput member called at a periodic interval. The interval time is not under your control. BeginPulse passes out the interval using the plIntervalMsec argument.
Call this to stop having your IMfxEventFilter::OnInput member called at a periodic interval.
This
interface is how a DXi 2.0 software synthesizer exposes different sets of names
(notes, controllers, etc.) per MIDI channel.
Using IMfxInstrument a synth
can define a specially named drum instrument solely for channel 10, for
example.
Returns
the name of this instrument as a DBCS string.
Returns an array of bank numbers, each of which provides patch names. Each bank number filled in panBank is guaranteed to provide a non-empty list of patch names when passed into GetPatchNames.
The caller is responsible for freeing the returned array panBank, by calling CoTaskMemFree.
If the specified patch in the specified bank is a drum patch, returns S_OK, otherwise returns S_FALSE.
Returns E_INVALIDARG is nBank is outside the range 0 through 16383 or if nPatch is outside the range 0 through 127.
Diatonic note names are names of musical notes like “C#0”, “Gb5”, and so on. Note names for a drum patch will typically not be diatonic note names, but rather names like “Kick”, “Snare”, and so on.
If the specified note name list is the list of diatonic note names, returns S_OK, otherwise returns S_FALSE.
Returns E_INVALIDARG is nBank is outside the range 0 through 16383 or if nPatch is outside the range 0 through 127.
For a specified bank, provides an IMfxNameList2*, which represents the names of the patches within that bank.
Returns S_OK, else returns E_INVALIDARG if nBank is outside the range 0 through 16383 or IMfxNameList2 is a bad pointer.
For a specified bank and patch, provides an IMfxNameList2*, which represents the names of the patches within that bank.
Returns S_OK, else returns E_INVALIDARG if nBank is outside the range 0 through 16383, if nPatch is outside the range 0 through 127, or if IMfxNameList2 is a bad pointer.
Provides an IMfxNameList2*, which represents the names of controllers. Returns S_OK.
Provides an IMfxNameList2*, which represents the names of RPNs (Registered Parameter Numbers).
Provides an IMfxNameList2*, which represents the names of NRPNs (Non-Registered Parameter Numbers).
IMfxInstruments represents all instruments currently known by the host application.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Get the IMfxNameListSet containing all the IMfxNameLists for patch names.
Important: You must call Release on the IMfxNameListSet * when you're done.
Get the IMfxNameListSet containing all the IMfxNameLists for note names.
Important: You must call Release on the IMfxNameListSet * when you're done.
Get the IMfxNameListSet containing all the IMfxNameLists for controller names.
Important: You must call Release on the IMfxNameListSet * when you're done.
IMfxKeySigMap represents the key signature map of the host application.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Get the index of the key signature change in effect for time 'lTicks'.
Get the number of key signature changes in the map.
Get a meter change. Returns:
pnMeasure: the measure number of the key signature change .
pnKeySig: the key signature (-7 = 7 flats, 0 = C Major, +7 = 7 sharps).
IMfxMarkerMap represents the list of markers of the host application.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Get the index of
the marker in effect for time lTicks.
Get the number of markers in the map.
Specifies the
possible units returned by the GetMarkerAt
method.
Get the marker at the specified index. Upon return, *plTime contains the time of the marker, in the units specified by *peUnits, and *ppwszName contains the name of the marker.
IMfxMarkerMap2 extends (inherits from) IMfxMarkerMap, adding access to “pitch markers.” Pitch markers were added in SONAR 1.0 to represent project key changes, for the rendering of SONAR Groove Clips.
Determine the pitch
in effect at the project timeline, at the time given by MFX_TIME.
IMfxMeterMap represents the meter (time signature) map of the host application.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Convert between musical ticks and Measure:Beat:Ticks format.
Convert between Measure:Beat:Ticks and musical ticks format.
Get the index of the meter change in effect for time 'lTicks'.
Get the number of meter changes in the map.
Get a meter change. Returns:
*pnMeasure: the measure number of the meter change.
*pnTop: the number of beats per measure.
*eBottom: the beat value (shift 1 left by this amount to get the beat value; e.g., 2 means 1 << 2 or 4).
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Get the title of this name list (e.g. "General MIDI").
Get the maximum number of names that this list can contain, e.g.. 128 for note and controller names, 16384 for patch names. This number is one greater than the maximum valid value for the ix argument to GetAt.
Get the specified name.
This interface is used by soft synths to expose lists of banks, patches, controllers, RPNs and NRPNs. It uses IMfxNameList as a base class, so all of those methods (listed above) must be implemented by an object which exposes IMfxNameList2.
IMfxNameList2 extends IMfxNameList by allowing names to stored (and iterated) as sparse arrays. For example, the MIDI specification allows up to 16k RPNs, though few synthesizers expose that many. By using IMfxNameList2, a soft synth can provide names for only those RPN numbers that are valid, thereby conserving memory.
Fills pnCount with the number of names in this map. Return S_OK on success or E_POINTER if pnCount is a bad pointer.
This function, along with GetNextAssoc, allow a client of IMfxNameList2 to iterate over all elements in the map. (This function has the same behavior as in the MFC container classes.)
This function, along with GetStartPosition, allow a client of IMfxNameList2 to iterate over all elements in the map. (This function has the same behavior as in the MFC container classes.)
The following code fragment shows how to iterate over elements in an IMfxNameList2:
MFX_POSITION pos;
char sz;
int nKey;
HRESULT hr = pMfxMap->GetStartPosition( &pos );
if (SUCCEEDED( hr ))
{
while (S_OK == pMfxMap->GetNextAssoc( &pos, &nKey, sz, sizeof(sz) ))
{
// do something with nKey, sz
}
}
Retrieves the string associated with the given key. Returns S_OK if an entry was found, S_FALSE if an entry was not found, or E_POINTER is pszString is a bad pointer.
IMfxNameListSet represents all list of names of a particular type.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Get the number of IMfxNameLists available.
Get the indicated IMfxNameList.
Important: You must call Release on the IMfxNameListSet* when you're done.
IMfxNotify is exposed by the soft synth to allow about mute, solo, etc, to be applied "just in time".
When data is fed to the soft ware synth via IMfxSoftSynth::OnEvents or IMfxSoftSynth::OnInput, the supplied data will not have certain properties, such as a key offset or velocity offset, subject to last minute changes. IMfxNotify is the means for applying these last minute changes to the data stream.
Tell the soft synth about the last minute change to data.
pMsg->m_type specifies which property has been changed (such as channel velocity offset).
pMsg->m_mfxChannel specifies the channel for which the property has changed.
Additional information about the property change is encoded within the remainder of the MfxNotifyMsg structure.
This interface is exposed by the host
application, to allow a DXi or MFX plug-in to tell the host about important changes
that may have occurred.
You may try to obtain this interface by
using QueryInterface on the IUnknown* passed to Connect.
It is not exposed by SONAR 1.x or earlier
Cakewalk products.
This method is called by the DXi or MFX when it needs to notify the host about some changes. The following is a list of valid values for uMessage, as defined in MidiFilter.h
The caller is a multi-output DXi, and is notifying the host about a change in its audio output count. If the host returns an error code from this notification, the DXi must leave its output count in the state it was in before making this request.
In this notification, lParam should be set to the new (desired) number of synthesizer
audio outputs.
The caller is a multi-output DXi, and is notifying the host that it has completed changing its audio output count. If the host returns an error code from this notification, the DXi restore its output count in the state it was in before making this request.
In this notification, lParam should be set to the new (desired) number of synthesizer
audio outputs.
This provides information about the selection, when a filter is being used as an offline edit command.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is provided by Cakewalk products when your filter is being used as an offline edit command. Otherwise QueryInterface will fail, indicating that your filter is being used e.g. during playback or in some other situation.
Returns the selection start time.
Returns the selection end time. This is inclusive (“through”, not “up to”) -- the selection extends up through and including this tick.
Returns the start time of the first selected event.
This may be a later time than what GetFrom returns. The reason is that the application may allow the user to indicate a time selection that begins before the first event.
Returns the start time of the last selected event.
This may be an earlier time than what GetThru returns. The reason is that the application may allow the user to indicate a time selection that extends after the last event.
Returns the end time of the last selected event. For a note event, this will be the note’s start time plus its duration. For other types of events, this will be the same as the start time.
This may be an earlier time than what GetThru returns. The reason is that the application may allow the user to indicate a time selection that extends after the last event.
This interface must be implemented by a software synthesis filter. It is very similar to the IMfxEventFilter interface intended for MIDI effects.
This method is called before any other. It gives your filter a chance to obtain interfaces provided by the host application. To do so, QueryInterface the pContext pointer.
Your filter may require a certain interface like IMfxTempoMap. If QueryInterface fails, you should return E_FAIL to indicate that your filter cannot work at all.
Return NOERROR (or S_OK) to indicate that your filter found what was required and that it can work.
Note: Your filter may want to use an interface if available, but be able to work without it (perhaps in some limited fashion). This is an important COM concept: An object can find out at runtime what capabilities another object supports. For example, the host application might add a new “ISuperDuperMap” interface. Subsequently, you might write a new filter for which ISuperDuperMap might be handy but not required. Your future filter could QueryInterface for ISuperDuperMap, and proceed accordingly. This way, your filter could take advantage of a specific host application, but remain compatible with all other host applications that don’t supply ISuperDuperMap.
If you do not retain a copy of pContext, be sure to call Release on it before returning.
This method is called before the final Release of your filter. At this time, you should Release any interface pointers that you obtained in Connect.
Called when playback (or other streaming session) starts, with the start time.
Called when playback (or other streaming session) loops back to an earlier time without stopping and restarting.
A typical DXi will require some kind of internal cache or queue of events which have been sent via OnEvents (below), but not yet rendered into the audio output stream. To prevent this queue from growing on each loop repetition, a DXi should be sure to respond to this by method by flushing any queued events that lie outside the loop boundary.
Also, note that loops can be very short – even as short as a handful of MIDI ticks, even shorter than a single audio output buffer. This implies that a DXi must take care to remember which loop iteration “owns” a particular MIDI event. When it comes time to render that event into audio, the DXi will have repeated copies of the same event (from multiple loop repetitions) queued for the same time. Unless the DXi knows the loop iteration associated with each event, it may mistakenly play them all at once.
Called when playback (or other streaming session) stops, with the stop time.
This is called when a period of time is being processed. The period is represented by timeFrom through timeThru.
If there are any events to process, they are in the IMfxEventQueue* pqIn.
This method may be running on a time-critical thread, and should be written to execute as quickly as possible.
Your synth is free to remove elements from pqIn if desired, but this is not required, since the synth is always the last processing element in the chain.
Note that OnEvents is always called ahead of real-time – the events in pqIn will actually be rendered at a short time in the future. In order to allow certain output changes to occur at the last minute, “just in time”, the mch parameter is provided. This parameter can be used in the conjunction with the IMfxNotify interface to determine the actual state of the MIDI output stream the instant before the note is rendered.
Also, note that if the application is performing looping playback, that timeThru may actually extend beyond the loop end time. This behavior is necessary in order to allow DXi’s to quantize their MIDI data – which might mean moving “future” data back in time. For this reason, OnLoop should take care to flush any cached MIDI events outside the loop boundary.
The MfxEvent structure has members for the MIDI port and channel: m_byPort and m_byChan. Each event has a MIDI channel stored with it. In addition, tracks have a forced channel property, which can be “none” or a specific channel from 1 to 16. If the forced channel is other than “none”, then each event is stamped with that channel during playback. This stamping process takes place before you get the MfxEvent in OnEvents. If the user changes the track’s MIDI channel in the middle of playback, the DXi will be notified of the change via the IMfxNotify interface.
This is called to give you an opportunity to process MIDI input.
This method may be running on a time-critical thread, and should be written to execute as quickly as possible.
Your synth is free to remove elements from pqIn if desired, but this is not required, since the synth is always the last processing element in the chain.
In order to allow certain output changes to occur at the last minute, “just in time”, the mch parameter is provided. This parameter can be used in the conjunction with the IMfxNotify interface to determine the actual state of the MIDI output stream the instant before the note is rendered.
Returns an array of bank numbers, each of which provides patch names. Each bank number filled in panBank is guaranteed to provide a non-empty list of patch names when passed into GetPatchNames.
The caller is responsible for freeing the returned array panBank, by calling CoTaskMemFree.
If the specified patch in the specified bank is a drum patch, returns S_OK, else return S_FALSE.
Returns E_INVALIDARG is nBank is outside the range 0 through 16383 or if nPatch is outside the range 0 through 127.
Diatonic note names are names of musical notes like “C#0”, “Gb5”, and so on. Note names for a drum patch will typically not be diatonic note names, but rather names like “Kick”, “Snare”, and so on.
If the specified note name list is the list of diatonic note names, returns S_OK, else return S_FALSE.
Returns E_INVALIDARG is nBank is outside the range 0 through 16383 or if nPatch is outside the range 0 through 127.
For a specified bank, provides an IMfxNameList2*, which represents the names of the patches within that bank.
Returns S_OK, else returns E_INVALIDARG if nBank is outside the range 0 through 16383 or IMfxNameList2 is a bad pointer.
For a specified bank and patch, provides an IMfxNameList2*, which represents the names of the patches within that bank.
Returns S_OK, else returns E_INVALIDARG if nBank is outside the range 0 through 16383, if nPatch is outside the range 0 through 127, or if IMfxNameList2 is a bad pointer.
Provides an IMfxNameList2*, which represents the names of controllers. Returns S_OK.
Provides an IMfxNameList2*, which represents the names of RPNs (Registered Parameter Numbers). Returns S_OK.
Provides an IMfxNameList2*, which represents the names of NRPNs (Non-Registered Parameter Numbers). Returns S_OK.
This interface
extends (inherits from) IMfxSoftSynth, to allow a
DXi to expose a different instrument per MIDI channel.
Return the
instrument definition for the specified MIDI channel. As per standard COM rules, the pointer
returned in ppInstrument should be
AddRef’d before returning.
IMfxTempoMap represents the tempo map of the host application.
You may try to obtain this interface by using QueryInterface on the IUnknown* passed to Connect. This interface is always provided by Cakewalk products.
Convert between musical ticks and absolute milliseconds
Convert between absolute milliseconds and musical ticks
Get the number of ticks per quarter-note (the "PPQ" or "time base").
Get the index of the tempo change in effect for time 'lTicks'.
Get the number of tempo changes in the map.
Get a tempo change.
Returns the time of the tempo change in *plTicks, and the value of the tempo in
100ths of beats per minute in *plBPM100. (For example, 100.05 BPM is returned as
10005.)
This interface is provided by the host application, to perform a variety of time conversions for time values on the project time-line. It is especially useful for DXi synthesizers, who need to convert between musical time and absolute audio sample times, with high precision.
Convert the
supplied time value pTime into a
different format, specified by newFormat. See MFX_TIME
for a description of the available time formats for conversion.
The sample code is compatible with Microsoft® Visual C++ 6.0. There is one sample included, a multi-output DXi 2.0 synthesizer called “TONAR”, which can synthesize sine, square or saw tooth waveforms.
This sample code is compatible with Microsoft Visual C++
5.0. There are two samples included, one
developed using MFC and another using the Windows API. The sample is a simple MIDI echo effect.
This SDK includes 2 App Wizards for Visual C++. The first can be used to create a new MFX plug-in. The other can be used to create a new DXi or DirectX plug-in. Theses wizards are designed to automate much of the busy-work associated with creating new plug-ins, and thus to significantly decrease production time.
Tasks automated by the wizard include:
1. Generation of CLSIDs.
2. Creation of workspace and project files.
3. Creation of major classes.
4. Helpful commenting of the source files.
To use the wizard:
1. Copy
*.awx to the template directory of your Developer Studio installation,
typically:
C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Template
2. Select
File->New… and the Projects tab of the dialog box that will
appear.
3. Select
Cakewalk MFX Plug-in Wizard or DXi2
/ DirectX Plug-In from the list of templates, type a name for you project,
and click OK.
4. Follow
the wizard prompts. Note that the DXi
2.0 wizard requires you have already installed the Microsoft DirectX 8.0 SDK.
5. Modify
the generated source-files, filling in the “TODO” comments. For the DXi 2.0 App Wizard, most of your
coding will occur in {Project}.cpp and
{Project}PropPage.cpp, where {Project}
is the name you supplied to the App Wizard.
6. Note: A Readme.txt file that contains additional
information on each of the files will be included with the project created by
the wizard. Refer to the sample code
examples.