Share via


자습서: 오디오 디코딩

이 자습서에서는 원본 판독 기를 사용하여 미디어 파일에서 오디오를 디코딩하고 오디오를 WAVE 파일에 쓰는 방법을 보여줍니다. 이 자습서는 오디오 클립 샘플을 기반으로 합니다.

개요

이 자습서에서는 두 개의 명령줄 인수인 오디오 스트림이 포함된 입력 파일의 이름과 출력 파일 이름을 사용하는 콘솔 애플리케이션을 만듭니다. 애플리케이션은 입력 파일에서 5초의 오디오 데이터를 읽고 오디오를 출력 파일에 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 'data'
DWORD 4 오디오 데이터의 크기입니다.
BYTE[] 상황에 따라 다름 오디오 데이터.

 

참고

FOURCC는 4개의 ASCII 문자를 연결하여 형성된 DWORD입니다.

 

이 기본 구조는 이 자습서의 scope 벗어나는 파일 메타데이터 및 기타 정보를 추가하여 확장할 수 있습니다.

헤더 및 라이브러리 파일

프로젝트에 다음 헤더 파일을 포함합니다.

#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 파일 작성

대부분의 작업은 에서 호출되는 함수에서 wmain발생합니다WriteWavFile.

//-------------------------------------------------------------------
// 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 함수는 헤더와 'data' 청크의 시작을 포함하여 WAVE 파일의 첫 번째 부분을 씁니다.
  3. CalculateMaxAudioDataSize 함수는 파일에 쓸 최대 오디오 양을 바이트 단위로 계산합니다.
  4. WriteWaveData 함수는 PCM 오디오 데이터를 파일에 씁니다.
  5. FixUpChunkSizes 함수는 WAVE 파일의 'RIFF' 및 'data' FOURCC 값 다음에 나타나는 파일 크기 정보를 씁니다. (이러한 값은 완료될 때까지 WriteWaveData 알 수 없습니다.)

이러한 함수는 이 자습서의 나머지 섹션에 나와 있습니다.

원본 판독기 구성

함수는 ConfigureAudioStream 원본 파일의 오디오 스트림을 디코딩하도록 원본 판독기를 구성합니다. 또한 디코딩된 오디오의 형식에 대한 정보도 반환합니다.

Media Foundation에서 미디어 형식은 미디어 형식 개체를 사용하여 설명합니다. 미디어 형식 개체는 IMFAttributes 인터페이스를 상속하는 IMFMediaType 인터페이스를 노출합니다. 기본적으로 미디어 형식은 형식을 설명하는 속성의 컬렉션입니다. 자세한 내용은 미디어 형식을 참조하세요.

//-------------------------------------------------------------------
// 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 파일 헤더를 씁니다.

이 함수에서 호출된 유일한 Media Foundation API는 미디어 형식을 WAVEFORMATEX 구조체로 변환하는 MFCreateWaveFormatExFromMFMediaType입니다.

//-------------------------------------------------------------------
// 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 바이트(약 4GB)로 제한됩니다. 이 값에는 파일 헤더의 크기가 포함됩니다. 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