@@ -0,0 +1,81 @@
|
||||
"""Immutable source events, meter boundaries and tempo. No solo construction."""
|
||||
from __future__ import annotations
|
||||
import copy
|
||||
import dataclasses
|
||||
import fractions
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import generator.model
|
||||
import generator.score
|
||||
import generator.solo.types
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Analysis:
|
||||
boundaries: tuple
|
||||
tempo: fractions.Fraction
|
||||
melody: tuple
|
||||
body_offset: fractions.Fraction = fractions.Fraction()
|
||||
|
||||
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):
|
||||
measures = generator.score.source_staff(document)['measures']
|
||||
boundaries = [fractions.Fraction()]
|
||||
for m in measures:
|
||||
boundaries.append(boundaries[-1] + generator.model.fraction(m['duration']))
|
||||
tempo = next((generator.model.fraction(t['bpm']) * generator.model.written_length(t['written']) for m in measures for t in m.get('tempos', []) if 'bpm' in t), fractions.Fraction(120))
|
||||
return Analysis(tuple(boundaries), tempo, attacks(document), boundaries[generator.score.body_start_measure_index(measures)])
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SourceAnalysis:
|
||||
document_json: str
|
||||
musical_key: str
|
||||
@classmethod
|
||||
def read(cls, document):
|
||||
prepared = copy.deepcopy(document)
|
||||
staff = generator.score.source_staff(prepared)
|
||||
for i, m in enumerate(staff['measures']):
|
||||
m['id'], m['number'] = f'source_{i}', str(i + 1)
|
||||
staff['tuplets'] = generator.score.reidentify(staff['measures'], staff.get('tuplets', []))
|
||||
prepared['metadata'] = {k: v for k, v in prepared['metadata'].items() if k not in ('title', 'composer', 'copyright', 'software')}
|
||||
prepared['metadata'].update(title='Source', composer='')
|
||||
staff.update(id='source', name='Source')
|
||||
melodic = analyze(prepared)
|
||||
content = {'melody': [(str(n.start), str(n.end), dataclasses.asdict(n.pitch)) for n in melodic.melody],
|
||||
'measures': [{k: m.get(k) for k in ('duration', 'time_signature', 'key', 'harmonies', 'tempos', 'section_label', 'implicit')} for m in staff['measures']]}
|
||||
# Fraction/string spelling is canonical and never includes filesystem IDs.
|
||||
key = hashlib.sha256(json.dumps(content, sort_keys=True, default=str).encode()).hexdigest()
|
||||
return cls(json.dumps(prepared, sort_keys=True), key)
|
||||
def document(self):
|
||||
return json.loads(self.document_json)
|
||||
def accompaniment_key(self):
|
||||
document = self.document()
|
||||
document['metadata'].pop('tempo', None)
|
||||
for measure in generator.score.source_staff(document)['measures']:
|
||||
measure['tempos'] = []
|
||||
return SourceAnalysis.read(document).musical_key
|
||||
Reference in New Issue
Block a user