Fix Generation

This commit is contained in:
khodowany
2026-09-26 22:59:14 -05:00
parent 83154b1a01
commit 3beeb87549
9 changed files with 239 additions and 9 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ def measure_rank(bar, offset, intervals, melody_attacks, melody_spans, companion
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])
density = abs(len(attacks) - duration / {'electric': 2, '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)
+6 -1
View File
@@ -7,6 +7,7 @@ import pathlib
import generator.accompaniment
import generator.acoustic
import generator.arrange.form
import generator.electric
import generator.guitar
import generator.melody
import generator.model
@@ -48,7 +49,7 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
staff = generator.score.source_staff(document)
source = staff['measures']
sections = generator.arrange.form.full_arrangement_sections(source, part_type)
output, tuplets, octave_shifts = [], [], []
output, tuplets, octave_shifts, voicing_origins = [], [], [], []
absolute = fractions.Fraction()
source_starts = []
source_time = fractions.Fraction()
@@ -67,6 +68,8 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
rhythm = (plan.acoustic_bass[len(segment)], plan.acoustic_upper[len(segment)]) if part_type == 'acoustic' else plan.electric[len(segment)]
result = generator.parts.build_measure(m, f'measure-{len(output) + len(segment) + 1}', section.role, part_type, previous[si], section.label if not segment else '', rhythm, melody_octave=True)
segment.append(result)
if part_type == 'electric' and section.role in ('melody', 'accompaniment'):
voicing_origins.append(generator.electric.MeasureOrigin(source_starts[si], section.role, result))
identity_segment = segment
segment_length = sum((generator.model.fraction(m['duration']) for m in segment), fractions.Fraction())
if section.role in ('vocal', 'melody'):
@@ -116,9 +119,11 @@ def _arrange_document(document: dict, part_type: str, plans: dict[int, generator
if part_type != 'vocal':
generator.acoustic.prepare(result)
generator.melody.harmonize(result)
voicing_links = generator.electric.link_voicings(source, voicing_origins) if part_type == 'electric' else []
generator.guitar.place_guitar(result)
if generator.accompaniment.connect_continuations(result):
generator.guitar.place_guitar(result)
generator.electric.reuse_voicings(voicing_links)
generator.acoustic.combine(result)
return result
+62
View File
@@ -0,0 +1,62 @@
from __future__ import annotations
import bisect
import copy
import dataclasses
import fractions
import generator.accompaniment
import generator.guitar
import generator.model
@dataclasses.dataclass(frozen=True)
class MeasureOrigin:
onset: fractions.Fraction
role: str
measure: dict
@dataclasses.dataclass(frozen=True)
class VoicingLink:
target: dict
references: tuple[dict, ...]
def link_voicings(source: list[dict], origins: list[MeasureOrigin]) -> list[VoicingLink]:
"""Link accompaniment to melody attacks in the same authored harmony span."""
boundaries, previous, start = [], None, fractions.Fraction()
for measure in source:
for harmony in measure['harmonies']:
if not generator.accompaniment.same_chord(previous, harmony):
boundaries.append(start + generator.model.fraction(harmony['onset']))
previous = harmony
start += generator.model.fraction(measure['duration'])
melody, targets = {}, []
for origin in origins:
for voice in origin.measure['voices']:
for note in voice['notes']:
if '_harmony' not in note:
continue
onset = origin.onset + generator.model.fraction(note['onset'])
span = bisect.bisect_right(boundaries, onset)
if origin.role == 'melody' and note.get('_harmonize'):
melody.setdefault(span, []).append((onset, note['_harmony'], note))
elif origin.role == 'accompaniment':
targets.append((span, onset, note))
links = []
for span, onset, note in targets:
matches = [(time, reference) for time, harmony, reference in melody.get(span, []) if generator.accompaniment.same_chord(harmony, note['_harmony'])]
# Before the first melody chord, use the first grip in this harmony span.
matches.sort(key=lambda item: (item[0] > onset, abs(item[0] - onset), item[1]['id']))
if matches:
links.append(VoicingLink(note, tuple(reference for _, reference in matches)))
return links
def reuse_voicings(links: list[VoicingLink]) -> None:
"""Copy placed melody grips without changing either part's rhythm or ties."""
for link in links:
for reference in link.references:
strings = tuple(sorted((p['tablature']['string'] for p in reference['pitches']), reverse=True))
if strings not in generator.guitar.ELECTRIC_STRINGS:
continue
note = link.target
note['pitches'] = [copy.deepcopy({**{k: p[k] for k in ('letter', 'alter', 'octave', 'tablature')}, 'id': f'{note["id"]}-s{p["tablature"]["string"]}', 'duration': note['duration'], 'written': note['written']}) for p in reference['pitches']]
break
+6 -1
View File
@@ -236,7 +236,12 @@ class EventPlacement:
def acoustic_candidates(span: generator.acoustic.HarmonySpan):
required, allowed, bass = chord_requirements(span.harmony, 'acoustic')
for shape in chord_positions(required, allowed, bass, profile='acoustic'):
basses = {bass}
degrees = generator.harmony.degrees(span.harmony)
if 'bass' not in span.harmony and 'inversion' not in span.harmony and 5 in degrees:
basses.add((bass + generator.harmony.degree_semitones(5, degrees[5])) % 12)
shapes = sorted({shape for value in sorted(basses) for shape in chord_positions(required, allowed, value, profile='acoustic')})
for shape in shapes:
active = tuple((str(p.string_number), p) for p in shape)
choices = tuple(EventPlacement(note, (), tuple(p for p in shape if (p.string_number >= 5) == (note['_guitar_lane'] == 'bass'))) for note in span.notes)
yield AttackCandidate(active, choices, shape, voicing_cost(shape, 'acoustic', allowed))