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 関数を使用する際に渡すパラメータ audioBufferDirectBuffer でなければならず、そうでないと音声データを正しく読み取ることができません。この場合、その 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の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 ファイルに合成します。その重要なステップは以下の通りです:

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

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