Introduction
This article is a follow-up of my previous articles Audio File Saving for the DirectX.Capture Class and Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library. Those articles describe how to do file saving for captured audio and video. This article will show a solution for getting a specific broadcast station by the selection of a specific station name or just entering the broadcast frequency to get the audio and video to be captured. Associated with this article, some extra features such as the VMR9, de-interlacing, and FM Radio are described.
Background
The article that gave me the basic idea was DirectShow - Fine TV Tuning using IKsPropertySet written by Liaqat Fayyaz.
When I started this project, I thought, "What is happening here?" Step-by-step, I discovered what was happening, and I discovered that translating about 70 lines of C code into working C# code was not that easy! I had to understand the meaning of a lot of DirectShow-specific code. It was quite difficult to find usable information. The Liaqat Fayyaz article helped me find the proper terms, and then, with much thought, I discovered how to use the unmanaged data in C# without getting errors. Finally, it took 300 lines of C# code to make TV fine-tuning work. The count of 300 lines is because there is also some "invisible C code" needed that is in some DirectX SDK include files. To understand everything, I had to visit MSDN many times.
The normal way of selecting a TV channel or a TV broadcast station is to choose a channel number between 1 and 368 (for the Netherlands, Europe). These channel numbers are based on a pre-defined frequency table, with frequencies between 45 and 863 MHz. The exact frequencies depend on the country, video standard (PAL, NTSC, SECAM), and the TV tuner. Via the IAMTuner
interface and the put_Channel
method, the desired TV channel can be chosen.
Still, there is the problem that not every TV broadcast station can be tuned. This is the main reason why I prefer the solution of just choosing one of the frequencies my cable provider offers me. Simply enter the frequency, do a little fine-tuning, and then view the TV program. Happily, the IKsPropertySet
interface of the TV tuner object offers this solution.
Using the Code
To understand the C# code, you must understand the original C code that I used as a starting point. This code can be found in Liaqat Fayyaz's article DirectShow - Fine TV Tuning using IKsPropertySet. Based on this article, I wrote a C# solution. We'll start with some original C code and a short explanation. Looking at the next macro, you will probably think, "Wow, what is happening here?" Well, that was at least what I thought.
#define INSTANCEDATA_OF_PROPERTY_PTR(x) ((PKSPROPERTY((x))) + 1)
This is a pointer to the actual data that is used as input and/or returned as output.
#define INSTANCEDATA_OF_PROPERTY_SIZE(x) (sizeof((x)) - sizeof(KSPROPERTY))
This is the size of the actual property data structure that is used for input and output.
hr = m_pTvtuner->QueryInterface(IID_IKsPropertySet, (void**)&m_pKSProp);
QueryInterface
does a query for the interface pointer of an object, in this case the TV tuner object. Without having such an interface, there is nothing to do.
KSPROPERTY_TUNER_MODE_CAPS_S ModeCaps;
KSPROPERTY_TUNER_FREQUENCY_S Frequency;
memset(&ModeCaps,0,sizeof(KSPROPERTY_TUNER_MODE_CAPS_S));
memset(&Frequency,0,sizeof(KSPROPERTY_TUNER_FREQUENCY_S));
This shows the property data structures and the memory allocation for the TUNER_MODE_CAPS_S
and TUNER_FREQUENCY_S
properties used in the SetFrequency()
function. The TUNER_MODE_CAPS_S
property data structure provides the capabilities of the TV tuner devices. Important information that is required includes the minimum frequency and the maximum frequency that can be tuned. The TUNER_FREQUENCY_S
property data structure is used for setting the frequency.
hr = m_pKSProp->QuerySupported(PROPSETID_TUNER, KSPROPERTY_TUNER_MODE_CAPS,
&dwSupported);
QuerySupported
checks if it is possible to use the get
or set
methods for a specific property.
if(SUCCEEDED(hr) && dwSupported&KSPROPERTY_SUPPORT_GET)
{
DWORD cbBytes=0;
hr = m_pKSProp->Get(PROPSETID_TUNER,KSPROPERTY_TUNER_MODE_CAPS,
INSTANCEDATA_OF_PROPERTY_PTR(&ModeCaps),
INSTANCEDATA_OF_PROPERTY_SIZE(ModeCaps),
&ModeCaps, sizeof(ModeCaps), &cbBytes);
}
else
return E_FAIL;
The TUNER_MODE_CAPS_S
property data is used to initialize the tuning flags. The new frequency is copied into the TUNER_FREQUECY_S
property data.
Frequency.Frequency=Freq;
if(ModeCaps.Strategy==KS_TUNER_STRATEGY_DRIVER_TUNES)
Frequency.TuningFlags=KS_TUNER_TUNING_FINE;
else
Frequency.TuningFlags=KS_TUNER_TUNING_EXACT;
The new frequency is validated. If the frequency is within the range, the frequency change will be sent to the TV tuner object via the set
method.
if(Freq>=ModeCaps.MinFrequency && Freq<=ModeCaps.MaxFrequency)
{
hr = m_pKSProp->Set(PROPSETID_TUNER,
KSPROPERTY_TUNER_FREQUENCY,
INSTANCEDATA_OF_PROPERTY_PTR(&Frequency),
INSTANCEDATA_OF_PROPERTY_SIZE(Frequency),
&Frequency, sizeof(Frequency));
if(FAILED(hr))
return E_FAIL;
}
else
return E_FAIL;
If everything goes as expected, the new TV frequency changes and the TV channel shows up. If the previous code is not complete, the following code must be taken into account too! The code shown comes from strmif.h, ks.h, and ksmedia.h in the DirectX SDK.
MIDL_INTERFACE("31EFAC30-515C-11d0-A9AA-00AA0061BE93")
IKsPropertySet : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Set(
REFGUID guidPropSet,
DWORD dwPropID,
LPVOID pInstanceData,
DWORD cbInstanceData,
LPVOID pPropData,
DWORD cbPropData) = 0;
virtual HRESULT STDMETHODCALLTYPE Get(
REFGUID guidPropSet,
DWORD dwPropID,
LPVOID pInstanceData,
DWORD cbInstanceData,
LPVOID pPropData,
DWORD cbPropData,
DWORD *pcbReturned) = 0;
virtual HRESULT STDMETHODCALLTYPE QuerySupported(
REFGUID guidPropSet,
DWORD dwPropID,
DWORD *pTypeSupport) = 0;
};
typedef struct
{
GUID Set;
ULONG Id;
ULONG Flags;
}
KSIDENTIFIER;
typedef KSIDENTIFIER KSPROPERTY;
typedef struct
{
KSPROPERTY Property;
ULONG Frequency; ULONG LastFrequency; ULONG TuningFlags; ULONG VideoSubChannel; ULONG AudioSubChannel; ULONG Channel; ULONG Country; }
KSPROPERTY_TUNER_FREQUENCY_S, *PKSPROPERTY_TUNER_FREQUENCY_S;
This code shows the interface and some data structures. Maybe, you will not believe this, but the most difficult part of the whole C-to-C# translation was to get a usable data structure.
Now the Real Stuff: Go from C to C#
QueryInterface()
is, in C#, a typecast of the IAMTVTuner
object to the IKsPropertySet
interface pointer. QuerySupported()
is called to check if the property data can be read and written. In the C# version, the TUNER_MODE_CAPS_S
property data structure will not be used, mainly because this keeps the code simple. Instead of that, the minimum and maximum frequency are set to fixed values. It is up to you how to initialize. For NTSC countries, the range is normally 45 to 801MHz, and for PAL countries, the range is normally 45 to 863MHz. Keep in mind that TV tuners might have a different range.
The TUNER_FREQUENCY_S
property data is read first, so it can be used for writing too. The main reason is that this way, all attributes will be initialized. The tuning flag in the TUNER_FREQUENCY_S
property data attribute TuningFlags
will be set to KS_TUNER_TUNING_EXACT
because this is what I want to do: change the tuning frequency into the specified value. The new frequency is stored in the TUNER_FREQUENCY_S
property data attribute Frequency
.
After this, the TUNER_FREQUENCY_S
property data is written with the set
method. To make the code more correct, I added a check for using get
and set
via QuerySupported
. There is little chance that the functionality is not supported. Putting everything together, the following solution came up:
public int SetFrequency(int Freq)
{
int hr;
IKsPropertySet pKs = tvTuner as IKsPropertySet;
KSPropertySupport dwSupported = new KSPropertySupport();
DshowError errorCode = DshowError.VFW_NO_ERROR;
if(pKs == null)
{
errorCode = DshowError.VFW_E_NO_INTERFACE;
return (int)errorCode;
}
hr = pKs.QuerySupported(
PROPSETID_TUNER,
(int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
out dwSupported);
if(hr == 0)
{
if( ((dwSupported & KSPropertySupport.Get)==
KSPropertySupport.Get)&&
((dwSupported & KSPropertySupport.Set)== KSPropertySupport.Set)&
(Freq >= this.minFrequency && Freq <=
this.maxFrequency) )
{
KSPROPERTY_TUNER_FREQUENCY_S Frequency =
new KSPROPERTY_TUNER_FREQUENCY_S();
IntPtr freqData = Marshal.AllocCoTaskMem(
Marshal.SizeOf(Frequency));
IntPtr instData = Marshal.AllocCoTaskMem(
Marshal.SizeOf(Frequency.Instance));
int cbBytes = 0;
Marshal.StructureToPtr(Frequency, freqData, true);
Marshal.StructureToPtr(Frequency.Instance, instData, true);
hr = pKs.Get(
PROPSETID_TUNER,
(int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
instData,
Marshal.SizeOf(Frequency.Instance),
freqData,
Marshal.SizeOf(Frequency),
out cbBytes);
if(hr == 0)
{
Frequency.Instance.Frequency = Freq;
Frequency.Instance.TuningFlags =
(int)KS_TUNER_TUNING_FLAGS.TUNING_EXACT;
Marshal.StructureToPtr(Frequency, freqData, true);
Marshal.StructureToPtr(Frequency.Instance, instData, true);
hr = pKs.Set(
PROPSETID_TUNER,
(int)KSPROPERTY_TUNER.TUNER_FREQUENCY,
instData,
Marshal.SizeOf(Frequency.Instance),
freqData,
Marshal.SizeOf(Frequency));
if(hr < 0)
{
errorCode = (DshowError)hr;
}
}
else
{
errorCode = (DshowError)hr;
}
if(freqData != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(freqData);
}
if(instData != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(instData);
}
}
}
else
{
errorCode = (DshowError)hr;
}
return (int)errorCode;
}
KSPROPERTY_TUNER_FREQUENCY
The most difficult part for me was to make a data structure that did not cause an error when using the get
and set
methods. The get
and set
methods need parameters pointing to the tuner data. In C, the get
/set
interface needs a pointer (done via the two macros mentioned before) to the whole data structure KSPROPERTY_TUNER_FREQUENCY_S
, the part with only the tuner-specific attributes (starting from the attribute Frequency
), and the size of the data. C# does not know pointers, so I needed a different solution.
The first step was to translate the KSPROPERTY_TUNER_FREQUENCY_S
structure into C# using [StructLayout( ...
. How do we access the tuner-specific attributes in a C# friendly way? Well, I decided to put the tuner-specific attributes in a new TUNER_FREQUENCY
-specific data structure and create another structure with the KSPROPERTY
structure and TUNER_FREQUENCY
. Still, the code did not work as expected. For some unknown reason, I decided to change the size of the data structure by adding dummy attributes. Now, the C# code came to life.
[StructLayout(LayoutKind.Sequential)]
public struct KSPROPERTY_TUNERFREQUENCY
{
[MarshalAs(UnmanagedType.U4)]
public int Frequency;
[MarshalAs(UnmanagedType.U4)]
public int LastFrequency;
[MarshalAs(UnmanagedType.U4)]
public int TuningFlags;
[MarshalAs(UnmanagedType.U4)]
public int VideoSubChannel;
[MarshalAs(UnmanagedType.U4)]
public int AudioSubChannel;
[MarshalAs(UnmanagedType.U4)]
public int Channel;
[MarshalAs(UnmanagedType.U4)]
public int Country;
[MarshalAs(UnmanagedType.U4)]
public int Dummy;
}
[StructLayout(LayoutKind.Sequential)]
public struct KSPROPERTY_TUNER_FREQUENCY_S
{
public KSPROPERTY Property;
public KSPROPERTY_TUNERFREQUENCY Instance;
}
Additional Features in this Code Example
Video De-interlacing
Quite often, the video preview quality looks bad. Using a de-interlace filter may improve the preview quality dramatically. The main reason I added the de-interlace filter has nothing to do with the preview quality. I added this filter because it enabled a preview for my Hauppauge PVR150 TV-card. Usually, I got a black screen, so no video was displayed. With this filter in, however, video seems to be playing right away.
I chose the Alpary filter because it can be used freely. Other filters, such as the ffdshow filter at Sourceforge.net, might be usable too. But I have not tested any. The function FindDeinterlaceFilter()
scans filters.LegacyFilters
to find the specified filter. It is easy to specify a different filter.
string filterName = "Alparysoft Deinterlace Filter";
Filter DeInterlace = null;
for (int i = 0; i < this.filters.LegacyFilters.Count; i++)
{
if (filters.LegacyFilters[i].Name.StartsWith(filterName))
{
this.capture.DeInterlace = filters.LegacyFilters[i];
return true;
}
}
This filter is added to the graph just before calling RenderStream()
to render the video. If the filter is in, RenderStream()
will usually add this filter automatically. In some cases, the de-interlace filter will not be added to the graph. In those cases, extra code is needed to add the de-interlace filter explicitly. The preview quality will be better when VMR9 (Video Mixing Renderer 9) is used, even if no extra de-interlace filter is used. Interestingly, VMR9 offers de-interlacing itself (IVMRDeinterlaceControl9
). It can be used via the VMR9 property page or via a software control.
Video De-interlacing and VMR9
I added an option to the program to use VMR9 more easily. To initialize the proper video renderer, the function InitVideoRenderer
should be called upon rendering a video.
#if DSHOWNET
[ComImport, Guid("70e102b0-5556-11ce-97c0-00aa0055595a")]
public class VideoRenderer
{
}
#endif
private bool useVMR9 = false;
private IBaseFilter videoRendererFilter = null;
public bool UseVMR9
{
get { return this.useVMR9; }
set { this.useVMR9 = value; }
}
private bool InitVideoRenderer()
{
if(this.useVMR9)
{
this.videoRendererFilter = (IBaseFilter)new VideoMixingRenderer9();
}
else
{
this.videoRendererFilter = (IBaseFilter)new VideoRenderer();
}
if(this.videoRendererFilter != null)
{
this.graphBuilder.AddFilter(this.videoRendererFilter,
"Video Renderer");
}
return false;
}
The video renderer is put in the graph via RenderStream
:
#if DSHOWNET
hr = captureGraphBuilder.RenderStream(ref cat, ref med, videoDeviceFilter,
null, this.videoRendererFilter);
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat),
DsGuid.FromGuid(med), videoDeviceFilter, null, this.videoRendererFilter);
#endif
Using FM Radio
I added this feature because it might be useful when testing code for TV tuners that support FM Radio. FM Radio can be selected only if the TV tuner supports this. I did not add a broadcast station selection list (yet). For testing reasons, it was sufficient for me to switch between the TV-specific code and the FM Radio-specific code.
Switching between TV and FM Radio looks straightforward, but it is not. It might be possible that when the tuner property page is accessed, the new settings should be taken into account. FM Radio does not need video preview, and how do we deal with that? Currently, the video preview is ignored. FM Radio needs different presets; currently, this functionality is not supported.
#if DSHOWNET
private DShowNET.AMTunerModeType TunerModeType
#else
private AMTunerModeType TunerModeType
#endif
{
get { return this.tunerModeType; }
set
{
this.tunerModeType = value;
if((this.capture != null)&&(this.capture.Tuner != null))
{
this.capture.Tuner.AudioMode = value;
this.capture.Tuner.InputType = this.tunerInputType;
this.capture.Tuner.TuningSpace = this.DefaultTuningSpace;
this.capture.Tuner.CountryCode = this.DefaultCountryCode;
}
this.numericUpDown1.Enabled = false;
switch(value)
{
case AMTunerModeType.TV:
if((this.capture != null)&&(this.capture.Tuner != null))
{
this.numericUpDown1.Maximum = this.capture.Tuner.MaxFrequency;
this.numericUpDown1.Minimum = this.capture.Tuner.MinFrequency;
this.numericUpDown1.Value = this.LastTvFrequency;
this.numericUpDown1.Increment = 500000;
if(this.LastTvFrequency == 0)
{
this.LastTvFrequency = this.tvSelections.GetChannelFrequency;
}
this.capture.Tuner.SetFrequency(this.LastTvFrequency);
this.numericUpDown1.Enabled = true;
}
break;
case AMTunerModeType.FMRadio:
if((this.capture != null)&&(this.capture.Tuner != null))
{
this.capture.Tuner.Channel = this.LastFMRadioFrequency;
this.numericUpDown1.Minimum = this.capture.Tuner.ChanelMinMax[0];
this.numericUpDown1.Maximum = this.capture.Tuner.ChanelMinMax[1];
this.numericUpDown1.Increment = 50000;
this.numericUpDown1.Value = this.LastFMRadioFrequency;
this.numericUpDown1.Enabled = true;
}
break;
default:
break;
}
}
}
Features are Made Optional
In the real code example, I added the new features as options. To use a new feature, the corresponding option needs to be selected first. The main reason for doing this is that a program sometimes fails at first use, due to one of the option settings. Then, you can just change the option value and try again. There is one demand: a new value of an option becomes active upon (re)selecting the Audio or Video device. To get the options properly initialized, the function InitMenu()
is added. This function should be called when a capture device is (re)selected.
private void initMenu()
{
if (this.capture != null)
{
this.capture.VideoSource = this.capture.VideoSource;
this.capture.UseVMR9 = this.menuUseVMR9.Checked;
this.menuUseDeInterlace1.Checked =
this.FindDeinterlaceFilter(this.menuUseDeInterlace1.Checked);
}
}
Points of Interest
Compared with the previous articles, Audio File Saving for the DirectX.Capture Class and Video File Saving in Windows Media Video Format for the DirectX.Capture Class Library, most of the features are kept in, and a number of features have been removed to make it a more usable TV program:
- Added a TV tuning frequency up/down box to test the new TV fine-tuning functionality. The TV fine-tune functionality is put in a new class
TVFineTune
which inherits from the original Tuner
class. To use the TV tuner's new (and old) functionality in the code, the TVFineTune
class should be used instead of the Tuner
class. - A new class called
TVSelections
was added, so the TV channel selection functionality becomes far more usable than the one in the original implementation. The implementation is very simple, and it can be modified easily. The channel selections are hard-coded! To use it on your own system, the settings needs to be modified. The code shows three tables: one with the channel names, one with the tuning frequency, and one with the channel number. The use of channel numbers is very system-specific; I added the values to show that it cannot be used that easily unless you know which frequency corresponds with it. Of course, it is possible to write a nice program using Channel
to set the TV Tuner and GetVideoFrequnecy()
to get the corresponding tuning frequency. Why do this, though, if you already know the TV tuning frequency? - Added functionality to initialize country dependent settings. Based on the country dependent settings, this code tries to retrieve the minimum and maximum TV tuning frequency.
- Change of color space and video standard is supported.
- Support for TV card drivers that support a video device only. There is no separate audio device to choose, so the audio device needs to be found a little bit differently. The modifications were needed so my Hauppauge PVR150 MCE TV card could be used with the newest TV card driver in the code example.
- This code example has a possible solution to get TV sound. This solution was needed because there were problems with getting audible sound and the selection of audio sources. The problems also show up in the original version of the DirectX.Capture Class Library written by Brian Low. So, it is not a new problem introduced by my code enhancements. Via debugging, I noticed that in some cases, the audio source and/or video source becomes invalid. As a result of this, exceptions are fired. For that reason, the data causing the problem is reinitialized upon using it in PropertyPages, VideoSources, and/or AudioSources.
- The code example has been tested with Visual Studio 2003 as well as Visual Studio 2005. Conflicts between these two compiler versions might occur. I added the conditional
VS2003
to show the code differences. A difference was that in Visual Studio 2003, a signal was named Closed
, while in Visual Studio 2005, this signal has the name FormClosed
. This code example has two versions: the Visual Studio 2003 version using DShowNET as the DirectShow interface library and the Visual Studio 2005 version using DirectShowLib-2005 as the DirectShow interface library. It should still be possible to use DShowNET with Visual Studio 2005, but I did not test that. If both Visual Studio versions are needed, then use different directories for this code example to prevent build problems. It is not my intention to solve coding conflicts and build problems that might occur between the several Visual Studio versions. - Important: Choose DirectShowLib or DShowNET! The DirectX.Capture class example uses DShowNET. The DirectX.Capture Class Library (Refresh) uses DirectShowLib, which is more complete than DShowNET. It is up to you what to use.
- The DirectX.Capture examples that go with this article contain new solutions to increase stability. Still, exceptions may occur, but most of them can be solved by either redesigning this code example, or by catching and handling the exceptions in a more appropriate way. Keep in mind that this code example is for learning purposes only. It teaches you how to use DirectShow in C#, and teaches you to use GUI. Exceptions that occur should not be seen as a problem, but as a challenge! The major advantage of an exception is that it tells you when something goes wrong. As a side effect, the program fails and, by debugging, the cause of the problem can be found much easier because you know where to start.
History
- January 31, 2007: First release.
- August 1, 2007: Added support for FM Radio and video de-interlacing. The solution of capturing audio via the video device filter has been improved. Furthermore, the code example supports either
DShowNET
or DirectShowLib
as the interface library via the conditional DSHOWNET
. - August 10, 2007: Added
SampleGrabber
and VMR9 support in an extra code example. - November 28, 2007: Fixed minor bugs in downloads.
- February 17, 2008: Minor text modifications, links corrected, and the SampleGrabber example has been modified to support Visual Studio 2003 and Visual Studio 2005. Please use either one of these versions only. When both Visual Studio versions are used, put the code in different directories!
- April 1, 2009: SampleGrabber code example moved to the new article. Minor text modifications, and fixed minor bugs in the downloads.