# harmony

A deterministic music engine that **cannot play a wrong note**.

Give it a seed. It gives you a piece of music: tempo, key, an eight-bar phrase structure, a
memorable two-bar motif that gets developed and carried through every key change, a bass
line, a voice-led four-part pad, an arpeggio, a drum kit with fills, and key changes that a
music student could analyse. Same seed, same music, forever, on every machine.

The unusual part is the guarantee. Every pitch that leaves this library has been authorised
by a `HarmonyContext` — a snapshot of the key and chord sounding at that instant — and
carries the id of the context that authorised it. Not "we check afterwards". There is no way
to construct a playable note except through the gate.

- **No dependencies.** Not one, not even for tests.
- **No build step.** Plain ES modules. The source is what ships.
- **Runs unchanged** in Node 22 and in a browser via `<script type="module">`.
- **No `Math.random`, no `Date.now()`**, no iteration over unordered collections.

---

## Install

Copy the folder.

```
cp -r harmony/ your-project/harmony/
```

That is the whole installation. There is no package to add, no bundler to configure and
nothing to keep up to date.

```js
import { planComposition, renderBars } from './harmony/index.js';
```

---

## Thirty seconds

```js
import { planComposition, renderBars } from './harmony/index.js';

const song = planComposition('any string or number');

console.log(song.homeKey.name, song.bpm);          // "A aeolian" 128
console.log(song.sections[0].progression.map(c => c.roman).join(' '));
                                                   // "i VII VI V"

const notes = renderBars(song, 0, 8);              // every note of the first eight bars
notes.slice(0, 3).forEach(n => console.log(n));
// { midi: 45, beat: 0, dur: 1, vel: 0.75, layer: 'bass', voice: 0,
//   role: 'chordTone', ctxId: 'A aeolian|i@b0:0', resolvesTo: null, ... }
```

Every note is `{midi, beat, dur, vel, layer, voice, role, ctxId}`. Time is in **beats**;
seconds only exist at the audio boundary. Pitch is a MIDI number; middle C is 60.

Play it however you like — the library has no opinion. If you want the batteries included:

```js
import { loadEngine } from './harmony/index.js';
const { createEngine } = await loadEngine();       // Web Audio, browser
const engine = createEngine(new AudioContext());
engine.loadComposition(song);
engine.start();
engine.gesture('COLLECT', { intensity: 0.8, streak: 3 });
```

---

## Why every note is in key

There is exactly one function in the library that can build a note:

```js
emit(ctx, { midi, beat, dur, vel, layer, voice, role })   // → Note, or throws
```

`ctx` is a `HarmonyContext`: one key, one chord, at one moment. `role` is the caller's
justification, and there are only four:

| role | promise | example |
|---|---|---|
| `chordTone` | the pitch is in the chord sounding now | the bass playing a root |
| `scaleTone` | the pitch is in the key, but not the chord | a quick passing note |
| `tension` | a colour the chord explicitly permits | the 9th on top of an arpeggio |
| `approach` | a chromatic note that has **already declared where it resolves** | a bass slide |

`emit` refuses anything else. A chromatic pitch is only reachable as an `approach`, and an
approach must name the chord tone it steps into, one or two semitones away — so there is no
API through which a wrong note can be requested, let alone played. Approach tones are
created by `emitApproachPair`, which returns the chromatic note *and its resolution
together*: a dangling chromatic note is not a state this library can represent.

The rule is written **once**, in `key.js`:

```js
checkNote(ctx, note) → Violation[]      // [] means legal
```

`emit()` calls it and throws. The offline verifier imports the same function. Two copies of
a rule is a lie waiting to happen, so there is one copy, and the thing that runs in your
speakers is checked by the same predicate that the test suite runs over thousands of seeds.

There is a second, harder check for what happens **vertically** — everything sounding at
once:

```js
checkVertical(ctx, soundingNotes) → Violation[]
```

which catches the failures a note-by-note check cannot see: a sustained note that has
survived into the next chord, two long notes grinding a semitone apart, voices crossing
inside a layer.

And because being right is not the same as being *provably* right, `naive.js` ships the
counter-example: the same piece, same rhythms, same instruments, same velocities, same drum
fills — with the pitches chosen at random. It exists so the test suite can assert that the
verifier **fails** on it. A check that cannot fail proves nothing.

---

## What it actually composes

This is not a random walk through a scale. In order:

**Form.** Eight-bar phrases built 4 + 4, as question and answer: the antecedent ends
hanging on the dominant (a half cadence), the consequent answers it with a real ending.
Sections are 8 or 16 bars, never odd lengths. Every three sections is a *tier*, and every
tier begins with a modulation.

**Harmony.** Progressions come from a library of real grammars — `I V vi IV`, `i VII VI VII`,
`ii V I`, the Andalusian `i VII VI V`, twelve-bar shapes — chosen by mode and energy, then
decorated with at most one secondary dominant and one borrowed chord per phrase. Harmonic
rhythm is one chord per bar, two bars per chord when the music is calm, and half a bar at
cadences so the phrase leans forward into its ending.

**A motif, developed.** One two-bar cell per run: four to seven notes, a clear contour, one
leap. It is stored as **scale degrees and relative rhythm, never as pitches**, which is what
lets it survive a key change intact — degree 4 is the fifth of whatever key you are in now.
The lead never plays anything else: every bar of melody is that cell as a statement, a
sequence up a third, an inversion, a rhythmic augmentation, or a fragment of its last three
notes. That is why you can still hum the tune after the music has moved to a new key.

