LoginSignup
19
21

More than 5 years have passed since last update.

KotlinでVisualizerを作ってみる

Posted at

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

こんなアプリです

  • 上に波形があります
  • 開始、停止、するだけのボタンがあります
  • 開始されると音声に反応して波形が描画されます

image

マニフェストです

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

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して、ふむふむ。と

19
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
21