TeaCon 2023 言传身教奖获奖文章·二·Romatic Tp

原文载于知乎专栏《minecraft mod开发日志:在游戏中实现midi系统》,作者为 modist,
原地址:https://zhuanlan.zhihu.com/p/652658921


minecraft mod开发日志:在游戏中实现midi系统

0. 前言

参与过两届teacon模组开发茶会后,本人对minecraft模组制作也有了些许经验。为了响应teacon 2023“言传身教”的号召,给新手们提供技术参考,同时也是为了将来的回顾与反思,特作此文章,记录下自己走过的种种弯路。

本届teacon中,本人的参赛mod为Romantic Tp(名称来源于ZUN号的音源名称),内容包括各类虚拟乐器的演奏与midi文件播放。相比于去年的Art of TNT,规模是小了许多,但技术上的难点有增无减。其中,最主要的便是本篇文章的主题——midi系统的搭建。

Romantic Tp的github仓库位于https://github.com/ModistAndrew/RomanticTp,代码实现细节可供参考。

1. 综述

minecraft中实现midi系统的mod并不少:MusicCraftmimi-mod,包括同是本届mod的Bocchi_The_Rock,都为本人提供了许多参考。于这些mod,本人有所学习借鉴,也有所改进创新;对于各类可能的实现,在不断的重构中,有所选择,也有所舍弃。

我们的目标,是实现一套能够接收midi设备或键盘输入,以及播放midi文件;加载音色库,合成音频;并将音频接入原版openAL管线进行播放的midi系统。对于游戏内的物品,方块等,就不多做介绍了。

2. 前置知识介绍

Java内置音频与midi系统

java sound API位于javax.sound包中,提供了与音频相关的一系列功能。选择以下重点作简要介绍:

Sequencer(音序器)

Sequencer用于读取midi文件并转化为一系列Midi Message,在特定时刻发送给Synthesizer。Midi Message即midi信号,可以传输Note on,Note off,Program Change,Control Change等信息1

Synthesizer(合成器)

Synthesizer根据音色库和Midi Message合成音频,输出数据流到Source Data Line。系统内部提供默认的音色库;也可以自己加载sf2音色库2

SourceDataLine(源数据线)

Source Data Line接收音频数据流,这里的数据会直接作为音频输出播放。

openAL音频系统

openAL的音频系统在官网上有详细介绍3。它没有提供midi处理,但作为原版MC使用的音频API,它提供了良好的3D音效支持。

其中,我们最为关心的是流式播放的内容。openAL采取了一种较为原始的方法实现流式播放:我们需要向缓冲区中写入byte[]数据,定期查询已经处理完毕的数据并写入新数据,以此保持音频数据流的持续播放。

openAL还有一套EFX拓展,便于使用,能够为音频提供混响效果4

原版音频系统

原版MC的音频系统由Sound Engine管理,各个音频被包装为Channel与openAL进行互动。在上层,还有Sound Instance的封装,包含各类音频设置,并提供tick方法进行更多控制。

3. midi系统设计

许多类通过重写进行修改,这些类有Instrument或Al前缀

本人将整套系统大致分为4个部分,以下一一介绍。

红:音频输出对接到原版openAL管线

前文提到,Java的midi系统最终会通过Source Data Line进行输出。然而,我们希望不直接输出,而是对接到原版openAL管线,以获得3D音效与混响效果,同时也更方便进行管理。

首先,我们借助原版提供的Sound Buffer将Al Data Line中的数据处理成合适的格式,再写入到Al Channel中。原版的Sound Engine在tickNonPaused方法中会实时更新Channel,包括位置、音量等信息,同时对openAL缓冲区进行处理。

黄:midi信号处理与游戏数据更新

这一部分主要是通过Instrument Sound Instance进行管理的。该类负责:

  • midi信号传输。提供sendMessage方法直接传输信号,以及attachSequencer方法播放midi文件。信号会输入到Midi Filter中进行过滤(比如设置合适的乐器);再输入到Synthesizer中进行合成。
  • 资源的申请与释放。通过Instrument Sound Manager创建后,会申请Synthesizer等资源;在tick方法中实时检查是否应当调用destroy方法对自身进行资源释放。也可通过外部控制,如游戏退出时强制destroy。
  • 游戏数据更新。包括声音的位置信息,玩家使用了何种乐器,何种混响,是否被移除等等。这些都在tick方法中进行管理。

绿:游戏输入与数据同步

