Udostępnij przez


Samouczek: dekodowanie dźwięku

W tym samouczku pokazano, jak używać czytnika źródeł do dekodowania dźwięku z pliku multimedialnego i zapisywania dźwięku w pliku WAVE. Samouczek jest oparty na przykładzie Audio Clip.

Przegląd

W tym samouczku utworzysz aplikację konsolową, która przyjmuje dwa argumenty wiersza polecenia: nazwę pliku wejściowego zawierającego strumień audio i nazwę pliku wyjściowego. Aplikacja odczytuje pięć sekund danych dźwiękowych z pliku wejściowego i zapisuje dźwięk w pliku wyjściowym jako dane WAVE.

Aby uzyskać zdekodowane dane audio, aplikacja używa obiektu czytnika źródłowego. Czytnik źródła odsłania interfejs IMFSourceReader. Aby zapisać zdekodowany dźwięk do pliku WAVE, aplikacje używają funkcji We/Wy systemu Windows. Na poniższej ilustracji przedstawiono ten proces.

diagram przedstawiający czytnik źródła uzyskujący dane audio z pliku źródłowego.

W najprostszej formie plik WAVE ma następującą strukturę:

Typ danych Rozmiar (bajty) Wartość
FOURCC 4 "RIFF"
DWORD 4 Całkowity rozmiar pliku, bez uwzględniania pierwszych 8 bajtów
FOURCC 4 "WAVE"
FOURCC 4 "fmt"
DWORD 4 Rozmiar danych WAVEFORMATEX, które następują.
WAVEFORMATEX Różni się Nagłówek formatu audio.
FOURCC 4 "dane"
DWORD 4 Rozmiar danych dźwiękowych.
BYTE[] Różni się Dane audio.

 

Notatka

FOURCC jest DWORD utworzony przez łączenie czterech znaków ASCII.

 

Tę podstawową strukturę można rozszerzyć, dodając metadane pliku i inne informacje, które wykraczają poza zakres tego samouczka.

Pliki nagłówków i bibliotek

Uwzględnij następujące pliki nagłówkowe w projekcie:

#define WINVER _WIN32_WINNT_WIN7

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

Połącz z następującymi bibliotekami:

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

Zaimplementuj wmain

Poniższy kod przedstawia funkcję punktu wejścia dla aplikacji.

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;
};

Ta funkcja wykonuje następujące czynności:

  1. Wywołuje CoInitializeEx, aby zainicjować bibliotekę COM.
  2. Wywołuje MFStartup w celu zainicjowania platformy Media Foundation.
  3. Wywołuje MFCreateSourceReaderFromURL w celu utworzenia czytnika źródła. Ta funkcja przyjmuje nazwę pliku wejściowego i otrzymuje wskaźnik interfejsu IMFSourceReader.
  4. Tworzy plik wyjściowy poprzez wywołanie funkcji CreateFile, która zwraca uchwyt pliku.
  5. Wywołuje funkcję WriteWavFile zdefiniowaną przez aplikację. Ta funkcja dekoduje dźwięk i zapisuje plik WAVE.
  6. Zwalnia wskaźnik IMFSourceReader i uchwyt pliku.
  7. Wywołuje MFShutdown, aby zamknąć platformę Media Foundation.
  8. Wywołuje CoUninitialize w celu wydania biblioteki COM.

Zapisywanie pliku WAVE

Większość pracy odbywa się w funkcji WriteWavFile, która jest wywoływana z 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;
}

Ta funkcja wywołuje serię innych funkcji zdefiniowanych przez aplikację:

  1. Funkcja ConfigureAudioStream inicjuje czytnik źródła. Ta funkcja odbiera wskaźnik do interfejsu IMFMediaType, który służy do uzyskiwania opisu zdekodowanego formatu audio, w tym częstotliwości próbkowania, liczby kanałów i głębokości bitów (bitów na próbkę).
  2. Funkcja WriteWaveHeader zapisuje pierwszą część pliku WAVE, w tym nagłówek i początek fragmentu "data".
  3. Funkcja CalculateMaxAudioDataSize oblicza maksymalną ilość dźwięku do zapisu w pliku w bajtach.
  4. Funkcja WriteWaveData zapisuje dane audio PCM w pliku.
  5. Funkcja FixUpChunkSizes zapisuje informacje o rozmiarze pliku wyświetlane po wartościach "RIFF" i "data" FOURCC w pliku WAVE. (Te wartości nie są znane do czasu ukończenia WriteWaveData).

Te funkcje są wyświetlane w pozostałych sekcjach tego samouczka.

Konfigurowanie czytnika źródła

Funkcja ConfigureAudioStream konfiguruje czytnik źródłowy w celu dekodowania strumienia audio w pliku źródłowym. Zwraca również informacje o formacie zdekodowanego dźwięku.

W programie Media Foundation formaty multimediów są opisywane przy użyciu typu nośnika obiektów. Obiekt typu mediów uwidacznia interfejs IMFMediaType, który dziedziczy interfejs IMFAttributes. Zasadniczo typ nośnika jest kolekcją właściwości opisujących format. Aby uzyskać więcej informacji, zobacz Typy multimediów.

