DXi 2 / DirectX Plug-In Wizard Tutorial

 

In this tutorial, we will use the Plug-In Wizard to create a simple automated plug-in.  This plug-in will implement a very simple "fuzz box" effect, by hard-clipping the audio waveform against a particular threshold.  The plug-in will also provide an output gain control and bypass control.

A bird's-eye view of the tutorial reveals a simple, straight-forward process:

I. Initialize a Brand New Plug-in Project

II. Add New Plug-In Parameters

III. Implement the DSP

IV. Implement the User Interface

V. Adding UI Capture Logic

VI. Sample Accurate Rendering

 


I. Initialize a Brand New Plug-in Project

The first thing we'll need to do is create a new "boilerplate" project.This is done automatically by the plug-in Wizard, as follows.

1.           Launch VC6.

2.           Choose File | New.

3.           Click on the [Projects] tab

4.           Click on the icon named "DXi 2 / DirectX Plug-In".

5.           Enter the project name:AutoClip

6.           Press [OK].

At this point the DXi 2 / DirectX Plug-In Wizard dialog box will appear.In this example, we'll be building a DirectX plug-in.

7.           Click the "DirectX Audio Plug-In" radio button.

8.           If you have not installed the DirectX or Platform SDK in their default locations, you may need to specify the directory where the DirectX SDK is installed.To do this, click the "DirectX SDK Location" checkbox, and enter the path to the DirectX SDK on your system.

9.           Make sure that "Use MFC for user interface" is selected.

10.       Press [Finish]

At this point a confirmation window will appear, detailing the files that have been created for you by the Wizard.

The plug-in will now build, and should automatically register itself.

Note that if you have incorrectly specified the path to the DirectX SDK, you will get the following error upon attempting to build:

fatal error C1083: Cannot open include file: 'streams.h': No such file or directory

If this happens, simply exit from VC6, delete the project you created, and recreate a new one.Be sure to specify the correct path to your DirectX SDK in step 8 (above).


II. Add New Plug-In Parameters

In this part of the tutorial, we'll specify the parameter to be used by the plug-in.

1.           Open Parameters.h for editing.

Note that the plug-in wizard has already created an "enable" parameter for you.In your code, you will refer to this parameter by its integer index, PARAM_ENABLE.This parameter is defined to be a boolean variable (MPT_BOOL), and can respond jumps, line segments, and curve shapes.


Let's add the parameters for clip threshold and output gain:

2.           Add parameter indices for these new parameters to the existing enumeration:

enum

{

������ PARAM_ENABLE,

������ PARAM_THRESHOLD,

������ PARAM_OUTPUTGAIN,

 

������ // TODO: add new parameter indices here

 

������ NUM_AUTOMATED_PARAMS,

 

������ // TODO: Add new internal parameter IDs here.Make sure to assign the

������ // first value to NUM_AUTOMATED_PARAMS, i.e.,

������ //

������ // _PARAM_INTERNAL1 = NUM_AUTOMATED_PARAMS,

������ // _PARAM_INTERNAL2,

������ // ...

 

������ NUM_PARAMS

};

 

3.           Add parameter info to the existing table.Threshold will be expressed as percentage of full-code where the clipping should be applied.Output gain will be expressed as percentage scale factor from 0x to 2x.

const ParamInfo CMediaParams::m_aParamInfo[ NUM_PARAMS ] =

{

// MP_TYPE��� MP_CAPS������ �� min max�� def��� unitslabel���� int.minint.max"Enum1,Enum2,..."

// -------��� -------������ �� --- ---�� ---��� ----------���� ------------------------------

{MPT_BOOL,MP_QUADS,0, 1,��� 1,���� L"",�� L"Enabled",��� 0,��� ����� 1,���� �� NULL������ },

{MPT_FLOAT, MP_QUADS,0, 100,50,��� L"%",L"Threshold",0,��� ����� 1,���� �� NULL������ },

{MPT_FLOAT, MP_QUADS,0, 200,100,�� L"%",L"OutputGain", 0,��� 2,���� �� NULL������ },

 

// TODO: add entries for additional parameters here

};

 

 

