如果你玩过很多游戏或使用过多个应用程序,就一定会发现,游戏和应用程序中的音效可以为用户提供更沉浸的体验。如果一个应用程序没有音频,沉浸感就少了很多。
接下来我们就游戏领域来具体谈一谈这个问题。声音是人类生活不可或缺的一部分,视频游戏同理,我们通常称视频游戏里的声音为“游戏音效”。游戏中的音频包括声音效果、环境声音、背景音乐,以及游戏玩家的声音。我们可以利用游戏工具(例如 unity、unreal 等)在应用程序中开发音频仿真功能。
开发完成之后呢?测试!!!! 那么,有没有可以帮助我们实现 XR 空间音频共享等功能的自动化工具或软件?
免责声明:本文是针对一个应用程序的案例研究,我们该应用程序中有化身,他们能够相互交谈,就像通过实时音/视频应用程序(比如 zoom)交谈一样, 唯一的区别是,该应用程序是一个在 unity 中构建的桌面 XR 应用程序。我们将在这个 3D 应用程序中测试音频共享功能。
前期准备:要有 unity、C# 和使用 Arium 测试自动化的基本知识。
技术规格要求:
- 语言:C#
- 游戏开发工具:Unity
- 集成提供音频共享功能的 SDK:声网引擎视频 SDK
- 测试框架:Arium
- 应用类型:桌面
- 操作系统:Mac OS
功能介绍:
我们要构建的是一个建立在 unity 内的 Mac 版 XR 桌面应用程序,该应用程序有声网视频 SDK 提供的音频共享功能。每个加入其特定频道的用户都有化身,该化身可以通过静音/取消静音按钮相互交流。当一个用户取消静音并开始说话时,聊天室里的其他人都可以听到该用户的声音。
步骤
- 登录应用程序
- 进入主聊天室
- 连接本地主机/客户端 IP
- 用户默认处于静音状态,所以需要取消静音
- 随便说一些内容。
- 聊天室里的其他人应该能够听到
探索测试很好!但是自动化呢?如何通过自动化来复制这个场景?
自动化测试方法
接下来我们详述将这个方案自动化的方法。
关于 XR 自动化,我们没有能提供所有自动化功能的特定工具。常用的方法都要求开发者对开发代码有一定的理解。要了解开发,可以参考下列链接:
https://api-ref.agora.io/en/video- sdk/unity/3.x/index.html
在探讨自动化方法之前,我们先讨论一下要验证什么!
- 验证当一个用户处于静音状态时,聊天室里的其他用户听不到该用户的声音。
- 验证当一个用户取消静音时,聊天室里的其他用户能听到该用户的声音。
- 验证声网视频 sdk 是否与应用程序正确集成。
- 验证当用户点击静音按钮时,静音图标变为取消静音图标。
- 验证当用户点击取消静音按钮时,取消静音的图标变为静音图标。
本文只探讨前两个验证,因为后三个验证可以通过使用 Arium 和屏幕共享测试方法轻松实现自动化。具体可参考: https://medium.com/xrpractices/getting-started-with-3d-automation-arium-at-glance-fca27273426d
https://medium.com/xrpractices/screen-share-test-in-unity-59803935f1e3
测试方法 1:同时使用 AudioSource 和 AudioListener
这是我们想到的第一个方法。
1. 我们创建了两个空的 gameobject。
2. 在其中一个中添加了 audiosource 组件并把游戏对象重命名为 microphonePrefab。
3. 然后是 audiosource 组件的 audioclip 子组件。如果我们在这个 audioclip 子组件上附加任何音频片段,该音频就会在运行时播放。但如果我们保持该字段为空,它就会接受麦克风的输入。
4. 所以我们保持 audioclip 为空,并将这些 audiosource 和 audiolistener 游戏对象转换为 prefabs,在 Arium 框架的 Arium.cs 文件中加入以下代码,就可以在运行时实例化。
public GameObject InstantiatePrefab(GameObject gameobject, Vector3 position)
{
GameObject go = GameObject.Instantiate(gameobject, position, Quaternion.identity);
return go;
}
5. 在将这个 microphonePrefab gameobject 转换为 prefab 之前,我们在该 gameobject 附加了下列脚本。
using UnityEngine;
public class VoiceScript1 : MonoBehaviour
{
public AudioSource audioSource;
void Awake()
{
audioSource = GetComponent<AudioSource>();
audioSource.clip = Microphone.Start("", true, 5, 44100);
audioSource.loop = true;
while (!(Microphone.GetPosition(null) > 0)){}
audioSource.Play();
}
}
这个 voicescript1 有一个 AudioSource 音频源变量,所以我们要在把它转换为 prefab 之前,把 microphonePrefab 本身的 audiosource 组件拖放到这个公共变量中。
6. 接下来我们写了一个脚本来播放音频,就像麦克风输入一样。这是用“进程类”和 “afplay” 终端命令实现的。
using System.Diagnostics;
public class PlayAudio
{
public void PlayAudioFile()
{
var p = new Process
{
StartInfo =
{
FileName = "afplay",
Arguments = "Assets/Tests/why-hello-there-103596.mp3"
}
}.Start();
}
}
7. 我们还写了一个脚本来获取 audiolistener 的数据,这个数据可以帮助我们计算音频强度。
using UnityEngine;
public class audioData : MonoBehaviour
{
public int qSamples = 4096;
private float[] samples;
private float clipLoudness = 0f;
public void Start ()
{
samples = new float[qSamples];
}
public float GetRMS(int channel )
{
AudioListener.GetOutputData(samples, channel
float sum = 0;
for (int i=0; i < qSamples; i++)
{
Debug.Log("Sample: " + i + "::" +samples[i]);
sum += samples[i]*samples[i];
// sum squared samples
}
foreach (var sample in samples) {
clipLoudness += Mathf.Abs(sample);
}
return Mathf.Sqrt(sum/qSamples);
}
}
局限性
因为 RMS 的结果不同,所以测试出现偏差。
预期结果:当我们取消静音并播放音频时,输出的 RMS 应该大于 0;当我们处于静音状态并播放音频时,RMS 应该等于 0。
实际结果:无论静音或不静音,测试获得的 RMS 均大于 0。
测试失败的原因
AudioSource 在游戏场景中,从麦克风直接输入并在游戏场景中播放音频。Audiolistener 不是在听麦克风,而是在听 audiosource。因此,当处于静音状态并播放音频时,audiosource 仍然会接受麦克风的输入,Audiolistener 就会得到输入,导致 均方根大于 0。
测试方法 2:不用 AudioSource,只使用 AudioListener
这个方法从麦克风直接输入到 audiolistener。
测试脚本:
//Loading the home scene where we have audio sharing feature
LoadHomeScene();
yield return new WaitForSeconds(15);
//connect to localhost/client ip using network manager
GameObject networkManager = _findGameObjects.FindNetworkManager();
networkManager.GetComponent<NetworkManager>().StartHost();
yield return new WaitForSeconds(10);
//click on microphone button to unmute
GameObject microphone = _findGameObjects.FindMicButton();
_arium.PerformAction(new UnityPointerClick(), microphone);
yield return new WaitForSeconds(4);
//play the audio which will act like microphone input
PlayAudio audio = new PlayAudio();
audio.PlayAudioFile();
yield return new WaitForSeconds(4);
//calling audioData script to calculate RMS value
audioData listen = _arium.GetComponent<audioData>("LoadAvator [connId=0]/Avatar");
//here 0 inside GetRMS denotes channel
float vol = listen.GetRMS(0);
Debug.Log("volume ====" + vol);
输出:
每次我们都会得到 RMS=0
局限性:
AudioListener 不能直接监听麦克风。
测试失败的原因:
场景中的 audiolistener 只能获得 audiosource 输入,不能直接获得麦克风输入。因此,每当我们播放音频并试图从 audiolistener 获取数据时,就会得到 RMS =0。
测试方法 3:通过语音识别进行验证
我们测试的第三个方法是基于语音识别。我们在大量的研究后发现了这段代码。这个方法的主要想法是,创建一个游戏对象(例如,立方体),并在运行时在主场景中将该对象实例化,然后在该脚本中创建一个字典,其中有一组立方体可以识别的关键词。然后,我们通过自动化给其中一些关键词音频输入,也可以说是通过自动化播放这些关键词的音频。 针对每个关键词,我们都有一个附加给立方体的动作。我们将在游戏场景中通过测试脚本在运行时调用这个立方体,然后播放音频,立方体识别关键词并在场景中执行相应动作。这意味着音频是在游戏场景内进行的,因此聊天室的所有其他人也能听到正在发送的音频。
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections;
using UnityEngine.Windows.Speech;
public class VoiceControl : MonoBehaviour
{
// Start is called before the first frame update
// Voice command vars
private Dictionary<string, Action> keyActs = new Dictionary<string, Action>();
private KeywordRecognizer recognizer;
// Var needed for color manipulation
private MeshRenderer cubeRend;
//Var needed for spin manipulation
private bool spinningRight;
//Vars needed for sound playback.
private AudioSource soundSource;
public AudioClip[] sounds;
void Start()
{
cubeRend = GetComponent<MeshRenderer>();
soundSource = GetComponent<AudioSource>();
//Voice commands for changing color
keyActs.Add("red", Red);
keyActs.Add("green", Green);
keyActs.Add("blue", Blue);
keyActs.Add("white", White);
//Voice commands for spinning
keyActs.Add("spin right", SpinRight);
keyActs.Add("spin left", SpinLeft);
//Voice commands for playing sound
keyActs.Add("please say something", Talk);
//Voice command to show how complex it can get.
keyActs.Add("pizza is a wonderful food that makes the world better", FactAcknowledgement);
recognizer = new KeywordRecognizer(keyActs.Keys.ToArray());
recognizer.OnPhraseRecognized += OnKeywordsRecognized;
recognizer.Start();
}
void OnKeywordsRecognized(PhraseRecognizedEventArgs args)
{
Debug.Log("Command: " + args.text);
keyActs[args.text].Invoke();
}
void Red()
{
cubeRend.material.SetColor("_Color", Color.red);
}
void Green()
{
cubeRend.material.SetColor("_Color", Color.green);
}
void Blue()
{
cubeRend.material.SetColor("_Color", Color.blue);
}
void White()
{
cubeRend.material.SetColor("_Color", Color.white);
}
void SpinRight()
{
spinningRight = true;
StartCoroutine(RotateObject(1f));
}
void SpinLeft()
{
spinningRight = false;
StartCoroutine(RotateObject(1f));
}
private IEnumerator RotateObject(float duration)
{
float startRot = transform.eulerAngles.x;
float endRot;
if (spinningRight)
endRot = startRot - 360f;
else
endRot = startRot + 360f;
float t = 0f;
float yRot;
while (t < duration)
{
t += Time.deltaTime;
yRot = Mathf.Lerp(startRot, endRot, t / duration) % 360.0f;
transform.eulerAngles = new Vector3(transform.eulerAngles.x, yRot, transform.eulerAngles.z);
yield return null;
}
}
void Talk()
{
soundSource.clip = sounds[UnityEngine.Random.Range(0, sounds.Length)];
soundSource.Play();
}
void FactAcknowledgement()
{
Debug.Log("How right you are.");
}
}
局限性:
我们必须在基于 Mac 的应用程序上进行此测试,但是代码只适用于 windows 系统。它使用的是 UnityEngine.Windows.Speech 库。我们找不到任何基于 mac 的应用程序的类似的库。虽然我们没有在 Mac 系统上进行测试,但我们证明了它在 windows 系统上可以正常工作。
测试方法 4:录制游戏音频
本方法计划使用 ffmpeg 命令来录制游戏音频。我们计划先通过脚本播放音频,然后录制游戏音频,再将 .mp3 或 .wav 文件转换为文本,即字符串,然后可以对给定的输入进行验证。
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
public class RecordAudio
{
public void RecordAudioFile()
{
var p = new Process
{
StartInfo =
{
FileName = "ffmpeg",
Arguments = "/Users/alisha.raizada/Music/audiofilecapture.mp3"
}
}.Start();
}
}
局限性:
此方法无法实现预期目的。
预期结果:记录一个特定的麦克风频道。
实际结果:只实现了正常的音频录制。
实施这个方法后,我们发现需要获取特定的 AudioFrame 的程序,这样才能实现预期目的,而且这种功能只能由声网引擎来提供。这就是我们一开始提到的问题,在 XR 自动化中,更好地理解开发代码和实现是非常重要的,这为我们打开了很多学习和探索的大门。
测试方法 5:从特定的 AudioFrame 获取音频数据
使用这个方法,我们需要添加一个内部类“AudioFrameObserver”,该类包含了从加入特定频道到离开特定频道的所有方法。
在“Join”函数中,添加下列几行代码:
另外,我们需要创建一个名为“GetAudioFrame”的单独函数,将其返回音频帧。
internal class AudioFrameObserver : IAudioFrameObserver
{
private readonly SpatialAudio _videoSample;
internal AudioFrameObserver(SpatialAudio videoSample)
{
_videoSample = videoSample;
}
// Sets whether to receive remote video data in multiple channels.
public virtual bool IsMultipleChannelFrameWanted()
{
return true;
}
// Occurs each time the player receives an audio frame.
public bool OnFrame(AudioPcmFrame videoFrame)
{
return true;
}
// Retrieves the mixed captured and playback audio frame.
public override bool OnMixedAudioFrame(string channelId, AudioFrame audioFrame)
{
return true;
}
// Gets the audio frame for playback.
public override bool OnPlaybackAudioFrame(string channelId, AudioFrame audioFrame)
{
return true;
}
// Retrieves the audio frame of a specified user before mixing.
public override bool OnPlaybackAudioFrameBeforeMixing(string channelId, uint uid, AudioFrame audioFrame)
{
return true;
}
// Gets the playback audio frame before mixing from multiple channels.
public virtual bool OnPlaybackAudioFrameBeforeMixingEx(string channelId, uint uid, AudioFrame audioFrame)
{
return false;
}
// Gets the captured audio frame.
public override bool OnRecordAudioFrame(string channelId, AudioFrame audioFrame)
{
_videoSample._audioFrame = audioFrame;
return true;
}}
On join
public void Join(string _token , string _channelName)
{
RtcEngine.RegisterAudioFrameObserver(new AudioFrameObserver(this));
// Set the format of the captured raw audio data.
int SAMPLE_RATE = 16000, SAMPLE_NUM_OF_CHANNEL = 1, SAMPLES_PER_CALL = 1024;
RtcEngine.SetRecordingAudioFrameParameters(SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL,
RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, SAMPLES_PER_CALL);
RtcEngine.SetPlaybackAudioFrameParameters(SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL,
RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, SAMPLES_PER_CALL);
RtcEngine.SetMixedAudioFrameParameters(SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, SAMPLES_PER_CALL);
}
//test func
public AudioFrame GetAudioFrame()
{
Debug.Log("Bytes:::"+ _audioFrame);
return _audioFrame;
}
完成以上步骤后,转到主测试文件,调用“GetAudioFrame”方法。
SpatialAudio obj = new SpatialAudio();
obj.GetAudioFrame();
然后,从上述选项中选择一种方法来获取特定音频帧的数据。
注意:从不同的文件夹或目录中调用特定方法时,需要在程序集定义中提供该方法的引用。
以上就是所有内容,感谢大家的阅读!
原文作者:Alisha Raizada
原文链接:https://medium.com/xrpractices/playing-with-audio-in-unity-453970fdc2ef