共用方式為


教學課程:譯碼音訊

本教學課程示範如何使用 來源讀取器 將音訊從媒體檔案譯碼,並將音訊寫入 WAVE 檔案。 本教學課程是以 音訊剪輯 範例為基礎。

概述

在本教學課程中,您將建立採用兩個命令行自變數的控制台應用程式:包含音訊數據流的輸入檔名稱,以及輸出檔名。 應用程式會從輸入檔讀取五秒的音訊數據,並將音訊寫入輸出檔案作為WAVE數據。

若要取得譯碼的音訊數據,應用程式會使用來源讀取器物件。 來源讀取器公開了 IMFSourceReader 介面。 若要將譯碼的音訊寫入 WAVE 檔案,應用程式會使用 Windows I/O 函式。 以下圖片說明此過程。

圖表,顯示來源讀取器從來源檔案取得音訊數據。

以最簡單的形式,WAVE 檔案具有下列結構:

數據類型 大小 (位元組) 價值
FOURCC 4 'RIFF'
DWORD 4 檔案大小總計,不包括前8個字節
FOURCC 4 'WAVE'
FOURCC 4 'fmt '
DWORD 4 接在 WAVEFORMATEX 資料後面的數據大小。
WAVEFORMATEX 不同 音訊格式標頭。
FOURCC 4 資料
DWORD 4 音訊數據的大小。
BYTE[] 不同 音訊數據。

 

注意

FOURCC 是串連四個 ASCII 字元所形成的 DWORD

 

這個基本結構可以藉由新增檔案元數據和其他資訊來擴充,這已超出本教學課程的範圍。

標頭和程式庫檔案

在您的項目中包含下列頭檔:

#define WINVER _WIN32_WINNT_WIN7

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <stdio.h>
#include <mferror.h>

連結至下列程式庫:

  • mfplat.lib
  • mfreadwrite.lib
  • mfuuid.lib

實現 wmain 函數的功能

下列程式代碼顯示應用程式的進入點函式。

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

    if (argc != 3)
    {
        printf("arguments: input_file output_file.wav\n");
        return 1;
    }

    const WCHAR *wszSourceFile = argv[1];
    const WCHAR *wszTargetFile = argv[2];

    const LONG MAX_AUDIO_DURATION_MSEC = 5000; // 5 seconds

    HRESULT hr = S_OK;

    IMFSourceReader *pReader = NULL;
    HANDLE hFile = INVALID_HANDLE_VALUE;

    // Initialize the COM library.
    hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

    // Initialize the Media Foundation platform.
    if (SUCCEEDED(hr))
    {
        hr = MFStartup(MF_VERSION);
    }

    // Create the source reader to read the input file.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateSourceReaderFromURL(wszSourceFile, NULL, &pReader);
        if (FAILED(hr))
        {
            printf("Error opening input file: %S\n", wszSourceFile, hr);
        }
    }

    // Open the output file for writing.
    if (SUCCEEDED(hr))
    {
        hFile = CreateFile(wszTargetFile, GENERIC_WRITE, FILE_SHARE_READ, NULL,
            CREATE_ALWAYS, 0, NULL);

        if (hFile == INVALID_HANDLE_VALUE)
        {
            hr = HRESULT_FROM_WIN32(GetLastError());
            printf("Cannot create output file: %S\n", wszTargetFile, hr);
        }
    }

    // Write the WAVE file.
    if (SUCCEEDED(hr))
    {
        hr = WriteWaveFile(pReader, hFile, MAX_AUDIO_DURATION_MSEC);
    }

    if (FAILED(hr))
    {
        printf("Failed, hr = 0x%X\n", hr);
    }

    // Clean up.
    if (hFile != INVALID_HANDLE_VALUE)
    {
        CloseHandle(hFile);
    }

    SafeRelease(&pReader);
    MFShutdown();
    CoUninitialize();

    return SUCCEEDED(hr) ? 0 : 1;
};

此函式會執行下列動作:

  1. 呼叫 CoInitializeEx,以初始化 COM 連結庫。
  2. 呼叫 MFStartup 來初始化 Media Foundation 平臺。
  3. 呼叫 MFCreateSourceReaderFromURL 來建立來源讀取器。 此函式會接受輸入檔的名稱,並接收 IMFSourceReader 介面指標。
  4. 透過呼叫 CreateFile 函式來建立輸出檔案,該函式會傳回檔案控制代碼。
  5. 呼叫應用程式定義的 WriteWavFile 函式。 此函式會譯碼音訊並寫入 WAVE 檔案。
  6. 釋放 IMFSourceReader 指標和檔案句柄。
  7. 呼叫 MFShutdown 來關閉 Media Foundation 平臺。
  8. 呼叫 CoUninitialize 來釋放 COM 連結庫。

