教程:使用 WMContainer 对象编写 WMA 文件

本教程演示如何通过从 (.wav) 解压缩的音频文件中提取媒体内容,然后将其压缩为 ASF 格式,编写新的音频文件 (.wma) 。 用于转换的编码模式是 常量比特率编码 (CBR) 。 在此模式下,在编码会话之前,应用程序会指定编码器必须达到的目标比特率。

在本教程中,你将创建一个控制台应用程序,该应用程序将输入和输出文件名作为参数。 应用程序从随本教程提供的波形文件分析应用程序获取未压缩的媒体示例。 这些示例将发送到编码器以转换为 Windows Media Audio 9 格式。 编码器配置为 CBR 编码,并使用媒体类型协商期间提供的第一个比特率作为目标比特率。 编码的示例将发送到多路复用器,以便以 ASF 数据格式进行数据包化。 这些数据包将写入表示 ASF 数据对象的字节流。 数据部分准备就绪后,你将创建一个 ASF 音频文件,并写入合并所有标头信息的新 ASF 标头对象,然后追加 ASF 数据对象字节流。

本教程包含以下部分:

先决条件

本教程的假设条件如下:

  • 你熟悉 ASF 文件的结构以及 Media Foundation 为使用 ASF 对象而提供的组件。 这些组件包括 ContentInfo、拆分器、多路复用器和配置文件对象。 有关详细信息,请参阅 WMContainer ASF 组件
  • 你熟悉 Windows Media 编码器和各种编码类型,尤其是 CBR。 有关详细信息,请参阅 Windows Media Encoders
  • 你熟悉 媒体缓冲区 和字节流:具体而言,使用字节流的文件操作,并将媒体缓冲区的内容写入字节流。

术语

本教程使用以下术语:

  • 源媒体类型:媒体类型对象,公开 IMFMediaType 接口,用于描述输入文件的内容。
  • 音频配置文件:配置文件对象公开 IMFASFProfile 接口,该接口仅包含输出文件的音频流。
  • 流示例:媒体示例,公开 IMFSample 接口,表示输入文件的媒体数据从处于压缩状态的编码器获取。
  • ContentInfo 对象: ASF ContentInfo 对象公开 IMFASFContentInfo 接口,该接口表示输出文件的 ASF 标头对象。
  • 数据字节流:字节流对象公开 IMFByteStream 接口,该接口表示输出文件的整个 ASF 数据对象部分。
  • 数据包:媒体示例,公开由 ASF 多路复用器生成的 IMFSample 接口;表示将写入数据字节流的 ASF 数据包。
  • 输出字节流:字节流对象公开 IMFByteStream 接口,其中包含输出文件的内容。

1. 设置项目

  1. 在源文件中包含以下标头:

    #include <new>
    #include <stdio.h>       // Standard I/O
    #include <windows.h>     // Windows headers
    #include <mfapi.h>       // Media Foundation platform
    #include <wmcontainer.h> // ASF interfaces
    #include <mferror.h>     // Media Foundation error codes
    
  2. 链接到以下库文件:

    • mfplat.lib
    • mf.lib
    • mfuuid.lib
  3. 声明 SafeRelease 函数。

  4. 在项目中包括 CWmaEncoder 类。 有关此类的完整源代码,请参阅 编码器示例代码

2. 声明帮助程序函数

本教程使用以下帮助程序函数从字节流读取和写入。

  • AppendToByteStream:将一个字节流的内容追加到另一个字节流。
  • WriteBufferToByteStream:将数据从媒体缓冲区写入字节流。

有关详细信息,请参阅 IMFByteStream::Write。 以下代码演示以下帮助程序函数:

//-------------------------------------------------------------------
// AppendToByteStream
//
// Reads the contents of pSrc and writes them to pDest.
//-------------------------------------------------------------------

HRESULT AppendToByteStream(IMFByteStream *pSrc, IMFByteStream *pDest)
{
    HRESULT hr = S_OK;

    const DWORD READ_SIZE = 1024;

    BYTE buffer[READ_SIZE];

    while (1)
    {
        ULONG cbRead;

        hr = pSrc->Read(buffer, READ_SIZE, &cbRead);

        if (FAILED(hr)) { break; }

        if (cbRead == 0)
        {
            break;
        }

        hr = pDest->Write(buffer, cbRead, &cbRead);

        if (FAILED(hr)) { break; }
    }

    return hr;
}
//-------------------------------------------------------------------
// WriteBufferToByteStream
//
// Writes data from a media buffer to a byte stream.
//-------------------------------------------------------------------