The plug-in will now expose parameters to an automation-enabled host, such as Cakewalk SONAR.


III. Implement the DSP

At this point we've got a plug-in with parameters.Let's make it do something!

1.           Open AutoClip.cpp for editing.

This file is where all the work is done.It contains methods for managing which audio formats are supported by the plug-in, as well as the entry point for actual DSP.By default, the Wizard provides code for the plug-in to be a "process in place" filter, that accepts either stereo or mono floating point data.All you need to do is write the processing code.


Add the following code to CAutoClip::Process

HRESULT CAutoClip::Process( LONGLONG llSampAudioTimestamp,

��������������������������� AudioBuffer* pbufIn, AudioBuffer* pbufOut

{

������ BOOL const bGenerateTail = (NULL == pbufIn);

������ BOOL const bIsInPlace = (pbufIn == pbufOut);

 

������ . . .

 

������ // TODO: Put your DSP code here

 

������ // Silent input always produces silent output

������ if (pbufIn->GetZerofill())

������ {

������������� pbufOut->SetZerofill( TRUE );

������������� return S_OK;

������������� //////

������ }

 

������ // Get buffer pointers now that we know they aren't zero-filled

������ float* pfSrc = pbufIn->GetPointer();

������ float* pfDst = pbufOut->GetPointer();

 

������ // If we're bypassed, copy input to output without processing

������ float fEnabled = GetParamValue( PARAM_ENABLE );

������ if (fEnabled < 0.5f)

������ {

������������� memcpy pfDst, pfSrc, pbufIn->cSamp * m_wfxIn.nBlockAlign );

������������� return S_OK;

������ }

 

������ // Get the current parameter values for this buffer

������ float fThreshold = GetParamValue( PARAM_THRESHOLD );

������ float fOutputGain =GetParamValue( PARAM_OUTPUTGAIN );

 

������ // Process the buffer

������ unsigned const cFloats = pbufIn->cSamp * m_wfxIn.nChannels;

������ for (unsigned ix = 0; ix < cFloats; ix++)

������ {

������������� // Clip the input sample against the threshold

������������� float fSrc = pfSrc[ ix ];

������������� if (fSrc > fThreshold)

�������������������� fSrc = fThreshold;

������������� else if (fSrc < -fThreshold)

�������������������� fSrc = -fThreshold;

 

������������� // Apply the output gain and store it

������������� pfDst ix ] = fSrc * fOutputGain;

������ }

 

������ return S_OK;

}

 

 


 

IV. Implement the User Interface

In this part of the tutorial, we'll create a simple UI, to provide a slider for the threshold and output gain; and check box for the enable parameter, which acts like a bypass.

First, we'll need to lay out the property page.

1.           Click on the [Resource] tab of your plug-in project, expand the "Dialog" folder, and double-click on IDD_PROPPAGE to begin editing it.

2.           Delete the static text control which says "TODO: Insert dialog controls here."

3.           Drag 2 static text controls and 2 sliders into the property page.Name the text "Threshold" and "Output Gain", respectively.Assign the slider controls IDs to be IDC_THRESHOLD and IDC_OUTPUTGAIN, respectively.

4.           Drag a checkbox into the property page.Name the checkbox "Enabled", and assign its ID to IDC_ENABLED.

Next, we'll want to assign some variables and actions in our controls, so that our property page code can retrieve values from them.

5.           Choose View | Class Wizard...

6.           Click on the [Message Maps] tab.

