Capturing Audio with DSAudioCap

Capturing Audio with DSAudioCap

DSAudioCap is a console-based application that will record audio to a file (MyAVIFile.AVI) for as long as it runs. As a console application, it sends diagnostic information to the text-mode window it opens on the display, which is useful for debugging. This window also prompts the user to press the Enter key to stop recording, at which point the filter graph is stopped and the application terminates.

Examining the main Function

Let s begin our analysis of DSAudioCap by taking a look at the main function. As in the earlier examples of DSBuild and DSRender, main opens with the instantiating of the Filter Graph Manager object. This object is followed by the acquisition of the IMediaControl interface to the Filter Graph Manager, which controls execution of the filter graph.

// A very simple program to capture audio to a file using DirectShow // int main(int argc, char* argv[]) { IGraphBuilder *pGraph = NULL; // Filter graph builder object IMediaControl *pControl = NULL; // Media control object IFileSinkFilter *pSink = NULL; // Interface on file writer IBaseFilter *pAudioInputFilter = NULL; // Audio capture filter IBaseFilter *pFileWriter = NULL; // File writer filter // Initialize the COM library. HRESULT hr = CoInitialize(NULL); if (FAILED(hr)) { // We'll send our error messages to the console. printf("ERROR - Could not initialize COM library"); return hr; } // Create the Filter Graph Manager and query for interfaces. hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph); if (FAILED(hr)) // FAILED is a macro that tests the return value { printf("ERROR - Could not create the Filter Graph Manager."); return hr; } // Using QueryInterface on the graph builder object, // get the IMediaControl object. hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl); if (FAILED(hr)) { printf("ERROR - Could not create the Media Control object."); pGraph->Release(); // Clean up after ourselves CoUninitialize(); // And uninitalize COM return hr; } // OK, so now we want to build the filter graph // using an AudioCapture filter. // But there are several to choose from, // so we need to enumerate them and then pick one. hr = EnumerateAudioInputFilters((void**) &pAudioInputFilter); hr = EnumerateAudioInputPins(pAudioInputFilter); // Add the audio capture filter to the filter graph. hr = pGraph->AddFilter(pAudioInputFilter, L"Capture"); // Next add the AVIMux. (You'll see why.) IBaseFilter *pAVIMux = NULL; hr = AddFilterByCLSID(pGraph, CLSID_AviDest, L"AVI Mux", &pAVIMux); // Connect the filters. hr = ConnectFilters(pGraph, pAudioInputFilter, pAVIMux); // And now we instance a file writer filter. hr = AddFilterByCLSID(pGraph, CLSID_FileWriter, L"File Writer", &pFileWriter); // Set the file name. hr = pFileWriter->QueryInterface(IID_IFileSinkFilter, (void**)&pSink); pSink->SetFileName(L"C:\\MyWAVFile.AVI", NULL); // Connect the filters. hr = ConnectFilters(pGraph, pAVIMux, pFileWriter); if (SUCCEEDED(hr)) { // Run the graph. hr = pControl->Run(); if (SUCCEEDED(hr)) { // Wait patiently for completion of the recording. wprintf(L"Started recording...press Enter to stop recording.\n"); // Wait for completion. char ch; ch = getchar(); // We wait for keyboard input } // And stop the filter graph. hr = pControl->Stop(); wprintf(L"Stopped recording.\n"); // To the console // Before we finish, save the filter graph to a file. SaveGraphFile(pGraph, L"C:\\MyGraph.GRF"); } // Now release everything and clean up. pSink->Release(); pAVIMux->Release(); pFileWriter->Release(); pAudioInputFilter->Release(); pControl->Release(); pGraph->Release(); CoUninitialize(); return 0; }

Enumerating System Devices in DirectShow

At this point in the function, a call is made to EnumerateAudioInputFilters, which is local to the application. This function is required because before a capture source filter can be added to the filter graph, it must be identified and selected. Unlike other DirectShow filters (which can be identified by a GUID), capture source filters and other filters that are tied to hardware devices are identified as a class of device. Using DirectShow calls, you can walk through the list of all filters in this class and select the one you want to use for your capture application.

