









In this article, we are going to utilize DirectShow to do file format conversion, i.e from WAV file into MP3 and vice versa. You are also going to learn how to construct filter graph manually, including how to add filter into filter graph and connect a filter with another filter.
You are expected to have some knowledge about DirectShow and how to use DirectShow with Delphi. You can read my articles Multimedia Player with DirectShow Part 1 and Multimedia Player with DirectShow Part 2 as a starting point. You are also expected to have knowledge on COM programming subject, because we are going to use it a lot in DirectX, at least you know how to create an instance of COM object. Why? Because DirectX (including its components) built on the top of COM.
We are going to need access to these following softwares:
DirectShow is architecture for multimedia streaming on Windows. Building blocks of DirectShow are software component called filter. Filters are COM server which is operate on multimedia stream data and it's encapsulated in IBaseFilter interface. Filters are doing many things, some filters can do data loading from storage media, decompressing and/or compressing data, manipulating data, writing multimedia stream to storage media and still many more.
Filters may need input or produce output. A filter at least, has one input or output, where number of input or output is not limited. A filter may need two inputs and produce one output. Input and output on filter is called pin.
A filter in general, only doing specific task, for example, to load data from file. To be able to do something more complex, filters can be connected through their pins. For two pins of two filters can be connected, input pin of first filter must be connected to output pin of second filter and both pins must agree with media type for data transfer between both pins. How to connect pins will be discussed soon. Example is shown on following diagram:

Fig.1 Filter graph WAV playback
To play WAV file on the speaker, first thing to do is to add and set File Source with name of file to load. Its output pin, which is in the form of berupa audio stream data, connected to Wave Parser filter which its job is to parse audio stream into audio data suitable for WaveOut Renderer. WaveOut Renderer filter sends audio data to the speaker. Note that File Source only has one output without any input. Wave Parser has one input and one output. WaveOut Renderer has one input pin.
Typically, filters must be connected downstream, i.e from source to destination. So, for three filters above, File Source must connect to Wave Parser first, before Wave Parser can connect toWaveOut Renderer. But some filters don't have to be connected downstream.
To convert WAV file into MP3, we are goin to use following filter construction:

Fig.2 Filter graph for WAV toMP3 conversion.
For some WAV files with known waveformat, Wave Parser can connect directly to MPEG Layer-3 filter. For example, file that I used (thud.wav), its waveformat is not known, Wave Parser must be connected to intermediate filter capable to convert waveformat into suitable waveformat for MPEG Layer-3. WAV Dest transforms audio data into stream so it can send it to File Writer for saving stream to storage media. WAV Dest is not DirectShow standard filte. This filter is a sample filter included in DirectX SDK. For your convenience, this filter (wavdest.ax) is included in source code download. You must register it with Windows i.e run RegSvr32 wavdest.ax or excute batch file regwavedest.bat.
For MP3 to WAV conversion, we need this filter graph construction:

Fig.3 Filter graph for MP3 toWAV conversion.
To get filter instance, we can use CoCreateInstance()
CoCreateInstance(clsid,
nil,
CLSCTX_INPROC_SERVER,
IID_IBaseFilter,
afilter);
where clsid holds GUID filter identifier and aFilter is variabel of type IBaseFilter.
Some filters cannot be created directly with CoCreateInstance(), for example compressor category filters. From my experience, with CoCreateInstance, those filters will be successfully created, but do not doing any encoding as expected. For compressor filters, we are going to get its instance through enumeration.
To enumerate filters available on our system, we must create system device enumerator with type of ICreateDevEnum using CoCreateInstance(). System device enumerator class identiifier is CLSID_SystemDeviceEnum.
If we succeed, we create class enumerator by using CreateClassEnumerator() with GUID class in parameter classid. For audio compressors, we use kita bisa CLSID_AudioCompressorCategory. Second parameter is variable that will hold moniker from enumeration result. Third parameter is type of filter we are going to enumerate. If we set it to zero, we enumerate all kind of filter type.
function FindFilterByFriendlyName(const classid:TGUID;
const friendlyname: string): IBaseFilter;
var sysEnum:ICreateDevEnum;
enumMoniker:IEnumMoniker;
moniker:IMoniker;
propBag:IPropertyBag;
hr:HRESULT;
afriendlyName:OleVariant;
propName,aname:widestring;
begin
result:=nil;
CoCreateInstance(CLSID_SystemDeviceEnum,
nil,
CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,
sysenum);
if sysEnum<>nil then
begin
aname:=FriendlyName;
propName:='FriendlyName';
hr:=sysEnum.CreateClassEnumerator(classid,
enumMoniker,0);
if hr=S_OK then
begin
while enumMoniker.Next(1,moniker,nil)=S_OK do
begin
hr:=moniker.BindToStorage(nil,nil,IPropertyBag,propBag);
if succeeded(hr) then
begin
propBag.Read(PWideChar(propName),
afriendlyName,nil);
if afriendlyName=aname then
begin
moniker.BindToObject(nil,nil,
IID_IBaseFilter,
result);
exit;
end;
end;
end;
end else
RaiseDirectshowException(hr,'Create Class Enumerator failed');
end;
end;
With Next(), we take moniker for each filter. First parameter of Next(), is number of moniker that we are going to retrieve. We set with 1 to retrieve moniker one by one. Second parameter is variable that will hold moniker instance. Third parameter is number of moniker actually retrieved. We set it to nil bcuse we don't need it. Next() returns value of S_OK if moniker succesfully retrieved.
If we succeed, we take FriendlyName filter and compare it with name passed via parameter. To get FriendlyName, we use BindToStorage(). First and second parameters respectively are context binding dan moniker to the left. Both parameters is not relevant and can be set to nil. Third parameter of BindToStorage is GUID of interface we requested. Fourth parameter is variable that will hold pointer to IPropertyBag instance.
From propBag, we read its content to get its FriendlyName through Read() function. We need to make sure that filter name uses widestring type, because COM uses widestring. Result is compared. If it is same, then this moniker holds filter that we need. We create filter instance by using BindToObject() of moniker. Parameters of BindToObject() is equal to BindToStorage().
Filter is added to filter graph via AddFilter() function belong to IFilterGraph interface.
FFilterGraph.AddFilter(afilter,PWideChar(FilterName));
afilter is instance of IBaseFilter that will be added. Second parameter is name of filter, whic is type of PWidechar.
We need pins to be able to connect filters. To get pin of a filter, we enumerate pins in a filter. EnumPins() function of IBaseFilter interface is for you. Number of parameters of this function is only one, i.e variable that will hold instance of IEnumPins.
function GetPin(aFilter: IBaseFilter;
const PinDir: TPin_Direction; const indx: integer): IPin;
var pPins:IEnumPins;
ctr:integer;
aPin:IPin;
CurPinDir:TPin_Direction;
begin
result:=nil;
//start pin enumeration
aFilter.EnumPins(pPins);
if pPins<>nil then
begin
//pin enumeration ok
ctr:=0;
while pPins.Next(1,aPin,nil)=S_OK do
begin
aPin.QueryDirection(curPinDir);
if (ctr=indx) and (curPinDir=PinDir) then
begin
result:=aPin;
exit;
end;
end;
end;
end;
If EnumPins succeed, pPins holds pointer IEnumPins instance. With Next(), we retrieve pin one by one.If a pin is found, we take its pin direction, and check whether pin is input pin or output pin. Then, we compare with pin direction that we are looking for. And also we compare if its index is same with index we need.
To connect two filters, we need to know their pins, by using GetPin above. Thera are two ways to connect two filters, i.e using Connect of IPin interface or Connect of IGraphBuilder. The first Connect is direct connection, if both pins agree with media type, connection can be created. Othewise, connection is failed. Connect of IGraphBuilder, is little bit different. If both pins do not have any media type matched, IGraphBuilder will try to connect first filter with other filter that is capable to transform media type of first filter into media type matched secod filter. If it cannot find matched intermediate filter, connection is failed. In our pplication demo we are going to use Connect IGraphBuilder.
function ConnectFilter(OutFilter,
InFilter: IBaseFilter): HResult;
var outpin,inPin:IPin;
begin
outPin:=GetPin(OutFilter,PINDIR_OUTPUT,0);
inPin:=GetPin(InFilter,PINDIR_INPUT,0);
result:=FFilterGraph.Connect(outPin,inPin);
end;
We take output pin of first filter and input pin of second filter and then connect both pins with Connect().
Conversion process will be separated into two classes, TWAVToMP3Converter for WAV to MP3 conversion and TMP3ToWAVConverter for MP3 to WAV conversion. we are going to inherit both classes from TBasicConverter classes which is derived from TBasicPlayer class. TBasicPlayer class is base class for multimedia player that we have built in Multimedia Player with DirectShow Part 1 and Multimedia Player with DirectShow Part 2 articles. Following diagram is UML classes diagram for WAV - MP3 conversion.

