Programming DirectX Audio Plug-ins

By Ian Drumm

 

1.1   Background

 

DirectX audio plug-ins provide a convenient, modular and highly expandable way to process audio data in real time from within a client program such as sequencer or audio editor (e.g. cakewalk, cool edit, etc). The client application will recognise and make available a list of available plug-ins (e.g. Eqs, Reverbs, Compressors, Chorus, etc) from which the user can select and add to an audio processing chain. These plug-ins can also take as input midi data for midi based control of parameters and even sound synthesis.

 

DirectX plug-ins are based on Microsoft’s Component Object Model (COM) which allows plug-ins to be recognised and used by other applications via common interfaces. Plug-ins connect to applications and other plug-ins with pins via which they can pass and processes buffered streams of audio (or video) data.

 

1.2 Prerequisites

 

To develop DirectX applications familiarity with Microsoft’s Visual C++ is needed. The author assumes the reader is familiar with C programming (variables, operators, arrays, pointers, etc) and C++ object oriented programming (classes, objects, member functions, events, etc).

 

Some familiarity with developing simple dialog based applications using Microsoft Visual C++’s ‘MFC AppWizard’ is recommended. Visual C++ 6.0 provides some excellent features for the rapid development of user interfaces. For more info I recommend Sam’s Teach Yourself Visual C++ in 21 Days by N. Gurewich & O. Gurewich or similar books from the ‘Idiots’ and ‘Dummies’ series.

 

1.3 Getting Started

 

You will need

 

 

 

 

 

After installing appropriate SDKs and software you will need to copy the two wizards (*.awx files) that are include with the DXi SDK to the appropriate Visual C++ wizards directory

(e.g. C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Template)

 

A wizard will automatically create a code template for your plug-in and select appropriate build options to create a DirectX DLL that is automatically registered with windows and hence accessible to other audio applications .To use the wizard….

 

 

 

 

 

 

 

 

Build->Set Active Configuration … Win 32 Release

 

Having built your plug-in it will hence be visible to client audio applications such as Cakewalk. You could try the plug-in out on a wav file though as yet it won’t change your audio data.   

1.4 Simple Gain Plug-in

 

Now that you have built and tested your default plug-in as generated by the wizard you can next extend it’s functionality.

 

Essentially most of your DSP processing will take place in a file called

 

                {Your Plug-in Name}.cpp

 

In a member function called

 

                C{Your Plug-in Name}::Process()

 

When using your plug-in the member function Process() will be called repeatedly with successive small chunks of audio data. The method takes as it’s arguments pointers of type AudioBuffer (see DXi SDK Documentation) which allow the programmer to get pointers to input and output buffers of audio data and the data chunk size cSamp.

 

 

For example you can extend to Process() member function to include….

 

// TODO: Put your DSP code here

                int n;

float* x=pbufIn->GetPointer();                                                          //point to input audio data

                float* y=pbufOut->GetPointer();                                                       //point to output audio data

         

                for(n=0;n<pbufIn->cSamp;n++) {

                                y[n]=x[n]*fGain;                                                                    //change gain of each element

                }

                return S_OK;

 

which will in effect modify the gain of the audio data based on a floating point variable fGain. This variable comes from a slider control that was created using Visual C++’s drag and drop developers interface. You can add user interface controls to the plug-in via…

 

ResourceView ->{Your Plug-in Name} resources->Dialog-IDD_PROPAGE

 

From here you can select and position user interface controls and hence generate event handler member functions and control objects via the MFC Class Wizard.  To find out more about in implementing plug-in user interface controls properly and how to make those controls respond to automation I strongly recommend you read the ‘DXi 2 / DirectX Plug-In Wizard Tutorial’ that comes with the DXi2 SDK.

 

At the moment the plug-in won’t handle stereo data, this will be discussed later on.

 

The above shows a Simple Gain Plug-in running within Cakewalk.

 

To download this plug-in right click on … test2.dll

 

Registering Plug-ins

