見出し画像

snowboy 入門 (3) - Androidでのウェイクワード検出

ウェイクワードエンジン「snowboy」のAndroidでの使い方をまとめました。

・snowboy v1.3.0
・macOS 11.4
・API 28 : Android 9 (Pie)

前回

1. Anroid用のsnowboyのビルド

2. Anroid用のsnowboyのビルドの手順

Android用の「snowboy」のビルド手順は、次のとおりです。

(1) 「swig v3」のインストール。

$ brew install swig@3

(2) 「swig v3」のパスを通す。

export PATH=/usr/local/opt/swig@3/bin:$PATH

(3) 「snowboy」のクローン。

$ git clone https://github.com/Kitt-AI/snowboy.git

(4) 「snowboy/swig/Android」フォルダでmakeします。
引数なしでARMv7、引数(BIT=64)ありでARMv8用のクロスコンパイルライブラリが生成されます。

$ make
$ make BIT=64
jniLibs/
    ├── arm64-v8a
    │   └── libsnowboy-detect-android.so
    └── armeabi-v7a
        └── libsnowboy-detect-android.so

Javaラッパーも生成されます。

java
└── ai
    └── kitt
    └── snowboy
            ├── SnowboyDetect.java
            ├── snowboy.java
            └── snowboyJNI.java

3. アプリの実装

アプリの実装手順は、次のとおりです。

(1) AndroidStudioで新規プロジェクトの作成。
(2) Anroid用のsnowboyのビルドを新規プロジェクトにコピー。

・snowboy/swig/Android/java
    → <新規プロジェクト>/app/src/main/java
snowboy/swig/Android/jniLibs
    → <新規プロジェクト>/app/src/main/jniLibs

(3) モデルを新規プロジェクトにコピー。

snowboy/swig/resources/common.res
    → <新規プロジェクト>/app/src/main/assets/snowboy/common.res
snowboy/swig/resources/model/snowboy.umdl
    → <新規プロジェクト>/app/src/main/assets/snowboy/snowboy.umdl

(4) PermissionsDispatcherのセットアップ。

(5) activity_main.xmlでボタンを1つ配置。

画像1

(6) AndroidManifest.xmlに以下のパーミッションを追加。

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

(7) MainActivityを以下のように編集。

package net.npaka.snowboyex
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import ai.kitt.snowboy.SnowboyDetect
import android.Manifest
import android.app.AlertDialog
import android.util.Log
import android.view.View
import android.content.res.AssetManager
import android.media.MediaRecorder
import android.media.AudioFormat
import android.media.AudioRecord
import android.os.Environment
import android.os.Handler
import android.os.Process
import android.widget.Button
import android.widget.Toast
import java.io.InputStream
import java.io.OutputStream
import java.io.FileOutputStream
import java.io.File
import java.lang.Exception
import java.nio.ByteBuffer
import java.nio.ByteOrder
import permissions.dispatcher.*

@RuntimePermissions
class MainActivity : AppCompatActivity(), View.OnClickListener {
    companion object {
        init {
            System.loadLibrary("snowboy-detect-android")
        }
    }

    // システム
    private var detector: SnowboyDetect? = null
    private var handler: Handler? = null
    private var thread: Thread? = null

    // UI
    private var button: Button? = null


//====================
// ライフサイクル
//====================
    // 起動時に呼ばれる
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // システム
        this.handler = Handler()

        // UI
        this.button = findViewById(R.id.button)
        this.button!!.setOnClickListener(this)
        this.button!!.setText("録音の開始")
        this.button!!.isEnabled = false

        // アセットのコピー
        copyAssets("snowboy")

        // 検出器の生成
        val path = getExternalFilesDir(null).absolutePath
        this.detector = SnowboyDetect(path+"/common.res", path+"/snowboy.umdl")
        this.detector!!.SetSensitivity("0.6")
        this.detector!!.SetAudioGain(1f)
        this.detector!!.ApplyFrontend(true)
    }

    // アプリ再開時に呼ばれる
    override fun onResume() {
        super.onResume()
        setupPermissionWithPermissionCheck()
    }