Fig.4 UML Diagram for WAV - MP3 conversion.
TBasicPlayer will be modified. We will add protected methods useful for filter connection process (ConnectFilter), getting pin of a filter (GetPin), getting filter through enumeration (FindFilterByName) and adding filter to filter graph by its class identifier (AddFilterByCLSID).
For TBasicConverter, we add two properties, i.e SrcFilename and DstFilename. Those properties will hold source filename and destination filename. We also add Convert method which its purpose is for building filter graph and run conversion process.
In TWAVToMP3Converter class, virtual abstract method, BuildFilterGraph, is overriden. In this method we construct filters we beed for WAV to MP3 conversion.
In TMP3ToWAVConverter class, BuildFilterGraph is override to do filter graph contruction for MP3 to WAV conversion.
TBasicConverter=class(TBasicPlayer)
private
FSrcFilename: string;
FDstFilename: string;
procedure SetDstFilename(const Value: string);
procedure SetSrcFilename(const Value: string);
protected
public
procedure Convert;
published
property SrcFilename:string read FSrcFilename write SetSrcFilename;
property DstFilename:string read FDstFilename write SetDstFilename;
end;
TWAVToMP3Converter=class(TBasicConverter)
public
procedure BuildFilterGraph;override;
end;
TMP3ToWAVConverter=class(TBasicConverter)
public
procedure BuildFilterGraph;override;
end;
TBasicPlayer looks like following code:
TBasicPlayer=class(TObject)
private
...
protected
...
function GetPin(aFilter:IBaseFilter;
const PinDir:TPin_Direction;
const indx:integer):IPin;
function ConnectFilter(OutFilter,InFilter:IBaseFilter):HResult;
function AddFilterByCLSID(clsid:TGUID;const name:string):IBaseFilter;
function FindFilterByFriendlyName(const friendlyname:string):IBaseFilter;
...
end;
{ TBasicConverter }
procedure TBasicConverter.Convert;
begin
BuildFilterGraph;
Run;
end;
procedure TBasicConverter.SetDstFilename(const Value: string);
begin
FDstFilename := Value;
end;
procedure TBasicConverter.SetSrcFilename(const Value: string);
begin
FSrcFilename := Value;
end;
Convert() is only wrapper for two method call i.e BuildFilterGraph and Run.
I think all comments in code will explained flow of execution. Maybe what I need to explain is how to set File Source filter to load file to processed. File Source where its address is in aFileReader variable has type of IBaseFilter. To be able to set source filename, we need instance of IFileSourceFilter interface. With its Load() method, we load source file. First parameter is name of file with type of PWideChar and second parameter is playlist. We do not use it here, so we set it to nil.
MPEG Layer-3 filter instance is retrieved with FindFilterByName().
{ TWAVToMP3Converter }
procedure TWAVToMP3Converter.BuildFilterGraph;
var pFileSink:IFileSinkFilter;
pFileSource:IFileSourceFilter;
afileReader,awaveParser,aWaveDest,
aMPEGLayer3,
aFileWriter:IBaseFilter;
aSrcFilename,aDestFilename:widestring;
hr:HResult;
begin
if (FFilterGraph<>nil) then
begin
//add file reader filter
afileReader:=AddFilterByCLSID(CLSID_AsyncReader,'File Reader');
//add Wave parser filter
aWaveParser:=AddFilterByCLSID(CLSID_WaveParser,'Wave Parser');
//Add MPEG Layer 3 Encoder
aMPEGLayer3:=FindFilterByFriendlyName(CLSID_AudioCompressorCategory,
'MPEG Layer-3');
FFilterGraph.AddFilter(aMPEGLayer3,'MPEG Layer-3');
//add Wave Dest filter
awaveDest:=AddFilterByCLSID(CLSID_WaveDest,'Wave Dest');
//add file writer filter
afileWriter:=AddFilterByCLSID(CLSID_FileWriter,'File Writer');
if (afileReader<>nil) and
(awaveParser<>nil) and
(aMPEGLayer3<>nil) and
(aWaveDest<>nil) and
(aFileWriter<>nil) then
begin
//ambil instance IFileSourceFilter
afileReader.QueryInterface(IID_IFileSourceFilter,pFileSource);
if pFileSource<>nil then
begin
aSrcFilename:=FSrcFilename;
pFileSource.Load(PWideChar(aSrcFilename),nil);
end;
//connect output pin file reader ke
//input pin Wave Parser
hr:=ConnectFilter(aFileReader,aWaveParser);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi File Reader ke MPEG-1 Splitter gagal. ');
//connect output Wave Parser ke
//input pin MPEG Layer 3
hr:=ConnectFilter(aWaveParser,aMPEGLayer3);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi Wave Parser ke MPEG Layer 3. ');
//connect output MPEG Layer 3 ke
//input pin Wave Dest
hr:=ConnectFilter(aMPEGLayer3,aWaveDest);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi MPEG Layer 3 ke Wave Dest gagal. ');
//ambil instance IFileSinkFilter
afileWriter.QueryInterface(IID_IFileSinkFilter,pFileSink);
if pFileSink<>nil then
begin
aDestFilename:=FDstFilename;
pFileSink.SetFileName(PWideChar(aDestFilename),nil);
end;
//connect output wave Dest ke
//input pin FileWriter
hr:=ConnectFilter(aWaveDest,aFileWriter);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi Wave Dest ke File Writer gagal. ');
end;
end;
end;
We do the similar way for File Writer, except that we get instance of IFileSinkFilter instead of IFileSourceFilter. We set target filename using SetFilename(). First parameter is filename and second parameter is playlist.
Similar way we do for MP3 to WAV conversion. What's different is only filters contruction.
{ TMP3ToWAVConverter }
procedure TMP3ToWAVConverter.BuildFilterGraph;
var pFileSink:IFileSinkFilter;
pFileSource:IFileSourceFilter;
afileReader,aMPEG1Splitter,aWaveDest,
aMPEGLayer3,
aFileWriter:IBaseFilter;
aSrcFilename,aDestFilename:widestring;
hr:HResult;
begin
if (FFilterGraph<>nil) then
begin
//add file reader filter
afileReader:=AddFilterByCLSID(CLSID_AsyncReader,'File Reader');
//add MPEG-1 Stream Splitter filter
aMPEG1Splitter:=AddFilterByCLSID(CLSID_MPEG1Splitter,'MPEG-1 Stream Splitter');
//Add MPEG Layer 3 Decoder
aMPEGLayer3:=AddFilterByCLSID(CLSID_MPEGLayer3Decoder,'MPEG Layer-3 Decoder');
//add Wave Dest filter
awaveDest:=AddFilterByCLSID(CLSID_WaveDest,'Wave Dest');
//add file writer filter
afileWriter:=AddFilterByCLSID(CLSID_FileWriter,'File Writer');
if (afileReader<>nil) and
(aMPEG1Splitter<>nil) and
(aMPEGLayer3<>nil) and
(aWaveDest<>nil) and
(aFileWriter<>nil) then
begin
//ambil instance IFileSourceFilter
afileReader.QueryInterface(IID_IFileSourceFilter,pFileSource);
if pFileSource<>nil then
begin
aSrcFilename:=FSrcFilename;
pFileSource.Load(PWideChar(aSrcFilename),nil);
end;
//connect output pin file reader ke
//input pin MPEG-1 Splitter
hr:=ConnectFilter(aFileReader,aMPEG1Splitter);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi File Reader ke MPEG-1 Splitter gagal. ');
//connect output MPEG-1 Splitter ke
//input pin MPEG Layer 3 Decoder
hr:=ConnectFilter(aMPEG1Splitter,aMPEGLayer3);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi MPEG-1 Splitter ke MPEG Layer 3 gagal. ');
//connect output MPEG Layer 3 Decoder ke
//input pin Wave Dest
hr:=ConnectFilter(aMPEGLayer3,aWaveDest);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi MPEG Layer 3 ke Wave Dest gagal. ');
//ambil instance IFileSinkFilter
afileWriter.QueryInterface(IID_IFileSinkFilter,pFileSink);
if pFileSink<>nil then
begin
aDestFilename:=FDstFilename;
pFileSink.SetFileName(PWideChar(aDestFilename),nil);
end;
//connect output wave Dest ke
//input pin FileWriter
hr:=ConnectFilter(aWaveDest,aFileWriter);
if hr<>S_OK then
RaiseDirectShowException(hr,'Koneksi Wave Dest ke File Writer gagal. ');
end;
end;
end;
Ok, now we discuss main application's implementation. Create new application and drag drop controls on form to make it looks like figure below:

Fig.5 Main form design.
Following code is complete code of main application.
unit ufrmMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls,
directshow,uDirectShowPlayer, ComCtrls;
type
TfrmMain = class(TForm)
Panel1: TPanel;
GroupBox1: TGroupBox;
rdgrpConvertType: TRadioGroup;
edSrcFilename: TEdit;
edDestFilename: TEdit;
btnSrcBrowse: TButton;
btnDestBrowse: TButton;
Label1: TLabel;
Label2: TLabel;
btnConvert: TButton;
btnClose: TButton;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
procedure btnSrcBrowseClick(Sender: TObject);
procedure btnDestBrowseClick(Sender: TObject);
procedure btnConvertClick(Sender: TObject);
procedure btnCloseClick(Sender: TObject);
private
mp3ToWavConverter:TMP3ToWAVConverter;
WavToMP3Converter:TWAVToMP3Converter;
{ Private declarations }
procedure WavToMP3(const srcfile,dstfile:string);
procedure MP3ToWav(const srcfile,dstfile:string);
procedure WM_MMNotify(var msg: TMessage);message WM_MMNotify;
public
constructor Create(AOwner:TComponent);override;
destructor destroy;override;
{ Public declarations }
end;
var
frmMain: TfrmMain;
implementation
{$R *.dfm}
procedure TfrmMain.btnSrcBrowseClick(Sender: TObject);
begin
if OpenDialog1.Execute then
begin
edSrcFilename.Text:=OpenDialog1.FileName;
end;
end;
procedure TfrmMain.btnDestBrowseClick(Sender: TObject);
begin
if SaveDialog1.Execute then
begin
edDestFilename.Text:=SaveDialog1.FileName;
end;
end;
procedure TfrmMain.btnConvertClick(Sender: TObject);
begin
case rdgrpConvertType.ItemIndex of
0:WavToMP3(edSrcFilename.Text,edDestFilename.Text);
1:MP3ToWav(edSrcFilename.Text,edDestFilename.Text);
end;
end;
procedure TfrmMain.btnCloseClick(Sender: TObject);
begin
Close;
end;
procedure TfrmMain.MP3ToWav(const srcfile, dstfile: string);
begin
mp3toWavconverter.SrcFilename:=srcfile;
mp3toWavconverter.DstFilename:=dstfile;
mp3toWavconverter.Convert;
end;
procedure TfrmMain.WavToMP3(const srcfile, dstfile: string);
begin
WavToMp3converter.SrcFilename:=srcfile;
WavToMp3converter.DstFilename:=dstfile;
WavToMp3converter.Convert;
end;
constructor TfrmMain.Create(AOwner: TComponent);
begin
inherited;
mp3ToWavconverter:=TMP3ToWavConverter.Create;
mp3ToWavConverter.Handle:=Handle;
WavToMp3converter:=TWavToMP3Converter.Create;
WavToMp3Converter.Handle:=Handle;
end;
destructor TfrmMain.destroy;
begin
mp3ToWAVConverter.Free;
WAVToMp3Converter.Free;
inherited;
end;
procedure TfrmMain.WM_MMNotify(var msg: TMessage);
var aplayer:TBasicPlayer;
evCode,param1,param2:integer;
begin
aplayer:=TBasicPlayer(msg.LParam);
aplayer.EventObj.GetEvent(evCode,param1,param2,0);
case evCode of
EC_COMPLETE:begin
aplayer.RemoveAllFilters;
end;
end;
aplayer.EventObj.FreeEventParams(evCode,param1,param2);
end;
end.
I will only explain about WM_MMNotify method. This method will be called when DirectShow needs to notify application. We must check for conversion process completion to get rid of all filters we constructed, because every time Convert is called, BuildFilterGraph will be called and adds filters to filter graph. If we don't remove filters, filters will be duplicated in filter graph.
Download application source code here.
We have discussed how to do WAV to MP3 conversion and vice versa. We also discussed how to construct flter graph manually by connecting filters one by one.
Do you like this article? Help this website improve by donating. Any amounts is appreciated.
Or you can help by bookmarking this page.
Bookmark this on Delicious