IkMijn pagina voor
prettig werken op de PC
www.pcpret.nl
Homepage Stuur bericht Gastenboek Website Blog Eindhoven

Recording FM radio/TV sound in Wma format using the DirectX.Capture class library

This article shows a coding example to save audio as Windows Media audio file. A number of enhancements to the DirectX.Capture class are described to save the captured audio to a file. The DirectX.Capture class is originally written by Brian Low. He did a tremendous job to make DirectX available to C#. As result everyone can start with a working example to learn and use DirextX, DirectShow and C#. And also a lot of people wrote modifications for this class that could be used by others. An interesting webiste is DirectShow for C#, made by David David Wohlferd(?). At this site he provides (temporarily) some interesting examples using DirectX and Windows Media functionality. You can also find here an interesting modified version of the DirextX.Capture class example, named CaptureTest. He added much future functionality that was mentioned by Brian Low before. Very interesting is the way the new CaptureTest program can store program settings in a configuration file.

I wish you much pleasure with learning and using the DirectX.Capture class library and I hope you will enjoy my article also.
Hans Vosman

Sample Image - Capture Test

Introduction

Originally the DirectX.Capture supported Avi file saving only. This format is not very usable because it very often results in huge files. Sometimes only audio needs to be saved (e.g. FM Radio or just TV-sound). The enhancements described in this article makes it possible to save audio in the Windows Media Audio format. It took quite a lot searching and reading before I came to the solution presented in this article. Also I did not find complete C# examples on this subject, so an article on this subject might be interesting.

Considerations

My first idea was to save audio as Wav (about 10MB per minute) or as MP3 (about 1MB per minute?) and describe the resulting implementation. These are general known formats, but coding was not very straightforward. But looking around to possible coding examples, I came across the Windows Media format. Comparing these examples with the DirectX.Capture class example, I noticed that minor to many code modifications seems to be needed. I was in favor of the minor version using SetOutputFileName() and RenderStream(). I noticed that ConfigureFilterUsingProfileGuid() was used also. Only it was not fully clear to me what to do with ConfigureFilterUsingProfileGuid(). It was used for changing the Windows Media audio/video format but the example showed me a video only guid and I had the idea that there were much more possible choices. The SetOutputFileName() is very important. It is not only setting the filename as the name suggest, no, it will also add filters to the graph. For Asf this will be the Asf file writer, for Avi this will be Avi muxer and file writer (two filters!).
But it is obvious, the selection of the preferred Windows Media audio/video format is essential. So, when starting the implementation for saving Windows Media files, many questions raised. What file writer needed to be used (looking into graphedt)? The WM Asf file writer seems to be the one. Which audio/video format should be used? Well, I did not know. How to change the audio/video format? This could be done via the property window that belongs to the Asf file writer. Via a right click on the filter in graphedt, the windows showed up so I knew it was there. Other ways to change the audio/video format? Via a profile representing the audio/video format. Which formats are possible? I did not know. What is the resulting video resolution? I did not know.
When starting coding I got strange errors from the code. In other words: I had to do more research to find answers.

IWMProfile interface

Where should we start? First go to MSDN. For capturing audio, the following link might be interesting: Creating an Audio Capture graph. There you can find a lot of information! Also there is special SDK for Windows Media: Windows Media Format 9.5 SDK. There I found the guid's that DirectX offers by default, so that was one answer I wanted.
The best way for selecting the wanted audio/video file format seems to be the IWMProfile interface. Actually the selection is done by loading a profile representing the audio/video file format. I noticed that MSDN also mentioned that the ConfigureFilterUsingProfileGuid() without a note it should not be used anymore. The amazes me a bit because in DirectShowLib I found a note that the interface became obsolete. I found an interesting example that showed me that it was possible to get the audio/video formats: Windows Media Audio compressor By Idael Cardoso. This article describes how IWMProfile can be used, and the sample program showed me a nice listbox with a list of Windows Media Audio formats. It helped me in finding more information: CSharp Windows Media Format SDK, Translation by Idael Cardoso. Than I made up my mind and I thought, first get the program working, the rest will come later.

Problem with the Asf file writer

