Generation
This commit is contained in:
+90
-43
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import fractions
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
import generator.guitar
|
||||
import generator.harmony
|
||||
import generator.model
|
||||
import generator.rhythm
|
||||
|
||||
@@ -18,7 +21,8 @@ class Context:
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Plan:
|
||||
acoustic: tuple[generator.rhythm.Bar, ...]
|
||||
acoustic_bass: tuple[generator.rhythm.Bar, ...]
|
||||
acoustic_upper: tuple[generator.rhythm.Bar, ...]
|
||||
electric: tuple[generator.rhythm.Bar, ...]
|
||||
|
||||
def active_chord(measure: dict, onset: fractions.Fraction, previous: dict | None) -> dict | None:
|
||||
@@ -56,37 +60,48 @@ def context(measure: dict, previous: dict | None) -> Context:
|
||||
occupied.append((start, end))
|
||||
return Context(measure, previous, offset, intervals, frozenset(attacks), tuple(occupied))
|
||||
|
||||
@functools.lru_cache(maxsize=4096)
|
||||
def sounding_rhythm(bar, offset, intervals):
|
||||
attacks = tuple(t - offset for t in bar.attacks() if any(a <= t - offset < b for a, b in intervals))
|
||||
spans = tuple((max(event.onset - offset, a), min(event.onset + event.duration - offset, b)) for event in bar.events if event.sounding and event.onset + event.duration > offset for a, b in intervals if a <= max(event.onset - offset, 0) < b)
|
||||
return attacks, spans
|
||||
|
||||
def sounding_attacks(bar: generator.rhythm.Bar, item: Context) -> tuple[fractions.Fraction, ...]:
|
||||
return tuple(t - item.offset for t in bar.attacks() if any(a <= t - item.offset < b for a, b in item.intervals))
|
||||
return sounding_rhythm(bar, item.offset, item.intervals)[0]
|
||||
|
||||
def sounding_spans(bar: generator.rhythm.Bar, item: Context) -> tuple[tuple[fractions.Fraction, fractions.Fraction], ...]:
|
||||
return tuple((max(event.onset - item.offset, a), min(event.onset + event.duration - item.offset, b)) for event in bar.events if event.sounding and event.onset + event.duration > item.offset for a, b in item.intervals if a <= max(event.onset - item.offset, 0) < b)
|
||||
return sounding_rhythm(bar, item.offset, item.intervals)[1]
|
||||
|
||||
def rank(bar: generator.rhythm.Bar, contexts: list[tuple[Context, tuple[fractions.Fraction, ...]]], electric: bool) -> tuple:
|
||||
missing_bars, uncovered, density, collisions, occupied, pulse = 0, 0, fractions.Fraction(), 0, 0, 0
|
||||
for item, acoustic in contexts:
|
||||
attacks = sounding_attacks(bar, item)
|
||||
spans = sounding_spans(bar, item)
|
||||
missing_bars += bool(item.intervals) and not spans and not electric
|
||||
uncovered += sum(not any(start < b and end > a for start, end in spans) for a, b in item.intervals)
|
||||
duration = sum((b - a for a, b in item.intervals), fractions.Fraction())
|
||||
density += abs(len(attacks) - duration / (4 if electric else 2))
|
||||
collisions += sum((t in item.attacks) + (t in acoustic) for t in attacks)
|
||||
occupied += sum(any(a <= t < b for a, b in item.occupied) for t in attacks)
|
||||
pulse -= sum(2 if t == 0 else 1 if t.denominator == 1 else 0 for t in attacks)
|
||||
return missing_bars, uncovered, density, collisions, occupied, pulse
|
||||
@functools.lru_cache(maxsize=16384)
|
||||
def measure_rank(bar, offset, intervals, melody_attacks, melody_spans, companions, role):
|
||||
attacks, spans = sounding_rhythm(bar, offset, intervals)
|
||||
missing_bars = int(bool(intervals) and not spans and role != 'electric')
|
||||
uncovered = sum(not any(start < b and end > a for start, end in spans) for a, b in intervals)
|
||||
duration = sum((b - a for a, b in intervals), fractions.Fraction())
|
||||
density = abs(len(attacks) - duration / {'electric': 4, 'bass': 2, 'upper': 1}[role])
|
||||
collisions = sum((t in melody_attacks) + (t in companions) for t in attacks)
|
||||
occupied = sum(any(a <= t < b for a, b in melody_spans) for t in attacks)
|
||||
pulse = -sum(2 if t == 0 else 1 if t.denominator == 1 else 0 for t in attacks)
|
||||
if role == 'electric':
|
||||
return missing_bars, density, collisions, occupied, uncovered, pulse
|
||||
return missing_bars, uncovered, density, pulse if role == 'bass' else collisions, collisions if role == 'bass' else pulse, occupied
|
||||
|
||||
def rank(bar: generator.rhythm.Bar, contexts: list[tuple[Context, tuple[fractions.Fraction, ...]]], role: str) -> tuple:
|
||||
result = (0, 0, 0, 0, 0, 0)
|
||||
for item, companions in contexts:
|
||||
result = add_ranks(result, measure_rank(bar, item.offset, item.intervals, item.attacks, item.occupied, companions, role))
|
||||
return result
|
||||
|
||||
def add_ranks(left: tuple, right: tuple) -> tuple:
|
||||
return tuple(a + b for a, b in zip(left, right))
|
||||
|
||||
def cycle(snippets: tuple[generator.rhythm.Snippet, ...], contexts: list[Context], acoustic: list[generator.rhythm.Bar] | None = None) -> tuple[generator.rhythm.Bar, ...]:
|
||||
electric = acoustic is not None
|
||||
def cycle(snippets: tuple[generator.rhythm.Snippet, ...], contexts: list[Context], role: str, companions: list[tuple[fractions.Fraction, ...]] | None = None) -> tuple[generator.rhythm.Bar, ...]:
|
||||
bars = {}
|
||||
for snippet in snippets:
|
||||
for index, bar in enumerate(snippet.bars):
|
||||
bars.setdefault(bar, f'{snippet.id}:{index}')
|
||||
phases = [[(item, sounding_attacks(acoustic[i], item) if acoustic else ()) for i, item in enumerate(contexts) if i % 2 == phase] for phase in range(2)]
|
||||
scores = [{bar: rank(bar, phase, electric) for bar in bars} for phase in phases]
|
||||
phases = [[(item, companions[i] if companions else ()) for i, item in enumerate(contexts) if i % 2 == phase] for phase in range(2)]
|
||||
scores = [{bar: rank(bar, phase, role) for bar in bars} for phase in phases]
|
||||
candidates = [(add_ranks(scores[0][s.bars[0]], scores[1][s.bars[1]]), s.id, s.bars) for s in snippets if len(s.bars) == 2]
|
||||
single = [s for s in snippets if len(s.bars) == 1]
|
||||
if single:
|
||||
@@ -96,20 +111,20 @@ def cycle(snippets: tuple[generator.rhythm.Snippet, ...], contexts: list[Context
|
||||
if score[0]:
|
||||
failing = next(item.measure for i, item in enumerate(contexts) if item.intervals and not sounding_spans(pair[i % 2], item))
|
||||
raise ValueError(f'measure {failing["number"]}: no rhythm motif provides acoustic accompaniment in every harmonically active bar')
|
||||
fourth = [(item, sounding_attacks(acoustic[i], item) if acoustic else ()) for i, item in enumerate(contexts) if i % 4 == 3]
|
||||
fourth = [(item, companions[i] if companions else ()) for i, item in enumerate(contexts) if i % 4 == 3]
|
||||
second = pair[1]
|
||||
alternatives = [bar for bar in bars if sum(bar.beat(n) != second.beat(n) for n in range(int(second.length))) == 1]
|
||||
variation = min(alternatives, key=lambda bar: (rank(bar, fourth, electric), bars[bar])) if alternatives and fourth else second
|
||||
if rank(variation, fourth, electric)[0]:
|
||||
variation = min(alternatives, key=lambda bar: (rank(bar, fourth, role), bars[bar])) if alternatives and fourth else second
|
||||
if rank(variation, fourth, role)[0]:
|
||||
variation = second
|
||||
result = (pair[0], second, pair[0], variation)
|
||||
if electric and any(item.intervals for item in contexts) and not any(sounding_spans(result[i % 4], item) for i, item in enumerate(contexts)):
|
||||
if role == 'electric' and any(item.intervals for item in contexts) and not any(sounding_spans(result[i % 4], item) for i, item in enumerate(contexts)):
|
||||
raise ValueError(f'measure {contexts[0].measure["number"]}: rhythm library cannot provide an audible electric response')
|
||||
return result
|
||||
|
||||
def plan_section(measures: list[dict], library: generator.rhythm.Library, *, initial: dict | None = None, electric: bool = True) -> Plan:
|
||||
previous = carried_chords(measures, initial)
|
||||
acoustic, responses = [], []
|
||||
basses, uppers, responses = [], [], []
|
||||
start = 0
|
||||
while start < len(measures):
|
||||
meter = measures[start]['time_signature']
|
||||
@@ -122,13 +137,15 @@ def plan_section(measures: list[dict], library: generator.rhythm.Library, *, ini
|
||||
if any('meter' in change and generator.model.fraction(change['onset']) != 0 for change in measure.get('attributes', [])):
|
||||
raise ValueError(f'measure {measure["number"]}: accompaniment does not support mid-measure meter changes')
|
||||
contexts = [context(m, previous[start + i]) for i, m in enumerate(segment)]
|
||||
foundation = cycle(snippets, contexts)
|
||||
chosen = [foundation[i % 4] for i in range(len(segment))]
|
||||
response = cycle(snippets, contexts, chosen) if electric else foundation
|
||||
acoustic.extend(chosen)
|
||||
bass = cycle(snippets, contexts, 'bass')
|
||||
upper = cycle(snippets, contexts, 'upper')
|
||||
companions = [tuple(sorted(set(sounding_attacks(bass[i % 4], item)) | set(sounding_attacks(upper[i % 4], item)))) for i, item in enumerate(contexts)]
|
||||
response = cycle(snippets, contexts, 'electric', companions) if electric else upper
|
||||
basses.extend(bass[i % 4] for i in range(len(segment)))
|
||||
uppers.extend(upper[i % 4] for i in range(len(segment)))
|
||||
responses.extend(response[i % 4] for i in range(len(segment)))
|
||||
start = end
|
||||
return Plan(tuple(acoustic), tuple(responses))
|
||||
return Plan(tuple(basses), tuple(uppers), tuple(responses))
|
||||
|
||||
def same_chord(left: dict | None, right: dict | None) -> bool:
|
||||
return left is not None and right is not None and all(left.get(k) == right.get(k) for k in ('root', 'kind', 'degrees', 'bass', 'inversion'))
|
||||
@@ -159,7 +176,7 @@ def normalize_beams(events: list[dict]) -> None:
|
||||
if not event.get('beams'):
|
||||
event.pop('beams', None)
|
||||
|
||||
def realize(measure: dict, bar: generator.rhythm.Bar, previous: dict | None) -> list[dict]:
|
||||
def realize(measure: dict, bar: generator.rhythm.Bar, previous: dict | None, *, lane: str = '') -> list[dict]:
|
||||
item = context(measure, previous)
|
||||
length = generator.model.fraction(measure['duration'])
|
||||
events = []
|
||||
@@ -178,44 +195,74 @@ def realize(measure: dict, bar: generator.rhythm.Bar, previous: dict | None) ->
|
||||
if not same_chord(chord, preceding):
|
||||
chord = None
|
||||
sounding_end = end
|
||||
if chord and lane == 'electric':
|
||||
if continuation:
|
||||
chord = None
|
||||
else:
|
||||
sounding_end = min(end, start + 1)
|
||||
for change in measure['harmonies']:
|
||||
time = generator.model.fraction(change['onset'])
|
||||
if chord and start < time < sounding_end and not same_chord(chord, active_chord(measure, time, previous)):
|
||||
sounding_end = time
|
||||
written = {'type': source.written_type, 'dots': source.dots} if end - start == source.duration and sounding_end == end else None
|
||||
event = generator.model.make_event(f'{measure["id"]}-rhythm-{len(events)}', start, sounding_end - start, 'note' if chord else 'rest', written=written)
|
||||
event = generator.model.make_event(f'{measure["id"]}-rhythm-{lane}-{len(events)}', start, sounding_end - start, 'note' if chord else 'rest', written=written)
|
||||
if chord:
|
||||
event['_harmony'] = chord
|
||||
if lane:
|
||||
event.update(_guitar_profile='electric' if lane == 'electric' else 'acoustic', _guitar_lane=lane)
|
||||
if continuation:
|
||||
event['_rhythm_continuation'] = True
|
||||
if source.beams and written and chord:
|
||||
event['beams'] = [{'level': level, 'type': kind} for level, kind in source.beams]
|
||||
events.append(event)
|
||||
if sounding_end < end:
|
||||
events.append(generator.model.make_event(f'{measure["id"]}-rhythm-{len(events)}', sounding_end, end - sounding_end, 'rest'))
|
||||
events.append(generator.model.make_event(f'{measure["id"]}-rhythm-{lane}-{len(events)}', sounding_end, end - sounding_end, 'rest'))
|
||||
normalize_beams(events)
|
||||
return events
|
||||
|
||||
def connect_continuations(document: dict) -> bool:
|
||||
previous = None
|
||||
previous = {}
|
||||
changed = False
|
||||
for measure in document['staves'][0]['measures']:
|
||||
if measure.get('section_label'):
|
||||
previous = None
|
||||
previous.clear()
|
||||
for voice in measure['voices']:
|
||||
for note in voice['notes']:
|
||||
continuation = note.pop('_rhythm_continuation', False)
|
||||
if continuation and previous and previous['pitches'] and note['pitches']:
|
||||
note['pitches'] = copy.deepcopy(previous['pitches'])
|
||||
for index, tone in enumerate(note['pitches']):
|
||||
source = previous['pitches'][index]
|
||||
tone.update(id=f'{note["id"]}-p{index + 1}', duration=copy.deepcopy(note['duration']), written=copy.deepcopy(note['written']), tie_from=source['id'])
|
||||
tone.pop('tie_to', None)
|
||||
source_note = previous.get(voice['id'])
|
||||
if continuation and source_note and source_note['pitches'] and note['pitches']:
|
||||
old = {(p['tablature']['string'], p['tablature']['fret']): p for p in source_note['pitches']}
|
||||
for tone in note['pitches']:
|
||||
source = old[(tone['tablature']['string'], tone['tablature']['fret'])]
|
||||
tone['tie_from'] = source['id']
|
||||
source['tie_to'] = tone['id']
|
||||
changed = True
|
||||
elif continuation:
|
||||
note.update(kind='rest', pitches=[])
|
||||
note.pop('beams', None)
|
||||
changed = True
|
||||
previous = note
|
||||
previous[voice['id']] = note
|
||||
return changed
|
||||
|
||||
def prepare_basses(document: dict) -> None:
|
||||
previous, attack, current_bass = None, 0, None
|
||||
for measure in document['staves'][0]['measures']:
|
||||
if measure.get('section_label'):
|
||||
previous, attack = None, 0
|
||||
notes = sorted((n for v in measure['voices'] for n in v['notes'] if n.get('_guitar_profile') == 'acoustic'), key=lambda n: generator.model.fraction(n['onset']))
|
||||
for _, group in itertools.groupby(notes, key=lambda n: generator.model.fraction(n['onset'])):
|
||||
group = list(group)
|
||||
chord = group[0]['_harmony']
|
||||
root = (generator.model.NATURAL[chord['root']['letter']] + generator.model.alteration(chord['root']['alter'])) % 12
|
||||
if not same_chord(chord, previous):
|
||||
attack, current_bass = 0, root
|
||||
explicit = 'bass' in chord or 'inversion' in chord
|
||||
if any(n['_guitar_lane'] == 'bass' and not n.get('_rhythm_continuation') for n in group):
|
||||
degrees = generator.harmony.degrees(chord)
|
||||
current_bass = (root + generator.harmony.degree_semitones(5, degrees[5])) % 12 if not explicit and attack % 2 and 5 in degrees else root
|
||||
attack += 1
|
||||
if explicit:
|
||||
current_bass = generator.guitar.chord_requirements(chord, 'acoustic')[2]
|
||||
for note in group:
|
||||
note['_bass_pc'] = current_bass
|
||||
previous = chord
|
||||
|
||||
Reference in New Issue
Block a user