跳转到内容

架构总览

jpFun 的排版主线将源码逐步转换为三种表示:函数调用树、时间列、带坐标的谱面对象。这三种表示分别组织语法、音乐时间和页面空间。播放从时间列分出独立路径,生成实际演奏的事件计划。函数定义每种记号的规则,引擎负责将这些规则组合成完整乐谱。

图中实线表示数据转换,虚线表示函数向各阶段提供的规则。

flowchart TB
Functions["函数定义:语法与行为"]
Source["源码"] -->|解析| AST["AST:函数调用树"]
AST -->|Lowering:产生事件、组织时间| Columns["时间列:TemporalNode 与 Track"]
AST -->|Lowering:建立附属关系| Attachments["Attachment:关系与附属对象"]
Columns --> Layout["Layout:测量、对齐、确定坐标"]
Attachments --> Layout
Layout --> Geometry["谱面对象与最终几何"]
Geometry -->|Paint:按坐标绘制| Output["SVG / Canvas"]
Columns --> Playback["Playback:组织演奏区间与声音"]
Attachments --> Playback
Playback --> Plan["播放计划:定时事件与时间映射"]
Functions -.->|参数与语法糖| AST
Functions -.->|时间行为| Columns
Functions -.->|附属规则| Attachments
Functions -.->|测量与布局规则| Layout
Functions -.->|播放规则| Playback

解析器把源码中的记号转换为函数调用,并根据内容的嵌套关系建立 AST。显式调用和语法糖都归入同一套函数模型,例如 1/ 表达减时函数作用于音符,树中由减时节点包含音符节点。

函数类定义一种记号的名称、参数、语法糖和处理行为;AST 节点则表示源码中一次具体的调用,保存这次调用的参数、子节点和源码位置。树的结构表达“谁包含谁、谁修饰谁”。

Lowering 遍历调用树,执行各节点的时间行为。音符等节点产生时间事件 TemporalNode(基类为 TemporalNodeBase);范围函数修饰内部事件,声部函数组织顺序或并行关系。

事件在这一阶段获得开始时间、持续时间和所属轨道 Track,再按时间与对齐规则归并成一列一列。每列提供一个横向对齐基准,Track 表达纵向基线及其层级关系。多声部中同时发生的音符由此可以在谱面上对齐。

连音线等关系通过 attachment 保留下来,与时间列一起交给后续阶段。至此,树中的嵌套语义已转换为事件的时间位置、轨道归属和对象间的关系。

Layout 测量可见对象的尺寸,根据时间列求横向位置,根据 Track 和对象占用求纵向位置,再放入页面。音乐时值影响间距,实际坐标同时受符号尺寸、对齐和页面空间约束。

主体坐标确定后,附属对象据此生成连线、包围框等几何。输出包含各对象的位置、尺寸和分页结果,绘制阶段按这些结果输出 SVG 或 Canvas。

Playback 按实际演奏顺序访问 Lowering 的时间列,收集有声与无声区间。它先确定区间结构、音段连接和效果范围,再生成最终速度状态、展开装饰音,最后输出供音频后端调度的定时事件。

区间是播放编译中的中间表示:休止符可以承载速度效果,增时线可以延长同一个音段,连音线可以连接保留各自装饰的独立音段。输出计划保存事件与时间映射,设备适配器据此调度声音。谱面布局与播放各自处理空间和演奏时间。

一个函数的实现可以涵盖语法、时间、布局、绘制和播放。相关行为由不同阶段调用,运行数据保存在相应对象中:

对象生成时机后续作用
函数 AST 节点解析具体调用时在 Lowering 中产生事件、修饰内容或建立关系
TemporalNodeAST 节点在 Lowering 中产生保存记谱时间;按自身能力参与布局与绘制,或发布演奏区间与控制声明
AttachmentLowering 建立关系或汇总事件时按自身能力参与布局、绘制、播放控制或音段连接
局部装饰Layout 根据主体携带的装饰信息创建扩展主体的尺寸、连接位置或绘制内容

以 1/ 为例:解析建立“减时函数包含音符函数”的树;Lowering 中音符节点产生 TemporalNode,减时函数将其时长减半,并记录减时线信息;Layout 为音符及减时线准备几何、确定位置;Paint 绘制数字和减时线。播放使用同一事件的减半时值。

函数按需要参与各阶段:音符产生主体事件,减时函数修饰内部事件,连音线函数建立附属关系。AST 在解析后保持只读;每次 Lowering 创建本轮事件与附属对象,供后续布局和播放使用。

函数负责一种记号的局部语义:参数如何解释、时长怎样变化、图形如何绘制、音段怎样演奏。引擎负责乐谱整体的组织:遍历调用树、归并时间列、求解坐标、调度绘制,以及组合播放状态与定时事件。

这种分工让每种记号的规则集中在自己的实现中,同时共享多声部对齐、分页和渲染等通用能力。新增记号时,先确定它产生或修饰哪些对象,再接入对应阶段。

具体协议与算法见源码解析、Lowering、布局、渲染和播放。公共 API 的用法见在项目中使用。