客户端/服务端数据同步一直是mod开发中的重难点。游戏内有三种不同的实体可以进行midi输入,需要不同的数据同步方式:

  • 实体,只能播放midi文件。服务端播放midi文件(数据直接存储在Score物品中,方便同步,且midi文件大小较小),会借助Server Instrument Sound Manager类广播数据到所有客户端。
  • AutoPlayer(自动播放器),只能播放midi文件。会自动同步数据到客户端,服务端开始播放时,会更新状态到客户端,通过Instrument Sound Manager进行播放。
  • 玩家,可播放midi文件或直接输入midi信号。前者同实体;后者输入到Local Receiver,通过Instrument Sound Manager的broadcast发送数据到服务端,再同步到所有客户端。

最终,客户端的Instrument Sound Manager接收信息,获取或创建Instrument Sound Instance并发送midi信号。

(必须承认,由于多次重构,以及原版数据同步奇奇怪怪的问题,这一部分的代码显得十分臃肿,还请各位谅解)

蓝:资源加载与分配

  • MidiFileLoader & SoundbankLoader。通过ResourceManager::listResources,我们很容易加载位于assets文件夹下的各种资源;继承Resource Manager Reload Listener,即可实现F3+T资源热重载。
  • MidiKeyboardLoader。通过配置文件中指定的名称,查找对应的midi输入设备,加载后接入Local Receiver,midi信号便会直接输入到这里。
  • SynthesizerPool。Synthesizer(通过Synthesizer Wrapper封装)申请与释放会消耗大量时间资源,需要统一管理。在游戏启动时,一次性创建指定数目(默认64个)Synthesizer Wrapper;可通过request方法申请,通过free方法释放;F3+T资源重载时,清空重建所有Synthesizer Wrapper,以更新音色库信息。

4. 翻车记录(划掉)注意事项

cpu占用

本人因cpu占用问题被组委会多次点名,实在是惨痛的经历!问题出在Source Data Line的write方法,该方法要求在无需处理音频时阻塞。然而本人起初在这里直接返回(没有Al Channel时,无需输出),造成死循环,cpu占用一度飙升到80%。后来,本人在无需输出时调用内部Source Data Line的write方法,以为完事大吉,却仍然被指cpu占用过高(可见IO永远是性能瓶颈)。最后的解决方法是短短一行:

1
Thread.sleep(10);

音频延迟

那么问题来了,我们为什么不用wait/notify让线程一直睡到Al Channel存在呢?因为阻塞太久会造成延迟:开始播放时,之前写入的数据需要先播放完毕,这造成了从启动游戏到开始播放之间的巨量延迟。打断点,游戏卡顿也会造成类似的情况,因为Sound Engine被阻塞,无法及时处理数据。

关于延迟,我们还需要了解一下Jitter Corrector5:输入端的数据不可能一直稳定,因而直接输出会造成音乐的节奏不稳。为此,Jitter Corrector被引入,在输入数据有波动时,维持输出的稳定,类似电路中的稳压器。显然,这种算法需要一定的时间来处理数据。在使用设备进行输入时,造成的延迟是很明显的。

关闭Jitter Correction后,输入延迟会大大降低;同时,由于不知名原因(可能是缓冲方式不同),阻塞造成的延迟也消失了。当然,此时播放midi音乐,能感受到十足的Rubato风格。

体积压缩

鉴于本次teacon 10MB的mod jar大小限制,在准备自带音源时有必要进行体积压缩。之前编曲用Kontakt音源动辄几十G上百G,如今却要将音源压缩到仅仅10MB,这难道不是时代的倒退吗?(划掉)

为了达到相对较高的音质,将近100MB的sf2音源是必须的。本人先后尝试了合并双声道,降低采样率等方法,最终得到的听感十分塑料。经过大佬的指点,我转而研究采样格式:sf2格式音源内部采用原始的wav格式存储,若是使用ogg等压缩格式,大小能降低至约1/8。幸运的是,恰好有一种格式的音源(.sf3)实现了这一点,而且除了压缩采样,其余没有什么改动;不幸的是,这种非官方的格式并没有什么编辑器支持,也无法通过Java Sound API直接加载。

有困难就得克服。本人利用sftools工具将sf2转换为sf3,又借助原版提供的Ogg Audio Stream,在SF2 Soundbank的基础上完成了sf3音源的支持。

内存占用

从事过编曲工作的人都知道,音频处理对于内存的需求是十分巨大的。本mod提供的音源体积不算大,但是64个Synthesizer一开,若是没有合理共享资源,内存也会吃紧。

全部Synthesizer使用的音源都是相同的,因此本人建立了Instrument Cache,相同的虚拟乐器,就不需要加载第二次了。

线程安全

音频处理中,多线程的使用是十分有必要的,因为许多操作需要消耗大量时间或是持续进行,不可阻塞主线程。这就涉及到线程安全的问题。

