DirectShow:使用 SampleGrabber 抓取帧和构建 VU Meter 的示例
DirectX.Capture 类示例,展示如何从视频中抓取帧以及如何制作音频 VU 表
- 下载 SampleGrabber 演示 - 95.93 KB
- 下载 SampleGrabber 源代码 - 278.47 KB
- 下载音频 VU 表演示 - 95.36 KB
- 下载音频 VU 表源代码 - 302.21 KB

引言
本文是我之前文章的后续
- DirectX.Capture 类的音频文件保存
- DirectX.Capture 类库中 Windows Media 视频格式的视频文件保存
- DirectShow: 使用 C# 中的 IKsPropertySet 进行电视微调
这些文章描述了如何保存捕获的音频和视频文件,并微调电视调谐器。本文将解释如何将 SampleGrabber
用于音频和视频。第一部分展示了如何从视频流中抓取帧,第二部分通过制作 VU 表展示了音频电平!
从视频流中抓取帧
背景
DirectShow 提供了两种基本方法来抓取将要渲染的帧或图像。第一种抓取帧的方法是 SampleGrabber
方法。通过 SampleGrabber
,可以通过帧事件或 GetCurrentBuffer
来抓取帧。第二种方法涉及通过调用 GetCurrentImage()
使用 VideoMixingRenderer
或 BasicVideo
接口。这种方法肯定可以用于 VMR 或 VMR9,有时也可能适用于 Video Renderer。
如果捕获设备有 VP(视频端口)引脚,则不能使用 SampleGrabber
代码。只有带有视频捕获设备的显卡(例如 Nivdia MX460 Vivo 显卡)才会有此类引脚。这不是一个大问题,因为 SampleGrabber
可以通过捕获引脚使用,或者 GetCurrentImage()
可以通过连接到 VP 引脚以渲染视频的 VMR 使用。
在这个示例中,SampleGrabber
方法用于通过帧事件抓取帧。此方法在互联网上流传的大多数示例中都有使用。另一个优点是您可以选择捕获一帧或所有帧。大多数示例没有显示使 SampleGrabber
正常工作真正需要哪些操作。这个示例展示了需要完成的工作。
代码
首先,我将描述应该放入 DirectX.Capture\Capture.cs 中的代码更改。函数 InitSampleGrabber
将 SampleGrabber
过滤器添加到图中,并且此函数还初始化了它应该使用的媒体类型。此函数应在渲染视频进行预览时调用。
private bool InitSampleGrabber()
{
// Get SampleGrabber
this.sampGrabber = new SampleGrabber() as ISampleGrabber;
if(this.sampGrabber == null)
{
return false;
}
#if DSHOWNET
this.baseGrabFlt = (IBaseFilter)this.sampGrabber;
#else
this.baseGrabFlt = sampGrabber as IBaseFilter;
#endif
if(this.baseGrabFlt == null)
{
Marshal.ReleaseComObject(this.sampGrabber);
this.sampGrabber = null;
}
AMMediaType media = new AMMediaType();
media.majorType = MediaType.Video;
media.subType = MediaSubType.RGB24;
media.formatPtr = IntPtr.Zero;
hr = sampGrabber.SetMediaType(media);
if(hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
hr = graphBuilder.AddFilter(baseGrabFlt, "SampleGrabber");
if(hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
hr = sampGrabber.SetBufferSamples(false);
if( hr == 0 )
{
hr = sampGrabber.SetOneShot(false);
}
if( hr == 0 )
{
hr = sampGrabber.SetCallback(null, 0);
}
if( hr < 0 )
{
Marshal.ThrowExceptionForHR(hr);
}
return true;
}
可能选定的媒体类型 RGB24
不可用。在这种情况下,请修改代码。以下代码行显示了如何在渲染视频时将 SampleGrabber
放入图中
#if DSHOWNET
hr = captureGraphBuilder.RenderStream(ref cat, ref med, videoDeviceFilter,
this.baseGrabFlt, this.videoRendererFilter);
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat),
DsGuid.FromGuid(med), videoDeviceFilter, this.baseGrabFlt,
this.videoRendererFilter);
#endif
如果使用 GetCurrentBuffer
,则应调用 SetBufferSamples(true)
而不是 SetBufferSamples(false)
。函数 SetMediaSampleGrabber
检索媒体特定数据并将该数据存储以供以后使用。此函数应在初始化预览窗口时调用。
private int snapShotWidth = 0;
private int snapShotHeight = 0;
private int snapShotImageSize = 0;
private bool snapShotValid = false;
private void SetMediaSampleGrabber()
{
this.snapShotValid = false;
if((this.baseGrabFlt != null)&&(this.AllowSampleGrabber))
{
AMMediaType media = new AMMediaType();
VideoInfoHeader videoInfoHeader;
int hr;
hr = sampGrabber.GetConnectedMediaType(media);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr ==
IntPtr.Zero))
{
Throw new NotSupportedException(
"Unknown Grabber Media Format");
}
videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(
media.formatPtr, typeof(VideoInfoHeader));
this.snapShotWidth = videoInfoHeader.BmiHeader.Width;
this.snapShotHeight = videoInfoHeader.BmiHeader.Height;
this.snapShotImageSize = videoInfoHeader.BmiHeader.ImageSize;
Marshal.FreeCoTaskMem(media.formatPtr);
media.formatPtr = IntPtr.Zero;
this.snapShotValid = true;
}
if (!this.snapShotValid)
{
this.snapShotWidth = 0;
this.snapShotHeight = 0;
this.snapShotImageSize = 0;
}
}
请记住,如果媒体类型发生变化,每像素字节数(步幅)也可能发生变化。抓取帧的代码可能如下所示
/// <summary> Interface frame event </summary>
public delegate void HeFrame(System.Drawing.Bitmap BM);
/// <summary> Frame event </summary>
public event HeFrame FrameEvent2;
private byte[] savedArray;
private int bufferedSize;
int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer,
int BufferLen )
{
this.bufferedSize = BufferLen;
int stride = this.SnapShotWidth * 3;
Marshal.Copy( pBuffer, this.savedArray, 0, BufferLen );
GCHandle handle = GCHandle.Alloc( this.savedArray, GCHandleType.Pinned );
int scan0 = (int) handle.AddrOfPinnedObject();
scan0 += (this.SnapShotHeight - 1) * stride;
Bitmap b = new Bitmap(this.SnapShotWidth, this.SnapShotHeight, -stride,
System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr) scan0 );
handle.Free();
SetBitmap=b;
return 0;
}
/// <summary> capture event, triggered by buffer callback. </summary>
private void OnCaptureDone()
{
Trace.WriteLine( "!!DLG: OnCaptureDone" );
}
/// <summary> Allocate memory space and set SetCallBack </summary>
public void GrapImg()
{
Trace.Write ("IMG");
if( this.savedArray == null )
{
int size = this.snapShotImageSize;
if( (size < 1000) || (size > 16000000) )
return;
this.savedArray = new byte[ size + 64000 ];
}
sampGrabber.SetCallback( this, 1 );
}
/// <summary> Transfer bitmap upon firing event </summary>
public System.Drawing.Bitmap SetBitmap
{
set
{
this.FrameEvent2(value);
}
}
我看到的大多数示例都没有释放 SampleGrabber
特定数据。这个代码示例应该能正确完成这项工作...但错误仍然可能发生。为了使 SampleGrabber
代码正常工作,CaptureTest\CaptureTest.cs 中的主程序也必须修改。首先,CaptureTest
表单需要添加两个额外的按钮和一个 PictureBox
。在代码示例中,我添加了特殊代码来添加一个小的 PictureBox
。您可以自己调整表单大小以及调整按钮和 PictureBox
的大小和位置。
我故意制作了一个非常小的 PictureBox
。我还添加了一些代码来隐藏视频文件保存按钮和文件名,当 SampleGrabber
被放入图中时。我这样做是故意的,因为这在 CaptureTest
表单上给我留出了一些自由的设计空间。这也防止了与视频文件保存功能可能发生的交互。
private void button1_Click(object sender, System.EventArgs e)
{
this.capture.FrameEvent2 += new Capture.HeFrame(this.CaptureDone);
this.capture.GrapImg();
}
private void CaptureDone(System.Drawing.Bitmap e)
{
this.pictureBox1.Image=e;
// Show only the selected frame ...
// If you want to capture all frames, then remove the next line
this.capture.FrameEvent2 -= new Capture.HeFrame(this.CaptureDone);
}
private void button2_Click(object sender, System.EventArgs e)
{
if( (this.pictureBox1 != null)&&
(this.pictureBox1.Image != null)&&
(this.imageFileName.Text.Length > 0) )
{
this.pictureBox1.Image.Save(this.imageFileName.Text,
System.Drawing.Imaging.ImageFormat.Bmp);
}
}
功能被设置为可选
在实际代码示例中,我将新功能作为选项添加。要使用新功能,需要首先选择相应的选项。这样做的主要原因是,程序有时在首次使用时会失败,原因在于某个选项设置。现在您只需更改选项值并重试即可。有一个要求:选项的新值在(重新)选择音频或视频设备时才生效。为了正确初始化选项,添加了函数 InitMenu()
。此函数应在捕获设备被(重新)选择时调用。
private void initMenu()
{
if (this.capture != null)
{
// Set flag only if capture device is initialized
this.capture.AllowSampleGrabber =
this.menuAllowSampleGrabber1.Checked;
this.menuSampleGrabber1.Enabled =
this.menuAllowSampleGrabber1.Checked;
this.menuSampleGrabber1.Visible =
this.menuAllowSampleGrabber1.Checked;
this.capture.VideoSource = this.capture.VideoSource;
this.capture.UseVMR9 = this.menuUseVMR9.Checked;
this.menuUseDeInterlace1.Checked = this.FindDeinterlaceFilter(
this.menuUseDeInterlace1.Checked);
}
}