When starting coding I got problems with the Asf file writer. For some reason errors occured when using mediaControl.Run(). What I did in general was adding the filter, having the property window for making specific settings, to the graph in advance. Upon the real capturing the added filter(s) were be connected to the graph (either using RenderStream() or a direct connect). That method I used for Wav, Mp3 and Avi file saving, but seems to cause problems for Asf (+ Wmv, Wmv). Why the mediaControl.Run() was called, well when selecting something in the menu, updateMenu() is called ... My impression is that it has something to do with the filter itself. I noticed that all examples I saw, either configured the filter immediately after the SetOutputFileName() or did not configure at all. So I choosed for the first solution. For audio this is okay because the default choice is good enough. For video this would not be acceptable.

The current implementation

Before telling what the solution, I want to explain first the current implementation of the audio/video capturing to file first. This functionality is listed here and can be found in the function renderGraph in Capture.cs. In the code the mediaSubtype is set first, then SetOutputFileName() is called to add the Avi multiplexer and the file writer. Furthermore the filename is stored. The next step is to initialize the video rendering. A possible compressor filter is taken into account. After this the audio rendering is initialized. Upon a call of mediaControl.Run() in the function StartPreviewIfNeeded(), the capture graph starts.
//Original code fragment renderGraph Capture.cs

Guid mediaSubType = MediaSubType.Avi;
hr = captureGraphBuilder.SetOutputFileName( ref mediaSubType, Filename, out 

muxFilter, out fileWriterFilter );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );

// Render video (video -> mux)
if ( VideoDevice != null )
{
	// Try interleaved first, because if the device supports it,
	// it's the only way to get audio as well as video
	cat = PinCategory.Capture;
	med = MediaType.Interleaved;
	hr = captureGraphBuilder.RenderStream( ref cat, ref med, videoDeviceFilter, videoCompressorFilter, muxFilter ); 
	if( hr < 0 ) 
	{
		med = MediaType.Video;
		hr = captureGraphBuilder.RenderStream( ref cat, ref med, videoDeviceFilter, videoCompressorFilter, muxFilter ); 
		if ( hr == -2147220969 ) throw new DeviceInUseException( "Video device", hr );
		if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
	}
}

// Render audio (audio -> mux)
if ( AudioDevice != null )
{
	cat = PinCategory.Capture;
	med = MediaType.Audio;
	hr = captureGraphBuilder.RenderStream( ref cat, ref med, audioDeviceFilter, audioCompressorFilter, muxFilter );
	if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
}

isCaptureRendered = true;
didSomething = true;

The enhancements

I made the choice to present code that enables Windows Media file saving and uses ConfigureFilterUsingProfileGuid() for selecting the proper audio/video format via the Guid that corresponds with a specific audio/video format and so corresponds with a specific profile. Another advantage is that way the coding will be easier to understand. In a future topic the IWMProfile interface could be explained in some more detail. For now it would mean that this article would not be written because it would become a little bit too long. So it this article makes you more curious, I think, that is very positive.
I also made the choice to show changes for Capture.cs (in DirectX.Capture) only, and not for CaptureTest.cs. Changes to CaptureTest.cs are not really needed unless you want to select between an audio or an video format, or to switch between Avi or a Windows media Asf format such as Wma. Such selections could be implemented via a menu option (or a button press). I think it is a good learning goal to add an user control functionality yourself (if the functionality is really needed). Well now  go on the enhancements:

Recording file mode

The enumeration RecFileModeType is declared which specifies the possible audio/video recording file choices. The choices are Avi, Wmv and Wma. The Wma choice becomes the default because this example is about Audio file saving. The Avi choice was added, so the original functionality is still there and could be used if needed. The Wmv is not explained in this article. It is a good learning goal to figure out how to use that. Also the use of Wmv is a little more complicated because video could be a video + audio stream or a video stream only. A good implementation should check in advance if conflicts may occur. For this the IWMProfile interface can be used because IWMProfile can provide information about the streams it uses.
The variable recFileMode contains the audio/video recording file mode, so this variable gets value RecFileModeType.Wma. The variable recFileMode should be accessed via RecFileMode. There is a check added that prevents changing the file mode during file capturing. This to prevent strange side effects. It also shows that other functionality could be executed upon changing the value of recFileMode. A nice feature would be the change of the filename extension. This code should be put in Capture.cs, preferably in the beginning because there you can find more declarations.
/// 
/// Recording file mode type enumerations
/// 
public enum RecFileModeType
{
	///  Avi video (+ audio) 
	Avi,
	///  Wmv video (+ audio) 
	Wmv,
	///  Wma audio 
	Wma,
}