For the plug-in to be visible to other applications it’s DLL must be registered with the windows system. The DXi wizard will register the plug-in at the build stage however when transferring to other computers the plug-in must be registered using…

 

                Start -> Run -> regsvr32 ‘plug-in path’

 

This could be done by hand or as part of a setup application created with DirectSetup.

 

You could un-register the plug-in using DXMan from http://www.analogx.com/.

 

1.5 Simple Filter Plug-in

 

The buffered structure of audio data lends itself to the construction of FIR and IIR filter plug-ins. The following example implements a second order IIR filter based on coefficients entered by the user. The equation for such a second order filter is…

 

 

whereis the current element of the input data buffer, is current element of the output data buffer and is the unit delay operator,  for example ,  

 

If you have access to MATLAB filter coefficients to the order n can be easily found with MATLAB functions such as

 
[b,a] = butter(n,Wn)
 
[b,a] = cheby1(n,Rp,Wn)

 

Alternatively there are dozens of java based web sites and freebie downloads that will calculate filter coefficients, for example…

 

http://www.dsptutor.freeuk.com/IIRFilterDesign/IIRFilterDesign.html

 

http://dolphin.wmin.ac.uk/filter_design.html

 

http://www-users.cs.york.ac.uk/~fisher/mkfilter/

 

 

Code for a second order filter as part of the plug-in’s process() method could take the form

 

// TODO: Put your DSP code here

                int n;

                float* x=pbufIn->GetPointer();

                float* y=pbufOut->GetPointer();

               

                for(n=2;n<pbufIn->cSamp;n++) {

                                y[n]=(b0*x[n]+b1*x[n-1]+b2*x[n-2]-a1*y[n-1]-a2*y[n-2])/a0;

                }

 

where a0, a1, a2, b0, b1, b2 are external floats set by user interface components.

 

Though the first two points of the sample buffer aren’t processed so additional delay buffers

 

                float xd[2];

                float yd[2];

 

could be created to keep a record of important data from previous sample buffer. Hence code could take the form…

 

// TODO: Put your DSP code here

                int n;

                float* x=pbufIn->GetPointer();

                float* y=pbufOut->GetPointer();

 

                y[0]=(b0*x[0]+b1*xd[1]+b2*xd[0]-a1*yd[1]-a2*yd[0])/a0;

                y[1]=(b0*x[1]+b1*x[0]+b2*xd[1]-a1*y[0]-a2*yd[1])/a0;

                for(n=2;n<pbufIn->cSamp;n++) {

                                y[n]=(b0*x[n]+b1*x[n-1]+b2*x[n-2]-a1*y[n-1]-a2*y[n-2])/a0;

                }                             

                xd[1]=x[n];

                xd[0]=x[n-1];

                yd[1]=y[n];

                yd[0]=yd[n-1];

 

  

 

The above shows the IIR Filter plug-in running with-in Cakewalk. The user can enter coefficients manually from text input boxes or choose from some examples.

 

To download this plug-in right click on … IIR_Filter.dll

 

A more practical example would automatically calculate coefficients based on sliders for cut-off and resonance.  This is quite difficult requiring the application of bilinear transformation to analog prototypes of desired filters.

Fortunately there are recipes for the calculation of filter coefficients from simple parameters for example via http://www.harmony-central.com/Computer/Programming/Audio-EQ-Cookbook.txt.

 

The following function below will calculate coefficients for variety of filter types based on slider values for frequency and resonance and radio button selections for filter type.

 

Set_Coefficients(float f,float Q, int option)