寫入 WAVE 檔案

大部分的工作都會在 WriteWavFile 函式中進行,而該函式是從 wmain呼叫的。

//-------------------------------------------------------------------
// WriteWaveFile
//
// Writes a WAVE file by getting audio data from the source reader.
//
//-------------------------------------------------------------------

HRESULT WriteWaveFile(
    IMFSourceReader *pReader,   // Pointer to the source reader.
    HANDLE hFile,               // Handle to the output file.
    LONG msecAudioData          // Maximum amount of audio data to write, in msec.
    )
{
    HRESULT hr = S_OK;

    DWORD cbHeader = 0;         // Size of the WAVE file header, in bytes.
    DWORD cbAudioData = 0;      // Total bytes of PCM audio data written to the file.
    DWORD cbMaxAudioData = 0;

    IMFMediaType *pAudioType = NULL;    // Represents the PCM audio format.

    // Configure the source reader to get uncompressed PCM audio from the source file.

    hr = ConfigureAudioStream(pReader, &pAudioType);

    // Write the WAVE file header.
    if (SUCCEEDED(hr))
    {
        hr = WriteWaveHeader(hFile, pAudioType, &cbHeader);
    }

    // Calculate the maximum amount of audio to decode, in bytes.
    if (SUCCEEDED(hr))
    {
        cbMaxAudioData = CalculateMaxAudioDataSize(pAudioType, cbHeader, msecAudioData);

        // Decode audio data to the file.
        hr = WriteWaveData(hFile, pReader, cbMaxAudioData, &cbAudioData);
    }

    // Fix up the RIFF headers with the correct sizes.
    if (SUCCEEDED(hr))
    {
        hr = FixUpChunkSizes(hFile, cbHeader, cbAudioData);
    }

    SafeRelease(&pAudioType);
    return hr;
}

這個函式會呼叫一系列其他應用程式定義的函式:

  1. ConfigureAudioStream 函式會初始化來源讀取器。 此函式接收 IMFMediaType 介面的指標,用來取得解碼音訊格式的描述,包括取樣率、通道數量和位元深度(每個樣本的位元數)。
  2. WriteWaveHeader 函式會寫入 WAVE 檔案的第一個部分,包括標頭和 'data' 區塊的開頭。
  3. CalculateMaxAudioDataSize 函式會計算要寫入檔案的音訊數量上限,以位元組為單位。
  4. WriteWaveData 函式會將 PCM 音訊數據寫入檔案。
  5. FixUpChunkSizes 函式會將出現在 'RIFF' 和 'data' 之後的檔案大小資訊寫入 WAVE 檔案中的 FOURCC 值。 這些值直到 WriteWaveData 完成才知道。

這些功能將在本教學課程的其餘章節中展示。

設定來源讀取器

ConfigureAudioStream 函式會設定來源讀取器,以譯碼來源檔案中的音訊數據流。 它也會傳回已譯碼音訊格式的相關信息。

在媒體基金會中,媒體格式會使用 媒體類型 物件來描述。 媒體類型物件會公開 的 IMFMediaType 介面,該介面繼承自 的 IMFAttributes 介面。 基本上,媒體類型是描述格式的屬性集合。 如需詳細資訊,請參閱 媒體類型

//-------------------------------------------------------------------
// ConfigureAudioStream
//
// Selects an audio stream from the source file, and configures the
// stream to deliver decoded PCM audio.
//-------------------------------------------------------------------

