sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

MediaPlayer ファイル読込、再生時の例外 0xC00D11BA

WPF の MediaPlayer の MediaFailed イベント は、オーディオデバイスが存在しないときに(も)発生するようです。その他の例は、いくつか記事がありました
これに関して、よろしくないのは MS Docs の概要に「Occurs when an error is encountered.」しかないことだと思います。例外の詳細がわからなくて原因を調べるにこまった……。0xC00D11BA の例外では、さすがに情報がなくてわからない。

f:id:shikaku_sh:20190422154110p:plain:w300
エラー番号しか特定できない

なので、この記事は、オーディオデバイスが存在するかどうかをチェックする方法に関する内容です。

開発環境

Windows 10 Pro x64
WPF + C#

f:id:shikaku_sh:20190422154244p:plain:w400
Visual Studio

オーディオデバイスの確認

オーディオデバイスを調べる方法は、おそらく .Net では用意が無いと思うので、ネイティブの DLL を呼び出す P/Invoke で調べることにしました。

まず、必要になるアンマネージド関数、構造体、列挙体を定義します。構造体は、StructLayout の属性などをつかって、データフィールドの物理的なレイアウト(アライメント)を制御しています。ネイティブな DLL が期待するレイアウトを自分で調べるのは面倒なので「pinvoke.net」などで調べてしまったほうが、はやいと思います。

[DllImport("winmm.dll", SetLastError = true)]
private static extern uint waveOutGetNumDevs();

[DllImport("winmm.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int waveOutGetDevCaps(int uDeviceID, ref WaveOutCaps pwoc, int cbwoc);

private enum MMRESULT : int
{
    MMSYSERR_NOERROR = 0,
    MMSYSERR_ERROR = 1,
    MMSYSERR_BADDEVICEID = 2,
    MMSYSERR_NOTENABLED = 3,
    MMSYSERR_ALLOCATED = 4,
    MMSYSERR_INVALHANDLE = 5,
    MMSYSERR_NODRIVER = 6,
    MMSYSERR_NOMEM = 7,
    MMSYSERR_NOTSUPPORTED = 8,
    MMSYSERR_BADERRNUM = 9,
    MMSYSERR_INVALFLAG = 10,
    MMSYSERR_INVALPARAM = 11,
    MMSYSERR_HANDLEBUSY = 12,
    MMSYSERR_INVALIDALIAS = 13,
    MMSYSERR_BADDB = 14,
    MMSYSERR_KEYNOTFOUND = 15,
    MMSYSERR_READERROR = 16,
    MMSYSERR_WRITEERROR = 17,
    MMSYSERR_DELETEERROR = 18,
    MMSYSERR_VALNOTFOUND = 19,
    MMSYSERR_NODRIVERCB = 20,
    WAVERR_BADFORMAT = 32,
    WAVERR_STILLPLAYING = 33,
    WAVERR_UNPREPARED = 34
}

[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Auto)]
private struct WaveOutCaps
{
    public short wMid;
    public short wPid;
    public uint vDriverVersion;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string szPname;
    public int dwFormats;
    public short wChannels;
    public short wReserved1;
    public int dwSupport;
}

つぎに、オーディオデバイスの情報を取得するメソッドの作成です。Find メソッドがそれで、P/Invoke を使ったときの返り値は、MS Docs を参照にするとよいと思います。 1
MMRESULT の型には、他にもたくさんの値が存在していますが、見た感じはこれだけでよさそうです。

public class AudioDevice
{
    #region P/Invoke

        // 上記各種を定義

    #endregion

    #region Properties

    public string Name { get; private set; }
    public int Id { get; private set; }

    #endregion

    #region Initializes

    public AudioDevice()
    {

    }

    #endregion

    #region Public Methods

    public static IEnumerable<AudioDevice> Find()
    {
        var devices = new List<AudioDevice>();
        var waveOutCaps = new WaveOutCaps();
        var deviceCount = waveOutGetNumDevs();

        for (int id = 0; id < deviceCount; id++)
        {
            var codeValue = (MMRESULT)waveOutGetDevCaps(id, ref waveOutCaps, Marshal.SizeOf(typeof(WaveOutCaps)));
            var device = new AudioDevice();

            if (codeValue == MMRESULT.MMSYSERR_NOERROR)
            {
                device.Id = id;
                device.Name = waveOutCaps.szPname;
                devices.Add(device);
            }
            else
            {
                // エラー処理
                switch (codeValue)
                {
                    case MMRESULT.MMSYSERR_BADDEVICEID:
                        throw new IndexOutOfRangeException($"指定されたインデックス {id} は領域外の値です。");
                    case MMRESULT.MMSYSERR_NODRIVER:
                        throw new InvalidOperationException($"再生デバイスの提供がありませんでした。");
                    case MMRESULT.MMSYSERR_NOMEM:
                        throw new OutOfMemoryException("メモリーの割当またはロックができませんでした。");
                    default:
                        throw new InvalidOperationException($"インデックス {id} は、特定できないエラー ({codeValue}) が発生しました。");
                }
            }
        }

        return devices;
    }

    #endregion
}

結果の表示

private void Run()
{
    var devices = AudioDevice.Find();

    if (devices.Count() > 0)
    {
        foreach (var dev in devices)
        {
            Console.WriteLine($"{dev.Id}:{dev.Name}");
        }
    }
    else
    {
        Console.WriteLine("オーディオデバイスが存在しません。");
    }
}

ひとつもオーディオデバイスが存在しないときに音声の読み込みや再生をしても MediaFailed イベントが発生するので、Find の結果を見てから実行するかどうかを決定するようにするとよいと思います。次の図だと1件だけオーディオデバイスがあるので MediaPlayer を使おうかな、という感じです。

f:id:shikaku_sh:20190422154359p:plain:w400
イヤホンを1つ繋いだ状態の例

サンプルのプログラムをあげています。「DeviceCheckSample」です。

参考


  1. waveOutGetDevCaps function) とかを見ます。