harmony/ · a deterministic music engine

A synth that cannot
play a wrong note

Most generative music picks a pitch and hopes. This engine cannot pick a pitch at all without first saying why that pitch is allowed — and the answer is checked before the note exists. Not afterwards. There is no unchecked note to check.

Nothing plays until you press this. One click, and the engine starts composing in real time.

key
chord
bar
seed

One

What a wrong note actually sounds like

Here are the same eight bars, twice. Same seed. Same tempo, same drum pattern, same bass rhythm, same chord progression underneath, same note lengths, same velocities, same instruments, same everything. One difference: how each pitch was chosen.

Start it, let a bar go by, then hit the other button. The switch happens mid-phrase on purpose.

Broken how:
  • chord tone
  • scale tone — hollow, a note passing through
  • tension the chord allows
  • approach, with a line to what it resolves to
  • outside the key — nothing legal can sit here
Each lit row is one of the seven notes of the key. The brighter rows inside a chord's column are that chord's own notes. Legal notes land on a stripe.
Press a button to start.

The broken version is not a strawman, and the page would be worthless if it were. harmony/naive.js does not compose anything. It asks the real arranger for the bar, then rewrites the pitch of each note and keeps everything else: same onsets, same durations, same velocities, same layers, same drum fills. Same note count, note for note. If the broken side also had worse rhythm you would be judging the rhythm.

The only difference between these two is which notes were allowed.

Look at the roll while you switch. In-key notes sit on the lit stripes. The random ones land in the dark gaps between stripes — those gaps are the five pitches that are not in this key — and they land there at the exact moment your ear flinches. You do not have to read music to see it. You have to be able to see a stripe.

The second failure mode is the more instructive one, and it is the mistake real generative music actually makes. Choose one scale forever. Now every note belongs to some key; it just is not this key. It sounds plausible for eight bars, faintly sour for eight more, and by the first key change it is unmistakably wrong while never once playing a note you could call random.

Two

Twelve notes, and why seven of them are yours

A key is a set of seven pitch classes out of twelve. A chord is three or four of those seven. That is the entire idea, and this wheel is it: the ring is the twelve, the lit ones are the key, the filled ones are the chord sounding right now.

Point at a note to see what it is in this key. Choose one to hear it against the chord that is playing.

The dashed lines are the only route the five unlit notes have: an approach tone one or two semitones from a chord tone, which must resolve onto it.

Choose an unlit note and you will hear the problem instantly — not as a rule violation, as a sound. That note was not refused by taste. It was refused by emit(), which is what we look at next.

Three

The gate

Everything so far is a claim about behaviour. Here is the mechanism, and it is small enough to read in one sitting. This listing is fetched from ../harmony/key.js when the page loads — it is not a copy, and if the engine changed, this would change with it.

harmony/key.js emit()
loading the real source…

There is no other constructor of a playable note anywhere in the library. Not a private one, not a fast path, not a "just this once" in the drum code. Every layer — bass, pad, arpeggio, lead, and the reactive notes the game fires when you jump — comes through this function, and the function will not build a note unless the caller states a role: a reason the pitch is allowed.

Four roles, four promises

chordTone

The note is in the chord sounding right now. Always safe, any length. This is most of the music: of every pitched note in the last verification run.

scaleTone

In the key but not in the chord. Fine passing through; a long one is how amateur generative music gives itself away, so the vertical check polices those.

tension

A colour the chord explicitly permits — a 9th, a 6th, an 11th on a minor chord. The permitted set is computed by the avoid-note rule, so a tension can never form a semitone with a chord tone.

approach

The only route to a pitch outside the key, and it is a loan. You must name the chord tone it resolves to, one or two semitones away, and the resolution has to arrive inside the note's own duration.

Roles are not labels stuck on afterwards. They are the argument the caller has to make, and checkNote() is the referee — the function emit() just called. This is the whole of it, fetched from the same file:

harmony/key.js checkNote()
loading the real source…

The offline verifier imports that same function. So does the live check in the proof section below, running in your browser. Two copies of a rule is a lie waiting to happen; there is exactly one copy, and everything that claims to enforce it is calling it.

Hear a role

Pick a role and a scale degree and hear it against whatever chord is currently sounding. The list tells you what each degree could legally be right now, and it changes when the chord does. Degrees, not pitches: degree 4 means the fourth note of this key, so the same choice keeps meaning the same musical thing after a key change — which is exactly why the tune survives a modulation.

Choose a role and press Emit it.

Being honest about the other five notes

It would be tidy to say this music only ever uses seven pitches. It is not true, and a page that said so would be teaching something false. The engine plays notes outside the seven-note scale in two clearly-bounded ways, and both of them are declared.

