from __future__ import annotations import generator.harmony import generator.model import generator.pitch def chord_notes(chord: dict) -> tuple[tuple[int, str], ...]: return tuple((degree, generator.model.pitch_name(generator.pitch.shift_pitch({**chord['root'], 'octave': 4}, degree - 1, generator.harmony.degree_semitones(degree, alter)))) for degree, alter in generator.harmony.degrees(chord).items()) def passage(part: str, solo, choices, library) -> str: lines = [f'{part.capitalize()}: {solo.label}', 'Positions use the score’s measure labels and quarter-note beats; sequential measure indices are also shown.', ''] lines.append('Scale map:') previous = None for choice in choices: label = choice.scale.label() if choice.scale else 'No exact match within the permitted catalog.' if choice.span.chord else 'N.C.' if label != previous: lines.append(f' Measure {choice.span.source_measure}, beat {choice.span.beat}: {label}') lines.append(' ' + choice.reason) previous = label lines.extend(('', 'Chord detail:')) for choice in choices: span, scale = choice.span, choice.scale if span.chord is None: continue chord = span.chord notes = chord_notes(chord) required = [name for _, name in notes] if bass := chord.get('bass'): required.append(generator.model.pitch_name(bass)) targets = [name for degree, name in notes if degree in (3, 7)] or [name for _, name in notes] lines.append(f'Measure {span.source_measure}, beat {span.beat}; sequential measure {span.measure}; offset {span.start} through {span.end}') lines.append(' Chord: ' + generator.harmony.symbol(chord) + '; required pitches: ' + ', '.join(dict.fromkeys(required))) if scale: root = generator.model.pitch_number({**chord['root'], 'octave': 4}) % 12 degree, mode = library.chord_mode(scale, generator.model.pitch_name(chord['root'])) if root in scale.pitches else (None, None) lines.append(' Parent: ' + scale.label() + ' [' + ', '.join(scale.notes) + ']') lines.append(f' Parent degree: {degree or "omitted root"}; chord-root mode: {mode or "not assigned"}') lines.append(' Chord-tone targets: ' + ', '.join(targets)) if generator.model.pitch_name(chord['root']) == scale.root: lines.append(' Tonic resolution target: ' + scale.root) else: lines.append(' No exact match within the permitted catalog.') lines.extend((' Reason: ' + choice.reason, '')) return '\n'.join(lines)