Slide 1

Slide 1 text

人の声を可視化する 1 / 45 Miyuki Onuma

Slide 2

Slide 2 text

Who am I? 大沼 美幸 Miyuki Onuma Android Lead at Voicy Android 開発は2010 年〜 2 / 45

Slide 3

Slide 3 text

Agenda Audio 処理の基本を理解する 音声を可視化する方法 Audio 処理を実装する際の注意すべき点 低レイテンシライブラリ Oboe のご紹介 3 / 45

Slide 4

Slide 4 text

Audio 処理の基本を理解する ~ Recorder 4 / 45

Slide 5

Slide 5 text

MediaRecorder 録画・録音 自動的にファイルに書き込み シンプルなステートマシンで制御 Initial →Initialized →Prepared →Recording AudioRecord 録音 自前でフォーマットに合わせた書き込み リアルタイムに音声情報を操作が可能 細やかなメディア情報の設定が必要 5 / 45 API level 1 API level 3

Slide 6

Slide 6 text

権限 6 / 45 録音で外部ストレージとマイクを使用するので、アプリのマニフェストファイルで以下を宣言する 録音権限に ついては runtime permission のため、ユーザーの許可が必要

Slide 7

Slide 7 text

音声レコーダーでやる こと メディア情報を設定しバッファサイズを定義 音声データの読み込み データをファイルに書き込む 7 / 45

Slide 8

Slide 8 text

メディア情報の設定 入力ソース サンプリングレート 音声チャンネル エンコーディング形式 音声データのバッファサイズ 8 / 45 val audioRecorder = AudioRecord( AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

Slide 9

Slide 9 text

入力ソース 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()

Slide 10

Slide 10 text

サンプリングレート Sample Rate 、サンプリング周波数 1 秒当り、何分割して音を採取するか 単位はヘルツ(Hz) 数値が大きいほどに高音質 比例してデータ容量が増加 デバイスに合ったサンプリングレート(通常は 44.1 kHz )を選択する 10 / 45 SAMPLE_RATE, val audioRecorder = AudioRecord( AudioSource.MIC, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

Slide 11

Slide 11 text

音声チャンネル ステレオ、モノラル 11 / 45 AudioFormat.CHANNEL_IN_MONO, val audioRecorder = AudioRecord( AudioSource.MIC, SAMPLE_RATE, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

Slide 12

Slide 12 text

エンコーディング形式とビット深度 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT… 12 / 45 AudioFormat.ENCODING_PCM_16BIT, val audioRecorder = AudioRecord( AudioSource.MIC, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, bufferSize)

Slide 13

Slide 13 text

サンプリングレート 時間軸に分割 1 秒当り、何分割して音を採取するか ビット深度 サンプリングに対するデータ量 分割した1 つ1 つのデータ(サンプリング)にどれだけ のデータ量を割り当てるか 13 / 45

Slide 14

Slide 14 text

最小バッファサイズの見積値を取得 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()

Slide 15

Slide 15 text

音声データ読込 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() }

Slide 16

Slide 16 text

エンコードしたデータをファイルに書き込む 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()) }

Slide 17

Slide 17 text

Audio 処理の基本を理解する ~ Player 17 / 45

Slide 18

Slide 18 text

MediaPlayer 動画・音声 ファイルを指定し再生するのに適している CPU やリソースを多く消費 シンプルなステートマシンで制御 Idle →Initialized →Prepared →Started →PlaybackCompleted AudioTrack 音声 低レイヤーAPI ストリーミング再生 バイトデータを直接扱う 18 / 45 API level 1 API level 3

Slide 19

Slide 19 text

音声プレーヤーでやる こと メディア情報・オーディオ属性の設定 取得したデータをバイトデータにデコード プレイヤーへ書き込む 19 / 45

Slide 20

Slide 20 text

メディア情報・オーディオ属性の設定 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()

Slide 21

Slide 21 text

