Date

※ 2009/9/9 更新 ソースコードおよび本文を修正、整理した。

Windows 7 では Media Foundation の機能が 強化され、サポートされるフォーマットが増えている。

本記事では Media Session を用いて WAV ファイルを AAC-LC で圧縮し、 MPEG-4 audio 形式(拡張子 .m4a) のファイルにエンコードする処理を実装してみる。

※ サンプルアプリケーションの動作には Windows 7 Home Premium 以上が必要です。開発には Windows SDK v7.0 が必要です。Vista や Windows 7 Starter などでは動作しません。Windows 7 には Fast Transcode という圧縮・変換のための API がある。が、このエントリは Media Session の使い方を示すのが趣旨ですので使いません。

図1にクラス図を示す。以降、これを踏まえて解説する。

{% img /img/20090909-ClassDiagram1.png 図1.クラス図 %}

Topology の構築

Media Session で構築する Topology を図2に示す。

{% img /img/20090825-aacenc_topology.png 図2.トポロジ %}

Media Session を作成する

Media Session を作成するために MFCreateMediaSession を呼ぶ。IMFMediaEventGenerator::BeginEventを呼び CMyWindow クラスで Media Session のイベントを取得できるようにする。

1
2
3
4
5
6
7
8
9
LRESULT CMyWindow::OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
    ATLASSERT(m_MediaSession == NULL);
    CHResult hr;
    try {
        CComPtr<IMFTopology> topology;
        // Media Session を作成し、イベントを受け取れるようにする
        hr = MFCreateMediaSession(NULL, &m_MediaSession);
        hr = m_MediaSession->BeginGetEvent(this, NULL);

Media Source を作成する

入力元となる WAV ファイルを読むための Media Source を作成する。Media Source を作成するには Source Resolver を使う。Source Resolver は URL やバイトストリームをもとに適切な Media source オブジェクト を返す。MFCreateSourceResolver を呼び Source Resolver を作成し、CreateObjectFromURL で Media Source を作成する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Source Resolver を作成する
        MF_OBJECT_TYPE object_type;
        CComPtr<IMFSourceResolver> src_resolver;
        hr = MFCreateSourceResolver(&src_resolver);
        hr = src_resolver->CreateObjectFromURL(
            INPUT_FILE_NAME,
            MF_RESOLUTION_MEDIASOURCE,
            NULL,
            &object_type,
            (IUnknown**)&m_Source);
        // NOTE : object_type == MF_OBJECT_INVALID の場合、
        //        INPUT_FILE_NAME で指定したファイル名を確認してください
        ATLASSERT(object_type == MF_OBJECT_MEDIASOURCE);

Topology を構築する

まず Topology を作成するために MFCreateTopology を呼ぶ。作成した Toplogy に Node を追加し、それらを接続することによりエンコード処理を行うことができるようになる。

次に Source Resolver から Presentaion Descriptor と Stream Descriptor を取得する。これらは Source Node および Output Node を初期化するときに使う。読み取る対象が WAV ファイルなので Stream Descriptor の数は必ず 1 であり、選択済み(selected) であることを確認する。

それから Source Node、Transform Node、Output Node を Topology に追加し、それぞれを接続する。ここでは各 Node を追加、接続する処理を別のメソッド(AddSourceNode, AddTransformNode, AddOutputNode として定義している(後述)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Topology を構築する
        CComPtr<IMFPresentationDescriptor> pres_desc;
        CComPtr<IMFStreamDescriptor> stream_desc;
        CComPtr<IMFTopologyNode> src_node;
        CComPtr<IMFTopologyNode> transform_node;
        CComPtr<IMFTopologyNode> output_node;
        DWORD stream_count;
        BOOL selected;
        hr = MFCreateTopology(&topology);
        hr = m_Source->CreatePresentationDescriptor(&pres_desc);
        hr = pres_desc->GetStreamDescriptorCount(&stream_count);
        ATLASSERT(stream_count == 1);
        hr = pres_desc->GetStreamDescriptorByIndex(0, &selected, &stream_desc);
        ATLASSERT(selected == TRUE);
        AddSourceNode(topology, pres_desc, stream_desc, src_node);
        AddTransformNode(topology, src_node, stream_desc, transform_node);
        AddOutputNode(topology, transform_node, output_node);
        hr = m_MediaSession->SetTopology(0, topology);
    }
    catch (CAtlException &) {
        MessageBox(_T("Failed"), APP_NAME, MB_OK | MB_ICONSTOP);
        DestroyWindow();
    }
    return 0;
}

