@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
import typing
|
||||
|
||||
import generator.arrange.ending_plan
|
||||
import generator.arrange.pickup
|
||||
import generator.model
|
||||
import generator.score
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -55,3 +59,57 @@ def full_arrangement_sections(measures: list[dict], part_type: str) -> list[Arra
|
||||
for label, start, count, index in specs
|
||||
if count > 0
|
||||
]
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FormPlan:
|
||||
sections: tuple[ArrangementSection, ...]
|
||||
windows: tuple[tuple[fractions.Fraction, fractions.Fraction, str], ...]
|
||||
stops: tuple[tuple[int, fractions.Fraction] | None, ...]
|
||||
@classmethod
|
||||
def read(cls, document, part):
|
||||
source = generator.score.source_staff(document)['measures']
|
||||
sections = tuple(full_arrangement_sections(source, part))
|
||||
stops = tuple(section_stop(source, section, sections[i + 1] if i + 1 < len(sections) else None) for i, section in enumerate(sections))
|
||||
return cls(sections, _windows(document, part, sections), stops)
|
||||
|
||||
def section_stop(source, section, following):
|
||||
if following is None or not generator.score.body_start_measure_index(source):
|
||||
return None
|
||||
last = section.source_measure_index + section.measure_count - 1
|
||||
pad = generator.arrange.pickup.offset(source[last], source[0])
|
||||
if following.role == 'slash':
|
||||
return last, pad
|
||||
if following.role == 'melody' and generator.arrange.pickup.enters(following, source):
|
||||
attacks = [generator.model.fraction(note['onset']) for note in generator.score.measure_notes(source[0]) if note['pitches']]
|
||||
if attacks:
|
||||
return last, pad + min(attacks)
|
||||
return None
|
||||
|
||||
def windows(document, part):
|
||||
return FormPlan.read(document, part).windows
|
||||
|
||||
def _windows(document, part, sections):
|
||||
source = generator.score.source_staff(document)['measures']
|
||||
result, cursor = [], fractions.Fraction()
|
||||
for section in sections:
|
||||
duration = sum((generator.model.fraction(m['duration']) for m in source[section.source_measure_index:section.source_measure_index + section.measure_count]), fractions.Fraction())
|
||||
role = {'slash': 'solo', 'vocal': 'melody'}.get(section.role, section.role)
|
||||
if section.role in ('vocal', 'melody') and generator.arrange.pickup.enters(section, source) and result:
|
||||
attacks = [generator.model.fraction(n['onset']) for n in generator.score.measure_notes(source[0]) if n['pitches']]
|
||||
if attacks:
|
||||
start = cursor - generator.model.fraction(source[0]['duration']) + min(attacks)
|
||||
a, _, old = result[-1]
|
||||
result[-1] = (a, start, old)
|
||||
result.append((start, cursor, part + '_' + role))
|
||||
end = cursor + duration
|
||||
start = cursor
|
||||
if section.role == 'slash' and generator.score.body_start_measure_index(source) and result:
|
||||
# Allocate the source's complete pickup bar before composition. This
|
||||
# does not inspect either generated solo; a role may leave it silent.
|
||||
start = cursor - generator.model.fraction(source[0]['duration'])
|
||||
a, _, old = result[-1]
|
||||
result[-1] = (a, start, old)
|
||||
result.append((start, end, part + '_' + role))
|
||||
cursor = end
|
||||
result.append((cursor, cursor + generator.arrange.ending_plan.duration(source), part + ('_accompaniment' if part != 'vocal' else '_rest')))
|
||||
return tuple(result)
|
||||
|
||||
Reference in New Issue
Block a user