Being able to enumerate capture filters gives you ultimate flexibility in the design of your DirectShow application because all capture resources are available to the application, not just those that have been designated as default devices by the user or the system. This functionality also means that DirectShow remains open to new hardware devices as they re introduced by manufacturers you won t need a revision of DirectShow to deal with every new card or interface that comes down the path. Some WDM drivers installed in conjunction with the installation of multimedia devices are recognized by DirectShow and added to its enumerated lists of available filters. Figure 4-1 shows the Insert Filters dialog box listing audio capture devices.

On the other hand, now is a good time to ask yourself an important question: are you relying on specific hardware components in your DirectShow application? You could specify which capture source filter will be used in your application, but if that source filter isn t on the user s system (because it relies on hardware the user hasn t installed), your application will fail. It s best to design a DirectShow application for the widest possible range of devices to ensure that a user won t encounter any unexpected failures when he or she runs your code.

figure 4-1 graphedit enumerating audio capture devices in the insert filters dialog box

Figure 4-1. GraphEdit enumerating audio capture devices in the Insert Filters dialog box

Here s the source code for EnumerateAudioInputFilters:

// Enumerate all of the audio input devices // Return the _first_ of these to the caller // That should be the one chosen in the control panel. HRESULT EnumerateAudioInputFilters(void** gottaFilter) { // Once again, code stolen from the DX9 SDK. // Create the System Device Enumerator. ICreateDevEnum *pSysDevEnum = NULL; HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum); if (FAILED(hr)) { return hr; } // Obtain a class enumerator for the audio input category. IEnumMoniker *pEnumCat = NULL; hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioInputDeviceCategory, &pEnumCat, 0); if (hr == S_OK) { // Enumerate the monikers. IMoniker *pMoniker = NULL; ULONG cFetched; if (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK) { // Bind the first moniker to an object. IPropertyBag *pPropBag; hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pPropBag); if (SUCCEEDED(hr)) { // To retrieve the filter's friendly name, // do the following: VARIANT varName; VariantInit(&varName); hr = pPropBag->Read(L"FriendlyName", &varName, 0); if (SUCCEEDED(hr)) { wprintf(L"Selecting Audio Input Device: %s\n", varName.bstrVal); } VariantClear(&varName); // To create an instance of the filter, // do the following: // Remember to release gottaFilter later. hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter, gottaFilter); pPropBag->Release(); } pMoniker->Release(); } pEnumCat->Release(); } pSysDevEnum->Release(); return hr; }

This function begins by creating an instance of a COM object known as the System Device Enumerator. This object will enumerate all the hardware devices of a specified type once its CreateClassEnumerator method is invoked with a GUID indicating the class of devices to be enumerated. In this case, the GUID CLSID_AudioInputDeviceCategory is requesting an enumeration of all the audio capture source filters on the system, but it could also be CLSID_VideoInputDeviceCategory for video capture source filters, or even CLSID_AudioCompressorCategory for all audio compression filters. (Although audio compression isn t generally performed in hardware, each compressor is considered a system device and is treated as if it were hardware rather than software.) The resulting list of devices is placed into an object of the IEnumMoniker class. An enumerated class (which generally includes the letters Enum in its class name) can be examined, element by element, by invoking its Next method. In this case, invoking Next will return an IMoniker object, which is a lightweight COM object used to obtain information about other objects without having to instantiate them. The IMoniker object returns references to a bag of data related to the enumerated DirectShow filter, which is then placed into an IPropertyBag object with an invocation of the IMoniker method BindToStorage.

Once the IPropertyBag object has been instantiated, it can be queried using its Read method for string data labeled as its FriendlyName, which is to say the English-readable name (rather than the GUID) for the filter. That string is then printed to the console. The string identifies the first filter in the enumerated list, which is also the default audio capture device, established by the user through his or her preferences in the Sound control panel. (It s a bad idea to count on any particular device showing up first on the list of audio capture devices because, at any point, the user might go into the Sound control panel and muck things up.)