{

// Find Coefficients based on f(0-1.0) and Q

                float w, pi=3.14159265;

                w=pi*f;

                switch(option) {

                case LP:

                                b0=(1-cos(w))/2;

                                b1=1-cos(w);

                                b2=(1-cos(w))/2;

                                a0=1+sin(w)/(2*Q);

                                a1=-2*cos(w);

                                a2=1-sin(w)/(2*Q);

                                break;

                case HP:

                                b0=(1+cos(w))/2;

                                b1=-(1+cos(w));

                                b2=(1+cos(w))/2;

                                a0=1+sin(w)/(2*Q);

                                a1=-2*cos(w);

                                a2=1-sin(w)/(2*Q);

                                break;

                case BP:

                                b0=sin(w)/2;

                                b1=0;

                                b2=-sin(w)/2;

                                a0=1+sin(w)/(2*Q);

                                a1=-2*cos(w);

                                a2=1-sin(w)/(2*Q);

                                break;

                case NOTCH:

                                b0=1;

                                b1=-2*cos(w);

                                b2=1;

                                a0=1+sin(w)/(2*Q);

                                a1=-2*cos(w);

                                a2=1-sin(w)/(2*Q);

                                break;

                }

}

 

Hence a more practical filter plug-in was developed as shown. Note the frequency is expressed as a value between 0 and 1.0 corresponding to the range between 0Hz and Nyquist limit.

 

 

To download this plug-in right click on … Biquad_Filter.dll

 

1.6 Simple Delay Plug-in

 

A delay needs to keep some kind of record of previous audio data. The easiest way I can find is by appending chunks of sample data to a larger delay buffer. Every time processing of a chunk finishes the whole delay buffer is shifted left by the size of a chunk. I suspect this is not the quickest way to do things though the demands on the CPU don’t seem prohibitive. As well as facilitating simple delays having access a large left shifting delay buffer facilitates more sophisticated reverberation and FIR filtering lending it ‘self to convolution with impulse responses.

 

In the code below the external variable ‘delay’ is a float between 0.0 and 1.0 representing delay time being linked to a slider. The delay buffer ‘d[...]’ should be created with malloc() though could be declared globally, for example…

 

const DSIZE=5000;

float d[DSIZE];

 

hence the DSP code in Process() is…

 

// TODO: Put your DSP code here

                int n, delay_pts;

 

                delay_pts =(int)(delay*(DSIZE-pbufIn->cSamp));

 

                float *in=pbufIn->GetPointer();

                float *out=pbufIn->GetPointer();

 

                nprev=DSIZE-pbufIn->cSamp;

                memcpy(&d[nprev],in,4*(pbufIn->cSamp));

 

                for(n=nprev;n<(nprev+pbufIn->cSamp);n++)

                                                out[n-nprev]=d[n]+d[n-delay_pts];

 

                memcpy(&d[0],&d[pbufIn->cSamp],4*(DSIZE-pbufIn->cSamp));

 

A delay is also a good starting point for effects such as chorus and flange where a LFO (e.g. sine-wave) could be used to modulate the delay time there by creating a moving comb filter.

 

To download this plug-in right click on … Delay.dll

 

1.7 Stereo

 

DXi plugins can process stereo data. You can determine the number of channels with…

 

                int chans=GetOutputFormat()->nChannels;

 

and set a flag to indicate if stereo input is being used…

 

                BOOL const bStereo=(chans==2);

 

Hence presuming the right and left buffers are interleaved your processing would take the form…

 

for(i=0;i<(pbufIn->cSamp*chans);i+=chans) {

                                y[i]=f( x[i] );

                                if(bStereo) {

                                                j=i+1;

                                                y[j]=f( x[j] );

                                }

}                                             

 

where f(…) is some DSP function.

1.8 Programming Softsynths 

 

The DXi API lends itself to the creating of quite sophisticated ‘softsynths’ by parsing midi input data hence producing audio output. Unfortunately the example given with the SDK (twonar) deals with a relatively sophisticated plug-in. The following is based on twonar though is somewhat simplified in an attempt to explain the absolute basics. It’s a simple monophonic sine-wave synth.

 

As with previous examples we have input and output audio buffers, however for most cases we’ll wish to ignore the input buffer, instead writing our sin-wave to an output buffer. DXi2 supports multiple outputs, for each new output a new ‘output pin’ needs to be created. However we’ll ignore this and use the default single output and single output pin.

 

