# @hiyve/react-music-theory

React hooks and components for music theory drills: a note, an interval, a chord, a key signature or a scale on the staff to name — or heard first and named by ear — the answers to choose from — or the answer played on an instrument — the question played once answered, and the score at the end. The questions come from `@hiyve/music-theory`; the staff is engraved by the notation engine you supply, as with `@hiyve/react-music-notation`.

## Installation

```bash
npm install @hiyve/react-music-theory @hiyve/music-theory @hiyve/react-music-notation
```

Peer dependencies: `react`, `react-dom`, `@mui/material`, `@mui/icons-material`, `@emotion/react`, `@emotion/styled`.

## Quick start

```tsx
import { useState } from 'react';
import { defaultTheoryDrillSettings, generateTheoryQuestion, newSeed } from '@hiyve/music-theory';
import { useDrill, DrillCard, DrillScoreCard, TheoryDrillPanel } from '@hiyve/react-music-theory';

const loadEngine = () => import('opensheetmusicdisplay');

function TheoryDrill() {
  const [settings, setSettings] = useState(defaultTheoryDrillSettings);
  const [length, setLength] = useState(10);
  const [seed, setSeed] = useState(() => newSeed());
  const drill = useDrill({ generate: (random) => generateTheoryQuestion(settings, random), length, seed });
  return (
    <>
      <TheoryDrillPanel settings={settings} onChange={setSettings} length={length} onLengthChange={setLength} onStart={() => setSeed(newSeed())} locked={!drill.finished && drill.summary.answered > 0} />
      {drill.round ? (
        <DrillCard round={drill.round} index={drill.index} total={drill.total} summary={drill.summary} loadEngine={loadEngine} onAnswer={drill.answer} onNext={drill.next} />
      ) : (
        <DrillScoreCard summary={drill.summary} onAgain={() => drill.restart()} onAnother={() => setSeed(newSeed())} />
      )}
    </>
  );
}
```

A drill is drawn from its seed, so "Try the same one again" asks the same questions and a new seed asks new ones. Each question is played as an answer is chosen — a chord at once, a melodic interval or a scale note by note — so it is heard as well as seen, and the card keeps the score as the drill goes. Answers show by name: "Major 3rd", "Half-diminished 7th", "Dorian", "E♭ major".

## By ear

Turn on the panel's **By ear** switch — or set `byEar: true` in a drill's settings — and every question of notes, intervals, chords and scales is heard before it is seen. `DrillCard` plays it as it appears, with the staff hidden and a button to hear it again; a note is played after a C, to name it from ("First you hear C, then the question"). Key signatures are read — there is nothing in a signature to hear — so the switch is not offered for them. Once answered, the staff shows and the question plays again, each note lit as it sounds. No two answers offered sound alike, so F♯ and G♭, or an augmented 4th and a diminished 5th, are never both there.

The first question plays as the card appears, after the click that started the drill; if the browser keeps the sound from starting, the "Hear it again" button plays it.

## Answered by playing

Turn on the panel's **Play the answer** switch — or set `playAnswer: true` in a drill's settings, for any kind — and each question is answered on an instrument instead of by choosing. `DrillCard` says what to play ("Play or sing this note. Then the Major 3rd above it.", "Play the Minor chord in first inversion, with this note at the bottom."), shows and sounds the note to start from — a note to read and play is shown alone, giving nothing away — and once answered shows the notes to play marked as a graded take's are — each coloured by how it was played, with the note played in a wrong one's place drawn beside it. With By ear on too, the question is heard and played back.

The listening itself is `PlayDrillCard`, from `@hiyve/react-music-performance/theory`: it hears the playing through the microphone or a MIDI instrument, with the same note following as a practice take, and judges it note by note. A card of your own can listen any other way through `DrillCard`'s `answerArea`, `noteStyles` and `onSoundingChange`; without an `answerArea`, a question set to be played is answered by choosing, like any other.

## Hooks

