如何在 SwiftUI 中显示五线谱?

如何在 SwiftUI 中显示五线谱?

在开发音乐相关 App 的时候,免不了需要展示五线谱,笔者在使用 SwifUI 开发的时候发现相关资料非常的少,因此将自己的摸索做个记录。
如果单纯用图片来展示五线谱的话,肯定是不现实的,有限的几种音符能产生无限种的组合,因此我们需要有一种能够根据结构化的信息将五线谱基础元素进行组合的手段,这就是音乐字体(Music Font)。

标准音乐字体布局(SMuFL)

SMuFL 全称是 Standard Music Font Layout,标准音乐字体布局,这是一项音乐字体映射的开放标准。 此标准最初由 Steinberg 公司的 Daniel Spreadbury 为其打谱软件 Dorico 开发,但现在由 W3C 音乐记谱社区小组开发和维护,W3C 同时也在维护 MusicXML 的标准。

参考:SMuFL 维基百科
SMuFL 是音乐字体标准的重要进展,超越了1985年 Cleo Huggins 为
Adobe公司设计的Sonata字体中事实上创建的符号映射标准45(这也是Adobe的第一个原创字体6)。
目前有许多打谱软件支持SMuFL标准
7(截止2022年3月,DoricoFinaleMuseScore支持,但LilyPondSibelius不支持),并且有许多 SMuFL 标准的免费和付费字体。8
Daniel Spreadbury 为 Dorico 设计的 Bravura 字体最初于2013年发布,它是 SMuFL 的参考字体,也是 Dorico 的默认字体。
8 9 10

对这个布局标准的实现有很多种,笔者在研究尝试了 BravurabirdfontEdwinCampaniaLeland 等字体之后,最终成功使用 Bravura 字体在 SwiftUI 中完美实现五线谱显示。

Bravura 字体

按照 SMuFL 的标准,音乐字体的实现可以分为两类,一类是用于打谱印刷场景的,另一类是用于在网页等电子设备显示的,后者在字体的文件名后面以 Text 结尾以区分。字体格式遵循 OpenType 标准,如此便可以实现一些复杂的组合语法(稍后介绍)。

字体下载

Bravura GitHub repo release 页面下载最新版本的 zip 包,解压之后是这样的结构:

image.png

对于 SwiftUI 开发,我们需要的就是上图红框里面的两个 otf 文件,而实际用到的就是 BravuraText 这个。

XCode 字体安装及配置

下载好字体文件后,只需要两步即可在 XCode 工程中安装好该字体。

第一步 添加文件

将其拖到你 XCode 工程目录里面(可以将字体文件统一都放在 Fonts 目录下)。将文件拖到左侧对应目录下之后,XCode 会自动弹出一个确认对话框,记得在「Add to targets」里面勾选你的 App。

添加好文件之后的目录如下图所示

第二步 配置 Info.plist

  1. 在「Information Property List」点击「+」新增一个 Key,选择「Fonts provided by application」;
  2. 点击「Fonts provided by application」右侧「+」新增两行记录,并在新增记录的 Value 栏填写新添加两个字体的文件名(包括 otf 后缀);
    最终结果如下图

使用举例

按照上述步骤安装好字体之后即可在 SwiftUI 中显示五线谱了。
Bravura 在其 README.md 中详细介绍了使用方法,建议将这个文档仔细阅读一下。这里介绍几个核心概念和在实际使用过程中的注意事项。
一个例子:

Text("\u{E01A}\u{E050}\u{E01A}\u{EB99}\u{E0A2}\u{EB9B}\u{E0A2}\u{E0A2}- ")  
   .font(Font.custom("BravuraText", fixedSize: 64)  
   .baselineOffset(-30)

其效果如下

关于 Unicode

乐谱中所用的基础符号都需要使用 Unicode,具体可以查符号对照表

关于「宽度」

  1. 零宽度:因为在乐谱显示过程中需要将许多符号进行叠加,因此该字体的一部分符号是没有宽度的,便于其后面的符号覆盖在其上,例如五条线(5-line staff)就是零宽度的。因此一个 staff5Lines 的符号后面跟一个 NoteHead 的话,那么最终效果就是小蝌蚪画在五线谱上啦。
  2. 垂直定位:这里注意,因为 E4,G4,都是通过垂直定位符调整了的,所以其宽度为零,但是 B4 因为不需要调整垂直位置,所以是具备宽度的,因此需要将 E4(u{EB9B}u{E0A2}),G4(u{EB99}u{E0A2}) 放在前面。
  3. 音符宽度:在上述例子中,全音符的宽度是 1.5 倍 Space,U+E01A 所代表的 staff5LinesWide 的宽度是 3 倍 Space;这里 Space 即是五线谱一间的高度。
  4. NoteHead 是具有宽度的,但如果在 NoteHead 前面使用了垂直定位符(vertical positioning),则该 NoteHead 便成为了零宽度的。如此便能够显示柱式和弦的效果,如前面例子所示。
  5. 占位符:在上述例子中,最后使用了一个 “-” 符号和一个空格结尾,目的是在全音符之后进行占位,将五线谱撑到其全尺寸(3 倍 Space),“-” 占据 1 倍 Space,空格占据 0.5 倍 Space,加上全音符的 1.5 Space,刚好 3 Space,跟 U+E01A 所代表的 staff5LinesWide 的宽度相等。
  6. 高音谱号:在上述例子中,高音谱号占据接近 3 倍 Space 宽度,所以高音谱号前面的五线谱一定要用 Wide 版本的。
  7. 在上例中,我使用了 baselineOffset(-30) 做了个调整,否则高音谱号的顶部会被切割掉一点,应该是跟字体的 Ascenders 参数有关,具体原因我暂时还没搞清楚。这里推荐一篇不错的讲字体结构的文章:真的理解font-size和line-height了吗?

后记

关于五线谱的展示,本文只是初步探索了最基础、最简单的元素展示方法,更复杂的谱子如何展示,还需要花更多时间去研究和实践。
如果需要深入做这块的开发,推荐大家花点时间仔细研究下 SMuLF 标准,设计上还是挺巧妙的。

暂时还有疑惑的问题

  1. Clef 符号顶部被削掉,使用 baselineOffset 解决掉了,具体原因还没弄清楚;
  2. 垂直定位的标记最多是上移 8 或者下移 8,如果要加更多的线,如何解决?
  3. 在 SwiftUI 中使用 Unicode 字符,每次修改完都需要编译一下才能更新显示,是什么原因?