ホーム >プログラム >Delphi 6 ローテクTips >amazonのmp3をDirectShowで再生

amazonのmp3をDirectShowで再生

amazonでダウンロードしたmp3ファイルは普通にやったのではDirectShowでは再生できません。
ID3タグを削れば再生できるのですが、それでは折角のタグがもったいないし…
ということで色々やって何とか再生にまで漕ぎ着けました。


参考サイト


WM ASF Reader

始めに

amazonからダウンロードしたmp3は普通にやったのではDirectShowでは再生できません。
GraphBuilderのRenderFileでエラーになってしまいます。
とうことでGraphEdit.exeを使ってあれこれやってみたところWM ASF Readerというフィルターで読み込めば再生できることが分かりました。
このWM ASF Readerは自分で作成してグラフに追加しなければなりません。
それだけでなくオーディオレンダラーと繋ぐことも自分でやらなければなりません。
結構大変です。

作成

作成はCoCreateInstanceを使います。
GraphBuilderやビデオレンダラーを作成するのと大差ないので問題ありません。

  //WM ASF Reader作成。
  CoCreateInstance(
    CLSID_WMAsfReader,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_IBaseFilter,
    l_ASFReader
  );
  //ファイル読み込み。
  //http://www.codeproject.com/KB/directx/saveimagefromstreamingurl.aspx
  F_GraphBuilder.AddFilter(l_ASFReader, 'WM ASF Reader');
  l_ASFReader.QueryInterface(IID_IFileSourceFilter, l_FileSourceFilter);
  l_FileSourceFilter.Load(PWideChar(sFileName), nil);
  l_FileSourceFilter := nil;

ここまでは問題ありません。

接続

オーディオレンダラーと繋ぐにはICaptureGraphBuilder2インターフェースを利用するのが便利だというのでやってみたのですがうまくいきません。

  //WM ASF Reader作成。
  CoCreateInstance(
    CLSID_WMAsfReader,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_IBaseFilter,
    l_ASFReader
  );
  //ファイル読み込み。
  //http://www.codeproject.com/KB/directx/saveimagefromstreamingurl.aspx
  F_GraphBuilder.AddFilter(l_ASFReader, 'WM ASF Reader');
  l_ASFReader.QueryInterface(IID_IFileSourceFilter, l_FileSourceFilter);
  l_FileSourceFilter.Load(PWideChar(sFileName), nil);
  l_FileSourceFilter := nil;

  //CaptureGraphBuilder2作成。
  CoCreateInstance(
    CLSID_CaptureGraphBuilder2,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_ICaptureGraphBuilder2,
    l_CaptureGraphBuilder2
  );
  l_CaptureGraphBuilder2.SetFiltergraph(F_GraphBuilder);
  //サウンドのフィルターを繋ぐ。
  li_Ret := l_CaptureGraphBuilder2.RenderStream(nil, nil, l_ASFReader, nil, nil); //失敗する。

CaptureGraphBuilder2のRenderStreamの説明を読むと、第三引数にはフィルターかPinを指定するとなっています。
フィルターを指定する場合は第一引数と第二引数にメディアタイプを指定するようなのですが、それをやってもうまくきません。
それではというのでWM ASF Readerフィルターのピンを取得して渡してもこれまたうまくいきません。
ということであれこれやってみた結果どうやらデフォルトで使われるオーディオレンダラーもグラフに追加してからRenderStreamで繋がないといけないようでした。

オーディオレンダラーの作成

で、ここからが問題なのですがデフォルトで使われるオーディオレンダラー(Default DirectSound Device)を作成する方法が分からないのです。
GraphEdit.exeのフィルター一覧の「Audio Renderers」の「Default DirectSound Device」のDisplayName欄を見てもCLSIDが載っていません。
DirectShow9.pasやDirectSound.pasでそれらしきものを探しても見つかりません。
CLSIDが分からなければCoCreateInstanceで作成できないのでどうにもなりません。

あれこれやった時には、amazonのmp3とは別の開けるファイルをGraphBuilderのRednerFileで開いてからグラフのフィルターのピン接続を全部切り、FindFilterByNameでDefault DirectSound Deviceを取得していました。
さすがに別のファイルを余分に読み込ませてDefault DirectSound Deviceを取得するというのは邪道なのでもう少しまともなやり方を探りました。

