Skip to content

Generation Pipeline ​

When you call generator.generate(config), the composer engine runs a fixed pipeline that turns the request into a complete, deterministic composition.

Musical vocabulary in this pipeline

The pipeline treats music as structured data: forms allocate voices and authored note carriers, harmony supplies chord targets, and validation rejects illegal voice interactions. The compact glossary is in Music Primer for Engineers.

The seven steps of a generate() callA config enters at the top left and runs through seven fixed steps: compose request, form director, candidate search, rule validator, renderer, final validation and expression, and MIDI and event export. The finalization step applies ornaments, validates FinalScore and the form budget, applies velocity, saves the notated snapshot, applies articulation, re-renders, then adds CC and tempo. The row wraps from the third step down to the fourth. A side branch shows that a blocking validation failure aborts the run and returns the report instead of producing output.BachConfigform, key, isMinor, character,bpm, seed, scale / targetBars1 · Compose Requestdefaults filled in, the seedresolved, the request validated2 · Form Directorvoice intents laid out over barspans, following the design arc3 · Candidate Searchevery span dispatched; shippedforms replay authored carriers4 · Rule Validatorcounterpoint and structurefailures accumulated with spans5 · Initial Renderervoices become tracks, one pervoice, with channel and program6 · Finalize & Expressornament → FinalScore + budgetvelocity → notated → articulationre-render → CC / tempo7 · MIDI & Event Exportthe output key applied, then aType 1 SMF and event dataa blocking failure aborts and returns the reportgetMidi() · getEvents()With a fixed non-zero seed, the same config follows this path to identical bytes; seed 0 resolves a new seed.

The whole pipeline is deterministic for an explicit non-zero seed: the same config and seed yield byte-identical output. With seed: 0, the request asks the runtime to choose a new seed.

Step 1: Compose Request ​

The raw config is resolved into a compose request. Defaults are filled in, the seed is resolved (a seed of 0 becomes a concrete non-zero value, reported via getInfo().seedUsed), and the request is validated.