Source Node を Topology に追加する

Source Node を作成し、 Source Resolver から取得した Presentaion Descriptor と Stream Descriptor を設定し、Topology に追加する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void CMyWindow::AddSourceNode(
    CComPtr<IMFTopology> topology,
    CComPtr<IMFPresentationDescriptor> pres_desc,
    CComPtr<IMFStreamDescriptor> stream_desc,
    CComPtr<IMFTopologyNode> &node)
{
    CHResult hr;
    hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &node);
    hr = node->SetUnknown(MF_TOPONODE_SOURCE, m_Source);
    hr = node->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, pres_desc);
    hr = node->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, stream_desc);
    hr = topology->AddNode(node);
}

Transform Node を Topology に追加する

AAC エンコーダの MFT (Media Foundation Transform) と Transform Node を作成し、MFT を Transform Node に紐付ける。

まず Source Resolver から得た Stream Descriptor の Media Type Handler を得る。次に IMFMediaTypeHandler::GetMediaTypeByIndex を呼び、メディアタイプを取得する。

メディアタイプを取得したら、それが無圧縮 WAV 形式であるか確認する。具体的にはメジャータイプが MFMediaType_Audio であることと、WAVEFORMATEX::wFormatTag が WAVE_FORMAT_PCM であることを確認する。メディアタイプ (IMFMediaType) からWAVEFORMATEX に変換するには MFCreateWaveFormatExFromMFMediaType を呼びます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void CMyWindow::AddTransformNode(
    CComPtr<IMFTopology> topology,
    CComPtr<IMFTopologyNode> src_node,
    CComPtr<IMFStreamDescriptor> stream_desc,
    CComPtr<IMFTopologyNode> &node)
{
    CHResult hr;
    // 入力タイプを取得する
    CComPtr<IMFMediaType> in_mfmt;
    CComPtr<IMFMediaTypeHandler> mt_handler;
    GUID major_type_guid = GUID_NULL;
    GUID subtype;
    DWORD mt_count;
    hr = stream_desc->GetMediaTypeHandler(&mt_handler);
    hr = mt_handler->GetMajorType(&major_type_guid);
    ATLASSERT(major_type_guid == MFMediaType_Audio);
    hr = mt_handler->GetMediaTypeCount(&mt_count);
    ATLASSERT(mt_count == 1);
    hr = mt_handler->GetMediaTypeByIndex(0, &in_mfmt);
    hr = in_mfmt->GetGUID(MF_MT_SUBTYPE, &subtype);
    WAVEFORMATEX *in_wfx, *out_wfx;
    UINT32 wfx_size;
    UINT32 mft_count;
    IMFActivate** mf_activate = NULL;
    CComPtr<IMFTransform> mft;
    CComPtr<IMFMediaType> out_mf_media_type;
    CComPtr<IMFMediaType> out_curr_mfmt;
    try {
        MFCreateWaveFormatExFromMFMediaType(in_mfmt, &in_wfx, &wfx_size);
        ATLASSERT(in_wfx->wFormatTag == WAVE_FORMAT_PCM);
        ATLTRACE(_T("Input Media Type\n"));
        TraceWavFormatEx(in_wfx);

メディアタイプが確認できたので、 AAC エンコードを行う MFT を作成する。ここでは MFTEnumEx API を使う。この API は MFT_REGISTER_TYPE_INFO 構造体に入力、出力それぞれのメジャータイプ、サブタイプを設定し引数として渡すことにより、該当する MFT を検索してくれる。

ここで指定するメジャータイプは MFMediaType_Audio である。サブタイプは入力が Source Resolver から取得したメディアタイプのサブタイプ、出力は MFAudioFormat_AAC である。

検索結果は Activation Object の配列 IMFActivate ** が返ってくる。Activation Object は IMFActivate::ActivateObject を呼ぶことにより、実際の MFT を作成できる。

それから、Transform Node を作成し、MFT を紐付け、 Source Node から Transform Node へ接続する。

最後に MFT が出力するサンプリングレートや、ビットレートを設定する。これは MFT の出力メディアタイプを設定することにより行う。設定できる値には制限がある ので注意すること。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 入力タイプと出力タイプに合うようなMFTを検索する
        MFT_REGISTER_TYPE_INFO in_info = {MFMediaType_Audio, subtype};
        MFT_REGISTER_TYPE_INFO out_info = {MFMediaType_Audio, MFAudioFormat_AAC};
        hr = MFTEnumEx(MFT_CATEGORY_AUDIO_ENCODER, 0,
            &in_info, &out_info, &mf_activate, &mft_count);
        ATLASSERT(mft_count == 1);
        ATLASSERT(mf_activate[0] != NULL);
        hr = mf_activate[0]->ActivateObject(__uuidof(IMFTransform), (void**)&mft);
        // Transform node を作成する
        hr = MFCreateTopologyNode(MF_TOPOLOGY_TRANSFORM_NODE, &node);
        hr = node->SetObject(mft);
        hr = topology->AddNode(node);
        hr = src_node->ConnectOutput(0, node, 0);
        // メディアタイプをMFTに設定する
        hr = MFCreateMediaType(&out_mf_media_type);
        out_mf_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
        out_mf_media_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_AAC);
        out_mf_media_type->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16);
        out_mf_media_type->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, in_wfx->nSamplesPerSec);
        out_mf_media_type->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, in_wfx->nChannels);
        out_mf_media_type->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 12000);
        out_mf_media_type->SetUINT32(MF_MT_AAC_PAYLOAD_TYPE, 0);
        out_mf_media_type->SetUINT32(MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION, 0x29);
        out_mf_media_type->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 1);
        out_mf_media_type->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, 0);
        out_mf_media_type->SetUINT32(MF_MT_AVG_BITRATE, 96000);
        hr = mft->SetOutputType(0, out_mf_media_type, 0);
        hr = mft->GetOutputCurrentType(0, &out_curr_mfmt);
        hr = MFCreateWaveFormatExFromMFMediaType(out_curr_mfmt, &out_wfx, &wfx_size);
        ATLTRACE(_T("Output Media Type\n"));
        TraceWavFormatEx(out_wfx);
    }
    catch (CAtlException &) {}
    if (mf_activate != NULL) {
        for (UINT32 i = 0;i < mft_count;i++) {
            if (mf_activate[i] != NULL) {
                mf_activate[i]->Release();
            }
        }
    }
    CoTaskMemFree(mf_activate);
    CoTaskMemFree(in_wfx);
    CoTaskMemFree(out_wfx);
}

