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