subsequence.melodic_state¶
Persistent melodic context for NIR-guided single-note line generation.
Provides MelodicState, a stateful object that tracks recent pitch
history across bar rebuilds and applies the Narmour Implication-Realization
(NIR) model to score candidate pitches. Because pattern builders are
recreated fresh each cycle, this state must live at module level (the same
pattern as EasedValue) so melodic continuity
survives bar boundaries.
The NIR rules operate on absolute MIDI pitches (direct semitone subtraction), not pitch-class modular arithmetic, so registral direction is properly tracked across octaves: a leap from C4 (60) to G4 (67) is +7 upward, not an ambiguous -5.
Scoring follows the CHORAL separation: the hard constraint is structural
and singular (the pitch pool — candidates outside it never exist), while
everything tasteful is a soft factor in MelodicState.factors —
a pluggable list of multipliers (NIR expectation, chord-tone pull, range
gravity, pitch diversity, contour envelope, tessitura regression), every one
a dial and never a law. Replace or extend the list to reshape the
generator’s taste.
Module Contents¶
- class subsequence.melodic_state.MelodicState(key: str | None = None, mode: str | None = None, low: int = 48, high: int = 72, nir_strength: float = 0.5, chord_weight: float = 0.4, rest_probability: float = 0.0, pitch_diversity: float = 0.6, tessitura_strength: float = 0.0)[source]¶
Persistent melodic context that applies NIR scoring to single-note lines.
Initialise a melodic state for a given key, mode, and MIDI register.
- Parameters:
key – Root note of the key (e.g.
"C","F#","Bb"). When omitted, the state adopts the composition’s key the first timep.melody()uses it (falling back to"C").mode – Scale mode name. Accepts any mode registered with
scale_pitch_classes()(e.g."ionian","aeolian","dorian"). When omitted, adopts the composition’s scale (falling back to"ionian").low – Lowest MIDI note (inclusive) in the pitch pool.
high – Highest MIDI note (inclusive) in the pitch pool.
nir_strength – 0.0–1.0. Scales how strongly the NIR rules influence candidate scores. 0.0 = uniform; 1.0 = full boost.
chord_weight – 0.0–1.0. Additive multiplier bonus for candidates whose pitch class belongs to the current chord tones.
rest_probability – 0.0–1.0. Probability of producing a rest (returning
None) at any given step.pitch_diversity – 0.0–1.0. Exponential penalty per recent repetition of the same pitch. Lower values discourage repetition more aggressively.
tessitura_strength – 0.0–1.0. Regression pull toward the centre of the register after the line strays (off by default; the generate path enables it).
- choose_next(chord_tones: List[int] | None, rng: random.Random, beat: float | None = None, position: float | None = None, contour_target: float | None = None) int | None[source]¶
Score all pitch-pool candidates and return the chosen pitch, or None for a rest.
beat(the note’s beat within its cycle),position(0–1 through a generated span), andcontour_target(the envelope’s height there) thread caller context into the scoring factors.
- clone() MelodicState[source]¶
An independent copy — settings, factors, pool, and history.
Value constructors (
Motif.generate) copy the state they are given and walk the copy, so a module-level live state is never mutated by building a value.
- configure_defaults(key: str | None, mode: str | None) None[source]¶
Adopt the surrounding key/scale where this state left them unset.
Called by
p.melody()every build. It tracks the builder’s current key/scale (which is the section’s effective key under a form), so a state placed across sections follows each section’s key — its melodic history is untouched, only the pitch pool and tonic move. An explicit constructor key/scale or an explicit pool always wins and is never overridden.
- class subsequence.melodic_state.ScoringContext[source]¶
Everything a scoring factor may read about one candidate.
beat,position, andcontour_targetare optional threading from the caller —Nonewhen the context does not apply (a factor that needs them returns 1.0 without them).- low / high
The register bounds of the pitch pool.
- subsequence.melodic_state.chord_tone_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Boost candidates whose pitch class belongs to the current chord.
- subsequence.melodic_state.contour_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Pull candidates toward the contour envelope’s target height.
Active only when the caller threads
position/contour_target(the generate path); a melodic walk without an envelope is unshaped.
- subsequence.melodic_state.diversity_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Exponential penalty for recently-heard pitches.
- subsequence.melodic_state.nir_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Narmour expectation: reversal after leaps, continuation after steps, closure on the tonic, preference for proximity — scaled by
nir_strength.
- subsequence.melodic_state.range_gravity_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Penalise notes far from the centre of the register (quadratic).
- subsequence.melodic_state.tessitura_factor(state: MelodicState, ctx: ScoringContext) float[source]¶
Regression toward the tessitura — von Hippel’s reading of post-skip reversal.
The further the line has strayed from the register’s centre, the more candidates that move back toward it are boosted. Off by default (
tessitura_strength=0); the generate path turns it on, where it buys gap-fill and post-skip reversal without hard rules.