@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import fractions
|
||||
|
||||
import generator.model
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SoundingTone:
|
||||
pitch: dict
|
||||
start: fractions.Fraction
|
||||
end: fractions.Fraction
|
||||
|
||||
def written_durations(length: fractions.Fraction):
|
||||
while length:
|
||||
written = generator.model.written_for(length)
|
||||
duration = length
|
||||
# Binary spans such as five eighths need ties, not an invented tuplet.
|
||||
if 'ratio' in written and length.denominator & (length.denominator - 1) == 0:
|
||||
duration = next((size for size in generator.model.TYPE_LENGTHS.values() if size < length), length)
|
||||
written = generator.model.written_for(duration)
|
||||
yield duration, written
|
||||
length -= duration
|
||||
|
||||
def combine(document: dict) -> None:
|
||||
"""Consolidate generated acoustic lanes without changing their sounding notes."""
|
||||
staff = document['staves'][0]
|
||||
if staff.get('arrangement_part_type') != 'acoustic':
|
||||
return
|
||||
measures, sounds, roots = [], {}, {}
|
||||
absolute = fractions.Fraction()
|
||||
for measure in staff['measures']:
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
if {voice['id'] for voice in measure['voices']} == {'bass', 'upper'}:
|
||||
members = set()
|
||||
for voice in measure['voices']:
|
||||
for note in voice['notes']:
|
||||
start = absolute + generator.model.fraction(note['onset'])
|
||||
for pitch in note['pitches']:
|
||||
end = start + generator.model.fraction(pitch['duration'])
|
||||
if source := pitch.get('tie_from'):
|
||||
root = roots[source]
|
||||
sounds[root].end = end
|
||||
else:
|
||||
root = pitch['id']
|
||||
sounds[root] = SoundingTone(pitch, start, end)
|
||||
roots[pitch['id']] = root
|
||||
members.add(root)
|
||||
measures.append((measure, absolute, members))
|
||||
absolute += length
|
||||
previous = {}
|
||||
for measure, absolute, members in measures:
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
spans = [(root, max(fractions.Fraction(), sounds[root].start - absolute), min(length, sounds[root].end - absolute)) for root in sorted(members)]
|
||||
boundaries = sorted({fractions.Fraction(), length} | {time for _, start, end in spans for time in (start, end)})
|
||||
notes = []
|
||||
for start, end in zip(boundaries, boundaries[1:]):
|
||||
active = sorted((root for root, low, high in spans if low <= start and end <= high), key=lambda root: (generator.model.pitch_number(sounds[root].pitch), sounds[root].pitch['tablature']['string'], root))
|
||||
for duration, written in written_durations(end - start):
|
||||
pitches = [{key: value for key, value in sounds[root].pitch.items() if key not in ('tie_from', 'tie_to', 'stem')} for root in active]
|
||||
note = generator.model.make_event(f'{measure["id"]}-acoustic-{len(notes) + 1}', start, duration, 'note' if active else 'rest', pitches, written)
|
||||
note['voice'] = 'voice-arranged'
|
||||
for root, pitch in zip(active, note['pitches']):
|
||||
if root in previous:
|
||||
previous[root]['tie_to'], pitch['tie_from'] = pitch['id'], previous[root]['id']
|
||||
previous[root] = pitch
|
||||
notes.append(note)
|
||||
start += duration
|
||||
if not members and length == generator.model.meter_length(measure['meter']):
|
||||
notes = [generator.model.make_event(f'{measure["id"]}-acoustic-1', fractions.Fraction(), length, 'rest', written={'type': 'whole', 'dots': 0}) | {'voice': 'voice-arranged', 'measure_rest': True}]
|
||||
measure['voices'] = [{'id': 'voice-arranged', 'notes': notes}]
|
||||
@@ -5,6 +5,7 @@ import fractions
|
||||
import pathlib
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.acoustic
|
||||
import generator.arrange.form
|
||||
import generator.guitar
|
||||
import generator.melody
|
||||
@@ -118,6 +119,7 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
|
||||
generator.guitar.place_guitar(result)
|
||||
if generator.accompaniment.connect_continuations(result):
|
||||
generator.guitar.place_guitar(result)
|
||||
generator.acoustic.combine(result)
|
||||
return result
|
||||
|
||||
def generate_performance_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, rhythms: generator.rhythm.Library) -> int:
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ class TabPosition(typing.NamedTuple):
|
||||
midi: int
|
||||
|
||||
def within_fret_span(positions) -> bool:
|
||||
fretted = [position.fret for position in positions if position.fret > 0]
|
||||
return not fretted or max(fretted) - min(fretted) <= MAX_FRET_SPAN
|
||||
frets = [position.fret for position in positions]
|
||||
return not frets or max(frets) - min(frets) <= MAX_FRET_SPAN
|
||||
|
||||
def positions_for_pitch(midi: fractions.Fraction) -> tuple[TabPosition, ...]:
|
||||
if midi.denominator != 1:
|
||||
@@ -131,12 +131,12 @@ def movement(previous: tuple[TabPosition, ...], current: tuple[TabPosition, ...]
|
||||
|
||||
@functools.lru_cache(maxsize=4096)
|
||||
def position_center(positions: tuple[TabPosition, ...]) -> tuple[int, int]:
|
||||
frets = [p.fret for p in positions if p.fret] or [0]
|
||||
frets = [p.fret for p in positions] or [0]
|
||||
strings = [p.string_number for p in positions]
|
||||
return min(frets) + max(frets), min(strings) + max(strings) - 2
|
||||
|
||||
def compactness(positions) -> int:
|
||||
frets = [p.fret for p in positions if p.fret]
|
||||
frets = [p.fret for p in positions]
|
||||
return max(frets) - min(frets) if frets else 0
|
||||
|
||||
def extend_positions(held, pending):
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import pathlib
|
||||
|
||||
import generator.accompaniment
|
||||
import generator.acoustic
|
||||
import generator.guitar
|
||||
import generator.melody
|
||||
import generator.musicxml
|
||||
@@ -38,6 +39,7 @@ def _practice_document(document: dict, part_type: str, plan: generator.accompani
|
||||
generator.guitar.place_guitar(result)
|
||||
if generator.accompaniment.connect_continuations(result):
|
||||
generator.guitar.place_guitar(result)
|
||||
generator.acoustic.combine(result)
|
||||
return result
|
||||
|
||||
def generate_practice_scores(source_dir: pathlib.Path, output_dir: pathlib.Path, *, rhythms: generator.rhythm.Library) -> int:
|
||||
|
||||
Reference in New Issue
Block a user