この Codelab では、音声サンプラーを作成します。このアプリは、スマートフォンの内蔵マイクから音声を録音し、再生します。
このアプリでは、[Record] ボタンを押している間に最長 10 秒間の音声を録音できます。[Play] を押すと、録音した音声が(ボタンを長押ししている間に)1 回再生されます。または [Loop] を選択すると、[Play] ボタンを離すまで、録音を何回も繰り返し再生できます。[Record] を押すたびに、以前に録音した音声が上書きされます。
学習内容
- 低レイテンシの録音ストリームを作成するための基本的なコンセプト
- マイクから録音した音声データを保存し再生する方法
前提条件
この Codelab を開始する前に、WaveMaker パート 1 の Codelab を完了しておくことをおすすめします。パート 1 では、今回扱わない音声ストリームの作成の基本的なコンセプトを取り上げています。
必要なもの
- Android Studio 3.0.0 以降
- Android 8.0(API レベル 26)SDK
- インストール済みの NDK とビルドツール
- テスト用として Android 8.0(API レベル 26)以降を搭載しているシミュレータまたは Android デバイス
- C++ の知識をある程度持っていると役に立ちますが、必須ではありません
このサンプラー アプリには 4 つのコンポーネントがあります。
- UI - Java で記述されており、MainActivity クラスがタッチイベントの受信と JNI ブリッジへの転送を担います。
- JNI ブリッジ - この C++ ファイルは、JNI を使用して UI と C++ オブジェクト間の通信メカニズムを提供します。UI からオーディオ エンジンにイベントを転送します。
- オーディオ エンジン - この C++ クラスによって、録音 / 再生の音声ストリームが作成されます。
- サウンド レコーディング - この C++ クラスによって、音声データがメモリに保存されます。
アーキテクチャは次のようになっています。
プロジェクトのクローンを作成する
GitHub で Codelab リポジトリのクローンを作成します。
git clone https://github.com/googlecodelabs/android-wavemaker2
Android Studio にプロジェクトをインポートする
Android Studio を開き、プロジェクトをインポートします。
- [File] > [New] > [Import project]
- 「android-wavemaker2」フォルダを選択します。
プロジェクトを実行する
基本実行構成を選択します。
その後、Ctrl+R キーを押してテンプレート アプリをビルドし実行します。コンパイルと実行が行われますが、動作はしません。この Codelab で、機能を追加します。
ベース モジュールを開く
この Codelab で作業するファイルは base
モジュールに格納されます。[Project] ウィンドウでこのモジュールを展開し、[Android] ビューが選択されていることを確認します。
注: 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 です。
各サンプルが書き込まれると、その次の書き込みインデックスが 1 つずつ移動します。
これは、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
と似ています。次の読み取りインデックスを格納します。
そして、サンプルの読み取り後にインデックスをインクリメントします。
このプロセスは、要求された数のサンプルが読み込まれるまで繰り返されます。では、利用可能なデータの最後に達したらどうなるでしょうか。動作は 2 種類あります。
- ループが有効になっている場合: 読み取りインデックスを 0 に設定する - データ配列の開始
- ループが無効になっている場合: 何もしない - 読み取りインデックスをインクリメントしないでください
どちらの動作でも、利用可能なデータの最後に達したタイミングを把握する必要があります。便利なことに、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
メソッドを使用してaudioData
をSoundRecording
に供給します。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
のように録音データを再生する場合は、次の操作を行います。- データの
numFrames
をmSoundRecording
から読み取ります。 convertArrayMonoToStereo
を使用してaudioData
をモノラルからステレオに変換します。- 読み取られたフレーム数が要求されたフレーム数より少なかった場合は、すでに録音データの最後に到達しています。
mIsPlaying
をfalse
に設定して再生を停止します。
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)
録音ボタンを押すと isRecording
は true になります。ボタンを離すと isRecording
は false になります。
メンバー変数 mIsRecording
を使用して、recordingCallback
内のデータ ストレージを切り替えます。必要な作業は、これを isRecording
の値に設定することだけです。
さらにもう 1 つの動作を追加する必要があります。録音が始まったら、mSoundRecording
の既存のデータを上書きする必要があります。そのためには、mSoundRecording
の書き込みインデックスを 0 にリセットする clear
を使用します。
setRecording
のコードは次のとおりです。
if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;
再生を開始 / 停止する
再生のコントロールも録音と同様です。再生ボタンを押すか離すと、setPlaying
が呼び出されます。メンバー変数 mIsPlaying
は playbackCallback
内での再生を切り替えます。
再生ボタンを押したときに録音データの先頭から再生が始まるようにします。そのためには、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 が表示されるはずです。
赤色のボタンをタップして録音を開始します。ボタンを押したままにすると、最長 10 秒間、録音が続行します。緑色のボタンをタップすると、録音したデータが再生されます。ボタンを押したままにすると、音声データの最後に達するまで再生が続きます。LOOP が有効になっている場合、音声データは永続的にループします。