跳转到内容

在自己的项目里使用 jpFun

jpFun 的本质是一个通用的解析、布局引擎。“通用”意味着你可以在任何支持 JavaScript 的环境中使用 jpFun,而不必依赖特定的框架或平台。

因此,你可以在自己的项目中直接引入 jpFun:

终端窗口
npm install jpFun
import { compileScore, renderLayoutPagesToSvg } from "jpfun";
const source = `1,/ 2/ 3//`;
const result = compileScore(source);
const pages = renderLayoutPagesToSvg(result.layout);

或者直接在 html 中导入:

<script src="https://unpkg.com/jpfun/dist/jpfun.min.js"></script>
<script>
const { compileScore, renderLayoutPagesToSvg } = jpfun; // 暴露的全局变量
document.body.innerHTML = renderLayoutPagesToSvg(compileScore("1 2 3 | 4 -").layout)[0];
</script>

不过“通用”也有代价——具体怎么呈现、如何播放,需要开发者根据运行环境自行实现。

两个生动的例子就是配套的文档和编辑器。还记得教程中时不时插入的“示例”吗?这就是通过 jpFun 渲染出来的(构建时执行)。而编辑器是最完整的使用 jpFun 的示例,具体实现了什么功能,请参考 编辑器集成

下面将介绍 jpFun 的接口层级与调用方法。

compileScore(source, options?): CompileScoreResult

source 是完整的 jpFun 源码。compileScore 会依次完成预处理、解析、时间固化和布局,并保留每个阶段对外有用的结果:

字段内容常见用途
lineStarts每个逻辑行在源码中的起始偏移把诊断位置换算成行列号
maskedSource注释和续行符经等长空格替换后的源码按原始偏移检查有效源码字符
diagnostics解析、固化和布局阶段共享的诊断展示语法、参数和排版问题
ast完整 AST 根节点源码工具、自定义分析
lowering音乐时间、轨道和关系对象播放、MIDI 或时间分析
layout页面、对象位置和最终几何SVG、Canvas 或自定义渲染

maskedSource 与输入源码等长,所有字符偏移仍对应原始源码。它不是另一种可保存的 jpFun 文本,而是预处理阶段的中间结果。例如,预览可以借它判断一次源码点击是否落在已被掩码的注释内。

常用选项可以在一次编译中一起传入:

const compiled = compileScore(source, {
fontSize: 16,
rowGap: 18,
textMeasurer,
functions,
});
  • fontSize 设置根解析作用域的默认字号,单位为 CSS 像素。
  • rowGap 强制设置每行轨道之间的间距;缺省时按该行最大字号推导。
  • textMeasurer 替换默认文本测量器,适合需要匹配特定字体度量的应用。
  • functions 替换内置函数注册表,并非在默认函数后追加。扩展默认能力时需要自行包含 defaultFunctions
  • dampingmaxIterepscrossPunishglobalC 用于调整横向布局求解器,通常不需要设置。

页面尺寸、边距和页码属于乐谱内容,应通过源码中的 @page(...) 设置,而不是通过 compileScore 选项设置。

能够恢复的问题会进入 compiled.diagnostics,编译仍然返回可用的 AST 和布局。每条诊断都包含稳定的 code、说明文字 message 和源码偏移 span

import { ErrorDiagnostic, compileScore } from "jpfun";
const compiled = compileScore(source);
for (const diagnostic of compiled.diagnostics) {
const range = diagnostic.toLineCol(compiled.lineStarts);
console.log({
severity: diagnostic instanceof ErrorDiagnostic ? "error" : "warning",
code: diagnostic.code,
message: diagnostic.message,
range,
});
}

toLineCol 返回从 1 开始的行号和列号,适合没有文本编辑器模型的环境。CodeMirror 可以直接使用 doc.lineAt(diagnostic.span.start),VS Code 则可使用 document.positionAt(...),不必再做一次行起点查找。

无法恢复的解析错误或非法编译选项会由 compileScore 抛出,因此处理用户输入时还应包住调用。抛出的值若是 Diagnostic,其 span 仍可用于定位;其他异常应当作为内部错误展示,而不要假定它带有源码位置。

try {
const compiled = compileScore(source);
showScore(compiled.layout);
showDiagnostics(compiled.diagnostics);
} catch (error) {
showCompileError(error);
}

SVG 接口直接返回每页对应的字符串,适合服务端生成、静态站点构建和浏览器预览:

import { compileScore, renderLayoutPagesToSvg } from "jpfun";
const { layout } = compileScore(source);
const pages = renderLayoutPagesToSvg(layout, { background: "#fff" });

Canvas 接口消费同一份 layout。调用方负责为每页创建画布,并设置 CSS 尺寸和设备像素比:

import {
compileScore,
layoutPageBounds,
renderLayoutPagesToCanvas,
} from "jpfun";
const compiled = compileScore(source);
const pixelRatio = window.devicePixelRatio || 1;
const canvases = layoutPageBounds(compiled.layout).map(page => {
const canvas = document.createElement("canvas");
canvas.width = Math.ceil(page.w * pixelRatio);
canvas.height = Math.ceil(page.h * pixelRatio);
canvas.style.width = `${page.w}px`;
canvas.style.height = `${page.h}px`;
const context = canvas.getContext("2d")!;
context.scale(pixelRatio, pixelRatio);
return { canvas, context };
});
renderLayoutPagesToCanvas(
compiled.layout,
canvases.map(item => item.context),
);

