# Ein Harmonielehrbuch mit Notenbeispielen in ABC

> Ein Kapitel über Kadenzen, dessen Notenbeispiele ABC-Blöcke im Markdown sind, auf Spaltenbreite gestochen; eines davon läuft über zwei Seiten.

- HTML-Fassung: https://postext.dev/de/cookbook/harmony-textbook-abc-examples
- Rezept Nº 163 · Schulbücher · Stufe 2 (Mittel) · Ausgaben: Canvas, PDF
- Gattungen: Lehrbücher
- Benötigt postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · getestet mit 1.54.0, postext-pdf 1.54.0 am 2026-10-22
- Seiten: [1](https://postext.dev/cookbook/harmony-textbook-abc-examples/en/p01.webp?v=75766f03), [2](https://postext.dev/cookbook/harmony-textbook-abc-examples/en/p02.webp?v=75766f03), [3](https://postext.dev/cookbook/harmony-textbook-abc-examples/en/p03.webp?v=75766f03), [4](https://postext.dev/cookbook/harmony-textbook-abc-examples/en/p04.webp?v=75766f03)
- PDF: https://postext.dev/cookbook/harmony-textbook-abc-examples/en/harmony-textbook-abc-examples.pdf?v=75766f03
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=harmony-textbook-abc-examples&lang=en (.postext: https://postext.dev/cookbook/harmony-textbook-abc-examples/en/harmony-textbook-abc-examples.postext)
- Zuletzt aktualisiert: 2026-10-22
- Andere Sprachen: [en](https://postext.dev/en/cookbook/harmony-textbook-abc-examples.md), [es](https://postext.dev/es/cookbook/harmony-textbook-abc-examples.md), [ca](https://postext.dev/ca/cookbook/harmony-textbook-abc-examples.md), [fr](https://postext.dev/fr/cookbook/harmony-textbook-abc-examples.md), [it](https://postext.dev/it/cookbook/harmony-textbook-abc-examples.md), [pt](https://postext.dev/pt/cookbook/harmony-textbook-abc-examples.md), [zh](https://postext.dev/zh/cookbook/harmony-textbook-abc-examples.md), [ja](https://postext.dev/ja/cookbook/harmony-textbook-abc-examples.md), [ar](https://postext.dev/ar/cookbook/harmony-textbook-abc-examples.md)

## Kurz gesagt

Ein Kapitel eines Musiktheoriebuchs darüber, wie musikalische Phrasen enden. Die Notenbeispiele stehen als kurze Textzeilen im Kapitel, und Postext zeichnet daraus Notenlinien und Noten; das längste geht auf der nächsten Seite weiter.

## Was Sie setzen

Kapitel 5 eines Kurses in Harmonielehre am Klavier, 180 × 250 mm in der Libre Caslon, über die fünf Kadenzen: vollkommener und unvollkommener Ganzschluss, Halbschluss, plagaler Schluss und Trugschluss. Jede Kadenz bekommt zwei kurze Absätze und ein dreistimmiges Notenbeispiel mit seinen Stufenziffern unter der Notenzeile, in ABC direkt im Markdown geschrieben. Das Kapitel endet mit *Greensleeves* in a-Moll, über jedem Takt eine Stufenziffer, und diese Melodie ist zu lang für den Platz, der auf Seite 3 bleibt: Sie setzt dort drei Systeme und die letzten zwei oben auf Seite 4, zwischen Systemen umbrochen, so wie ein Absatz zwischen Zeilen umbricht. Größe der Notenzeile, Notenschrift und Abstände werden einmal in der Konfiguration festgelegt, sodass die fünf Beispiele und die Melodie gleich aussehen.

**Dieses Rezept beantwortet:**

- Wie setze ich Notenbeispiele, in Notenschrift geschrieben, in den Text eines Lehrbuchs?

## Die kurze Antwort

````js
// script.js, Zeilen 36–51
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
  staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
  musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
  textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
  spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
  barNumbers: 'system',
  systemGap: mm(4),
  marginTop: pt(LEAD / 2),
  marginBottom: pt(LEAD / 2),
  color: col('ink'),
  // A long example runs on into the next page between two systems, never leaving one
  // system alone at the foot of a page or at the head of the next.
  minSystemsAtBreak: 2,
};
````

## Zutaten

**Zeigt**

- [Notensatz](https://postext.dev/de/docs/music.md#noten-im-text): Noten als Text in ```abc-, ```musicxml-, ```mei- oder ```humdrum-Blöcken, von Verovio in Spaltenbreite gestochen und in Systeme geteilt, die über Spalten und Seiten weiterlaufen.
- [Notenstil](https://postext.dev/de/docs/configuration-resources.md#notenstil): scoreStyle und benannte scoreStyles: Systemgröße, Notenschrift, Textschrift, Abstand, Taktzahlen, Systemabstand, Ränder, Farbe und wie viele Systeme bei einem Umbruch zusammenbleiben.

**Verwendet außerdem**

- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-headings.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-headers-footers.md#textelemente)
- [Kästen](https://postext.dev/de/docs/configuration-callouts.md#kastenstile)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Witwen, Waisen und kurze Ausgangszeilen](https://postext.dev/de/docs/configuration-page-flow.md#waisen-witwen-zu-kurze-ausgangszeilen-und-regeln-zum-zusammenhalten)
- [PDF-Export](https://postext.dev/de/docs/configuration-pdf-output.md#pdfs-erzeugen)
- [Code-Listings](https://postext.dev/de/docs/configuration-styles.md#code-listings)
- [Im PDF eingebettete Schriften](https://postext.dev/de/docs/configuration-pdf-output.md#warum-ein-font-provider)
- [Hoch- und Tiefstellung](https://postext.dev/de/docs/document-format-inline.md#inline-formatierung)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-callouts.md#kastenstile), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`headings`](https://postext.dev/de/docs/configuration-headings.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-language.md#silbentrennung), [`orderedLists`](https://postext.dev/de/docs/configuration-lists-math.md#nummerierte-listen), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`scoreStyle`](https://postext.dev/de/docs/configuration-resources.md#notenstil), [`tableStyle`](https://postext.dev/de/docs/configuration-resources.md#tabellenstil)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`decompressWoff2`](https://postext.dev/de/docs/configuration-pdf-output.md#font-provider-im-browser-fontsource--woff2), [`initMusicEngine`](https://postext.dev/de/docs/music.md#noten-im-text), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern), [`renderToPdf`](https://postext.dev/de/docs/configuration-pdf-output.md#pdfs-erzeugen)

**Schriften**

- Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)

## Zubereitung

### 1 · Das Beispiel dort schreiben, wo der Text es braucht

Ein mit `abc` markierter Block enthält die Noten, und der Text läuft danach weiter. `L:1/2` macht jeden Akkord ohne Längenangabe zur Halben, `[Bdg]` stapelt drei Noten in einer Stimme, und eine `w:`-Zeile setzt ein Wort unter jeden Akkord, hier die Stufenziffern. Das Attribut `alt` ist das, was ein Screenreader und das getaggte PDF anstelle des Bildes sagen.

````md
```abc {alt="Ganzschluss in G-Dur: I, IV, V, I"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
```
````

### 2 · Jeder Partitur das Aussehen des Buchs geben

[Die kurze Antwort](https://postext.dev/de/cookbook/harmony-textbook-abc-examples.md#die-kurze-antwort) ist die ganze Gestaltung der Noten: eine Notenzeile von 5,5 mm, wie Harmonielehrbücher sie für Beispiele verwenden, die am Klavier gelesen werden, die Bravura neben dem Text in der Caslon, und die Ziffern in der Textschrift statt in der Times des Notenstechers. `spacing: 1.6` verteilt vier Akkorde in Halben über ein Drittel der Satzbreite, sodass sich eine Akkordfolge Akkord für Akkord liest. Dieselbe Einstellung entscheidet, wie viele Takte von *Greensleeves* auf ein System passen: drei, bei 125 mm Satzbreite.

### 3 · Ein langes Beispiel weiterlaufen lassen

`minSystemsAtBreak: 2` hält *Greensleeves* über das Umblättern hinweg lesbar. Die Melodie ergibt fünf Systeme; drei passen an den Fuß von Seite 3, also gehen zwei auf Seite 4, und keine Seite bleibt mit einem einzelnen System zurück. Eine Partitur mit weniger als vier Systemen wandert ganz auf die nächste Seite, wie es die kurzen Kadenzbeispiele täten.

### 4 · Den Notenstecher vor dem Build starten

```js
// script.js, Zeilen 264–270
// The engraver (Verovio) loads only for books with music: start it before the build,
// or every score is laid out as a grey placeholder box.
await initMusicEngine();
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Harmony textbook', es: 'Manual de armonía' }) });
// The PDF draws every system as vector paths, the numerals in the embedded text face.
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);
```

Verovio umfasst etwa 3,7 MB WebAssembly, deshalb lädt die Engine es nur, wenn Sie danach fragen. Ohne `initMusicEngine()` läuft das Layout trotzdem, und jede Partitur ist ein grauer Kasten in der richtigen Größe. `renderToPdf` braucht nichts weiter: Das PDF zeichnet jedes System als Vektorpfade und setzt die Ziffern in der eingebetteten Caslon.

### 5 · Das Kapitel mit einem abgesenkten Titel eröffnen

```js
// script.js, Zeilen 55–72
const opener = {
  enabled: true,
  minHeight: mm(48),
  slot: { elements: [
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(8.5),
      fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'),
      align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(12) } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(34), lineHeight: 1.05, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
        size: { width: mm(110), height: 'auto' } } },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
      color: col('accent'),
      placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(16) } } },
  ] },
};
```

Der Auftakt nimmt seine Dachzeile aus dem Attribut `kicker` der Überschrift und reserviert 48 mm, genug für den Titel und seine Linie, während das erste Beispiel noch auf Seite 1 steht.

## Das ganze Rezept

Eine Datei, aus dem Ordner des Rezepts zusammengesetzt, mit dem Beispieltext und dem gemeinsamen Baukasten der Rezepte bereits eingebettet; sie baut ihre eigene Seite auf. Zum Ausführen setzen Sie sie in ein `<script type="module">` auf einer leeren Seite oder fügen sie in das JS-Feld eines neuen CodePen-Pens ein (als Modul). Sie importiert postext von esm.sh, es gibt also nichts zu installieren oder zu bauen.

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/harmony-textbook-abc-examples

### script.js

````js
// ═══ Postext Cookbook · Nº 163 · Harmony textbook with ABC examples ═════════════
// https://postext.dev/en/cookbook/harmony-textbook-abc-examples
// Code: MIT · Text: original (CC BY 4.0) · Music: Greensleeves (traditional, PD)
// Fonts: Libre Caslon Text & Display, Libre Franklin (SIL OFL 1.1) · 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 = 'harmony-textbook-abc-examples';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a conservatory green for the accents, warm ink for text and notes
const palette = {
  ink: '#1f1c19', // text and notation: a warm near-black
  accent: '#2f5d4f', // kickers, rules, Roman numerals in the table
  tint: '#edf2ef', // the box of key terms
  rule: '#c3cfc9', // hairlines
  muted: '#5c6863', // running heads, colophon
  paper: '#ffffff',
};
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, never the default blue.
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const TRIM = { width: 180, height: 250 };
const MARGIN = { top: 22, bottom: 24, inner: 22, outer: 33 }; // a 125 mm measure
const LEAD = 14.5; // body leading in pt: the baseline grid
const LABEL = 'Libre Franklin';

// #region answer: one score style for every example in the text
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
  staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
  musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
  textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
  spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
  barNumbers: 'system',
  systemGap: mm(4),
  marginTop: pt(LEAD / 2),
  marginBottom: pt(LEAD / 2),
  color: col('ink'),
  // A long example runs on into the next page between two systems, never leaving one
  // system alone at the foot of a page or at the head of the next.
  minSystemsAtBreak: 2,
};
// #endregion

// #region opener: kicker, Caslon Display title and a short green rule, sunk 48 mm
const opener = {
  enabled: true,
  minHeight: mm(48),
  slot: { elements: [
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(8.5),
      fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'),
      align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(12) } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(34), lineHeight: 1.05, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
        size: { width: mm(110), height: 'auto' } } },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
      color: col('accent'),
      placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(16) } } },
  ] },
};
// #endregion

// #region running-heads: book title on the verso, chapter on the recto, folios outside
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5),
  fontWeight: 600, letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('accent') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, folio),
  head('verso-title', '{title}', 'even', 'top-left', MARGIN.outer + 9),
  head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(MARGIN.outer + 9)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, folio),
] };
const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'bottom', 0, {
  ...folio, pages: 'opener', align: 'center',
  placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { x: mm(0), y: mm(-12) } },
})] };
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }), // the language the text is hyphenated in
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true }, // left = inner
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: 'Libre Caslon Text', fontSize: pt(10.5), lineHeight: pt(LEAD),
    color: col('ink'), boldColor: col('ink'), italicColor: col('ink'),
    referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4.5),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: 'Libre Caslon Text', color: col('ink'), fontWeight: 700,
    levels: [
      { level: 1, fontSize: pt(34), breakBefore: { parity: 'odd' }, advancedDesign: opener },
      { level: 2, fontFamily: LABEL, fontSize: pt(9), lineHeight: pt(LEAD), fontWeight: 700,
        letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('accent'),
        marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) },
    ],
  },
  orderedLists: { numberFormat: 'arabic', color: col('accent'), fontWeight: 700,
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(7), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  calloutStyles: [
    { id: 'terms', title: t({ en: 'Key terms', es: 'Términos clave' }), background: col('tint'),
      stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') },
      padding: { top: mm(3), right: mm(4), bottom: mm(3), left: mm(4) },
      titleStyle: { fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 700,
        textTransform: 'uppercase', letterSpacing: pt(1.2), color: col('accent') },
      body: { fontSize: pt(9.5), lineHeight: pt(13), color: col('ink'), firstLineIndent: pt(0),
        boldColor: col('accent'), italicColor: col('ink'), textAlign: 'left' } },
  ],
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('accent'), headerColor: col('paper'), headerFontFamily: LABEL,
    headerFontSize: pt(7.5), bodyFontFamily: 'Libre Caslon Text', bodyFontSize: pt(9),
    bodyColor: col('ink'), cellPadding: mm(1.4) },
  scoreStyle,
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = `---
title: "Harmony at the Keyboard"
---

# Cadences {kicker="Chapter 5"}

A phrase of music ends the way a sentence does: with a pause that tells the listener how much has been said. Harmony marks that pause with a cadence, a short chord progression at the end of a phrase. Some cadences close the thought, others leave it open and ask for the next phrase. Hearing the difference is the first step towards writing phrases that end where you mean them to end.

This chapter works in G major, in three upper voices in close position. The bass is not written: it takes the root of each chord, as the Roman numeral under the staff says, upper case for a major chord and lower case for a minor one. Play every example at the keyboard, the right hand on the staff and the left hand on the bass note, and sing the top voice before you read on.

:::callout{style="terms"}
**Cadence:** the last two chords of a phrase. **Soprano:** the top voice. **Leading tone:** the seventh degree of the scale, a semitone below the tonic, which wants to rise to it.
:::

## The perfect authentic cadence

The strongest close is dominant to tonic, V–I, with both chords in root position and the tonic in the soprano at the end. The leading tone (F-sharp in G major) rises a semitone to the tonic.

\`\`\`abc {alt="Perfect authentic cadence in G major: I, IV, V, I, the soprano ending on G"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
\`\`\`

The approach to the dominant matters as much as the cadence. Here IV prepares V: the upper voices hold G, then step down, and nothing jumps. A phrase that reaches V by leap sounds hurried, as if it had skipped a word.

## The imperfect authentic cadence

Keep the same chords and end with the third of the tonic in the soprano, and the close softens. The progression is still V–I, but the melody stops on B instead of G and the ear expects more. Composers use this cadence in the middle of a period and save the perfect one for its end.

\`\`\`abc {alt="Imperfect authentic cadence in G major: V to I with B in the soprano"}
X:2
M:4/4
L:1/2
K:G
[dgb] [egc'] | [dfa] [dgb] |]
w: I IV V I
\`\`\`

## The half cadence

A phrase may stop on the dominant itself. The half cadence ends on V, reached from I, ii or IV, and it sounds like a comma: the music has said something and owes us the rest. Most folk songs and hymns put a half cadence at the end of their first line and an authentic cadence at the end of the second.

\`\`\`abc {alt="Half cadence in G major: I, IV, V, held"}
X:3
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [A2d2f2] |]
w: I IV V
\`\`\`

## The plagal cadence

IV–I closes without the leading tone. It is the cadence of the *Amen* sung after a hymn, and it usually follows an authentic cadence that has already ended the phrase: the plagal one adds a coda, not a conclusion.

\`\`\`abc {alt="Plagal cadence in G major: I, IV, I"}
X:4
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [B2d2g2] |]
w: I IV I
\`\`\`

## The deceptive cadence

When V moves to vi instead of I, the listener gets a chord that shares two notes with the tonic but is not the tonic. The leading tone still rises to G, so the voice leading keeps its promise while the harmony breaks it. The other two upper voices step down, and the chord doubles its third, G, which keeps the bass and the inner voices out of parallel octaves. The phrase has to go on, and it usually ends with an authentic cadence a few bars later.

\`\`\`abc {alt="Deceptive cadence in G major: V to VI, the leading tone rising to G"}
X:5
M:4/4
L:1/2
K:G
[ceg] [Adf] | [G2B2g2] |]
w: IV V vi
\`\`\`

## Cadences in a melody

A melody shows its cadences even without chords. *Greensleeves*, an English tune from the sixteenth century, is written here in A minor with a Roman numeral over each bar, the chords a guitarist would play. Its first phrase stops on E, the dominant, over a half cadence; the second ends on A over V–i, the leading tone G-sharp rising to the tonic. The same plan returns in the refrain, which starts on a high G over C, the relative major, and comes back to A minor for the final close. Sing it, mark each cadence and say which kind it is before you check the numerals over the staff.

\`\`\`abc {alt="Greensleeves in A minor with a Roman numeral over each bar"}
X:6
M:6/8
L:1/8
K:Am
A | "i"c2 d e>fe | "VII"d2 B G>AB | "i"c2 A A>^GA | "V"B2 ^G E2 A |
"i"c2 d e>fe | "VII"d2 B G>AB | "i"c>BA "V"^G>^FG | "i"A3 A2 z |
"III"g3 g>fe | "VII"d2 B G>AB | "i"c2 A A>^GA | "V"B2 ^G E3 |
"III"g3 g>fe | "VII"d2 B G>AB | "i"c>BA "V"^G>^FG | "i"A3 A2 |]
\`\`\`

The four phrases follow the plan of most folk songs: open, closed, open, closed. Notice where the leading tone appears. In the half cadences G-sharp sits inside the dominant chord and goes nowhere; in the authentic cadences it is the note before the last, and it resolves.

## Exercises

1. Write each cadence of this chapter in D major and in F major, in three voices, and play it with the bass in the left hand.
2. Turn the imperfect authentic cadence into a perfect one by changing a single note of the soprano.
3. Find the cadences of a hymn you know. For each phrase, write the last two Roman numerals and name the cadence.
4. Harmonize the first phrase of *Greensleeves* again, ending it with an imperfect authentic cadence instead of a half cadence. What changes in the second phrase?


:::paragraphs{style="colophon"}
Set in Libre Caslon Text, Libre Caslon Display and Libre Franklin (SIL OFL) · Music engraved by Verovio in Bravura · Text: CC BY 4.0 · *Greensleeves*: traditional
:::
`; // content.<lang>.md, inlined by the Cookbook

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Libre Caslon Text': ['400', '400i', '700'],
  'Libre Caslon Display': ['400'],
  'Libre Franklin': ['600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// #region engine: start the engraver, build, and draw the scores into the PDF too
// The engraver (Verovio) loads only for books with music: start it before the build,
// or every score is laid out as a grey placeholder box.
await initMusicEngine();
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Harmony textbook', es: 'Manual de armonía' }) });
// The PDF draws every system as vector paths, the numerals in the embedded text face.
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);
// #endregion

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

