0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

F#でリフレクションを駆使してWinRTを扱う

Last updated at Posted at 2020-06-24

F# でリフレクションを駆使して WinRT を扱ったコードを C# と比較します。

この記事のコードは以下のリポジトリに掲載しています。

C# のコードは以下の記事から引用します。実行結果はこちらを参照してください。

将来展望

現在開発中の C#/WinRT と F# 5.0 を組み合わせれば、リフレクションを駆使した泥臭いコードは不要になる見込みです。

概要

WinRT は通常の .NET アセンブリとは異なるため言語側でもサポートが必要です。記事執筆時点で F# には WinRT のサポートがないため、リフレクション経由で扱う必要があります。

特に WinRT で多用される非同期処理が問題になります。詳細は以下の記事を参照してください。

音声一覧

読み上げに使用できる音声一覧を取得します。

voices/Program.cs
using System;
using Windows.Media.SpeechSynthesis;

class Program
{
    static void Main()
    {
        foreach (var voice in SpeechSynthesizer.AllVoices) {
            Console.WriteLine("{0}: {1}", voice.Language, voice.Description);
        }
    }
}
voices.fsx
open System
let SpeechSynthesizer = Type.GetType(@"
    Windows.Media.SpeechSynthesis.SpeechSynthesizer,
    Windows.Foundation.UniversalApiContract,
    ContentType=WindowsRuntime")
for voice in SpeechSynthesizer.GetProperty("AllVoices").GetValue(null) :?> seq<obj> do
    let t = voice.GetType()
    let lang = t.GetProperty("Language"   ).GetValue(voice) :?> String
    let desc = t.GetProperty("Description").GetValue(voice) :?> String
    printfn "%s: %s" lang desc

インスタンスは obj のまま扱って、メンバーはリフレクション経由で触ります。かなり面倒です。

音声合成

音声を指定してしゃべらせます。

speak/Program.cs
using System;
using System.Threading.Tasks;
using Windows.Media.Core;
using Windows.Media.SpeechSynthesis;
using Windows.Media.Playback;

class Program
{
    static void SetVoice(SpeechSynthesizer synthesizer, string v) {
        foreach (var voice in SpeechSynthesizer.AllVoices) {
            if (voice.DisplayName == v) {
                synthesizer.Voice = voice;
                break;
            }
        }
    }

    static async Task Main()
    {
        var voice = "Microsoft Ichiro";
        var text  = "こんにちは、世界";
        var synthesizer = new SpeechSynthesizer();
        SetVoice(synthesizer, voice);
        var stream = await synthesizer.SynthesizeTextToStreamAsync(text);
        var player = new MediaPlayer();
        player.Source = MediaSource.CreateFromStream(stream, stream.ContentType);
        var tcs = new TaskCompletionSource<int>();
        player.MediaEnded += (sender, o) => tcs.SetResult(0);
        player.Play();
        await tcs.Task;
    }
}
speak.fsx
#r "System.Runtime.WindowsRuntime"

open System
open System.Threading.Tasks

let SpeechSynthesizer = Type.GetType @"
    Windows.Media.SpeechSynthesis.SpeechSynthesizer,
    Windows.Foundation.UniversalApiContract,
    ContentType=WindowsRuntime"
let SpeechSynthesisStream = Type.GetType @"
    Windows.Media.SpeechSynthesis.SpeechSynthesisStream,
    Windows.Foundation.UniversalApiContract,
    ContentType=WindowsRuntime"
let MediaPlayer = Type.GetType @"
    Windows.Media.Playback.MediaPlayer,
    Windows.Foundation.UniversalApiContract,
    ContentType=WindowsRuntime"
let TypedEventHandler(a, b) = Type.GetType(@"
    Windows.Foundation.TypedEventHandler`2,
    Windows.Foundation.FoundationContract,
    ContentType=WindowsRuntime").MakeGenericType(a, b)

let AsTaskGeneric = query {
    for m in typeof<WindowsRuntimeSystemExtensions>.GetMethods() do
    where (m.Name = "AsTask")
    let ps = m.GetParameters()
    where (ps.Length = 1 && ps.[0].ParameterType.Name = "IAsyncOperation`1")
    select m
    exactlyOne }
let await resultType winRtTask =
    let AsTask = AsTaskGeneric.MakeGenericMethod [|resultType|]
    let task = AsTask.Invoke(null, [|winRtTask|])
    task.GetType().GetProperty("Result").GetValue(task)

let setVoice synthesizer (v: string) =
    let AllVoices = SpeechSynthesizer.GetProperty("AllVoices")
    match [ for voice in AllVoices.GetValue(null) :?> seq<obj> do
            let DisplayName = voice.GetType().GetProperty("DisplayName")
            if (DisplayName.GetValue(voice) :?> String) = v then yield voice ] with
    | [voice] -> synthesizer.GetType().GetProperty("Voice").SetValue(synthesizer, voice)
    | _ -> ()

let voice = "Microsoft Ichiro"
let text  = "こんにちは、世界"
let synthesizer = Activator.CreateInstance(SpeechSynthesizer)
setVoice synthesizer voice
let stream =
    SpeechSynthesizer.GetMethod("SynthesizeTextToStreamAsync").Invoke(synthesizer, [|text|])
    |> await SpeechSynthesisStream
let player = Activator.CreateInstance(MediaPlayer)
MediaPlayer.GetMethod("SetStreamSource").Invoke(player, [|stream|])
let tcs = TaskCompletionSource<unit>()
type C = static member f(_: obj, _: obj) = tcs.SetResult()
let d = Delegate.CreateDelegate(TypedEventHandler(MediaPlayer, typeof<obj>), typeof<C>.GetMethod("f"))
MediaPlayer.GetEvent("MediaEnded").AddMethod.Invoke(player, [|d|])
MediaPlayer.GetMethod("Play").Invoke(player, [||])
tcs.Task.Result

かなり厳しい感じです。この調子で複雑なものを作るのは、あまり現実的ではない気がします。

イベントハンドラ

型を指定してデリゲートを作るには MethodInfo が必要です。F# の関数から MethodInfo を取得しようとすると、IL を解析するようなハックが必要になるようです。

そこまでしなくてもクラスを定義すれば済むため、メソッドが1つだけのクラスを作ります。

type C = static member f(_: obj, _: obj) = tcs.SetResult()
let d = Delegate.CreateDelegate(TypedEventHandler(MediaPlayer, typeof<obj>), typeof<C>.GetMethod("f"))

TypedEventHandler は2つの引数を持つジェネリック デリゲートです。

public delegate void TypedEventHandler<TSender,TResult>(TSender sender, TResult args);

TypedEventHandler<MediaPlayer, Object>C.f では第1引数の型が異なりますが、引数の反変性として容認されます。

17_反変引数.png

感想

いずれ不要になることも分かっているので、あまり頑張っても仕方ない気はしますが、リフレクションで動的に型を扱う練習にはなると思いました。(WinRT でなくてもできますが)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?