65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 DirectShow 从 MPEG 视频文件捕获音频到 WAV 文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (14投票s)

2004年12月29日

9分钟阅读

viewsIcon

166058

downloadIcon

4708

本文介绍如何使用 DirectShow 过滤器将电影文件(.mpeg, .mpg, .avi 和 .dat)的音频数据存储到硬盘上的 WAV 文件中。

Sample Image - Audio5.jpg

引言

本文主要关注以下 DirectShow 过滤器

  • MPEG-1 流分割过滤器。
  • MPEG 音频解码器过滤器。
  • Wav Dest 过滤器以及
  • 文件写入器过滤器。

MPEG-1 流分割过滤器

此过滤器将 MPEG-1 系统流分割为其组件音频流和视频流。

MPEG-1 音频解码器过滤器

将 MPEG-1 Layer I 和 Layer II 音频解码为 PCM。

Wav Dest 过滤器

Wav Dest 过滤器将音频流写入 WAV 文件。它接收单个音频流作为输入,并且其输出引脚必须连接到文件写入器过滤器。

文件写入器过滤器

文件写入器过滤器可以用于将文件写入磁盘,而不管其格式。该过滤器仅将它接收到的任何数据写入磁盘,因此它必须连接到上游的多路复用器,该多路复用器可以正确格式化文件。

工作原理

Figure 1 Screenshot of the desired Filter Graph

图 1:所需过滤器图形的屏幕截图。

图 1 显示了以正确顺序连接所有所需过滤器的过滤器图形。

注意IGraphBuilderRenderFile 方法会自行创建 File Source (Async.) Filter 并将其添加到图形中。

为了实现此功能,首先必须创建这些过滤器,然后将它们添加到图形中。

CMpeg2Wav 类的 Mpeg2WavConvertor 方法

Mpeg2WavConvertor”方法(它是 ATL 中开发的“CMpeg2Wav”类的一个方法,并存储在“MpegToWav.dll”中)是启动将音频数据从提供的视频文件保存到 WAV 文件的整个过程的方法。它首先创建所有必需的过滤器,将它们添加到图形中,然后执行图形。

注意:您将在 CMpeg2Wav 的两个方法中找到所有必需的 DirectShow 工作:Mpeg2WavConvertorhandleEvent(稍后讨论)。

