【Unity(C#)】OculusQuestでAndroidネイティブの音声認識機能を呼び出せるのか検証

## はじめに

VRヘビーユーザーからすれば当たり前のことかもしれませんが、
[OculusQuest](https://www.oculus.com/quest-2/)にマイクが搭載されているのはご存じでしたでしょうか。

ボイスチャットに利用されることがほとんどで、
その他の用途で使われている事例をあまり見たことがありませんでした。
(たぶん世の中にはたくさんある)

しかし、最近目にした記事にボイスチャット以外の用途でマイクを使った事例がありました。
【参考リンク】：[Synamon、ロゼッタと「リアルタイム多言語翻訳システム装備のVRオフィス」を共同開発](https://prtimes.jp/main/html/rd/p/000000053.000025330.html)

一言で説明すると、翻訳VRアプリです。
マイクを音声認識の受け口として利用しています。

そこで、私も勉強がてら"OculusQuestのマイクを利用したVRアプリ作りに挑戦してみたい"と思い、実際に作りました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">勉強がてら作成していた翻訳VRできました！😎<br><br>OculusQuestのマイクで拾った音声を音声認識APIに渡して、認識結果を翻訳APIに渡す、、、というやり方です💪<br><br>次はマルチ対応していきます😆<a href="https://twitter.com/hashtag/OculusQuest?src=hash&amp;ref_src=twsrc%5Etfw">#OculusQuest</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://t.co/k95D73gEnh">pic.twitter.com/k95D73gEnh</a></p>&mdash; KENTO⚽️XRエンジニア😎Zenn100記事マラソン挑戦中29/100 (@okprogramming) <a href="https://twitter.com/okprogramming/status/1378167158694277120?ref_src=twsrc%5Etfw">April 3, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Androidネイティブの音声認識機能

Androidには音声認識の機能が搭載されており、開発者がアプリに組み込めるようにその機能が公開されています。
【参考リンク】：[RecognitionListener](https://developer.android.com/reference/android/speech/RecognitionListener)

そして、そのAndroidのネイティブの機能をUnityからネイティブプラグインとして呼び出すことができます。

OculusQuestはAndroidベースであり、Androidアプリとしてビルドすることから、
"もしかしたら、Androidのネイティブプラグインを導入すれば動くんじゃないか？"という仮説に辿り着きました。

その検証を行った結果を記していきます。

##ネイティブプラグイン作成
まずは下記リンクを参考にネイティブプラグインを作成します。
【参考リンク】：[Unity向けAndroidネイティブプラグインの作り方](https://gaprot.jp/2020/03/30/unity-android-native-plugin/)
【参考リンク】：[ AndroidのSpeechRecognizerをネイティブプラグイン化してUnityで使う](https://edom18.hateblo.jp/entry/2018/03/21/175104)

記事の通りでほとんど詰まることなくいけました。
なんか動かん、という場合には地味にやることが多いので原因の特定はそこそこの苦行となります。
ですので面倒かもしれませんが最初からやり直した方が早い場合もあるかもしれません。

Javaで書いたプラグイン側のコードとC#で書いたUnity側のコードだけメモ残します。

### プラグイン側のコード

```Java:プラグイン側のコード
package com.kento.speechtest;

import java.util.ArrayList;
import java.util.Locale;
import android.content.Context;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.content.Intent;
import static com.unity3d.player.UnityPlayer.UnitySendMessage;

public class Speech
{
    static public void StartRecognizer(Context context, final String callbackTarget, final String callbackMethod)
    {
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, Locale.JAPAN.toString());
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName());

        SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
        recognizer.setRecognitionListener(new RecognitionListener()
        {
            @Override
            public void onReadyForSpeech(Bundle params)
            {
                // On Ready for speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onReadyForSpeech");
            }

            @Override
            public void onBeginningOfSpeech()
            {
                // On begining of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onBeginningOfSpeech");
            }

            @Override
            public void onRmsChanged(float rmsdB)
            {
                // On Rms changed.
                UnitySendMessage(callbackTarget, callbackMethod, "onRmsChanged");
            }

            @Override
            public void onBufferReceived(byte[] buffer)
            {
                // On buffer received.
                UnitySendMessage(callbackTarget, callbackMethod, "onBufferReceived");
            }

            @Override
            public void onEndOfSpeech()
            {
                // On end of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onEndOfSpeech");
            }

            @Override
            public void onError(int error)
            {
                // On error.
                UnitySendMessage(callbackTarget, callbackMethod, "onError");
            }

            @Override
            public void onResults(Bundle results)
            {
                ArrayList<String> list = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
                String str = "";
                for (String s : list)
                {
                    if (str.length() > 0)
                    {
                        str += "\n";
                    }
                    str += s;
                }

                UnitySendMessage(callbackTarget, callbackMethod, "onResults\n" + str);
            }

            @Override
            public void onPartialResults(Bundle partialResults)
            {
                // On partial results.
                UnitySendMessage(callbackTarget, callbackMethod, "onPartialResults");
            }

            @Override
            public void onEvent(int eventType, Bundle params)
            {
                // On event.
                UnitySendMessage(callbackTarget, callbackMethod, "onEvent");
            }
        });

        recognizer.startListening(intent);
    }
}
```

### Unity側のコード

```C#:Unity側のコード
using System;
using UnityEngine;
using UnityEngine.Android;
using UnityEngine.UI;

// <summary>
/// Androidのネイティブ音声認識機能呼び出し
/// </summary>
public class AndroidNativeSpeech : MonoBehaviour
{
    [SerializeField] private Text recText;
    [SerializeField] private Image microPhoneImage;

    private void Start()
    {
#if UNITY_ANDROID
        if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
        {
            Debug.Log("Request");
            Permission.RequestUserPermission(Permission.Microphone);
        }
#endif
    }

    private void Update()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.touches[0];
            if (touch.phase == TouchPhase.Began)
            {
                StartSpeech();
            }
        }
    }

    /// <summary>
    /// 認識開始
    /// </summary>
    private void StartSpeech()
    {
#if UNITY_ANDROID
            var nativeRecognizer = new AndroidJavaClass("com.kento.speechtest.Speech");
            var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            var context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
            
            context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
            {
                nativeRecognizer.CallStatic("StartRecognizer", context,gameObject.name, "CallbackMethod");
            }));
#endif
    }

    /// <summary>
    /// 音声認識後のコールバックとして実行するメソッド
    /// </summary>
    /// <param name="message">認識したメッセージ</param>
    private void CallbackMethod(string message)
    {
        var messages = message.Split('\n');

        //ユーザーが話すのを開始した際のコールバック
        if (messages[0] == "onBeginningOfSpeech")
        {
            microPhoneImage.enabled = true;
        }
        
        //認識した音量変化のコールバック
        if (messages[0] == "onRmsChanged")
        {
            recText.text = "認識中...";
        }
        
        //ユーザーが話すのを終了した際のコールバック
        if (messages[0] == "onEndOfSpeech")
        {
            microPhoneImage.enabled = false;
        }
        
        //認識結果の準備が完了したコールバック
        if (messages[0] == "onResults")
        {
            var msg = "";
            for (var i = 1; i < messages.Length; i++)
            {
                msg += messages[i] + "\n";
            }

            Debug.Log(msg);
            recText.text = msg;
        }
    }
}
```

## デモ
音声が無いのでアレですが、無事動作しました。
![SpeechSample2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/f73ccaed-7ae7-7586-69fb-0081b8c363ac.gif)


## VRで検証
**結果から言うと動きません**でした。
`adb logcat -s Unity:*`とコマンドプロンプトに入力することで
動作中のUnity製Androidアプリのログを出力できます。(教えてくださった方ありがとうございます！)
その方法で実行中のVRアプリのログを確認しましたが、特にエラーを吐くことも無くただただ動いていませんでした。

同じ検証で四苦八苦している先駆者がOculusの公式コミュティに質問を記していました。
(そして、それらしい回答もなく終了していました)
![QuestQuestionSS.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/b25e0ba0-a31e-91c1-240c-c4278e32f5f8.png)

【引用元】：[On-device Speech Recognition on the Quest with Unity](https://forums.oculusvr.com/t5/Oculus-Quest-Development/On-device-Speech-Recognition-on-the-Quest-with-Unity/m-p/777619#M1242)

冒頭の翻訳VRは下記リンクの手法で実現しました。
【参考リンク】：[【Unity(C#)】Watson API × OculusQuestで音声認識](https://qiita.com/OKsaiyowa/items/59805da08d0da1e392e8)
【参考リンク】：[【Unity(C#)】Microsoft Translatorの使い方](https://qiita.com/OKsaiyowa/items/cec1e201e0eff5408f6c)

---

ちなみに、海外ユーザーは音声入力や音声コマンドが利用可能なようです。
音声コマンドは「ヘイ、フェイスブック！」ってやつです。
【参考リンク】：[音声コマンド・音声入力](https://support.oculus.com/3287153321370720/)

![QuestVoiceSS.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/c2e767fe-f9c1-ea77-a506-717a96467a98.png)

そのうち、日本語にも対応して開発者に公開されるのを期待します。

---
2021/04/08 追記
下記アセットを使えばオフラインの音声認識も可能なようです。
【Asset】：[Android Speech TTS](https://assetstore.unity.com/packages/tools/integration/android-speech-tts-45168?aid=1011l7N9K&utm_source=aff)

ただし、日本語はまだ対応していません。
英語での認識についてデモAPKを落としてきて試しましたところなかなか良い精度でした。

通常のネイティブの音声認識機能ではなく、下記ライブラリを使用しているようです。
[Kaldi Speech Recognition Toolkit](https://github.com/kaldi-asr/kaldi)

(教えてくださった方、感謝です！)

## おわりに
今回の検証結果は**OculusQuestでAndroidネイティブの音声認識機能は呼び出せない**となりました。

もし成功した方がいらっしゃったら遠慮なくツッコんでください。~~(あわよくば方法知りたい)~~