# Uma partitura de piano de MusicXML em várias páginas

> Dois minuetos para teclado gravados a partir de arquivos MusicXML em pauta dupla, divididos entre sistemas para que cada peça ocupe uma página dupla.

- Versão HTML: https://postext.dev/pt/cookbook/piano-score-from-musicxml
- Receita Nº 167 · Figuras e imagens · Nível 2 (Intermediário) · Saídas: Canvas, PDF
- Gêneros: Qualquer gênero
- 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/piano-score-from-musicxml/en/p01.webp?v=c38bdad1), [2](https://postext.dev/cookbook/piano-score-from-musicxml/en/p02.webp?v=c38bdad1), [3](https://postext.dev/cookbook/piano-score-from-musicxml/en/p03.webp?v=c38bdad1), [4](https://postext.dev/cookbook/piano-score-from-musicxml/en/p04.webp?v=c38bdad1), [5](https://postext.dev/cookbook/piano-score-from-musicxml/en/p05.webp?v=c38bdad1)
- PDF: https://postext.dev/cookbook/piano-score-from-musicxml/en/piano-score-from-musicxml.pdf?v=c38bdad1
- Abrir no Sandbox: https://postext.dev/pt/sandbox#recipe=piano-score-from-musicxml&lang=en (.postext: https://postext.dev/cookbook/piano-score-from-musicxml/en/piano-score-from-musicxml.postext)
- Última atualização: 2026-10-22
- Outros idiomas: [en](https://postext.dev/en/cookbook/piano-score-from-musicxml.md), [es](https://postext.dev/es/cookbook/piano-score-from-musicxml.md), [ca](https://postext.dev/ca/cookbook/piano-score-from-musicxml.md), [de](https://postext.dev/de/cookbook/piano-score-from-musicxml.md), [fr](https://postext.dev/fr/cookbook/piano-score-from-musicxml.md), [it](https://postext.dev/it/cookbook/piano-score-from-musicxml.md), [zh](https://postext.dev/zh/cookbook/piano-score-from-musicxml.md), [ja](https://postext.dev/ja/cookbook/piano-score-from-musicxml.md), [ar](https://postext.dev/ar/cookbook/piano-score-from-musicxml.md)

## Em poucas palavras

Uma pequena edição de dois minuetos de piano muito conhecidos. Mostra como pegar um arquivo exportado de um programa de notação e compô-lo como uma partitura que continua de uma página para outra, pronta para tocar.

## O que você vai compor

Uma edição para intérpretes dos dois minuetos do Caderno de Anna Magdalena Bach de 1725: o de sol maior, que todo aluno de piano aprende, e seu par em sol menor, hoje atribuídos a Christian Petzold. A página mede 230 × 305 mm, o tamanho de um álbum de partituras. Uma página ímpar apresenta as peças em duas colunas de Libre Caslon sob um título de 52 pt em Bodoni. Depois cada minueto começa numa página par e ocupa a página dupla numa pauta dupla de 7 mm, gravada em Bravura a partir de um arquivo MusicXML: quatro compassos por sistema, os compassos 1 a 16 à esquerda e 17 a 32 à direita, de modo que o pianista não vira a página no meio de uma peça.

**Esta receita responde a:**

- Como faço uma partitura de piano de um arquivo MusicXML ao longo de várias páginas?

## A resposta curta

```js
// script.js, linhas 36–61
const scoreStyle = {
  staffSize: mm(7), // the rastral of a performing edition: read at arm's length
  musicFont: 'Bravura', // the engraved plate look
  barNumbers: 'system', // a number at the head of each system, as editions print them
  spacing: 1.7, // about four bars to a system, eight systems to a piece
  systemGap: mm(14), // room for the ledger lines above and below the grand staff
  marginTop: pt(LEAD),
  marginBottom: pt(0),
  minSystemsAtBreak: 2, // never one system alone at a foot or the head of a page
  color: col('ink'),
};
// A fence breaks between systems, so a piece runs on over pages; a score resource is kept
// whole. The file is what notation software exports: the Markdown keeps a placeholder
// line in each fence and the pen puts the fetched file there.
const SCORES = {
  'bwv-anh-114': asset('bwv-anh-114.musicxml'),
  'bwv-anh-115': asset('bwv-anh-115.musicxml'),
};
async function withScores(text) {
  const files = await Promise.all(Object.entries(SCORES).map(async ([id, url]) => {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Score not found (${res.status}): ${url}`);
    return [id, (await res.text()).trim()];
  }));
  return files.reduce((md, [id, xml]) => md.replace(`{{${id}}}`, () => xml), text);
}
```

## 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)
- [Estilos de título](https://postext.dev/pt/docs/configuration-styles.md#estilos-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)
- [Colunas num boxe ou no texto](https://postext.dev/pt/docs/document-format-directives.md#columns)
- [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)
- [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)
- [Cabeços por seção](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-título)
- [Capítulos sem número](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-título)

**A configuração em resumo**

- [`bodyText`](https://postext.dev/pt/docs/configuration-text.md#texto-do-corpo), [`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), [`headingStyles`](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-título), [`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), [`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)

