Soloing
This commit is contained in:
@@ -92,9 +92,65 @@ class Harmony:
|
||||
if self.step(a, b, note.start) and self.step(b, c, note.start):
|
||||
return 'neighbor' if a == c else 'passing' if (b - a) * (c - b) > 0 else ''
|
||||
return ''
|
||||
def evaluate(self, notes, treatments=(), *, until=None, complete=False):
|
||||
def enclosure_events(self, notes, enclosures, until=None):
|
||||
"""Validate whole gestures before granting any exception; never trust labels."""
|
||||
notes = generator.solo.types.sounding_notes(notes)
|
||||
events = {}
|
||||
for note in notes:
|
||||
if note.event_id:
|
||||
if note.event_id in events:
|
||||
return {}, 'duplicate sounding event ID in enclosure context'
|
||||
events[note.event_id] = note
|
||||
accepted, intervals = {}, []
|
||||
for contract in enclosures:
|
||||
upper, lower, target = contract.upper, contract.lower, contract.target
|
||||
if len({n.event_id for n in (upper, lower, target)}) != 3 or not all(n.event_id for n in (upper, lower, target)):
|
||||
return {}, contract.id + ': enclosure requires three distinct event IDs'
|
||||
for expected in (upper, lower, target):
|
||||
actual = events.get(expected.event_id)
|
||||
if not actual or (actual.start, actual.end, actual.pitch) != (expected.start, expected.end, expected.pitch):
|
||||
return {}, contract.id + ': missing or changed enclosure event ' + expected.event_id
|
||||
if upper.end-upper.start != fractions.Fraction(1,2) or lower.end-lower.start != fractions.Fraction(1,2) or upper.end != lower.start or lower.end != target.start:
|
||||
return {}, contract.id + ': enclosure must be two contiguous eighths immediately before its target'
|
||||
if not 0 < upper.pitch.number()-target.pitch.number() <= 3 or lower.pitch.number() != target.pitch.number()-1:
|
||||
return {}, contract.id + ': enclosure does not surround the exact target register'
|
||||
if any(n.start < target.start and n.end > upper.start and n.event_id not in (upper.event_id,lower.event_id) for n in notes):
|
||||
return {}, contract.id + ': interrupted or overlapping enclosure'
|
||||
if any(a < target.start and b > upper.start for a,b in intervals):
|
||||
return {}, contract.id + ': overlapping enclosure contracts'
|
||||
if until is not None and target.end > until:
|
||||
return {}, contract.id + ': enclosure resolution exceeds the available arrangement'
|
||||
span = self.at(target.start)
|
||||
tail = contract.protected_tail
|
||||
if not span or span.chord != contract.chord or target.pitch.number()%12 not in self.members(target.start,target.end):
|
||||
return {}, contract.id + ': target is not a chord member throughout its hold'
|
||||
if contract.target_role not in ('intermediate','terminal') or tail.pitch != target.pitch.number() or not target.start <= tail.start < tail.end <= target.end:
|
||||
return {}, contract.id + ': invalid protected destination tail'
|
||||
for note in (upper, lower, target):
|
||||
covered=note.start
|
||||
for region,start,end in self.segments(note):
|
||||
if start != covered or not region.scale:
|
||||
return {}, contract.id + ': missing harmony in enclosure'
|
||||
if note is not lower and note.pitch.number()%12 not in region.scale.pitches:
|
||||
return {}, contract.id + ': upper approach or target outside mapped collection'
|
||||
covered=end
|
||||
if covered != note.end:
|
||||
return {}, contract.id + ': incomplete harmony in enclosure'
|
||||
# The upper neighbor is the nearest common mapped pitch above T.
|
||||
allowed=generator.solo.types.permitted(self.choices,upper.start,upper.end)
|
||||
if any(p%12 in allowed for p in range(target.pitch.number()+1,upper.pitch.number())):
|
||||
return {}, contract.id + ': upper approach is not the nearest mapped neighbor'
|
||||
accepted[lower.event_id]=contract
|
||||
accepted[upper.event_id]=contract
|
||||
intervals.append((upper.start,target.start))
|
||||
return accepted, ''
|
||||
|
||||
def evaluate(self, notes, treatments=(), *, until=None, complete=False, enclosures=()):
|
||||
notes = generator.solo.types.sounding_notes(notes)
|
||||
until = until if until is not None else max((n.end for n in notes), default=fractions.Fraction())
|
||||
ornaments, error = self.enclosure_events(notes, enclosures, until) if enclosures else ({}, '')
|
||||
if error:
|
||||
return Result(False, 0, error=error)
|
||||
pending, valid_contracts = [], []
|
||||
for treatment in treatments:
|
||||
if treatment.start >= until:
|
||||
@@ -130,11 +186,13 @@ class Harmony:
|
||||
covered = note.start
|
||||
for span, start, end in self.segments(note):
|
||||
contract = next((t for t in valid_contracts if t.pitch == number and t.start <= start and t.end >= end), None)
|
||||
if start > covered or not span.scale or number % 12 not in span.scale.pitches:
|
||||
enclosure = ornaments.get(note.event_id)
|
||||
exception = enclosure is not None and note.event_id == enclosure.lower.event_id
|
||||
if start > covered or not span.scale or number % 12 not in span.scale.pitches and not exception:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {start}-{end}: outside mapped collection or missing harmony')
|
||||
covered = end
|
||||
function = self.function(number, span)
|
||||
if function == 'basic' or note.role == 'quote':
|
||||
if function == 'basic' or note.role == 'quote' or enclosure:
|
||||
continue
|
||||
if duration >= generator.solo.policy.DEFAULT.long_note and contract is None:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {start}-{end} over {span.chord}: sustained {function} without a planned treatment')
|
||||
@@ -144,13 +202,16 @@ class Harmony:
|
||||
if covered < note.end:
|
||||
return Result(False, 0, error=f'{note.pitch.label()} at {covered}: missing harmonic coverage')
|
||||
return Result(True, exposed / max(sounding, 0.001), tuple(pending))
|
||||
def describe(self, notes, treatments=()):
|
||||
def describe(self, notes, treatments=(), enclosures=()):
|
||||
notes = generator.solo.types.sounding_notes(notes)
|
||||
ornaments, error = self.enclosure_events(notes,enclosures) if enclosures else ({}, '')
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
result = []
|
||||
for index, note in enumerate(notes):
|
||||
for span, start, end in self.segments(note):
|
||||
contracts = [t for t in treatments if t.pitch == note.pitch.number() and t.start <= start and t.end >= end]
|
||||
result.append({'start': str(start), 'end': str(end), 'attack': str(note.start), 'sounding_duration': str(note.end - note.start), 'pitch': note.pitch.label(), 'chord': span.chord, 'function': self.function(note.pitch.number(), span), 'motion': self.connected_motion(notes, index), 'protected': note.role == 'quote', 'treatments': [t.id for t in contracts]})
|
||||
result.append({'start': str(start), 'end': str(end), 'attack': str(note.start), 'sounding_duration': str(note.end - note.start), 'pitch': note.pitch.label(), 'chord': span.chord, 'function': self.function(note.pitch.number(), span), 'motion': self.connected_motion(notes, index), 'enclosure': ornaments[note.event_id].id if note.event_id in ornaments else '', 'scale_exception': note.event_id in ornaments and self.function(note.pitch.number(), span) == 'outside_collection', 'protected': note.role == 'quote', 'treatments': [t.id for t in contracts]})
|
||||
return result
|
||||
|
||||
def anchor_note(anchor, notes, *, until=None):
|
||||
|
||||
Reference in New Issue
Block a user