通話アプリで仮想空間での距離取得と距離に応じた音量の調整方法

リモートワークやリモート展示会等の通話アプリサービスが増えてきました。
どのサービスも品質や使い勝手がよく、非常に便利になってきました。
最近よく見かけるのは、より臨場感を出す仕組みの提供だと思います。

この記事ではAgora.io SDKを用いて以下の機能の実装方法を解説します。
 
 - 仮想空間上で相手とどの程度離れているか
 - 距離によって通話音量を変化させる

これらはメタバースでも必要な要素かと思います。
利用するSDKはWebSDKとRTM SDKになります。
サンプルは[AgoraIO-WebRTC-VoiceVolumeControl](https://github.com/fu-jimoto/AgoraIO-WebRTC-VoiceVolumeControl)に公開しています。

Agora社からはUnityでのデモが公開されています。
[記事](https://www.agora.io/en/blog/implementing-spatial-audio-chat-in-unity-using-agora/) / [Github](https://github.com/AgoraIO-Community/agora-unity-partychat-demo)


##実装内容
実装内容を詳しく見ていきます。

###STEP1
まずは、音声通話への入室とRTMへの入室を実行します。
自拠点の音声をパブリッシュした後に、どのPositionにいるかを表示します。


```javascript:index.html
async function join() {

  userInfo.nickname = nickname.value;
  userInfo.position = position.value;

  await joinRTM();

  client.on("user-published", handleUserPublished);
  client.on("user-unpublished", handleUserUnpublished);

  [ uid, localTracks.audioTrack ] = await Promise.all([
    client.join(appId.value,channel.value, null,userInfo.uid),
    AgoraRTC.createMicrophoneAudioTrack({microphoneId:audioSource.value})
  ]);

  userInfo.join = true;
  await client.publish(localTracks.audioTrack);
  console.log("publish success");
  users[uid] = {nickname: nickname.value, position: position.value};
  updateUserInfo();//該当座標にニックネームを表示

}

async function joinRTM(){
  //rtm
  rtm = AgoraRTM.createInstance(appId.value);
  await rtm.login({ token: '', uid: String(userInfo.uid) }).then(() => {
    console.log('AgoraRTM client login success');

    //中略
 
    //rtm channel
    rtmChannel = rtm.createChannel(channel.value);
    rtmChannel.join().then(()=>{
      console.log("RTM Join Channel Success");

      //中略

    });
  }).catch(err => {
      console.log('AgoraRTM client login failure', err);
  });

}
```
Positionはプルダウンで選択します。入室後の移動もこのプルダウンを利用します。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/4223e3a4-b6c0-61e1-0061-c7ab3e5bb04d.png)


###STEP2
次に他拠点の入室イベントを取得した際、相手のPositionを取得する処理を行います。
（音量調整が必要ないアプリケーションであればこの時点で他拠点の音声ストリームを取得しますが、今回はワンクッション入ります）
RTMのピアメッセージAPIでどのPositionにいるかを問い合わせます。

```javascript:index.html
function handleUserPublished(user, mediaType) {
  console.log("handleUserPublished");
  const id = user.uid;
  remoteUsers[id] = {user:user, mediaType:mediaType};
  rtm.sendMessageToPeer({ text: JSON.stringify({meta: "getPosition"}) }, String(id), ).then(sendResult => {//位置情報を要求
    console.dir(sendResult);
  }).catch(error => {
  });
}
```

他拠点からの位置確認が呼ばれたらPositionとニックネームを送り返します。
Positionとニックネームを受け取ったら音声ストリームの取得・再生とPositionの表示を開始します。
ボリュームは0-100の間で指定します。今回は1Position毎に20ずつ下げていきます。

```javascript:index.html
async function joinRTM(){
  //rtm
  rtm = AgoraRTM.createInstance(appId.value);
  await rtm.login({ token: '', uid: String(userInfo.uid) }).then(() => {
    console.log('AgoraRTM client login success');
    rtm.on('MessageFromPeer', async function (message, peerId) {
      var res = JSON.parse(message.text);
      if(res.meta == "getPosition"){//他拠点からの位置確認
        rtm.sendMessageToPeer({ text: JSON.stringify({meta: "resPosition", nickname: userInfo.nickname, position: userInfo.position}) }, peerId,).then(sendResult => {
        }).catch(error => {
        });
      }else if(res.meta == "resPosition"){//他拠点の位置情報取得
        console.log("RTM: MessageFromPeer:pos");
        users[peerId] = {nickname: res.nickname, position: res.position};
        updateUserInfo();
        var dist = userInfo.position - res.position;
        var volume = 100 - (Math.abs(dist))*20;
        subscribe(remoteUsers[peerId].user, remoteUsers[peerId].mediaType, volume);
      }
    });

    //中略

  }).catch(err => {
      console.log('AgoraRTM client login failure', err);
  });

}

async function subscribe(user, mediaType,volume) {
  const uid = user.uid;
  await client.subscribe(user, mediaType);
  console.log("subscribe success");
  user.audioTrack.setVolume(volume);//他拠点音声の再生ボリューム指定
  user.audioTrack.play();
}
```
TANAKAさんとYAMADAさんが一番離れた状態で入室しました。この状態では音量は最小になります。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/d749b65e-a503-ad10-2a34-3d21aa9b52b2.png)