HRESULT WriteBufferToByteStream(
    IMFByteStream *pStream,   // Pointer to the byte stream.
    IMFMediaBuffer *pBuffer,  // Pointer to the media buffer.
    DWORD *pcbWritten         // Receives the number of bytes written.
    )
{
    HRESULT hr = S_OK;
    DWORD cbData = 0;
    DWORD cbWritten = 0;
    BYTE *pMem = NULL;

    hr = pBuffer->Lock(&pMem, NULL, &cbData);

    if (SUCCEEDED(hr))
    {
        hr = pStream->Write(pMem, cbData, &cbWritten);
    }

    if (SUCCEEDED(hr))
    {
        if (pcbWritten)
        {
            *pcbWritten = cbWritten;
        }
    }

    if (pMem)
    {
        pBuffer->Unlock();
    }
    return hr;
}

3.打开音频文件

本教程假定应用程序将生成用于编码的未压缩音频。 为此,本教程中声明了两个函数:

HRESULT OpenAudioFile(PCWSTR pszURL, IMFMediaType **ppAudioFormat);
HRESULT GetNextAudioSample(BOOL *pbEOS, IMFSample **ppSample);

这些函数的实现留给读取器。

  • 函数 OpenAudioFile 应打开 pszURL 指定的媒体文件,并返回指向描述音频流的媒体类型的指针。
  • 函数 GetNextAudioSample 应从由 OpenAudioFile打开的文件读取未压缩的 PCM 音频。 到达文件末尾时, pbEOS 将接收值 TRUE。 否则, ppSample 接收包含音频缓冲区的媒体示例。

4.配置编码器

接下来,创建编码器,将其配置为生成 CBR 编码的流示例,并协商输入和输出媒体类型。

在 Media Foundation 中,编码器 (公开 IMFTransform 接口) 实现为 媒体基础转换 (MFT) 。

在本教程中,编码器是在为 MFT 提供包装器的 类中 CWmaEncoder 实现的。 有关此类的完整源代码,请参阅 编码器示例代码

注意

可以选择将编码类型指定为 CBR。 默认情况下,编码器配置为使用 CBR 编码。 有关详细信息,请参阅 常量比特率编码。 可以根据编码类型设置其他属性,有关特定于编码模式的属性的信息,请参阅 基于质量的可变比特率编码无约束可变比特率编码峰值约束可变比特率编码

 

    CWmaEncoder* pEncoder = NULL; //Pointer to the Encoder object.

    hr = OpenAudioFile(sInputFileName, &pInputType);
    if (FAILED(hr))
    {
        goto done;
    }

    // Initialize the WMA encoder wrapper.

    pEncoder = new (std::nothrow) CWmaEncoder();
    if (pEncoder == NULL)
    {
        hr = E_OUTOFMEMORY;
        goto done;
    }

    hr = pEncoder->Initialize();
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->SetEncodingType(EncodeMode_CBR);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->SetInputType(pInputType);
    if (FAILED(hr))
    {
        goto done;
    }

5. 创建 ASF ContentInfo 对象。

ASF ContentInfo 对象包含有关输出文件的各种标头对象的信息。

首先,为音频流创建 ASF 配置文件:

  1. 调用 MFCreateASFProfile 以创建空的 ASF 配置文件对象。 ASF 配置文件公开 IMFASFProfile 接口。 有关详细信息,请参阅 创建和配置 ASF 流
  2. 从 对象获取编码的 CWmaEncoder 音频格式。
  3. 调用 IMFASFProfile::CreateStream 为 ASF 配置文件创建新流。 传入指向 IMFMediaType 接口的指针,该接口表示流格式。
  4. 调用 IMFASFStreamConfig::SetStreamNumber 以分配流标识符。
  5. 通过设置流对象上的 MF_ASFSTREAMCONFIG_LEAKYBUCKET1 属性来设置“泄漏存储桶”参数。
  6. 调用 IMFASFProfile::SetStream 将新流添加到配置文件。

现在,按如下所示创建 ASF ContentInfo 对象:

  1. 调用 MFCreateASFContentInfo 创建一个空的 ContentInfo 对象。
  2. 调用 IMFASFContentInfo::SetProfile 以设置 ASF 配置文件。

以下代码演示了这些步骤:

