Xamarin.Android で音声ファイルを順次再生する方法

の回答で書いたやつなんですが。

メディアファイル「a.mp3」「b.mp3」「c.mp3」があり、 a の再生が終わったら b を再生…とする方法です(MediaPlayer 使用)。

//using Android.App;
//using Android.Widget;
//using Android.OS;
//using Android.Media;
//using System.Threading.Tasks;

public class MainActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.Main);
        var button = FindViewById<Button>(Resource.Id.myButton);

        var sounds = new int[] 
        {
            Resource.Raw.trumpet1,
            Resource.Raw.trumpet2
        };

        button.Click += async (sender, e) =>
        {
            foreach (var id in sounds)
            {
                await PlayAsync(id);
            }
        };
    }

    // 再生が終了したら true を、エラーだったら false を返す
    private Task<bool> PlayAsync(int rscId)
    {
        var compSource = new TaskCompletionSource<bool>();
        var mp = MediaPlayer.Create(this, rscId);

        mp.Completion += (_, __) =>
        {
            compSource.SetResult(true);
        };

        mp.Error += (_, __) =>
        {
            compSource.SetResult(false);
        };

        mp.Start();
        return compSource.Task;
    }
}

MediaPlayer は、再生が完了すると onCompletion を通知するので、それを受信して次の曲を再生開始すればよいのですが、普通に書くとコールバック地獄に陥るので、Task<T> 化して、フラットに書けるようにします。

このような、「非同期処理で完了がイベントやコールバックで通知されるやつ」を Task<T> な非同期メソッドに変換するために TaskCompletionSource<T> を使う方法、は非常によく使うので覚えておくとよいと思います。過去にはダイアログボックスの表示について同様のテクニックで async/await 化する方法を書きました。

この Task<T> を使ったテクニックは 「C# ならでは」 でしたが、Androidアプリ開発の公式言語である Kotlin でも同じようなことができます。

package nepula.net.soundsample

import android.media.MediaPlayer
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.experimental.launch
import kotlin.coroutines.experimental.suspendCoroutine

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sounds = arrayOf(
            R.raw.trumpet1,
            R.raw.trumpet2
        )

        button.setOnClickListener {
            launch {
                sounds.forEach {id ->
                    playAsync(id)
                }
            }
        }
    }

    suspend fun playAsync(id:Int) : Boolean {
        return suspendCoroutine { cont : Continuation<Boolean> ->
            val mp = MediaPlayer.create(this, id)

            mp.setOnCompletionListener {
                cont.resume(true)
            }

            mp.setOnErrorListener ( object : MediaPlayer.OnErrorListener {
                override fun onError(p0: MediaPlayer?, p1: Int, p2: Int): Boolean {
                    cont.resume(false)
                    return true
                }
            })

            mp.start()
        }

    }
}

TaskCompletionSource<T> の代わりに Continuation<T> を使う感じで。 非同期処理でも、レスポンス(or エラー)が一発で終わるものは RxJava を使う必要はないので、上記のようなパターンもよく使いますね。