通过 VU 表使用 SampleGrabber 显示音频电平
背景
录制音频时,需要正确设置音频电平。我注意到有时音频音量太低,有时音频音量太强。可听见的音频提供了捕获音频音量的一些概念,但这还不够。可听见的音量通常设置为使音频听起来很棒的水平。对于录音,这太模糊了。已经发布了一些有趣的代码示例,例如 模拟和 LED 表、LED vu Meter 用户控件 或 使用 DirectX 的 LED 风格音量表。一篇文章展示了外观精美的 VU 表,下一篇文章只展示了 VU 表,最后一篇文章使用了 DirectSound。这些文章教会了我如何制作和使用用户控件。这些文章没有教我如何获取和分析音频,所以我必须自己弄清楚这一点。
此外,我想知道捕获音频的平均和峰值电平。DirectShow 或捕获设备没有提供此类接口,因此需要一个过滤器来分析音频并将音频电平传递给程序 GUI。该过滤器可以是 DMO
过滤器,不幸的是 DMO 输出接口非常有限。该过滤器可以是 SampleGrabber
,这是一个制作新代码示例的不错挑战!
在测试过程中,我注意到抓取音频可能会因为抓取和显示音频需要太多的 CPU 时间而导致小的“咔哒”声,从而影响音频录制。特别是 Hauppauge PVR150 给我带来了这个问题。这个问题听起来有点奇怪,因为视频流通常涉及更多数据,因此出现问题的可能性更大。我认为音频采样率更高是导致问题的原因。视频每秒出现 25 或 30 帧,但音频通常每秒出现 44100 或 48000 次。我为此修改了 DMO
过滤器,但问题仍然存在,所以我认为这与 SampleGrabber
可能的限制无关。这个捕获设备出现问题的原因可能是因为音频缓冲区看起来很小,但也可能是一个隐藏的问题... 我用其他电视卡(Pinnacle PCTV 310i、Pinnacle PCTV 330eV、SB!Live 和其他声卡)测试了这段代码,这些卡没有出现问题。主要区别在于缓冲区大小,它更大了...
代码
首先,我将描述应该放入 DirectX.Capture\Capture.cs 中的代码更改。函数 InitSampleGrabber
将 SampleGrabber
过滤器添加到图中,并且此函数还初始化了它应该使用的媒体类型。此函数应在渲染音频进行预览时调用。
/// <summary>
/// SampleGrabber flag, if false do not insert SampleGrabber in graph
/// </summary>
private bool allowSampleGrabber = false;
/// <summary>
/// Check if usage SampleGrabber is allowed
/// </summary>
public bool AllowSampleGrabber
{
get { return this.allowSampleGrabber; }
set { this.allowSampleGrabber = value; }
}
/// <summary> Audio grabber filter interface. </summary>
private IBaseFilter audioGrabFlt = null;
/// <summary>
/// Null renderer for AudioGrabber in case off old TV card with
/// audio capturing via the soundcard. TV card is connected via
/// a wired connection with the soundcard.
/// </summary>
private IBaseFilter nullRendererFlt = null;
/// <summary>
/// Audio Grabber interface
/// </summary>
protected ISampleGrabber audioGrabber = null;
int ISampleGrabberCB.SampleCB( double SampleTime, IMediaSample pSample )
{
Trace.Write ("Audio sample ...");
return 0;
}
/// <summary>
/// Enable Sample Grabber event
/// </summary>
/// <param name="handler"></param>
public void EnableEvent(AudioFrame handler)
{
if(this.audioGrabber == null)
{
return;
}
Trace.Write ("Init audio grabbing ...");
if( this.savedArray == null )
{
this.savedArray = new short[ 50000 ];
}
//this.InitAudioGrabbing();
if(this.audioGrabber == null)
{
return;
}
this.FrameEvent += new AudioFrame(handler);
this.audioGrabber.SetCallback( this, 1 );
}
/// <summary>
/// Disable Sample Grabber event
/// </summary>
/// <param name="handler"></param>
public void DisableEvent(AudioFrame handler)
{
if(this.audioGrabber == null)
{
return;
}
this.FrameEvent -= handler;
this.audioGrabber.SetCallback(null, 0);
}
/// <summary> Interface frame event </summary>
public delegate void AudioFrame(short[] AS, int BufferLen);
/// <summary> Frame event </summary>
public event AudioFrame FrameEvent;
private short[] savedArray;
int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
{
Marshal.Copy(pBuffer, this.savedArray, 0, BufferLen/2);
this.FrameEvent(this.savedArray, BufferLen/2);
return 0;
}
private bool InitAudioGrabber()
{
if (!this.AudioAvailable)
{
// nothing to do
return false;
}
if (!this.allowSampleGrabber)
{
return false;
}
this.DisposeAudioGrabber();
int hr = 0;
// Get SampleGrabber if needed
if(this.audioGrabber == null)
{
this.audioGrabber = new SampleGrabber() as ISampleGrabber;
}
if(this.audioGrabber == null)
{
return false;
}
#if DSHOWNET
this.audioGrabFlt = (IBaseFilter)this.audioGrabber;
#else
this.audioGrabFlt = audioGrabber as IBaseFilter;
#endif
if(this.audioGrabFlt == null)
{
Marshal.ReleaseComObject(this.audioGrabber);
this.audioGrabber = null;
}
AMMediaType media = new AMMediaType();
media.majorType = MediaType.Audio;
#if DSHOWNET
media.subType = PCM;
#else
media.subType = MediaSubType.PCM;
#endif
media.formatPtr = IntPtr.Zero;
hr = this.audioGrabber.SetMediaType(media);
if(hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
hr = graphBuilder.AddFilter(audioGrabFlt, "AudioGrabber");
if(hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
hr = this.audioGrabber.SetBufferSamples(false);
if( hr == 0 )
{
hr = this.audioGrabber.SetOneShot(false);
}
if( hr == 0 )
{
hr = this.audioGrabber.SetCallback(null, 0);
}
if( hr < 0 )
{
Marshal.ThrowExceptionForHR(hr);
}
return true;
}
private void SetMediaSampleGrabber()
{
this.snapShotValid = false;
if((this.audioGrabFlt != null)&&(this.AllowSampleGrabber))
{
AMMediaType media = new AMMediaType();
media.formatType = FormatType.WaveEx;
int hr;
hr = this.audioGrabber.GetConnectedMediaType(media);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
if ((media.formatType != FormatType.WaveEx) ||
(media.formatPtr == IntPtr.Zero))
{
throw new NotSupportedException
("Unknown Grabber Media Format");
}
WaveFormatEx wav = new WaveFormatEx();
wav = (WaveFormatEx)Marshal.PtrToStructure
(media.formatPtr, typeof(WaveFormatEx));
this.avgBytesPerSec = wav.nAvgBytesPerSec;
this.audioBlockAlign = wav.nBlockAlign;
this.audioChannels = wav.nChannels;
this.audioSamplesPerSec = wav.nSamplesPerSec;
this.audioBitsPerSample = wav.wBitsPerSample;
Marshal.FreeCoTaskMem(media.formatPtr);
media.formatPtr = IntPtr.Zero;
this.snapShotValid = true;
}
if (!this.snapShotValid)
{
this.avgBytesPerSec = 0;
this.audioBlockAlign = 0;
this.audioChannels = 0;
this.audioSamplesPerSec = 0;
this.audioBitsPerSample = 0;
}
}
private int avgBytesPerSec = 0;
private int audioBlockAlign = 0;
private int audioChannels = 0;
private int audioSamplesPerSec = 0;
private int audioBitsPerSample = 0;
private bool snapShotValid = false;
/// <summary>
/// Dispose Sample Grabber specific data
/// </summary>
public void DisposeAudioGrabber()
{
if(this.audioGrabFlt != null)
{
try
{
this.graphBuilder.RemoveFilter(this.audioGrabFlt);
}
catch
{
}
Marshal.ReleaseComObject(this.audioGrabFlt);
this.audioGrabFlt = null;
}
if(this.audioGrabber != null)
{
Marshal.ReleaseComObject(this.audioGrabber);
this.audioGrabber = null;
}
if(this.nullRendererFlt != null)
{
try
{
this.graphBuilder.RemoveFilter(this.nullRendererFlt);
}
catch
{
}
Marshal.ReleaseComObject(this.nullRendererFlt);
this.nullRendererFlt = null;
}
this.savedArray = null;
}
对于音频抓取器,需要将 SampleGrabber 添加到图中,以下代码将为带有音频和视频捕获设备以及通过 PCI 总线(非有线音频)获取音频的电视卡执行此操作。
// Special option to enable rendering audio via PCI bus
if((this.AudioViaPci)&&(audioDeviceFilter != null))
{
cat = PinCategory.Preview;
med = MediaType.Audio;
if(this.InitAudioGrabber())
{
Debug.WriteLine("AudioGrabber added to graph.");
#if DSHOWNET
hr = captureGraphBuilder.RenderStream
( ref cat, ref med, audioDeviceFilter, this.audioGrabFlt, null );
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat),
DsGuid.FromGuid(med), audioDeviceFilter, this.audioGrabFlt, null);
#endif
}
else
{
#if DSHOWNET
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
audioDeviceFilter, null, null );
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat),
DsGuid.FromGuid(med), audioDeviceFilter, null, null);
#endif
}
if( hr < 0 )
{
Marshal.ThrowExceptionForHR( hr );
}
对于那些使用有线音频旧电视卡的人来说,必须为 SampleGrabber 渲染音频。在这种情况下不允许音频预览,因为它已经通过普通声卡完成。因此,需要添加空渲染器。以下代码将完成此操作。
if((!this.AudioViaPci)&&(audioDeviceFilter != null)&&(this.audioGrabFlt != null))
{
// Special scenario because normally no audio rendering is needed, however the
// SampleGrabber must be inserted. So audio rendering is needed
// for this specific scenario. In addition the null renderer will be added.
cat = PinCategory.Preview;
med = MediaType.Audio;
if(this.InitAudioGrabber())
{
this.nullRendererFlt = (IBaseFilter)new NullRenderer();
hr = graphBuilder.AddFilter(this.nullRendererFlt, "Null Renderer");
if(hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
Debug.WriteLine("AudioGrabber added to graph.");
#if DSHOWNET
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
audioDeviceFilter, this.audioGrabFlt, this.nullRendererFlt);
#else
hr = captureGraphBuilder.RenderStream(DsGuid.FromGuid(cat),
DsGuid.FromGuid(med), audioDeviceFilter, this.audioGrabFlt, null);
#endif
}
if( hr < 0 )
{
Marshal.ThrowExceptionForHR( hr );
}
}
首先,我将描述应该放入 CaptureTest\CaptureTest.cs 中的代码更改。此代码将处理抓取的音频。为了在 LED 显示屏中显示音频输出,需要将音频数据转换为图像。这可以直接通过以特定颜色绘制矩形或通过用户控件来完成。用户控件也会绘制图像,但它是一个独立的代码片段,可以很容易地在其他程序中使用。因此,为了显示音频电平,使用了一个用户控件。本程序中使用的用户控件是 Gary Perkin 的 vuMeterLed 实现的简化版本。我的用户控件只会做它应该做的事情:显示音频电平,没有花哨的边框。在 Visual Studio 2005 中,我在使用用户控件时遇到了问题,所以我手动添加了用户控件。在 Visual Studio 2003 中,用户控件可以通过表单添加。为了使用户控件正常工作,需要一些额外的代码来使某些控件(不)可见。
private bool sampleGrabber = false;
private bool SampleGrabber
{
get { return this.sampleGrabber; }
set
{
if ((this.capture != null) && (this.capture.AllowSampleGrabber))
{
this.sampleGrabber = value;
}
else
{
this.sampleGrabber = false;
}
this.menuSampleGrabber1.Checked = this.sampleGrabber;
if(this.vuMeterLed1 == null)
{
this.vuMeterLed1 = new UserControl.VuMeterLed();
this.vuMeterLed1.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.vuMeterLed1.Visible = false;
this.vuMeterLed1.Location = new System.Drawing.Point
(this.label2.Location.X + 49, this.label2.Location.Y);
this.vuMeterLed1.Name = "vuMeterLed1";
this.vuMeterLed1.Peak = 0;
this.vuMeterLed1.Size = new System.Drawing.Size(100, 20);
this.vuMeterLed1.TabIndex = 30;
this.vuMeterLed1.Volume = 0;
this.Controls.Add(this.vuMeterLed1);
}
if(this.vuMeterLed2 == null)
{
this.vuMeterLed2 = new UserControl.VuMeterLed();
this.vuMeterLed2.Anchor =
((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Left)));
this.vuMeterLed2.Visible = false;
this.vuMeterLed2.Location = new System.Drawing.Point
(this.label4.Location.X + 49, this.label4.Location.Y);
this.vuMeterLed2.Name = "vuMeterLed2";
this.vuMeterLed2.Peak = 0;
this.vuMeterLed2.Size = new System.Drawing.Size(100, 20);
this.vuMeterLed2.TabIndex = 31;
this.vuMeterLed2.Volume = 0;
this.Controls.Add(this.vuMeterLed2);
}
if (this.sampleGrabber)
{
this.button1.Visible = true;
this.txtFilename.Visible = false;
this.btnCue.Visible = false;
this.btnStart.Visible = false;
this.btnStop.Visible = false;
this.label1.Visible = false;
this.label2.Visible = true;
this.label4.Visible = true;
this.vuMeterLed1.Visible = true;
this.vuMeterLed1.Enabled = true;
this.vuMeterLed2.Visible = true;
this.vuMeterLed2.Enabled = true;
}
else
{
this.button1.Visible = false;
this.vuMeterLed1.Visible = false;
this.vuMeterLed2.Visible = false;
this.label2.Visible = false;
this.label4.Visible = false;
this.txtFilename.Visible = true;
this.btnCue.Visible = true;
this.btnStart.Visible = true;
this.btnStop.Visible = true;
this.label1.Visible = true;
}
}
}
private bool audioSampling = false;
private bool AudioSampling
{
get { return this.audioSampling; }
set
{
this.audioSampling = value;
if(value)
{
this.timer1.Start();
this.timer1running = true;
this.capture.EnableEvent(audioHandler);
this.button1.Text = "Stop VuMeter";
}
else
{
this.capture.DisableEvent(audioHandler);
this.timer1running = false;
this.timer1.Stop();
this.button1.Text = "Start VuMeter";
}
}
}
private void button1_Click(object sender, System.EventArgs e)
{
if((this.capture != null)&&(this.capture.AllowSampleGrabber))
{
this.AudioSampling = !this.AudioSampling;
}
else
{
this.AudioSampling = false;
this.SampleGrabber = false;
}
}
private void menuSampleGrabber1_Click(object sender, System.EventArgs e)
{
if(this.SampleGrabber)
{
this.SampleGrabber = false;
}
else
{
this.SampleGrabber = true;
}
}
private void menuAllowSampleGrabber1_Click(object sender, System.EventArgs e)
{
// Set flag, if set, then after reselection of audio or video device,
// the SampleGrabber shows up in or disappears from the menu.
this.menuAllowSampleGrabber1.Checked = !this.menuAllowSampleGrabber1.Checked;
}
捕获的音频通过事件传递给主程序。程序分析音频,假定为 16 位立体声音频。为了节省 CPU 时间,将分析总音频数据的一部分。
VU 表将显示左右音频通道的平均音频电平和峰值电平。使用定时器来显示 VU 表电平,以使音频电平较少依赖于抓取。可以使用事件来显示 VU 表电平,但是,在分析捕获的音频后绘制 VU 表电平可能会消耗过多的 CPU 时间。16 位音频的音频电平可以在 0 到 32767 之间变化,因此动态范围为 90 dB:20 x 10log(32767) = 90.31 dB。这个 VU 表可以分十五步显示峰值和音频电平。通常,VU 表以 3 dB 的步长显示音频,因此可以显示动态范围的一部分。由于较低的电平不那么有趣,我选择只显示较高的电平。尽管如此,VU 表的动态范围约为 40 dB,与模拟 VU 表相比,这相当大。因此,显示音频电平的实际公式可以是:((20 x 10log(音频电平)) - 40) / 3 得到 15 个正值来显示。我将此公式简化为 (6 x 10log(音频电平)) - 12。
private DirectX.Capture.Capture.AudioFrame audioHandler =
new DirectX.Capture.Capture.AudioFrame(AudioCapture);
const int MAXSAMPLES = 250; // Number of samples to be checked
static int volumePeakR = 0;
static int volumePeakL = 0;
static int volumeAvgR = 0;
static int volumeAvgL = 0;
static bool volumeInfoBusy = false;
/// <summary>
/// Analyse audio information in the buffer.
/// The code works for 16 bit, stereo audio.
/// </summary>
/// <param name="e"></param>
/// <param name="BufferLen"></param>
private static void AudioCapture(short[] e, int BufferLen)
{
if((BufferLen <= 0)||(volumeInfoBusy))
{
return;
}
volumeInfoBusy = true;
int leftS = 0;
int rightS = 0;
int avgR = 0;
int avgL = 0;
int peakR = 0;
int peakL = 0;
int size = e.GetLength(0)/ 2; // Assume this is 2 channel audio
if(size >(BufferLen / 2))
{
size = BufferLen / 2;
}
if(size > MAXSAMPLES)
{
size = MAXSAMPLES;
}
if(size < 2)
{
volumeInfoBusy = false;
return;
}
// Check array contents
for(int i = 0; i < size; i += 2)
{
leftS = Math.Abs(e[i]);
avgL += leftS;
if(leftS > peakL)
{
peakL = leftS;
}
rightS = Math.Abs(e[i + 1]);
avgR += rightS;
if(rightS > peakR)
{
peakR = rightS;
}
} // for
volumeAvgR = avgR / size;
volumeAvgL = avgL / size;
volumePeakR = peakR;
volumePeakL = peakL;
}
bool timer1running = false;
const int DbMultiplier = 6;
const int DbOffset = 12;
private void timer1_Tick(object sender, System.EventArgs e)
{
if(this.timer1running)
{
if(this.capture == null)
{
return;
}
int avgR, avgL, peakR, peakL;
if(volumeInfoBusy)
{
avgR = (int)((Math.Log10(volumeAvgR)*
DbMultiplier)- DbOffset);
avgL = (int)((Math.Log10(volumeAvgL)*
DbMultiplier)- DbOffset);
peakR = (int)((Math.Log10(volumePeakR)*
DbMultiplier)- DbOffset);
peakL = (int)((Math.Log10(volumePeakL)*
DbMultiplier)- DbOffset);
#if DEBUG
Debug.WriteLine("L="+ avgL.ToString() + " "+
peakL.ToString() + " R=" + avgR.ToString() +
" " + peakR.ToString());
#endif
this.vuMeterLed1.Peak = peakL;
this.vuMeterLed1.Volume = avgL;
this.vuMeterLed2.Peak = peakR;
this.vuMeterLed2.Volume = avgR;
volumeInfoBusy = false;
}
}
}
关注点
与之前的文章(DirectX.Capture 类的音频文件保存、DirectX.Capture 类库中 Windows Media 视频格式的视频文件保存 和 DirectShow:使用 C# 中的 IKsPropertySet 进行电视微调)相比,大多数功能都保留了下来,并添加了 SampleGrabber
功能。
此代码示例已使用 Visual Studio 2003 和 Visual Studio 2005 进行了测试。这两个编译器版本之间可能会发生冲突。我添加了条件 VS2003
以显示代码差异。一个不同之处在于,在 Visual Studio 2003 中,信号名为 Closed
,而在 Visual Studio 2005 中,此信号名为 FormClosed
。此代码示例有两个版本:Visual Studio 2003 版本使用 DShowNET 作为 DirectShow 接口库,Visual Studio 2005 版本使用 DirectShowLib-2005 作为 DirectShow 接口库。仍然可以使用 DShowNET 和 Visual Studio 2005,但我没有测试过。如果需要同时使用两个 Visual Studio 版本,则为此代码示例使用不同的目录以防止构建问题。我无意解决可能在多个 Visual Studio 版本之间发生的编码冲突和构建问题。
本文附带的 DirectX.Capture
示例包含新的解决方案,以提高稳定性。尽管如此,异常仍然可能发生,但大多数可以通过重新设计此代码示例或以更适当的方式捕获和处理异常来解决。请记住,此代码示例仅用于学习目的。它教您如何在 C# 中使用 DirectShow 并教您使用 GUI。发生的异常不应被视为问题,而应被视为挑战!异常的主要优点是它会告诉您何时出错。作为副作用,程序会失败,通过调试,您可以更轻松地找到问题的原因,因为您知道从何处开始。
反馈和改进
我希望此代码能帮助您理解 DirectX.Capture
类的结构。我也希望我为您提供了一个可能对您有用的增强功能。欢迎发表任何评论和问题。
历史
- 2007 年 8 月 10 日:最初编写了用于抓取帧的
SampleGrabber
代码示例 - 2009 年 4 月 1 日:在新文章中创建了用于抓取帧的
SampleGrabber
示例 - 2009 年 5 月 8 日:更新 - 添加了通过 VU 表显示音频电平的代码示例