共用方式為


教學課程:解碼音訊

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

概觀

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

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

顯示來源讀取器從來源檔案取得音訊資料的圖表。

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

資料類型 大小 (位元組)
FOURCC 4 'RIFF'
DWORD 4 檔案大小總計,不包括前 8 個位元組
FOURCC 4 'WAVE'
FOURCC 4 'fmt '
DWORD 4 後續 的「其後置」的「產生者」資料 大小。
波擷取 不一定 音訊格式標頭。
FOURCC 4 'data'
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 函式會寫入 WAVE 檔案中 'RIFF' 和 'data' FOURCC 值之後出現的檔案大小資訊。 (完成之前 WriteWaveData 不知道這些值。)

這些函式會顯示在本教學課程的其餘章節中。

設定來源讀取器

函式 ConfigureAudioStream 會將來源讀取器設定為解碼來源檔案中的音訊資料流程。 它也會傳回解碼音訊格式的相關資訊。

在 Media Foundation 中,媒體格式是使用 媒體類型 物件來描述。 媒體類型物件會公開 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