7.           Click on Object ID IDC_ENABLED, and double-click on BN_CLICKED.Accept the proposed handler name, OnEnabled.(This function will be our handler for when the user clicks on the "Enabled" checkbox.

8.           Click on the [Member Variables] tab.

9.           Click on IDC_OUTPUTGAIN, and the [Add Variable...] button.Name the variable m_sliderOutputGain, using the category "Control" and variable type "CSliderCtrl."

10.       Click on IDC_THRESHOLD, and the [Add Variable...] button.Name the variable m_sliderThreshold, using the category "Control" and variable type "CSliderCtrl."

If you've ever worked with slider controls before, you know that they require some initialization to establish the min/max range, etc.This is best handled when the dialog box is initialized.So, we'll need to add a custom OnInitDialog method.

Slider controls send their position via a Windows scroll message.Since the sliders are horizontally layed out, the dialog box will require a custom OnHScroll method.

Also, we'll want our property page to have "flying faders" that update to follow the current parameter value.We'll do this by associating a Windows timer with our property page, and updating the contents of our controls on the timer callback.So, we'll need to add a custom OnTimer method.

We'll create the timer when we initialize the dialog box.But we'll need to destroy it too, so we'll add a custom OnDestroy method.

11.       Click on the [Message Maps] tab.

12.       Click on Object ID CAutoClipPropPage, and double click on the following messages: WM_INITDIALOG, WM_HSCROLL, WM_TIMER, WM_DESTROY.

13.       Click [OK].


Now we can implement the behaviors for the property page controls.First, let's implement the dialog initialization code.Open AutoClipPropPage.cpp for editing:

BOOL CAutoClipPropPage::OnInitDialog()

{

������ COlePropertyPage::OnInitDialog();

������

������ // Set ranges of slider controls to match parameters

������ m_sliderThreshold.SetRange( 0, 100, TRUE );

������ m_sliderOutputGain.SetRange( 0, 200, TRUE );

 

������ // Set up a timer to periodically update every 50 msec

������ SetTimer 0, 50, NULL );

 

������ return TRUE;// return TRUE unless you set the focus to a control

������ ������������� // EXCEPTION: OCX Property Pages should return FALSE

}

 

14.       Implement the handler for the "Enabled" checkbox.

void CAutoClipPropPage::OnEnabled()

{

������ float fValue = IsDlgButtonChecked( IDC_ENABLED );

������ if (m_pUICallback)

������ {

������������� DWORD dwIndex = PARAM_ENABLE;

������������� m_pUICallback->ParamsBeginCapture &dwIndex, 1 );

������������� m_pUICallback->ParamsChanged &dwIndex, 1, &fValue );

������������� m_pUICallback->ParamsEndCapture &dwIndex, 1 );

������ }

}

 

15.       Handle updating all of our controls on the periodic timer.

void CAutoClipPropPage::OnTimer(UINT nIDEvent)

{

������ if (m_pMediaParams)

������ {

������������� float fVal;

�������������

������������� m_pMediaParams->GetParam PARAM_THRESHOLD, &fVal );

������������� m_sliderThreshold.SetPos( int(fVal) );

�������������

������������� m_pMediaParams->GetParam PARAM_OUTPUTGAIN, &fVal );

������������� m_sliderOutputGain.SetPos( int(fVal) );

�������������

������������� m_pMediaParams->GetParam PARAM_ENABLE, &fVal );

������������� CheckDlgButton IDC_ENABLED, fVal != 0 );

������ }

������

������ COlePropertyPage::OnTimer(nIDEvent);

}

 

16.       Clean up the timer when the dialog box is destroyed.

void CAutoClipPropPage::OnDestroy()

{

������ COlePropertyPage::OnDestroy();

������

������ KillTimer 0 );

}

 



17.       Handle scroll notifications from the sliders.

void CAutoClipPropPage::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

������ COlePropertyPage::OnHScroll(nSBCode, nPos, pBar);

 

������ if (m_pUICallback)

������ {

������������� float fVal = 0;

������������� DWORD dwIndex = 0;

������������� if (IDC_THRESHOLD == pScrollBar->GetDlgCtrlID())

������������� {

�������������������� fVal = static_cast<float>( m_sliderThreshold.GetPos() );

�������������������� dwIndex = PARAM_THRESHOLD;

������������� }

������������� else if (IDC_OUTPUTGAIN == pScrollBar->GetDlgCtrlID())

������ {

�������������������� fVal = static_cast<float>( m_sliderOutputGain.GetPos() );

�������������������� dwIndex = PARAM_OUTPUTGAIN;

������������� }

������������� else

������������� {

�������������������� ASSERT(FALSE);

�������������������� return;

������������� }

������������� m_pUICallback->ParamsChanged &dwIndex, 1, &fVal );