Borrowed and applied chords bring their own notes. When the progression reaches for a chord from outside the key — a V chord in a minor key, borrowed from harmonic minor; a bVII; a secondary dominant like V7/vi — the chord's own tones become legal for as long as that chord is sounding. That is not a loophole. In the engine's own words, the chord is the local authority: allowedPcs = scale ∪ chord ∪ tensions. A borrowed chord whose notes were forbidden would not be a borrowed chord, it would be a mistake.

loading…

Approach tones are the only other route, and they are the reason the bass can walk chromatically without the door being open to anything else. An approach tone is not granted, it is loaned: emit() refuses one unless you name the chord tone it resolves to, and the API for making one does not exist on its own — you ask for emitApproachPair() and get the chromatic note and its resolution, or you get an exception. An unresolved chromatic note is not a bug this library can have, because there is no code path that produces one.

In the last full verification run, chromatic approach tones were of every pitched note, and checkResolutions() found zero that failed to land.

Try to break it

Ask for a pitch directly. Type a note name and a role. If the request is legal you hear it; if it is not, the engine throws a real HarmonyViolation and the message below is the actual one, thrown by the actual gate.

> waiting

Four

Legal is not the same as good

If the argument stopped at the gate it would be a bad argument, because staying in key is the floor and not the ceiling. Here are three versions of the same eight bars, in the same key, over the same chords. Every note in all three is legal. Only one of them is music.

  1. Pitches chosen at random from whatever the chord permits, on a flat rhythm. Every one of these went through emit(). It is correct, and it is wallpaper. There is no shape, so there is nothing to remember.

  2. The same bars with the engine's real lead line. One two-bar idea, stated, answered, and developed. Antecedent and consequent: the first phrase asks, the second replies and closes. Suddenly there is a tune to hum.

  3. Drums, arpeggio, voice-led pad, dynamics, dropouts and a fill before the phrase turns. Same notes in the tune. This is the difference between notes and a soundtrack, and none of it is harmony.

Choose a clip. The roll shows what you are hearing.

What the tune is actually doing

A motif is not repeated, it is developed, and the engine plans that development two bars at a time. This is the real plan for the eight bars above, read out of describeBar() — not an illustration of one. The cell that is sounding lights up while a clip plays.

Six moves is the entire vocabulary, and it is the vocabulary a first-year composition class is taught: state it, answer it upside down, push the same shape up a third, stretch it to half speed, break off its tail and repeat that, and know when to shut up. Change the seed and the plan above changes with it. None of this is harmony — the gate in section three has no opinion about any of it. All of it is the difference between a tune and a stream of correct notes.

One more thing makes it survive the rest of the piece. The motif is generated once per seed and then transposed by scale degree, never by semitones — so when the key changes, "up a third" still means up a third of the new key and the tune arrives intact instead of smearing. Contour preserved, degrees preserved. That is the difference between a soundtrack and a note generator that happens to be well behaved.

Five

Changing key without a seam

Difficulty in the game does not rise by speeding the music up. It rises by modulating — moving the whole piece to a new key. Do that badly and it sounds like someone bumped the record player. Do it the way music does it and most listeners will not be able to tell you the moment it happened, only that something lifted.

Two things make it seamless, and you can hear both.

loading…

Four pad voices across the boundary. Every voice moves two semitones or less; the notes shared by both keys are held, not struck again.

Same chords, same rhythm, same bars. Voice-led is what the engine ships. Jump cut re-voices every chord from its root, so the four pad voices leap instead of stepping.

The first mechanism is the pivot chord: one chord that is a legitimate member of both keys at once. The music arrives on it belonging to the old key and leaves it belonging to the new one, so there is no moment you can point at. The second is voice leading: the pad's four voices are moved to their nearest legal positions in the new chord rather than re-stacked from the root, so every voice steps by a semitone or a tone and anything shared between the two chords is simply held.

Those are not stylistic preferences here, they are checked. Across modulations in the last verification run, the worst single voice movement anywhere was semitones.

Six

One seed, two views

This is the part that is not really about music. The level and the song are not synchronised — synchronising implies two things. They are one thing, read twice.

A seed produces a Composition: tempo, key, sections, chord progression, energy curve. The arranger turns that into notes. The level generator reads the same object — the same bars, the same energy, the same accents — and places obstacles on beat subdivisions. It never invents a tempo or a structure, because it has no way to. Change the seed and both change together, and they cannot drift, because there is nothing to drift apart from.

bar 0

Above: the level. Solid bars are platforms and blocks, triangles are spikes, the slabs hanging from the top are ceilings, and a break in the ground line is a pit. Below: the music, on the same axis. The faint vertical lines are obstacle onsets, drawn through both. Scrub to move the pair together and hear the bar you are pointing at. An obstacle sitting above a note is not a drawing trick — the two were placed by the same number.

