もっと波形を作る - サンプラー

この Codelab では、音声サンプラーを作成します。このアプリは、スマートフォンの内蔵マイクから音声を録音し、再生します。

このアプリでは、[Record] ボタンを押している間に最長 10 秒間の音声を録音できます。[Play] を押すと、録音した音声が(ボタンを長押ししている間に)1 回再生されます。または [Loop] を選択すると、[Play] ボタンを離すまで、録音を何回も繰り返し再生できます。[Record] を押すたびに、以前に録音した音声が上書きされます。

7eb653b71774dfed.png

学習内容

  • 低レイテンシの録音ストリームを作成するための基本的なコンセプト
  • マイクから録音した音声データを保存し再生する方法

前提条件

この Codelab を開始する前に、WaveMaker パート 1 の Codelab を完了しておくことをおすすめします。パート 1 では、今回扱わない音声ストリームの作成の基本的なコンセプトを取り上げています。

必要なもの

このサンプラー アプリには 4 つのコンポーネントがあります。

  • UI - Java で記述されており、MainActivity クラスがタッチイベントの受信と JNI ブリッジへの転送を担います。
  • JNI ブリッジ - この C++ ファイルは、JNI を使用して UI と C++ オブジェクト間の通信メカニズムを提供します。UI からオーディオ エンジンにイベントを転送します。
  • オーディオ エンジン - この C++ クラスによって、録音 / 再生の音声ストリームが作成されます。
  • サウンド レコーディング - この C++ クラスによって、音声データがメモリに保存されます。

アーキテクチャは次のようになっています。

a37150c7e35aa3f8.png

プロジェクトのクローンを作成する

GitHub で Codelab リポジトリのクローンを作成します。

git clone https://github.com/googlecodelabs/android-wavemaker2

Android Studio にプロジェクトをインポートする

Android Studio を開き、プロジェクトをインポートします。

  • [File] > [New] > [Import project]
  • android-wavemaker2」フォルダを選択します。

プロジェクトを実行する

基本実行構成を選択します。

f65428e71e9bdbcf.png

その後、Ctrl+R キーを押してテンプレート アプリをビルドし実行します。コンパイルと実行が行われますが、動作はしません。この Codelab で、機能を追加します。

ベース モジュールを開く

この Codelab で作業するファイルは base モジュールに格納されます。[Project] ウィンドウでこのモジュールを展開し、[Android] ビューが選択されていることを確認します。

cae7ee7b54407790.png

注: WaveMaker2 アプリの完成済みソースコードは、final モジュールで確認できます。

録音された音声データは、メモリ内で SoundRecording オブジェクトによって表されます。これによりアプリが、マイクからのデータをメモリに書き込み、再生用に読み出せるようになります。

まずは、この音声データの保存方法について確認しましょう。

音声フォーマットを選択する

まず、サンプルの音声フォーマットを選択する必要があります。AAudio では、次の 2 つのフォーマットがサポートされています

  • float: 単精度浮動小数点(1 サンプルあたり 4 バイト)
  • int16_t: 16 ビット整数(1 サンプルあたり 2 バイト)

ここでは、低音量時の音質の良さなどの複数の理由から、float サンプルを使用します。メモリ容量が十分でない場合は、16 ビット整数を使用することで、音質と引き換えにメモリスペースの問題を解決できます。

必要なストレージ容量を選択する

10 秒の音声データを格納するとします。サンプルレートを最新の Android デバイスで最も一般的な 48,000 サンプル/秒とすると、480,000 サンプル分のメモリを割り当てる必要があります。

base/cpp/SoundRecording.h を開き、ファイルの冒頭でこの定数を定義します。

constexpr int kMaxSamples = 480000; // 10s of audio data @ 48kHz

ストレージ配列を定義する

float の配列を定義するために必要な情報が、すべてそろいました。SoundRecording.h に次の宣言を追加します。

private:
    std::array<float,kMaxSamples> mData { 0 };