/** A loaded FontFace covers this family, weight and style (fonts.check() would
 *  also say yes for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook
/** The pages as spreads on a dark desk, page 1 alone, then verso | recto,
 *  each painted when it scrolls near. Other options go to renderPageToCanvas. */
function showPages(docs, { title, width = 460, ...render } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── Kit · pdf v3 ── the same in every recipe that exports a PDF
/** The Fontsource files the screen used, as TrueType, with their range and
 *  place: the nearest weight the family ships, upright with no italic;
 *  latin, then what its letters need (kitSubsetsFor). */
async function fontsourceProvider(family, weight, style, request) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const text = String.fromCodePoint(...(request?.codePoints ?? []));
  const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => {
    const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`);
    if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`);
    return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())),
      { unicodeRange: kitRanges()[subset], order });
  }));
  return files.length === 1 ? files[0] : files;
}

/** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame
 *  shows no PDFs) and a download link. */
function offerPdf(makePdf, filename) {
  viewer();
  const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' });
  button.dataset.postextPdf = filename;
  button.addEventListener('click', async () => {
    button.disabled = true;
    button.textContent = 'Building the PDF…';
    try {
      const bytes = await makePdf();
      const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
      const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`;
      button.replaceWith(
        Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }),
        Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` }));
    } catch (error) {
      button.disabled = false;
      button.textContent = 'Build the PDF';
      kitFail(error);
    }
  });
  document.getElementById('pt-actions').append(button);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
