# Um manual de harmonia com exemplos musicais em ABC

> Um capítulo sobre cadências cujos exemplos são blocos ABC no Markdown, gravados na largura da coluna; um deles continua na página seguinte.

- Versão HTML: https://postext.dev/pt/cookbook/harmony-textbook-abc-examples
- Receita Nº 163 · Livros didáticos · Nível 2 (Intermediário) · Saídas: Canvas, PDF
- Gêneros: Livros didáticos
- Requer postext ≥ 1.54.0, postext-pdf ≥ 1.54.0 · testada com 1.54.0, postext-pdf 1.54.0 em 2026-10-22
- Páginas: [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
- Abrir no Sandbox: https://postext.dev/pt/sandbox#recipe=harmony-textbook-abc-examples&lang=en (.postext: https://postext.dev/cookbook/harmony-textbook-abc-examples/en/harmony-textbook-abc-examples.postext)
- Última atualização: 2026-10-22
- Outros idiomas: [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), [fr](https://postext.dev/fr/cookbook/harmony-textbook-abc-examples.md), [it](https://postext.dev/it/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)

## Em poucas palavras

Um capítulo de um livro de teoria musical sobre como as frases terminam. Os exemplos são escritos como linhas curtas de texto no capítulo, e o Postext os desenha como pautas e notas; o mais longo continua na página seguinte.

## O que você vai compor

O capítulo 5 de um curso de harmonia ao teclado, em 180 × 250 mm e Libre Caslon, sobre as cinco cadências: perfeita e imperfeita, meia cadência, plagal e de engano. Cada cadência ganha dois parágrafos curtos e um exemplo a três vozes com os algarismos romanos sob a pauta, escrito em ABC no próprio Markdown. O capítulo termina com *Greensleeves* em lá menor, com um algarismo sobre cada compasso, e essa melodia é longa demais para o espaço que sobra na página 3: põe ali três sistemas e os dois últimos no alto da página 4, quebrada entre sistemas como um parágrafo se quebra entre linhas. O tamanho da pauta, a fonte musical e o espaçamento são definidos uma vez só na configuração, e assim os cinco exemplos e a melodia têm a mesma aparência.

**Esta receita responde a:**

- Como coloco exemplos musicais no texto de um livro didático, escritos como notação?

## A resposta curta

````js
// script.js, linhas 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

**Ensina**

- [Notação musical](https://postext.dev/pt/docs/music.md#uma-partitura-no-texto): Partituras escritas como texto em blocos ```abc, ```musicxml, ```mei ou ```humdrum, gravadas pelo Verovio na largura da coluna e cortadas em sistemas que continuam na coluna ou página seguinte.
- [Estilo de partitura](https://postext.dev/pt/docs/configuration-resources.md#estilo-de-partitura): scoreStyle e os estilos com nome de scoreStyles: tamanho da pauta, fonte musical, fonte do texto, espaçamento, números de compasso, espaço entre sistemas, margens, tinta e quantos sistemas ficam juntos numa quebra.

**Também usa**

- [Aberturas desenhadas](https://postext.dev/pt/docs/configuration-headings.md#largura-e-design-avançado)
- [Atributos de título](https://postext.dev/pt/docs/document-format.md#atributos-de-título)
- [Cabeços e fólios](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés)
- [Margens espelhadas](https://postext.dev/pt/docs/configuration-page-layout.md#margens-espelhadas)
- [Cabeços por tipo de página](https://postext.dev/pt/docs/configuration-headers-footers.md#elementos-de-texto)
- [Boxes](https://postext.dev/pt/docs/configuration-callouts.md#estilos-de-boxe)
- [Estilos de parágrafo](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-parágrafo)
- [Paleta de cores semântica](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#paleta-de-cores)
- [Viúvas, órfãs e linhas curtas](https://postext.dev/pt/docs/configuration-page-flow.md#órfãs-viúvas-linhas-curtas-e-regras-de-manter-junto)
- [Exportação para PDF](https://postext.dev/pt/docs/configuration-pdf-output.md#geração-de-pdf)
- [Listagens de código](https://postext.dev/pt/docs/configuration-styles.md#listagens-de-código)
- [Fontes incorporadas ao PDF](https://postext.dev/pt/docs/configuration-pdf-output.md#por-que-um-provedor-de-fontes)
- [Sobrescritos e subscritos](https://postext.dev/pt/docs/document-format-inline.md#formatação-em-linha)

**A configuração em resumo**

- [`bodyText`](https://postext.dev/pt/docs/configuration-text.md#texto-do-corpo), [`calloutStyles`](https://postext.dev/pt/docs/configuration-callouts.md#estilos-de-boxe), [`colorPalette`](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#paleta-de-cores), [`footer`](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés), [`header`](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés), [`headings`](https://postext.dev/pt/docs/configuration-headings.md#títulos), [`layout`](https://postext.dev/pt/docs/configuration-page-layout.md#diagramação), [`locale`](https://postext.dev/pt/docs/configuration-language.md#hifenização), [`orderedLists`](https://postext.dev/pt/docs/configuration-lists-math.md#listas-numeradas), [`page`](https://postext.dev/pt/docs/configuration-page-layout.md#página), [`paragraphStyles`](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-parágrafo), [`scoreStyle`](https://postext.dev/pt/docs/configuration-resources.md#estilo-de-partitura), [`tableStyle`](https://postext.dev/pt/docs/configuration-resources.md#estilo-de-tabela)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/pt/docs/configuration-programmatic-usage.md#carregar-as-fontes-antes-da-diagramação), [`decompressWoff2`](https://postext.dev/pt/docs/configuration-pdf-output.md#provedor-de-fontes-no-navegador-fontsource--woff2), [`initMusicEngine`](https://postext.dev/pt/docs/music.md#uma-partitura-no-texto), [`renderPageToCanvas`](https://postext.dev/pt/docs/configuration-programmatic-usage.md#renderizar-uma-página-como-bitmap), [`renderToPdf`](https://postext.dev/pt/docs/configuration-pdf-output.md#geração-de-pdf)

**Tipos**

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

## Preparo

### 1 · Escrever o exemplo onde o texto precisa dele

Um bloco marcado `abc` contém a notação, e o texto continua depois dele. `L:1/2` faz de cada acorde sem duração uma mínima, `[Bdg]` empilha três notas numa só voz e uma linha `w:` põe uma palavra sob cada acorde, aqui os algarismos romanos. O atributo `alt` é o que um leitor de tela e o PDF etiquetado dizem no lugar da imagem.

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

### 2 · Dar a cada partitura a aparência do livro

[A resposta curta](https://postext.dev/pt/cookbook/harmony-textbook-abc-examples.md#a-resposta-curta) é todo o design da música: uma pauta de 5,5 mm, como os manuais de harmonia usam para exemplos lidos ao teclado, Bravura ao lado do texto em Caslon e os algarismos na fonte do texto em vez da Times do gravador. `spacing: 1.6` espalha quatro acordes em mínimas por um terço da largura da coluna, para que a progressão se leia acorde por acorde. A mesma configuração decide quantos compassos de *Greensleeves* cabem num sistema: três, numa coluna de 125 mm.

### 3 · Deixar um exemplo longo continuar

`minSystemsAtBreak: 2` é o que mantém *Greensleeves* legível na virada da página. A melodia sai em cinco sistemas; três cabem no pé da página 3, então dois vão para a página 4, e nenhuma página fica com um sistema sozinho. Uma partitura com menos de quatro sistemas passa inteira para a página seguinte, como fariam os exemplos curtos de cadências.

### 4 · Iniciar o gravador antes da composição

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

O Verovio tem cerca de 3,7 MB de WebAssembly, por isso o motor só o carrega quando você pede. Sem `initMusicEngine()` a diagramação roda mesmo assim, e cada partitura é uma caixa cinza do tamanho certo. `renderToPdf` não precisa de mais nada: o PDF desenha cada sistema como traçados vetoriais e compõe os algarismos na Caslon embutida.

### 5 · Abrir o capítulo com um título rebaixado

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

A abertura tira o chapéu do atributo `kicker` do título e reserva 48 mm, o bastante para o título e o seu fio com o primeiro exemplo ainda na página 1.

## A receita completa

Um único arquivo, composto a partir da pasta da receita com o texto de exemplo e o kit comum das Receitas já incluídos; ele monta a própria página. Para executá-lo, coloque-o em um `<script type="module">` de uma página vazia ou cole-o no painel JS de um pen novo do CodePen (como módulo). Ele importa o postext do esm.sh, então não há nada para instalar nem compilar.

- Pasta da receita: 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 ───────────────────────────────────────────────────────────────────────
````

## Variações

### Usar Leland para uma página mais leve

Leland, a fonte do MuseScore, tem hastes mais finas e cabeças de nota menores que a Bravura; combina com um livro composto numa fonte de texto leve.

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

### Escrever uma segunda voz em outro formato

O leitor de ABC aceita uma só voz. Para soprano e baixo em pautas separadas, escreva o exemplo em Humdrum ou MusicXML; [o método de conservatório](https://postext.dev/pt/cookbook/conservatory-method-chorales.md) compõe assim corais a quatro vozes.

## Erros comuns

- Escreva a duração de um acorde dentro dos colchetes, `[B2d2g2]`, ou ajuste `L:` ao valor do acorde: uma duração depois do colchete, `[Bdg]2`, é lida como a duração padrão.
- O leitor de ABC do Verovio dá cada sílaba de uma linha `w:` à nota seguinte e ignora `*` e `_`, então uma linha `w:` não pode pular notas. Para marcar alguns compassos, escreva as marcas como cifras sobre a pauta, como faz o exemplo de *Greensleeves*.
- Uma fonte de texto raramente tem os sinais ♯ e ♭: escreva “fá sustenido” no texto, ou o navegador põe o sinal com uma fonte do sistema.

## Créditos

- Receita: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Texto: The chapter on cadences and its examples, written for this recipe: Ignacio Ferro, CC-BY-4.0
- Texto: Greensleeves, English tune of the sixteenth century, in its common modern form: Traditional ([fonte](https://en.wikipedia.org/wiki/Greensleeves)), domínio público
- Tipos: Libre Caslon Text (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Código: MIT · Conteúdo de exemplo: CC-BY-4.0

## Relacionadas

- [Nº 008 · Uma família de boxes de livro didático com código de cores](https://postext.dev/pt/cookbook/textbook-box-family.md): Seis tipos de boxe de livro didático em três cores, diferenciados por faixa com ícone, selo, aba numerada, pictogramas ou um indicador fora da moldura. · Nível 2 (Intermediário) · Livros didáticos
- [Nº 001 · Livro didático com coluna na margem](https://postext.dev/pt/cookbook/textbook-margin-column.md): Página em coluna e meia cuja coluna externa só recebe flutuantes: figuras e glosas com span 'side' se empilham ali, e captionSide leva as demais legendas. · Nível 3 (Avançado) · Livros didáticos
- [Nº 124 · Um livro de leitura escolar com furigana em cada kanji](https://postext.dev/pt/cookbook/jukugo-furigana-textbook.md): “Gon, a raposa”, de Niimi Nankichi, num livro escolar: furigana por caractere, por palavra e jukugo, em 1:2:1, apoiado em kana e nunca em kanji. · Nível 2 (Intermediário) · Livros didáticos
