# Un manual de armonía con ejemplos musicales en ABC

> Un capítulo sobre las cadencias cuyos ejemplos son bloques ABC en el Markdown, grabados al ancho de la columna; uno de ellos sigue en la página siguiente.

- Versión HTML: https://postext.dev/es/cookbook/harmony-textbook-abc-examples
- Receta N.º 163 · Libros de texto · Nivel 2 (Intermedio) · Salidas: Canvas, PDF
- Géneros: Libros de texto
- Requiere postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · probada con 1.54.0, postext-pdf 1.54.0 el 2026-10-22
- Páginas: [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
- Abrir en el Sandbox: https://postext.dev/es/sandbox#recipe=harmony-textbook-abc-examples&lang=es (.postext: https://postext.dev/cookbook/harmony-textbook-abc-examples/es/harmony-textbook-abc-examples.postext)
- Última actualización: 2026-10-22
- Otros idiomas: [en](https://postext.dev/en/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), [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 pocas palabras

Un capítulo de un manual de teoría musical sobre cómo terminan las frases. Los ejemplos se escriben como texto dentro del capítulo y Postext los dibuja como pentagramas; el más largo sigue en la página siguiente.

## Lo que vas a componer

El capítulo 5 de un curso de armonía al teclado, de 180 × 250 mm en Libre Caslon, sobre las cinco cadencias: perfecta, imperfecta, semicadencia, plagal y rota. Cada cadencia lleva dos párrafos breves y un ejemplo a tres voces con sus números romanos bajo el pentagrama, escrito en ABC dentro del propio Markdown. El capítulo termina con *Greensleeves* en la menor, con un número romano sobre cada compás, y esa melodía no cabe en lo que queda de la página 3: pone allí tres sistemas y los dos últimos en la cabeza de la página 4, partida entre sistemas igual que un párrafo se parte entre líneas. El tamaño del pentagrama, la fuente musical y el espaciado se fijan una sola vez en la configuración, así que los cinco ejemplos y la melodía comparten el mismo aspecto.

**Esta receta responde a:**

- ¿Cómo pongo ejemplos musicales en el texto de un libro de texto, escritos como notación?

## La respuesta corta

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

## Ingredientes

**Enseña**

- [Notación musical](https://postext.dev/es/docs/music.md#una-partitura-en-el-texto): Partituras escritas como texto en bloques ```abc, ```musicxml, ```mei o ```humdrum, grabadas por Verovio al ancho de la columna y cortadas en sistemas que siguen en la columna o la página siguiente.
- [Estilo de partitura](https://postext.dev/es/docs/configuration-resources.md#estilo-de-partitura): scoreStyle y los estilos con nombre de scoreStyles: tamaño del pentagrama, fuente musical, fuente del texto, espaciado, números de compás, separación entre sistemas, márgenes, tinta y cuántos sistemas se mantienen juntos en un corte.

**También usa**

- [Aperturas diseñadas](https://postext.dev/es/docs/configuration-headings.md#apertura-a-toda-página-y-diseño-avanzado)
- [Atributos de título](https://postext.dev/es/docs/document-format.md#atributos-de-encabezado)
- [Cabeceras y folios](https://postext.dev/es/docs/configuration-headers-footers.md#cabeceras-y-pies)
- [Márgenes simétricos](https://postext.dev/es/docs/configuration-page-layout.md#márgenes-simétricos-espejo)
- [Cabeceras según el tipo de página](https://postext.dev/es/docs/configuration-headers-footers.md#elementos-de-texto)
- [Recuadros](https://postext.dev/es/docs/configuration-callouts.md#estilos-de-recuadro)
- [Estilos de párrafo](https://postext.dev/es/docs/configuration-styles.md#estilos-de-párrafo)
- [Paleta de color semántica](https://postext.dev/es/docs/configuration-fonts-colors-viewers.md#paleta-de-colores)
- [Viudas, huérfanas y líneas cortas](https://postext.dev/es/docs/configuration-page-flow.md#huérfanas-viudas-líneas-cortas-y-reglas-de-cohesión)
- [Exportación a PDF](https://postext.dev/es/docs/configuration-pdf-output.md#generación-de-pdf)
- [Listados de código](https://postext.dev/es/docs/configuration-styles.md#listados-de-código)
- [Fuentes incrustadas en el PDF](https://postext.dev/es/docs/configuration-pdf-output.md#por-qué-un-proveedor-de-fuentes)
- [Superíndices y subíndices](https://postext.dev/es/docs/document-format-inline.md#formato-en-línea)

**La configuración de un vistazo**

- [`bodyText`](https://postext.dev/es/docs/configuration-text.md#texto-base), [`calloutStyles`](https://postext.dev/es/docs/configuration-callouts.md#estilos-de-recuadro), [`colorPalette`](https://postext.dev/es/docs/configuration-fonts-colors-viewers.md#paleta-de-colores), [`footer`](https://postext.dev/es/docs/configuration-headers-footers.md#cabeceras-y-pies), [`header`](https://postext.dev/es/docs/configuration-headers-footers.md#cabeceras-y-pies), [`headings`](https://postext.dev/es/docs/configuration-headings.md#encabezados), [`layout`](https://postext.dev/es/docs/configuration-page-layout.md#disposición), [`locale`](https://postext.dev/es/docs/configuration-language.md#separación-silábica), [`orderedLists`](https://postext.dev/es/docs/configuration-lists-math.md#listas-ordenadas), [`page`](https://postext.dev/es/docs/configuration-page-layout.md#página), [`paragraphStyles`](https://postext.dev/es/docs/configuration-styles.md#estilos-de-párrafo), [`scoreStyle`](https://postext.dev/es/docs/configuration-resources.md#estilo-de-partitura), [`tableStyle`](https://postext.dev/es/docs/configuration-resources.md#estilo-de-tablas)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/es/docs/configuration-programmatic-usage.md#cargar-las-fuentes-antes-de-componer), [`decompressWoff2`](https://postext.dev/es/docs/configuration-pdf-output.md#proveedor-de-fuentes-en-el-navegador-fontsource--woff2), [`initMusicEngine`](https://postext.dev/es/docs/music.md#una-partitura-en-el-texto), [`renderPageToCanvas`](https://postext.dev/es/docs/configuration-programmatic-usage.md#renderizar-una-página-como-mapa-de-bits), [`renderToPdf`](https://postext.dev/es/docs/configuration-pdf-output.md#generación-de-pdf)

**Tipografías**

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

## Elaboración

### 1 · Escribe el ejemplo donde lo pide el texto

Un bloque marcado `abc` contiene la notación, y el texto sigue después. `L:1/2` hace de cada acorde sin duración una blanca, `[Bdg]` apila tres notas en una sola voz y una línea `w:` pone una palabra bajo cada acorde, aquí los números romanos. El atributo `alt` es lo que leen un lector de pantalla y el PDF etiquetado en lugar de la imagen.

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

### 2 · Da a cada partitura el aspecto del libro

La [respuesta corta](https://postext.dev/es/cookbook/harmony-textbook-abc-examples.md#la-respuesta-corta) es todo el diseño de la música: un pentagrama de 5,5 mm, el que usan los manuales de armonía para ejemplos que se leen sentado al piano, Bravura junto al texto en Caslon y los números en la letra del texto en lugar de la Times del grabador. `spacing: 1.6` reparte cuatro acordes de blanca por un tercio de la caja, de modo que la progresión se lee acorde a acorde. El mismo ajuste decide cuántos compases de *Greensleeves* caben en un sistema: tres, en una caja de 125 mm.

### 3 · Deja que un ejemplo largo continúe

`minSystemsAtBreak: 2` es lo que mantiene legible *Greensleeves* al pasar la página. La melodía sale en cinco sistemas; tres caben al pie de la página 3, así que dos pasan a la 4, y ninguna página se queda con un sistema suelto. Una partitura de menos de cuatro sistemas pasa entera a la página siguiente, como harían los ejemplos breves de cadencias.

### 4 · Arranca el grabador antes de componer

```js
// script.js, líneas 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 unos 3,7 MB de WebAssembly, así que el motor solo lo carga cuando se lo pides. Sin `initMusicEngine()` la composición se hace igual y cada partitura es un recuadro gris del tamaño correcto. `renderToPdf` no necesita nada más: el PDF dibuja cada sistema como trazados vectoriales y pone los números en la Caslon incrustada.

### 5 · Abre el capítulo con el título hundido

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

La apertura toma el antetítulo del atributo `kicker` del título y reserva 48 mm, lo justo para el título y su filete con el primer ejemplo todavía en la página 1.

## La receta completa

Un solo archivo, compuesto a partir de la carpeta de la receta con el texto de ejemplo y el kit común del Recetario ya incluidos; construye su propia página. Para ejecutarlo, ponlo en un `<script type="module">` de una página vacía o pégalo en el panel JS de un pen nuevo de CodePen (como módulo). Importa postext desde esm.sh, así que no hay nada que instalar ni compilar.

- Carpeta de la receta: 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 ───────────────────────────────────────────────────────────────────────
````

## Variantes

### Usa Leland para una página más ligera

Leland, la fuente de MuseScore, tiene plicas más finas y cabezas más pequeñas que Bravura; va bien con un libro compuesto en una letra de texto ligera.

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

### Escribe una segunda voz en otro formato

El lector de ABC toma una sola voz. Para un soprano y un bajo en pentagramas distintos, escribe el ejemplo en Humdrum o en MusicXML; [el método de conservatorio](https://postext.dev/es/cookbook/conservatory-method-chorales.md) compone así corales a cuatro voces.

## Errores frecuentes

- Escribe la duración de un acorde dentro de los corchetes, `[B2d2g2]`, o ajusta `L:` al valor del acorde: una duración tras el corchete, `[Bdg]2`, se lee como la duración por defecto.
- El lector de ABC de Verovio da cada sílaba de una línea `w:` a la nota siguiente e ignora `*` y `_`, así que una línea `w:` no puede saltarse notas. Para señalar unos pocos compases, escribe las marcas como cifrados sobre el pentagrama, como hace el ejemplo de *Greensleeves*.
- Una letra de texto rara vez trae los signos ♯ y ♭: escribe «fa sostenido» en la prosa, o el navegador pondrá el signo con una fuente del sistema.

## Créditos

- Receta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Texto: El capítulo sobre las cadencias y sus ejemplos, escritos para esta receta: Ignacio Ferro, CC-BY-4.0
- Texto: Greensleeves, canción inglesa del siglo XVI, en su forma moderna habitual: Traditional ([fuente](https://en.wikipedia.org/wiki/Greensleeves)), dominio público
- Tipografías: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Código: MIT · Contenido de ejemplo: CC-BY-4.0

## Relacionadas

- [N.º 008 · Recuadros de libro de texto con código de colores](https://postext.dev/es/cookbook/textbook-box-family.md): Seis tipos de recuadro en tres colores, que se distinguen por una franja con icono, un distintivo, una pestaña numerada, pictogramas o una marca lateral. · Nivel 2 (Intermedio) · Libros de texto
- [N.º 001 · Libro de texto con columna al margen](https://postext.dev/es/cookbook/textbook-margin-column.md): Columna y media con la columna exterior solo para flotantes: en ella se apilan figuras y glosas con span 'side', y captionSide lleva allí los demás pies. · Nivel 3 (Avanzado) · Libros de texto
- [N.º 124 · Un libro de lectura escolar con furigana en cada kanji](https://postext.dev/es/cookbook/jukugo-furigana-textbook.md): «Gon, el zorro», de Niimi Nankichi, en un libro escolar: furigana por carácter, por palabra y jukugo, repartido 1:2:1 y apoyado en kana, nunca en kanji. · Nivel 2 (Intermedio) · Libros de texto
