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