````

## Varianten

### Leland für eine leichtere Seite nehmen

Leland, die Notenschrift von MuseScore, hat dünnere Hälse und kleinere Notenköpfe als Bravura; sie passt zu einem Buch, das in einer leichten Textschrift gesetzt ist.

```diff
-  musicFont: 'Bravura',
+  musicFont: 'Leland',
```

### Eine zweite Stimme in einem anderen Format schreiben

Der ABC-Leser nimmt nur eine Stimme. Für Sopran und Bass auf getrennten Notenzeilen schreiben Sie das Beispiel in Humdrum oder MusicXML; [die Konservatoriumsschule](https://postext.dev/de/cookbook/conservatory-method-chorales.md) setzt vierstimmige Choräle auf diese Weise.

## Häufige Fehler

- Schreiben Sie die Länge eines Akkords in die Klammern, `[B2d2g2]`, oder stellen Sie `L:` auf den Wert des Akkords: Eine Länge nach der Klammer, `[Bdg]2`, wird als Standardlänge gelesen.
- Verovios ABC-Leser gibt jede Silbe einer `w:`-Zeile der nächsten Note und ignoriert `*` und `_`, sodass eine `w:`-Zeile keine Noten überspringen kann. Um einige Takte zu kennzeichnen, schreiben Sie die Zeichen als Akkordsymbole über die Notenzeile, wie es das Beispiel mit *Greensleeves* tut.
- Eine Textschrift hat selten die Zeichen ♯ und ♭: Schreiben Sie „Fis“ in den Fließtext, sonst setzt der Browser das Zeichen aus einer Systemschrift.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: The chapter on cadences and its examples, written for this recipe: Ignacio Ferro, CC-BY-4.0
- Text: Greensleeves, English tune of the sixteenth century, in its common modern form: Traditional ([Quelle](https://en.wikipedia.org/wiki/Greensleeves)), gemeinfrei
- Schrift: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 008 · Eine farbcodierte Familie von Lehrbuchkästen](https://postext.dev/de/cookbook/textbook-box-family.md): Sechs Lehrbuchkästen in drei Farben, unterschieden durch Streifen mit Symbol, Abzeichen, nummeriertes Etikett, Piktogrammleiste oder Marke neben der Kontur. · Stufe 2 (Mittel) · Lehrbücher
- [Nº 001 · Lehrbuch mit Randspalte](https://postext.dev/de/cookbook/textbook-margin-column.md): Anderthalb Spalten, die äußere nur für Gleitobjekte: Abbildungen mit span 'side' und Erläuterungen stapeln sich dort, captionSide holt die Legenden hinein. · Stufe 3 (Fortgeschritten) · Lehrbücher
- [Nº 124 · Ein Schullesebuch mit Furigana über jedem Kanji](https://postext.dev/de/cookbook/jukugo-furigana-textbook.md): Niimi Nankichis „Gon, der Fuchs“ auf einer Lesebuchseite: Furigana pro Zeichen, pro Wort und als Jukugo, im Verhältnis 1:2:1, Überhang auf Kana, nie auf Kanji. · Stufe 2 (Mittel) · Lehrbücher