Once the name of the device has been printed to the console, another method call to the IMoniker object BindToObject creates an instance of the filter object, and this object is returned to the calling function. It will later need to be destroyed with a call to its Release method.

Although this function returns an object representing the first filter in the enumerated list the default selection as specified in the control panel with a few modifications, the function could return a list of objects and names. These objects and names could then be used to build menus or other GUI features that would allow the user to have complete control over which of possibly several audio capture sources would be used within a DirectShow application.

Enumerating Input Pins on an Audio Capture Device

Immediately upon return to the main function, another call is made to a different local function, EnumerateAudioInputPins. This function examines all the input pins on the selected audio input filter object; we ll need to do this to determine which input pin is active. If you examine of the Volume control panel (generally accessible through the system tray), you can see that several different sources of audio input usually exist on a PC. Most PCs have some sort of CD audio, line audio, microphone, and auxiliary inputs. Other sources will capture the sound data passing through the system, as though the PC had an internal microphone, capturing its own sound-making capabilities in real-time. Figure 4-2 shows various audio input pins on an audio capture filter.

figure 4-2 graphedit showing the various audio input pins on an audio capture filter

Figure 4-2. GraphEdit showing the various audio input pins on an audio capture filter

The local function EnumerateAudioInputPins allows us to examine the audio input filter s pins, as shown here:

// Code adopted from example in the DX9 SDK. // This code allows us to find the input pins an audio input filter. // We'll print out a list of them, indicating the enabled one. HRESULT EnumerateAudioInputPins(IBaseFilter *pFilter) { IEnumPins *pEnum = NULL; IPin *pPin = NULL; PIN_DIRECTION PinDirThis; PIN_INFO pInfo; IAMAudioInputMixer *pAMAIM = NULL; BOOL pfEnable = FALSE; // Begin by enumerating all the pins on a filter. HRESULT hr = pFilter->EnumPins(&pEnum); if (FAILED(hr)) { return NULL; } // Now, look for a pin that matches the direction characteristic. // When we've found it, we'll examine it. while(pEnum->Next(1, &pPin, 0) == S_OK) { // Get the pin direction. pPin->QueryDirection(&PinDirThis); if (PinDirThis == PINDIR_INPUT) { // OK, we've found an input pin on the filter. // Now let's get the information on that pin // so we can print the name of the pin to the console. hr = pPin->QueryPinInfo(&pInfo); if (SUCCEEDED(hr)) { wprintf(L"Input pin: %s\n", pInfo.achName); // Now let's get the correct interface. hr = pPin->QueryInterface(IID_IAMAudioInputMixer, (void**) &pAMAIM); if (SUCCEEDED(hr)) { // Find out whether the pin is enabled. // Is it the active input pin on the filter? hr = pAMAIM->get_Enable(&pfEnable); if (SUCCEEDED(hr)) { if (pfEnable) { wprintf(L"\tENABLED\n"); } } pAMAIM->Release(); } pInfo.bFilter->Release(); // from QueryPinInfo } } pPin->Release(); } pEnum->Release(); return hr; } 

The EnumerateAudioInputPins function was adapted from the GetPin function used in the previous chapter. A pointer to a DirectShow IBaseFilter object is passed by the caller, and its pins are enumerated within an IEnumPins object by a call to the IBaseFilter method EnumPins. Using the Next method on the IEnumPins object, an IPin object is instantiated for every pin within the enumerated list. The IPin object is then tested with a call to the QueryDirection method: is the pin an input pin? If it is, the QueryPinInfo method of IPin is invoked. This method returns a PIN_INFO data structure, containing (in English) the name of the pin, which is then printed on the console.

