本文は Android
の音声・映像開発における AudioRecord
の使用について紹介します。ケーススタディは前の MediaCodec
による MP4
の録画を基にしており、AudioRecord
を使用して音声データを MP4
に合成します。Android
の音声・映像に関するシリーズ記事は以下の通りです:
本文の主な内容は以下の通りです:
- AudioRecord の紹介
- AudioRecord のライフサイクル
- AudioRecord の音声データの読み取り
- 直接バッファとバイトオーダー(選択)
- AudioRecord の使用
AudioRecord の紹介#
AudioRecord
は Android においてハードウェアデバイスの音声を録音するためのツールで、pulling
の方式で音声データを取得します。一般的には原音の音声 PCM
フォーマットのデータを取得するために使用され、録音と再生を同時に行うことができ、音声データのリアルタイム処理に多く用いられます。
AudioRecord
の作成に関するパラメータと説明は以下の通りです:
// AudioRecordの作成
public AudioRecord (int audioSource,
int sampleRateInHz,
int channelConfig,
int audioFormat,
int bufferSizeInBytes)
- audioSource:音声源を示し、音声源は
MediaRecorder.AudioSource
に定義されています。一般的な音声源には主マイクMediaRecorder.AudioSource.MIC
などがあります。 - sampleRateInHz:ヘルツ単位のサンプリングレートを示し、各チャンネルの 1 秒あたりのサンプリング数を意味します。一般的なサンプリングレートの中で、44100Hz のサンプリングレートのみがすべてのデバイスで正常に使用できることが保証されています。実際のサンプリングレートは
getSampleRate
で取得できます。このサンプリングレートは音声コンテンツの再生サンプリングレートではなく、例えば 48000Hz のデバイスで 8000Hz の音声を再生することができます。対応プラットフォームは自動的にサンプリングレートの変換を行うため、6 倍の速度で再生されることはありません。 - channelConfig:チャンネル数を示し、チャンネルは
AudioFormat
に定義されています。一般的なチャンネルの中で、モノラルAudioFormat.CHANNEL_IN_MONO
のみがすべてのデバイスで正常に使用できることが保証されています。他の例として、AudioFormat.CHANNEL_IN_STEREO
はステレオを示します。 - audioFormat:
AudioRecord
が返す音声データのフォーマットを示します。線形PCM
の場合、各サンプルのサイズ(8、16、32 ビット)および表現形式(整数型、浮動小数点型)を反映します。音声フォーマットはAudioFormat
に定義されており、一般的な音声データフォーマットの中でAudioFormat.ENCODING_PCM_16BIT
のみがすべてのデバイスで正常に使用できることが保証されています。AudioFormat.ENCODING_PCM_8BIT
はすべてのデバイスで正常に使用できることが保証されていません。 - bufferSizeInBytes:音声データを書き込むバッファのサイズを示します。この値は
getMinBufferSize
のサイズより小さくてはならず、つまりAudioRecord
に必要な最小バッファのサイズより小さくてはなりません。そうでないとAudioRecord
の初期化に失敗します。このバッファのサイズは負荷のかかる状況での正常な録音を保証するものではなく、必要に応じてより大きな値を選択できます。
AudioRecord のライフサイクル#
AudioRecord
のライフサイクル状態には STATE_UNINITIALIZED
、STATE_INITIALIZED
、RECORDSTATE_RECORDING
、RECORDSTATE_STOPPED
が含まれ、それぞれ未初期化、初期化済み、録音中、録音停止に対応しています。以下の図に示します:
簡単に説明します:
- 作成前または
release
後にAudioRecord
はSTATE_UNINITIALIZED
状態に入ります。 AudioRecord
を作成するとSTATE_INITIALIZED
状態に入ります。startRecording
を呼び出すとRECORDSTATE_RECORDING
状態に入ります。stop
を呼び出すとRECORDSTATE_STOPPED
状態に入ります。
では、AudioRecord
の状態をどのように取得するかというと、getState
と getRecordingState
を使用してその状態を取得できます。正しく使用するために、AudioRecord
オブジェクトを操作する前にその状態を確認することが重要です。
AudioRecord の音声データの読み取り#
AudioRecord
が提供する音声データを読み取る 3 つの方法は以下の通りです:
// 1. 音声データを読み取る、音声フォーマットはAudioFormat#ENCODING_PCM_8BIT
int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
// 2. 音声データを読み取る、音声フォーマットはAudioFormat#ENCODING_PCM_16BIT
int read(@NonNull short[] audioData, int offsetInShorts, int sizeInShorts)
// 3. 音声データを読み取る、後の章を参照
int read(@NonNull ByteBuffer audioBuffer, int sizeInBytes)
音声データを読み取る際の戻り値は 0 以上であり、音声データの読み取りに関する一般的な例外は以下の通りです:
- ERROR_INVALID_OPERATION:
AudioRecord
が未初期化であることを示します。 - ERROR_BAD_VALUE:パラメータが無効であることを示します。
- ERROR_DEAD_OBJECT:音声データがすでに転送されている場合、エラーコードを返さず、次回の
read
でエラーコードを返します。
上記の 3 つの read
関数はすべてハードウェア音声デバイスから音声データを読み取ります。前の 2 つの主な違いは音声フォーマットが異なり、それぞれ 8 ビット、16 ビットで、対応する量子化レベルは 2^8 と 2^16 の量子化レベルです。
3 つ目の read
関数は音声データを読み取る際に、直接バッファ(DirectBuffer
)に記録します。このバッファが DirectBuffer
でない場合は常に 0 を返します。つまり、3 つ目の read
関数を使用する際に渡すパラメータ audioBuffer
は必ず DirectBuffer
でなければならず、そうでないと音声データを正しく読み取ることができません。この場合、その Buffer
の position
は変わらず、バッファ内のデータの音声フォーマットは AudioRecord
で指定されたフォーマットに依存し、バイトの格納方法はネイティブバイトオーダーです。
直接バッファとバイトオーダー#
上記で言及した 2 つの概念、直接バッファとバイトオーダーについて簡単に説明します:
直接バッファ#
DirectBuffer
は NIO のもので、ここでは普通のバッファと直接バッファのいくつかの違いを簡単に見てみましょう。
- 普通のバッファ
ByteBuffer buf = ByteBuffer.allocate(1024);
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
普通のバッファはヒープからバイトバッファを割り当てます。このバッファは JVM によって管理されており、適切なタイミングで GC によって回収される可能性があります。GC の回収はメモリの整理を伴い、ある程度パフォーマンスに影響を与えます。
- 直接バッファ
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
public static ByteBuffer allocateDirect(int capacity) {
// Android-changed: Android's DirectByteBuffers carry a MemoryRef.
// return new DirectByteBuffer(capacity);
DirectByteBuffer.MemoryRef memoryRef = new DirectByteBuffer.MemoryRef(capacity);
return new DirectByteBuffer(capacity, memoryRef);
}
上記は Android における DirectBuffer
の実装で、メモリから割り当てられています。この方法で得られるバッファの取得コストと解放コストは非常に大きいですが、ガベージコレクションのヒープの外に留まることができます。一般的には大規模で長寿命のバッファに割り当てられ、最終的にこのバッファを割り当てることで顕著なパフォーマンス向上が得られる場合に行われます。DirectBuffer
であるかどうかは isDirect
で確認できます。
バイトオーダー#
バイトオーダーはメモリ内のバイトの格納方法を指し、バイトオーダーは主に 2 つのタイプに分けられます:BIG-ENDIAN と LITTLE-ENDIAN。一般的にはネットワークバイトオーダーとネイティブバイトオーダーと呼ばれ、具体的には以下の通りです:
- ネイティブバイトオーダー、つまり LITTLE-ENDIAN(小バイトオーダー、低バイトオーダー)は、低位バイトがメモリの低アドレス端に配置され、高位バイトがメモリの高アドレス端に配置されます。それに対してネットワークバイトオーダーがあります。
- ネットワークバイトオーダーは、一般的に TCP/IP プロトコルで使用されるバイトオーダーを指します。TCP/IP の各層プロトコルはバイトオーダーを BIG-ENDIAN と定義しているため、ネットワークバイトオーダーは一般的に BIG-ENDIAN を指します。
AudioRecord の使用#
前の文章 Camera2、MediaCodec 録制 mp4 では動画のみを録画し、MediaCodec
の使用に重点を置いていました。ここでは動画録画の基礎の上に AudioRecord
を使用して音声の録音を追加し、それを MP4
ファイルに合成します。その重要なステップは以下の通りです:
- スレッドを開いて
AudioRecord
を使用してハードウェアの音声データを読み取ります。スレッドを開くことでカクつきを避けることができます。文末のケーススタディにもコード例があります。AudioEncode2
を参照してください。以下のようになります:
/**
* 音声読み取りRunnable
*/
class RecordRunnable : Runnable{
override fun run() {
val byteArray = ByteArray(bufferSize)
// 録音状態 -1はデフォルト状態、1は録音状態、0は録音停止
while (recording == 1){
val result = mAudioRecord.read(byteArray, 0, bufferSize)
if (result > 0){
val resultArray = ByteArray(result)
System.arraycopy(byteArray, 0, resultArray, 0, result)
quene.offer(resultArray)
}
}
// カスタムストリーム終了データ
if (recording == 0){
val stopArray = byteArrayOf((-100).toByte())
quene.offer(stopArray)
}
}
}
ここで言及しておきますが、AudioRecord
を使用して音声データを録音するだけの場合、音声データを読み取ったらファイルに書き込むことができます。
- 音声データを
MP4
に合成するには、まず音声データのエンコードを行う必要があります。音声データエンコーダの設定は以下の通りです:
// 音声データエンコーダの設定
private fun initAudioCodec() {
L.i(TAG, "init Codec start")
try {
val mediaFormat =
MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_AAC,
RecordConfig.SAMPLE_RATE,
2
)
mAudioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000)
mediaFormat.setInteger(
MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC
)
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 8192)
mAudioCodec.setCallback(this)
mAudioCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
} catch (e: Exception) {
L.i(TAG, "init error:${e.message}")
}
L.i(TAG, "init Codec end")
}
エンコードに関しては MediaCodec
の使用について前述の 2 つの記事を参照できます:
ここでは MediaCodec
の非同期処理モードを使用して音声データをエンコードします。コードは貼り付けませんが、InputBuffer
を解放する際には条件を必ず確認してください。InputBuffer
を解放しないと、使用可能な InputBuffer
がなくなり、音声エンコードが失敗します。また、ストリーム終了の処理にも注意が必要です。
- ファイルの合成には
MediaMuxer
を使用します。MediaMuxer
を起動する前に、視トラックと音トラックを追加しておく必要があります。
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
L.i(TAG, "onOutputFormatChanged format:${format}")
// 音トラックを追加
addAudioTrack(format)
// 音トラックと視トラックの両方が追加された場合のみMediaMuxerを起動
if (RecordConfig.videoTrackIndex != -1) {
mAudioMuxer.start()
RecordConfig.isMuxerStart = true
L.i(TAG, "onOutputFormatChanged isMuxerStart:${RecordConfig.isMuxerStart}")
}
}
// 音トラックを追加
private fun addAudioTrack(format: MediaFormat) {
L.i(TAG, "addAudioTrack format:${format}")
RecordConfig.audioTrackIndex = mAudioMuxer.addTrack(format)
RecordConfig.isAddAudioTrack = true
}
// ...
AudioRecord
の使用は基本的に以上の通りです。