Oculus Quest 2 で話してみよう!Part1


目次

やること

今回は前回作成した、Oculus Quest 2のアプリに追加機能をつけていきたいと思います。

実際に付ける内容は、
- 一定時間話しかけた後で、頷くor 相槌をうってもらう
- 特定のフレーズや単語に対して、反応してもらう
の2つの機能をつけていきます。

では、さっそくやっていきましょう!

いいタイミングで頷いてもらおう!

マイクを取り付けよう!

まず、音声の取得に必要なマイクをunityに入れていきます。

OVRCamearaRigの子に空のオブジェクトを作成して、名前をMICとしてください。

音声を取得してみよう!

以下のGetVoice.csファイルをMICにアタッチしてください。

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;

public class GetVoice : MonoBehaviour
{
    [SerializeField] 
    AudioSource listenSource;

    [SerializeField]
    Animator animator;

    //private fields
    private float gain = 8000.0f;
    float volume;
    float frequency;
    bool isSpeaking;
    bool isRecording;
    float bufferTimer; //話した後の余韻の時間 or 言葉に詰まった時の時間 の計測時間
    float speakingTimer; //実際に話している時間

    //constant values
    const int RECORD_SECONDS = 2;
    const int SAMPLING_FREQUENCY = 16000; // MSのSSTはサンプリングレートが16000Hz
    const int FFTSAMPLES = 2 << 8; //256bitのサンプルをFFTでは選んでとる。

    const float MIN_FREQ = 100.0f; //母音のFrequency は 100Hz 以上 1400Hz以下。 ただし、子音字は異なる。
    const float MAX_FREQ = 1400.0f;
    const float MIN_VOLUME = 1.0f;
    const float BUFFER_TIME = 1.6f; //話した後の余韻の時間 or 言葉に詰まった時の時間。


    // Start is called before the first frame update
    void Start()
    {
        #if UNITY_EDITOR
        gain = 2000.0f;
        #endif

        GetMic();
    }


    void Update()
    {
        Waiting();
    }



    private void GetMic()
    {
        while (Microphone.devices.Length< 1) { }
        string device = Microphone.devices[0];
        listenSource.loop = true;
        listenSource.clip = Microphone.Start(device, true, RECORD_SECONDS, SAMPLING_FREQUENCY);
        while (!(Microphone.GetPosition(device) > 0)) { }
        listenSource.Play();
    }

    async private void Waiting()
    {
        CalculateVowel();
        if (MIN_FREQ < frequency && frequency < MAX_FREQ && MIN_VOLUME < volume) //しゃべり始めの時間
        {
            isSpeaking = true;

            bufferTimer = 0.0f;
            speakingTimer += Time.deltaTime;

            //始めてレコーディングを開始したとき
            if (!isRecording)
            {
                isRecording = true;

                bufferTimer = 0.0f;
                speakingTimer = 0.0f;

            }
        }
        else if (isSpeaking && volume > MIN_VOLUME)
        {
            //子音字をしゃべっていると判定
            bufferTimer = 0.0f;
            speakingTimer += Time.deltaTime;
        }
        else if (isSpeaking && bufferTimer < BUFFER_TIME) // 余韻の時間
        {
            bufferTimer += Time.deltaTime;
            speakingTimer += Time.deltaTime;
        }
        else
        {
            bufferTimer = 0.0f; // 後で、speakingTimerの条件処理あり!

            isSpeaking = false;
            if (isRecording)
            {
                isRecording = false;

                AizuchiAnimation(speakingTimer);
            }

            speakingTimer = 0.0f;
        }

    }

    private void CalculateVowel()
    {

        //ここが処理の重さ的にやばいかも?
        var max_volume = 0.0f;
        var max_index = 0;
        var total_volume = 0.0f;
        //録音時間*サンプリング周波数の個数のデータがほしい!
        float[] temp = new float[FFTSAMPLES];
        listenSource.GetSpectrumData(temp, 0, FFTWindow.Blackman);
        for (int i = 0; i < temp.Length; i++)
        {
            if (max_volume < temp[i])
            {
                max_index = i;
                max_volume = temp[i];
            }
            total_volume += Mathf.Abs(temp[i]);
        }

        if (temp.Length > 0)
        {
            frequency = max_index * AudioSettings.outputSampleRate / 2 / temp.Length;
            volume = total_volume / temp.Length * gain;
        }
    }