Output Node を Topology に追加する

エンコード結果を MPEG-4 audio 形式 (.mp4) ファイルとして書き出すために Media Sinkである MPEG-4 File Sink と、Output Node を作成し、それらを紐付ける。MPEG-4 File Sink は入力ストリームを H.264/AVC video, AAC audio, MP3 audio 形式で出力することができる。

MPEG-4 File Sink を作成する前に出力先となるファイルを用意する必要がある。そのために MFCreateFile を呼び出し、Byte Stream オブジェクトを受け取る。これは IMFByteStream インターフェイスを持っている。

MPEG-4 File Sink を作成するには MFCreateMPEG4MediaSink を呼び出す。ここで Byte Stream オブジェクトを引数として渡すことにより MPEG-4 File Sink が AAC 形式でファイルに書き出してくれる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void CMyWindow::AddOutputNode(
    CComPtr<IMFTopology> topology,
    CComPtr<IMFTopologyNode> transform_node,
    CComPtr<IMFTopologyNode> &output_node)
{
    CHResult hr;
    CComPtr<IMFByteStream> byte_stream;
    CComPtr<IMFStreamSink> stream_sink;
    CComPtr<IMFMediaType> out_mf_media_type;
    CComQIPtr<IMFTransform> mft;
    hr = transform_node->GetObjectW((IUnknown**)&mft);
    hr = mft->GetOutputCurrentType(0, &out_mf_media_type);
    hr = MFCreateFile(
        MF_ACCESSMODE_WRITE, MF_OPENMODE_DELETE_IF_EXIST, MF_FILEFLAGS_NONE,
        OUTPUT_FILE_NAME, &byte_stream);
    hr = MFCreateMPEG4MediaSink(byte_stream, NULL, out_mf_media_type, &m_Sink);
    // Stream Sink を取得する
    DWORD sink_count;
    hr = m_Sink->GetStreamSinkCount(&sink_count);
    ATLASSERT(sink_count == 1);
    hr = m_Sink->GetStreamSinkByIndex(0, &stream_sink);
    // Output Node を作成し、接続
    hr = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &output_node);
    hr = output_node->SetObject(stream_sink);
    hr = topology->AddNode(output_node);
    hr = transform_node->ConnectOutput(0, output_node, 0);
}