//-------------------------------------------------------------------
// 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;
}

Funkcja ConfigureAudioStream wykonuje następujące czynności:

  1. Wywołuje metodę IMFSourceReader::SetStreamSelection, aby zaznaczyć strumień audio i odznaczyć wszystkie pozostałe strumienie. Ten krok może zwiększyć wydajność, ponieważ uniemożliwia czytnikowi źródłowemu trzymanie się ramek wideo, których aplikacja nie używa.
  2. Tworzy częściowy typ nośnika, który określa dźwięk PCM. Funkcja tworzy typ częściowy w następujący sposób:
    1. Wywołuje MFCreateMediaType, aby utworzyć pusty obiekt typu nośnika.
    2. Ustawia atrybut MF_MT_MAJOR_TYPE na wartość MFMediaType_Audio.
    3. Ustawia atrybut MF_MT_SUBTYPE na wartość MFAudioFormat_PCM.
  3. Wywołuje IMFSourceReader::SetCurrentMediaType, aby ustawić typ częściowy na czytniku źródłowym. Jeśli plik źródłowy zawiera zakodowany dźwięk, czytnik źródłowy automatycznie ładuje niezbędny dekoder audio.
  4. Wywołuje IMFSourceReader::GetCurrentMediaType, aby uzyskać aktualny typ nośnika PCM. Ta metoda zwraca typ nośnika ze wszystkimi wypełnionymi szczegółami formatu, takimi jak częstotliwość próbkowania audio i liczba kanałów.
  5. Wywołuje IMFSourceReader::SetStreamSelection, aby włączyć strumień audio.

Pisanie nagłówka pliku WAVE

Funkcja WriteWaveHeader zapisuje nagłówek pliku WAVE.

Jedynym interfejsem API programu Media Foundation wywoływanym z tej funkcji jest MFCreateWaveFormatExFromMFMediaType, który konwertuje typ multimediów na strukturę WAVEFORMATEX.

//-------------------------------------------------------------------
// 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;
}

Funkcja WriteToFile jest prostą funkcją pomocnika, która opakowuje funkcję WriteFile systemu Windows i zwraca wartość 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;
}

Obliczanie maksymalnego rozmiaru danych

Ponieważ rozmiar pliku jest przechowywany jako wartość 4-bajtowa w nagłówku pliku, plik WAVE jest ograniczony do maksymalnego rozmiaru 0xFFFFFFFF bajtów — około 4 GB. Ta wartość zawiera rozmiar nagłówka pliku. Dźwięk PCM ma stałą szybkość bitów, dzięki czemu można obliczyć maksymalny rozmiar danych z formatu audio w następujący sposób:

//-------------------------------------------------------------------
// 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;
}

Aby uniknąć częściowych ramek dźwiękowych, rozmiar jest zaokrąglany do wyrównania bloku, które jest przechowywane w atrybucie MF_MT_AUDIO_BLOCK_ALIGNMENT.

Dekodowanie dźwięku

Funkcja WriteWaveData odczytuje dekodowany dźwięk z pliku źródłowego i zapisuje w pliku 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;
}

Funkcja WriteWaveData wykonuje następujące czynności w pętli:

  1. Wywołuje IMFSourceReader::ReadSample, aby odczytać dźwięk z pliku źródłowego. Parametr dwFlags odbiera bitową LUB flag z wyliczenia MF_SOURCE_READER_FLAG. Parametr pSample odbiera wskaźnik do interfejsu IMFSample, który jest używany do uzyskiwania dostępu do danych dźwiękowych. W niektórych przypadkach wywołanie ReadSample nie generuje danych, wówczas wskaźnik IMFSample jest o wartości NULL.
  2. Sprawdza dwFlags pod kątem następujących flag:
    • MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED. Ta flaga wskazuje zmianę formatu w pliku źródłowym. Pliki WAVE nie obsługują zmian formatu.
    • MF_SOURCE_READERF_ENDOFSTREAM. Ta flaga wskazuje koniec strumienia.
  3. Wywołuje IMFSample::ConvertToContiguousBuffer, aby uzyskać wskaźnik do obiektu buforu.
  4. Wywołuje IMFMediaBuffer::Lock, aby uzyskać wskaźnik do pamięci buforu.
  5. Zapisuje dane audio w pliku wyjściowym.
  6. Wywołuje IMFMediaBuffer::Unlock, aby odblokować obiekt buforu.

Funkcja przerywa pętlę, gdy wystąpi któreś z następujących zdarzeń:

  • Format strumienia zmienia się.
  • Osiągnięto koniec strumienia.
  • Maksymalna ilość danych audio jest zapisywana w pliku wyjściowym.
  • Wystąpi błąd.

Finalizowanie nagłówka pliku

Wartości rozmiaru przechowywane w nagłówku WAVE nie są znane, dopóki poprzednia funkcja nie zostanie ukończona. FixUpChunkSizes wypełnia następujące wartości:

//-------------------------------------------------------------------
// 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;
}

typy multimediów audio

czytelnik źródłowy

IMFSourceReader