Now comes a bit of DirectShow magic. Each pin on an audio input filter is an object in its own right and presents an IAMAudioInputMixer interface as one of its properties. This interface allows you to control various parameters of the pin, such as its volume level. The IAMAudioInputMixer interface for each IPin object is retrieved with a QueryInterface call. (This call will fail if you re not acting on an IPin that is part of an audio input filter.) Once this object is instanced, one of its properties is examined with a call to get_Enable. If TRUE is returned, the pin is enabled that pin is the active input pin for the filter, and the corresponding audio input is enabled. (The default active input pin is set through the Volume control panel; it s the enabled item in the list of recording inputs.) Figure 4-3 shows pin properties of the audio input pins.

figure 4-3 graphedit showing pin properties for the audio input pins

Figure 4-3. GraphEdit showing pin properties for the audio input pins

Although this routine only reads whether a pin is enabled, it is possible, through a corresponding call to the put_Enable method, to change the value on the pin, thereby selecting or removing an audio input. (You should be very careful that you have only one input enabled at a time, unless you know that your audio hardware can handle mixing multiple inputs.) This function indicates only the currently enabled input, but it s very easy to modify it so that it selects an alternative input. And, once again, this function could easily be rewritten to return an array of IPin objects to the caller. This list could then be used to build a GUI so that users could easily pick the audio pin themselves.

Connecting DirectShow Filters

DSAudioCap adds a convenience function, ConnectFilters, which can be used by the programmer to handle all the nitty-gritty of connecting two filters together. It s actually three separate functions, as shown here:

// Find an unconnected pin on a filter. // This too is stolen from the DX9 SDK. HRESULT GetUnconnectedPin( IBaseFilter *pFilter, // Pointer to the filter. PIN_DIRECTION PinDir, // Direction of the pin to find. IPin **ppPin) // Receives a pointer to the pin. { *ppPin = 0; IEnumPins *pEnum = 0; IPin *pPin = 0; HRESULT hr = pFilter->EnumPins(&pEnum); if (FAILED(hr)) { return hr; } while (pEnum->Next(1, &pPin, NULL) == S_OK) { PIN_DIRECTION ThisPinDir; pPin->QueryDirection(&ThisPinDir); if (ThisPinDir == PinDir) { IPin *pTmp = 0; hr = pPin->ConnectedTo(&pTmp); if (SUCCEEDED(hr)) // Already connected--not the pin we want { pTmp->Release(); } else // Unconnected--this is the pin we want { pEnum->Release(); *ppPin = pPin; return S_OK; } } pPin->Release(); } pEnum->Release(); // Did not find a matching pin. return E_FAIL; } // Connect two filters together with the Filter Graph Manager, // Stolen from the DX9 SDK. // This is the base version. HRESULT ConnectFilters( IGraphBuilder *pGraph, // Filter Graph Manager. IPin *pOut, // Output pin on the upstream filter. IBaseFilter *pDest) // Downstream filter. { if ((pGraph == NULL) (pOut == NULL) (pDest == NULL)) { return E_POINTER; } // Find an input pin on the downstream filter. IPin *pIn = 0; HRESULT hr = GetUnconnectedPin(pDest, PINDIR_INPUT, &pIn); if (FAILED(hr)) { return hr; } // Try to connect them. hr = pGraph->Connect(pOut, pIn); pIn->Release(); return hr; } // Connect two filters together with the Filter Graph Manager. // Again, stolen from the DX9 SDK. // This is an overloaded version. HRESULT ConnectFilters( IGraphBuilder *pGraph, IBaseFilter *pSrc, IBaseFilter *pDest) { if ((pGraph == NULL) (pSrc == NULL) (pDest == NULL)) { return E_POINTER; } // Find an output pin on the first filter. IPin *pOut = 0; HRESULT hr = GetUnconnectedPin(pSrc, PINDIR_OUTPUT, &pOut); if (FAILED(hr)) { return hr; } hr = ConnectFilters(pGraph, pOut, pDest); pOut->Release(); return hr; }