その結果システムに登録されているデバイスを列挙してその中からDefault DirectSound Deviceを探し当てることで作成できることが分かりました。
単にデフォルトのオーディオデバイスを使いたいだけなのに意外と手間がかかります。
(私が遠回りしてるだけなのかもしれませんが)

以下の関数はフィルターのカテゴリのGUIDとフレンドリ名を与えるとABaseFilterに該当するフィルタがセットされるもので、デフォルトのオーディオデバイスを取得するには

  CreateBaseFilter(F_AudioRenderer, CLSID_AudioRendererCategory, 'Default DirectSound Device');

のようにします。

function CreateBaseFilter(var ABaseFilter : IBaseFilter; ACategory : TGUID; sFriendryName : WideString) : HResult;
//登録フィルターを列挙して該当するフレンドリ名のフィルタを作成する
//http://msdn.microsoft.com/ja-jp/library/cc371168.aspx
//http://msdn.microsoft.com/ja-jp/library/cc353921.aspx
//http://www.geekpage.jp/programming/directshow/list-capture-device-2.php
//http://miraiware.net/memo/dshow-device.html

var
  l_CreateDevEnum : ICreateDevEnum;
  l_EnumMoniker   : IEnumMoniker;
  l_Moniker       : IMoniker;
  l_PropertyBag   : IPropertyBag;
  l_varName       : OleVariant;
  ls_Name         : WideString;
begin
  Result := CoCreateInstance(
    CLSID_SystemDeviceEnum,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_ICreateDevEnum,
    l_CreateDevEnum
  );
  if (FAILED(Result)) then begin
    Exit;
  end;

  try
    Result := l_CreateDevEnum.CreateClassEnumerator(ACategory, l_EnumMoniker, 0);
    if (Result <> S_OK) then begin
      Exit;
    end;
    //モニカを列挙する。
    while (l_EnumMoniker.Next(1, l_Moniker, nil) = S_OK) do begin
      try
        Result := l_Moniker.BindToStorage(nil, nil, IID_IPropertyBag, l_PropertyBag);
        if (Succeeded(Result)) then begin
          // フィルタのフレンドリ名を取得するには、次の処理を行う。
          VariantInit(l_varName);
          l_PropertyBag.Read('FriendlyName', l_varName, nil);
          ls_Name := WideString(l_varName);
          VariantClear(l_varName);
          if (WideUpperCase(ls_Name) = WideUpperCase(sFriendryName)) then begin
            //フィルタ作成。
            Result := l_Moniker.BindToObject(nil, nil, IID_IBaseFilter, ABaseFilter);
            Exit;
          end;
        end;
      finally
        l_PropertyBag := nil;
        l_Moniker     := nil;
      end;
    end;
    //フィルタが作成された場合は途中でExitするのでここに来る場合は作成失敗であるのでE_Failを返す。
    Result := E_Fail;
  finally
    l_EnumMoniker   := nil;
    l_CreateDevEnum := nil;
  end;
end;

フィルターのカテゴリとフレンドリ名を変えればデフォルトのオーディオレンダラーだけでなくシステムに登録されているデバイスを色々取得できます。

サンプルコード

unit main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls,
  ActiveX,
  DirectShow9;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    F_GraphBuilder  : IGraphBuilder;
    F_AudioRenderer : IBaseFilter;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

function GetUnconnectedPin(pFilter : IBaseFilter; PinDir : PIN_DIRECTION; var ppPin : IPin) : HResult;
function ConnectFilters(pGraph : IGraphBuilder; pOut : IPin;        pDest : IBaseFilter) : HResult; overload;
function ConnectFilters(pGraph : IGraphBuilder; pSrc : IBaseFilter; pDest : IBaseFilter) : HResult; overload;
function CreateBaseFilter(var ABaseFilter : IBaseFilter; ACategory : TGUID; sFriendryName : WideString) : HResult;


implementation
uses
  DSUtil;

{$R *.dfm}


function GetUnconnectedPin(pFilter : IBaseFilter; PinDir : PIN_DIRECTION; var ppPin : IPin) : HResult;
//http://msdn.microsoft.com/ja-jp/library/cc973418.aspx
var
  li_Ret     : HResult;
  pEnum      : IEnumPins;
  pPin       : IPin;
  pTmp       : IPin;
  ThisPinDir : TPinDirection ;
