Kotlin練習用に
マイクからの音声入力に反応して
波形(Visualizer)を表示するだけのアプリを作ってみました。
こんなアプリです
- 上に波形があります
- 開始、停止、するだけのボタンがあります
- 開始されると音声に反応して波形が描画されます
マニフェストです
ハードウェアアクセラレーション効いてるんだろうか。
あまり変わらない気がする。
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>
レイアウトです
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で作ってみました
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です
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は使い捨て1回こっきりなので毎回作ります
        _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 とかが簡潔に書けました
MainActivity.kt
        _button?.setOnClickListener {
            if(_isRecording)
                stopRecord()
            else
                doRecord()
        }
findViewById の結果を受け取る時は as でキャストだと知りました
MainActivity.kt
val surface = findViewById(R.id.visualizer) as SurfaceView
nullな可能性のあるオブジェクトのメソッドをnullチェックしつつ気軽に呼び出せました
↓の例では_recordがnullでない場合だけexecute関数をコール
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して、ふむふむ。と
