前提・実現したいこと
Androidアプリでkotlinを用いて、スマホのマイクから認識した音声をSurfaceviewを用いて波形データを表示する機能を実装しました。
音声取得はAudioRECORDを用いています。
スタートボタンを押すと、音声取得が始まり、音が大きければ波形が大きく、小さければ小さく表示されるという一般的な波形データを時間経過とともに表示します。
その波形データから、数値も同時に表示することはできないかと考えています。
音が大きければ(波形がおおきければ)、数値も上がり、小さければ下がる、といった音量レベルみたいなものを数値で表せないかと思っております。
波形を分析するにはRMS(二条平均平方根)を用いると可能らしいのですが、限定はせず色々と模索しています。
何か使えそうな検索ワードや、機能があれば教えていただければと思います。
以下、音声取得から波形を表すコードです。
SurfaceView.kt
kotlin
1 2import android.annotation.SuppressLint 3import android.content.Context 4import android.graphics.Canvas 5import android.graphics.Color 6import android.graphics.Paint 7import android.util.Log 8import android.view.SurfaceHolder 9import android.view.SurfaceView 10 11@SuppressLint("ViewConstructor") 12class VisualizerSurfaceView// 線の太さ、アンチエイリアス、色、とか 13 14// この2つを書いてフォーカスを当てないとSurfaceViewが動かない? 15// isFocusable = true 16// requestFocus() 17 (context: Context, surface: SurfaceView) : SurfaceView(context), SurfaceHolder.Callback, Runnable { 18 19 private val _paint = Paint() 20 private var _buffer: ShortArray = ShortArray(0) 21 private var _holder: SurfaceHolder = surface.holder 22 private var _thread: Thread? = null 23 24 override fun run() { 25 while (_thread != null) { 26 doDraw(_holder) 27 } 28 } 29 30 init { 31 _holder.addCallback(this) 32 _paint.strokeWidth = 2f 33 _paint.isAntiAlias = true 34 _paint.color = Color.WHITE 35 } 36 37 override fun surfaceCreated(holder: SurfaceHolder?) { 38 if (holder != null) { 39 val canvas = holder.lockCanvas() 40 41 holder.unlockCanvasAndPost(canvas) 42 } 43 } 44 45 override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { 46 _thread = Thread(this) 47 _thread?.start() 48 } 49 50 override fun surfaceDestroyed(holder: SurfaceHolder?) { 51 _thread = null 52 } 53 54 fun update(buffer: ShortArray, size: Int) { 55 _buffer = buffer.copyOf(size) 56// postInvalidate() 57 } 58 59 private fun doDraw(holder: SurfaceHolder) { 60 if (_buffer.isEmpty()) { 61 return 62 } 63 64 try { 65 val canvas: Canvas = holder.lockCanvas() 66 67 canvas.drawColor(Color.BLACK) 68 69 val baseLine: Float = canvas.height / 2f 70 var oldX = 0f 71 var oldY: Float = baseLine 72 73 for ((index, _) in _buffer.withIndex()) { 74 val x: Float = canvas.width.toFloat() / _buffer.size.toFloat() * index.toFloat() 75 val y: Float = _buffer[index] / 128 + baseLine 76 77 canvas.drawLine(oldX, oldY, x, y, _paint) 78 79 oldX = x 80 oldY = y 81 } 82 83 _buffer = ShortArray(0) 84 85 holder.unlockCanvasAndPost(canvas) 86 } catch (e: Exception) { 87 Log.e(this.javaClass.name, "doDraw", e) 88 } 89 } 90} 91
MainActivity.kt
kotlin
1import android.annotation.SuppressLint 2import android.content.Intent 3import android.media.AudioFormat 4import android.media.AudioRecord 5import android.media.MediaRecorder 6import android.os.AsyncTask 7import android.os.Bundle 8import android.os.Parcel 9import android.os.Parcelable 10import android.support.v7.app.AppCompatActivity 11import android.view.SurfaceView 12import android.view.inputmethod.EditorInfo 13import android.widget.Button 14import android.widget.TextView 15import kotlinx.android.synthetic.main.activity_main.* 16import kotlinx.coroutines.GlobalScope 17import kotlinx.coroutines.delay 18import kotlinx.coroutines.launch 19 20 21class MainActivity() : AppCompatActivity(), Parcelable { 22 23 private var _record: Record? = null 24 private var _isRecording = false 25 private var _visualizer: VisualizerSurfaceView? = null 26 private var _button: Button? = null 27 28 constructor(parcel: Parcel) : this() { 29 _isRecording = parcel.readByte() != 0.toByte() 30 } 31 32 override fun onCreate(savedInstanceState: Bundle?) { 33 super.onCreate(savedInstanceState) 34 setContentView(R.layout.activity_main) 35 36 37 val surface = findViewById<SurfaceView>(R.id.visualizer) 38 _visualizer = VisualizerSurfaceView(this, surface) 39 40 _button = this.findViewById(R.id.buttonStart) 41 _button?.setOnClickListener { 42 if(_isRecording) 43 stopRecord() 44 else 45 doRecord() 46 } 47 } 48 49 override fun onPause() { 50 super.onPause() 51 stopRecord() 52 } 53 54 @SuppressLint("SetTextI18n") 55 private fun stopRecord(){ 56 _isRecording = false 57 _button?.text = "start" 58 _record?.cancel(true) 59 } 60 61 @SuppressLint("SetTextI18n") 62 private fun doRecord(){ 63 _isRecording = true 64 _button?.text = "stop" 65 66 // AsyncTaskは使い捨て1回こっきりなので毎回作ります 67 _record = Record() 68 _record?.execute() 69 } 70 71 @SuppressLint("StaticFieldLeak") 72 inner class Record : AsyncTask<Void, DoubleArray, Void>() { 73 override fun doInBackground(vararg params: Void): Void? { 74 // サンプリングレート。1秒あたりのサンプル数 75 // (8000, 11025, 22050, 44100, エミュでは8kbじゃないとだめ?) 76 val sampleRate = 8000 77 78 // 最低限のバッファサイズ 79 val minBufferSize = AudioRecord.getMinBufferSize( 80 sampleRate, 81 AudioFormat.CHANNEL_IN_MONO, 82 AudioFormat.ENCODING_PCM_16BIT) * 2 83 84 // バッファサイズが取得できない。サンプリングレート等の設定を端末がサポートしていない可能性がある。 85 if(minBufferSize < 0){ 86 return null 87 } 88 89 val audioRecord = AudioRecord( 90 MediaRecorder.AudioSource.MIC, 91 sampleRate, 92 AudioFormat.CHANNEL_IN_MONO, 93 AudioFormat.ENCODING_PCM_16BIT, 94 minBufferSize) 95 96 val sec = 1 97 val buffer = ShortArray(sampleRate * (16 / 8) * 1 * sec) 98 99 audioRecord.startRecording() 100 101 try { 102 while (_isRecording) { 103 val readSize = audioRecord.read(buffer, 0, minBufferSize) 104 105 if (readSize < 0) { 106 break 107 } 108 if (readSize == 0) { 109 continue 110 } 111 112 _visualizer?.update(buffer, readSize) 113 } 114 } finally { 115 audioRecord.stop() 116 audioRecord.release() 117 } 118 119 return null 120 } 121 } 122 123 override fun writeToParcel(parcel: Parcel, flags: Int) { 124 parcel.writeByte(if (_isRecording) 1 else 0) 125 } 126 127 override fun describeContents(): Int { 128 return 0 129 } 130 131 companion object CREATOR : Parcelable.Creator<MainActivity> { 132 override fun createFromParcel(parcel: Parcel): MainActivity { 133 return MainActivity(parcel) 134 } 135 136 override fun newArray(size: Int): Array<MainActivity?> { 137 return arrayOfNulls(size) 138 } 139 } 140} 141 142