Two consequences fall straight out of this. A run is shareable as a single small string, because the string is the level and the song. And the game can hand the player's own actions back as music: when you jump, the engine is not told to play a pitch, it is told ATTACK — an abstract musical gesture — and harmony decides what that means in the key that happens to be sounding.

Be precise about that claim, because this page invites you to check it. grep -ri '\bjump\b' harmony/ returns 21 hits, so "the word never appears" would be false. Every one of them is in a comment, explaining which game idea maps onto which gesture. No branch, no condition and no value in the library is computed from a game concept. The gesture vocabulary is ATTACK · LAND · GRAZE · COLLECT · FAIL · MILESTONE, and a rhythm game, a card game or a text editor could drive all six. That is the claim worth making, and it survives reading the source. The weaker, tidier version does not.

Seven

The proof

Everything above is a claim. This is the part that makes it checkable, and the numbers on this page are read out of docs/verification.json, which is written by the test runner. Nobody typed them in.

seeds planned and rendered
notes put through the checker
sixteenths checked vertically
violations

What the checker actually checks

  1. Every note names its judge. A note carries the id of the HarmonyContext that authorised it, and that context must be the one that really governs the moment the note starts — so a note cannot shop around for a chord that happens to contain its pitch.
  2. Every note satisfies its role. chordTone in the chord, scaleTone in the key, tension in the chord's permitted set, approach chromatic and resolving by one or two semitones onto a chord tone.
  3. Every sixteenth is checked vertically. Everything sounding at that instant, together: all pitches legal in the context, no sustained semitone grinding (except the sanctioned root-against-major-7th), no voice crossing inside a layer.
  4. Every chromatic note lands. A separate pass proves each approach tone is actually followed by the resolution it promised, in the same layer and voice, inside its own duration.

Run it yourself, here, now

This does not ask you to trust the number above. The button below plans fresh seeds the test suite has never seen, renders them, and runs checkNote, checkVertical and checkResolutions — imported from harmony/key.js, the same functions emit() itself calls — in a worker on your machine.

> idle

A check that cannot fail proves nothing

That is the whole reason the second button exists, and the reason harmony/naive.js ships inside the library rather than living in a test folder. A verifier that always returns all clear is indistinguishable from a verifier with the body deleted. So the test suite does not merely check that the engine passes; it asserts that the same checker rejects the deliberately broken composer, and it fails the build if it ever stops rejecting it.

of naive notes rejected by checkNote
still illegal after being handed the friendliest role their pitch could claim
landed outside the chord that was sounding

The first number is total because a naive note carries no authorisation at all — it is thrown out before its pitch is even looked at. The second is the honest musical figure: hand each naive pitch the most generous role it could possibly claim, let it be a tension if that helps, let it be a scale tone, and of them still have no defence in any key or chord. That is the number the page quotes, and it is the one the second button reproduces live.

The other seven proofs in the suite

Eight

Take it

harmony/ is standalone. It has no dependencies, no build step and no framework. It runs unchanged in Node 22 and in a browser as a plain module. Installation is copying the folder.

twelve lines that make a sound

The surface you would actually use:

planComposition(seed, opts?)
A whole piece as plain data: tempo, home key, sections, chords, motif, groove, palette. Same seed, byte-identical result, every time, on every platform. Extends forever with composition.extend(toBar).
renderBars(comp, from, to)
Every note of a bar range, as {midi, beat, dur, vel, layer, voice, role, ctxId}. Pure with respect to the range: bars 12–14 render the same whether you asked for them alone or as part of the whole piece.
emit(ctx, spec)
The gate. The only constructor of a playable note in the library.
checkNote(ctx, note) / checkVertical / checkResolutions
The same predicates emit() uses, exposed so you can verify a stream you did not generate.
gestureNotes(ctx, gesture, opts)
Reactive music. You say ATTACK, LAND, GRAZE, COLLECT, FAIL, MILESTONE; harmony decides what those mean in the current key. It has no idea what your application is.
createEngine(audioContext)
Batteries included: a 25 ms lookahead scheduler and a Web Audio synth with ADSR on every voice, per-voice delay and reverb sends, and sidechain ducking. Loaded on demand so a consumer that only wants chord data never pulls in audio.
renderToBuffer(comp, opts)
The same music rendered headlessly in pure JavaScript, no Web Audio — for tests, for CI, for writing a WAV from a script.
makeNaiveComposer(seed, {mode})
The counter-example, shipped on purpose. Same arrangement, broken pitch selection. Keep your verifier honest.

The idea underneath all of it is not musical, and it is not new. Do not validate values; make the invalid ones unrepresentable. Wrong notes are not filtered out of this engine's output. There is no way to build one, so there is nothing to filter.