banner
jzman

jzman

Coding、思考、自觉。
github

AudioRecordによる音声データの収集と合成

本文は Android の音声・映像開発における AudioRecord の使用について紹介します。ケーススタディは前の MediaCodec による MP4 の録画を基にしており、AudioRecord を使用して音声データを MP4 に合成します。Android の音声・映像に関するシリーズ記事は以下の通りです:

本文の主な内容は以下の通りです:

  1. AudioRecord の紹介
  2. AudioRecord のライフサイクル
  3. AudioRecord の音声データの読み取り
  4. 直接バッファとバイトオーダー(選択)
  5. 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_UNINITIALIZEDSTATE_INITIALIZEDRECORDSTATE_RECORDINGRECORDSTATE_STOPPED が含まれ、それぞれ未初期化、初期化済み、録音中、録音停止に対応しています。以下の図に示します:

Mermaid Loading...

簡単に説明します:

  1. 作成前または release 後に AudioRecordSTATE_UNINITIALIZED 状態に入ります。
  2. AudioRecord を作成すると STATE_INITIALIZED 状態に入ります。
  3. startRecording を呼び出すと RECORDSTATE_RECORDING 状態に入ります。
  4. stop を呼び出すと RECORDSTATE_STOPPED 状態に入ります。

では、AudioRecord の状態をどのように取得するかというと、getStategetRecordingState を使用してその状態を取得できます。正しく使用するために、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 以上であり、音声データの読み取りに関する一般的な例外は以下の通りです:

  1. ERROR_INVALID_OPERATION:AudioRecord が未初期化であることを示します。
  2. ERROR_BAD_VALUE:パラメータが無効であることを示します。
  3. ERROR_DEAD_OBJECT:音声データがすでに転送されている場合、エラーコードを返さず、次回の read でエラーコードを返します。

上記の 3 つの read 関数はすべてハードウェア音声デバイスから音声データを読み取ります。前の 2 つの主な違いは音声フォーマットが異なり、それぞれ 8 ビット、16 ビットで、対応する量子化レベルは 2^8 と 2^16 の量子化レベルです。

3 つ目の read 関数は音声データを読み取る際に、直接バッファ(DirectBuffer)に記録します。このバッファが DirectBuffer でない場合は常に 0 を返します。つまり、3 つ目の read 関数を使用する際に渡すパラメータ audioBuffer は必ず DirectBuffer でなければならず、そうでないと音声データを正しく読み取ることができません。この場合、その Bufferposition は変わらず、バッファ内のデータの音声フォーマットは 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 ファイルに合成します。その重要なステップは以下の通りです:

  1. スレッドを開いて 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 を使用して音声データを録音するだけの場合、音声データを読み取ったらファイルに書き込むことができます。

  1. 音声データを 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 がなくなり、音声エンコードが失敗します。また、ストリーム終了の処理にも注意が必要です。

  1. ファイルの合成には 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 の使用は基本的に以上の通りです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。