Midi event data comes in the form of a queue of objects of type DXiEvent that can be pulled off from a list. The trick will be to filter out and convert the note events from the list into frequency values.

 

(for more on DxiEvent class see Cakewalk Plug-in Development Kit documentation)

 

Firstly create a new DXi softsynth using the DXi wizard. As before the program will compile and become available as a plug-in to other compatible applications – though as yet won’t make a noise. Note by compatible applications I mean those that support DXi softsynths (e.g. Sonar).

 

Override the method

 

C{Your Plug-in Name}:: Initialize()

 

Here you could add a look up table that converts midi note numbers to frequencies. This is easily done knowing that note 69 corresponds to frequency 440Hz so any notes above or below this will be given by corresponding equal temperament semitone intervals. Given the number of notes n up or down, frequencies are given by…

 

               

 

               

 

So the Initialize( ) method could contain….

 

HRESULT C{Your Plug-in Name}::Initialize()

{

                // TODO: put all initialization code here

               

                // Create table to convert note number to frequency value

                double const semitone_interval=pow(2.0,1.0/12.0);

                m_freq[69]=440;   // A5=note number 69=440Hz

                int i;

                for(i=70;i<127;i++)

                                m_freq[i]=m_freq[i-1]*semitone_interval;

                for(i=68;i>=0;i--)

                                m_freq[i]=m_freq[i+1]/semitone_interval;

 

                return S_OK;

}

 

where m_freq is a globally accessible array of say 128 doubles as defined by...

 

                static double m_freq[128];

 

Next you need to filter only those DXi events you need with the IsValidEvent( ) method, e.g. for midi notes…

 

HRESULT C{Your Plug-in Name}::IsValidEvent( DXiEvent& de )

{

                if(MfxEvent::Note==de.me.m_eType)

                                return S_OK;

                else

                                return S_FALSE;

}

 

Every DXi event can have associated with it your own data via the InitializeNoteEvent() method. In the InitializeNoteEvent() method below I’m instantiating and filling a data structure called NoteState that was defined earlier to hold useful sample position and phase angle data to be used when generating wave shapes.

 

                struct NoteState {

                                long        lSampDuration;

                                long        lSampCurrent;

                                double    dTheta;

                                double    dStep;

                };

 

Every time a new note is picked up it will first be passed to the InitializeNoteEvent ( ) method before being added to the note event queue for processing. The DXiEvent class has a special pointer (pvData) where one can associate their own data object with each DXi event.

 

HRESULT C{Your Plug-in Name}::InitializeNoteEvent( DXiEvent& de )

{

                NoteState* pns = new NoteState;

                if (NULL == pns)

                                return E_OUTOFMEMORY;

 

                pns->lSampCurrent = 0;

                pns->dTheta = 0;

 

                // work out a phase angle = 2*pi*f*t using look up table created earlier

                pns->dStep = 2*PI*m_freq[ de.me.m_byKey ] / GetOutputFormat()->nSamplesPerSec;

               

                // Note-on events that are played live have "infinite" duration.  Check for these

                // and handle them specially.

                if (ULONG_MAX == de.me.m_dwDuration)

                                pns->lSampDuration = LONG_MAX;

                else

                                pns->lSampDuration = m_pCtx->TicksToSamples( de.me.m_lTime + de.me.m_dwDuration ) - m_pCtx->TicksToSamples( de.me.m_lTime );

 

                de.pvData = pns;

 

                return S_OK;

}

 

It will also be important to later free memory for such temporary event objects using…

 

HRESULT C{Your Plug-in Name}::ExpireNoteEvent( DXiEvent& de, BOOL bForce )

{

                // TODO: add your own code here to test whether a note event

                // has been played to completion and/or to free any memory,

                // allocated for de.pvData and return S_OK.

 

                NoteState* pns = reinterpret_cast<NoteState*>( de.pvData );

                if (NULL == pns)

                                return S_OK;

 

                if (bForce || pns->lSampCurrent >= pns->lSampDuration)

                {

                                delete pns;

                                de.pvData = NULL;

                                return S_OK;

                }

 

                return S_FALSE;

}

 

