浅谈 Wwise WWU文件处理

什么是Wwise的WWU文件
Wwise工程中,音频结构和内容的数据存储都是以WorkUnit为单位。WorkUnit在硬盘中以.wwu文件的形式进行储存。
wwu的文件格式并不神秘,用记事本/notepad等软件进行预览,可以看到第一行就有它的格式声明:

由此可以看出它是一个遵循xml(可扩展标记语言)格式的文本文件。也就是说,只要掌握了xml的编辑方法我们可以直接在外部编辑wwise的workunit。
为什么需要编辑WWU
通常来说,针对Wwise结构的管线操作,大部分情况下使用WAAPI都能顺利解决(e.g. 定制规则的批量导入、批量修改属性、批量建立Wwise结构)。
但是Waapi还是有一定的局限性:
1. 必须开着Wwise工程才能使用Waapi功能。
2. Waapi的操作是较慢的。(根据你的流程和量级,可能会要数秒至十几秒。WWU的编辑则是毫秒级的)【更正一下,Wwise2022速度快了非常多!】
3. 并不是所有的“Wwise操作”都能通过waapi实现。
当遇到这些局限性时,直接编辑WWU便成为了一个可选的方向。(另外的方向是二次开发Wwise)
如何编辑xml的内容
1) 理解xml的结构
每个由 <tag></tag> 闭合的一个区域叫做一个元素(element),每个元素的完整结构如下
<tag attrib> text <若干子元素> </tag> tail
子元素中可以再嵌套子元素,构建出完整的文件。
为方便理解,下面以一个Events类目下的DefaultWorkUnit为例进行解释。

用NotePad打开它后,首先能看到第一行是xml的格式声明。

第2行至20行是<WwiseDocument>元素。它的attrib里记录了该WWU是个“WorkUnit”、它的guid等信息。
第3行至19行的<Events>则是WwiseDocument的子元素,这表示它在Events目录下。
第4行至18行,则是<WorkUnit>元素。它的attrib里面记录了workunit的具体名字、guid。 PersistMode为“Standalone”代表着它是在根目录下,从属于其他目录下的workunit这里会显示“Nested”。
接下来让我们展开第5行的ChildrenList元素:

此时可以看到,ChildrenList里面包含了一个Event元素,Event的attrib包含了它的命名和Guid。
再展开它的ChildrenList,可以看到该Event包含的Action信息。

此时笔者在同一个Event中再加入一个Action,可以看到Event的子元素里面多了相应的信息。


对比可以看出,“Play”这个Action所需的信息特别少,只有一条target引用。而“Stop”这个Action,不仅有引用,还有各类属性信息。此例中,Delay和FadeTime填写了非默认值,被记录在文本中,而Fade-out Curve是默认值,不被记录在文本里。
同理,想要掌握其他类型的Wwise对象的数据格式,只要按照类似的方法进行观察比对即可。
2) 编辑xml的方法
理解Wwise中WWU的格式规律之后,需要的时候就可以直接编辑WWU文本来修改Wwise工程内容。
要程序化这个操作,笔者推荐使用Python标准库中的xml.etree.ElementTree模块来进行xml的读写。

这个库较为常用并且有大量的中文教程可参考
参考资料推荐:
https://docs.python.org/zh-cn/3/library/xml.etree.elementtree.html
https://www.cnblogs.com/ifantastic/archive/2013/04/12/3017110.html
PS:如果使用XPath的话,要额外注意python版本。
3) 代码示范
例1:在workUnit内找到第一个名为in_MusicName的MusicSwitchContainer元素


因为上述同类操作(根据Attrib的某个值寻找元素)非常常见,可以封装一下:

如果想用XPath来实现也可以:

例2:在指定的音乐的WorkUnit下,生成一个新的WorkUnit
def createWorkUnit_Music(self,in_parentFilePath,in_newWwuName):
rootPath,fullname=os.path.split(in_parentFilePath)
parentName,ext=os.path.splitext(fullname)
# 1.1读取母级的wwu
tree_parent=ET.parse(in_parentFilePath)
root_parent=tree_parent.getroot()
# 1.2找到新的WorkUnit应该写入的位置
workUnit_parent=self.findElementByAttrib(root_parent,"WorkUnit","Name",parentName)
if workUnit_parent==None:
print("parent 里读取workunit失败!终止")
return
ChildrenList=workUnit_parent.find("ChildrenList")
if ChildrenList==None:
print("create ChildrenList")
ChildrenList=ET.SubElement(workUnit_parent,"ChildrenList")
# 1.3 在childrenList下写入新的元素(对新建的wwu文件的引用)。此时确定了新WorkUnit的GUID
newID=self.generateGUID()
_attrib_parent={
"Name": in_newWwuName,
"ID": newID,
"PersistMode": "Reference"
}
ET.SubElement(ChildrenList,"WorkUnit",_attrib_parent)
# 1.4 存储母级的ID以备使用
parentId=workUnit_parent.get("ID")
if not parentId:
print("获取parentId失败!终止")
return
# 1.5 美化格式并且覆盖母级WWU文件
self._pretty_xml(root_parent,'\t','\n')
tree_parent.write(in_parentFilePath,encoding="utf-8",method="xml",xml_declaration=True)
# 2.1 准备新建wwu,先写入最外层的WwiseDocument元素的信息
_attrib_WwiseDocument={
"Type": "WorkUnit",
"ID": newID,
"SchemaVersion": "103",
"RootDocumentID": parentId,
"ParentDocumentID": parentId
}
newroot=ET.Element("WwiseDocument",_attrib_WwiseDocument)
# 2.2 写入InteractMusic元素的信息
InteractiveMusic=ET.SubElement(newroot,"InteractiveMusic")
_attrib_newWorkUnit={
"Name":in_newWwuName,
"ID": newID,
"OwnerID": parentId,
"PersistMode": "Nested"
}
ET.SubElement(InteractiveMusic,"WorkUnit",_attrib_newWorkUnit)
# 美化并且创建WWU文件
self._pretty_xml(newroot,'\t','\n')
newTree=ET.ElementTree(newroot)
newTree.write(rootPath+"\\"+in_newWwuName+".wwu",encoding="utf-8",method="xml",xml_declaration=True)
pass
创建完的WWU长这个样子:

4) 注意事项
1. Wwise的wwu文件格式是不断更新的,进行相关工具制作的时候一定要确认好每个写入步骤都和Wwise当前的wwu格式一致。本文的案例包括103版本(2021.1.5)和110版本(2022.1.1)

2. 因为不使用Waapi,所有版本控制必须额外完成
比如上文例2,在一个workunit下级创建新的workunit,需要迁出它母级WWU才能操作。配套使用P4的模块进行操作即可。
如何制作出Wwise的WWU格式
缩进、换行
当你试着用ElementTree读取了wwise的某个现有的wwu文件,再原样导出,你会发现你生产出的wwu文件长这个样子:所有的信息都写在了一行。

虽然排版不影响Wwise的读取,但是对debug阅读还是不方便,因此我个人采用了如下文章里介绍的排版方法,排版出的结果和Wwise基本一致。
https://blog.csdn.net/u012692537/article/details/101395192
下面则是同一个xml进行美化排版后的结果:

Attrib排序问题
上面一个问题解决以后,你还会发现你的输出的Attrib和Wwise的格式略有不同:
我们使用ElementTree输出的xml文件,它的Attrib,会强制按照字母来排序(ID在Name的前面)。它不影响任何功能,只是变得不易读了。

只要在ElementTree的源码中修改 _serialize_xml() 中的一行代码即可解决:

参考文献:https://cxyzjd.com/article/weixin_42997255/100084091
最终,我们得到了和Wwise自行生成的xml一样的格式:

如何生成GUID
从Wwise自己生成的ID的规则可以看出:Wwise使用的是4代的GUID,并且转换成了纯大写。

因此生成的方式如下:

总结
本文分享了笔者在制作wwu编辑工具时的学习路径。扫清这些障碍之后,WWU文件的程序化修改就成为了可能。这种“土法炼金”方式,可以在不二次开发Wwise的情况下,辅助Waapi,拓展Waapi工具链。