取得したデータをバイトデータにデコード MediaCodec MediaExtractor 21 / 45 使用するクラス

Slide 22

Slide 22 text

音声を再生するDAC について 22 / 45

Slide 23

Slide 23 text

音声を再生するDAC について 23 / 45

Slide 24

Slide 24 text

音声を再生するDAC について 24 / 45

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

// デコーダーに供給する前にバッファーを読み取る 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

Slide 27

Slide 27 text

Audio 処理を実装する際の注意すべき点 再生や収録が終わるまでメインスレッドを占有しないように基本的には別スレッドで処理 大容量ファイルでのOut Of Memory 27 / 45

Slide 28

Slide 28 text

音声を可視化する方法 ~ RecyclerView を使って波形 を表示する 28 / 45

Slide 29

Slide 29 text

線 = 0.01 秒 線と線の間 = 0.03 秒 1 秒で大体25 本の線を描画 29 / 45

Slide 30

Slide 30 text

線 = 0.01 秒 線と線の間 = 0.03 秒 1 秒で大体25 本の線を描画 ↓ 1 秒間に読み込めるバッファサイズが端末によ って変わってくる 30 / 45

Slide 31

Slide 31 text

RecyclerView で波形を作るには RecyclerView LinearSmoothScroller AudioRecord 31 / 45 使用するクラス

Slide 32

Slide 32 text

要点 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

Slide 33

Slide 33 text

音声を可視化する方法 ~ Visualizer を使って波形を表 示する 33 / 45

Slide 34

Slide 34 text

Visualizer で波形を作るには Visualizer Visualizer.OnDataCaptureListener AudioTrack / MediaPlayer 表示用のView 34 / 45 使用するクラス

Slide 35

Slide 35 text

実装 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 }

Slide 36

Slide 36 text

36 / 45

Slide 37

Slide 37 text

低レイテンシライブラリ-Oboe のご紹介 37 / 45

Slide 38

Slide 38 text

ExoPlayer 動画・音声 DASH 、SmoothStreaming をサポート メディアプレーヤーアプリでもっとも主流 YouTube Google Play ムービー&TV Oboe 音声 低レイテンシ, 高パフォーマンスを要件とするアプリ向 け Koala Sampler SoundCloud FluidSynth 38 / 45 API level 16 API level 16

Slide 39

Slide 39 text

なぜ Oboe を使うのか? 39 / 45 Oboe はAndroid デバイスの 99 % 以上で最低レイテンシを提供 できるだけリアルタイムでサウンドを記録した り再生するといった技術要件がある場合、 Oboe が最適

Slide 40

Slide 40 text

なぜ 低レイテンシで動作する必要があるか? 2018/3 JUCE Mobile Audio Quality 40 / 45 主要なモバイルデバイスの性能差について

Slide 41

Slide 41 text

Oboe Sample Oboe Github Sample 41 / 45

Slide 42

Slide 42 text

参考 Kotlin でVisualizer を作ってみる | @ssuzaki android-audio-visualizer | GautamChibde Oboe audio library | Android Developers Oboe Github 42 / 45

Slide 43

Slide 43 text

まとめ AudioTrack 、AudioPlayer は低レベルAPI で高パフォーマンスだが バイトデータを直接扱うので各要素の設 定が必要 Audio 処理は扱いがややこしくおまじないのようなコードが多くあるので、ラッパーやらサービス等で抽象 化し使いやすくする 録音や再生制御のために必要になる主要なメディア情報を把握する 音声データを波形に可視化するにVisualizer を使う場合カスタムView を作る 音声の収録および再生処理はメインスレッドを占有しないよう別スレッドで行う 長時間音声を再生する際にはメモリの使い切りに注意する 43 / 45

Slide 44

Slide 44 text

ご静聴ありがとうございました! Voicy tech blog Voicy 採用情報 44 / 45

Slide 45

Slide 45 text

45 / 45