begin
  ppPin  := nil;
  pEnum  := nil;
  pPin   := nil;
  li_Ret := pFilter.EnumPins(pEnum);
  if (FAILED(li_Ret)) then begin
    Result := li_Ret;
    Exit;
  end;

  while (pEnum.Next(1, pPin, nil) = S_OK) do begin
    pPin.QueryDirection(ThisPinDir);
    if (ThisPinDir = PinDir) then begin
      pTmp   := nil;
      li_Ret := pPin.ConnectedTo(pTmp);
      if (SUCCEEDED(li_Ret)) then begin
        // 既に接続済み、必要なピンではない。
        pTmp := nil;
      end else begin
        // 未接続、これが必要なピンである。
        pEnum := nil;
        ppPin := pPin;
        Result := S_OK;
        Exit;
      end;
    end;
    pPin := nil;
  end;
  pEnum := nil;
  // 一致するピンが見つからなかった。
  Result := E_FAIL;
end;

function ConnectFilters(
  pGraph : IGraphBuilder; // フィルタ グラフ マネージャ。
  pOut   : IPin;          // アップストリーム フィルタの出力ピン。
  pDest  : IBaseFilter    // ダウンストリーム フィルタ。
) : HResult;
//http://msdn.microsoft.com/ja-jp/library/cc973418.aspx
var
  PinDir : TPinDirection;
  pIn    : IPin;
begin
  if (pGraph = nil)
  or (pOut   = nil)
  or (pDest  = nil)
  then begin
    Result := E_POINTER;
    Exit;
  end;

  pOut.QueryDirection(PinDir);
  if (PinDir = PINDIR_OUTPUT) then begin
    //アップストリームフィルターのピンが出力ピンではなかった。
    Result := E_FAIL;
    Exit;
  end;

  // ダウンストリーム フィルタの入力ピンを検索する。
  pIn    := nil;
  Result := GetUnconnectedPin(pDest, PINDIR_INPUT, pIn);
  if (FAILED(Result)) then begin
    Exit;
  end;

  // 接続を試す。
  Result := pGraph.Connect(pOut, pIn);
  pIn    := nil;
end;

function ConnectFilters(
  pGraph : IGraphBuilder;
  pSrc   : IBaseFilter;
  pDest  : IBaseFilter
) : HResult;
//http://msdn.microsoft.com/ja-jp/library/cc973418.aspx
var
  pOut : IPin;
begin
  if (pGraph = nil)
  or (pSrc   = nil)
  or (pDest  = nil)
  then begin
    Result := E_POINTER;
    Exit;
  end;

  // 最初のフィルタの出力ピンを検索する。
  pOut   := nil;
  Result := GetUnconnectedPin(pSrc, PINDIR_OUTPUT, pOut);
  if (FAILED(Result)) then begin
    Exit;
  end;

  Result := ConnectFilters(pGraph, pOut, pDest);
  pOut   := nil;
end;

function CreateBaseFilter(var ABaseFilter : IBaseFilter; ACategory : TGUID; sFriendryName : WideString) : HResult;
//登録フィルタを列挙して該当するフレンドリ名のフィルタを作成する
//http://msdn.microsoft.com/ja-jp/library/cc371168.aspx
//http://msdn.microsoft.com/ja-jp/library/cc353921.aspx
//http://www.geekpage.jp/programming/directshow/list-capture-device-2.php
//http://miraiware.net/memo/dshow-device.html

var
  l_CreateDevEnum : ICreateDevEnum;
  l_EnumMoniker   : IEnumMoniker;
  l_Moniker       : IMoniker;
  l_PropertyBag   : IPropertyBag;
  l_varName       : OleVariant;
  ls_Name         : WideString;