������ }

}



V. Adding UI Capture Logic

Play with the plug-in at this point.Create an automation envelope for one of the parameters, and watch how the fader in the plug-in tracks the envelope's value.Cool!Now grab the fader (while it's flying), and wiggle it.��Not cool!The fader is "fighting" with the "envelope."

The reason this is happening is basically because the UI isn't done yet.We need to implement the last bit of mechanism where the UI can tell the plug-in that the user is dragging on control.This lets the plug-in respond by disregarding envelopes.With this functionality in place, not only do you end the "fight" between the fader and the envelope, but you also enable recording fader movements directly from the plug-in's UI.This is a critical end-user feature!

What we need is a slider control that can tell us when a mouse-down or mouse-up event occurs on it.The standard CSliderCtrl that comes with MFC doesn't do this for us, so we'll need to derive a special flavor of CSliderCtrl, called CCaptureSlider.Add the following code to the top of AutoClipPropPage.h:

#ifndef _PLUGIN_PROP_PAGE_H_

#define _PLUGIN_PROP_PAGE_H_

 

#if _MSC_VER > 1000

#pragma once

#endif // _MSC_VER > 1000

// FilterPropPage.h header file

//

 

struct IMediaParams;

struct IMediaParamsUICallback;

 

/////////////////////////////////////////////////////////////////////////////

// Custom slider control, to track mouse capture/release

 

class CCaptureSlider;

 

class CCaptureNotify

{

public:

������ virtual void OnBeginCapture( int idCtrl, UINT nFlags, CPoint point ) = 0;

������ virtual void OnEndCapture( int idCtrl, UINT nFlags, CPoint point ) = 0;

};

 

class CCaptureSlider : public CSliderCtrl

{

public:

������ CCaptureSlider CCaptureNotify* pNotify ) :

������������� m_pNotify(pNotify), m_bCaptured(FALSE) {}

 

������ BOOL IsCaptured) const { return m_bCaptured; }

 

������ // Implementation

protected:

������ // Generated message map functions

������ //{{AFX_MSG(CCaptureSlider)

������ afx_msg void OnLButtonDown( UINT nFlags, CPoint point );

������ afx_msg void OnLButtonUp( UINT nFlags, CPoint point );

������ //}}AFX_MSG

������ DECLARE_MESSAGE_MAP();

 

private:

������ CCaptureNotify*����� m_pNotify;

������ BOOL���������������� m_bCaptured;

};

 

 

/////////////////////////////////////////////////////////////////////////////

// CAutoClipPropPage dialog

 

class CAutoClipPropPage :

������ public COlePropertyPage,

������ public CUnknown,

������ public CCaptureNotify

{

// Construction

public:

������ CAutoClipPropPage IUnknown* pUnk, HRESULT* phr );

������ virtual ~CAutoClipPropPage();

 

������ STDMETHODIMP QueryInterfaceREFIID riid, void **ppv);

������ STDMETHODIMP_(ULONG) AddRef();

������ STDMETHODIMP_(ULONG) Release();

������ STDMETHODIMP NonDelegatingQueryInterfaceREFIID riid,void **ppv);

������ STDMETHODIMP_(ULONG) NonDelegatingRelease();

������ STDMETHODIMP_(ULONG) NonDelegatingAddRef();

 

// Dialog Data

������ //{{AFX_DATA(CAutoClipPropPage)

������ enum { IDD = IDD_PROPPAGE };

������ CCaptureSlider m_sliderThreshold;

������ CCaptureSlider m_sliderOutputGain;

������ //}}AFX_DATA

 

������ // CCaptureNotify