Two versions of ConnectFilters are included as local functions in DSAudio Cap, which is possible because the two have different calling parameters and C++ can handle them differently through its capability with overloaded functions. The bottom version of ConnectFilters is the one invoked by main, and it calls GetUnconnectedPin, which is a function very much like GetPin, except that it returns the first unconnected output pin on the source filter. The results are passed along to the other version of ConnectFilters, which calls GetUnconnectedPin again, this time searching for an input pin on the destination filter. Once everything s been discovered, a call is made to the Filter Graph Manager method Connect, which connects the two pins.

Adding a Filter by Its Class ID

One more convenience function exists in DSAudioCap, AddFilterByCLSID, which adds a filter to the graph by its unique class ID. It will be used frequently in future examples.

// A very useful bit of code // stolen from the DX9 SDK. HRESULT AddFilterByCLSID( IGraphBuilder *pGraph, // Pointer to the Filter Graph Manager const GUID& clsid, // CLSID of the filter to create LPCWSTR wszName, // A name for the filter IBaseFilter **ppF) // Receives a pointer to the filter { if (!pGraph ! ppF) return E_POINTER; *ppF = 0; IBaseFilter *pF = 0; HRESULT hr = CoCreateInstance(clsid, 0, CLSCTX_INPROC_SERVER, IID_IBaseFilter, reinterpret_cast<void**>(&pF)); if (SUCCEEDED(hr)) { hr = pGraph->AddFilter(pF, wszName); if (SUCCEEDED(hr)) *ppF = pF; else pF->Release(); } return hr; }

Although AddFilterByCLSID doesn t do anything spectacular, it does save some time in coding because it encapsulates the object creation and filter addition actions into a single function call. You ll probably want to use it in your own DirectShow applications.

Using the Audio Capture Filter Graph

The filter graph for DSAudioCap is composed of three filters. First is an audio input filter, as explored earlier. Next comes an AVI multiplexer (or AVI mux). This filter takes a number of media streams and multiplexes them into a single stream formatted as an AVI file. These streams can be video, audio, or a combination of the two. An AVI mux is one of the ways to create a movie with synchronized video and audio portions; as streams arrive at the multiplexer, they re combined into a single, synchronized stream. The final component in the filter graph is a File Writer filter, which writes the AVI stream to disk.

Adding a File Writer Filter

Nearly all capture applications write their captured streams to a disk file. Several objects can serve as file sinks, meaning that they write stream data. The most common of these is the DirectShow File Writer filter object. In DSAudioCap, the File Writer filter is instantiated and added to the filter graph, and then its IFileSink interface is instantiated through a call to QueryInterface. This object exposes methods that allow you to programmatically set the name and path of the file to be written to disk, through a call to its SetFileName method. Figure 4-4 shows the filter graph created by DSAudioCap.

figure 4-4 filter graph mygraph.grf, as created by dsaudiocap

Figure 4-4. Filter graph MyGraph.GRF, as created by DSAudioCap

Executing the DSAudioCap Filter Graph

Once the file name has been set and all the filters have been connected together, the Run method is sent to the filter graph s control object. From this point, the filter graph captures audio and writes it to an ever-growing AVI file until the Enter key is pressed. Watch out: this file can get very big very quickly. (We re using an old but venerable call to getchar to watch for keyboard input.) Once the Enter key has been pressed, the filter graph control object makes a call to its Stop method, which terminates execution of the filter graph. Figure 4-5 shows DSAudioCap running.

figure 4-5 dsaudiocap executing from the command line

Figure 4-5. DSAudioCap executing from the command line

We ve kept the call to SaveGraphFile in this program (and will continue to do so), which means that after the program terminates its execution, you can use GraphEdit to examine the filter graph created by DSAudioCap.

After you execute this application, there should be a file named MyAVIFile.AVI on disk. Double-clicking the file (or opening it from Windows Media Player) will allow you to hear the sound that was captured by DSAudioCap.



Programming Microsoft DirectShow for Digital Video and Television
Programming Microsoft DirectShow for Digital Video and Television (Pro-Developer)
ISBN: 0735618216
EAN: 2147483647
Year: 2002
Pages: 108
Authors: Mark D. Pesce

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net