跳到主要内容
食谱编号164

排版食谱 · 第9章 · 完整出版物

旋律、歌词与和弦齐备的歌本

每首歌是一个ABC代码块:旋律、谱表上方的和弦名、音符下方的第一段歌词,由同一个乐谱样式按栏宽刻印。

本页内容
输出
Canvas · PDF
难度
中级
Postext
已用Postext 1.54.0测试
需要≥ 1.54.0 · postext-pdf ≥ 1.54.0
许可证
更新于2026年10月22日
代码MIT · 文本CC BY 4.0

第2–3页,共6页

  • 英文样例:尚无中文版本
  • 成品尺寸148 × 210 mm
  • 1栏
  • Spectral 10.3/14
  • Fraunces
  • Spectral SC
  • 6页
  • 难度
  • Postext 1.54.0
  • 排版用时300 ms
  • 164行代码

简单来说

一本围着餐桌唱歌用的小歌本。每首歌用一种叫ABC的记谱法写成纯文本,Postext画出谱表、音符、吉他和弦和音符下的歌词,其余几段歌词分两栏排在后面。

成品一览

《Songs for the Long Table》是一本A5开本的口袋歌本:茜红色的书名页,一篇写给唱歌的人的说明(附目录),三首歌,以及每首歌出处的注释。每首歌开头是一个茜红色的大号编号、用Fraunces排的歌名和一行小型大写字母排的署名。下面是旋律,用Leland排在5.5 mm的谱表上,吉他和弦标在小节上方,第一段歌词用本书的正文字体Spectral排在音符下。其余几段歌词分两栏排,唱的人从最后一小节直接往下读。《Amazing Grace》的诗行很长,放进一栏会转行,所以它的几段歌词一段接一段居中排列。西班牙语版是一本生日歌本,收三首较短的歌,每页两首。

这道食谱解答

  • 怎样排一本歌集:旋律、音符下的歌词和和弦记号?

简短回答

script.js · 第33–50行在完整代码中
// A song is an ```abc fence in the Markdown: the melody, its chords written "G" before the note
// they fall on, and the first verse on a w: line under it. The look is set here, once.
const scoreStyle = {
  staffSize: mm(5.5), // a songbook rastral; the words under it come out at about 8 pt
  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
  textFont: TEXT, // words and chord names in the text face, not the engraver's Times
  spacing: 1, // the engraver's own: five or six bars of 3/4 to a system on this measure
  barNumbers: 'none', // a table of singers counts verses, not bars
  systemGap: mm(4.5), // room for the chord names between one system's words and the next
  marginTop: pt(4),
  marginBottom: pt(LEAD),
  color: col('ink'),
  minSystemsAtBreak: 2, // never a lone system at a page foot or head
};
// Two habits of Verovio's ABC reader shape the w: lines. Every sung note takes the next
// syllable in order: the `_` and `*` holds are read as nothing, so the second note of a
// slurred pair (B/G/ under "-ing") gets a no-break space, which prints nothing. A tied note
// takes none. `~` joins two syllables on one note: "de~a-trás".

用料

类型
Spectral, Spectral SC, Fraunces(SIL OFL 1.1)
素材
无:所有图片都用代码绘制

做法

#1 · 写下歌曲,乐谱样式只设一次

