欢迎来到 jpFun 的世界!
jpFun 是一个基于文本的简谱制作语言。只需遵循简单的编写规范,jpFun 就可以渲染出美观的简谱页面,供打印、播放等用途。它的语法设计简单优雅,是创作者吸收、反思了多个类似工具而形成的,既可以快速打谱,也能进行细致而自由的调控,同时还极具拓展性。
Why jpFun?
Section titled “Why jpFun?”jpFun 的名字构成非常简单:“简谱”的拼音首字母(参考了 JPWord 的命名),以及“函数(function)”的缩写“Fun”。双关的“乐趣(fun)”寄托了易上手、方便用的追求。
“函数” 暗示了 jpFun 最大的创新点:所有的功能使用,本质都是函数调用。这意味着,你仅需学习几个基本类型和函数调用方式,就解锁了所有功能的使用,而不必记住各种奇怪的规则和写法。配合编辑器提示,你甚至不需要熟悉各个函数即可开始创作。不过,函数的形式固然降低了上手成本,写起来却一点也不精炼。为此,jpFun 为常用的函数提供了简写(语法糖),使得效率与功能性并存。
“函数式”的本质在于 用统一的方式组织所有能力,背后是 jpFun “插件式”的架构设计宗旨:
框架实现机制,函数定义行为;函数之间解耦,注册到框架中被调用。
这使得 jpFun 能够方便地加入新功能,极具横向拓展性。
“函数” “语法糖” “插件式架构” 三者是 jpFun 设计之初就确定的原则,兼顾了使用者和开发者的体验。如果你对这三个原则的来源、或者 jpFun 整个项目的设计思路感兴趣,可以展开阅读下面的内容:
jpFun 的设计历程
我关注到“简谱脚本”是因为“je谱”。je谱诞生于“justice_eternal”吧,最早用于在贴吧、评论区分享动漫曲谱。比如现在你在b站搜索“xxx(某动漫歌曲) 口琴”,随便点击一个视频,评论区很可能就会出现 je 谱的分享。它长这个样子:
(1) (#1) b2 3 [4] [[#5]]其中,音符的写法和简谱上看到的一样,一层小括号()内表示降低八度,一层中括号[]表示升高八度,括号可以嵌套使用。
能广泛流传证明这个记谱方式确实足够便捷,但是它缺少对时值的描述。演奏者需要熟悉原曲的节奏和旋律,才能正确演奏。jpFun 的最初目的,就是拓展 je记谱法,使之可以变为正式的简谱。
乐谱排版语言并不是新事物。早在 1996 年,LilyPond 就已经实现了一个功能完备的五线谱排版系统(简直是乐谱界的 Latex)。同一时期还有 ABC 记谱法等 DSL(domain specific language, 领域特定语言) 方案。
即使细化到简谱,也已经有了很多优秀的实现:
jianpu-ly:对 LilyPond 的深度拓展,功能和 LilyPond 一样强大;JPWord:超流行的简谱打谱软件,底层是JPW-ABD脚本,但提供了图形界面,功能强大;- 番茄简谱:老牌的简谱脚本,语法简单,谱面美观;
Sparks NMN:新兴的开源简谱 DSL,可设置性较强,功能颇为丰富。完成度非常高,是本项目的主要对标对象。
但实际使用时,我始终没有找到恰好位于“容易书写”和“自由控制”之间的工具。简单的工具往往只能覆盖固定场景,一旦想调整布局或加入新记号,就会碰到边界;功能完整的工具则常常需要记忆大量规则,让写一份普通简谱也变得沉重。具体来说:
LilyPond功能强大,但过于复杂。这就好比大家对 Latex 的诟病。JPWord的脚本功能强大而完备,但是基本没法读:歌词和旋律分离、用会连起来的_表示减时线……脚本本身便不是给直接阅读设计的。番茄简谱简单高效易读,但定制性不强、功能有限,且不更新。这就好比大家对 Markdown 的诟病。Sparks NMN像番茄简谱的超级升级版,但是为了支持更多的功能,给每个功能设计了奇怪的专属语法。这不仅让语法变得复杂,也限制了功能的拓展性:新功能要新的表达,需要修改整个解析布局流程,牵一发而动全身。此外,其布局模型设计也有缺陷,不如番茄简谱美观。我专门提了个 issue。ABC记谱法也简洁,但是不支持直接阅读:需要规定好最小时值。之后为了更加广泛的应用,往里面强加了很多奇怪的非规范功能。
这些方案并不是简单的“好”或“不好”。它们分别选择了成熟能力、输入简洁和丰富控制,而 jpFun 想探索的是:能否用一个足够小的内核,同时容纳这三件事。
语法是怎样设计出来的
Section titled “语法是怎样设计出来的”jpFun 借鉴了 Typst 的思路:底层能力由函数表达,常用写法由语法糖保持简洁。Typst 中,函数调用的前缀为 #;而 # 在简谱中有“升记号”的含义,因此我换用了相邻的 @ 作为函数调用前缀。此时,创建一个音符就可以写成:@note(...) 的形式;而这里的参数,至少要支持文本、数字。
为了拥有复杂的功能,函数之间是可以嵌套的,即函数作为参数,比如“减时线”功能,一次还可以传递很多音符。因此,需要定义一个“内容”的类型——和 Typst 一样,后者是用中括号 [] 来表达的,但是考虑je谱用中括号表示升八度,我改用了大括号 {}。此时,创建一个有减时线的音符就可以写成:
@div({ @note(...), @note(...),}, ...)div 的第一个参数接收一个内容,内容里每个元素都会被画上减时线。
但是仅仅有函数的嵌套,是无法完成音乐的排版的:函数嵌套只能描述“相邻”的关系。最典型的例子是连音线,尤其是跨音符、甚至跨行的连音线。一番思考后,我设计了“引用”的机制:可以用 @xxx 给前一个元素打上标签 xxx(这里复用符号 @,很巧妙吧!),之后只要用 xxx 就可以引用元素,作为参数被函数使用。此时,连音线的调用长这样:
@note(...)@x @note(...) @note(...)@y@tie(x,y) % 传入的是两个标签也算变相的“赋值”吧!
虽然无法证明完备性,但“函数调用+标签”的组合,已经足以承载目前所有功能了。至此,jpFun 的语法框架搭建完成。而具体函数该如何设计参数类型与功能边界,则需要考虑排版布局的算法与实现,详情请参考 面向开发者。
值得注意的是,本语法设计是一个非图灵完备的声明式 DSL,采用“上下文无关的核心语法 + 上下文相关的名称解析与语义检查”。不过……我认为能力已经够用 😊
初次记谱、日常查阅和参与开发,分别从下面三个板块开始。
你将从最简单的曲谱示例开始,逐步了解 jpFun 的理念和语法,进而熟练运用常用功能。
如果你发现教程有的地方令你疑惑,可以在 编辑器 中进行尝试,然后修改一下看看。
你需要有基本的简谱知识,并克服对代码的恐惧。
按名称、别名或功能查找内置函数,查看参数、默认值,与使用示例。
大部分信息在编辑器提示中即可获得;而在这里你可以了解到函数的设计理念和实现细节。
了解源码如何变成音乐时间、页面几何和绘制命令,以及各模块的职责边界。
之后,你可以直接拓展 jpFun 的功能、补充自己需要的功能!