本文紹介するのは 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のDirectByteBuffersは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
の非同期処理モードを使用して音声データをエンコードします。コードは貼りませんが、Buffer
の充填と解放の際には条件を必ず確認してください。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
の使用は基本的に以上の通りです。