乐谱样式就是上面的简短回答。在Markdown中,每首歌是一个```abc代码块:头部几行给出拍号、音符时值和调号;双引号里的和弦名("D7"A)印在紧跟其后的那个音符上方;音乐下面的w:行在每个音符下放一个音节,音节之间用连字符分开。有主歌和副歌的曲子写成两行音乐,每行各有自己的w:行。谱表大小、音乐字体、歌词字体、小节编号和谱行间距都来自scoreStyle,所以三首歌外观一致,代码块里只有音乐。

#2 · 每首歌以编号和署名开头

script.js · 第66–81行在完整代码中
// # Amazing Grace {credit="Words John Newton, 1779 · Tune New Britain, American, 1835"}
// The songs run on, three lines apart: a short song shares its page with the next one.
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const text = (id, content, font, size, color, placement, extra = {}) => ({ kind: 'text', id,
  content, fontFamily: font, fontSize: pt(size), color: col(color), align: 'left', placement,
  ...extra });
const songHead = { enabled: true, minHeight: mm(21), slot: { elements: [
  text('number', '{number}', DISPLAY, 26, 'madder', at('container', 'top-left', 0, 1),
    { fontWeight: 600, lineHeight: 1 }),
  text('title', '{titleText}', DISPLAY, 21, 'ink', { ...at('container', 'top-left', 12, 2),
    size: { width: mm(100) } }, { fontWeight: 600, lineHeight: 1.05 }),
  text('credit', '{attr.credit}', LABEL, 8.4, 'madder', { ...at('#title', 'below', 0, 2.2),
    size: { width: mm(100) } }, { fontWeight: 600, letterSpacing: pt(0.6) }),
] } };
const songs = { level: 1, numberingTemplate: '{1}', advancedDesign: songHead,
  marginTop: pt(LEAD * 3), marginBottom: pt(0), breakBefore: { enabled: false } };

歌曲是一个带credit属性的一级标题,它的设计据此排出编号、歌名和署名行。minHeight让每首歌的标题区都高21 mm,所以每一页的谱表都从同样的深度开始。歌曲接排,每个歌名上方空三行;乐谱绝不在第二行谱之前断开(minSystemsAtBreak: 2):英文版的歌一首占一页,西班牙语版的两首短歌合占一页。

#3 · 其余几段歌词分两栏排

script.js · 第54–62行在完整代码中
// :::columns{count=2} holds one :::verse block per stanza; each line of the stanza is a
// line of Markdown, the turned lines indented two spaces (one em). A hymn in long lines
// (Amazing Grace) drops the columns: its stanzas stand one under another, centred.
const verse = { id: 'verse', fontSize: pt(9.4), lineHeight: pt(13), hyphenation: false,
  marginBottom: pt(13 / 2) };
const chorus = { id: 'chorus', fontSize: pt(9.4), lineHeight: pt(13), italic: true,
  hyphenation: false, marginBottom: pt(LEAD / 2) };
const colophon = { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.8), lineHeight: pt(11),
  color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD * 2) };

每段歌词是一个:::verse块,所以诗行在诗人断行的地方断开,第二行和第四行缩进1 em。一个:::columns{count=2 gap=4mm}组把它们以9.4 pt并排。先数一数字符:Burns的诗行放得进55 mm宽的栏,Newton的长格律(long metre)放不进,所以《Amazing Grace》保留一个居中的:::verse块,用空行分隔各段。

#4 · 书眉和页码保持低调

script.js · 第119–131行在完整代码中
const head = (id, content, parity, edge, x, style = {}) => ({
  kind: 'text', id, content, parity, pages: 'all', // a song's first page carries them too
  fontFamily: LABEL, fontSize: pt(8.4), fontWeight: 600, letterSpacing: pt(0.9),
  color: col('muted'), ...style,
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12.5) } },
});
const folio = { fontFamily: TEXT, fontWeight: 600, letterSpacing: pt(0), color: col('madder') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
  head('recto-song', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 8)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };

歌曲排到的每一页都有书眉,左页印书名,右页印歌名,页码用与歌曲编号相同的茜红色。书名页和写给唱歌的人的说明在各自的标题样式中设一个空的header,去掉书眉。

完整食谱

沙盒
// ═══ Postext Cookbook · Nº 164 · Songbook: melody, words under the notes, chords ═══════
// https://postext.dev/en/cookbook/songbook-melody-words-chords
// Code: MIT · Words and tunes: public domain (see the notes in each edition) · Prose: CC BY 4.0
// Fonts: Spectral, Spectral SC, Fraunces (SIL OFL 1.1); Leland (music) · Needs postext ≥ 1.54.0
import {
  buildDocumentWithFonts, renderPageToCanvas, initMusicEngine,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'songbook-melody-words-chords';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#211e1b', // text and notes: a warm near-black
  madder: '#8a3324', // the one accent: song numbers, the title page, credit lines
  tint: '#f1e7d8', // the subtitle on the madder ground (8.2:1)
  rule: '#cbbca6', // hairlines
  muted: '#675e55', // running heads, notes and the colophon (5.9:1 on the paper)
  paper: '#fbf8f1', // a cream stock
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's defaults link to 'main-color': point it at the accent, so nothing prints blue.
  { id: 'main-color', name: 'madder (defaults)', value: { hex: palette.madder, model: 'hex' } },
];
const [TEXT, LABEL, DISPLAY] = ['Spectral', 'Spectral SC', 'Fraunces'];
const LEAD = 14; // pt: the body's leading
const OUTER = 15; // mm: the outer margin, where the running heads and folios align

// #region answer: the score style: a 5 mm staff in Leland, the words in the book's face
// A song is an ```abc fence in the Markdown: the melody, its chords written "G" before the note
// they fall on, and the first verse on a w: line under it. The look is set here, once.
const scoreStyle = {
  staffSize: mm(5.5), // a songbook rastral; the words under it come out at about 8 pt
  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
  textFont: TEXT, // words and chord names in the text face, not the engraver's Times
  spacing: 1, // the engraver's own: five or six bars of 3/4 to a system on this measure
  barNumbers: 'none', // a table of singers counts verses, not bars
  systemGap: mm(4.5), // room for the chord names between one system's words and the next
  marginTop: pt(4),
  marginBottom: pt(LEAD),
  color: col('ink'),
  minSystemsAtBreak: 2, // never a lone system at a page foot or head
};
// Two habits of Verovio's ABC reader shape the w: lines. Every sung note takes the next
// syllable in order: the `_` and `*` holds are read as nothing, so the second note of a

组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗ (在新标签页中打开)

变化

#用Bravura刻谱

Bravura的符头更粗,有铜版刻印乐谱的样子,适合用颜色较深的正文字体排的赞美诗集。

-  musicFont: 'Leland', // lighter noteheads than Bravura, close to Spectral's colour
+  musicFont: 'Bravura',

#给小节编号

合唱团排练时按小节号找位置:在每行谱的开头印一个小节号。

-  barNumbers: 'none', // a table of singers counts verses, not bars
+  barNumbers: 'system',

常见问题

易错点

标题样式会继承其级别的分页设置

headingStyles中的条目没有写出的字段都取自它所属的标题级别,breakBefore也不例外。在:::pagebreak之后以H1设置样式的目录页或版权页会继承parity 'odd',结果落在一张空白页之后。给这类样式设置breakBefore: { enabled: false }。 标题样式 →

  • Verovio的ABC读取器忽略w:行中的_和*这两个占位记号:每个音符都取下一个音节。在圆滑线的第二个音符下,单独写一个不换行空格(U+00A0)作为一个音节,它什么也不印。用延音线连起的音符不取音节。
  • ~把两个音节合到一个音符下(de~a-trás),但刻谱引擎用普通空格把它们印出来,不印连接弧。
  • 和弦名尽量不用♭或♯:大多数正文字体没有这些符号,页面会改用系统字体。选和弦都是本位音的调(G、C、D7、Em),或者在正文中写成“降B”。

致谢

文本
字体
Spectral (SIL OFL 1.1) · Spectral SC (SIL OFL 1.1) · Fraunces (SIL OFL 1.1)
沙盒PDF