# Un manuel d’harmonie aux exemples musicaux en ABC

> Un chapitre sur les cadences dont les exemples sont des blocs ABC dans le Markdown, gravés à la largeur de la colonne ; l’un d’eux continue sur deux pages.

- Version HTML: https://postext.dev/fr/cookbook/harmony-textbook-abc-examples
- Recette Nº 163 · Manuels scolaires · Niveau 2 (Intermédiaire) · Sorties: Canvas, PDF
- Genres: Manuels scolaires
- Nécessite postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · testée avec 1.54.0, postext-pdf 1.54.0 le 2026-10-22
- Pages: [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
- Ouvrir dans le Sandbox: https://postext.dev/fr/sandbox#recipe=harmony-textbook-abc-examples&lang=en (.postext: https://postext.dev/cookbook/harmony-textbook-abc-examples/en/harmony-textbook-abc-examples.postext)
- Dernière mise à jour: 2026-10-22
- Autres langues: [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), [de](https://postext.dev/de/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)

## En bref

Un chapitre d’un manuel de théorie musicale sur la fin des phrases. Les exemples s’écrivent en courtes lignes de texte dans le chapitre, et Postext les dessine en portées et en notes ; le plus long continue sur la page suivante.

## Ce que vous allez composer

Le chapitre 5 d’un cours d’harmonie au clavier, en 180 × 250 mm et en Libre Caslon, consacré aux cinq cadences : parfaite, imparfaite, demi-cadence, plagale et rompue. Chaque cadence reçoit deux courts paragraphes et un exemple à trois voix, avec ses chiffres romains sous la portée, écrit en ABC directement dans le Markdown. Le chapitre se termine par *Greensleeves* en la mineur, un chiffre au-dessus de chaque mesure, et cette mélodie est trop longue pour la place qui reste en page 3 : elle y pose trois systèmes et met les deux derniers en tête de la page 4, coupée entre deux systèmes comme un paragraphe se coupe entre deux lignes. La taille de la portée, la police musicale et l’espacement sont réglés une seule fois dans la configuration : les cinq exemples et la mélodie partagent ainsi le même aspect.

**Cette recette répond à:**

- Comment insérer dans le texte d’un manuel des exemples musicaux écrits en notation ?

## La réponse courte

````js
// script.js, lignes 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,
};
````

## Ingrédients

**Enseigne**

- [Notation musicale](https://postext.dev/fr/docs/music.md#une-partition-dans-le-texte): Des partitions écrites en texte dans des blocs ```abc, ```musicxml, ```mei ou ```humdrum, gravées par Verovio à la largeur de la colonne et coupées en systèmes qui se poursuivent sur la colonne ou la page suivante.
- [Style de partition](https://postext.dev/fr/docs/configuration-resources.md#style-de-partition): scoreStyle et les styles nommés de scoreStyles : taille de la portée, police musicale, police du texte, espacement, numéros de mesure, écart entre systèmes, marges, encre et nombre de systèmes gardés ensemble à une coupure.

**Utilise aussi**

- [Ouvertures composées](https://postext.dev/fr/docs/configuration-headings.md#étendue-et-maquette-avancée)
- [Attributs de titre](https://postext.dev/fr/docs/document-format.md#attributs-de-titre)
- [Titres courants et folios](https://postext.dev/fr/docs/configuration-headers-footers.md#en-têtes-et-pieds-de-page)
- [Marges en miroir](https://postext.dev/fr/docs/configuration-page-layout.md#marges-en-miroir)
- [Titres courants selon le rôle de la page](https://postext.dev/fr/docs/configuration-headers-footers.md#éléments-de-texte)
- [Encadrés](https://postext.dev/fr/docs/configuration-callouts.md#styles-dencadré)
- [Styles de paragraphe](https://postext.dev/fr/docs/configuration-styles.md#styles-de-paragraphe)
- [Palette de couleurs sémantique](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#palette-de-couleurs)
- [Veuves, orphelines et fins de paragraphe trop courtes](https://postext.dev/fr/docs/configuration-page-flow.md#orphelines-veuves-lignes-creuses-et-règles-de-maintien)
- [Export PDF](https://postext.dev/fr/docs/configuration-pdf-output.md#générer-des-pdf)
- [Listings de code](https://postext.dev/fr/docs/configuration-styles.md#listings-de-code)
- [Polices intégrées au PDF](https://postext.dev/fr/docs/configuration-pdf-output.md#pourquoi-un-fournisseur-de-polices)
- [Exposants et indices](https://postext.dev/fr/docs/document-format-inline.md#mise-en-forme-en-ligne)

**La configuration en un coup d’œil**

- [`bodyText`](https://postext.dev/fr/docs/configuration-text.md#texte-courant), [`calloutStyles`](https://postext.dev/fr/docs/configuration-callouts.md#styles-dencadré), [`colorPalette`](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#palette-de-couleurs), [`footer`](https://postext.dev/fr/docs/configuration-headers-footers.md#en-têtes-et-pieds-de-page), [`header`](https://postext.dev/fr/docs/configuration-headers-footers.md#en-têtes-et-pieds-de-page), [`headings`](https://postext.dev/fr/docs/configuration-headings.md#titres), [`layout`](https://postext.dev/fr/docs/configuration-page-layout.md#mise-en-page), [`locale`](https://postext.dev/fr/docs/configuration-language.md#césure), [`orderedLists`](https://postext.dev/fr/docs/configuration-lists-math.md#listes-numérotées), [`page`](https://postext.dev/fr/docs/configuration-page-layout.md#page), [`paragraphStyles`](https://postext.dev/fr/docs/configuration-styles.md#styles-de-paragraphe), [`scoreStyle`](https://postext.dev/fr/docs/configuration-resources.md#style-de-partition), [`tableStyle`](https://postext.dev/fr/docs/configuration-resources.md#style-de-tableau)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page), [`decompressWoff2`](https://postext.dev/fr/docs/configuration-pdf-output.md#fournisseur-de-polices-dans-le-navigateur-fontsource--woff2), [`initMusicEngine`](https://postext.dev/fr/docs/music.md#une-partition-dans-le-texte), [`renderPageToCanvas`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#rendre-une-page-en-bitmap), [`renderToPdf`](https://postext.dev/fr/docs/configuration-pdf-output.md#générer-des-pdf)

**Caractères**

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

## Préparation

### 1 · Écrire l’exemple là où le texte en a besoin

Un bloc marqué `abc` contient la notation, et le texte reprend après lui. `L:1/2` fait de chaque accord sans durée une blanche, `[Bdg]` empile trois notes dans une seule voix, et une ligne `w:` place un mot sous chaque accord, ici les chiffres romains. L’attribut `alt` est ce que disent un lecteur d’écran et le PDF balisé à la place de l’image.

````md
```abc {alt="Cadence parfaite en sol majeur : I, IV, V, I"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
```
````

### 2 · Donner à chaque partition l’allure du livre

[La réponse courte](https://postext.dev/fr/cookbook/harmony-textbook-abc-examples.md#la-réponse-courte) contient toute la maquette de la musique : une portée de 5,5 mm, comme dans les manuels d’harmonie pour des exemples lus au clavier, Bravura à côté du texte en Caslon, et les chiffres dans le caractère du texte au lieu du Times du graveur. `spacing: 1.6` étale quatre accords en blanches sur un tiers de la justification, pour que la progression se lise accord par accord. Le même réglage décide combien de mesures de *Greensleeves* tiennent sur un système : trois, dans une justification de 125 mm.

### 3 · Laisser un long exemple continuer

`minSystemsAtBreak: 2` garde *Greensleeves* lisible au changement de page. La mélodie sort en cinq systèmes ; trois tiennent au pied de la page 3, deux passent donc à la page 4, et aucune page ne reste avec un seul système. Une partition de moins de quatre systèmes passe entière à la page suivante, comme le feraient les courts exemples de cadences.

### 4 · Démarrer le graveur avant la composition

```js
// script.js, lignes 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 pèse environ 3,7 Mo de WebAssembly : le moteur ne le charge donc que si vous le demandez. Sans `initMusicEngine()`, la mise en page se fait quand même, et chaque partition est un rectangle gris de la bonne taille. `renderToPdf` n’a besoin de rien d’autre : le PDF dessine chaque système en tracés vectoriels et compose les chiffres dans la Caslon incorporée.

### 5 · Ouvrir le chapitre sur un titre abaissé

```js
// script.js, lignes 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) } } },
  ] },
};
```

L’ouverture tire son surtitre de l’attribut `kicker` du titre et réserve 48 mm, assez pour le titre et son filet, avec le premier exemple encore en page 1.

## La recette complète

Un seul fichier, composé à partir du dossier de la recette avec le texte d’exemple et le kit commun des Recettes déjà inclus ; il construit sa propre page. Pour l’exécuter, placez-le dans un `<script type="module">` d’une page vide ou collez-le dans le panneau JS d’un nouveau pen CodePen (en tant que module). Il importe postext depuis esm.sh : il n’y a rien à installer ni à compiler.

- Dossier de la recette: 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 ───────────────────────────────────────────────────────────────────────
````

## Variantes

### Prendre Leland pour une page plus légère

Leland, la police de MuseScore, a des hampes plus fines et des têtes de notes plus petites que Bravura ; elle convient à un livre composé dans un caractère de texte léger.

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

### Écrire une deuxième voix dans un autre format

Le lecteur ABC ne prend qu’une voix. Pour un soprano et une basse sur des portées séparées, écrivez l’exemple en Humdrum ou en MusicXML ; [la méthode de conservatoire](https://postext.dev/fr/cookbook/conservatory-method-chorales.md) compose ainsi des chorals à quatre voix.

## Erreurs fréquentes

- Écrivez la durée d’un accord à l’intérieur des crochets, `[B2d2g2]`, ou réglez `L:` sur la valeur de l’accord : une durée placée après le crochet, `[Bdg]2`, est lue comme la durée par défaut.
- Le lecteur ABC de Verovio donne chaque syllabe d’une ligne `w:` à la note suivante et ignore `*` et `_` : une ligne `w:` ne peut donc pas sauter de notes. Pour marquer quelques mesures, écrivez les marques comme des chiffrages d’accords au-dessus de la portée, comme le fait l’exemple de *Greensleeves*.
- Un caractère de texte a rarement les signes ♯ et ♭ : écrivez « fa dièse » dans le texte, sinon le navigateur prend le signe dans une police du système.

## Crédits

- Recette: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Texte: The chapter on cadences and its examples, written for this recipe: Ignacio Ferro, CC-BY-4.0
- Texte: Greensleeves, English tune of the sixteenth century, in its common modern form: Traditional ([source](https://en.wikipedia.org/wiki/Greensleeves)), domaine public
- Caractères: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Code: MIT · Contenu d’exemple: CC-BY-4.0

## Recettes liées

- [Nº 008 · Une famille d’encadrés de manuel codée par couleur](https://postext.dev/fr/cookbook/textbook-box-family.md): Six sortes d’encadrés de manuel en trois couleurs, distinguées par une bande à icône, un badge, un onglet numéroté, des pictogrammes ou un repère hors bordure. · Niveau 2 (Intermédiaire) · Manuels scolaires
- [Nº 001 · Manuel avec une colonne marginale](https://postext.dev/fr/cookbook/textbook-margin-column.md): Une page à colonne et demie dont la colonne extérieure ne prend que des flottants : figures span 'side' et gloses s’y empilent, captionSide y met les légendes. · Niveau 3 (Avancé) · Manuels scolaires
- [Nº 124 · Un livre de lecture scolaire, furigana sur chaque kanji](https://postext.dev/fr/cookbook/jukugo-furigana-textbook.md): « Gon le renard » de Niimi Nankichi en manuel : furigana par caractère, par mot et jukugo, espacés 1:2:1, qui débordent sur les kana mais jamais sur un kanji. · Niveau 2 (Intermédiaire) · Manuels scolaires