������ virtual void OnBeginCapture( int idCtrl, UINT nFlags, CPoint point );

������ virtual void OnEndCapture( int idCtrl, UINT nFlags, CPoint point );

 

// Overrides

������ // ClassWizard generate virtual function overrides

������ //{{AFX_VIRTUAL(CAutoClipPropPage)

������ protected:

������ virtual void DoDataExchange(CDataExchange* pDX);��� // DDX/DDV support

������ //}}AFX_VIRTUAL

 

. . .

 

 

The theory here is every CCaptureSlider is owned by a CCaptureNotify.The slider will tell its owner to start capture when the mouse comes down, and to end capture when the mouse goes up.

The implementation of CCaptureSlider is as follows.Add this code to AutoClipPropPage.cpp:

/////////////////////////////////////////////////////////////////////////////

// CCaptureSlider

 

BEGIN_MESSAGE_MAP(CCaptureSlider, CSliderCtrl)

������ //{{AFX_MSG_MAP(CCaptureSlider)

������ ON_WM_LBUTTONDOWN()

������ ON_WM_LBUTTONUP()

������ //}}AFX_MSG_MAP

END_MESSAGE_MAP()

 

void CCaptureSlider::OnLButtonDown( UINT nFlags, CPoint point )

{

������ m_bCaptured = TRUE;

������ if (m_pNotify)

������������� m_pNotify->OnBeginCapture GetDlgCtrlID(), nFlags, point );

������ CSliderCtrl::OnLButtonDown( nFlags, point );

}

 

void CCaptureSlider::OnLButtonUp( UINT nFlags, CPoint point )

{

������ m_bCaptured = FALSE;

������ if (m_pNotify)

������������� m_pNotify->OnEndCapture GetDlgCtrlID(), nFlags, point );

������ CSliderCtrl::OnLButtonUp( nFlags, point );

}

 

 

Finally, CAutoClipPropPage will need to be made aware of its new capture sliders.First, we'll need to construct the new CCaptureSlider members:

CAutoClipPropPage::CAutoClipPropPage( IUnknown* pUnk, HRESULT* phr ) :

������ COlePropertyPage CAutoClipPropPage::IDD, IDS_NAME_PLUGIN ),

������ CUnknown "AutoClipPropPage", pUnk ),

������ m_sliderThreshold(this),

������ m_sliderOutputGain(this),

������ m_pMediaParams(NULL),

������ m_pUICallback(NULL),

������ m_bFirstQI(TRUE)

{�����

������ AFX_MANAGE_STATE( AfxGetStaticModuleState() );

 

������ //{{AFX_DATA_INIT(CAutoClipPropPage)

������ //}}AFX_DATA_INIT

 

#ifdef _DEBUG

������ // Turn off obnoxious "non-standard size" warnings from MFC.At least

������ // they gave us a flag to do this!

������ m_bNonStandardSize = TRUE;

#endif

}

 


Next, we'll need to add the 2 overrides for CCaptureNotify:

void CAutoClipPropPage::OnBeginCapture( int idCtrl, UINT nFlags, CPoint point )

{

������ if (m_pUICallback)

������ {

������������� DWORD dwIndex = 0;

������������� if (IDC_THRESHOLD == idCtrl)

�������������������� dwIndex = PARAM_THRESHOLD;

������ else if (IDC_OUTPUTGAIN == idCtrl)

�������������������� dwIndex = PARAM_OUTPUTGAIN;

������������� else

������������� {

�������������������� ASSERT(FALSE);

�������������������� return;

������������� }

������������� m_pUICallback->ParamsBeginCapture &dwIndex, 1 );

������ }�����

}

 

void CAutoClipPropPage::OnEndCapture( int idCtrl, UINT nFlags, CPoint point )