**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), Bodoni Moda (OFL-1.1), Archivo Narrow (OFL-1.1)

## Preparo

### 1 · Trazer a peça em MusicXML

Os dois arquivos em `assets/` são MusicXML 4.0 comum, o formato que Dorico, Sibelius, Finale e MuseScore gravam ao exportar uma partitura (escolha o `.musicxml` sem compressão, não o `.mxl`). Eles têm uma parte em duas pautas, com as claves, a armadura, as barras de colcheia, a direção das hastes nos compassos a duas vozes, os ornamentos e as barras de repetição. O nome da parte leva `print-object="no"`: um rótulo «Piano» impresso empurraria cada sistema para a direita. O título em `<work-title>` não é impresso, serve só de descrição da partitura, então o Markdown dá a cada peça um título próprio.

### 2 · Compor num bloco de código, não como recurso

A [resposta curta](https://postext.dev/pt/cookbook/piano-score-from-musicxml.md#a-resposta-curta) baixa cada arquivo e o escreve num bloco ` ```musicxml `, onde o Markdown guarda uma linha de marcador. Um bloco é gravado na largura da coluna e cortado em sistemas, e cada sistema é colocado como uma linha de parágrafo: a peça continua na página seguinte entre dois sistemas. Um recurso `score` fica inteiro e não poderia atravessar uma página. `minSystemsAtBreak: 2` impede que um sistema fique sozinho no pé de uma página ou passe sozinho para o alto da seguinte.

### 3 · Dimensionar a pauta para quem toca

A pauta tem 7 mm de altura, o tamanho de um álbum de piano impresso lido na estante. `spacing: 1.7` afasta as notas até que um sistema de 186 mm de medida tenha quatro compassos, e assim cada metade de um minueto ocupa quatro sistemas. `systemGap: 14 mm` deixa espaço para as linhas suplementares entre as mãos, e com ele cabem quatro sistemas sob o título da primeira página e quatro na página em frente: a barra dupla cai na passagem de uma página à outra da página dupla.

### 4 · Abrir cada peça numa página par

```js
// script.js, linhas 65–103
const label = (id, content, edge, x, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: LABEL, fontSize: pt(9), fontWeight: 500,
  color: col('ink'), align: edge.endsWith('right') ? 'right' : 'left',
  placement: { anchor: { to: 'container', edge }, offset: { x: mm(x), y: mm(y) } }, ...extra,
});
const kicker = { fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
  textTransform: 'uppercase', color: col('accent'), align: 'center' };
const centred = (to, y) => ({ anchor: { to, edge: to === 'container' ? 'top' : 'below' },
  offset: { x: mm(0), y: mm(y) }, size: { width: 'fill' } });
const pieceOpener = {
  enabled: true,
  minHeight: mm(34),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 2,
      { ...kicker, placement: centred('container', 2) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(26),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 2.5) },
    label('catalogue', '{attr.catalogue}', 'bottom-left', 0, -3),
    label('composer', '{attr.composer}', 'bottom-right', 0, -3, { italic: true }),
  ] },
};
const frontOpener = {
  enabled: true,
  minHeight: mm(78),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 14,
      { ...kicker, placement: centred('container', 14) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(52),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 5) },
    { kind: 'text', id: 'composer', content: '{attr.composer}', fontFamily: SERIF,
      fontSize: pt(13), italic: true, color: col('ink'), align: 'center',
      placement: centred('#title', 4) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.75), color: col('accent'),
      placement: { anchor: { to: '#composer', edge: 'below', at: 'center' }, offset: { y: mm(8) },
        size: { width: mm(24) } } },
  ] },
};
```

`breakBefore: { parity: 'even' }` faz cada peça começar numa página da esquerda, e a página de rosto do editor usa um estilo de título que abre numa página ímpar, sem cabeços. O número de catálogo e o compositor vêm dos atributos do título e ficam nos cantos inferiores da abertura, onde as edições gravadas os imprimem.

### 5 · Pôr o nome da peça no cabeço da direita

```js
// script.js, linhas 107–120
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: LABEL, fontSize: pt(8), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{chapterTitle}', 'even', 'top-left', OUTER + 9),
  head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
const footer = { elements: [] };
```

A primeira página de uma peça já leva o seu título, então os cabeços ficam para as páginas seguintes (`pages: 'body'`). A página ímpar imprime o título da peça e o fólio na borda externa, na fonte de rótulos a 8 pt.

## 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/piano-score-from-musicxml

### script.js

```js
// ═══ Postext Cookbook · Nº 167 · A piano score from MusicXML over several pages ═══════
// https://postext.dev/en/cookbook/piano-score-from-musicxml
// Code: MIT · Text: original (CC BY 4.0) · Music: Mutopia Project (public domain)
// Fonts: Libre Caslon Text, Bodoni Moda, Archivo Narrow (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 = 'piano-score-from-musicxml';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a plate-black ink, one deep green accent, every colour linked by id
const palette = {
  ink: '#1b1a18', // the notes and the text: a warm near-black
  accent: '#2f5a46', // titles' kickers, rules: the green of an old music-shop wrapper
  tint: '#eef2ee',
  rule: '#b9c4bc', // hairlines
  muted: '#625f59', // running heads, the 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' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const SERIF = 'Libre Caslon Text';
const DISPLAY = 'Bodoni Moda';
const LABEL = 'Archivo Narrow';
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt

// #region answer: the score style of a performing edition, and a MusicXML file in a fence
const scoreStyle = {
  staffSize: mm(7), // the rastral of a performing edition: read at arm's length
  musicFont: 'Bravura', // the engraved plate look
  barNumbers: 'system', // a number at the head of each system, as editions print them
  spacing: 1.7, // about four bars to a system, eight systems to a piece
  systemGap: mm(14), // room for the ledger lines above and below the grand staff
  marginTop: pt(LEAD),
  marginBottom: pt(0),
  minSystemsAtBreak: 2, // never one system alone at a foot or the head of a page
  color: col('ink'),
};
// A fence breaks between systems, so a piece runs on over pages; a score resource is kept
// whole. The file is what notation software exports: the Markdown keeps a placeholder
// line in each fence and the pen puts the fetched file there.
const SCORES = {
  'bwv-anh-114': asset('bwv-anh-114.musicxml'),
  'bwv-anh-115': asset('bwv-anh-115.musicxml'),
};
async function withScores(text) {
  const files = await Promise.all(Object.entries(SCORES).map(async ([id, url]) => {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`Score not found (${res.status}): ${url}`);
    return [id, (await res.text()).trim()];
  }));
  return files.reduce((md, [id, xml]) => md.replace(`{{${id}}}`, () => xml), text);
}
// #endregion

// #region openers: title centred, the catalogue number left, the composer right
const label = (id, content, edge, x, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: LABEL, fontSize: pt(9), fontWeight: 500,
  color: col('ink'), align: edge.endsWith('right') ? 'right' : 'left',
  placement: { anchor: { to: 'container', edge }, offset: { x: mm(x), y: mm(y) } }, ...extra,
});
const kicker = { fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
  textTransform: 'uppercase', color: col('accent'), align: 'center' };
const centred = (to, y) => ({ anchor: { to, edge: to === 'container' ? 'top' : 'below' },
  offset: { x: mm(0), y: mm(y) }, size: { width: 'fill' } });
const pieceOpener = {
  enabled: true,
  minHeight: mm(34),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 2,
      { ...kicker, placement: centred('container', 2) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(26),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 2.5) },
    label('catalogue', '{attr.catalogue}', 'bottom-left', 0, -3),
    label('composer', '{attr.composer}', 'bottom-right', 0, -3, { italic: true }),
  ] },
};
const frontOpener = {
  enabled: true,
  minHeight: mm(78),
  slot: { elements: [
    label('kicker', '{attr.kicker}', 'top', 0, 14,
      { ...kicker, placement: centred('container', 14) }),
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(52),
      fontWeight: 500, color: col('ink'), align: 'center',
      placement: centred('#kicker', 5) },
    { kind: 'text', id: 'composer', content: '{attr.composer}', fontFamily: SERIF,
      fontSize: pt(13), italic: true, color: col('ink'), align: 'center',
      placement: centred('#title', 4) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.75), color: col('accent'),
      placement: { anchor: { to: '#composer', edge: 'below', at: 'center' }, offset: { y: mm(8) },
        size: { width: mm(24) } } },
  ] },
};
// #endregion

// #region running-heads: the piece on every page after its first, folios outside
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: LABEL, fontSize: pt(8), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{chapterTitle}', 'even', 'top-left', OUTER + 9),
  head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
const footer = { elements: [] };
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(230), height: mm(305), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(24), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10), 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: DISPLAY, color: col('ink'), fontWeight: 500,
    levels: [
      // Each piece opens on a verso, so it reads across the spread with no page turn.
      { level: 1, fontSize: pt(26), breakBefore: { parity: 'even' }, advancedDesign: pieceOpener },
      { level: 2, fontFamily: LABEL, fontSize: pt(8.5), lineHeight: pt(LEAD), fontWeight: 700,
        letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('accent'),
        marginTop: pt(LEAD), marginBottom: pt(0) },
    ],
  },
  headingStyles: [
    { id: 'front', numbered: false, breakBefore: { parity: 'odd' }, advancedDesign: frontOpener,
      header: { elements: [] }, footer: { elements: [] } },
  ],
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  scoreStyle,
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = await withScores(`---
title: "Two Menuets"
---

# Two Menuets {style="front" kicker="From the Notebook for Anna Magdalena Bach, 1725" composer="Christian Petzold (1677–1733)"}

:::columns{count=2}

## The pieces

In 1725 Johann Sebastian Bach gave his wife Anna Magdalena a bound book of blank music paper. Over the next years she and the rest of the household filled it with keyboard pieces, songs and chorales: two of Bach's own partitas, arias, a few marches and polonaises, and pieces by other composers that the family played at home. The two menuets in this edition stand one after the other in Anna Magdalena's hand, the first in G major, the second in G minor.

For two centuries both were taken for Bach's own work. The Bach-Werke-Verzeichnis lists them among the doubtful works, as Anh. 114 and Anh. 115, and the first became one of the best-known pieces of piano teaching under his name. In the 1970s both were found in a harpsichord suite by Christian Petzold, an organist in Dresden, and they are now given to him.

Each menuet has 32 bars in two halves of 16, and each half is repeated. The first stays close to G major and visits D major in its second half; the second answers it in G minor and turns to B flat major at its double bar. Paired dances of the period were often played as a set: Menuet I, Menuet II, then Menuet I again without its repeats.

## This edition

The notes follow the edition of the Bach-Gesellschaft, as entered by Allen Garvin for the Mutopia Project, which placed it in the public domain. They were converted to MusicXML 4.0 and proofread bar by bar against that source: pitches, rhythms, accidentals, the two voices of the closing bars and the beaming. The ornaments are the source's, and so are the grace note in bar 8 and the reminder natural in bar 24 of Menuet I.

Neither the source nor this edition gives fingering, dynamics or slurs. The margins leave room to add them in pencil.

## Performance notes

A menuet of this kind is felt in one beat to the bar rather than three, at around crotchet = 120 to 132. Crotchets sound well slightly detached and quavers joined, which is how harpsichordists phrase the piece; on a piano, use the pedal sparingly, at the cadences if at all.

The mordent, the sign with a stroke through it, is the main note, the note below and the main note again, quick and on the beat. The short trill without a stroke, in bar 30 of Menuet I and in bars 8, 9, 15 and 31 of Menuet II, starts on the note above, on the beat.

Each menuet is set across a pair of facing pages, so no page is turned inside a piece. The one turn comes between the two menuets.

:::paragraphs{style="colophon"}
Set in Libre Caslon Text, Bodoni Moda and Archivo Narrow (SIL OFL). Music in Bravura (SIL OFL), engraved by Verovio. Notes after the Mutopia Project (public domain).
:::

:::

# Menuet in G major {kicker="Menuet I" catalogue="BWV Anh. 114" composer="Christian Petzold"}

\`\`\`musicxml {alt="Menuet in G major, BWV Anh. 114, by Christian Petzold, for keyboard"}
{{bwv-anh-114}}
\`\`\`

# Menuet in G minor {kicker="Menuet II" catalogue="BWV Anh. 115" composer="Christian Petzold"}

\`\`\`musicxml {alt="Menuet in G minor, BWV Anh. 115, by Christian Petzold, for keyboard"}
{{bwv-anh-115}}
\`\`\`
`); // content.<lang>.md, then the two files

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Libre Caslon Text': ['400', '400i', '700'],
  'Bodoni Moda': ['500'],
  'Archivo Narrow': ['400', '500', '500i', '600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await initMusicEngine(); // the engraver (Verovio) loads only for books with music
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Two Menuets', es: 'Dos minuetos' }) });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

// ─── 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

### Fazer uma edição de estudo com uma pauta menor

Uma pauta de 6 mm com o espaçamento próprio do gravador leva sete ou oito compassos por sistema, e cada minueto cabe na sua primeira página.

```diff
-  staffSize: mm(7),
+  staffSize: mm(6),
-  spacing: 1.7,
+  spacing: 1,
```

### Numerar os exemplos curtos

Quando a música são exemplos curtos citados no texto, um recurso `score` numerado serve melhor: [corais a quatro vozes como exemplos numerados](https://postext.dev/pt/cookbook/conservatory-method-chorales.md).

## Erros comuns

- As quebras de sistema escritas no arquivo (`<print new-system="yes">`) não são mantidas: os sistemas preenchem a largura da coluna, então ajuste `spacing` e `staffSize` até que tenham os compassos que você quer.
- O gravador não distribui os compassos entre os sistemas, e o último sistema é composto na sua largura natural: com 6 mm e `spacing: 1`, o compasso 32 fica sozinho num sistema. Mude `spacing` até que o último sistema tenha mais de um compasso.
- Um `.mxl` comprimido é um arquivo zip: descompacte-o, ou exporte o formato sem compressão, antes de baixá-lo.

## Créditos

- Receita: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Texto: The introduction and performance notes, written for this edition: Ignacio Ferro, CC-BY-4.0
- Imagens: Menuet in G major, BWV Anh. 114: the notes of the Bach-Gesellschaft edition as entered for the Mutopia Project (Mutopia-2017/01/19-75), converted to MusicXML 4.0 and proofread bar by bar for this edition: Christian Petzold; Allen Garvin (Mutopia Project) ([fonte](https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=75)), domínio público
- Imagens: Menuet in G minor, BWV Anh. 115: the notes of the Bach-Gesellschaft edition as entered for the Mutopia Project (Mutopia-2015/08/21-76), converted to MusicXML 4.0 and proofread bar by bar for this edition: Christian Petzold; Allen Garvin (Mutopia Project) ([fonte](https://www.mutopiaproject.org/cgibin/piece-info.cgi?id=76)), domínio público
- Tipos: Libre Caslon Text (OFL-1.1), Bodoni Moda (OFL-1.1), Archivo Narrow (OFL-1.1)
- Código: MIT · Conteúdo de exemplo: CC-BY-4.0

## Relacionadas

- [Nº 163 · Um manual de harmonia com exemplos musicais em ABC](https://postext.dev/pt/cookbook/harmony-textbook-abc-examples.md): 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. · Nível 2 (Intermediário) · Livros didáticos
- [Nº 164 · Um cancioneiro com melodia, letra e acordes](https://postext.dev/pt/cookbook/songbook-melody-words-chords.md): Cada canção é um bloco ABC: a melodia, as cifras sobre a pauta e a primeira estrofe sob as notas, gravadas na largura da coluna por um estilo de partitura. · Nível 2 (Intermediário) · Poesia, Folhas avulsas e impressos efêmeros
- [Nº 165 · Exemplos musicais em MEI com aparato crítico nas notas](https://postext.dev/pt/cookbook/musicology-mei-apparatus.md): Blocos MEI 5 gravados no texto numa pauta de 5 mm, variantes codificadas com app, lem e rdg, e o aparato por compasso e nota nas notas de rodapé. · Nível 2 (Intermediário) · Artigos e trabalhos acadêmicos