begin
  Result := CoCreateInstance(
    CLSID_SystemDeviceEnum,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_ICreateDevEnum,
    l_CreateDevEnum
  );
  if (FAILED(Result)) then begin
    Exit;
  end;

  try
    Result := l_CreateDevEnum.CreateClassEnumerator(ACategory, l_EnumMoniker, 0);
    if (Result <> S_OK) then begin
      Exit;
    end;
    //モニカを列挙する。
    while (l_EnumMoniker.Next(1, l_Moniker, nil) = S_OK) do begin
      try
        Result := l_Moniker.BindToStorage(nil, nil, IID_IPropertyBag, l_PropertyBag);
        if (Succeeded(Result)) then begin
          // フィルタのフレンドリ名を取得するには、次の処理を行う。
          VariantInit(l_varName);
          l_PropertyBag.Read('FriendlyName', l_varName, nil);
          ls_Name := WideString(l_varName);
          VariantClear(l_varName);
          if (WideUpperCase(ls_Name) = WideUpperCase(sFriendryName)) then begin
            //フィルタ作成。
            Result := l_Moniker.BindToObject(nil, nil, IID_IBaseFilter, ABaseFilter);
            Exit;
          end;
        end;
      finally
        l_PropertyBag := nil;
        l_Moniker     := nil;
      end;
    end;
    //フィルタが作成された場合は途中でExitするのでここに来る場合は作成失敗であるのでE_Failを返す。
    Result := E_Fail;
  finally
    l_EnumMoniker   := nil;
    l_CreateDevEnum := nil;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  li_Ret             : HResult;
  l_WMASFReader      : IBaseFilter;
  l_FileSourceFilter : IFileSourceFilter;
  l_MediaControl     : IMediaControl;
begin
  if (OpenDialog1.Execute) then begin
    F_AudioRenderer := nil;
    F_GraphBuilder  := nil;

    //グラフ作成
    CoCreateInstance(
      CLSID_FilterGraph,
      nil,
      CLSCTX_INPROC,
      IID_IGraphBuilder,
      F_GraphBuilder
    );

    //ファイル読み込み。
    li_Ret := F_GraphBuilder.RenderFile(PWideChar(WideString(OpenDialog1.FileName)), nil);
    if (FAILED(li_Ret)) then begin
      //amazonのmp3は↑だと失敗するのでWM ASF Readerを使う。
      //WM ASF Readerは自分で作成してグラフに追加しておかなければならない。
      //更に繋ぐオーディオレンダラーも(デフォルトのものであっても)自分で作成してグラフに追加しておかなくてはならない。

      CoCreateInstance(
        CLSID_WMAsfReader,
        nil,
        CLSCTX_INPROC_SERVER,
        IID_IBaseFilter,
        l_WMASFReader
      );
      //http://www.codeproject.com/KB/directx/saveimagefromstreamingurl.aspx
      F_GraphBuilder.AddFilter(l_WMASFReader, 'WM ASF Reader');
      l_WMASFReader.QueryInterface(IID_IFileSourceFilter, l_FileSourceFilter);
      l_FileSourceFilter.Load(PWideChar(WideString(OpenDialog1.FileName)), nil);
      l_FileSourceFilter := nil;

      CreateBaseFilter(F_AudioRenderer, CLSID_AudioRendererCategory, 'Default DirectSound Device');
      F_GraphBuilder.AddFilter(F_AudioRenderer, 'Default DirectSound Device');
      li_Ret := ConnectFilters(F_GraphBuilder, l_WMASFReader, F_AudioRenderer);
      l_WMASFReader := nil;
    end;

    F_GraphBuilder.QueryInterface(IMediaControl, l_MediaControl);
    l_MediaControl.Run;
    l_MediaControl := nil;
  end;
end;

注意点

uses節にActiveXとDirectShow9とDSUtilを付け足す必要があります。
DirectShow9.pasとDSUilt.pasはDSPackの中に入っています

D6のファイル選択ダイアログのFileNameプロパティはAnsiStringなのでF_GraphBuilder.RenderFileやl_FileSourceFilter.Loadでファイル名を渡す時にはWideStringでキャストしてからPWideCharでキャストします。

この例ではフィルターを接続するのにConnectFilters関数を使用していますがICaptureGraphBuilder2インターフェースを使用しても接続できます。
ICaptureGraphBuilder2インターフェースを使わずに接続したのは単にあれこれやった時にフィルターの接続を切ったり繋いだりする関数を作っておいたのでそっちの方が色々試せて便利だったからというだけのことです。

制限

テスト用実行ファイル

test.zip 実行ファイルとソースコードの詰め合わせ。