イベント処理

Topology は非同期に実行されるので、エンコード処理で発生する Media Session で発生するイベントを捕まえてみよう。ここでは「Topology の準備が完了したとき」と「エンコードが完了したとき」の2つのイベントを捕まえる。

Topology の準備が完了したときは、Topology を開始させる。

エンコードが完了したときは、「Completed.」というメッセージを表示させる。

イベントを捕まえるには、捕まえるクラスに IMFAsyncCallback インターフェイスを実装し、そのクラスをイベントを発生するオブジェクトに通知する。(Media Session 作成時に CMyWindow クラスに通知するように実装済み。)

イベントが発生するたびに IMFAsyncCallback::Invoke が呼ばれるので、イベントの種別に応じた処理を行う。IMFAsyncCallback::Invoke はアプリケーションのメインスレッドとは異なるスレッドで実行されるので直接処理をせず PostMessage を呼んでウィンドウメッセージを投げている。これにより Media Foundation のイベントをメインスレッドで処理できる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
HRESULT STDMETHODCALLTYPE CMyWindow::Invoke(
    /* [in] */ __RPC__in_opt IMFAsyncResult *pAsyncResult)
{
    CHResult hr;
    MediaEventType me_type;
    CComPtr<IMFMediaEvent> media_event;
    try {
        hr = m_MediaSession->EndGetEvent(pAsyncResult, &media_event);
        hr = media_event->GetType(&me_type);
        if (me_type == MESessionClosed) {
            m_MediaSessionClosed.Set();
        } else {
            media_event.p->AddRef();
            PostMessage(WM_MF_EVENT,
                (WPARAM)(IMFMediaEvent*)media_event, (LPARAM)0);
            hr = m_MediaSession->BeginGetEvent(this, NULL);
        }
    }
    catch (CAtlException &) {}
    return hr;
}

イベントにはさまざまなタイプがあるので、ハンドリングすべきイベントを判別する必要がある。そのために、IMFMediaEvent::GetType を呼び、イベントタイプ (MediaEventType 型)を取得する。 Topology の状態が変化したときのイベントタイプは MESessionTopologyStatus です。イベントの詳細は属性として設定されています。属性 MF_EVENT_TOPOLOGY_STATUS の値が MF_TOPOSTATUS_READY のとき、 Topology の準備が完了したことを意味し、 MF_TOPOSTATUS_ENDED となったとき、エンコードの終了を意味する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
LRESULT CMyWindow::OnMFEvent(UINT, WPARAM wparam, LPARAM, BOOL&)
{
    CHResult hr;
    CComPtr<IMFMediaEvent> media_event;
    MediaEventType me_type;
    media_event.Attach((IMFMediaEvent*)wparam);
    try {
        hr = media_event->GetType(&me_type);
        if (me_type == MESessionTopologyStatus) {
            MF_TOPOSTATUS topo_status = (MF_TOPOSTATUS)MFGetAttributeUINT32(
                media_event, MF_EVENT_TOPOLOGY_STATUS, MF_TOPOSTATUS_INVALID);
            switch (topo_status) {
            case MF_TOPOSTATUS_READY:
                ATLTRACE(_T("MF_TOPOSTATUS_READY\n"));
                OnTopologyReady(media_event);
                break;
            case MF_TOPOSTATUS_STARTED_SOURCE:
                ATLTRACE(_T("MF_TOPOSTATUS_STARTED_SOURCE\n"));
                break;
            case MF_TOPOSTATUS_ENDED:
                ATLTRACE(_T("MF_TOPOSTATUS_ENDED\n"));
                OnTopologyEnded(media_event);
                break;
            }
        }
    }
    catch (CAtlException &) {}
    media_event.Release();
    return 0;
}
HRESULT CMyWindow::OnTopologyReady(CComPtr<IMFMediaEvent> mf_event)
{
    CHResult hr;
    PROPVARIANT varStart;
    PropVariantInit(&varStart);
    varStart.vt = VT_EMPTY;
    hr = m_MediaSession->Start(NULL, &varStart);
    return hr;
}
HRESULT CMyWindow::OnTopologyEnded(CComPtr<IMFMediaEvent> mf_event)
{
    MessageBox(_T("Completed"), APP_NAME, MB_OK | MB_ICONINFORMATION);
    DestroyWindow();
    return S_OK;
}

https://github.com/mahorigahama/mf_aacenc


Comments

comments powered by Disqus