Validation is strict: unknown form/character/instrument/scale values and out-of-range bpm are rejected, as are forbidden character/form pairs (chorale_prelude rejects playful/restless; toccata_and_fugue rejects noble). The form fixes the voice count, meter, and natural length; scale/targetBars resolve the final bar count (snapped to the form's granularity and clamped to [min, 128]).

Step 2: Form Director ​

The form director lays out the piece. For the chosen form it assigns voice intents to bar spans — subjects and answers, ground basses, cantus firmus, figuration, variation material — across the resolved length.

Bar span and material

A bar span is a range of measures. Material is authored musical content, such as a fugue subject, a counterline, or a repeating ground bass, that a carrier replays verbatim.

The four layout familiesFour rows, each showing one layout family as three voice lanes across the bars of a piece. Fugal layouts stagger theme entries and put episodes between them. Ground-bass layouts repeat one frozen bass under upper voices that are rewritten each cycle. Cantus firmus layouts hold long fixed tones in the middle with busy figuration above and an independent bass below. Linear layouts trade material between two upper voices over a continuously running line.theme entry or variation materiala line the form freezesepisode, figuration, free materialFugalfugue, prelude_and_fugue,toccata_and_fugue, fantasia_and_fuguestaggered entries, episodes between themGround-bass variationpassacaglia, chaconne,goldberg_variationsone frozen bass, upper voices rewrittenCantus firmuschorale_preludelong fixed tones in the middle, withfiguration above and its own bass belowLinear / figuraltrio_sonata, cello_preludetwo voices trading over a running line;the cello prelude does it on one voiceThe form picks the family and the family fixes the shape — which is why the voice count, the meter and the natural length are not options.

The layout follows a design-value arc — establish, develop, climax at roughly 80% of the span, then resolve — that drives the density, register, and velocity tiers used downstream. The arc is fixed by the form, not searched.

Candidate search dispatches every voice-intent span. In all default shipped forms, those spans are carriers: they replay the notes assembled by the form builder verbatim. This carrier-assembly path produces every default note; the scored search branch contributes zero.

The scored branch is an off-by-default diagnostic option. bach_cli --free-counterpoint reroutes only the Passacaglia V1 (voice == 1) counterline to per-beat, chord-tone-anchored search. The ground and principal variation remain carriers. No other form has an eligible span, so requesting free counterpoint for another form returns FreeCounterpointUnavailable.

When a free Compose span exhausts every candidate at a position, the engine emits a rest and increments saturated_positions; it does not choose a fallback pitch. Saturation is a density signal, not itself a validation failure. Carrier spans never saturate.

Chords, modulation, cadences

Chords are the vertical targets at each point in time. Modulation means moving the tonal center to another key. A cadence is a phrase ending, usually a dominant-to-tonic arrival such as V to I.

Step 4: Rule Validator ​

The validator checks the assembled voices against counterpoint and structure rules. It accumulates the failures found during the pass and reports each with a rule identifier so the responsible span can be located. Learn the musical ideas in Counterpoint Course, then use the Validator Rule Reference when you need to look up a specific rule ID.

What happens after a blocking generation failure

The public generation path aborts and returns the validation report. It does not repair the score, retry the search, or choose a different candidate. The later FinalScore audit has its own failure point after ornaments are applied.

Step 5: Renderer ​

As the final part of Composer::run(), the assembled voices are rendered into initial tracks — one track per voice — with channels and note timings. After the public FinalScore and budget checks succeed, the generation path applies the instrument-specific velocity curve, captures the notated notes, applies character-dependent articulation, and re-renders the tracks with the General MIDI program.

Source tags survive rendering

Rendered notes keep their provenance: "material" for carrier material, "compose" for notes from the opt-in scored search, and "ornament" for notes added later. This is useful when locating the source of a validation finding.

Step 6: Finalize & Express (post-pass) ​

The public generation path runs these operations in order after Composer::run(): apply ornaments, run FinalScore validation, apply the form's counterpoint budget, apply the velocity curve, save the notated note snapshot for generated.v1, apply articulation, re-render the tracks, add controller events, add tempo events, then export MIDI and public event data. A blocking FinalScore or closed-budget failure aborts before velocity, articulation, controller, tempo, MIDI, or public event output.

  • Ornaments — trills (including cadential openings and a closing Nachschlag), appoggiaturas, turns, slides, and mordents, with density depending on character and instrument. Subject, answer, stretto, augmented, diminished, and inverted subject statements remain plain so their identity survives; middle entries remain eligible. Ground carriers are exempt. Cantus-firmus bar heads remain plain, while eligible within-bar notes can be decorated except under Severe, which keeps the whole cantus line plain.
  • Velocity — an instrument-aware phrase curve applied after final validation; the tracks are re-rendered so MIDI and getEvents() expose the updated values.
  • Expression — MIDI CC 7 level terraces and arc points for organ and harpsichord, and CC 11 continuous expression for piano, violin, and cello. These are playback level/expression events; CC 7 does not select organ stops, and the guitar profile receives the velocity curve without a continuous-expression profile.
  • Tempo — a form-dependent closing ritardando (none for the Trio Sonata) and, for the prelude, toccata, and fantasia forms, a section tempo change at the fugue entry.

Only notes inserted by the ornament pass carry the source: "ornament" provenance tag. Velocity and articulation modify existing notes without changing their source, while CC and tempo passes add events rather than notes.

Validation uses notated durations

FinalScore validation and its texture metrics run before articulation, on the notated note array. generated.v1 preserves those notated durations; the public event stream and MIDI use the shortened articulation gates.

Ornament

An ornament is a small decorative figure added around a structural note. It is not the source melody itself, so event data marks it separately with source: "ornament".

Step 7: MIDI Writer ​

The internal representation is composed and validated in C. The output writers apply the requested key and any output-octave shift. The MIDI writer also writes time signatures (3/4 for passacaglia and chaconne, 4/4 otherwise) and produces a Type 1 Standard MIDI File. The public event writer applies the same pitch transformation.

js
const midi = generator.getMidi()       // Uint8Array (transposed to your key)
const events = generator.getEvents()   // Event data (pitches in the output key)

TIP

getEvents() reports output-key pitches and tags every note with its source. The lower-level generated.v1 diagnostic artifact keeps internal C pitches. See the JavaScript API for the public event type definitions.

Beyond the pipeline: how quality is measured ​

The seven steps above are everything that happens at runtime. By default, the pipeline never searches for a "more Bach-like" candidate while generating: the form builder authors the carrier material, and the validator rejects illegal results. So how do we know those design values actually produce Bach-like output?

The answer is development-side quality gates. Whenever a form or figuration in the engine changes, the generated output is run through two families of automated checks. Both ship as public Python tooling in the engine repository (the texture-gate and closure commands of bach_tools.py).

The corpus statistics model ​

Five distributions are extracted from a generated piece's event data and scored by their distance (negative log-likelihood) from reference distributions estimated over a corpus of real works. A distance beyond a calibrated threshold fails the gate.

FeatureWhat it captures
Pitch classHow often each note name is used
Melodic intervalThe distribution of distances between adjacent notes within a voice
DurationThe distribution of note lengths
Beat positionWhere in the bar notes are attacked
Vertical interval classThe distribution of intervals between simultaneously sounding notes

The most heavily weighted of these is the melodic interval. In Bach's actual writing the dominant melodic interval is overwhelmingly the step (a move of one or two semitones to a neighboring scale tone — see the primer), so a leap-heavy line widens this distance immediately.

The tension between horizontal steps and vertical consonance ​

This is where the core tension of figuration design lives.

  • Horizontally: a line matches the corpus better the more stepwise it is — scalar runs, wave figures.
  • Vertically: beat onsets sound consonant against a sustained bass the more they are anchored to the bar's chord tones (see also Leaps need recovery).

Implemented naively, the two collide: a freely running scale line steps onto non-chord tones at beat onsets, while jumping every beat to the nearest chord tone produces a leap-riddled line. The engine's figures (scalar wave, sawtooth, broken chord) are written to keep both — landing on chord tones that lie ahead in the running direction, and choosing oscillation partners from bass-consonant neighbor tones before resorting to a leap.

Stepwise motion against vertical consonanceThree panels, each showing eight eighth-notes over one sustained bass. In the first, a freely running scale steps smoothly but two of its beat onsets land off the chord. In the second, every beat onset is a chord tone but the line has to leap to reach them. In the third, the line steps toward the chord tone that lies ahead in the running direction, so it is both stepwise and consonant on the beat.Each panel shows eight eighth-notes over one sustained bass — the band at the foot of the panel.The dashed lines are that bass's chord tones; the red rings and segments mark what breaks.a freely running scalematches the corpus horizontally,but steps onto non-chord tonestwo beat onsets land off the chorda chord tone on every beatvertically clean, but the line hasto jump to reach each oneevery beat arrives by a leapwhat the engine's figures dostep toward the chord tone thatlies ahead in the running directionstepwise, and consonant on every beatThe corpus model weights the melodic interval most heavily, so a leap-heavy line fails it — but so does a line that keeps stepping onto dissonance.

Texture gates ​

Alongside the statistics model, a seed × form sweep checks structural metrics directly — per-voice activity, silence ratios, repeated-note run lengths, parallel perfect intervals, and the model score threshold above. If a single case fails, the change does not merge.

These are not part of generate()

Neither the corpus model nor the texture gates run at runtime. Generation is always a deterministic single shot; quality is guaranteed on the side that produces it — in the design values, at development time.

Dual-licensed: AGPL-3.0 · commercial licensing available. Generated MIDI is yours to use freely.