KotlinでVisualizerを作ってみる

Kotlin練習用に
マイクからの音声入力に反応して
波形（Visualizer）を表示するだけのアプリを作ってみました。

# こんなアプリです
- 上に波形があります
- 開始、停止、するだけのボタンがあります
- 開始されると音声に反応して波形が描画されます

![image](https://qiita-image-store.s3.amazonaws.com/0/76483/9ec9dcf1-8f7a-3228-b9f0-02a173d57f7d.png)

# マニフェストです
ハードウェアアクセラレーション効いてるんだろうか。
あまり変わらない気がする。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="ssuzaki.visualizersurfaceview"
		  xmlns:android="http://schemas.android.com/apk/res/android">

	<uses-permission android:name="android.permission.RECORD_AUDIO" />

	<application
		android:allowBackup="true"
		android:icon="@mipmap/ic_launcher"
		android:launchMode="singleTask"
		android:label="@string/app_name"
		android:supportsRtl="true"
		android:hardwareAccelerated="true"
		android:theme="@style/AppTheme">
		<activity android:name=".MainActivity"
				  android:hardwareAccelerated="true"
			>
			<intent-filter>
				<action android:name="android.intent.action.MAIN"/>
				<category android:name="android.intent.category.LAUNCHER"/>
			</intent-filter>
		</activity>
	</application>

</manifest>
```

# レイアウトです

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:paddingBottom="@dimen/activity_vertical_margin"
	android:paddingLeft="@dimen/activity_horizontal_margin"
	android:paddingRight="@dimen/activity_horizontal_margin"
	android:paddingTop="@dimen/activity_vertical_margin"
	tools:context="ssuzaki.visualizersurfaceview.MainActivity"
	android:orientation="vertical">

	<SurfaceView
		android:id="@+id/visualizer"
		android:layout_width="match_parent"
		android:layout_height="200dp"
		/>

	<Button
		android:id="@+id/buttonStart"
		android:layout_width="match_parent"
		android:layout_height="80dp"
		android:text="start"
		/>

</LinearLayout>
```

# SurfaceViewで作ってみました

```kotlin:VisualizerSurfaceView.kt
package ssuzaki.visualizersurfaceview

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.Log
import android.view.SurfaceHolder
import android.view.SurfaceView

open class VisualizerSurfaceView : SurfaceView, SurfaceHolder.Callback, Runnable {

    val _paint = Paint()
    var _buffer: ShortArray = ShortArray(0)
    var _holder: SurfaceHolder
    var _thread: Thread? = null

    override fun run() {
        while(_thread != null){
            doDraw(_holder)
        }
    }

    constructor(context: Context, surface: SurfaceView)
        : super(context) {

        _holder = surface.holder
        _holder.addCallback(this)

        // 線の太さ、アンチエイリアス、色、とか
        _paint.strokeWidth  = 2f
        _paint.isAntiAlias  = true
        _paint.color        = Color.WHITE

        // この2つを書いてフォーカスを当てないとSurfaceViewが動かない？
//        isFocusable = true
//        requestFocus()
    }

    override fun surfaceCreated(holder: SurfaceHolder?) {
        if(holder != null){
            val canvas = holder.lockCanvas()

            holder.unlockCanvasAndPost(canvas)
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
        _thread = Thread(this)
        _thread?.start()
    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
        _thread = null
    }

    fun update(buffer: ShortArray, size: Int) {
        _buffer = buffer.copyOf(size)
//      postInvalidate()
    }

    private fun doDraw(holder: SurfaceHolder) {
        if(_buffer.size == 0){
            return
        }

        try {
            val canvas: Canvas = holder.lockCanvas()

            if (canvas != null) {
                canvas.drawColor(Color.BLACK)

                val baseLine: Float = canvas.height / 2f
                var oldX: Float = 0f
                var oldY: Float = baseLine

                for ((index, value) in _buffer.withIndex()) {
                    val x: Float = canvas.width.toFloat() / _buffer.size.toFloat() * index.toFloat()
                    val y: Float = _buffer[index] / 128 + baseLine

                    canvas.drawLine(oldX, oldY, x, y, _paint)

                    oldX = x
                    oldY = y
                }

                _buffer = ShortArray(0)

                holder.unlockCanvasAndPost(canvas)
            }
        }catch(e: Exception){
            Log.e(this.javaClass.name, "doDraw", e)
        }
    }
}
```

# MainActivityです

```kotlin:MainActivity.kt
package ssuzaki.visualizersurfaceview

import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.AsyncTask
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.SurfaceView
import android.widget.Button

class MainActivity : AppCompatActivity() {

    var _record: Record? = null
    var _isRecording = false
    var _visualizer: VisualizerSurfaceView? = null
    var _button: Button? = null

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

        val surface = findViewById(R.id.visualizer) as SurfaceView
        _visualizer = VisualizerSurfaceView(this, surface)

        _button = findViewById(R.id.buttonStart) as Button
        _button?.setOnClickListener {
            if(_isRecording)
                stopRecord()
            else
                doRecord()
        }
	}

    override fun onBackPressed() {
        super.onBackPressed()
        stopRecord()
    }

    override fun onPause() {
        super.onPause()
        stopRecord()
    }

    fun stopRecord(){
        _isRecording = false
        _button?.text = "start"
        _record?.cancel(true)
    }

    fun doRecord(){
        _isRecording = true
        _button?.text = "stop"

        // AsyncTaskは使い捨て１回こっきりなので毎回作ります
        _record = Record()
        _record?.execute()
    }

    inner class Record : AsyncTask<Void, DoubleArray, Void>() {
        override fun doInBackground(vararg params: Void): Void? {
            // サンプリングレート。1秒あたりのサンプル数
            // （8000, 11025, 22050, 44100, エミュでは8kbじゃないとだめ？）
            val sampleRate = 8000

            // 最低限のバッファサイズ
            val minBufferSize = AudioRecord.getMinBufferSize(
                    sampleRate,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT) * 2

            // バッファサイズが取得できない。サンプリングレート等の設定を端末がサポートしていない可能性がある。
            if(minBufferSize < 0){
                return null
            }

            val audioRecord = AudioRecord(
                    MediaRecorder.AudioSource.MIC,
                    sampleRate,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    minBufferSize)

            val sec = 1
            val buffer: ShortArray = ShortArray(sampleRate * (16 / 8) * 1 * sec)

            audioRecord.startRecording()

            try {
                while (_isRecording) {
                    val readSize = audioRecord.read(buffer, 0, minBufferSize)

                    if (readSize < 0) {
                        break
                    }
                    if (readSize == 0) {
                        continue
                    }

                    _visualizer?.update(buffer, readSize)
                }
            } finally {
                audioRecord.stop()
                audioRecord.release()
            }

            return null
        }
    }
}
```

# Kotlinで作ってみてどうでしたか
振り返り

## setOnClickListener とかが簡潔に書けました

```kotlin:MainActivity.kt
        _button?.setOnClickListener {
            if(_isRecording)
                stopRecord()
            else
                doRecord()
        }
```

## findViewById の結果を受け取る時は as でキャストだと知りました
```kotlin:MainActivity.kt
val surface = findViewById(R.id.visualizer) as SurfaceView
```

## nullな可能性のあるオブジェクトのメソッドをnullチェックしつつ気軽に呼び出せました
↓の例では_recordがnullでない場合だけexecute関数をコール

```kotlin:MainActivity.kt
_record?.execute()
```

## 配列の初期化とか、複製とか、forでの回し方とか

```
// 初期化
var _buffer: ShortArray = ShortArray(0)

// 複製
fun update(buffer: ShortArray, size: Int) {

}

// forループ
for ((index, value) in _buffer.withIndex()) {

}
```

## 手が止まったら？
Javaで一旦書いて、Kotlinプラグインでconvertして、ふむふむ。と