Audio Capture with DirectShow - Part 1






4.89/5 (11 votes)
A console program that captures audio but does not save it.
Introduction
This is the first part of a two part tutorial on capturing audio via DirectShow. The first part simply shows how to connect an input device with an output device. The first part does not save the data coming through the input device; the saving portion is being left out for the second part of the tutorial.
What You Should Know First
I have written two small tutorials on how to read audio/video devices installed on a system. For those who do not know how to get hold of such devices, please take a look at these articles:
This would make it easier for you to understand this tutorial, which uses a function that is explained in the previous two tutorials, and which I will not be explaining here. Plus, a bit of understanding of COM programming is always helpful.
How to Use the Program
This is not a program that you can just download and compile and run! Rather, this tutorial will take you through the steps of how to compile the program, and at the end, you will understand how and why GraphEdit and the properties of the filters shown in GraphEdit are being used in the program. The most important step would be changing the values of some variables. This step is very, very important, and must be followed in order to run the program correctly. And, in the end, you will know how to connect input pins to output pins.
First Open GraphEdit!
Step 1: Add an Audio Capture Source
I wrote this in a rather highly un-professional way of building the program. The very simple reason being that I wanted readers to have a real "look-and-feel" of what GrapEdit.exe shows when filters are accessed, and what properties are being used during a call to the various devices. What this means is that when you place a filter, such as an Audio Capture Source, what GraphEdit shows is a filter along with its properties. Therefore, the first step is to open GraphEdit and place a filter from the Audio Capture Source category. Shown below is a snapshot of the Audio Capture Sources on my system. Look at the red square that is around one of the filters. This is the mic that resides on the front panel of my system, and is shown as "Front Mic (IDT High Definition" in GraphEdit.
Step 2: Change of Variable Values
As you can see, my system has a filter (or a device) with the "FriendlyName" of "Front Mic (IDT High Definition". Read carefully as I have deliberately written the two terms in double quoted marks. The FriendlyName of the second input device in the list is "Front Mic (IDT High Definition" which happens to be on my system. For your system, you would probably, if not definitely, see something else. I am using this device as the audio input device in my code. A line which resides in the code is as follows:
bstrDeviceName =(L"Front Mic (IDT High Definition ");
// device name as seen in Graphedit.exe
The above line of code will allow the program to use the front mic of my system for audio input. What you need to do is replace this string "Front Mic (IDT High Definition " with the FriendlyName that is being "shown" by GraphEdit on your system. Yes, include the space at the end if it is there, like I have done. After placing an audio capture source that GraphEdit has on offer, the next step would be to look at what is the capturing pin in that source. Shown below is what is available on my system:
You can see the red square and the exact term "Capture" that is also being used in the code:
hr = pInputDevice->FindPin(L"Capture",&pIn);
//Get hold of the pin "Capture", as seen in GraphEdit
It's a jump in the code from main()
, but hold on, here the "Capture
" property shown by GraphEdit is actually a 'pin', and needs to be enumerated and initialized in order to use it. Whatever be shown by GraphEdit on your system, you should change the value of the variable:
FindPin(L"Capture",&pIn);
Step 3: Add Audio Renderers
Now, you need to add an audio rendering source which, in my case, is going to be the one shown by the red square.
Step 4: Change of Variable Values
Just like in step 1, here, you would need to change the value available in GraphEdit and modify the value in the following line of code; this is the head-phone (HP) on my system:
bstrDeviceName = (L"Speakers/HP (IDT High Definition");
// device name as seen in Graphedit.exe
Now, just like step 3, you would change the value in the following line of code:
hr = pOutputDevice->FindPin(L"Audio Input pin (rendered)",&pOut);
A peek into GraphEdit shows the following:
After making a change to the code in your program, it should be ready to compile, but first, try connecting the two sources, Audio capture and Audio renderer, in GraphEdit, and hit the Play button to test the graph. You should hear whatever is being said on the mic in the speaker.
Finally, the Rest of the Code!
Well, up to this point, you may have wondered how it is quite a puzzle to fix the variable values. But, this is because I want the coders to have a real taste of GraphEdit, the filters and their properties, and how to use these filter properties inside the code.
I am not going to discuss the code which enumerates the audio sources. The code which I am going to explain are of these functions:
//Function to initialize Input/Output devices
IBaseFilter* Device_Init(IMoniker*,IBaseFilter*);
//Function to add device to graph
void Device_Addition(IGraphBuilder*,IBaseFilter*,BSTR);
//Function to connect the two devices, in this case input and output
void Device_Connect(IBaseFilter*,IBaseFilter*);
//Function to run the graph
void Run_Graph(IMediaControl*);
The function Device_Init
takes a single pointer variable which is a reference to IBaseFilter
. The IBaseFilter
interface allows exclusive access to a specific device represented by the pDeviceMonik
pointer. In this case, the IBaseFilter
references are pInputDevice
and pOutputDevice
. These two variables retain the address of the input and output devices that were accessed by the Device_Read(...)
function.
IBaseFilter* Device_Init(IBaseFilter* pDevice)
{
//Instantiate the device
hr = pDeviceMonik->BindToObject(NULL, NULL,
IID_IBaseFilter,(void**)&pDevice);
if (SUCCEEDED(hr))
{
cout<<"Device initiation successful..."<<endl;
}
else HR_Failed();
hr = pGraph->AddFilter(pDevice,bstrDeviceName);// add to Graph
if (SUCCEEDED(hr))
{
cout<<"Device addition to graph successful..."<<endl;
}
else HR_Failed();
return pDevice;
}
The function Device_Addition(...);
takes pointers to the IBuilderGraph
, IBaseFilter
, and BSTR
types to actually add the audio devices to the graph. Here is a call to add the input device to the graph:
Device_Addition(pGraph,pInputDevice,bstrDeviceName);
//add device to graph
void Device_Addition(IGraphBuilder* pGraph,IBaseFilter* pDevice,BSTR bstrName)
{
HRESULT hr;
hr = pGraph->AddFilter(pDevice,bstrName);
if(SUCCEEDED(hr))
{
wcout<<"Addition of "<<bstrName<<" successful..."<<endl;
}
else HR_Failed(hr);
}
After making calls for output devices similar to those for input devices, we can now connect the two. In GraphEdit, we connect the two filters (devices) through their pins. Here, we do it manually in the Device_Connect(...)
function.
//Connect input to output
Device_Connect(pInputDevice,pOutputDevice);
The function Device_Connect(...)
takes the two input devices and connects them. Here, we first need to enumerate the input and output device pins:
IEnumPins *pInputPin = NULL,*pOutputPin = NULL;
// Pin enumeration
IPin *pIn = NULL, *pOut = NULL;// Pins
These pins need to be accessed, and this is done by an enumeration:
hr = pInputDevice->EnumPins(&pInputPin);// Enumerate the pin
The above is also repeated for pOutputDevice
. The next step is accessing the enumerated pins, which is done by a call to FindPin(...)
. Remember when I said "It's a jump in the code from main()
"? This is the point where the jump has to be made. The pins that are exposed by the device drivers are shown in GraphEdit, and that is why we had to change the values so that we correctly access the pins of the specific device. This method takes a string argument, and searches whether there is a pin by the name of either "Capture" or "Audio Input pin (rendered)" for the code working on my system. After the changes you make, the string values would certainly be different for your system.
//Get hold of the pin "Capture", as seen in GraphEdit
hr = pInputDevice->FindPin(L"Capture",&pIn);
Now, the two pins are ready to be connected, and are connected by the following code:
//Connect the input pin to output pin
hr = pIn->Connect(pOut,NULL);
The last and final step is to run the graph. This is done by the reference pointer variable pControl
of type IMediaControl
. This interface basically allows the running, stopping, and various other control functionalities of the graph.
void Run_Graph(IMediaControl* pControl)
{
HRESULT hr;
hr = pControl->Run();// Now run the graph, i.e. start listening!
if(SUCCEEDED(hr))
{
cout<<"You must be listening to something!!!"<<endl;
}
else HR_Failed(hr);
}
The last step completes the code, and hopefully there will be a successful run!
The code is not professional by any means, and should be optimized whenever this is a need for a professional approach. The task was to explain the code in the simplest possible way, and to cater for very limited errors. The error handling part is very basic, and should not be used for professional applications as it just displays an error text. I expect bugs to appear, and will highly appreciate changes to the code itself!
A Note on bool Bstr_Compare(BSTR,BSTR)
I had to update the previous code. There was a programming error which was pointed out by Igorka, which I highly appreciate, and I have fixed the error.
if(Bstr_Compare(varName.bstrVal,bstrDeviceName) == true)
//make a comparison
This calls the function Bstr_Compare(...)
and checks for the returned boolean value. If 'true
' was returned, it means the required device was found.
Rest of the code for Bstr_Compare
:
bool Bstr_Compare(BSTR bstrFilter,BSTR bstrDevice)
{
bool flag = true;
int strlenFilter = SysStringLen(bstrFilter);//set string length
int strlenDevice = SysStringLen(bstrDevice);//set string length
char* chrFilter = (char*)malloc(strlenFilter+1);// allocate memory
char* chrDevice = (char*)malloc(strlenDevice+1);// allocate memory
int j = 0;
if (strlenFilter!=strlenDevice)
//if the strings are of not the same length,
//means they totall different strings
flag = false;
//sety flag to false to indicate "not-same" strings
else
{
for(; j < strlenFilter;j++)
//now, copy 1 by 1 each char
//to chrFilter and chrDevice respectively
{
chrFilter[j] = (char)bstrFilter[j];//copy
chrDevice[j] = (char)bstrDevice[j];//copy
cout<<j;
}
chrFilter[strlenFilter] = '\0';//add terminating character
chrDevice[strlenDevice] = '\0';//add terminating character
for(j=0; j < strlenFilter;j++)//check loop
{
if(chrFilter[j] != chrDevice[j])
//check if there are chars that are not samne
flag = false;
//if chars are not same, set flag
//to false to indicate "not-same" strings
}
if(flag == true && j == strlenFilter-1)
//see if we went through the 'check loop'
flag = true;//means strings are same
}
return flag;
}
The Code is Built with the Following
- Windows Server 2008
- Microsoft Visual Studio 2008
- DirectX 10.1
- Microsoft Windows SDK 6.1
References
History
- 7th December, 2008: Initial post.
- 13th December, 2008: Article updated.
- Added new function
Bstr_Compare(...)
that correctly compares twoBSTR
type strings. - Added the functions
SysAllocString(...)
andSysFreeString(...)
to correctly allocate string values toBSTR
type variables and free them, respectively. - 8th February, 2008: Aritcle updated.
- Fixed
Bstr_Compare(...)
that correctly compares twoBSTR
type strings.