HRESULT CreateASFContentInfo(
    CWmaEncoder* pEncoder,
    IMFASFContentInfo** ppContentInfo
    )
{
    HRESULT hr = S_OK;
    
    IMFASFProfile* pProfile = NULL;
    IMFMediaType* pMediaType = NULL;
    IMFASFStreamConfig* pStream = NULL;
    IMFASFContentInfo* pContentInfo = NULL;

    // Create the ASF profile object.

    hr = MFCreateASFProfile(&pProfile); 
    if (FAILED(hr))
    {
        goto done;
    }

    // Create a stream description for the encoded audio.

    hr = pEncoder->GetOutputType(&pMediaType); 
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pProfile->CreateStream(pMediaType, &pStream); 
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pStream->SetStreamNumber(DEFAULT_STREAM_NUMBER); 
    if (FAILED(hr))
    {
        goto done;
    }

    // Set "leaky bucket" values.

    LeakyBucket bucket;

    hr = pEncoder->GetLeakyBucket1(&bucket);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pStream->SetBlob(
        MF_ASFSTREAMCONFIG_LEAKYBUCKET1, 
        (UINT8*)&bucket, 
        sizeof(bucket)
        );

    if (FAILED(hr))
    {
        goto done;
    }

    //Add the stream to the profile

    hr = pProfile->SetStream(pStream);
    if (FAILED(hr))
    {
        goto done;
    }

    // Create the ASF ContentInfo object.

    hr = MFCreateASFContentInfo(&pContentInfo); 
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pContentInfo->SetProfile(pProfile); 
    if (FAILED(hr))
    {
        goto done;
    }

    // Return the pointer to the caller.

    *ppContentInfo = pContentInfo;
    (*ppContentInfo)->AddRef();

done:
    SafeRelease(&pProfile);
    SafeRelease(&pStream);
    SafeRelease(&pMediaType);
    SafeRelease(&pContentInfo);
    return hr;
}

6. 创建 ASF 多路复用器

ASF 多路复用器生成 ASF 数据包。

  1. 调用 MFCreateASFMultiplexer 来创建 ASF 多路复用器。
  2. 调用 IMFASFMultiplexer::Initialize 以初始化多路复用器。 传入指向 ASF Content Info 对象的指针,该对象是在上一节中创建的。
  3. 调用 IMFASFMultiplexer::SetFlags 以设置 MFASF_MULTIPLEXER_AUTOADJUST_BITRATE 标志。 使用此设置时,多路复用器会自动调整 ASF 内容的比特率,以匹配多路复用流的特征。
HRESULT CreateASFMux( 
    IMFASFContentInfo* pContentInfo,
    IMFASFMultiplexer** ppMultiplexer
    )
{
    HRESULT hr = S_OK;
    
    IMFMediaType* pMediaType = NULL;
    IMFASFMultiplexer *pMultiplexer = NULL;

    // Create and initialize the ASF Multiplexer object.

    hr = MFCreateASFMultiplexer(&pMultiplexer);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pMultiplexer->Initialize(pContentInfo);
    if (FAILED(hr))
    {
        goto done;
    }

    // Enable automatic bit-rate adjustment.

    hr = pMultiplexer->SetFlags(MFASF_MULTIPLEXER_AUTOADJUST_BITRATE);
    if (FAILED(hr))
    {
        goto done;
    }

    *ppMultiplexer = pMultiplexer;
    (*ppMultiplexer)->AddRef();

done:
    SafeRelease(&pMultiplexer);
    return hr;
}

7. 生成新的 ASF 数据包

接下来,为新文件生成 ASF 数据包。 这些数据包将构成新文件的最终 ASF 数据对象。 生成新的 ASF 数据包:

  1. 调用 MFCreateTempFile 以创建临时字节流来保存 ASF 数据包。
  2. 调用应用程序定义的 GetNextAudioSample 函数以获取编码器的未压缩音频数据。
  3. 将未压缩的音频传递到编码器进行压缩。 有关详细信息,请参阅 处理编码器中的数据
  4. 调用 IMFASFMultiplexer::P rocessSample 将压缩的音频样本发送到 ASF 多路复用器进行数据包化。
  5. 从多路复用器获取 ASF 数据包并将其写入临时字节流。 有关详细信息,请参阅 生成新的 ASF 数据包
  6. 到达源流的末尾时,请清空编码器,并从编码器拉取剩余的压缩样本。 有关排出 MFT 的详细信息,请参阅 基本 MFT 处理模型
  7. 将所有样本发送到多路复用器后,调用 IMFASFMultiplexer::Flush 并从多路复用器拉取剩余的 ASF 数据包。
  8. 调用 IMFASFMultiplexer::End