    private void AizuchiAnimation(float speakingTime)
    {
        if (2.0f < speakingTime && speakingTime < 5.0f)
        {
            animator.SetInteger("aizuchi", 1);
        }

        else if (5.0f < speakingTime && speakingTime < 8.0f)
        {
            animator.SetInteger("aizuchi", 2);
        }
        else if (8.0f < speakingTime)
        {
            animator.SetInteger("aizuchi", 3);
        }
    }
}

スクリプトをアタッチ後に、AudioSourceにMICをいれて、AnimatorにはVRMのModelを入れてください。

以下に、各機能についての説明です。

  • gain  

取得した音声のVolumeを計算後に何倍するかを示しています。

PCでのテスト時には2000倍、Oculus Quest 2 での実行時には8000倍となるようにしています。

  • Waiting

音声を実際に取っているかを判定するところです。

ある一定の音量以上で、第一周波数(f0)が100Hz以上、1400Hz以下の時にしゃべっていると認識しています。

このf0の基準は日本語の母音の周波数帯が100Hz以上、1400Hz以下だからです。

  • AizuchiAnimation

実際に相槌や頷きのアニメーションを行う部分です。

秒数 動作
2s ~ 5s 「うん」と頷く
5s = 8s 「へー、そうなんだ。」と相槌をうつ
8s ~ 「なるほどね」と相槌をうつ

キャラクターの声だけを出力しよう!

上記の設定で、ご自身の声が入力できるようになったと思います。

しかし、同時にキャラクターの声と混じって、自分の声が出力されてしまいます。

なので、AudioMixerを使って、キャラクターの声だけが出力されるようにします。

まずProjectから、右クリックして Create > AudioMixer を作ってください。

つぎに、Audio Mixerを開いて、Groupsの+ボタンを押し、2つのグループのMicVoiceを追加してください。

そうしましたら、写真のようにAudioMixerを設定してください。

Micの-80dBにより、自分の声が出力されないようになります。

最後にモデルにアタッチされている Audio Source の Output に Voiceを指定し、MICにアタッチされている Audio Source の Output に Micを指定します。

これで、設定が完了しました。

Animatorを準備しよう!

Oculus Quest 2でいちゃいちゃしてみよう!Part4でのやりかたと、同一のやり方でアニメーションを作成していきます。

まず、AnimatorのParametersから、Int型のパラメータを追加し、名前をaizuchiにします。

次に、AnimationClipを3つつくり、AnimatorにDrag&Dropします。

Drag&DropしたそれぞれのAnimationClipとWaiting(待機モーション)をそれぞれに双方向につなぎます。

Waiting -> AnimationClipの方向の矢印を右クリックして、InspectorからConditionsを追加します。

Conditionsの内容は aizuchi Equals 1~3までの数字です。

つぎに、AninmationClipをクリックして、Add Behaviourからスクリプトを追加します。

前回使用した、ExitAnimation.csに以下の行を追加して、アタッチしてください。

animator.SetInteger("aizuchi", 0)

Animation Clipを編集しよう!

Oculus Quest 2でいちゃいちゃしてみよう!Part4でのAnimationClipを作成したときとやり方は同じです。

TimeLineで音声付きのAnimationClipを編集してください。

Animation Clipに音声をつけよう!

Oculus Quest 2でいちゃいちゃしてみよう!Part4で使用した、Voice_Controller.csに以下のスクリプトを追加します。

    [SerializeField] 
    public AudioClip[] aizuchiClips;

\\\
    void AizuchiPlayBack(int aizuchiIndex)
    {
        audioSource.PlayOneShot(aizuchiClips[aizuchiIndex-1], 1.0f);
    }

AnimationClipをダブルクリックして、AnimationWindowを開き、AddEventをクリックして、イベントを追加してください。

追加したEventについて、FunctionをAizuchiPlayBackとして、IntをVoiceController中のIndex+1の値に設定してください。

最後にアプリを起動させて、試してみてください。

お疲れ様でした。

次回に続きます。

今回のアプリのGitHub (git_temp2)