{

������ if (m_pUICallback)

������ {

������������� DWORD dwIndex = 0;

������������� if (IDC_THRESHOLD == idCtrl)

�������������������� dwIndex = PARAM_THRESHOLD;

������������� else if (IDC_OUTPUTGAIN == idCtrl)

�������������������� dwIndex = PARAM_OUTPUTGAIN;

������������� else

������������� {

�������������������� ASSERT(FALSE);

�������������������� return;

������������� }

������������� m_pUICallback->ParamsEndCapture &dwIndex, 1 );

������ }�����

}

 

That's it!Flying faders, without the fighting.Your plug-in is just about finished.That is, of course, unless you are a purist.


VI. Sample Accurate Rendering

As a final polishing step for the plug-in, we'll tweak the DSP code so that it renders automation data sample by sample.This powerful feature of DirectX automation allows for absolutely zipper-free volume transitions and high precision rendering.

When automation shapes are specified as line-segments or quadratic curves, it is possible to trace the contour of the curve sample by sample, using finite differences.(This is akin to the first- and second- derivative function, which is always linear or constant for a second order equation.)

The bottom line is, with the additional expense of 2 floating point additions per sample, you can automate gain transitions with complete sample accuracy.

Open AutoClip.cpp for editing, and add the following new lines of code:

HRESULT CAutoClip::Process( LONGLONG llSampAudioTimestamp,

�������������������� AudioBuffer* pbufIn, AudioBuffer* pbufOut

{

������ BOOL const bGenerateTail = (NULL == pbufIn);

������ BOOL const bIsInPlace = (pbufIn == pbufOut);

 

������ . . .

 

������ // TODO: Put your DSP code here

 

������ // Silent input always produces silent output

������ if (pbufIn->GetZerofill())

������ {

������������� pbufOut->SetZerofill( TRUE );

������������� return S_OK;

������������� //////

������ }

 

������ // Get buffer pointers now that we know they aren't zero-filled

������ float* pfSrc = pbufIn->GetPointer();

������ float* pfDst = pbufOut->GetPointer();

 

������ // If we're bypassed, copy input to output without processing

������ float fEnabled = GetParamValue( PARAM_ENABLE );

������ if (fEnabled < 0.5f)

������ {

������������� memcpy pfDst, pfSrc, pbufIn->cSamp * m_wfxIn.nBlockAlign );

������������� return S_OK;

������ }

 

������ // Get the current parameter values for this buffer

������ float fThreshold = GetParamValue( PARAM_THRESHOLD );

������ float fOutputGain =GetParamValue( PARAM_OUTPUTGAIN );

 

������ // Get parameter deltas for this buffer

������ double dThresholdD1 = 0;

������ double dThresholdD2 = 0;

������ double dGainD1 = 0;

������ double dGainD2 = 0;

������ GetParamDeltas PARAM_THRESHOLD, &dThresholdD1, &dThresholdD2 );

������ GetParamDeltas PARAM_OUTPUTGAIN, &dGainD1, &dGainD2 );

 

������ // Deltas are provided in units/sec; convert to units/sample

������ double const dPeriod = 1.0 / GetInputFormat()->nSamplesPerSec;

������ dThresholdD1 *= dPeriod;

������ dThresholdD2 *= (dPeriod * dPeriod);

������ dGainD1 *= dPeriod;

������ dGainD2 *= (dPeriod * dPeriod);

 

������ // Process the buffer

������ unsigned const cFloats = pbufIn->cSamp * m_wfxIn.nChannels;

������ for (unsigned ix = 0; ix < cFloats; ix++)

������ {

������������� // Clip the input sample against the threshold

������ ������ float fSrc = pfSrc[ ix ];

������������� if (fSrc > fThreshold)

�������������������� fSrc = fThreshold;

������������� else if (fSrc < -fThreshold)

�������������������� fSrc = -fThreshold;

 

������������� // Apply the output gain and store it

������������� pfDst ix ] = fSrc * fOutputGain;

������

������������� // Apply deltas to the threshold and gain

������������� fThreshold += dThresholdD1;

������������� dThresholdD1 += dThresholdD2;

������������� fOutputGain += dGainD1;

������������� dGainD1 += dGainD2;

}

 

������ return S_OK;

}