| Hook | Description |
|------|-------------|
| `useDrill({ generate, length?, seed? })` | A drill in a component: `drill`, `round`, `question`, `index`, `total`, `length` (the questions it was drawn with), `finished`, `summary`, and `answer(choice, correct?)`, `next()`, `finish()`, `restart({ seed?, length? })`. `finish()` ends the drill early, its summary of the rounds answered; `restart()` then asks all of it again. The drill starts anew whenever `seed` or `length` change; the time each answer takes is recorded with it. `correct` is for an answer judged elsewhere — one played on an instrument |
| `useNoteSynth({ gain?, durationMs?, samples?, sampleWaitMs?, onError? })` | Sound notes — on recorded samples when `samples` names a host, otherwise with the built-in voice: `play(midi \| midi[], durationMs?, listener?)` at once, `playInOrder(midis, stepMs?, durationMs?, listener?)` one after another, `preload(midis)`, `supported`, and `needsGesture`. `needsGesture` is true when the browser held the sound back until a tap, as iOS can, so nothing was heard; play again from a tap. Both plays return a function that stops them early; the optional `NoteSynthListener` (from `@hiyve/music-theory`) is told as each note starts and stops sounding, to light it on the staff. The sound starts on the first play — call it from a click, as browsers require — and stops when the component goes, after a ringing note finishes; without Web Audio nothing plays |

## Components

| Component | Description |
|-----------|-------------|
| `TheoryDrillPanel` | The settings of a drill: which drill — notes, intervals, chords, key signatures or scales — whether it is asked by ear (all but key signatures) and answered by playing, the clef, and that drill's own options, the choices, the questions, and a Start button |
| `TheoryDrillGuide` | What the exercise chosen in the panel is and how it goes — numbered steps, as By ear and Play the answer are set — in plain words for anyone new to music; with a Start button when `onStart` is given. Made for the space beside the panel before an exercise starts |
| `DrillCard` | One round: where it is in the drill and the score so far, the staff — or, asked by ear, the sound first and the staff once answered — the answers, right or wrong once answered — with the note played as the choice is made and a button to hear it again — and the button to the next round. Answered by playing: what to play, the note to start from, and a panel of yours that listens. When the staff cannot be drawn, a question read off it waits, with a button to try again; when the browser holds the sound back until a tap, a question asked by ear asks for one |
| `DrillScoreCard` | How the drill went: the share right as a ring with a word on it, the count, the time per answer, the questions missed, and buttons for the same drill again or another |
| `StaffSnippet` | A question's one-bar MusicXML, engraved and nothing else, centred in its box, with any of its notes lit. A staff that cannot be drawn says so, with a button to try again |
| `AnswerChoices` | The answers to choose from, as buttons, shown as `formatChoice` writes them; the right one shows green and a wrong choice red once answered |

### `TheoryDrillGuide`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `settings` | `TheoryDrillSettings` | **required** | The settings chosen in the panel: the guide follows them |
| `onStart` | `() => void` | — | Start the exercise; the button shows when given |
| `panelLabels` | `Partial<TheoryDrillPanelLabels>` | — | The panel's labels, when changed, so the guide names the exercise as the panel does |
| `sx`, `labels` | | | MUI sx; `Partial<TheoryDrillGuideLabels>` — the steps are one to a line |

```tsx
<TheoryDrillPanel settings={settings} onChange={setSettings} length={length} onLengthChange={setLength} onStart={start} />
{!started && <TheoryDrillGuide settings={settings} onStart={start} />}
```

The default words are short and plain, for anyone new to music: "Two notes. How far apart are they?", "Play or sing your answer." `drillHint(settings, labels)` is the one line the panel and the guide both show for what an exercise asks.

### `TheoryDrillPanel`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `settings` | `TheoryDrillSettings` | **required** | Every drill's settings and the drill asked, from `@hiyve/music-theory` (`defaultTheoryDrillSettings`) |
| `onChange` | `(next: TheoryDrillSettings) => void` | **required** | A setting changed. Each drill keeps its own settings; a change of clef, the By ear switch or the Play the answer switch moves them all (key signatures are always read). Answered by playing, there are no choices to count |
| `length` / `onLengthChange` | `number` / `(length) => void` | **required** | Questions in a drill |
| `lengthChoices` | `number[]` | `[5, 10, 20]` | Lengths offered |
| `onStart` | `() => void` | — | The Start button, shown when given |
| `locked` | `boolean` | `false` | A drill is running: the settings hold still |
| `names` | `TheoryNamesOverrides` | — | The names the intervals, chords and scales are listed by |
| `sx`, `labels` | | | MUI sx; `Partial<TheoryDrillPanelLabels>` |