以下代码生成 ASF 数据包。 函数返回指向包含 ASF 数据对象的字节流的指针。

HRESULT EncodeData(
    CWmaEncoder* pEncoder, 
    IMFASFContentInfo* pContentInfo,
    IMFASFMultiplexer* pMux, 
    IMFByteStream** ppDataStream) 
{
    HRESULT hr = S_OK;

    IMFByteStream* pStream = NULL;
    IMFSample* pInputSample = NULL;
    IMFSample* pWmaSample = NULL;

    BOOL bEOF = FALSE;

   // Create a temporary file to hold the data stream.
   hr = MFCreateTempFile(
        MF_ACCESSMODE_READWRITE, 
        MF_OPENMODE_DELETE_IF_EXIST,
        MF_FILEFLAGS_NONE,
        &pStream
        );

   if (FAILED(hr))
   {
       goto done;
   }

    BOOL bNeedInput = TRUE;

    while (TRUE)
    {
        if (bNeedInput)
        {
            hr = GetNextAudioSample(&bEOF, &pInputSample);
            if (FAILED(hr))
            {
                goto done;
            }

            if (bEOF)
            {
                // Reached the end of the input file.
                break;
            }

            // Encode the uncompressed audio sample.
            hr = pEncoder->ProcessInput(pInputSample);
            if (FAILED(hr))
            {
                goto done;
            }

            bNeedInput = FALSE;
        }

        if (bNeedInput == FALSE)
        {
            // Get data from the encoder.

            hr = pEncoder->ProcessOutput(&pWmaSample);
            if (FAILED(hr))
            {
                goto done;
            }

            // pWmaSample can be NULL if the encoder needs more input.

            if (pWmaSample)
            {
                hr = pMux->ProcessSample(DEFAULT_STREAM_NUMBER, pWmaSample, 0);
                if (FAILED(hr))
                {
                    goto done;
                }

                //Collect the data packets and write them to a stream
                hr = GenerateASFDataPackets(pMux, pStream);
                if (FAILED(hr))
                {
                    goto done;
                }
            }
            else
            {
                bNeedInput = TRUE;
            }
        }
        
        SafeRelease(&pInputSample);
        SafeRelease(&pWmaSample);
    }

    // Drain the MFT and pull any remaining samples from the encoder.

    hr = pEncoder->Drain();
    if (FAILED(hr))
    {
        goto done;
    }

    while (TRUE)
    {
        hr = pEncoder->ProcessOutput(&pWmaSample);
        if (FAILED(hr))
        {
            goto done;
        }

        if (pWmaSample == NULL)
        {
            break;
        }

        hr = pMux->ProcessSample(DEFAULT_STREAM_NUMBER, pWmaSample, 0);
        if (FAILED(hr))
        {
            goto done;
        }

        //Collect the data packets and write them to a stream
        hr = GenerateASFDataPackets(pMux, pStream);
        if (FAILED(hr))
        {
            goto done;
        }

        SafeRelease(&pWmaSample);
    }

    // Flush the mux and get any pending ASF data packets.
    hr = pMux->Flush();
    if (FAILED(hr))
    {
        goto done;
    }

    hr = GenerateASFDataPackets(pMux, pStream);
    if (FAILED(hr))
    {
        goto done;
    }
    
    // Update the ContentInfo object
    hr = pMux->End(pContentInfo);
    if (FAILED(hr))
    {
        goto done;
    }

    //Return stream to the caller that contains the ASF encoded data.
    *ppDataStream = pStream;
    (*ppDataStream)->AddRef();

done:
    SafeRelease(&pStream);
    SafeRelease(&pInputSample);
    SafeRelease(&pWmaSample);
    return hr;
}

函数的代码 GenerateASFDataPackets 显示在 生成新的 ASF 数据包的主题中。

8. 编写 ASF 文件

接下来,通过调用 IMFASFContentInfo::GenerateHeader 将 ASF 标头写入媒体缓冲区。 此方法将存储在 ContentInfo 对象中的数据转换为 ASF 标头对象格式的二进制数据。 有关详细信息,请参阅 生成新的 ASF 标头对象

生成新的 ASF 标头对象后,为输出文件创建字节流。 首先将 Header 对象写入输出字节流。 遵循标头对象,其中包含数据字节流中包含的数据对象。