传入的 context 数量必须与布局页数一致。SVG 和 Canvas 不会重新解析源码;切换后端时可以复用原来的编译结果。更详细的分页、绘制顺序和自定义 Painter 接口见渲染后端

布局和播放并列消费 loweringcompileScore 不会主动展开反复或生成播放事件;只有需要播放、导出 MIDI 或分析实际演奏顺序时,才调用 compilePlayback

import { compilePlayback, compileScore } from "jpfun";
const compiled = compileScore(source);
const plan = compilePlayback(compiled.lowering);
console.log(plan.events);
console.log(plan.durationSeconds);

PlaybackPlan 包含按演奏时间排序的速度、拍号、音色、NoteOn 和 NoteOff 事件,但不会自行发声。应用需要使用 Web Audio、Web MIDI 或其他音频后端调度这些事件。异常庞大的反复结构可以通过 { maxFlowSteps } 显式提高默认的 65,536 列访问预算;超过预算会抛出错误,而不会返回截断的计划。时间映射、反复展开和事件结构见播放

midiJsonToJpFun 接收 midi.jsJSON() 结果并生成可继续编辑的 jpFun 源码。MIDI 字节解析属于应用边界,jpFun 不会替你读取 .mid 文件:

import { midiJsonToJpFun } from "jpfun/converter/midi";
const source = midiJsonToJpFun(parsedMidiJson, {
title: "My Score",
pitchMode: "absolute",
alignRate: 4,
barsPerLine: 0,
});
  • pitchMode 默认为 "absolute";使用 "relative" 可生成以 C 为基准的数字谱。
  • alignRate 控制二进制时值的自适应量化精度。
  • barsPerLine 默认为 0,表示依据真实排版宽度自动换行;正整数表示每行固定小节数。
  • title 覆盖 MIDI 文件头中的名称。

转换器会保留音符、和弦、重叠声部、轨道名、速度、拍号和 MIDI program,并识别常见的 3:2 四分、八分与十六分三连音。鼓通道以及控制器、弯音等目前不会进入生成的源码。

musicXmlToJpFun 接收已经解析好的 XML 根元素。浏览器可以直接使用原生 DOMParser

import { musicXmlToJpFun } from "jpfun/converter/musicxml";
const xml = await file.text();
const document = new DOMParser().parseFromString(xml, "application/xml");
const parseError = document.querySelector("parsererror");
if (parseError) throw new SyntaxError(parseError.textContent || "Invalid MusicXML");
const source = musicXmlToJpFun(document.documentElement, {
pitchMode: "absolute",
barsPerLine: 4,
});

在 Node.js 中可使用任意兼容 DOM 实现,并传入满足 MusicXmlElement 接口的根元素。转换器支持 partwise/timewise、part/staff/voice、休止与和弦、倚音、连音、连音组、歌词、速度、调号、拍号、力度、反复与房子,以及基本的分页和谱头信息。

压缩的 .mxl 不在支持范围内;应先解压,或者从制谱软件导出 .musicxml

两个转换器既可以从 jpfun 根入口导入,也各自提供独立子路径。应用始终需要某个转换器时,优先静态导入对应子路径;这样入口依赖更明确,也避免把另一个转换器带进同一模块图。

对于“点击导入后才需要”的功能,使用动态 import() 可以让 Vite、Rollup、webpack 等构建工具把转换器生成独立 chunk:

async function importScore(file: File) {
if (/\.musicxml$/i.test(file.name)) {
const { musicXmlToJpFun } = await import("jpfun/converter/musicxml");
const document = new DOMParser().parseFromString(
await file.text(),
"application/xml",
);
return musicXmlToJpFun(document.documentElement);
}
if (/\.midi?$/i.test(file.name)) {
const { midiJsonToJpFun } = await import("jpfun/converter/midi");
const parsedMidiJson = await parseMidiFile(file);
return midiJsonToJpFun(parsedMidiJson);
}
return file.text();
}

这里的分包由应用构建工具完成,不是 jpFun 在运行时自行下载模块。import("jpfun") 会以整个根入口作为异步边界;若目标是分别延迟加载 MIDI 和 MusicXML,应使用上面的两个子路径。未配置打包器的原生 ESM 环境仍会按模块加载,但不会自动生成可部署的 chunk 文件。

编辑器不必在每次按键后都执行完整布局。analyzeScoreSyntax 只识别函数调用和 token,并尽量容忍尚未输入完整的代码:

import { analyzeScoreSyntax } from "jpfun";
const { syntax, diagnostics } = analyzeScoreSyntax(source);

它适合语法高亮、函数名悬浮、补全和输入过程中的轻量诊断;需要 AST、播放或渲染时仍应使用 compileScore。两条路径共享同一套语法定义,但承担不同的容错边界,具体配合方式见编辑器集成

多数应用只需要以下组合:

目标使用的入口
显示或导出谱面compileScore + renderLayoutPagesToSvg
绘制到浏览器画布compileScore + renderLayoutPagesToCanvas
播放或导出事件compileScore + compilePlayback
MIDI 转为 jpFunjpfun/converter/midi
MusicXML 转为 jpFunjpfun/converter/musicxml
编辑器即时语法反馈analyzeScoreSyntax
自定义绘制后端compileScore + paintLayoutPages / Painter

只有在实现自定义函数、编译阶段或渲染后端时,才需要直接操作 AST、Lowering 和 Layout 的低层类型。

仓库中的文档网站会在构建时调用核心库生成教程谱例;在线编辑器则展示了浏览器中编译、诊断、预览、播放和导出的完整集成。