//====================
// パーミッション
//====================
    // 許可された時に呼ばれる
    @NeedsPermission(Manifest.permission.RECORD_AUDIO)
    fun setupPermission() {
        this.runOnUiThread(Runnable{
            this.button!!.isEnabled = true
        })
    }

    // 説明が必要な時に呼ばれる
    @OnShowRationale(Manifest.permission.RECORD_AUDIO)
    fun onCameraShowRationale(request: PermissionRequest) {
        AlertDialog.Builder(this)
            .setPositiveButton("許可") { _, _ -> request.proceed() }
            .setNegativeButton("許可しない") { _, _ -> request.cancel() }
            .setCancelable(false)
            .setMessage("マイクを利用します")
            .show()
    }

    // 拒否された時に呼ばれる
    @OnPermissionDenied(Manifest.permission.RECORD_AUDIO)
    fun onCameraPermissionDenied() {
        Toast.makeText(this, "拒否されました", Toast.LENGTH_SHORT).show()
    }

    // 「今後表示しない」が選択された時に呼ばれる
    @OnNeverAskAgain(Manifest.permission.RECORD_AUDIO)
    fun onCameraNeverAskAgain() {
        Toast.makeText(this, "「今後表示しない」が選択されました", Toast.LENGTH_SHORT).show()
    }


//====================
// イベント
//====================
    // クリック時に呼ばれる
    override fun onClick(v: View?) {
        if (this.button!!.text.equals("録音の開始")) {
            startRecording()
            this.button!!.text = "録音の停止"
        } else {
            stopRecording()
            this.button!!.text = "録音の開始"
        }
    }


//====================
// 録音
//====================
    // 録音の開始
    private fun startRecording() {
        if (this.thread != null) return

        // スレッドの開始
        this.thread = Thread { record() }
        this.thread!!.start()
    }

    // 録音の停止
    fun stopRecording() {
        if (this.thread == null) return

        // スレッドの停止
        this.thread = null
    }

    // 録音
    private fun record() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)

        // バッファサイズ
        val SAMPLE_RATE = 16000
        val bufferSize = (SAMPLE_RATE * 0.1 * 2).toInt()
        val audioBuffer = ByteArray(bufferSize)

        // オーディオレコーダーの開始
        val record = AudioRecord(
            MediaRecorder.AudioSource.DEFAULT,
            SAMPLE_RATE,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSize
        )
        if (record.state != AudioRecord.STATE_INITIALIZED) {
            android.util.Log.d("debug", "初期化エラー")
            return
        }
        record.startRecording()

        // メインループ
        detector!!.Reset()
        while (this.thread != null) {
            record.read(audioBuffer, 0, audioBuffer.size)

            // バッファを配列に変換
            val audioData = ShortArray(audioBuffer.size / 2)
            ByteBuffer.wrap(audioBuffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() [audioData]

            // ウェイクワード検出
            val result = detector!!.RunDetection(audioData, audioData.size)
            if (result > 0) {
                handler!!.post({
                    Toast.makeText(this, "Detect!", Toast.LENGTH_LONG).show()
                })
                android.util.Log.d("debug","detect!")
            }
        }

        // レコーダーの破棄
        record.stop()
        record.release()
    }


//====================
// ユーティリティ
//====================
    // アセットをストレージにコピー
    private fun copyAssets(folderName: String) {
        var input: InputStream? = null
        var output: OutputStream? = null
        val buffer = ByteArray(1024)
        var size: Int
        try {
            val files = assets.list(folderName)
            for (fileName in files) {
                Log.d("debug", "asset_file="+fileName)
                input = assets.open(folderName+"/"+fileName)
                output = FileOutputStream(File(getExternalFilesDir(null), fileName))
                size = input.read(buffer)
                while (size != -1) {
                    output.write(buffer, 0, size)
                    size = input.read(buffer)
                }
                input.close()
                output.close()
            }
        } catch (e: Exception) {
            Log.d("debug", e.toString())
            try {
                if (input != null) input!!.close()
                if (output != null) output!!.close()
            } catch (e2: Exception) {
            }
        }
    }
}

「録音の開始」ボタンを押下後、「snowboy」と発話すると、「Detect!」とトーストが表示されます。

画像2



この記事が気に入ったらサポートをしてみませんか?