STDMETHODIMP CMpeg2Wav::Mpeg2WavConvertor(LPCWSTR file, HWND g_hwnd, LPCWSTR waveFile)
{
    if(g_hwnd == NULL)
    {
        MessageBox(NULL, "FAILED TO CREATE THE HANDLE" 
                   " OF THE WINDOW", NULL, NULL);
        exit(0);
    }
    //creating capture graph
    hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, 
         CLSCTX_INPROC,  IID_ICaptureGraphBuilder2, (void **)&m_pCapture);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Unable to create capture graph", NULL, NULL);
    }
    //creating graph builder
    hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, 
         IID_IGraphBuilder, (void **)&pGraphBuilder);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create Graph Builder", NULL, NULL);
    }
 
    hr = m_pCapture->SetFiltergraph(pGraphBuilder);  
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed", NULL, NULL);
    }
    pGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&pMC);
    pGraphBuilder->QueryInterface(IID_IMediaEventEx, (void **)&pEvent);
    hr = CoCreateInstance(CLSID_MPEG1Splitter, NULL, CLSCTX_INPROC,
                          IID_IBaseFilter, (void**)&pSplitter);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create MpegSplitter Filter", NULL, NULL);
    }
    //creating Audio Decoder
    static const GUID CLSID_MPEG_Audio_Decoder = 
        { 0x4a2286e0, 0x7bef, 0x11ce, 
        { 0x9b, 0xd9, 0x0, 0x0, 0xe2, 0x02, 0x59, 0x9c } };
    hr = CoCreateInstance(CLSID_MPEG_Audio_Decoder, NULL, 
         CLSCTX_INPROC, IID_IBaseFilter, (void**)&pDecoder);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create MpegAudioDecoder Filter", 
                   NULL, NULL);
    }
    //creating Video Decoder
    static const GUID CLSID_MPEG_Video_Decoder = 
        {0xfeb50740 , 0x7bef, 0x11ce, 
        {0x9b , 0xd9, 0x0, 0x0, 0xe2, 0x2, 0x59,  0x9c} };
    hr = CoCreateInstance(CLSID_MPEG_Video_Decoder, 
         NULL, CLSCTX_INPROC, IID_IBaseFilter, (void**)&pVideoDecoder);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create MpegVideoDecoder Filter", NULL, NULL);
    }
    //creating Video Renderer for windows XP
    static const GUID CLSID_MPEG_Video_Renderer = 
        {0x6BC1CFFA , 0x8FC1, 0x4261, 
        { 0xAC, 0x22,0xCF , 0xB4, 0xCC, 0x38, 0xDB,  0x50} };
    hr = CoCreateInstance(CLSID_MPEG_Video_Renderer, NULL, 
         CLSCTX_INPROC, IID_IBaseFilter, (void**)&pVideoRenderer);
    if(FAILED(hr))
    {
        //creating Video Renderer for windows 2000
        static const GUID CLSID_Video_Renderer = 
               {0x70E102B0 , 0x5556, 0x11CE, 
               { 0x97, 0xC0, 0x00 , 0xAA, 0x00, 0x55, 0x59,  0x5A} };
        hr = CoCreateInstance(CLSID_Video_Renderer, NULL, 
             CLSCTX_INPROC, IID_IBaseFilter, (void**)&pVideoRenderer);

        if (FAILED(hr))
        {
            MessageBox(NULL, "Still Error!", "Error", MB_OK);
        }
     }

    //creating wavedest filter
    static const GUID CLSID_WavDest = 
           { 0x3c78b8e2, 0x6c4d, 0x11d1, 
           { 0xad, 0xe2, 0x0, 0x0, 0xf8, 0x75, 0x4b, 0x99 } };
    hr = CoCreateInstance(CLSID_WavDest, NULL, 
         CLSCTX_INPROC,  IID_IBaseFilter, (void **)&pWavDest);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create WaveDest Filter", NULL, NULL);
    }
    //Creating FileSink2
    hr = CoCreateInstance(CLSID_FileWriter, NULL, CLSCTX_INPROC,
                          IID_IFileSinkFilter2, (void **)&pFileSink);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Create FileSink2 Filter", NULL, NULL);
    }
    // Get the file sink interface pointer from the File Writer
    hr = pFileSink->QueryInterface(IID_IBaseFilter, (void **)&pFileWriter);
    if(FAILED(hr))
    {
       MessageBox(NULL, "Unable to get the File Writer" 
                  " interface pointer from the File Sink", NULL, NULL);
    }
    hr = pFileSink->SetFileName(waveFile, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Couldnt create wav file", NULL, NULL);
    }
    //adding writer to the graph
    hr = pGraphBuilder->AddFilter((IBaseFilter *)pFileWriter, L"File Writer");
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add Writer To Graph", NULL, NULL);
    }
    //adding wavdest to the graph
    hr = pGraphBuilder->AddFilter(pWavDest, L"WAV DEST");
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add WavDest To Graph", NULL, NULL);
    }
    //adding audioDecoder to the graph
    hr = pGraphBuilder->AddFilter(pDecoder, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add audio decoder To Graph", NULL, NULL);
    }
    //adding video renderer to the graph
    hr = pGraphBuilder->AddFilter(pVideoRenderer, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add video renderer To Graph", NULL, NULL);
    }
    //adding video Decoder to the graph
    hr = pGraphBuilder->AddFilter(pVideoDecoder, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add video decoder To Graph", NULL, NULL);
    }
    //adding Mpeg Stream Splitter to the graph
    hr = pGraphBuilder->AddFilter(pSplitter, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to add MpegSplitter to graph", NULL, NULL);
    }
    hr = pFileSink->SetMode(AM_FILE_OVERWRITE);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to OverWrite", NULL, NULL);
    }
    hr = pGraphBuilder->RenderFile(file, NULL);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to RenderFile", NULL, NULL);
    }
    //Removing Video Renderer From the Graph
    hr = pGraphBuilder->RemoveFilter(pVideoRenderer);
    if(FAILED(hr))
    {
        MessageBox(NULL, "Couldn't remove the video" 
                         " rendere filter", NULL, NULL);
    }
    //Removing Video Decoder From the Graph
    hr = pGraphBuilder->RemoveFilter(pVideoDecoder);

    if(FAILED(hr))
    {
        MessageBox(NULL, "Couldn't remove the video decoder filter", NULL, NULL);
    }
    //Instructing the graph manager to send a windows message 
    //to the window of the calling application 
    //whenever an event occurs on the graph
    pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
    // Execute the graph
    hr = pMC->Run();
    if(FAILED(hr))
    {
        MessageBox(NULL, "Failed to Run", NULL, NULL);
    }
    return S_OK;
}

