• 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    units  label       int.min  int.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

    }