HRESULT ConfigureAudioStream(
    IMFSourceReader *pReader,   // Pointer to the source reader.
    IMFMediaType **ppPCMAudio   // Receives the audio format.
    )
{
    IMFMediaType *pUncompressedAudioType = NULL;
    IMFMediaType *pPartialType = NULL;

    // Select the first audio stream, and deselect all other streams.
    HRESULT hr = pReader->SetStreamSelection(
        (DWORD)MF_SOURCE_READER_ALL_STREAMS, FALSE);

    if (SUCCEEDED(hr))
    {
        hr = pReader->SetStreamSelection(
            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
    }

    // Create a partial media type that specifies uncompressed PCM audio.
    hr = MFCreateMediaType(&pPartialType);

    if (SUCCEEDED(hr))
    {
        hr = pPartialType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
    }

    if (SUCCEEDED(hr))
    {
        hr = pPartialType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
    }

    // Set this type on the source reader. The source reader will
    // load the necessary decoder.
    if (SUCCEEDED(hr))
    {
        hr = pReader->SetCurrentMediaType(
            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
            NULL, pPartialType);
    }

    // Get the complete uncompressed format.
    if (SUCCEEDED(hr))
    {
        hr = pReader->GetCurrentMediaType(
            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
            &pUncompressedAudioType);
    }

    // Ensure the stream is selected.
    if (SUCCEEDED(hr))
    {
        hr = pReader->SetStreamSelection(
            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
            TRUE);
    }

    // Return the PCM format to the caller.
    if (SUCCEEDED(hr))
    {
        *ppPCMAudio = pUncompressedAudioType;
        (*ppPCMAudio)->AddRef();
    }

    SafeRelease(&pUncompressedAudioType);
    SafeRelease(&pPartialType);
    return hr;
}

ConfigureAudioStream 函式會執行下列動作:

  1. 呼叫 IMFSourceReader::SetStreamSelection 方法來選取音訊數據流,並取消選取所有其他數據流。 此步驟可以改善效能,因為它可防止來源讀取器按住應用程式未使用的視訊畫面。
  2. 建立指定 PCM 音訊的 局部 媒體格式。 函式會建立部分類型,如下所示:
    1. 呼叫 MFCreateMediaType,以建立空的媒體類型物件。
    2. MF_MT_MAJOR_TYPE 屬性設定為 MFMediaType_Audio
    3. MF_MT_SUBTYPE 屬性設定為 MFAudioFormat_PCM
  3. 呼叫 IMFSourceReader::SetCurrentMediaType,以在來源讀取器上設定部分類型。 如果來源檔案包含編碼的音訊,來源讀取器會自動載入必要的音訊譯碼器。
  4. 呼叫 IMFSourceReader::GetCurrentMediaType,以取得實際的 PCM 媒體類型。 這個方法會傳回已填入所有格式詳細數據的媒體類型,例如音訊取樣率和通道數目。
  5. 呼叫 IMFSourceReader::SetStreamSelection 來啟用音訊串流。

寫入 WAVE 檔案標頭

WriteWaveHeader 函式會寫入 WAVE 檔案標頭。

從此函式呼叫的唯一媒體基礎 API 是 MFCreateWaveFormatExFromMFMediaType,它會將媒體類型轉換成 的WAVEATEX 結構。

//-------------------------------------------------------------------
// WriteWaveHeader
//
// Write the WAVE file header.
//
// Note: This function writes placeholder values for the file size
// and data size, as these values will need to be filled in later.
//-------------------------------------------------------------------

HRESULT WriteWaveHeader(
    HANDLE hFile,               // Output file.
    IMFMediaType *pMediaType,   // PCM audio format.
    DWORD *pcbWritten           // Receives the size of the header.
    )
{
    HRESULT hr = S_OK;
    UINT32 cbFormat = 0;

    WAVEFORMATEX *pWav = NULL;

    *pcbWritten = 0;

    // Convert the PCM audio format into a WAVEFORMATEX structure.
    hr = MFCreateWaveFormatExFromMFMediaType(pMediaType, &pWav, &cbFormat);

    // Write the 'RIFF' header and the start of the 'fmt ' chunk.
    if (SUCCEEDED(hr))
    {
        DWORD header[] = {
            // RIFF header
            FCC('RIFF'),
            0,
            FCC('WAVE'),
            // Start of 'fmt ' chunk
            FCC('fmt '),
            cbFormat
        };

        DWORD dataHeader[] = { FCC('data'), 0 };

        hr = WriteToFile(hFile, header, sizeof(header));

        // Write the WAVEFORMATEX structure.
        if (SUCCEEDED(hr))
        {
            hr = WriteToFile(hFile, pWav, cbFormat);
        }

        // Write the start of the 'data' chunk

        if (SUCCEEDED(hr))
        {
            hr = WriteToFile(hFile, dataHeader, sizeof(dataHeader));
        }

        if (SUCCEEDED(hr))
        {
            *pcbWritten = sizeof(header) + cbFormat + sizeof(dataHeader);
        }
    }


    CoTaskMemFree(pWav);
    return hr;
}

WriteToFile 函式是簡單的協助程式函式,會包裝 Windows WriteFile 函式,並傳回 HRESULT 值。

//-------------------------------------------------------------------
//
// Writes a block of data to a file
//
// hFile: Handle to the file.
// p: Pointer to the buffer to write.
// cb: Size of the buffer, in bytes.
//
//-------------------------------------------------------------------

HRESULT WriteToFile(HANDLE hFile, void* p, DWORD cb)
{
    DWORD cbWritten = 0;
    HRESULT hr = S_OK;

    BOOL bResult = WriteFile(hFile, p, cb, &cbWritten, NULL);
    if (!bResult)
    {
        hr = HRESULT_FROM_WIN32(GetLastError());
    }
    return hr;
}

計算數據大小上限

由於檔案大小會儲存為檔案標頭中的 4 位元組值,因此 WAVE 檔案的大小上限為0xFFFFFFFF位元組,大約 4 GB。 此值包含檔案標頭的大小。 PCM 音訊具有固定比特率,因此您可以從音訊格式計算數據大小上限,如下所示:

//-------------------------------------------------------------------
// CalculateMaxAudioDataSize
//
// Calculates how much audio to write to the WAVE file, given the
// audio format and the maximum duration of the WAVE file.
//-------------------------------------------------------------------

DWORD CalculateMaxAudioDataSize(
    IMFMediaType *pAudioType,    // The PCM audio format.
    DWORD cbHeader,              // The size of the WAVE file header.
    DWORD msecAudioData          // Maximum duration, in milliseconds.
    )
{
    UINT32 cbBlockSize = 0;         // Audio frame size, in bytes.
    UINT32 cbBytesPerSecond = 0;    // Bytes per second.

    // Get the audio block size and number of bytes/second from the audio format.

    cbBlockSize = MFGetAttributeUINT32(pAudioType, MF_MT_AUDIO_BLOCK_ALIGNMENT, 0);
    cbBytesPerSecond = MFGetAttributeUINT32(pAudioType, MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 0);

    // Calculate the maximum amount of audio data to write.
    // This value equals (duration in seconds x bytes/second), but cannot
    // exceed the maximum size of the data chunk in the WAVE file.

        // Size of the desired audio clip in bytes:
    DWORD cbAudioClipSize = (DWORD)MulDiv(cbBytesPerSecond, msecAudioData, 1000);

    // Largest possible size of the data chunk:
    DWORD cbMaxSize = MAXDWORD - cbHeader;

    // Maximum size altogether.
    cbAudioClipSize = min(cbAudioClipSize, cbMaxSize);

    // Round to the audio block size, so that we do not write a partial audio frame.
    cbAudioClipSize = (cbAudioClipSize / cbBlockSize) * cbBlockSize;

    return cbAudioClipSize;
}

若要避免部分音訊畫面,大小會四捨五入為區塊對齊,儲存在 MF_MT_AUDIO_BLOCK_ALIGNMENT 屬性中。

譯碼音訊

WriteWaveData 函式會從來源檔案讀取已譯碼的音訊,並寫入 WAVE 檔案。

//-------------------------------------------------------------------
// WriteWaveData
//
// Decodes PCM audio data from the source file and writes it to
// the WAVE file.
//-------------------------------------------------------------------

HRESULT WriteWaveData(
    HANDLE hFile,               // Output file.
    IMFSourceReader *pReader,   // Source reader.
    DWORD cbMaxAudioData,       // Maximum amount of audio data (bytes).
    DWORD *pcbDataWritten       // Receives the amount of data written.
    )
{
    HRESULT hr = S_OK;
    DWORD cbAudioData = 0;
    DWORD cbBuffer = 0;
    BYTE *pAudioData = NULL;

    IMFSample *pSample = NULL;
    IMFMediaBuffer *pBuffer = NULL;

    // Get audio samples from the source reader.
    while (true)
    {
        DWORD dwFlags = 0;

        // Read the next sample.
        hr = pReader->ReadSample(
            (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
            0, NULL, &dwFlags, NULL, &pSample );

        if (FAILED(hr)) { break; }

        if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
        {
            printf("Type change - not supported by WAVE file format.\n");
            break;
        }
        if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM)
        {
            printf("End of input file.\n");
            break;
        }

        if (pSample == NULL)
        {
            printf("No sample\n");
            continue;
        }

        // Get a pointer to the audio data in the sample.

        hr = pSample->ConvertToContiguousBuffer(&pBuffer);

        if (FAILED(hr)) { break; }


        hr = pBuffer->Lock(&pAudioData, NULL, &cbBuffer);

        if (FAILED(hr)) { break; }


        // Make sure not to exceed the specified maximum size.
        if (cbMaxAudioData - cbAudioData < cbBuffer)
        {
            cbBuffer = cbMaxAudioData - cbAudioData;
        }

        // Write this data to the output file.
        hr = WriteToFile(hFile, pAudioData, cbBuffer);

        if (FAILED(hr)) { break; }

        // Unlock the buffer.
        hr = pBuffer->Unlock();
        pAudioData = NULL;

        if (FAILED(hr)) { break; }

        // Update running total of audio data.
        cbAudioData += cbBuffer;

        if (cbAudioData >= cbMaxAudioData)
        {
            break;
        }

        SafeRelease(&pSample);
        SafeRelease(&pBuffer);
    }

    if (SUCCEEDED(hr))
    {
        printf("Wrote %d bytes of audio data.\n", cbAudioData);

        *pcbDataWritten = cbAudioData;
    }

    if (pAudioData)
    {
        pBuffer->Unlock();
    }

    SafeRelease(&pBuffer);
    SafeRelease(&pSample);
    return hr;
}

WriteWaveData 函式會在循環中執行下列動作:

  1. 呼叫 IMFSourceReader::ReadSample,以從來源檔案讀取音訊。 dwFlags 參數會從 MF_SOURCE_READER_FLAG 列舉接收一個位 OR 旗標。 pSample 參數會接收用來存取音訊數據的 IMFSample 介面指標。 在某些情況下,呼叫 ReadSample 不會產生數據,IMFSample 指針為 NULL
  2. 檢查 dwFlags 是否有下列旗標:
    • MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED。 此旗標表示來源檔案中的格式變更。 WAVE 檔案不支援格式變更。
    • MF_SOURCE_READERF_ENDOFSTREAM。 此旗標表示數據流的結尾。
  3. 呼叫 IMFSample::ConvertToContiguousBuffer 以取得緩衝區物件的指標。
  4. 呼叫 IMFMediaBuffer::Lock,以取得緩衝區內存的指標。
  5. 將音訊數據寫入輸出檔案。
  6. 呼叫 IMFMediaBuffer::Unlock 來解除鎖定緩衝區物件。

當發生下列任一情況時,函式會中斷迴圈:

  • 數據流格式會變更。
  • 已經到達資料流的結尾。
  • 音訊數據的最大數量會寫入輸出檔。
  • 發生錯誤。

完成文件標頭

在前一個函式完成後,才能知道儲存在WAVE標頭中的大小值。 FixUpChunkSizes 會填入下列值:

//-------------------------------------------------------------------
// FixUpChunkSizes
//
// Writes the file-size information into the WAVE file header.
//
// WAVE files use the RIFF file format. Each RIFF chunk has a data
// size, and the RIFF header has a total file size.
//-------------------------------------------------------------------

HRESULT FixUpChunkSizes(
    HANDLE hFile,           // Output file.
    DWORD cbHeader,         // Size of the 'fmt ' chuck.
    DWORD cbAudioData       // Size of the 'data' chunk.
    )
{
    HRESULT hr = S_OK;

    LARGE_INTEGER ll;
    ll.QuadPart = cbHeader - sizeof(DWORD);

    if (0 == SetFilePointerEx(hFile, ll, NULL, FILE_BEGIN))
    {
        hr = HRESULT_FROM_WIN32(GetLastError());
    }

    // Write the data size.

    if (SUCCEEDED(hr))
    {
        hr = WriteToFile(hFile, &cbAudioData, sizeof(cbAudioData));
    }

    if (SUCCEEDED(hr))
    {
        // Write the file size.
        ll.QuadPart = sizeof(FOURCC);

        if (0 == SetFilePointerEx(hFile, ll, NULL, FILE_BEGIN))
        {
            hr = HRESULT_FROM_WIN32(GetLastError());
        }
    }

    if (SUCCEEDED(hr))
    {
        DWORD cbRiffFileSize = cbHeader + cbAudioData - 8;

        // NOTE: The "size" field in the RIFF header does not include
        // the first 8 bytes of the file. (That is, the size of the
        // data that appears after the size field.)

        hr = WriteToFile(hFile, &cbRiffFileSize, sizeof(cbRiffFileSize));
    }

    return hr;
}

音訊媒體類型

來源讀取器

IMFSourceReader