Help us understand the problem. What is going on with this article?

C# (Xamarin) で Android の録音アプリを実装してみる

More than 3 years have passed since last update.

Android アプリを作成してみたくて、慣れない Java に奮闘していたところ、MS様から Xamarin 無償提供の吉報!
Android.Media.AudioRecord クラスを利用して、リアルタイムで録音を行うアプリケーションを早速作成してみました。
途中、主なてこずった点が以下です。

コード例

一部の機能のコード例(録音部のみ、しかもひたすら蓄積)です。実用には録音開始・終了・再生・保存などの機能を追加する必要があります。
Visual Studio 2015 Community で、プロジェクトテンプレート C# -> Android -> Blank App (Android) を選択してプロジェクト(ソリューション)を生成しています。
プロジェクトのプロパティにある Android Manifest の Required Permissions から RECORD_AUDIO にチェックを入れて、録音に関してアクセス許可を設定してあります。

起動すると、マイクから録音し、WAV 形式でキュー(自前構築の AudioBuffer クラス)へ蓄積していきます。

using System;
using Android.App;
using Android.Media;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;

namespace TestProgram
{
    [Activity(Label = "TestProgram", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        private const int c_samplesPerSecond = 8000; // 11025, 22050, 44100, ...

        private short[] _buffer = null;
        private AudioRecord _audioRecord = null;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            AudioBuffer.Instance.Frames = AudioRecord.GetMinBufferSize(c_samplesPerSecond, ChannelIn.Mono, Encoding.Pcm16bit);
            _buffer = new short[AudioBuffer.Instance.Frames];
            _audioRecord = new AudioRecord(AudioSource.Mic, c_samplesPerSecond, ChannelIn.Mono, Encoding.Pcm16bit, _buffer.Length * 2);
            _audioRecord.SetRecordPositionUpdateListener(new OnRecordPositionUpdateListener());
            _audioRecord.SetPositionNotificationPeriod(AudioBuffer.Instance.Frames);
            //                    _audioRecord.SetNotificationMarkerPosition(AudioBuffer.Instance.Frames); 用途によってはこちら
            _audioRecord.StartRecording();
            AudioBuffer.Instance.Enqueue(_audioRecord); // 最初に空読みさせないと、リスナーのイベントが発生しないらしい
        }

        public class OnRecordPositionUpdateListener : Java.Lang.Object, AudioRecord.IOnRecordPositionUpdateListener
        {
            public void OnMarkerReached(AudioRecord recorder)
            {
                AudioBuffer.Instance.Enqueue(recorder);
            }

            public void OnPeriodicNotification(AudioRecord recorder)
            {
                AudioBuffer.Instance.Enqueue(recorder);
            }
        }

        public class AudioBuffer
        {
            public static AudioBuffer Instance = new AudioBuffer();
            private System.Collections.Generic.Queue<short[]> Buffer = new System.Collections.Generic.Queue<short[]>();
            public int Frames { get; set; }
            public int Count { get { return this.Buffer.Count; } }

            public void Enqueue(AudioRecord ar)
            {
                var buff = new short[this.Frames];
                ar.Read(buff, 0, buff.Length);
                this.Buffer.Enqueue(buff);
            }

            public short[] Dequeue()
            {
                return (this.Buffer.Count == 0) ? null : this.Buffer.Dequeue();
            }
        }
    }
}

てこずった点

AudioRecord.GetMinBufferSizeメソッドで -2 が返る

AudioRecord.GetMinBufferSize メソッドで、サンプリングレート、チャネル、ビット数など、動作させるデバイスで利用できない条件を指定すると、-2 が返されるようです。

SetRecordPositionUpdateListenerメソッドで、AudioRecord.IOnRecordPositionUpdateListener をインプリメントしても不足する

SetRecordPositionUpdateListener メソッドのシグネチャは

public virtual void SetRecordPositionUpdateListener(IOnRecordPositionUpdateListener listener);
// または
public virtual void SetRecordPositionUpdateListener(IOnRecordPositionUpdateListener listener, Handler handler);

なので、

public class OnRecordPositionUpdateListener : Java.Lang.Object, AudioRecord.IOnRecordPositionUpdateListener

の基底クラス、インターフェイスを

public class OnRecordPositionUpdateListener : AudioRecord.IOnRecordPositionUpdateListener

と、最初コーディング。これは自動展開させると次のようなスケルトンが生成されます。

public class AudioBuffer : AudioRecord.IOnRecordPositionUpdateListener
{
    public IntPtr Handle
    {
        get
        {
            throw new NotImplementedException();
        }
    }

    public void Dispose()
    {
        throw new NotImplementedException();
    }

    public void OnMarkerReached(AudioRecord recorder)
    {
        throw new NotImplementedException();
    }

    public void OnPeriodicNotification(AudioRecord recorder)
    {
        throw new NotImplementedException();
    }
}

しかし、これらをすべて実装(実体をコーディング:Handle には AudioRecord オブジェクトの Handle プロパティを代入)しても

Unhandled Exception: Java.Lang.IncompatibleClassChangeError: interface not implemented

がスローされてしまいます。
Xamarin や Mono の何かがおかしいのだろうと半ばあきらめてほかの実装法を探っていましたが、
ビルド中のメッセージをよくよくみると

1>Type 'TestProgram.MainActivity/OnRecordPositionUpdateListener' implements Android.Runtime.IJavaObject but does not inherit from Java.Lang.Object. It is not supported.

と書かれているのを発見。メッセージ通り Java.Lang.Object からも派生させてみたところ、見事うまく動くようになりました。

イベントハンドラは呼ばれない

次のイベントはどう使うのか未だにわからずじまいです。名前からすると、上記のような面倒な実装をせずとも、それぞれ録音データがたまったらイベントハンドラを呼び出してくれそうなのですが、どうも呼ばれませんね。。SetNotificationMarkerPosition メソッドや SetPositionNotificationPeriod メソッド以外にも、何かおまじないが要るのでしょうか?

// 1つめ
public event EventHandler<MarkerReachedEventArgs> MarkerReached;
// 2つめ
public event EventHandler<PeriodicNotificationEventArgs> PeriodicNotification;

AudioRecord.IOnRecordPositionUpdateListener インターフェイスは、MainActivity クラスにインプリメントさせるほうが、Java 流?の書き方に沿っているようですね。Windows アプリ開発経験のほうが長いので、まだあまりしっくりこなくて...

Xamarin での録音・再生に関する素敵な情報、もっときれいな書き方などありましたら、ぜひご教示くださいまぜ!

otagaisama-1
自社の開発・運用で、皆様からの情報にとてもお世話になっています。ほぼ1人情SYSの身で、他のネット情報には少ないレアめなシステムを自社で使っている点でも、今めいたシステム情報入手面でも、Qiita のみなさまはとても貴重です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away