###STEP3
最後はPositionを変更した場合の処理です。
この処理では個別にメッセージを送るのではなく、通話参加者に一斉にPosition変更を伝えます。
又、自分自身も他拠点のボリュームを変化させる処理を行います。
変更の通知を受けた拠点側でもボリュームを変化とPositionの表示処理を行います。


```javascript:index.html
function changePosition(){
  if(userInfo.join === true){
    userInfo.position = position.value;
    //参加者全員に位置変更を通知
    rtmChannel.sendMessage({text: JSON.stringify({meta: "changePosition", nickname: userInfo.nickname, position: userInfo.position})});
    users[userInfo.uid] = {nickname: userInfo.nickname, position: userInfo.position};
    updateUserInfo();

    //自分自身も他拠点の音量を変更
    for(var i in users){
      if(i != userInfo.uid){
        var dist = userInfo.position - users[i].position;
        var volume = 100 - (Math.abs(dist))*20;
        remoteUsers[i].user.audioTrack.setVolume(volume);
      }
    } 
  }
}

async function joinRTM(){
  //rtm
  rtm = AgoraRTM.createInstance(appId.value);
  await rtm.login({ token: '', uid: String(userInfo.uid) }).then(() => {
    console.log('AgoraRTM client login success');
    
    //中略

    //rtm channel
    rtmChannel = rtm.createChannel(channel.value);
    rtmChannel.join().then(()=>{
      console.log("RTM Join Channel Success");
      rtmChannel.on('ChannelMessage', function (message, memberId) {
        console.log("Get channel message:"+memberId);
        console.dir(message);
        var res = JSON.parse(message.text);
        if(res.meta == "changePosition"){//他拠点が位置を変更した通知
          users[memberId] = {nickname: res.nickname, position: res.position};
          updateUserInfo();
          var dist = userInfo.position - res.position;
          var volume = 100 - (Math.abs(dist))*20;
          remoteUsers[memberId].user.audioTrack.setVolume(volume);
        }
      });
    });
  }).catch(err => {
      console.log('AgoraRTM client login failure', err);
  });

}

```
TANAKAさんとYAMADAさんが同じPositionにいるので音量は最大になります。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/99ddc516-3d32-aa41-c427-03cad730faa4.png)

実装の解説は以上になります。

###補足
実サービスにおいては縦横の座標を考慮したり、移動がマウスでの操作で行われたりすると想定されます。
又、同じPositionに移動したら通話開始という仕様も考えられます。
今回のサンプルではシンプルな実装で概念を理解するためのものになります。




##最後に                                                                                                    
[**agora.ioに関するお問い合わせはこちらから**](https://jp.vcube.com/lp/agora?utm_campaign=Agora&utm_source=Qiita&arc_id=457746af29bcb8dba854)   
[<img width="912" alt="スクリーンショット 0001-08-15 13.41.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300377/5d815376-9b89-08fa-9434-6fa7e8e67bcf.png">](https://jp.vcube.com/lp/agora?utm_campaign=Agora&utm_source=Qiita&arc_id=457746af29bcb8dba854)