请注意,在 Mpeg2WavConvertor 方法中还创建并添加了另外两个过滤器:MPEG Video Decoder 和 Video Renderer。这是因为 IGraphBuilderRenderStream 方法(在添加所有过滤器以智能连接所有过滤器之后调用)也会创建并添加这两个方法。

Figure 2 MPEG Video Decoder and MPEG Video Renderer created 
and added to the graph automatically by RenderFile

现在,这不是我们想要的行为,因为这两个过滤器的效果将是一个显示所提供视频文件的视频的窗口,并且不会有音频,因为音频数据正在被复制到 WAV 文件中,而不是渲染到默认的 DirectSound 设备。为了解决这个问题,这两个过滤器(MPEG Video Decoder 和 Video Renderer)都被创建并添加到图形中,并且在调用 IGraphBuilderRenderFile 方法之后,通过调用 IGraphBuilderRemoveFilter 方法将它们从图形中移除,然后执行图形。

一点小技巧

还应注意,过滤器(File Writer, Wav Dest, Audio Decoder, MPEG-1 Stream Splitter)已按相反的顺序添加到图形中(File Writer > Wav Dest > Audio Decoder > MPEG-1 Stream Splitter),因为 IGraphBuilderRenderFile 方法(在所有过滤器添加到图形后调用)实际上使用它们各自的输入/输出引脚智能地以正确的顺序连接所有过滤器,并且此方法还会为提供的电影文件创建 File Source (Async.) Filter 并将其添加到图形中。这一点很重要,因为如果您按升序添加过滤器(MPEG-1 Stream Splitter > MPEG Audio Decoder > Wav Dest > File Writer)然后调用 RenderFile 方法,MPEG Audio decoder 将被绕过,并且不会连接到图形中的任何过滤器;相反,MPEG-1 Stream Splitter 的音频输出引脚将直接连接到 Wav Dest Filter 的输入引脚。

Figure 3The MPEG Audio Decoder Filter is bypassed and it is not connected with any filter in the graph

图 3:MPEG Audio Decoder Filter 被绕过,并且没有连接到图形中的任何过滤器。

这不是我们想要的,因为 MPEG-1 Stream Splitter 的音频输出引脚上的数据格式为“WaveFormatEx: 44.100 KHz, 0 位单声道”。如果我们(在 MPEG-1 Stream Splitter 和 Wav Dest 之间)将 MPEG Audio Decoder 添加到图形中,如下所示

Figure 4The MPEG Audio Filter is connected between MPEG-1 Stream Splitter and Wav Dest Filters

图 4:MPEG Audio Filter 连接在 MPEG-1 Stream Splitter 和 Wav Dest Filters 之间。

现在,Wav Dest 的输入引脚接收到的数据是“WaveFormatEx: 44.100 KHz, 16 位单声道”,这是 MPEG Audio Decoder 输出引脚上的数据,也是 Wav Dest 应该写入 File Writer 的期望数据。

调用应用程序

现在,我们来看一下将调用“Mpeg2WavConvertor”方法的应用程序。该应用程序是使用 Managed C++ 开发的。

Figure 5 Application's GUI

图 5 应用程序的 GUI

加载表单

在此应用程序的 Form1_Load 方法中(表单加载事件的句柄,在表单加载时引发),“Convert”按钮被禁用,以防止用户在未提供任何视频文件的情况下启动整个过程,这当然是行不通的,并且还将一个无符号字符设置为值“a”(该值稍后由应用程序使用,并在本教程的后面进行讨论)。

Figure 3The MPEG Audio Decoder Filter is bypassed 
and it is not connected with any filter in the graph

图 2RenderFile 自动创建并添加到图形中的 MPEG Video Decoder 和 MPEG Video Renderer。

private: unsigned char check;
 
private: System::Void Form1_Load(System::Object *  sender, System::EventArgs *  e)
{
    buttonConvert->Enabled = false;
    check = 'a';
}