{ 0 } により、一括初期化を使用して配列内のすべての値が 0 に設定されます。

write を実装する

write メソッドには、次の署名があります。

int32_t write(const float *sourceData, int32_t numFrames);

このメソッドは、音声サンプルの配列を sourceData で受け取ります。配列のサイズは numFrames によって指定されます。このメソッドから、実際に書き出すサンプルの数が返されます。

これを実装するには、その次に利用可能な書き込みインデックスを保存します。初期状態では 0 です。

9b3262779d7a0a8c.png

各サンプルが書き込まれると、その次の書き込みインデックスが 1 つずつ移動します。

2971acee93b9869d.png

これは、for ループとして簡単に実装できます。SoundRecording.cpp の write メソッドに次のコードを追加します。

for (int i = 0; i < numSamples; ++i) {
    mData[mWriteIndex++] = sourceData[i];
}
return numSamples;

しかし、注意が必要です。今あるスペースでは足りなくなる数のサンプルを書き込むと、問題が起こります。境界外の配列インデックスにアクセスしようとすることで、セグメンテーション違反が発生します。

チェックを追加して、mData に十分なスペースが残っていない場合には numSamples を変更するようにしましょう。次のコードを既存のコードの上に追加します。

if (mWriteIndex + numSamples > kMaxSamples) {
    numSamples = kMaxSamples - mWriteIndex;
}

read を実装する

read メソッドは write と似ています。次の読み取りインデックスを格納します。

488ab2652d0d281d.png

そして、サンプルの読み取り後にインデックスをインクリメントします。

1a7fd22f4bbb4940.png

このプロセスは、要求された数のサンプルが読み込まれるまで繰り返されます。では、利用可能なデータの最後に達したらどうなるでしょうか。動作は 2 種類あります。

  • ループが有効になっている場合: 読み取りインデックスを 0 に設定する - データ配列の開始
  • ループが無効になっている場合: 何もしない - 読み取りインデックスをインクリメントしないでください

789c2ce74c3a839d.png

どちらの動作でも、利用可能なデータの最後に達したタイミングを把握する必要があります。便利なことに、mWriteIndex が知らせてくれます。これには、データ配列に書き込まれたサンプルの総数が含まれます。

これを踏まえて、SoundRecording.cpp の read メソッドを実装できるようになりました。

int32_t framesRead = 0;
while (framesRead < numSamples && mReadIndex < mWriteIndex){
    targetData[framesRead++] = mData[mReadIndex++];
    if (mIsLooping && mReadIndex == mWriteIndex) mReadIndex = 0;
}
return framesRead;

AudioEngine が実行する主なタスクは次のとおりです。

  • SoundRecording のインスタンスを作成する
  • 録音ストリームを作成してマイクからデータを録音する
  • 録音データを録音ストリーム コールバックの SoundRecording インスタンスに書き込む
  • 再生ストリームを作成して録音データを再生する
  • 録音データを再生ストリーム コールバックの SoundRecording インスタンスから読み取る
  • 録音、再生、ループの UI イベントに応答する

SoundRecording インスタンスの作成を始めましょう。

簡単なことから始めましょう。AudioEngine.h で SoundRecording のインスタンスを作成します。

private:
    SoundRecording mSoundRecording;

2 つのストリーム(再生と録音)を作成します。最初にどちらを作成すべきでしょうか。

最初に作成するのは再生ストリームです。これにはサンプルレートが 1 種類(レイテンシが最も低いレート)しかないからです。作成後、そのサンプルレートを録音ストリーム ビルダーに供給できます。これにより、再生ストリームと録音ストリームのサンプルレートが同一になるため、ストリーム間でリサンプリングを行う余分な作業が不要になります。

再生ストリームのプロパティ

次のプロパティを使用して、再生ストリームを作成する StreamBuilder を設定します。

  • 方向: 指定しない。デフォルトは出力
  • サンプルレート: 指定しない。デフォルトは、レイテンシの最も低いレート
  • フォーマット: float
  • チャンネル数: 2(ステレオ)
  • パフォーマンス モード: 低レイテンシ
  • 共有モード: エクスクルーシブ

