@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
import hashlib
|
||||
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.score
|
||||
import generator.solo.types
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Analysis:
|
||||
motifs: tuple[generator.solo.types.Motif, ...]
|
||||
boundaries: tuple[fractions.Fraction, ...]
|
||||
tempo: fractions.Fraction
|
||||
melody: tuple[generator.solo.types.Note, ...]
|
||||
|
||||
def attacks(document: dict) -> tuple[generator.solo.types.Note, ...]:
|
||||
"""Collapse tied tones before extracting monophonic melody fragments."""
|
||||
result, pending = [], {}
|
||||
absolute = fractions.Fraction()
|
||||
for measure in generator.score.source_staff(document)['measures']:
|
||||
for event in generator.score.measure_notes(measure):
|
||||
if event.get('grace') or event.get('cue'):
|
||||
continue
|
||||
for tone in event['pitches']:
|
||||
number = generator.model.pitch_number(tone)
|
||||
if number.denominator != 1:
|
||||
continue
|
||||
start = absolute + generator.model.fraction(event['onset'])
|
||||
end = start + generator.model.fraction(tone['duration'])
|
||||
pitch = generator.solo.types.Pitch(tone['letter'], generator.model.alteration(tone['alter']), tone['octave'])
|
||||
source = pending.pop(tone.get('tie_from'), None)
|
||||
if source is not None:
|
||||
result[source] = dataclasses.replace(result[source], end=end, sources=(*result[source].sources, event['id']))
|
||||
else:
|
||||
source = len(result)
|
||||
result.append(generator.solo.types.Note(start, end, pitch, (event['id'],)))
|
||||
if 'tie_to' in tone:
|
||||
pending[tone['id']] = source
|
||||
absolute += generator.model.fraction(measure['duration'])
|
||||
return tuple(sorted(result, key=lambda n: (n.start, -n.pitch.number(), n.sources)))
|
||||
|
||||
def analyze(document: dict) -> Analysis:
|
||||
measures = generator.score.source_staff(document)['measures']
|
||||
melody = attacks(document)
|
||||
boundaries = [fractions.Fraction()]
|
||||
for measure in measures:
|
||||
boundaries.append(boundaries[-1] + generator.model.fraction(measure['duration']))
|
||||
tempo = next((generator.model.fraction(t['bpm']) * generator.model.written_length(t['written']) for m in measures for t in m.get('tempos', [])), fractions.Fraction(120))
|
||||
candidates = []
|
||||
for index, measure in enumerate(measures):
|
||||
start = boundaries[index]
|
||||
# One- and two-bar ideas retain leading silence and the complete held ending.
|
||||
for count in (1, 2):
|
||||
if index + count >= len(boundaries):
|
||||
continue
|
||||
end = boundaries[index + count]
|
||||
notes = tuple(n for n in melody if start <= n.start < end)
|
||||
if not notes or any(n.end > end for n in notes) or any(a.end > b.start for a, b in zip(notes, notes[1:])):
|
||||
continue
|
||||
if len(notes) > 8:
|
||||
continue
|
||||
normalized = tuple(dataclasses.replace(n, start=n.start - start, end=n.end - start) for n in notes)
|
||||
identity = repr(tuple((n.start, n.end, n.pitch) for n in normalized)).encode('ascii')
|
||||
identifier = 'head_' + hashlib.sha256(identity).hexdigest()[:12]
|
||||
context = tuple(generator.harmony.symbol(h) for m in measures[index:index + count] for h in m['harmonies'])
|
||||
candidates.append(generator.solo.types.Motif(identifier, normalized, end - start, context))
|
||||
unique = {m.id: m for m in reversed(candidates)}
|
||||
# Preserve the opening idea, then favor a contrasting rhythm/contour with space.
|
||||
ordered = list(unique.values())
|
||||
ordered.sort(key=lambda m: (next(i for i, n in enumerate(melody) if n.sources[0] == m.notes[0].sources[0]), m.duration))
|
||||
return Analysis(tuple(ordered), tuple(boundaries), tempo, melody)
|
||||
Reference in New Issue
Block a user