【Unity(C#)×PUN2】OculusQuestでのボイスチャット実装方法

##ボイスチャット

ボイスチャットの実装に当たっていろいろなライブラリがありますが、
メジャーどころで情報が多そうなPhotonを使うことにしました。

しかしながら**Photon2のボイスチャットの実装方法に関する記事**を
見つけることができませんでした。

公式のドキュメント読めば余裕かな？と思ってましたが、
実際に繋がっているかどうかのテストとかも面倒で意外と時間使ったのでメモします。

##下準備
まずは**[Photon Voice 2というアセット](https://assetstore.unity.com/packages/tools/audio/photon-voice-2-130518)の導入です。
アセットストアからインポートします。**

**[PUN2](https://assetstore.unity.com/packages/tools/network/pun-2-free-119922)もインポートします。**



次にアプリケーションIDを作成する必要があります。
アカウント作成後に下記のように**PhotonVoice**という種類の
アプリケーションを作成します。

<img width="739" alt="PUN2Voice.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d9a8b74d-3c8b-6862-d553-7e7981479767.png">


[Photonアプリケーションの作成](https://dashboard.photonengine.com/ja-JP/PublicCloud)


##設定

下記パスの**PhotonServerSettings**のアプリケーションIDを設定します。
`Assets\Photon\PhotonUnityNetworking\Resources\PhotonServerSettings`

先ほど作成した**PhotonVoice**という種類のアプリケーションIDを入力します。
![PUN2VoiceSettings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/2dff7f43-e4cd-5cee-d9dd-aa6ab5a37b65.png)

---

次にHierarchyに必要なコンポーネントを用意します。
**PhotonVoiceNetwork**と**Recorder**が必要です。

適当なオブジェクトにアタッチして、
赤枠で囲った箇所が合ってればだいたい問題ないです。
![VoiceConnectionAndRecorder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/88d7d298-2568-f011-67b9-bc85b6492f92.png)

AvatarというPrefabをアタッチしている箇所にはPhotonNetwork経由で生成する
アバター(同期オブジェクト)を設定しています。

【参考リンク】：[【Unity(C#),PUN2】OculusQuestのハンドトラッキング同期実装](https://qiita.com/OKsaiyowa/items/80fb857a285ee4aed95c)

---

Avatar側にも設定が必要なので見ていきます。

**Speaker**と**PhotonVoiceView**が必要です。
AudioSourceは自動で追加されます。

![SpeakerSettings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d43fd6df-9c73-e566-72cf-64ac95759009.png)

赤枠で囲った箇所を設定します。
**SpeakerInUseは自身のオブジェクトからアタッチ**します。

これでおおよその設定が完了しました。

##音声を口の動きに反映、通信同期

今回利用したアバターは下記画像のものです。
<img width="163" alt="NormcoreAvatar.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/5799ace6-6372-b279-9212-0f7374eeeb25.png">

球体が頭部で口は独立したオブジェクトとして頭部の子階層に配置されています。

[Normcoreと](https://normcore.io/)いうVR/ARの通信同期実装のためのライブラリから拝借しました。

今回はこの**口のオブジェクトを声に合わせてパクパクさせたい**と思います。

もともとNormcoreのライブラリで口をパクパクさせるコードが
ドキュメントに公開されていますのでそれをPUN2用に利用してみます。

```C#:Normcoreのドキュメントのコード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Normal.Realtime;

public class MouthMove : MonoBehaviour {
    public Transform mouth;

    private RealtimeAvatarVoice _voice;
    private float _mouthSize;

    void Awake() {
        // Get a reference to the RealtimeAvatarVoice component
        _voice = GetComponent<RealtimeAvatarVoice>();
    }

    void Update() {
        // Use the current voice volume (a value between 0 - 1) to calculate the target mouth size (between 0.1 and 1.0)
        float targetMouthSize = Mathf.Lerp(0.1f, 1.0f, _voice.voiceVolume);

        // Animate the mouth size towards the target mouth size to keep the open / close animation smooth
        _mouthSize = Mathf.Lerp(_mouthSize, targetMouthSize, 30.0f * Time.deltaTime);

        // Apply the mouth size to the scale of the mouth geometry
        Vector3 localScale = mouth.localScale;
        localScale.y = _mouthSize;
        mouth.localScale = localScale;
    }
} 
```


##コード
実際のコード全文が下記です。

```C#
using Photon.Pun;
using Photon.Voice.PUN;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

/// <summary>
/// しゃべると口が動く機能
/// </summary>
public class MouthSyncVoice : MonoBehaviourPun
{
    [SerializeField] private Transform _mouth;

    private PhotonVoiceView _voice;
    private float _mouthSize;

    void Start() 
    {
        if (photonView.IsMine)
        {
            _voice = GetComponent<PhotonVoiceView>();
            _voice.RecorderInUse.TransmitEnabled = true;
            
            this.UpdateAsObservable()
                .Subscribe(_ =>
                {
                    //口のオブジェクトのY軸のスケールをLerpで滑らかに動かす
                    float targetMouthSize = Mathf.Lerp(0.1f, 1.0f,100 * _voice.RecorderInUse.LevelMeter.CurrentAvgAmp);
                    _mouthSize = Mathf.Lerp(_mouthSize, targetMouthSize, 30.0f * Time.deltaTime);
                    
                    //口の動きを同期通信させる
                    photonView.RPC(nameof(SyncMouth),RpcTarget.All,_mouthSize);
                })
                .AddTo(this);
        }
    }

    /// <summary>
    /// 口の動きの変化を送信
    /// </summary>
    /// <param name="mouthSize">口の大きさ</param>
    [PunRPC]
    private void SyncMouth(float mouthSize)
    {
        Vector3 localScale = _mouth.localScale;
        localScale.y = mouthSize;
        _mouth.localScale = localScale;
    }
}

```

`_voice.RecorderInUse.TransmitEnabled = true;`
の箇所で音声のやり取りを開始しています。

`_voice.RecorderInUse.LevelMeter.CurrentAvgAmp`
で直前の0.5秒間の平均した音の波形を取得しています。

この波形を利用して口のオブジェクトのY軸方向のスケールを変化させています。


最後の`SyncMouth`メソッドの中で口のオブジェクトの大きさの変化を双方のクライアントで同期しています。


##デモ

GIFなので音はありませんが、相手の音がHMDから聞こえてきて
口がパクパクしているのを確認できました。

![SignLanguageInVRMouthSync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/4c26b354-27de-3c82-c124-144d379e5d8e.gif)

##最後に
繋がったり、繋がらなかったり、音声が極端に小さかったりと
不具合まみれだったので原因がわかり次第追記していこうと思います。