Upgrade to Pro — share decks privately, control downloads, hide ads and more …

人の声を可視化する

numaMyk
October 06, 2022

 人の声を可視化する

ひとの声を可視化するとき、波形型で表示するのが一般的です。
Android端末のマイク性能にばらつきがあることによって録音時の波形が端末によっては小さく、音声が入っている箇所が分かりづらかったり、一定数秒間に表示する波の数がずれることがあります。
人の声を可視化するオーディオビジュアライズについて、Androidで起こりやすい課題と対策を紹介し、またAudio処理に関わるアーキテクチャ、AudioRecordの取り回し方の注意点などについてもご紹介するつもりです。

When visualizing human voices, it is common to display them in waveform form.
Due to variations in the microphone performance of Android devices, the waveform during recording may be small depending on the device, making it difficult to identify where the voice is coming from, and the number of waves displayed in a certain number of seconds may shift.
We will introduce the issues and countermeasures that are likely to occur in Android for audio visualization of human voices, and also introduce the architecture involved in Audio processing and points to note on how to work around AudioRecord.

numaMyk

October 06, 2022
Tweet

More Decks by numaMyk

Other Decks in Technology

Transcript

  1. Who am I? 大沼 美幸 Miyuki Onuma Android Lead at

    Voicy Android 開発は2010 年〜 2 / 45
  2. MediaRecorder 録画・録音 自動的にファイルに書き込み シンプルなステートマシンで制御 Initial →Initialized →Prepared →Recording AudioRecord 録音

    自前でフォーマットに合わせた書き込み リアルタイムに音声情報を操作が可能 細やかなメディア情報の設定が必要 5 / 45 API level 1 API level 3
  3. メディア情報の設定 入力ソース サンプリングレート 音声チャンネル エンコーディング形式 音声データのバッファサイズ 8 / 45 val

    audioRecorder = AudioRecord( AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)
  4. 入力ソース Audio Source DEFAULT 端末のデフォルトの入力ソース MIC マイク音源 VOICE_PERFORMANCE ライブ配信向け VOICE_RECOGNITION

    音声認識に最適化 9 / 45 [1] AudioSource.MIC, val audioRecorder = AudioRecord( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize) audioRecorder.startRecording()
  5. 最小バッファサイズの見積値を取得 14 / 45 // 音声データのバッファサイズ var bufferSize = AudioRecord.getMinBufferSize(

    SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT) val audioRecorder = AudioRecord( AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize) audioRecorder.startRecording()
  6. 音声データ読込 15 / 45 fun startRecording() { var bufferSize =

    AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN, ENCODE_FORMAT) val audioRecorder = AudioRecord(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_IN, ENCODE_FORMAT, bufferSize) // 音声データをいくつずつ処理するか( = 1 フレームのデータ数) audioRecord.positionNotificationPeriod = SAMPLE_RATE / 2 // 録音された音声データを書き込む配列 val audioBuffer = ShortArray(bufferSize / 2) // コールバックを指定 audioRecord.setRecordPositionUpdateListener(object : AudioRecord.OnRecordPositionUpdateListener { // フレームごとの処理 override fun onPeriodicNotification(recorder: AudioRecord) { recorder.read(audioBuffer, 0, SAMPLE_RATE / 2) // 音声データ読込 } override fun onMarkerReached(recorder: AudioRecord) {} }) audioRecord.startRecording() }
  7. エンコードしたデータをファイルに書き込む 16 / 45 override fun onPeriodicNotification(recorder: AudioRecord) { val

    read = recorder.read(audioBuffer, 0, SAMPLE_RATE / 2) writeByteToPCM(audioBuffer, read) } ... audioRecord.setRecordPositionUpdateListener(object : AudioRecord.OnRecordPositionUpdateListener { override fun onMarkerReached(recorder: AudioRecord) {} }) ... var bos : BufferedOutputStream fun writeByteToPCM(audioBuffer: ShortArray, len: Int) { val buffer = ByteBuffer.allocate(Short.SIZE / java.lang.Byte.SIZE * len) buffer.order(ByteOrder.BIG_ENDIAN) buffer.asShortBuffer().put(audioBuffer, 0, len) bos.write(buffer.array(), 0, buffer.limit()) }
  8. MediaPlayer 動画・音声 ファイルを指定し再生するのに適している CPU やリソースを多く消費 シンプルなステートマシンで制御 Idle →Initialized →Prepared →Started

    →PlaybackCompleted AudioTrack 音声 低レイヤーAPI ストリーミング再生 バイトデータを直接扱う 18 / 45 API level 1 API level 3
  9. メディア情報・オーディオ属性の設定 20 / 45 // インスタンス生成 audioTrack = AudioTrack.Builder() //

    オーディオ属性を設定 .setAudioAttributes( AudioAttributes.Builder() .setUsage(USAGE_MEDIA) // 用途 .setContentType(CONTENT_TYPE_MUSIC) // コンテンツタイプ .build()) // メディアフォーマットを設定 .setAudioFormat(AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(SAMPLE_RATE) .setChannelMask(CHANNEL_OUT_MONO) .build()) // 再生時に音声データを読み込むバッファの総サイズ(バイト数)を設定 .setBufferSizeInBytes(bufferSize) .build()
  10. 25 / 45 // 音楽ファイルのtrack をextractor に設定する // マルチメディアデータの構造を解析: mime,

    codec, sampling rate, count of channel var extractor = MediaExtractor() var trackIndex = 0 var audioTrackIndex: Int var format: MediaFormat? = null while (true) { format = extractor.getTrackFormat(trackIndex) trackIndex++ if (trackIndex >= extractor.trackCount){ error("no track") } } extractor.selectTrack(audioTrackIndex)
  11. // デコーダーに供給する前にバッファーを読み取る val res = codec?.dequeueOutputBuffer(bufferInfo, timeOutUs) ?: return if

    (res >= 0) { if (bufferInfo.size > 0) samplePosition = 0 val buf: ByteBuffer = codecOutputBuffers?.get(res) ?: return val chunk = ByteArray(bufferInfo.size) buf.get(chunk) buf.clear() if (chunk.isNotEmpty()) { // 音声ファイル をデコードし AudioTrack に書き込み audioTrack?.write(chunk, 0, chunk.size) audioTrack?.flush() if (state.get() != PlayerStates.PLAYING) { if (events != null) handler.post { events!!.onPlay() } state.set(PlayerStates.PLAYING) } } codec?.releaseOutputBuffer(res, false) if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { doneReadOutput = true } } 26 / 45
  12. 線 = 0.01 秒 線と線の間 = 0.03 秒 1 秒で大体25

    本の線を描画 ↓ 1 秒間に読み込めるバッファサイズが端末によ って変わってくる 30 / 45
  13. 要点 32 / 45 スクロール速度を調節する protected fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics!): Float

    val bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_IN, ENCODE_FORMAT) val audioBufferSize = bufferSize / 2 return audioBufferSize * ONE_SECOND / SAMPLE_RATE
  14. 実装 35 / 45 private fun setPlayer() { visualizer =

    Visualizer(audioManager.getAudioSessionId()) visualizer.enabled = false visualizer.captureSize = Visualizer.getCaptureSizeRange()[1] visualizer.setDataCaptureListener( object : OnDataCaptureListener { override fun onWaveFormDataCapture( visualizer: Visualizer, waveform: ByteArray, samplingRate: Int ) { lineWaveView?.update(waveform) } override fun onFftDataCapture(visualizer: Visualizer, fft: ByteArray, samplingRate: Int) {} }, Visualizer.getMaxCaptureRate(), true, false ) visualizer.enabled = true }
  15. ExoPlayer 動画・音声 DASH 、SmoothStreaming をサポート メディアプレーヤーアプリでもっとも主流 YouTube Google Play ムービー&TV

    Oboe 音声 低レイテンシ, 高パフォーマンスを要件とするアプリ向 け Koala Sampler SoundCloud FluidSynth 38 / 45 API level 16 API level 16
  16. なぜ Oboe を使うのか? 39 / 45 Oboe はAndroid デバイスの 99

    % 以上で最低レイテンシを提供 できるだけリアルタイムでサウンドを記録した り再生するといった技術要件がある場合、 Oboe が最適
  17. まとめ AudioTrack 、AudioPlayer は低レベルAPI で高パフォーマンスだが バイトデータを直接扱うので各要素の設 定が必要 Audio 処理は扱いがややこしくおまじないのようなコードが多くあるので、ラッパーやらサービス等で抽象 化し使いやすくする

    録音や再生制御のために必要になる主要なメディア情報を把握する 音声データを波形に可視化するにVisualizer を使う場合カスタムView を作る 音声の収録および再生処理はメインスレッドを占有しないよう別スレッドで行う 長時間音声を再生する際にはメモリの使い切りに注意する 43 / 45