HRESULT WriteASFFile( 
     IMFASFContentInfo *pContentInfo, 
     IMFByteStream *pDataStream,
     PCWSTR pszFile
     )
{
    HRESULT hr = S_OK;
    
    IMFMediaBuffer* pHeaderBuffer = NULL;
    IMFByteStream* pWmaStream = NULL;

    DWORD cbHeaderSize = 0;
    DWORD cbWritten = 0;

    //Create output file
    hr = MFCreateFile(MF_ACCESSMODE_WRITE, MF_OPENMODE_DELETE_IF_EXIST,
        MF_FILEFLAGS_NONE, pszFile, &pWmaStream);

    if (FAILED(hr))
    {
        goto done;
    }


    // Get the size of the ASF Header Object.
    hr = pContentInfo->GenerateHeader (NULL, &cbHeaderSize);
    if (FAILED(hr))
    {
        goto done;
    }

    // Create a media buffer.
    hr = MFCreateMemoryBuffer(cbHeaderSize, &pHeaderBuffer);
    if (FAILED(hr))
    {
        goto done;
    }

    // Populate the media buffer with the ASF Header Object.
    hr = pContentInfo->GenerateHeader(pHeaderBuffer, &cbHeaderSize);
    if (FAILED(hr))
    {
        goto done;
    }

    // Write the ASF header to the output file.
    hr = WriteBufferToByteStream(pWmaStream, pHeaderBuffer, &cbWritten);
    if (FAILED(hr))
    {
        goto done;
    }

    // Append the data stream to the file.

    hr = pDataStream->SetCurrentPosition(0);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = AppendToByteStream(pDataStream, pWmaStream);

done:
    SafeRelease(&pHeaderBuffer);
    SafeRelease(&pWmaStream);
    return hr;
}

9. 定义 Entry-Point 函数

现在,可以将前面的步骤一起放入完整的应用程序中。 在使用任何 Media Foundation 对象之前,请通过调用 MFStartup 初始化 Media Foundation 平台。 完成后,调用 MFShutdown。 有关详细信息,请参阅 初始化媒体基础

以下代码显示了完整的控制台应用程序。 命令行参数指定要转换的文件的名称和新音频文件的名称。

int wmain(int argc, WCHAR* argv[])
{
    HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

    if (argc != 3)
    {
        wprintf_s(L"Usage: %s input.wmv, %s output.wma");
        return 0;
    }

    const WCHAR* sInputFileName = argv[1];    // Source file name
    const WCHAR* sOutputFileName = argv[2];  // Output file name
    
    IMFMediaType* pInputType = NULL;
    IMFASFContentInfo* pContentInfo = NULL;
    IMFASFMultiplexer* pMux = NULL;
    IMFByteStream* pDataStream = NULL;

    CWmaEncoder* pEncoder = NULL; //Pointer to the Encoder object.

    HRESULT hr = CoInitializeEx(
        NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

    if (FAILED(hr))
    {
        goto done;
    }

    hr = MFStartup(MF_VERSION);
    if (FAILED(hr))
    {
        goto done;
    }

    CWmaEncoder* pEncoder = NULL; //Pointer to the Encoder object.

    hr = OpenAudioFile(sInputFileName, &pInputType);
    if (FAILED(hr))
    {
        goto done;
    }

    // Initialize the WMA encoder wrapper.

    pEncoder = new (std::nothrow) CWmaEncoder();
    if (pEncoder == NULL)
    {
        hr = E_OUTOFMEMORY;
        goto done;
    }

    hr = pEncoder->Initialize();
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->SetEncodingType(EncodeMode_CBR);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pEncoder->SetInputType(pInputType);
    if (FAILED(hr))
    {
        goto done;
    }

    // Create the WMContainer objects.
    hr = CreateASFContentInfo(pEncoder, &pContentInfo);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = CreateASFMux(pContentInfo, &pMux);
    if (FAILED(hr))
    {
        goto done;
    }

    // Convert uncompressed data to ASF format.
    hr = EncodeData(pEncoder, pContentInfo, pMux, &pDataStream);
    if (FAILED(hr))
    {
        goto done;
    }

    // Write the ASF objects to the output file.
    hr = WriteASFFile(pContentInfo, pDataStream, sOutputFileName);

done:
    SafeRelease(&pInputType);
    SafeRelease(&pContentInfo);
    SafeRelease(&pMux);
    SafeRelease(&pDataStream);
    delete pEncoder;

    MFShutdown();
    CoUninitialize();

    if (FAILED(hr))
    {
        wprintf_s(L"Error: 0x%X\n", hr);
    }

    return 0;
} 

WMContainer ASF 组件

媒体基础中的 ASF 支持