Ok so now to actually creating a musical note. As with effects the action takes placing in the Process() method though this time we ignore the audio input buffer – instead pulling note events from a list hence creating small segments

(~708 samples) of output data - on the fly.

 

Below is an overridden Process( ) method – can you see how the midi note data is pulled off an event queue using a for loop. The output buffer is hence written to using a simple sin() function.

 

HRESULT C{Your Plug-in Name}::Process( LONGLONG llSampAudioTimestamp,

                                                                AudioBuffer* pbufIn,

                                                                AudioBuffer* abufOut, long cBufOut,

                                                                LONGLONG llSampMidiClock, deque<DXiEvent>& qMidi )

{

                float* pfBufI = (NULL != pbufIn && !pbufIn->GetZerofill()) ? pbufIn->GetPointer() : NULL;

 

                // TODO: Put your DSP code here

                int n, i, j, chans;

                long cSampBuf = pbufIn->cSamp;

                float gain=0.5;

 

                //pull off midi events from a queue and process

                deque<DXiEvent>::iterator it;

                for(it=qMidi.begin();it!=qMidi.end();it++)

                {

                                DXiEvent& de=*it;

                                if(de.me.m_eType==MfxEvent::Note) {

 

                                                NoteState* pns=reinterpret_cast<NoteState*>(de.pvData);

                                                if (pns == NULL)

                                                                return E_UNEXPECTED;

 

                                                                long lBufOfs = static_cast<long>(max( 0, de.llSampTimestamp - llSampMidiClock - pns->lSampCurrent));

                                                long cSampToGo=pns->lSampDuration-pns->lSampCurrent;

                                                long cSampSynth=min(cSampToGo,cSampBuf-lBufOfs);

                                                ASSERT( cSampSynth >= 0 );

                                                cSampSynth=max(cSampSynth,0);

 

                                                BOOL const bNoteOff=(0==de.me.m_dwDuration);     

                                                BOOL const bFadeTail=

                                                                bNoteOff || (pns->lSampCurrent+cSampSynth>=pns->lSampDuration);

 

                                                //determine number of channels

                                                chans=GetOutputFormat()->nChannels;

                                                BOOL const bStereo=(chans==2);

                                               

                                                //get pointer to ouput buffer

                                                float        *y=abufOut[0].GetPointer();

                                                y=y+lBufOfs*chans;

                                               

                                                //process current chunk of data

                                                for(n=pns->lSampCurrent,i=0;n<(pns->lSampCurrent+cSampSynth);n++,i+=chans) {

                                               

                                                                y[i]=gain*sin(pns->dTheta);

                                                                if(bStereo) {

                                                                                j=i+1;

                                                                                y[j]=gain*sin(pns->dTheta);

                                                                }

                                                                pns->dTheta+=pns->dStep;

 

                                                }

                                               

                                                if(bNoteOff)

                                                                pns->lSampCurrent=pns->lSampDuration=0;

                                                else

                                                                pns->lSampCurrent+=cSampSynth;

                                }             

                }

                return S_OK;

} 

 

The simple sine wave player above could hence be extended to produce something more useful. For example you could…

 

 

 

 

 

Useful Links

 

http:://www.musicdsp.org – terrific resource for D.S.P. algorithms.

  

http://www.directxfiles.com – cakewalk’s dedicated site for plug-ins.

 

http://www.harmony-central.com – great all round music technology site.

 

http://www.inforamp.net/~poynton/Poynton-dsp.html - filter design stuff.

 

http://users.iafrica.com/k/ku/kurient/dsp/algorithms.html - intro to audio effects.

 

 

If anyone knows of a good book or web site that explains the creation of DirectX audio plug-ins please email me at i.drumm@salford.ac.uk.

 

I play in some bands, MP3s etc via https://members.tripod.com/acmerock/