本人相当喜欢使用Atomic,Concurrent,Completable Future这些工具,它们为异步处理带来许多便利,基本无需考虑那些底层;但一些奇奇怪怪的问题也随之浮现,包括初始化时无法加载类,上述的内存共享方案失效等问题。最终的解决方案是,将初始化全部放在加载阶段的主线程中,同步进行。

启示:非必要不使用多线程。

我的世界为什么没有声音

无声、延迟问题是开发过程中极为常见的,当然,重启或F3+T热重载能够解决其中的绝大多数。一般来说,插拔耳机会导致音频系统重启,声音消失,这点目前还没有什么解决方案;游戏被静音时,Channel会被删除,导致对应的Instrument Sound Instance失效,我们需要在创建时检测,确保Instrument Sound Instance最新。

5. 反思与展望

  • 利用好midi channel。midi信号可指定16个channel,对应不同的乐器和控制器。起初本人将所有信号发送到一个channel上(因为乐器只能指定一个),不同channel逐个新建,这样既占用资源,又需要手动分离midi文件中的channel,十分麻烦。后来搞了个“万能乐器”保留channel信息,但觉得这种方案也不算最优。最好是可以在游戏中指定每个channel对应的乐器。
  • 更多音色效果。目前对于EFX的使用还仅限于几种混响,更多的效果有待探索。
  • midi信号可以显示、控制音乐的速度,音量,位置,演奏技法等信息,这方面大有发挥空间。
  • midi文件名支持字符少,需要转义,能否通过mixin放宽限制?有何副作用?或是自己实现资源加载。以及,通过铁砧命名Score以存入midi信息有一种WIP的感觉,需要有专门的机器做这事情。
  • 总而言之,有必要设计一些GUI;要充分利用midi的优势,对音乐进行精准控制;要利用openAL管线提供的一系列声音处理功能。

-1.后记

今年是我第二次参加teacon了:2020年了解到它,2021年前来参观,2022年正式参赛。回想起去年高二升高三的暑假,每天从早7到晚11,干得如火如荼,当时想着,到了大学,就不会对游戏有如此热情了吧。还听说,teacon 2022可能是最后一届了,之后大家都要各奔东西。不过,今年的teacon还是如期举办了,我还是着手准备起来了,还是花了一个暑假在上面。

如果说去年的Art of TNT内容太过臃肿,那么今年的Romantic Tp就有些简陋了,而且没有什么吸引力——这种移植实在是吃力不讨好的事情。想搞视听结合,搞行为艺术,原版朴素的音符盒还更浪漫些;追求方便,纯粹为了听听音乐,那又比不上同是本届mod的网络音乐机了。

在开发Romantic Tp的过程中,盘根错节的地方多,柳暗花明的地方少。我常常碰到全新的领域,需要从头学起,那个c++项目sftools的构建,就花了我几天的时间;有些地方,没有前人的经验,我也不确定该怎么做,我把JDK中不可见的Gervill包整个fork进来,并稍作修改,但不知道这样是否合规,是否显得很蠢。

初期阶段,无声、延迟、杂音是常有的事。好不容易听到声音了,发现效果不够理想,发现有bug,发现框架需要重写,又花了几天时间,最终效果也没有说好很多。确实在一点点优化,但少豁然开朗,只是热情不再。

去年凑齐了四个人的队伍,今年只剩两人参与了。当然,主要的工作总是我来完成。按Zbx的话说,我需要更push一些?不过,不要把自己的喜好强加给别人,还是一开始就少些人好。

每当看到群里有人@我,我总是感到大事不妙,心急如焚,想着肯定是自己的mod写糠了,事实也往往如此。于是加急调试、修改、推送。这些问题都是因为自身经验不足,考虑不周,在此向各位道歉,会努力改进提升。

…其实也是有一些进步的。至今我的github仓库还全部都是MC mod,但Romantic Tp的开发已让我接触了许多新知识。学习新事物的意愿与能力在开发程序的过程中总是最为重要的。另一方面,今年请到了优秀的建筑师,还主动联系了其他队伍的人员进行联合,包括btr的作者mcczai,mtr的开发者Zbx1425等。(Zbx大佬待人温和又能力超强,他是我学习的榜样!)

为什么要继续参加teacon呢?它确实带来了一些收获,但也实在耗费了大量时间。开始时可能觉得Romantic Tp是个很好的创意,但现在我得重新考虑一下了。

截止到今天(9月9日),54个参赛mod的展馆中,有1个中途退赛,17个尚未动工,16个正在建设,5个基本完成,15个已经完成。我想,既然是参赛选手,就要去迎头赶上。

许多事情,开始时可能只是一种想法,一个创意,一次心血来潮;而这份热情褪去后,推动我们不断前进的,有惯性,有信念,还有大家的一起前行。

9.19 00:58:36更新:建筑服在线人数历史新高,大家都是ddl人!

参考