“Select Movie”按钮

该应用程序通过单击“Select Movie”按钮要求用户选择要从中提取 WAV 音频的电影。

private: System::Void buttonSelectMpg_Click(System::Object *  sender, 
                                              System::EventArgs *  e)
{
    buttonConvert->Enabled = true;
    OpenFileDialog *fileDialog3 = new OpenFileDialog();
    fileDialog3->InitialDirectory = S"C:\\" ;
    fileDialog3->Filter = "Video Files (*.mpg; *.dat; *.avi;" 
          " *.mpeg; *.m1v;)|*.mpg; *.dat; *.mpeg;" 
          " *.avi; *.m1v; | All Files (*.*)|*.*";
    fileDialog3->RestoreDirectory = true;
    fileDialog3->ShowDialog();
    String* mpegVidFile;
    mpegVidFile = fileDialog3->get_FileName();
    if (mpegVidFile == NULL || mpegVidFile == String::Empty)
        return;
    mpegTextBox->Text = mpegVidFile;
}

在其按钮处理程序中,将创建一个“OpenFileDialog”,以便用户可以通过单击此按钮从硬盘上的所需位置选择他们选择的电影文件,并且文件名也会被保留,以便在文本框中显示,并且还可以提供给“Mpeg2WavConvertor”方法(在“Convert”按钮处理程序中调用),以便 IGraphBuilderRenderFile 方法为该特定视频文件创建 File Source (Async.) Filter。

mpegTextBox->Text = mpegVidFile;

“Change”按钮

用户还可以通过单击“Wav file”文本框前面的“Change”按钮来设置所需的 WAV 文件名。

private: System::Void buttonSaveWav_Click(System::Object *  sender, 
                                            System::EventArgs *  e)
{
    SaveFileDialog *saveFileDialog2;
    saveFileDialog2 = new SaveFileDialog();
    saveFileDialog2->InitialDirectory = S"C:\\" ;
    saveFileDialog2->Filter = S"Wav files (*.wav)|*.wav”;
    saveFileDialog2->RestoreDirectory = true;
    saveFileDialog2->ShowDialog();
    String* waveFile = saveFileDialog2->get_FileName();
    if (waveFile == NULL)
    {
        return;
    }

    wavTextBox->Text = waveFile;
}

在其按钮处理程序中,将创建一个“SaveFileDialog”,以便用户可以设置 WAV 文件的所需文件名,并且还可以设置将要保存此文件的所需位置。文件名会被保留,以便提供给“Mpeg2WavConvertor”方法,文件写入器过滤器将使用该方法来创建具有用户所需文件名的 WAV 文件。默认情况下,文件名是“C:\test.wav”。

wavTextBox->Text = waveFile;

“Convert”按钮

正是“Convert”按钮实际调用“CMpeg2Wav”类的“Mpeg2WavConvertor”方法来启动整个过程。

private: System::Void buttonConvert_Click(System::Object *  sender, 
                                            System::EventArgs *  e)
{
    buttonConvert->Enabled = false;
    // prevent the user from clicking when in process

    String* mpgFile = mpegTextBox->get_Text();
    if (mpgFile == NULL || mpgFile->Length == 0)
    {
        MessageBox(NULL, "No MPEG file selected", "Error", MB_OK);
        return;
    }
    Interop::MpegToWavLib::_RemotableHandle *pointer = 
           (Interop::MpegToWavLib::_RemotableHandle*)get_Handle().ToPointer();
    ptrMpeg2Wav = new Interop::MpegToWavLib::Mpeg2WavClass();
    ptrMpeg2Wav->Mpeg2WavConvertor(mpegTextBox->Text, pointer, wavTextBox->Text);
    pointer = NULL;
}

此按钮处理程序调用“Mpeg2WavConvertor”方法来启动操作。它提供所选视频文件的文件名(这有助于 IGraphBuilderRenderFile 方法为该特定视频创建并添加到图形中,并按前面所述连接图形中的过滤器)、WAV 文件的文件名(用于文件写入器)以及父窗口的句柄。

需要此句柄来指示过滤器图形管理器,每当发生新事件时发送 Windows 消息(在本例中为 WM_GRAPHNOTIFY)。

#define WM_GRAPHNOTIFY  WM_APP + 1
 
