有一次接到这么一个需求:要在界面上显示出一段人物语音的长度,并且在播放的时候进行倒计时。当时看到这个需求的时候,心想就直接调官方提供的函数获取事件长度呗,但在做的时候就发现,自己天真了。
在官方提供的函数接口中(AkSoundEngine类),我翻来覆去找过很多次,并没有获取某个事件的持续时间的函数,只能自己想方设法的获取该信息,那从哪里能拿到呢?
当时的情况是,我们项目会在Timeline中使用Wwise事件来配置音频,在Timeline中,事件是可以看到长度的。(事件名涂掉了,放置泄露项目信息,虽然我也不知道为什么我要这么谨慎。。)
顺藤摸瓜找对应脚本看看,怎么得到事件长度的。
在项目中搜索AkTimelineEventPlayable
脚本,可以发现是通过AkUtilities.GetEventDurations
委托来获取的,最终这个委托是:
找到了?不要急,看脚本文件的最上面,该脚本内容只可用于UnityEditor下,在最终打包后是无法调用的,包括AkWwiseProjectInfo
脚本也是。
但既然在UnityEditor下可以获取,那我自己取来复制一份在项目中就可以了呗。
那么,解决方案就来了~
解决方案
直接上代码
- WwiseInfoCollector.cs,编辑器下的菜单脚本
[MenuItem("WWISE/Collect Event Duration")]
private static void GenerateCharacterVoiceInfo()
{
// NOTE 此处读取资源的类和方法是我们项目封装的,须自行替换。
// 读取判断是否已有.asset资源,若没有,则创建
ScriptWwiseInfo wwiseInfo = AssetLibrary.LoadAsset<ScriptWwiseInfo>(WwiseInfoHolder.WwiseInfoDataPath, true);
if (wwiseInfo == null)
{
wwiseInfo = ScriptableObject.CreateInstance<ScriptWwiseInfo>();
AssetDatabase.CreateAsset(wwiseInfo, WwiseInfoHolder.WwiseInfoDataPath);
}
var wwiseProjectData = AkWwiseProjectInfo.GetData();
// 转存信息
wwiseInfo.EventInfos = new List<WwiseEvent>();
foreach (var wwu in wwiseProjectData.EventWwu)
{
// 此处是根据事件的路径规则,只对人物语音部分进行转存,自行替换规则或删去该判断以用于所有事件。
if (wwu.ParentPath.Contains("Character"))
{
foreach (var wwuEvent in wwu.List)
{
wwiseInfo.EventInfos.Add(new WwiseEvent()
{
Id = wwuEvent.Id,
Name = wwuEvent.Name,
DurationMin = wwuEvent.minDuration,
DurationMax = wwuEvent.maxDuration,
});
}
break;
}
}
// 保存.asset文件
EditorUtility.SetDirty(wwiseInfo);
AssetDatabase.SaveAssets();
}
- WwiseInfoHolder.cs,运行时获取事件长度使用的脚本。其中有一个叫PlayNormalPrefix的变量,是我们项目里面播放音频事件的统一前缀,不是必须(其他可能还有Stop_前缀的事件等等)。实现分别使用字符串(事件名)和uint(事件id,可从Wwise_IDs脚本中获取,具体查看下方边边角角第1条)两种参数的查询方法。
public class WwiseInfoHolder
{
public const string WwiseInfoDataName = "WwiseEventDurations.asset";
public static readonly string WwiseInfoDataPath = "Assets/Wwise/ScriptableObjects/" + WwiseInfoDataName;
private readonly ScriptWwiseInfo _wwiseInfo;
private const string PlayNormalPrefix = "Play_";
public WwiseInfoHolder()
{
// NOTE 此处读取资源的类和方法是我们项目封装的,须自行替换。
_wwiseInfo = AssetLibrary.LoadAsset<ScriptWwiseInfo>(WwiseInfoDataPath, true);
if (_wwiseInfo == null)
WwiseLogger.LogMessage(WwiseLogger.LogLevel.Warning, "WwiseInfoData load failed.");
}
/// <summary>
/// Get sound duration.Only for character voice currently.
/// </summary>
/// <param name="soundName"></param>
/// <param name="durationMin"></param>
/// <param name="durationMax"></param>
public bool TryGetCharacterVoiceDuration(string soundName, out float durationMin, out float durationMax)
{
durationMin = -1;
durationMax = -1;
if (_wwiseInfo == null)
{
WwiseLogger.LogMessage(WwiseLogger.LogLevel.Warning, "WwiseInfoData was not loaded.");
return false;
}
var eventName = PlayNormalPrefix + soundName;
var result = _wwiseInfo.EventInfos.Find((wwiseEvent) => wwiseEvent.Name.ToLower() == eventName.ToLower());
if (result == null)
{
WwiseLogger.LogMessage(WwiseLogger.LogLevel.Warning,
string.Format("Didn't find duration of event named <{0}>", eventName));
return false;
}
durationMin = result.DurationMin;
durationMax = result.DurationMax;
return true;
}
/// <summary>
/// Get sound duration.Only for character voice currently.
/// </summary>
/// <param name="eventId">Wwise event id, defined in <see cref="AK.EVENTS"/></param>
/// <param name="durationMin"></param>
/// <param name="durationMax"></param>
public bool TryGetCharacterVoiceDuration(uint eventId, out float durationMin, out float durationMax)
{
durationMin = -1;
durationMax = -1;
if (_wwiseInfo == null)
{
WwiseLogger.LogMessage(WwiseLogger.LogLevel.Warning, "WwiseInfoData was not loaded.");
return false;
}
var result = _wwiseInfo.EventInfos.Find((wwiseEvent) => wwiseEvent.Id == eventId);
if (result == null)
{
WwiseLogger.LogMessage(WwiseLogger.LogLevel.Warning,
string.Format("Didn't find event duration with id <{0}>", eventId));
return false;
}
durationMin = result.DurationMin;
durationMax = result.DurationMax;
return true;
}
}
- ScriptWwiseInfo.cs,用于生成.asset文件的数据结构脚本。
[Serializable]
public class ScriptWwiseInfo : ScriptableObject
{
public List<WwiseEvent> EventInfos;
}
[Serializable]
public class WwiseEvent
{
public uint Id;
public string Name;
public float DurationMin;
public float DurationMax;
}
注意事项
- 有的事件是循环播放类型的,上述方法不可用于循环播放类型
- 非播放类型的事件持续时长为0
- 需要每隔一段时间或每次音频内容有更新或打包之前,调用一次上面的菜单脚本,保持最新的长度信息
边边角角
-
uint型事件ID
通过官方提供的方法来看(比如
AkSoundEngine.Post()
),有参数为uint类型的重载方法,标识事件的uint型ID是存于导出内容的Wwise_IDs.h
脚本中的,可用于虚幻引擎。如果要用于Unity,需要进行转换。
在Unity菜单栏Assets>Wwise>Convert Wwise SoundBank IDs,但是这个方法总是需要去手选目录,然后将生成的脚本放入项目。所以直接在IDE中搜索他的菜单项名字,找到其转换的脚本AkWwiseIDConverter,将转换的函数抄过来,写一个直接根据路径读取.c文件转换到.cs并存于项目的函数。
private static readonly string s_converterScript = Path.Combine(
Path.Combine(Path.Combine(Application.dataPath, "Wwise"), "Tools"),
"WwiseIDConverter.py");
private static readonly string s_progTitle = "WwiseUnity: Converting SoundBank IDs";
[MenuItem("WWISE/Convert Wwise_Ids.h into C# and replace")]
public static void ConvertWwiseId()
{
var wwiseAssetsPath = Path.Combine(Application.streamingAssetsPath, AkWwiseEditorSettings.Instance.SoundbankPath);
var bankIdHeaderPath = Path.Combine(wwiseAssetsPath, "Wwise_IDs.h");
if (string.IsNullOrEmpty(bankIdHeaderPath))
{
Debug.Log("WwiseUnity: User canceled the action.");
return;
}
var start = new System.Diagnostics.ProcessStartInfo();
start.FileName = "python";
start.Arguments = string.Format("\"{0}\" \"{1}\"", s_converterScript, bankIdHeaderPath);
start.UseShellExecute = false;
start.RedirectStandardOutput = true;
var progMsg = "WwiseUnity: Converting C++ SoundBank IDs into C# ...";
EditorUtility.DisplayProgressBar(s_progTitle, progMsg, 0.5f);
using (var process = System.Diagnostics.Process.Start(start))
{
process.WaitForExit();
try
{
//ExitCode throws InvalidOperationException if the process is hanging
if (process.ExitCode == 0)
{
EditorUtility.DisplayProgressBar(s_progTitle, progMsg, 1.0f);
Debug.Log(string.Format(
"WwiseUnity: SoundBank ID conversion succeeded. Find generated Unity script under {0}.", bankIdHeaderPath));
ReplaceOldWwiseIDsFile(wwiseAssetsPath);
}
else
Debug.LogError("WwiseUnity: Conversion failed.");
AssetDatabase.Refresh();
}
catch (Exception ex)
{
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
Debug.LogError(string.Format(
"WwiseUnity: SoundBank ID conversion process failed with exception: {0}. Check detailed logs under the folder: Assets/Wwise/Logs.",
ex));
}
EditorUtility.ClearProgressBar();
}
}
private static void ReplaceOldWwiseIDsFile(string wwiseAssetsPath)
{
var bankIdCsharp = "Wwise_IDs.cs";
var bankIdCSharpPath = Path.Combine(wwiseAssetsPath, bankIdCsharp);
var wwisePackagePath = Path.Combine(Path.Combine(Application.dataPath, "Wwise"), bankIdCsharp);
File.Replace(bankIdCSharpPath, wwisePackagePath, null);
}
小吐槽
Wwise资料真的少,官方文档倒是有,但真要解决个什么问题,网上都找不到方案,当然,也可能是我搜索的姿势不对。。