録音データをリアルタイムにJNIで処理する最もシンプルなサンプル

Androidにて、録音したデータをリアルタイムに処理する方法について書かれた記事が見つからなかったので書きます。ポイントはリアルタイムですのでJNIを使用します。

先に、今回作成したサンプルの画面（説明を加えたもの）を出しておきます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/64581/99777e22-271e-0ab5-6353-6637d1f18f75.png" width=50%>

リアルタイムに取得した入力データに対して、スライダーの設定値分だけ乗算した結果を返す、というシンプルなサンプルです。

そして、プログラムにおいてもJNIで処理する場合の最もシンプルなサンプルになっていると思います。
以下で説明していきます。

# プロジェクトの作成

現在のAndroid Studioのバージョン（2.2.1）では、プロジェクト名の設定を行う画面で、「Include C++ Support」というチェックボックスが表示されていると思いますので、このチェックボックスをオンにします。
（SDK Managerで、`CMake`と`NDK`はインストールしておく必要があります）

<img src="https://qiita-image-store.s3.amazonaws.com/0/64581/f844a680-43d3-eae9-bbe0-8c1378e294bc.png" width=75%>

プロジェクトを作成すると、ネイティブ用のソースファイルがすでに用意されています。

(プロジェクト名)/app/src/main/cpp/native-lib.cpp

また、MainActivity.javaにもすでに以下の記述が追加されています。

~~~java:MainActivity.java
    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
~~~

便利な世の中になったものだな〜と思ってしまいました。（Eclipseで作業していた頃は苦労しました）

# 実装

## パーミッション

まず、Manifestに録音のパーミッションを追加しておきます。

~~~AndroidManifest.xml
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
~~~

【追記 2019/1/1】
Android 6.0以降では、実装側でも録音のパーミッションに関する記述が必要になりました。

~~~java:MainActivity.java
	private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 100;

	private void checkPermission() {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
				// put your code for Version>=Marshmallow
			} else {
				if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
					Toast.makeText(this, "App required access to audio", Toast.LENGTH_SHORT).show();
				}
				requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_RECORD_AUDIO);
			}

		} else {
			// put your code for Version < Marshmallow
		}

	}

	@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);

		if (requestCode == MY_PERMISSIONS_REQUEST_RECORD_AUDIO) {
			if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
				Toast.makeText(getApplicationContext(), "Application will not have audio on record", Toast.LENGTH_SHORT).show();
			}
		}
	}
~~~

## 録音の実装

録音の実装は`AudioRecord`クラスを用います。

~~~java:MainActivity.java
	public final static int SAMPLING_RATE = 44100;

	private AudioRecord m_audioRecord = null;
~~~

AudioRecordの準備、初期化、録音開始の処理をまとめました。

~~~java:MainActivity.java
	// AudioRecordで必要なバッファサイズを取得
	int bufSize = android.media.AudioRecord.getMinBufferSize(SAMPLING_RATE,
			AudioFormat.CHANNEL_IN_MONO,
			AudioFormat.ENCODING_PCM_16BIT);

	// AudioRecordオブジェクトを作成
	m_audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
			SAMPLING_RATE,
			AudioFormat.CHANNEL_IN_MONO,
			AudioFormat.ENCODING_PCM_16BIT,
			bufSize);

	// Rec開始
	m_audioRecord.startRecording();

~~~

録音開始後、マイクからのサウンドデータの読み取りをスレッド内で行います。ここでJNIの処理も行います。
（以下のコードの詳細は、最後に紹介するGithubを参照してください）

~~~java:MainActivity.java
	Thread thread = new Thread(new Runnable() {
		@Override
		public void run() {
			while (m_isPlaying) {
				// 読み込み
				m_audioRecord.read(m_sInputData, 0, m_iSampleSize);

				int numberFrames = m_iSampleSize;

				for (int i = 0; i < numberFrames; i++) {
					m_fInputBuffer[i] = (float)m_sInputData[i];
					m_lAveValue[0] += m_sInputData[i];
				}
				m_lAveValue[0] /= numberFrames;

				// Native処理
				processAudio(numberFrames, m_fInputBuffer, m_fOutputBuffer, m_fRateTest);

				for (int i = 0; i < numberFrames; i++) {
					m_sOutputData[i] = (short)m_fOutputBuffer[i];
					m_lAveValue[1] += m_sOutputData[i];
				}
				m_lAveValue[1] /= numberFrames;

				runOnUiThread(new Runnable() {
					@Override
					public void run() {
						textView[0].setText(String.valueOf(m_lAveValue[0]));
						textView[1].setText(String.valueOf(m_lAveValue[1]));
					}
				});
			}
		}
	});
	thread.start();
~~~

上で呼び出しているprocessAudio関数が、今回サンプルとして用意したJNIの関数です。

定義は以下のように変更しました。

~~~java:MainActivity.java
	public native float[] processAudio(long numberFrames, float[] fInputData, float[] fOutputData, float fRateTest);
~~~

以下は、入力データ（inputBuffer）にスライダーを設定した値（fRateTest）を乗算して結果を出力データ（outputBuffer）とした例です。簡単ですね。

~~~cpp:native-lib.cpp
#include <jni.h>

extern "C"
JNIEXPORT jfloatArray JNICALL
Java_com_loopsessions_audiorecordsample_MainActivity_processAudio(JNIEnv* env, jobject thiz, jlong numberFrames, jfloatArray inputBuffer, jfloatArray outputBuffer, jfloat fRateTest)
{
    jfloat* fInputBuffer = env->GetFloatArrayElements(inputBuffer, 0);
    jfloat* fOutputBuffer = env->GetFloatArrayElements(outputBuffer, 0);

    // テスト処理（fRateTest倍）
    for (int i = 0; i < numberFrames; i++) {
        fOutputBuffer[i] = fInputBuffer[i] * fRateTest;
    }

    env->ReleaseFloatArrayElements(inputBuffer, fInputBuffer, 0);
    env->ReleaseFloatArrayElements(outputBuffer, fOutputBuffer, 0);
    env->DeleteLocalRef(thiz);

    return outputBuffer;
}
~~~

# 最後に

タイトルにあるように、「JNIで処理する最もシンプルなサンプル」ですので、JNIでの処理は最もシンプルにしたつもりです。
ただ、現在のJNIでのオーディオ処理の主流は`OpenSL ES`になっています。しかし実装量はかなり増えることになります。処理能力や精度面で差異が出るかを確認しつつ、実装方法を選択するのも良いかと思います。
別の機会でOpenSL ESについても触れたいと思います。

# サンプル

本記事の実装方法については、以下のサンプルプログラムを参照してください。
https://github.com/JunichiMinamino/AudioRecordSample