@@ -1,13 +1,14 @@
|
||||
"""Typed, target-driven harmonic expression against written harmony and explicit musical relationships."""
|
||||
from __future__ import annotations
|
||||
import dataclasses
|
||||
import functools
|
||||
import fractions
|
||||
import functools
|
||||
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.pitch
|
||||
import generator.vocabulary
|
||||
import generator.solo.types
|
||||
import generator.vocabulary
|
||||
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def parsed_chord(symbol):
|
||||
@@ -90,8 +91,9 @@ def _eligible_color(rule, span, harmony):
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate(notes, contracts, harmony):
|
||||
def validate(notes, contracts, harmony, *, composition=None):
|
||||
"""Return event permissions only after checking complete actual gestures."""
|
||||
composition = composition or generator.vocabulary.current()
|
||||
actual = {n.event_id: n for n in notes}
|
||||
approved = {}
|
||||
|
||||
@@ -115,7 +117,7 @@ def validate(notes, contracts, harmony):
|
||||
if not previous or previous.chord.strip('()') == span.chord.strip('()'):
|
||||
return {}, c.id + ': anticipation requires a harmonic change'
|
||||
if c.kind in ('approach', 'enclosure', 'connection'):
|
||||
rule = next((r for r in generator.vocabulary.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None)
|
||||
rule = next((r for r in composition.data('lead_ins')['patterns'] if r['id'] == c.rule and r['kind'] == c.kind), None)
|
||||
if not rule or len(c.events) != len(rule['semitones']):
|
||||
return {}, c.id + ': unknown approach formula'
|
||||
arrival = c.boundary if c.boundary is not None else target.start
|
||||
@@ -129,7 +131,7 @@ def validate(notes, contracts, harmony):
|
||||
return {}, c.id + ': invalid internal approach gaps'
|
||||
gap = fractions.Fraction(rule['gap'])
|
||||
cursor = target.start - gap - sum((fractions.Fraction(d) for d in rule['durations']), fractions.Fraction()) - sum(gaps, fractions.Fraction())
|
||||
if target.start - cursor > fractions.Fraction(generator.vocabulary.data('lead_ins')['max_approach']):
|
||||
if target.start - cursor > fractions.Fraction(composition.data('lead_ins')['max_approach']):
|
||||
return {}, c.id + ': approach too long'
|
||||
for i, (n, step, duration) in enumerate(zip(c.events, rule['semitones'], rule['durations'])):
|
||||
if n.start != cursor or n.end != cursor + fractions.Fraction(duration) or n.pitch.number() != target.pitch.number() + step:
|
||||
@@ -144,7 +146,7 @@ def validate(notes, contracts, harmony):
|
||||
if c.kind == 'connection' and (not c.preparation or c.preparation.end != c.events[0].start or abs(c.preparation.pitch.number() - c.events[0].pitch.number()) > 2):
|
||||
return {}, c.id + ': chromatic connection lacks its incoming motion'
|
||||
elif c.kind == 'passing':
|
||||
rule = generator.vocabulary.data('lead_ins')['passing']
|
||||
rule = composition.data('lead_ins')['passing']
|
||||
if c.rule != rule['id'] or len(c.events) != 1 or not c.preparation:
|
||||
return {}, c.id + ': unsupported passing figure'
|
||||
n, = c.events
|
||||
@@ -160,7 +162,7 @@ def validate(notes, contracts, harmony):
|
||||
if not supported(before) or not supported(target):
|
||||
return {}, c.id + ': unsupported passing endpoints'
|
||||
elif c.kind == 'delayed_approach':
|
||||
rule = generator.vocabulary.data('lead_ins').get('delayed_approach', {})
|
||||
rule = composition.data('lead_ins').get('delayed_approach', {})
|
||||
if c.rule != rule.get('id') or len(c.events) != 1:
|
||||
return {}, c.id + ': unknown delayed approach'
|
||||
n, = c.events
|
||||
@@ -171,7 +173,7 @@ def validate(notes, contracts, harmony):
|
||||
if target.pitch.number() % 12 not in harmony.members(target.start, target.end) or target.pitch.number() % 12 not in harmony.permitted(target.start, target.end):
|
||||
return {}, c.id + ': invalid delayed destination'
|
||||
elif c.kind == 'color':
|
||||
rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None)
|
||||
rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None)
|
||||
if not rule:
|
||||
return {}, c.id + ': unknown color policy'
|
||||
colored = False
|
||||
@@ -193,10 +195,10 @@ def validate(notes, contracts, harmony):
|
||||
return {}, c.id + ': no interval expresses the declared color'
|
||||
last = c.events[-1]
|
||||
if rule.get('resolve'):
|
||||
if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end):
|
||||
if target.event_id == last.event_id or not 0 <= target.start - last.end <= fractions.Fraction(composition.data('harmony')['resolution_gap']) or not 0 < abs(target.pitch.number() - last.pitch.number()) <= 2 or target.pitch.number() % 12 not in harmony.members(target.start, target.end):
|
||||
return {}, c.id + ': unresolved color'
|
||||
elif c.kind == 'suspension':
|
||||
rule = next((r for r in generator.vocabulary.data('harmony').get('suspensions', []) if r['id'] == c.rule), None)
|
||||
rule = next((r for r in composition.data('harmony').get('suspensions', []) if r['id'] == c.rule), None)
|
||||
if not rule or len(c.events) != 1 or c.boundary is None:
|
||||
return {}, c.id + ': unsupported suspension'
|
||||
held, = c.events
|
||||
@@ -211,23 +213,23 @@ def validate(notes, contracts, harmony):
|
||||
if held.pitch.number() % 12 not in harmony.permitted(held.start, held.end):
|
||||
return {}, c.id + ': suspension outside source-key/chord motion'
|
||||
elif c.kind == 'anticipated_color':
|
||||
rule = next((r for r in generator.vocabulary.data('harmony')['colors'] if r['id'] == c.rule), None)
|
||||
rule = next((r for r in composition.data('harmony')['colors'] if r['id'] == c.rule), None)
|
||||
if not rule or len(c.events) != 1 or c.boundary != span.start:
|
||||
return {}, c.id + ': unsupported anticipated color'
|
||||
n, = c.events
|
||||
if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']):
|
||||
if not n.start < c.boundary < n.end or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']):
|
||||
return {}, c.id + ': invalid anticipated color interval'
|
||||
arrived = dataclasses.replace(n, start=c.boundary)
|
||||
target = arrived if c.target == n else c.target
|
||||
inner = generator.solo.types.ExpressionContract(c.id + ':arrived', 'color', rule['id'], (arrived,), target, span.chord)
|
||||
_, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony)
|
||||
_, error = validate((arrived, *((target,) if target != arrived else ())), (inner,), harmony, composition=composition)
|
||||
if error:
|
||||
return {}, error
|
||||
elif c.kind == 'anticipation':
|
||||
if c.rule not in ('early_chord_tone', 'early_reattack') or len(c.events) != 1 or span.start != c.boundary:
|
||||
return {}, c.id + ': unknown anticipation or non-boundary target'
|
||||
n, = c.events
|
||||
if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']):
|
||||
if c.boundary is None or not n.start < c.boundary or c.boundary - n.start > fractions.Fraction(composition.data('harmony')['anticipation_max']):
|
||||
return {}, c.id + ': invalid anticipation interval'
|
||||
if c.rule == 'early_chord_tone' and (n != target or not c.boundary < n.end):
|
||||
return {}, c.id + ': invalid held anticipation'
|
||||
@@ -238,7 +240,7 @@ def validate(notes, contracts, harmony):
|
||||
elif c.kind == 'blues':
|
||||
if len(c.events) != 1:
|
||||
return {}, c.id + ': blues treatment requires one inflected event'
|
||||
rule = next((r for r in generator.vocabulary.data('lead_ins')['blues'] if r['id'] == c.rule), None)
|
||||
rule = next((r for r in composition.data('lead_ins')['blues'] if r['id'] == c.rule), None)
|
||||
n, = c.events
|
||||
chord = parsed_chord(span.chord)
|
||||
if not rule or n.end - n.start > fractions.Fraction(1, 2) or n.end != target.start or target.pitch.number() % 12 not in span.roles.basic:
|
||||
@@ -262,15 +264,16 @@ def validate(notes, contracts, harmony):
|
||||
approved[n.event_id] = c
|
||||
return approved, ''
|
||||
|
||||
def validate_frontier(notes, contracts, harmony, until, complete):
|
||||
def validate_frontier(notes, contracts, harmony, until, complete, *, composition=None):
|
||||
"""Validate heard prefixes and bounded future obligations separately.
|
||||
|
||||
Future contract events are used ONLY to establish rule feasibility. They do
|
||||
not count as heard resolution; the contract stays pending until its entire
|
||||
target hold is present. Missing past events and expired deadlines fail.
|
||||
"""
|
||||
composition = composition or generator.vocabulary.current()
|
||||
if complete:
|
||||
approved, error = validate(notes, contracts, harmony)
|
||||
approved, error = validate(notes, contracts, harmony, composition=composition)
|
||||
return approved, (), error
|
||||
actual = {n.event_id: n for n in notes}
|
||||
future = dict(actual)
|
||||
@@ -290,5 +293,5 @@ def validate_frontier(notes, contracts, harmony, until, complete):
|
||||
return {}, (), c.id + ': missing/changed event before resolution deadline'
|
||||
if incomplete:
|
||||
pending.append(c)
|
||||
approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony)
|
||||
approved, error = validate(tuple(sorted(future.values(), key=lambda n: (n.start, n.event_id))), contracts, harmony, composition=composition)
|
||||
return {key: c for key, c in approved.items() if key in actual}, tuple(pending), error
|
||||
|
||||
Reference in New Issue
Block a user