【Unity・VoicePro】Making 3D sound (spatial sound) voice chat in webGL

:::note
日本語：https://qiita.com/konbu9640/items/4f3cbaa15a1f5d3b673d
This article is translation of above.
:::

# What is VoicePro?
[VoicePro](https://assetstore.unity.com/packages/tools/input-management/voice-pro-webgl-mobile-desktop-169274) is an asset that enables voice chat in webGL build.
We will use PUN2 here.


# How to make 3D sound.
When each individual speaks, an object with an AudioSource is generated as a child of VoicePro. Let's call this as <strong>Voice Clip</strong>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/db9c877f-0210-b84b-2b08-5166dbdadaa1.png)

This is what we need to do.

Bring the voice clip to the corresponding player.
-> Adjust the AudioSource volume by the distance between client and voice clip

### Bring the voice clip to the corresponding player.
First, we need to fethch which Voice Clip corresponds to which player. For this , let's use the VoicePro network ID.
Add a edit the VoicePro asset to hold a public variable that records the network ID, and the client and Voice Clip will each obtain this network ID. The network IDs of non-clients (others) can be retrieved and identified by synchronizing them with the PUN RPC.

Make the Voice Clip's parent object the player's object, then make the coordinates the same, and you are done.

### Adjust the AudioSource volume by the distance between client and voice clip
WebGL does not support standard Spatial Sound, so you will need to adjust the volume on your own.

# Coding！

### New scrips

Attach this to the player object (enable this for client and non-clients)
```c#:VoiceSpeaker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using FrostweepGames.VoicePro;
using Photon.Pun;
using Photon.Realtime;

//VoiceClip comes to here
//ClientInit() is needed for initialization of client
public class VoiceSpeaker : MonoBehaviourPunCallbacks
{
    public string networkID;

    //get client's network ID
    public void ClientInit()
    {
        networkID = NetworkRouter.userID;

        photonView.RPC(nameof(FetchNetworkID), RpcTarget.All, networkID);
    }

    //Tell the network ID when others come
    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        base.OnPlayerEnteredRoom(newPlayer);

        photonView.RPC(nameof(FetchNetworkID), RpcTarget.All, networkID);
    }

    //Receive network ID
    [PunRPC]
    private void FetchNetworkID(string id)
    {
        networkID = id;
    }
}
```

This will be attached by Speaker.cs explained later.
```c#:VoiceClip.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//This brings the object to the corresponding VoiceSpeaker
//Also adjust the volume
public class VoiceClip : MonoBehaviour
{
    public string networkID;

    //Allocate完了済みか
    private bool isAllocated = false;

    //Listener発見済みか
    private bool hasFoundListener = false;

    private VoiceListener voiceListener = null;

    //trueならこのコンポーネントで音量調節（WebGLなら必要）
    [SerializeField] private bool shouldEditVolume = true;

    private AudioSource audioSource;

    [Header("Inverse Func")]
    [Tooltip("power")]
    [SerializeField] private int isPower = 2;
    [Tooltip("Expand coef of Y (increase output of func)")]
    [SerializeField] private float isY = 100f;
    [Tooltip("Expand coef of distance")]
    [SerializeField] private float isD = 1f;

    private void Update()
    {
        //Allocate未完なら実行
        if (!isAllocated)
        {
            Allocate();
        }

        //Listener未発見なら探す
        if (!hasFoundListener)
        {
            FindListener();
        }

        if (shouldEditVolume)
        {
            EditVolume();
        }
    }

    public void Init()
    {
        //Get the network ID
        //This is using the fact that the object name is "User_(networkID)"
        networkID = name.Split("_")[1];

        audioSource = GetComponent<AudioSource>();
    }

    private void EditVolume()
    {
        float distance = Vector3.Distance(transform.position, voiceListener.transform.position);

        //Change here if you want different adjustment
        float volume = InversePow(distance);

        //音量の最大値は1
        if(volume > 1f)
        {
            volume = 1f;
        }

        audioSource.volume = volume;
    }

    //逆二乗
    private float InversePow(float d)
    {
        return Mathf.Pow(1f / (d*isD), isPower) * isY;
    }

    //Make the corresponding player as parent
    private void Allocate()
    {
        //全VoiceSpeakerからネットワーク一致するものを探す
        VoiceSpeaker[] voiceSpeakers = FindObjectsOfType<VoiceSpeaker>();
        VoiceSpeaker targetSpeaker = null;
        foreach(VoiceSpeaker speaker in voiceSpeakers)
        {
            if(networkID == speaker.networkID)
            {
                targetSpeaker = speaker;
            }
        }

        //該当者がいない場合はAllocate未完のままreturn
        if (targetSpeaker == null)
        {
            return;
        }

        //本オブジェクトをtargetSpeakerに連結
        transform.parent = targetSpeaker.transform;
        transform.localPosition = Vector3.zero;     //座標を一致させる

        //完了登録
        isAllocated = true;
    }

    private void FindListener()
    {
        //Listenerを検索
        voiceListener = FindObjectOfType<VoiceListener>();

        //もしListenerがいなかったら未完のままにする
        if(voiceListener != null)
        {
            hasFoundListener = true;
        }
    }
}

```

Dummy component; attach this only to the client player object
```C#:VoiceListener.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VoiceListener : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

```

### Voice Pro script to edit

```diff_c_sharp:Assets/FrostweepGames/VoicePro/Scrips/Speaker.cs
//around line.70
public Speaker(INetworkActor networkActor, GameObject parent)
{
    NetworkActor = networkActor;

    _selfObject = new GameObject(Name);
    _source = _selfObject.AddComponent<AudioSource>();

//attach script to Voice Clip
+   VoiceClip voiceClip = _selfObject.AddComponent<VoiceClip>();
+   voiceClip.Init();

    _hasBeenDisposed = false;
    _buffer = new Buffer();

    SetObjectOwner(parent);
    ApplyConfig(GeneralConfig.Config.speakerConfig);
    InitSound();

    IsActive = true;         
}
```

```diff_c_sharp:Assets/FrostweepGames/VoicePro/Scrips/Networking/NetworkRouter.cs
namespace FrostweepGames.VoicePro
{
	public class NetworkRouter
	{
		private const string Unknown = "Unknown";

		/// <summary>
		/// Network event handler that raises when network data recieved
		/// </summary>
		public event Action<INetworkActor, byte[]> NetworkDataReceivedEvent;

		private static NetworkRouter _Instance;

		//network ID
+		public static string userID;

//////////////////////////////中略

static NetworkRouter()
{
    string id = GetUniqueUserId();
+    userID = id;	//save network ID
    Instance.Register(new NetworkActorInfo()
    {
        id = id,
        name = $"User_{id}"
    });
}
```

Write this when you PhotonNetwork.Instantiate()
```diff_c_sharp:PUNManager.cs
public override void OnJoinedRoom()
{
    //Generate Player
    GameObject player = PhotonNetwork.Instantiate("Player", Vector3.zero, Quaternion.identity, 0);

+    player.GetComponent<VoiceSpeaker>().ClientInit();
+    player.AddComponent<VoiceListener>();
}
```

Finish!