音を作ってみましょう。この Codelab では、AAudio API を使用して、Android 向けの低レイテンシのタップ操作式シンセサイザー アプリを作成します。
このアプリでは、ユーザーが画面に触れた後できるだけ早く音が出るようにします。入力から出力までの遅延をレイテンシと呼びます。スムーズな音声生成を実現するには、レイテンシを理解して最小限に抑えることが重要です。そのために、ここでは低レイテンシの音声ストリームを作成できる AAudio を使用します。
学習内容
- 低レイテンシのオーディオ アプリを作成するための基本的なコンセプト
- 音声ストリームを作成する方法
- 接続 / 切断されるオーディオ デバイスの処理方法
- 音声データを生成して音声ストリームに渡す方法
- Java と C++ の通信に関するおすすめの方法
- UI でタッチイベントをリッスンする方法
必要なもの
- Android Studio 2.3.3 以降
- 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++ クラスでは、サイン波形を計算する単純な数式を使用してデジタル音声データを生成します。
まず、Android Studio で新しいプロジェクトを作成します。
- [File] > [New] > [New Project] をクリックします。
- プロジェクトに「WaveMaker」という名前を付けます。
プロジェクト設定ウィザードを進めながら、デフォルト値を次のように変更します。
- C++ サポートを含める
- スマートフォンとタブレットの最小 SDK: API 26: Android O
- C++ 標準: C++11
注: WaveMaker アプリの完成済みソースコードを参照するには、こちらをご覧ください。
オシレーターは音声データを生成するオブジェクトなので、ここから始めるのが理に適っています。ここではシンプルに 440 Hz のサイン波を 1 つ作成します。
デジタル合成の基本
オシレーターは、デジタル合成の基盤となる構成要素です。このオシレーターでは、サンプルと呼ばれる一連の数値を生成する必要があります。それぞれのサンプルは振幅の値を表します。この値がオーディオ ハードウェアにより電圧に変換されて、ヘッドフォンやスピーカーを鳴らします。
サイン波を表すサンプルのプロットは次のとおりです。
作業に移る前に、デジタル音声データに関する重要な用語をいくつか紹介します。
- サンプル フォーマット - 各サンプルを表すために使用されるデータのタイプ。一般的なサンプル フォーマットには PCM16 や浮動小数点などがあります。ここでは、分解能が 24 ビットであることや低音量での精度が優れていることなどのさまざまな理由から、浮動小数点を使用します。
- フレーム - マルチチャンネル オーディオを生成する際、サンプルはフレームにまとめられます。フレーム内の各サンプルは、それぞれ異なる音声チャンネルに対応しています。たとえば、ステレオ音声には 2 つのチャンネル(左右)があるので、ステレオ音声のフレーム 1 つには、左チャンネル用と右チャンネル用に、計 2 つのサンプルが含まれます。
- フレームレート - 1 秒あたりのフレーム数。サンプルレートと呼ばれることもよくあります。通常、フレームレートとサンプルレートは同義語であり、どちらを使用しても意味に違いはありません。一般的なフレームレート値は 1 秒あたり 44,100 フレームと 48,000 フレームです。AAudio ではサンプルレートという用語が使用されているので、このアプリでもそれにならいます。
ソースファイルとヘッダー ファイルを作成する
/app/cpp
フォルダを右クリックして、[New] > [C++ Class] の順に移動します。
クラスに「Oscillator」という名前を付けます。
以下の行を CMakeLists.txt
に追加して、C++ ソースファイルをビルドに追加します。このファイルは、[Project] ウィンドウの [External Build Files
] セクションにあります。
add_library(...existing source filenames...
src/main/cpp/Oscillator.cpp)
プロジェクトが正常にビルドされたことを確認します。
コードを追加する
Oscillator.h
ファイルに次のコードを追加します。
#include <atomic>
#include <stdint.h>
class Oscillator {
public:
void setWaveOn(bool isWaveOn);
void setSampleRate(int32_t sampleRate);
void render(float *audioData, int32_t numFrames);
private:
std::atomic<bool> isWaveOn_{false};
double phase_ = 0.0;
double phaseIncrement_ = 0.0;
};
次に、Oscillator.cpp
ファイルに次のコードを追加します。
#include "Oscillator.h"
#include <math.h>
#define TWO_PI (3.14159 * 2)
#define AMPLITUDE 0.3
#define FREQUENCY 440.0
void Oscillator::setSampleRate(int32_t sampleRate) {
phaseIncrement_ = (TWO_PI * FREQUENCY) / (double) sampleRate;
}
void Oscillator::setWaveOn(bool isWaveOn) {
isWaveOn_.store(isWaveOn);
}
void Oscillator::render(float *audioData, int32_t numFrames) {
if (!isWaveOn_.load()) phase_ = 0;
for (int i = 0; i < numFrames; i++) {
if (isWaveOn_.load()) {
// Calculates the next sample value for the sine wave.
audioData[i] = (float) (sin(phase_) * AMPLITUDE);
// Increments the phase, handling wrap around.
phase_ += phaseIncrement_;
if (phase_ > TWO_PI) phase_ -= TWO_PI;
} else {
// Outputs silence by setting sample value to zero.
audioData[i] = 0;
}
}
}
void setSampleRate(int32_t sampleRate)
では、音声データのサンプルレートを任意に設定できます(必要な理由については後で詳しく説明します)。sampleRate
と FREQUENCY
に基づいて、render
で使用される phaseIncrement_
の値が計算されます。サイン波のピッチを変更するには、FREQUENCY
を新しい値に更新します。
void setWaveOn(bool isWaveOn)
は、isWaveOn_
フィールドのセッター メソッドです。これを render
で使用して、サイン波を出力するか無音にするかを決定します。
void render(float *audioData, int32_t numFrames)
は、呼び出されるたびに、浮動小数点のサイン波値を audioData
配列に代入します。
numFrames
は、レンダリングする必要のある音声フレームの数です。シンプルにするため、ここではオシレーターから出力されるサンプルは 1 フレームにつき 1 つとします(つまりモノラル出力)。
phase_
は現在の波形フェーズを保存し、各サンプルの生成後に phaseIncrement_
単位で増加します。
isWaveOn_
が false
の場合は、0 が出力されます(無音)。
これでオシレーターの設定は完了です。ですが、そのサイン波の音はどうすれば聞こえるのでしょうか。そのためにはオーディオ エンジンが必要です。
オーディオ エンジンは、以下の処理を担当します。
- デフォルトのオーディオ デバイスに音声ストリームを設定する
- データ コールバックを使用して、音声ストリームにオシレーターを接続する
- オシレーターの波形出力のオンとオフを切り替える
- 不要になったらストリームを終了する
AAudio API は、ストリームの構築とストリーム状態の管理に関する主な概念に関わるものです。そのため、まだよく知らないという方には、十分に学習しておくことをおすすめします。
ソースとヘッダーを作成する
前の手順と同様に、「AudioEngine」という名前の C++ クラスを作成します。
以下の行を CMakeLists.txt
に追加して、C++ ソースファイルと AAudio ライブラリをビルドに追加します。
add_library(...existing source files...
src/main/cpp/AudioEngine.cpp )
target_link_libraries(...existing libraries...
aaudio)
コードを追加する
AudioEngine.h
ファイルに次のコードを追加します。
#include <aaudio/AAudio.h>
#include "Oscillator.h"
class AudioEngine {
public:
bool start();
void stop();
void restart();
void setToneOn(bool isToneOn);
private:
Oscillator oscillator_;
AAudioStream *stream_;
};
次に、AudioEngine.cpp
ファイルに次のコードを追加します。
#include <android/log.h>
#include "AudioEngine.h"
#include <thread>
#include <mutex>
// Double-buffering offers a good tradeoff between latency and protection against glitches.
constexpr int32_t kBufferSizeInBursts = 2;
aaudio_data_callback_result_t dataCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames) {
((Oscillator *) (userData))->render(static_cast<float *>(audioData), numFrames);
return AAUDIO_CALLBACK_RESULT_CONTINUE;
}
void errorCallback(AAudioStream *stream,
void *userData,
aaudio_result_t error){
if (error == AAUDIO_ERROR_DISCONNECTED){
std::function<void(void)> restartFunction = std::bind(&AudioEngine::restart,
static_cast<AudioEngine *>(userData));
new std::thread(restartFunction);
}
}
bool AudioEngine::start() {
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setChannelCount(streamBuilder, 1);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDataCallback(streamBuilder, ::dataCallback, &oscillator_);
AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);
// Opens the stream.
aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);
if (result != AAUDIO_OK) {
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error opening stream %s",
AAudio_convertResultToText(result));
return false;
}
// Retrieves the sample rate of the stream for our oscillator.
int32_t sampleRate = AAudioStream_getSampleRate(stream_);
oscillator_.setSampleRate(sampleRate);
// Sets the buffer size.
AAudioStream_setBufferSizeInFrames(
stream_, AAudioStream_getFramesPerBurst(stream_) * kBufferSizeInBursts);
// Starts the stream.
result = AAudioStream_requestStart(stream_);
if (result != AAUDIO_OK) {
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error starting stream %s",
AAudio_convertResultToText(result));
return false;
}
AAudioStreamBuilder_delete(streamBuilder);
return true;
}
void AudioEngine::restart(){
static std::mutex restartingLock;
if (restartingLock.try_lock()){
stop();
start();
restartingLock.unlock();
}
}
void AudioEngine::stop() {
if (stream_ != nullptr) {
AAudioStream_requestStop(stream_);
AAudioStream_close(stream_);
}
}
void AudioEngine::setToneOn(bool isToneOn) {
oscillator_.setWaveOn(isToneOn);
}
コードは次のように動作します。
エンジンの起動
start()
メソッドで音声ストリームを設定します。AAudio の音声ストリームは AAudioStream
オブジェクトで表されます。それを作成するには、AAudioStreamBuilder
が必要です。
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
これで、streamBuilder
を使用してストリームのさまざまなパラメータを設定できるようになりました。
ここでは浮動小数点数の音声フォーマットを使用します。
AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);
出力はモノラルとします。
AAudioStreamBuilder_setChannelCount(streamBuilder, 1);
注: ここでは、AAudio の自動処理を目的としているので、たとえば次のようないくつかのパラメータは設定しませんでした。
- オーディオ デバイス ID - たとえば内蔵スピーカーなどを明示的に指定するのではなく、デフォルトのオーディオ デバイスを使用します。使用可能なオーディオ デバイスのリストは、
AudioManager.getDevices()
を使用して取得できます。 - ストリーム方向 - デフォルトでは出力ストリームが作成されます。録音したい場合は、代わりに入力ストリームを指定します。
- サンプルレート(詳細については後述)。
パフォーマンス モード
レイテンシをできるだけ低く抑えるため、低レイテンシのパフォーマンス モードを設定します。
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudio では、生成されるストリームのパフォーマンス モードがこれほど低レイテンシになるという保証はありません。このモードを取得できない理由には、次のようなことが考えられます。
- ネイティブ以外のサンプルレート、サンプル フォーマット、またはフレームあたりのサンプル数を指定した(詳しくは後述)。これにより、リサンプリングまたはフォーマット変換が生じる場合があります。リサンプリングとは、サンプル値を別のレートに再計算するプロセスです。リサンプリングとフォーマット変換のどちらも、計算の負荷やレイテンシを増加させる可能性があります。
- 利用できる低レイテンシのストリームがない。これは、おそらく自分のアプリまたは他のアプリですべて使用中であるためです。
ストリームのパフォーマンス モードは、AAudioStream_getPerformanceMode
で確認できます。
ストリームを開く
すべてのパラメータを設定したら(データ コールバックについては後で説明します)、ストリームを開いて結果を確認します。
aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);
AAUDIO_OK
以外の結果が出た場合は、出力を Android Studio の Android Monitor
ウィンドウに記録し、false
を返します。
if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error opening stream", AAudio_convertResultToText(result));
return false;
}
オシレーターのサンプルレートを設定する
ここでは、ストリームのサンプルレートは意図的に設定しませんでした。なぜなら、ネイティブのサンプルレート(つまり、リサンプリングやレイテンシ増加が回避されるレート)を使用したいからです。ストリームが開いているので、それに対してクエリを行い、ネイティブのサンプルレートを確認できます。
int32_t sampleRate = AAudioStream_getSampleRate(stream_);
次に、そのサンプルレートを使用して音声データを生成するようオシレーターに指示します。
oscillator_.setSampleRate(sampleRate);
バッファサイズを設定する
ストリームの内部バッファサイズは、ストリームのレイテンシに直接影響します。バッファサイズが大きいほど、レイテンシが大きくなります。
ここではバッファサイズをバーストサイズの 2 倍に設定します。バーストとは、各コールバック中に書き込まれる個別のデータ量のことです。このように設定することで、レイテンシとバッファ アンダーランの防止策との適切なバランスが保たれます。詳しくは、「AAudio でのバッファサイズ調整」のドキュメントをご覧ください。
AAudioStream_setBufferSizeInFrames(
stream_, AAudioStream_getFramesPerBurst(stream_) * kBufferSizeInBursts);
ストリームを開始する
すべての設定が完了したので、ストリームを開始して、音声データの使用とデータ コールバックのトリガーを開始できます。
result = AAudioStream_requestStart(stream_);
データ コールバック
では、ストリームに音声データを取り込むにはどうすればよいでしょうか。次の 2 つの方法があります。
- AAudioStream_write を使用してストリームに直接書き込む
- データ コールバック関数 AAudioStream_dataCallback を使用する
2 つ目のアプローチのほうが低レイテンシ アプリに適しているので、今回はこちらを使用します。データ コールバック関数は、ストリームに音声データが必要になるたびに、優先度の高いスレッドから呼び出されます。
dataCallback 関数
まず、グローバル名前空間でコールバック関数を定義します。
aaudio_data_callback_result_t dataCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames){
...
}
有利なことに、userData
パラメータは Oscillator
オブジェクトへのポインタとなっています。そのため、これを使用して音声データを audioData
配列にレンダリングできます。手順は次のとおりです。
((Oscillator *)(userData))->render(static_cast<float*>(audioData), numFrames);
また、audioData
配列を浮動小数点数にキャストします。それが render()
メソッドで必要なフォーマットであるためです。
このメソッドは最後に、音声データの使用を継続するようストリームに指示する値を返します。
return AAUDIO_CALLBACK_RESULT_CONTINUE;
コールバックの設定
これで dataCallback
関数の準備ができたので、start()
メソッドからそれを使用するようストリームに指示することも簡単に行えます(::
は、関数がグローバル名前空間にあることを示します)。
AAudioStreamBuilder_setDataCallback(streamBuilder, ::dataCallback, &oscillator_);
オシレーターの起動と停止
オシレーターの波形出力のオン / オフは簡単に切り替えられます。音声の状態をオシレーターに渡すメソッドは 1 つだけです。
void AudioEngine::setToneOn(bool isToneOn) {
oscillator_.setWaveOn(isToneOn);
}
オシレーターの波形がオフの間も render()
メソッドからは 0 で満たされた音声データが生成されていることに注目してください(上述のウォームアップ レイテンシの回避を参照してください)。
整理
ストリームを作成する start()
メソッドを用意したので、その削除に対応する stop()
メソッドも用意する必要があります。このメソッドは、ストリームが不要になった場合(アプリが終了したときなど)にいつでも呼び出すことができます。コールバックを停止するストリームを停止し、ストリームをクローズして削除されるようにします。
AAudioStream_requestStop(stream_);
AAudioStream_close(stream_);
エラー コールバックを使用してストリームの切断を処理する
再生ストリームが開始すると、デフォルトのオーディオ デバイスが使用されます。たとえば、内蔵スピーカーやヘッドフォン、または USB オーディオ インターフェースなどのオーディオ デバイスです。
デフォルトのオーディオ デバイスが変更された場合はどうなるのでしょうか。たとえば、ユーザーがスピーカーで再生を開始し、その後でヘッドフォンを接続したとします。この場合、音声ストリームがスピーカーから接続解除され、アプリは音声サンプルを出力に書き込めなくなります。つまり再生が停止します。
ユーザーはおそらくこのような結果を期待してはいません。引き続きヘッドフォンから音声が再生されるべきです(ただし、別のシナリオでは、再生を停止するほうがよい場合もあります)。
ストリームの切断を検出するコールバックと、必要に応じて新しいオーディオ デバイス向けにストリームを再開する関数が必要です。
エラー コールバックの設定
ストリームの切断イベントをリッスンするには、タイプ AAudioStream_errorCallback
の関数を定義します。
void errorCallback(AAudioStream *stream,
void *userData,
aaudio_result_t error){
if (error == AAUDIO_ERROR_DISCONNECTED){
std::function<void(void)> restartFunction = std::bind(&AudioEngine::restart,
static_cast<AudioEngine *>(userData));
new std::thread(restartFunction);
}
}
この関数は、ストリームでエラーが発生するたびに呼び出されます。エラーが AAUDIO_ERROR_DISCONNECTED
の場合は、ストリームを再開できます。
コールバックで音声ストリームを直接再開することはできません。代わりに、AudioEngine::restart()
を指す std::function
を作成した後、別の std::thread
からこの関数を呼び出すことで、ストリームを再開します。
最後に、start()
で dataCallback
に行ったのと同じ方法で、errorCallback
を設定します。
AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);
ストリームの再開
再開関数は複数のスレッドから呼び出される可能性があるため(たとえば、複数の切断イベントを立て続けに受信した場合など)、std::mutex
によってコードの重要セクションを保護します。
void AudioEngine::restart(){
static std::mutex restartingLock;
if (restartingLock.try_lock()){
stop();
start();
restartingLock.unlock();
}
}
オーディオ エンジンに関する説明はこれで終わりです。ほかにすべきことは、ほとんどありません。
Java の UI を C++ クラスと通信させる手段が必要です。ここに JNI が関わってきます。そのメソッド シグネチャは、見やすいとは言い難いかもしれませんが、幸いなことに 3 つしかありません。
ファイル native-lib.cpp
の名前を jni-bridge.cpp
に変更します。ファイル名を変更せずそのままにすることもできますが、この C++ ファイルが JNI メソッド用であることを明確に表すほうがよいと思います。必ず、名前変更済みのファイルで CMakeLists.txt
を更新してください(ライブラリ名は native-lib
のままにします)。
jni-bridge.cpp
に次のコードを追加します。
#include <jni.h>
#include <android/input.h>
#include "AudioEngine.h"
static AudioEngine *audioEngine = new AudioEngine();
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_touchEvent(JNIEnv *env, jobject obj, jint action) {
switch (action) {
case AMOTION_EVENT_ACTION_DOWN:
audioEngine->setToneOn(true);
break;
case AMOTION_EVENT_ACTION_UP:
audioEngine->setToneOn(false);
break;
default:
break;
}
}
JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_startEngine(JNIEnv *env, jobject /* this */) {
audioEngine->start();
}
JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_stopEngine(JNIEnv *env, jobject /* this */) {
audioEngine->stop();
}
}
JNI ブリッジはとてもシンプルです。
AudioEngine
の静的インスタンスを作成します。startEngine()
とstopEngine()
によってオーディオ エンジンを起動および停止します。touchEvent()
により、タッチイベントがメソッド呼び出しに変換され、音声のオンとオフが切り替わります。
最後に、UI を作成してバックエンドに書き込みましょう。
レイアウト
レイアウトは非常にシンプルです(今後の Codelab で改善する予定です)。中央に TextView があるだけの FrameLayout です。
res/layout/activity_main.xml
を次のように更新します。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/touchArea"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.wavemaker.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/tap_anywhere"
android:textAppearance="@android:style/TextAppearance.Material.Display1" />
</FrameLayout>
@string/tap_anywhere
の文字列リソースを res/values/strings.xml
に追加します。
<resources>
<string name="app_name">WaveMaker</string>
<string name="tap_anywhere">Tap anywhere</string>
</resources>
メイン アクティビティ
今度は、以下のコードで MainActivity.java
を更新します。
package com.example.wavemaker;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
private native void touchEvent(int action);
private native void startEngine();
private native void stopEngine();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startEngine();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
touchEvent(event.getAction());
return super.onTouchEvent(event);
}
@Override
public void onDestroy() {
stopEngine();
super.onDestroy();
}
}
このコードは次のように動作します。
private native void
メソッドはすべてjni-bridge.cpp
で定義されています。これらのメソッドを使用できるようにするには、ここで宣言する必要があります。- アクティビティのライフサイクル イベントの
onCreate()
とonDestroy()
が、JNI ブリッジを呼び出してオーディオ エンジンを起動および停止します。 onTouchEvent()
をオーバーライドしてActivity
のすべてのタッチイベントを受信し、JNI ブリッジに直接渡して音声のオンとオフを切り替えます。
テストデバイスまたはエミュレータを起動して、WaveMaker アプリを実行します。画面をタップすると、サイン波そのものの音が聞こえるはずです。
このアプリは、音楽制作に関する賞を目指したものではなく、Android で低レイテンシの合成音を生成するために必要な基本手法を示すものです。
今後の Codelab では、アプリをさらに魅力的のものにするトピックを扱います。この Codelab にご参加いただきありがとうございました。ご不明な点がございましたら、android-ndk グループでお問い合わせください。