# Un manual d'harmonia amb exemples musicals en ABC

> Un capítol sobre les cadències amb exemples en blocs ABC dins del Markdown, gravats a l'amplada de la columna; un d'ells continua a la pàgina següent.

- Versió HTML: https://postext.dev/ca/cookbook/harmony-textbook-abc-examples
- Recepta Núm. 163 · Llibres de text · Nivell 2 (Intermedi) · Sortides: Canvas, PDF
- Gèneres: Llibres de text
- Requereix postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · provada amb 1.54.0, postext-pdf 1.54.0 el 2026-10-22
- Pàgines: [1](https://postext.dev/cookbook/harmony-textbook-abc-examples/es/p01.webp?v=5ed22fda), [2](https://postext.dev/cookbook/harmony-textbook-abc-examples/es/p02.webp?v=5ed22fda), [3](https://postext.dev/cookbook/harmony-textbook-abc-examples/es/p03.webp?v=5ed22fda), [4](https://postext.dev/cookbook/harmony-textbook-abc-examples/es/p04.webp?v=5ed22fda)
- PDF: https://postext.dev/cookbook/harmony-textbook-abc-examples/es/harmony-textbook-abc-examples.pdf?v=5ed22fda
- Obre al Sandbox: https://postext.dev/ca/sandbox#recipe=harmony-textbook-abc-examples&lang=es (.postext: https://postext.dev/cookbook/harmony-textbook-abc-examples/es/harmony-textbook-abc-examples.postext)
- Última actualització: 2026-10-22
- Altres idiomes: [en](https://postext.dev/en/cookbook/harmony-textbook-abc-examples.md), [es](https://postext.dev/es/cookbook/harmony-textbook-abc-examples.md), [de](https://postext.dev/de/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)

## En poques paraules

Un capítol d'un manual de teoria musical sobre com acaben les frases. Els exemples s'escriuen com a text dins del capítol i Postext els dibuixa com a pentagrames; el més llarg continua a la pàgina següent.

## Què compondràs

El capítol 5 d'un curs d'harmonia al teclat, de 180 × 250 mm en Libre Caslon, sobre les cinc cadències: perfecta, imperfecta, semicadència, plagal i trencada. Cada cadència porta dos paràgrafs breus i un exemple a tres veus amb les seves xifres romanes sota el pentagrama, escrit en ABC dins del mateix Markdown. El capítol acaba amb *Greensleeves* en la menor, amb una xifra romana sobre cada compàs, i aquesta melodia no cap en el que queda de la pàgina 3: hi posa tres sistemes i els dos últims al capdamunt de la pàgina 4, partida entre sistemes igual que un paràgraf es parteix entre línies. La mida del pentagrama, la font musical i l'espaiat es fixen una sola vegada a la configuració, de manera que els cinc exemples i la melodia comparteixen el mateix aspecte.

**Aquesta recepta respon a:**

- Com poso exemples musicals en el text d'un llibre de text, escrits com a notació?

## La resposta curta

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

## Ingredients

**Ensenya**

- [Notació musical](https://postext.dev/ca/docs/music.md#una-partitura-dins-del-text): Partitures escrites com a text en blocs ```abc, ```musicxml, ```mei o ```humdrum, gravades per Verovio a l'amplada de la columna i tallades en sistemes que continuen a la columna o la pàgina següent.
- [Estil de partitura](https://postext.dev/ca/docs/configuration-resources.md#estil-de-partitura): scoreStyle i els estils amb nom de scoreStyles: mida del pentagrama, font musical, font del text, espaiat, números de compàs, separació entre sistemes, marges, tinta i quants sistemes es mantenen junts en un tall.

**També fa servir**

- [Obertures dissenyades](https://postext.dev/ca/docs/configuration-headings.md#extensió-i-disseny-avançat)
- [Atributs de títol](https://postext.dev/ca/docs/document-format.md#atributs-dencapçalament)
- [Capçaleres i folis](https://postext.dev/ca/docs/configuration-headers-footers.md#capçaleres-i-peus)
- [Marges simètrics](https://postext.dev/ca/docs/configuration-page-layout.md#marges-simètrics)
- [Capçaleres segons el tipus de pàgina](https://postext.dev/ca/docs/configuration-headers-footers.md#elements-de-text)
- [Requadres](https://postext.dev/ca/docs/configuration-callouts.md#estils-de-requadre)
- [Estils de paràgraf](https://postext.dev/ca/docs/configuration-styles.md#estils-de-paràgraf)
- [Paleta de color semàntica](https://postext.dev/ca/docs/configuration-fonts-colors-viewers.md#paleta-de-colors)
- [Vídues, òrfenes i línies curtes](https://postext.dev/ca/docs/configuration-page-flow.md#òrfenes-vídues-línies-curtes-i-regles-de-cohesió)
- [Exportació a PDF](https://postext.dev/ca/docs/configuration-pdf-output.md#generació-de-pdf)
- [Llistats de codi](https://postext.dev/ca/docs/configuration-styles.md#llistats-de-codi)
- [Fonts incrustades al PDF](https://postext.dev/ca/docs/configuration-pdf-output.md#per-què-un-proveïdor-de-fonts)
- [Superíndexs i subíndexs](https://postext.dev/ca/docs/document-format-inline.md#format-en-línia)

**La configuració d'un cop d'ull**

- [`bodyText`](https://postext.dev/ca/docs/configuration-text.md#text-de-cos), [`calloutStyles`](https://postext.dev/ca/docs/configuration-callouts.md#estils-de-requadre), [`colorPalette`](https://postext.dev/ca/docs/configuration-fonts-colors-viewers.md#paleta-de-colors), [`footer`](https://postext.dev/ca/docs/configuration-headers-footers.md#capçaleres-i-peus), [`header`](https://postext.dev/ca/docs/configuration-headers-footers.md#capçaleres-i-peus), [`headings`](https://postext.dev/ca/docs/configuration-headings.md#encapçalaments), [`layout`](https://postext.dev/ca/docs/configuration-page-layout.md#disposició), [`locale`](https://postext.dev/ca/docs/configuration-language.md#partició-de-mots), [`orderedLists`](https://postext.dev/ca/docs/configuration-lists-math.md#llistes-numerades), [`page`](https://postext.dev/ca/docs/configuration-page-layout.md#pàgina), [`paragraphStyles`](https://postext.dev/ca/docs/configuration-styles.md#estils-de-paràgraf), [`scoreStyle`](https://postext.dev/ca/docs/configuration-resources.md#estil-de-partitura), [`tableStyle`](https://postext.dev/ca/docs/configuration-resources.md#estil-de-taules)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/ca/docs/configuration-programmatic-usage.md#carregar-les-fonts-abans-de-compondre), [`decompressWoff2`](https://postext.dev/ca/docs/configuration-pdf-output.md#proveïdor-de-fonts-al-navegador-fontsource--woff2), [`initMusicEngine`](https://postext.dev/ca/docs/music.md#una-partitura-dins-del-text), [`renderPageToCanvas`](https://postext.dev/ca/docs/configuration-programmatic-usage.md#renderitzar-una-pàgina-en-un-mapa-de-bits), [`renderToPdf`](https://postext.dev/ca/docs/configuration-pdf-output.md#generació-de-pdf)

**Tipus de lletra**

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

## Elaboració

### 1 · Escriu l'exemple on el demana el text

Un bloc marcat `abc` conté la notació, i el text continua després. `L:1/2` fa de cada acord sense durada una blanca, `[Bdg]` apila tres notes en una sola veu i una línia `w:` posa una paraula sota cada acord, aquí les xifres romanes. L'atribut `alt` és el que llegeixen un lector de pantalla i el PDF etiquetat en lloc de la imatge.

````md
```abc {alt="Cadència perfecta en sol major: I, IV, V, I"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
```
````

### 2 · Dona a cada partitura l'aspecte del llibre

La [resposta curta](https://postext.dev/ca/cookbook/harmony-textbook-abc-examples.md#la-resposta-curta) és tot el disseny de la música: un pentagrama de 5,5 mm, el que fan servir els manuals d'harmonia per a exemples que es llegeixen assegut al piano, Bravura al costat del text en Caslon i les xifres en la lletra del text en lloc de la Times del gravador. `spacing: 1.6` reparteix quatre acords de blanca per un terç de la caixa, de manera que la progressió es llegeix acord per acord. El mateix ajust decideix quants compassos de *Greensleeves* caben en un sistema: tres, en una caixa de 125 mm.

### 3 · Deixa que un exemple llarg continuï

`minSystemsAtBreak: 2` és el que manté llegible *Greensleeves* en passar la pàgina. La melodia surt en cinc sistemes; tres caben al peu de la pàgina 3, així que dos passen a la 4, i cap pàgina no es queda amb un sistema solt. Una partitura de menys de quatre sistemes passa sencera a la pàgina següent, com farien els exemples breus de cadències.

### 4 · Engega el gravador abans de compondre

```js
// script.js, línies 263–269
// 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 ocupa uns 3,7 MB de WebAssembly, així que el motor només el carrega quan l'hi demanes. Sense `initMusicEngine()` la composició es fa igualment i cada partitura és un recuadre gris de la mida correcta. `renderToPdf` no necessita res més: el PDF dibuixa cada sistema com a traçats vectorials i posa les xifres en la Caslon incrustada.

### 5 · Obre el capítol amb el títol enfonsat

```js
// script.js, línies 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'obertura pren l'avanttítol de l'atribut `kicker` del títol i reserva 48 mm, just el que cal per al títol i el seu filet amb el primer exemple encara a la pàgina 1.

## La recepta completa

Un sol fitxer, compost a partir de la carpeta de la recepta amb el text d'exemple i el kit comú del Receptari ja inclosos; construeix la seva pròpia pàgina. Per executar-lo, posa'l en un `<script type="module">` d'una pàgina buida o enganxa'l al tauler JS d'un pen nou de CodePen (com a mòdul). Importa postext des d'esm.sh, així que no cal instal·lar ni compilar res.

- Carpeta de la recepta: 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 = 'es'; // @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: "Armonía al teclado"
---

# Cadencias {kicker="Capítulo 5"}

Una frase musical termina como termina una oración: con una pausa que le dice al oyente cuánto se ha dicho. La armonía marca esa pausa con una cadencia, una breve sucesión de acordes al final de la frase. Algunas cadencias cierran la idea; otras la dejan abierta y piden la frase siguiente. Oír la diferencia es el primer paso para escribir frases que terminen donde tú quieres que terminen.

Este capítulo trabaja en sol mayor, con tres voces superiores en posición cerrada. El bajo no se escribe: toma la fundamental de cada acorde, como indica el número romano bajo el pentagrama, en mayúscula para un acorde mayor y en minúscula para uno menor. Toca cada ejemplo al piano, la mano derecha en el pentagrama y la izquierda en el bajo, y canta la voz superior antes de seguir leyendo.

:::callout{style="terms"}
**Cadencia:** los dos últimos acordes de una frase. **Soprano:** la voz superior. **Sensible:** el séptimo grado de la escala, un semitono por debajo de la tónica, que tiende a subir hacia ella.
:::

## La cadencia perfecta

El cierre más firme va de dominante a tónica, V–I, con los dos acordes en estado fundamental y la tónica en el soprano al final. La sensible (fa sostenido en sol mayor) sube un semitono hasta la tónica.

\`\`\`abc {alt="Cadencia perfecta en sol mayor: I, IV, V, I, con el soprano terminando en sol"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
\`\`\`

La llegada a la dominante importa tanto como la cadencia. Aquí el IV prepara el V: las voces superiores mantienen el sol y después bajan por grados conjuntos, sin ningún salto. Una frase que llega al V por salto suena apresurada, como si se hubiera comido una palabra.

## La cadencia imperfecta

Con los mismos acordes, basta terminar con la tercera de la tónica en el soprano para que el cierre se suavice. La sucesión sigue siendo V–I, pero la melodía se detiene en si y no en sol, y el oído espera algo más. Los compositores usan esta cadencia en mitad de un periodo y guardan la perfecta para el final.

\`\`\`abc {alt="Cadencia imperfecta en sol mayor: V a I con si en el soprano"}
X:2
M:4/4
L:1/2
K:G
[dgb] [egc'] | [dfa] [dgb] |]
w: I IV V I
\`\`\`

## La semicadencia

Una frase puede detenerse en la propia dominante. La semicadencia termina en el V, al que se llega desde el I, el ii o el IV, y suena como una coma: la música ha dicho algo y nos debe el resto. Casi todas las canciones populares y los himnos ponen una semicadencia al final del primer verso y una cadencia perfecta al final del segundo.

\`\`\`abc {alt="Semicadencia en sol mayor: I, IV, V, sostenido"}
X:3
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [A2d2f2] |]
w: I IV V
\`\`\`

## La cadencia plagal

IV–I cierra sin sensible. Es la cadencia del *amén* que se canta después de un himno, y suele llegar tras una cadencia perfecta que ya ha terminado la frase: la plagal añade una coda, no una conclusión.

\`\`\`abc {alt="Cadencia plagal en sol mayor: I, IV, I"}
X:4
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [B2d2g2] |]
w: I IV I
\`\`\`

## La cadencia rota

Cuando el V va al vi en lugar de al I, el oyente recibe un acorde que comparte dos notas con la tónica pero no es la tónica. La sensible sigue subiendo a sol, de modo que la conducción de voces cumple su promesa mientras la armonía la rompe. Las otras dos voces superiores bajan por grados y el acorde duplica su tercera, sol, lo que evita octavas paralelas entre el bajo y las voces interiores. La frase tiene que continuar, y suele cerrar con una cadencia perfecta unos compases después.

\`\`\`abc {alt="Cadencia rota en sol mayor: V a vi, con la sensible subiendo a sol"}
X:5
M:4/4
L:1/2
K:G
[ceg] [Adf] | [G2B2g2] |]
w: IV V vi
\`\`\`

## Cadencias en una melodía

Una melodía muestra sus cadencias aunque no lleve acordes. *Greensleeves*, una canción inglesa del siglo XVI, aparece aquí en la menor con un número romano sobre cada compás, los acordes que tocaría un guitarrista. Su primera frase se detiene en mi, la dominante, sobre una semicadencia; la segunda termina en la sobre V–i, con el sol sostenido subiendo a la tónica. El mismo plan vuelve en el estribillo, que empieza en un sol agudo sobre do, el relativo mayor, y regresa a la menor para el cierre final. Cántala, marca cada cadencia y di de qué tipo es antes de comprobar los números sobre el pentagrama.

\`\`\`abc {alt="Greensleeves en la menor con un número romano sobre cada compás"}
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 |]
\`\`\`

Las cuatro frases siguen el plan de la mayoría de las canciones populares: abierta, cerrada, abierta, cerrada. Fíjate en dónde aparece la sensible. En las semicadencias, el sol sostenido está dentro del acorde de dominante y no va a ninguna parte; en las cadencias perfectas es la penúltima nota y resuelve.

## Ejercicios

1. Escribe cada cadencia de este capítulo en re mayor y en fa mayor, a tres voces, y tócala con el bajo en la mano izquierda.
2. Convierte la cadencia imperfecta en perfecta cambiando una sola nota del soprano.
3. Busca las cadencias de un himno que conozcas. En cada frase, escribe los dos últimos números romanos y nombra la cadencia.
4. Vuelve a armonizar la primera frase de *Greensleeves* terminándola con una cadencia imperfecta en lugar de una semicadencia. ¿Qué cambia en la segunda frase?

:::paragraphs{style="colophon"}
Compuesto en Libre Caslon Text, Libre Caslon Display y Libre Franklin (SIL OFL) · Música grabada por Verovio en Bravura · Texto: CC BY 4.0 · *Greensleeves*: tradicional
:::
`; // 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 ───────────────────────────────────────────────────────────────────────
````

## Variants

### Fes servir Leland per a una pàgina més lleugera

Leland, la font de MuseScore, té pals més prims i caps més petits que Bravura; va bé amb un llibre compost en una lletra de text lleugera.

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

### Escriu una segona veu en un altre format

El lector d'ABC pren una sola veu. Per a un soprano i un baix en pentagrames diferents, escriu l'exemple en Humdrum o en MusicXML; [el mètode de conservatori](https://postext.dev/ca/cookbook/conservatory-method-chorales.md) compon així corals a quatre veus.

## Errors freqüents

- Escriu la durada d'un acord dins dels claudàtors, `[B2d2g2]`, o ajusta `L:` al valor de l'acord: una durada després del claudàtor, `[Bdg]2`, es llegeix com la durada per defecte.
- El lector d'ABC de Verovio dona cada síl·laba d'una línia `w:` a la nota següent i ignora `*` i `_`, així que una línia `w:` no pot saltar-se notes. Per assenyalar uns quants compassos, escriu les marques com a xifrats sobre el pentagrama, com fa l'exemple de *Greensleeves*.
- Una lletra de text rarament porta els signes ♯ i ♭: escriu «fa sostingut» a la prosa, o el navegador posarà el signe amb una font del sistema.

## Crèdits

- Recepta: 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 ([font](https://en.wikipedia.org/wiki/Greensleeves)), domini públic
- Tipus de lletra: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Codi: MIT · Contingut d'exemple: CC-BY-4.0

## Relacionades

- [Núm. 008 · Requadres de llibre de text amb codi de colors](https://postext.dev/ca/cookbook/textbook-box-family.md): Sis tipus de requadre en tres colors, que es distingeixen per una franja amb icona, un distintiu, una pestanya numerada, pictogrames o una marca lateral. · Nivell 2 (Intermedi) · Llibres de text
- [Núm. 001 · Llibre de text amb columna al marge](https://postext.dev/ca/cookbook/textbook-margin-column.md): Columna i mitja amb la columna exterior només per a flotants: s'hi apilen figures i glosses amb span 'side', i captionSide hi porta la resta de peus. · Nivell 3 (Avançat) · Llibres de text
- [Núm. 124 · Un llibre de lectura escolar amb furigana a cada kanji](https://postext.dev/ca/cookbook/jukugo-furigana-textbook.md): «Gon, la guineu», de Niimi Nankichi, en un llibre escolar: furigana per caràcter, per paraula i jukugo, repartit 1:2:1 i recolzat en kana, mai en kanji. · Nivell 2 (Intermedi) · Llibres de text