Each drill's options: notes — on the staff or out to the ledger lines, sharps and flats; intervals — which intervals, together or one note after the other, sharps and flats; chords — which qualities, inversions, sharps and flats; key signatures — up to how many sharps or flats, major or minor keys or both; scales — which scales, sharps and flats. For intervals, chords and scales, "sharps and flats" lets the notes they are built on be sharps and flats too. A list always keeps at least one entry.

### `DrillCard`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `round` | `DrillRound` | **required** | The round being asked, from `useDrill` |
| `index` / `total` | `number` | **required** | Where the round is in the drill |
| `loadEngine` | `NotationEngineLoader` | **required** | Supplies the notation engine for the staff |
| `onAnswer` | `(choice: string, correct?: boolean) => void` | **required** | A choice was made — or an answer played, with whether it was right — as `useDrill`'s `answer` takes it |
| `onNext` | `() => void` | **required** | Move on |
| `onFinish` | `() => void` | — | End the drill early, keeping what was answered (`useDrill`'s `finish`): a Finish button beside the score, until the last round is answered |
| `summary` | `DrillSummary` | — | `useDrill`'s `summary`: given, the card shows the running count of right and wrong answers and a bar for how far through the drill it is |
| `sound` | `boolean` | `true` | Play the question as a choice is made, with a "Hear it again" button once answered. Each note lights up on the staff while it sounds — one after another for a scale or a melodic interval, together for a chord. A question asked by ear is also played as it appears, its staff hidden until answered; with `sound` off, or without Web Audio, it shows its staff like any other |
| `samples` | `SampleSource` | — | Play it on recorded samples — a piano, say — instead of the built-in voice; the question's notes download as it appears. See below |
| `onError` | `(error: Error) => void` | — | The staff failed to draw or the sound failed to start |
| `children` | `ReactNode` | — | Anything to show under the staff |
| `answerArea` | `ReactNode` | — | For a question answered by playing: shown in place of the answer buttons — a panel that listens and calls `onAnswer(played, correct)`. `PlayDrillCard` from `@hiyve/react-music-performance/theory` is one |
| `noteStyles` | `ReadonlyMap<string, MusicXmlNoteStyle>` | — | Answered by playing: how the notes to play were played, drawn on the staff once answered — the note styles `MusicXmlPlayer` takes, keyed by `musicXmlNoteKey`; a graded take's (`reportNoteStyles` from `@hiyve/react-music-performance`) show the note played in a wrong one's place beside it |
| `onSoundingChange` | `(sounding: boolean) => void` | — | Told as the card starts and stops sounding — the question, the note to start from — so a microphone can wait for it to fall quiet |
| `names` | `TheoryNamesOverrides` | — | The names answers are shown by |
| `sx`, `labels`, `colors` | | | MUI sx; `Partial<DrillCardLabels>`; `Partial<DrillColors>` |

### `DrillScoreCard`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `summary` | `DrillSummary` | **required** | From `useDrill` or `drillSummary` |
| `onAgain` / `onAnother` | `() => void` | — | The buttons, shown when given |
| `actions` | `ReactNode` | — | Buttons of your own in place of the usual row |
| `names` | `TheoryNamesOverrides` | — | The names the questions missed are listed by, each with its notes |
| `sx`, `labels`, `colors` | | | MUI sx; `Partial<DrillScoreCardLabels>`; `Partial<DrillColors>` |

`drillPraise(accuracy, labels)` and `drillTone(accuracy, colors)` give the word and the colour a score earns.

### `StaffSnippet`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `musicxml` | `string` | **required** | A question's `musicxml` |
| `loadEngine` | `NotationEngineLoader` | **required** | Supplies the notation engine |
| `height` | `number` | `176` | Height of the box the staff is centred in, in pixels |
| `zoom` | `number` | `1.1` | Size of the staff relative to the engine's own |
| `highlight` | `readonly number[]` | — | Notes to light, by their place in the question's `notes`: lowest first in a chord or harmonic interval, left to right otherwise |
| `highlightColor` | `string` | `'primary.main'` | Colour of a lit note: MUI palette path or CSS colour |
| `noteStyles` | `ReadonlyMap<string, MusicXmlNoteStyle>` | — | Note styles as `MusicXmlPlayer` takes them — a colour, and the note played in a wrong one's place drawn beside it — drawn by `@hiyve/react-music-notation`'s own note styling |
| `onError` | `(error: Error) => void` | — | The engine or the score failed to load |
| `onStatusChange` | `(status: StaffStatus) => void` | — | Told as the staff is drawn: `'drawing'`, then `'drawn'`, or `'failed'` when it cannot be. A failed staff says so in its box, with a button to try again |
| `labels` | `Partial<StaffSnippetLabels>` | — | `unavailable` and `retry` |

### `AnswerChoices`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `choices` | `readonly string[]` | **required** | As the question gives them |
| `chosen` | `string \| null` | `null` | The choice made |
| `answer` | `string \| null` | `null` | The right answer, once the round is answered |
| `onChoose` | `(choice: string) => void` | **required** | A choice was made |
| `disabled` | `boolean` | `false` | Nothing can be chosen |
| `formatChoice` | `(choice: string) => string` | — | How a choice is shown on its button |
| `sx`, `labels`, `colors` | | | MUI sx; `Partial<AnswerChoicesLabels>`; `right` and `wrong` |

## Piano sound

By default the notes play on a small built-in voice that needs no download. To hear a real piano, give `DrillCard` (or `useNoteSynth`) a sample host laid out like the `midi-js-soundfonts` collection:

```tsx
const PIANO = { baseUrl: 'https://cdn.example.com/soundfonts/FluidR3_GM/' };
<DrillCard samples={PIANO} … />
```

The SDK ships no samples: you choose the host, and that set's licence applies — FluidR3_GM, for one, is Creative Commons Attribution 3.0 and asks to be credited, while the University of Iowa Electronic Music Studios piano recordings may be used for any project without restrictions, once trimmed, encoded and hosted in this layout yourself. Each note is about 25 KB and is downloaded once, while its question is on screen. A note that cannot be fetched in time plays on the built-in voice, so a note always sounds. The layout and the `SampleSource` fields are described in the `@hiyve/music-theory` README.

## Names

Answers are codes (`M3`, `halfDiminished7`, `dorian`, `D:major`); the components show them by name. `defaultTheoryNames` holds the English names — `intervals`, `chords` and `scales` by code, and `keyMajor` / `keyMinor` templates with `{tonic}` — and `names` overrides any of them, name by name:

```tsx
<DrillCard names={{ intervals: { P8: 'Perfect 8th' }, keyMinor: '{tonic}m' }} … />
```

Notes are named by their letters ("F♯"). `names.note` names them another way, such as solfège, wherever a note is named: in the answers, the key names, the note to start from, and the questions missed:

```tsx
const SOLFEGE: Record<string, string> = { C: 'Do', D: 'Re', E: 'Mi', F: 'Fa', G: 'Sol', A: 'La', B: 'Si' };
<DrillCard names={{ note: (name) => SOLFEGE[name[0]] + name.slice(1) }} … />
```

For views of your own:

- `answerLabel(kind, answer, names)` gives the name of any answer.
- `noteLabel(pitch, names, octave?)` gives a note's name.
- `questionNotesText(question, names?)` gives the notes behind a question ("C4 E4", "D F A").
- `mergeTheoryNames(overrides?)` fills in the rest.

## Customization

Every component takes `labels`. The drill components also take `colors`, as MUI palette paths or CSS colours:

- `right`, `wrong`, `good`, `fair`, `poor`
- `playing`: a note on the staff while it sounds
- `staff`: behind the staff, which is drawn in black (default white)

The text on a right or wrong answer is picked to read on its colour. The defaults are exported (`defaultDrillCardLabels`, `defaultTheoryDrillPanelLabels`, `defaultDrillColors`, …) with merge functions (`mergeDrillCardLabels`, `mergeTheoryDrillPanelLabels`, `mergeTheoryDrillGuideLabels`, `mergeDrillColors`, …). `withClef(settings, clef)`, `withByEar(settings, byEar)`, `withPlayAnswer(settings, playAnswer)`, `noteIdRangeOf(settings)` and `withNoteIdRange(settings, clef, range)` are the helpers behind the panel's clef, by-ear, play and range choices.

## Requirements

- React 18 or 19, and MUI 9.
- A notation engine, supplied through `loadEngine`, as for `@hiyve/react-music-notation`'s player. The public `opensheetmusicdisplay` build is enough: the drills only draw, and play their notes with their own voice.
- Sound needs Web Audio; browsers start it only after a user gesture — choosing an answer is one, and so is the click that starts a drill, for a question asked by ear.
- With `samples`, the sample host must allow your page to fetch from it (CORS) and be allowed by your `connect-src`.