pGraphBuilder->QueryInterface(IID_IMediaEventEx, (void **)&pEvent);
 
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);

这有助于让应用程序了解图形执行期间发生的事件。在我们的例子中,我们只对 EC_COMPLETEEC_USERABORT 事件感兴趣。EC_COMPLETE 事件表示回放已正常完成,而 EC_USERABORT 事件表示用户中断了回放(当用户在过程中间关闭应用程序时)。

因此,当我们的应用程序收到 WM_GRAPHNOTIFY 消息时,它会调用“CMpeg2Wav”类的“handleEvent”方法。此消息的处理是通过覆盖调用应用程序中的“WndProc”方法来完成的。

覆盖 WndProc

protected:
void WndProc(Message* m) 
{
    switch (m->Msg) 
    {
      case WM_GRAPHNOTIFY:

          ptrMpeg2Wav->handleEvent(&check);
          if(check == 'z')
          {
              buttonConvert->Enabled = true;
              check = ‘a’;
          }

          break;
    }

    Form::WndProc(m);
}

每当调用“handleEvent”方法时(每当调用应用程序的父窗口收到 WM_GRAPHNOTIFY 消息时就会调用它),它会检查事件是“EC_COMPLETE”还是“EC_USERABORT”。如果找到其中任何一个,它将执行通用清理,并将 unsigned char* 的值更改为“z”(它作为 lone 参数传递给方法,并用作调用应用程序知道其中任一事件已发生的哨兵)。当调用应用程序看到提供的无符号字符的值更改为“z”时,它会启用“Convert”按钮,该按钮在用户单击它以开始整个过程时已被禁用(这是为了防止用户在将 WAV 音频保存到硬盘的整个过程中反复单击“Convert”按钮)。

CMpeg2Wav 类的 handleEvent 方法

STDMETHODIMP CMpeg2Wav::handleEvent(unsigned char *s)
{
    long evCode, param1, param2;
    while (hr = pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0), SUCCEEDED(hr))
    {
        hr = pEvent->FreeEventParams(evCode, param1, param2);
        if ((EC_COMPLETE == evCode) || (EC_USERABORT == evCode))
        {
            pMC->Stop();
            pEvent->SetNotifyWindow(NULL, 0, 0);
            hr = pGraphBuilder->RemoveFilter(pFileWriter);
            hr = pGraphBuilder->RemoveFilter(pWavDest);
            hr = pGraphBuilder->RemoveFilter(pDecoder);
            hr = pGraphBuilder->RemoveFilter(pSplitter);
            SAFE_RELEASE(pFileWriter);
            SAFE_RELEASE(pWavDest);
            SAFE_RELEASE(pDecoder);
            SAFE_RELEASE(pSplitter);
            SAFE_RELEASE(pVideoDecoder);
            SAFE_RELEASE(pVideoRenderer);
            SAFE_RELEASE(pGraphBuilder);
            SAFE_RELEASE(pEvent);
            SAFE_RELEASE(pMC);
            *s = 'z';
            break;
        }
    }
    return S_OK;
}

最后的几点提示

  • 要运行演示,您需要注册“Mpeg2Wav.dll”和“wavdest.ax”(Wav Dest Filter)。“wavdest.ax”也可以通过编译 DirectX SDK 中提供的 WavDest 示例来获得(驱动器名称:\DXSDK\Samples\C++\DirectShow\Filters\WavDest)。
  • 为了使源代码正常工作,您首先必须在您的机器上注册“wavdest.ax”。然后编译“MpegToWav”示例。这将创建“MpegToWav.dll”,然后打开 Mpeg2WavTestApplication 并将此新创建的“Mpeg2Wav.dll”添加到引用中,然后编译+运行该应用程序,您就可以从您的视频创建 WAV 文件了,我的朋友。
  • 创建提供的视频文件的音频的 WAV 文件的过程(如上所述)仅适用于 .mpg.mpeg.avi 等类型的电影文件,而不适用于 .wmv.asf 等流式视频文件类型。
  • 整个应用程序是使用 Microsoft Visual Studio .NET 2003 创建的,因此拥有旧版本 Visual Studio .NET 的读者如果想测试代码,则必须通过某个转换器将其转换为旧版本的 Visual Studio .NET。
© . All rights reserved.