**Voice leading.** The pad's four voices move as little as possible between chords, hold
common tones, never cross, and never make parallel fifths or octaves between the outer
voices. The same engine voices the boundary at a key change, where the contract is tighter:
every voice moves two semitones or fewer, four semitones of total motion across all four.

**Modulation as difficulty.** The tempo is **fixed for the whole run** — the player is timing
their hands against the beat, and moving it would punish them for having learned it. The
music gets harder by changing key and by getting denser. Every modulation keeps at least one
common tone and is set up by a pivot chord that is diatonic in both keys; the library will
tell you its roman numeral in each:

```js
song.sections[3].modulation.describe
// "A aeolian pivots to E aeolian via iv in A aeolian, which is VII in E aeolian.
//  6 of 7 notes stay the same; the pad voices move 3 semitones in total, none more than 1."
```

**Arrangement.** Layers enter in a fixed order as energy rises — bass and pad, hats, kick,
arp, the tune, then the full backbeat — because that order is what a listener perceives as a
build. Phrases end with drum fills. The bar before a big lift has the drums **removed**, and
that silence is the loudest thing in the piece.

---

## API

Import everything from `harmony/index.js`.

### Planning

```js
planComposition(seed, opts?) → Composition
```

`seed` is a number or a string. `opts` may set `{ bars, bpm, mode, tonicPc, key }`; all are
optional. Returns:

```js
Composition = {
  seed, version, bpm, beatsPerBar: 4, meter: [4,4],
  homeKey,          // { tonicPc, mode, pcs, name }
  motif,            // scale degrees + rhythm, the tune's DNA
  sections,         // Section[]: key, energy, tier, progression, cadence, modulation
  bars,             // Bar[]: flat index; each has its chord and HarmonyContext
  totalBars, groove, palette,
  extend(toBar),    // deterministic: a 40-minute run's first 8 minutes are unchanged
  contextById(id), barAt(i), sectionAt(barIndex), secondsPerBeat
}
```

`palette` is derived from musical facts only (mode brightness → hue band, tonic's position
on the circle of fifths → hue rotation, energy → saturation and bloom). The library does not
know it is describing colour; it is describing the music, and something else may choose to
paint with it.

### Rendering

```js
renderBars(composition, fromBar, toBar, opts?) → Note[]
renderSection(composition, sectionIndex, opts?) → Note[]
describeBar(composition, barIndex) → { layers, chords, fill, dropout, leadCell, ... }
```

`opts.layers` restricts to `'bass' | 'pad' | 'arp' | 'lead' | 'perc'`.

`renderBars` is **pure with respect to the range**: bars 12–14 render identically whether
you asked for them alone or as part of the whole piece. A scheduler can therefore fetch two
bars at a time from wherever the playhead happens to be.

Percussion notes carry `layer:'perc'`, `drum:'kick'|'snare'|'clap'|'hatClosed'|…` and
**no pitch at all** — which is a fact the verifier checks rather than an assumption it makes.

### Reactive events

The caller never asks for a pitch. It reports an abstract musical gesture and the library
decides what that means in the key and chord playing right now:

```js
gestureNotes(ctx, 'ATTACK' | 'LAND' | 'GRAZE' | 'COLLECT' | 'FAIL' | 'MILESTONE',
             { intensity, streak, beat, rng }) → Note[]
```

`streak` climbs the chord, so five near-misses in a row walk upward instead of repeating one
ping. `FAIL` is a soft descending cadence onto the root, not a buzzer — it stays in key, and
it is a sound you can hear three hundred times.

### The counter-example

```js
makeNaiveComposer(seed, { mode: 'chromatic' | 'staticScale' }) → { notes(bar) }
naiveStats(composition, naive, fromBar, toBar) → { outOfKeyRatio, outOfChordRatio, ... }
```

Identical rhythm, instrumentation and dynamics; only the pitches are wrong. `chromatic`
picks uniformly from all twelve pitch classes. `staticScale` snaps everything to one scale
forever, ignoring the chords and every key change — the more instructive failure, because it
sounds plausible for eight bars and unmistakably broken by the first modulation.

---

## Determinism

Same seed in, byte-identical `Composition` out, on any platform. The random number generator
is sfc32 seeded through splitmix32, with all arithmetic kept inside 32 bits so no engine's
floating point can disagree. Streams are **forked by label**:

```js
const rng = makeRng(seed);
const a = rng.fork('motif');     // independent of...
const b = rng.fork('groove');    // ...this one, and of the order they were created in
```

`fork()` does not advance the parent, which is a contract, not an implementation detail:
adding a new fork label later can never change the output of the existing ones. That is what
lets two subsystems — the music and, say, a level generator — both be functions of one seed
without either being able to disturb the other.

---

## Layout

| file | what lives there |
|---|---|
| `rng.js` | deterministic streams, forkable by label |
| `theory.js` | pitch classes, modes, chords, roman numerals. Pure, no state |
| `key.js` | **the legality oracle.** `emit`, `checkNote`, `checkVertical` |
| `voicing.js` | the voice-leading engine, shared by the pad and by modulation |
| `progression.js` | functional harmony: grammars, cadences, secondary dominants |
| `modulation.js` | key changes with pivots, common tones and voice-led boundaries |
| `composition.js` | the plan: tempo, form, motif, energy curve, groove, palette |
| `arrangement.js` | the plan turned into notes: bass, pad, arp, lead, drums |
| `events.js` | reactive gestures |
| `naive.js` | the honest counter-example |
| `index.js` | the public API |

`scheduler.js`, `synth.js` and `offline.js` make sound and are loaded on demand via
`loadEngine()` and `loadOffline()`, so a consumer that only wants to plan chords never pays
for an audio graph.

## Licence

Ship it.