private RecFileModeType recFileMode = RecFileModeType.Wma;

/// 
/// Recording file modes
/// 
public RecFileModeType RecFileMode
{
	get { return(recFileMode); }
	set
	{
		if(this.graphState == GraphState.Capturing)
		{
			// Value may not be changed now
			return;
		}
		recFileMode = value;
	}
}

Capturing, RenderStream()

The most interesting part are the modifications in the capture specific code with RenderStream(), mentioned earlier. The major difference is that the file recording mode is taken into account. Still this example is kept simple, so for saving a specific audio and/or video file settings might be important.

Keep in mind that saving of Wma file is needed:

  • No audio compressor
  • Filename with filename extension .wma (or .asf)
  • RecFileMode = RecFileModeType.Wma
  • Keep in mind that saving of Wmv file is needed:
  • No video compressor
  • No audio compressor
  • Filename with filename extension .wmv (or .asf)
  • Some video formats do not have an audio stream, video capturing will fail in the current implementation
  • RecFileMode = RecFileModeType.Wmv
  • Keep in mind that saving of Avi file is needed:
  • Video compressor, eg DV Avi
  • Filename with filename extension .avi
  • RecFileMode = RecFileModeType.Avi
  • The code explains itselfs (I hope). There are checks added so depending on the file format specific actions can be performed. The first action is the initialization of mediaSubtype. The next action is to configure the Asf file writer. The configuration is a one-liner. Interesting is the type casting of the file writer pointer for calling ConfigureFilterUsingProfileGuid(). This solution is specific for .Net, it gives an easy solution for changing the preferred audio/video profile. There is one question left: What does WMProfile_V80_64StereoAudio means? WMProfile_V80_64StereoAudio is the audio recording format I choosed as default. There are more choices possible (will be described later on). For a different choice, a different value must be used. Also, if you want to save video, just select a valid Windows media video format. The audio and video rendering sections looks the same, with one major difference. Upon entering the video rendering section there is a check on the file format, for audio capturing, no video must be rendered so that section must be ignored!
    // Record captured audio/video in Avi, Wmv or Wma format
    
    Guid mediaSubType; // Media sub type
    
    // Set media sub type
    if(RecFileMode == RecFileModeType.Avi)
    {
    	mediaSubType = MediaSubType.Avi;
    }
    else
    {
    	mediaSubType = MediaSubType.Asf;
    }
    
    // Intialize the Avi or Asf file writer
    hr = captureGraphBuilder.SetOutputFileName( ref mediaSubType, Filename, out muxFilter, out fileWriterFilter );
    if( hr < 0 )
    {
    	Marshal.ThrowExceptionForHR( hr );
    }
    
    // For Wma (and Wmv) a suitable profile must be selected. This can be done
    // via a property window, however the muxFilter is just created. if needed, the
    // property windows should show up right now!
    // Another solution is to configure the Asf file writer, however DShowNet does not
    // have the interface that is needed. Please ensure it is added. 
    if(RecFileMode == RecFileModeType.Wma)
    {
    	IConfigAsfWriter lConfig = muxFilter as IConfigAsfWriter;
    
    	// Obsolete interface?
    	// According to MSDN no, according to DirectShowLib yes.
    	// For simplicity, it will be used ...
    	hr = lConfig.ConfigureFilterUsingProfileGuid(WMProfile_V80_64StereoAudio);
    	if(hr < 0)
    	{
    		// Problems with selecting video write format
    		// Release resources ... (not done yet)
    		Marshal.ThrowExceptionForHR( hr );
    	}
    }
    
    // Render video (video -> mux) if needed or possible
    if((VideoDevice != null)&&(RecFileMode != RecFileModeType.Wma))
    {
    	// Try interleaved first, because if the device supports it,
    	// it's the only way to get audio as well as video
    	cat = PinCategory.Capture;
    	med = MediaType.Interleaved;
    	hr = captureGraphBuilder.RenderStream( ref cat, ref med, videoDeviceFilter, videoCompressorFilter, muxFilter ); 
    	if( hr < 0 ) 
    	{
    		med = MediaType.Video;
    		hr = captureGraphBuilder.RenderStream( ref cat, ref med, videoDeviceFilter, videoCompressorFilter, muxFilter ); 
    		if ( hr == -2147220969 )
    		{
    			throw new DeviceInUseException( "Video device", hr );
    		}
    		if( hr < 0 )
    		{
    			Marshal.ThrowExceptionForHR( hr );
    		}
    	}
    }
    
    // Render audio (audio -> mux) if possible
    if ( AudioDevice != null )
    {
    	// Keep in mind that for certain Wmv formats do not have an audio stream,
    	// so when using this code, please ensure you use a format which supports
    	// audio!
    	cat = PinCategory.Capture;
    	med = MediaType.Audio;
    	hr = captureGraphBuilder.RenderStream( ref cat, ref med, audioDeviceFilter, audioCompressorFilter, muxFilter );
    	if( hr < 0 )
    	{
    		 Marshal.ThrowExceptionForHR( hr );
    	}
    }
    
    isCaptureRendered = true;
    didSomething = true;
    

    Wma, audio formats

    The function ConfigureFilterUsingProfileGuid() needs to be called for setting the proper audio format. Which formats exists? In this example there are seven specific Windows media audio formats provided (hard coded) by means of a Guid. Each Guid stands for a special format and profile. There are more Windows Media formats; however, those formats supports also video (and audio). The best way to get these formats is to use IWMProfile functionality. Than you will be sure that you can get formats that really exists. In a future code sample, the IWMProfile method can be shown. For now, this will be though enough. Thanks to the IWMProfile interface I was able to retrieve all names that belong to a specific file format and profile. The following code, with at least the audio format you are going to use, should be put somewhere in Capture.cs.
    // Windows Media Audio 8 for Dial-up Modem (Mono, 28.8 Kbps)
    private static readonly Guid WMProfile_V80_288MonoAudio = new Guid("7EA3126D-E1BA-4716-89AF-F65CEE0C0C67");
    
    // Windows Media Audio 8 for Dial-up Modem (FM Radio Stereo, 28.8 Kbps)
    private static readonly Guid WMProfile_V80_288StereoAudio = new Guid("7E4CAB5C-35DC-45bb-A7C0-19B28070D0CC");
    
    // Windows Media Audio 8 for Dial-up Modem (32 Kbps)
    private static readonly Guid WMProfile_V80_32StereoAudio = new Guid("60907F9F-B352-47e5-B210-0EF1F47E9F9D");
    
    // Windows Media Audio 8 for Dial-up Modem (Near CD quality, 48 Kbps)
    private static readonly Guid WMProfile_V80_48StereoAudio = new Guid("5EE06BE5-492B-480a-8A8F-12F373ECF9D4");
    
    // Windows Media Audio 8 for Dial-up Modem (CD quality, 64 Kbps)
    private static readonly Guid WMProfile_V80_64StereoAudio = new Guid("09BB5BC4-3176-457f-8DD6-3CD919123E2D");
    
    // Windows Media Audio 8 for ISDN (Better than CD quality, 96 Kbps)
    private static readonly Guid WMProfile_V80_96StereoAudio = new Guid("1FC81930-61F2-436f-9D33-349F2A1C0F10");
    
    // Windows Media Audio 8 for ISDN (Better than CD quality, 128 Kbps)
    private static readonly Guid WMProfile_V80_128StereoAudio = new Guid("407B9450-8BDC-4ee5-88B8-6F527BD941F2");
    
    Using the names in the code, might look like this:
    FM Radio version 1.2

    IConfigAsWriter interface

    To get the code working still one interface needs to be addded because DShowNET does not support the interface of IConfigAsfWriter. DirectShowLib supports this interface; so if you use that library then there is no extra work. This interface can be added to Capture.cs or another suitable place. Keep in mind to change the naming accordingly.
    [Guid("45086030-F7E4-486a-B504-826BB5792A3B"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IConfigAsfWriter
    {
            /// Obsolete?
            [PreserveSig]
            int ConfigureFilterUsingProfileId([In] int dwProfileId);
    
            /// Obsolete?
            [PreserveSig] 
            int GetCurrentProfileId([Out] out int pdwProfileId);
    
            /// Obsolete?
            [PreserveSig]
            int ConfigureFilterUsingProfileGuid([In, MarshalAs(UnmanagedType.LPStruct)] Guid guidProfile);
    
            [PreserveSig]
            int GetCurrentProfileGuid([Out] out Guid pProfileGuid);
    
            /// Obsolete?
            [PreserveSig]
            int ConfigureFilterUsingProfile([In] IntPtr pProfile);
    
            /// Obsolete?
            [PreserveSig]
            int GetCurrentProfile([Out] out IntPtr ppProfile);
    
            [PreserveSig]
            int SetIndexMode([In, MarshalAs(UnmanagedType.Bool)] bool bIndexFile);
    
            [PreserveSig]
            int GetIndexMode([Out, MarshalAs(UnmanagedType.Bool)] out bool pbIndexFile);
    }
    

    Audio rendering, testing

    The modifications shown in this article have been tested. This does not mean, will work without any problem. During testing I noticed that I did not got audible sound. So, I had to make an additional modification. The modification was needed because I am using a Hauppauge PVR150-MCE (Amity2) TV-card. This card gets the audio via the PCI bus, and not via a wired connection. The current DirectX.Capture class example will work with TV-cards using a wired connection to a sound card. So for those who have a special TV card, the modification might be interesting: during preview: do audio rendering.

    I added an option via a variable audioviapci to tell the program to use the extra code or do nothing.

  • A value false must be used for wired audio connections (default choice).
  • A value true must be used when the audio is coming via the PCI bus.
  • The variable audioviapci should be put somewhere in Capture.cs. There is one limitation: the TV-card driver must have a capture device for audio. If the TV-card driver does not have such device, then the following code will not work either. How to handle such case could be a new article. For capturing audio, this modification is not needed. It is just meant for listening.
    // Option for selection audio rendering via the pci bus of the TV card
    // For wired audio connections the value must be false!
    // For TV-cards, like the Hauppauge PVR150, the value must be true!
    // This TV-card does not have a wired audio connection. However, this
    // option will work only if the TV-card driver has an audio device!
    private bool audioviapci = false;
    
    The actual code should be put in the function renderGraph() in Capture.cs. This code should be inserted after the check if (wantPreviewRendered && !isPreviewRendered) in renderGraph() where the video rendering is started.
    // Special option to enable rendering audio via PCI bus
    if(audioviapci)
    {
    	med = MediaType.Audio;
    	hr = captureGraphBuilder.RenderStream( ref cat, ref med, audioDeviceFilter, null, null ); 
    	if( hr < 0 )
    	{
    		Marshal.ThrowExceptionForHR( hr );
    	}
    }
    
    The last parameter in RenderStream() is null. This means the audio is rendered to the default audio renderer. It might be possible that there is still no audible sound. In that case do the following:
  • Go to the menu Audio Device in Devices and select the proper Audio device.
  • Go to the menu PropertyPages in Options, and click on Video Crossbar. Go to the listbox showing Video Decoder Out and select the choice Audio Decoder Out. Than go to the other list box showing Video Tuner In and select the choice Tuner Audio In. Than mark the checkbox link related streams and click on OK.
  • Points of Interest

    There is no demo program added. Please download the full source from the DirectX.Capture Class Library page, and download the demo version. The source file that can be downloaded contains the code changes (and the original code). Just replace the original file with my version.

    Normally, I use conditions to have the original and the new code in the same file. This is handy if there is an error in the code. Then I compare that code with the previous version. I removed the conditions because this souce file should be easy to use. I did not remove the conditions completely, so you will be able to find the exact location of the code changes easily.

    Feedback and Improvements

    I hope this code helps you in understanding the structure of the DirectX.Capture class, and I hope I provided you an enhancement that might be useful to you. Feel free to post any comments and questions.

    History

    March 15, 2006 - First release

    Back to the beginning.


    Back to the Software Development page,
    the Homepage or the Hobby activities page.


    Contact me? Go back to/look at the Software Development page.
    Date: march 15, 2006