再生ストリームを作成する

再生ストリームを作成して開くために必要なものがすべてそろいました。AudioEngine.cpp の start メソッドの冒頭に次のコードを追加します。

// Create the playback stream.
StreamBuilder playbackBuilder = makeStreamBuilder();
AAudioStreamBuilder_setFormat(playbackBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setChannelCount(playbackBuilder.get(), kChannelCountStereo);
AAudioStreamBuilder_setPerformanceMode(playbackBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(playbackBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setDataCallback(playbackBuilder.get(), ::playbackDataCallback, this);
AAudioStreamBuilder_setErrorCallback(playbackBuilder.get(), ::errorCallback, this);

aaudio_result_t result = AAudioStreamBuilder_openStream(playbackBuilder.get(), &mPlaybackStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening playback stream %s",
                       AAudio_convertResultToText(result));
   return;
}

// Obtain the sample rate from the playback stream so we can request the same sample rate from
// the recording stream.
int32_t sampleRate = AAudioStream_getSampleRate(mPlaybackStream);

result = AAudioStream_requestStart(mPlaybackStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting playback stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mPlaybackStream);
   return;
}

データとエラー コールバック用のテンプレート メソッドが作成されています。これらの仕組みを復習するには、最初の Codelab を再度ご確認ください。

録音ストリームのプロパティ

次のプロパティを使用して録音ストリームを作成します。

  • 方向: 入力(録音は入力、再生は出力)
  • サンプルレート: 出力ストリームと同じ
  • フォーマット: float
  • チャンネル数: 1(モノラル)
  • パフォーマンス モード: 低レイテンシ
  • 共有モード: エクスクルーシブ

録音ストリームを作成する

今度は、start にすでに追加済みのコードの下に、次のコードを追加します。

// Create the recording stream.
StreamBuilder recordingBuilder = makeStreamBuilder();
AAudioStreamBuilder_setDirection(recordingBuilder.get(), AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setPerformanceMode(recordingBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(recordingBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setFormat(recordingBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setSampleRate(recordingBuilder.get(), sampleRate);
AAudioStreamBuilder_setChannelCount(recordingBuilder.get(), kChannelCountMono);
AAudioStreamBuilder_setDataCallback(recordingBuilder.get(), ::recordingDataCallback, this);
AAudioStreamBuilder_setErrorCallback(recordingBuilder.get(), ::errorCallback, this);

result = AAudioStreamBuilder_openStream(recordingBuilder.get(), &mRecordingStream);

if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error opening recording stream %s",
                       AAudio_convertResultToText(result));
   closeStream(&mRecordingStream);
   return;
}

result = AAudioStream_requestStart(mRecordingStream);
if (result != AAUDIO_OK){
   __android_log_print(ANDROID_LOG_DEBUG, __func__,
                       "Error starting recording stream %s",
                       AAudio_convertResultToText(result));
   return;
}

作業はここから重要な部分に入ります。マイクから録音したデータを、SoundRecording オブジェクトに実際に保存してみます。

録音ストリームを構築した際、データ コールバックを ::recordingDataCallback として指定しました。このメソッドは、次の署名を持つ AudioEngine::recordingCallback を呼び出します。

aaudio_data_callback_result_t AudioEngine::recordingCallback(float *audioData,
                                                            int32_t numFrames)

音声データは audioData. で供給されます。モノラルで録音しているため 1 フレームあたりのサンプル数は 1 つだけなので、そのサイズ(サンプル数)は numFrames となります。

必要な手順は次のとおりです。

  • mIsRecording をチェックして、録音すべきかどうか確認します。
  • 必要ない場合は、受信データを無視します。
  • 録音する場合:
  • write メソッドを使用して audioDataSoundRecording に供給します。
  • write の戻り値を確認します。0 の場合は、SoundRecording に空きがないので、録音を停止する必要があります。
  • コールバックを継続できるように AAUDIO_CALLBACK_RESULT_CONTINUE を返します。

recordingCallback に次のコードを追加します。

if (mIsRecording) {
    int32_t framesWritten = mSoundRecording.write(audioData, numFrames);
    if (framesWritten == 0) mIsRecording = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

録音ストリームと同様、再生ストリームは、新しいデータが必要なときに playbackDataCallback を呼び出します。このメソッドは、次の署名を持つ AudioEngine::playbackCallback, を呼び出します。

aaudio_data_callback_result_t AudioEngine::playbackCallback(float *audioData, int32_t numFrames)

このメソッド内では以下を行う必要があります。

  • fillArrayWithZeros を使用して配列を 0 で埋め尽くします。
  • mIsPlaying のように録音データを再生する場合は、次の操作を行います。
  • データの numFramesmSoundRecording から読み取ります。
  • convertArrayMonoToStereo を使用して audioData をモノラルからステレオに変換します。
  • 読み取られたフレーム数が要求されたフレーム数より少なかった場合は、すでに録音データの最後に到達しています。mIsPlayingfalse に設定して再生を停止します。

playbackCallback に次のコードを追加します。

fillArrayWithZeros(audioData, numFrames * kChannelCountStereo);

if (mIsPlaying) {
   int32_t framesRead = mSoundRecording.read(audioData, numFrames);
   convertArrayMonoToStereo(audioData, framesRead);
   if (framesRead < numFrames) mIsPlaying = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;

録音を開始 / 停止する

メソッド setRecording を使用して、録音を開始 / 停止します。その署名は次のとおりです。

void setRecording(bool isRecording)

録音ボタンを押すと isRecordingtrue になります。ボタンを離すと isRecordingfalse になります。

メンバー変数 mIsRecording を使用して、recordingCallback 内のデータ ストレージを切り替えます。必要な作業は、これを isRecording の値に設定することだけです。

さらにもう 1 つの動作を追加する必要があります。録音が始まったら、mSoundRecording の既存のデータを上書きする必要があります。そのためには、mSoundRecording の書き込みインデックスを 0 にリセットする clear を使用します。

setRecording のコードは次のとおりです。

if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;

再生を開始 / 停止する

再生のコントロールも録音と同様です。再生ボタンを押すか離すと、setPlaying が呼び出されます。メンバー変数 mIsPlayingplaybackCallback 内での再生を切り替えます。

再生ボタンを押したときに録音データの先頭から再生が始まるようにします。そのためには、mSoundRecording の読み取りヘッドを 0 にリセットする setReadPositionToStart を使用します。

setPlaying のコードは次のとおりです。

if (isPlaying) mSoundRecording.setReadPositionToStart();
mIsPlaying = isPlaying;

ループ再生を切り替える

最後に、LOOP スイッチを切り替えると、setLooping が呼び出されます。この処理は簡単です。isOn 引数を mSoundRecording に渡すだけです。setLooping:

setLooping のコードは次のとおりです。

mSoundRecording.setLooping(isOn);

音声を録音するアプリは、ユーザーに RECORD_AUDIO 権限を要求する必要があります。権限処理コードの多くはすでに記述済みですが、さらに、アプリでこの権限を使用することを宣言する必要があります。

<manifest> セクション内の manifests/AndroidManifest.xml に次の行を追加します。

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

ここまでの作業が適切に行われていたかどうか確認しましょう。アプリをビルドして実行すると、次の UI が表示されるはずです。

7eb653b71774dfed.png

赤色のボタンをタップして録音を開始します。ボタンを押したままにすると、最長 10 秒間、録音が続行します。緑色のボタンをタップすると、録音したデータが再生されます。ボタンを押したままにすると、音声データの最後に達するまで再生が続きます。LOOP が有効になっている場合、音声データは永続的にループします。

参考資料

高性能音声サンプル

Android NDK に関する高性能音声ガイドのドキュメント

Android オーディオの動画のベスト プラクティス - Google I/O 2017