+41
-152
@@ -4,6 +4,8 @@ from itertools import product
|
||||
from typing import NamedTuple
|
||||
|
||||
from generator.transposition import mod12
|
||||
from generator import harmony
|
||||
from generator.model import NATURAL
|
||||
|
||||
STANDARD_TUNING = ((6, 40), (5, 45), (4, 50), (3, 55), (2, 59), (1, 64))
|
||||
WRITTEN_GUITAR_OCTAVE_OFFSET = 12
|
||||
@@ -15,134 +17,8 @@ class TabPosition(NamedTuple):
|
||||
midi: int
|
||||
|
||||
|
||||
class ChordSymbol(NamedTuple):
|
||||
root: str
|
||||
quality: str
|
||||
bass: str
|
||||
|
||||
|
||||
ROOTS = {
|
||||
"C": 0,
|
||||
"C#": 1,
|
||||
"Db": 1,
|
||||
"D": 2,
|
||||
"D#": 3,
|
||||
"Eb": 3,
|
||||
"E": 4,
|
||||
"F": 5,
|
||||
"F#": 6,
|
||||
"Gb": 6,
|
||||
"G": 7,
|
||||
"G#": 8,
|
||||
"Ab": 8,
|
||||
"A": 9,
|
||||
"A#": 10,
|
||||
"Bb": 10,
|
||||
"B": 11,
|
||||
}
|
||||
|
||||
|
||||
def parse_root(symbol: str) -> tuple[str, str] | None:
|
||||
symbol = symbol.strip()
|
||||
if not symbol or symbol[0].upper() not in "ABCDEFG":
|
||||
return None
|
||||
length = 1
|
||||
if len(symbol) > 1 and symbol[1] in {"#", "b"}:
|
||||
length = 2
|
||||
return symbol[:length], symbol[length:]
|
||||
|
||||
|
||||
def parse_chord_symbol(symbol: str) -> ChordSymbol | None:
|
||||
clean = symbol.strip().replace(" ", "")
|
||||
base, _, slash = clean.partition("/")
|
||||
parsed = parse_root(base)
|
||||
if parsed is None:
|
||||
return None
|
||||
root, quality = parsed
|
||||
bass = ""
|
||||
if slash:
|
||||
bass_parsed = parse_root(slash)
|
||||
if bass_parsed is not None:
|
||||
bass = bass_parsed[0]
|
||||
return ChordSymbol(root, quality, bass)
|
||||
|
||||
|
||||
def unique_intervals(values: tuple[int, ...]) -> tuple[int, ...]:
|
||||
result: list[int] = []
|
||||
seen: set[int] = set()
|
||||
for value in values:
|
||||
interval = mod12(value)
|
||||
if interval in seen:
|
||||
continue
|
||||
seen.add(interval)
|
||||
result.append(interval)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def color_intervals(value: str) -> tuple[int, ...]:
|
||||
colors: list[int] = []
|
||||
if "alt" in value:
|
||||
colors.append(1)
|
||||
if "b9" in value:
|
||||
colors.append(1)
|
||||
if "#9" in value:
|
||||
colors.append(3)
|
||||
if "9" in value and "b9" not in value and "#9" not in value:
|
||||
colors.append(2)
|
||||
if "#11" in value:
|
||||
colors.append(6)
|
||||
elif "11" in value:
|
||||
colors.append(5)
|
||||
if "b13" in value:
|
||||
colors.append(8)
|
||||
elif "13" in value:
|
||||
colors.append(9)
|
||||
return tuple(colors)
|
||||
|
||||
|
||||
def chord_intervals(quality: str) -> tuple[int, ...]:
|
||||
value = quality.lower().replace(" ", "")
|
||||
if "7b5" in value or "ø" in value or "half" in value:
|
||||
return (0, 3, 6, 10)
|
||||
if "dim" in value or "o" in value:
|
||||
return (0, 3, 6, 9)
|
||||
colors = color_intervals(value)
|
||||
if "aug" in value or "+" in value:
|
||||
return unique_intervals((0, 4, 8, 10) + colors) if ("7" in value or colors) else (0, 4, 8)
|
||||
if "sus" in value:
|
||||
return unique_intervals((0, 5, 10) + colors) if ("7" in value or colors) else (0, 5, 7)
|
||||
minor = value.startswith("-") or (value.startswith("m") and not value.startswith(("maj", "ma")))
|
||||
if minor:
|
||||
if "6" in value and "7" not in value:
|
||||
return unique_intervals((0, 3, 7, 9) + colors)
|
||||
if "7" in value or colors:
|
||||
return unique_intervals((0, 3, 10) + colors)
|
||||
return (0, 3, 7)
|
||||
if "maj" in value or value.startswith("ma") or "Δ" in value:
|
||||
return unique_intervals((0, 4, 11) + colors)
|
||||
if "6" in value and "7" not in value:
|
||||
return unique_intervals((0, 4, 7, 9) + colors)
|
||||
if "7" in value or colors:
|
||||
return unique_intervals((0, 4, 10) + colors)
|
||||
return (0, 4, 7)
|
||||
|
||||
|
||||
def pitch_class_for_root(root: str) -> int | None:
|
||||
return ROOTS.get(root[0].upper() + root[1:], ROOTS.get(root.upper()))
|
||||
|
||||
|
||||
def chord_pitch_classes(symbol: str) -> tuple[int, ...]:
|
||||
parsed = parse_chord_symbol(symbol)
|
||||
if parsed is None:
|
||||
return ()
|
||||
root_pc = pitch_class_for_root(parsed.root)
|
||||
if root_pc is None:
|
||||
return ()
|
||||
pitch_classes = [mod12(root_pc + interval) for interval in chord_intervals(parsed.quality)]
|
||||
bass_pc = pitch_class_for_root(parsed.bass) if parsed.bass else None
|
||||
if bass_pc is not None and bass_pc not in pitch_classes:
|
||||
pitch_classes.append(bass_pc)
|
||||
return tuple(pitch_classes)
|
||||
def chord_pitch_classes(symbol: str | dict) -> tuple[int, ...]:
|
||||
return harmony.pitch_classes(harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol)
|
||||
|
||||
|
||||
def positions_for_string(string_number: int, open_midi: int, pitch_classes: tuple[int, ...]) -> list[TabPosition]:
|
||||
@@ -168,8 +44,23 @@ def candidate_score(candidate: tuple[TabPosition, ...], pitch_classes: tuple[int
|
||||
return missing * 1000 + span * 80 + abs(low - 5) * 12 + open_penalty * 8 + sum(position.fret for position in candidate)
|
||||
|
||||
|
||||
def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
pitch_classes = chord_pitch_classes(symbol)
|
||||
def find_playable_voicing(symbol: str | dict) -> list[TabPosition]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
semantic_degrees = harmony.degrees(definition)
|
||||
voicing = dict(semantic_degrees)
|
||||
if 7 in voicing and voicing.get(5) == 0:
|
||||
del voicing[5]
|
||||
if len(voicing) > 4:
|
||||
for degree in (11, 9, 5):
|
||||
if len(voicing) > 4 and voicing.get(degree) == 0:
|
||||
del voicing[degree]
|
||||
root = definition.get('root', {'letter':'c','alter':0})
|
||||
root_pc = NATURAL[root['letter']] + root['alter']
|
||||
pitch_classes = tuple(dict.fromkeys((root_pc + harmony.degree_semitones(d,a)) % 12 for d,a in voicing.items()))
|
||||
bass = definition.get('bass')
|
||||
bass_pc = (NATURAL[bass['letter']] + bass['alter']) % 12 if bass else None
|
||||
if bass_pc is not None and bass_pc not in pitch_classes:
|
||||
pitch_classes += (bass_pc,)
|
||||
if not pitch_classes:
|
||||
return []
|
||||
|
||||
@@ -182,11 +73,13 @@ def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
if any(not item for item in options):
|
||||
continue
|
||||
for candidate in product(*options):
|
||||
if bass_pc is not None and min(candidate, key=lambda p:p.midi).midi % 12 != bass_pc:
|
||||
continue
|
||||
fretted = [position.fret for position in candidate if position.fret > 0]
|
||||
if fretted and max(fretted) - min(fretted) > 4:
|
||||
continue
|
||||
covered = {mod12(position.midi) for position in candidate}
|
||||
if len(covered & set(pitch_classes)) < min(3, len(set(pitch_classes))):
|
||||
if not set(pitch_classes).issubset(covered):
|
||||
continue
|
||||
score = candidate_score(candidate, pitch_classes)
|
||||
if score < best_score:
|
||||
@@ -195,24 +88,20 @@ def find_playable_voicing(symbol: str) -> list[TabPosition]:
|
||||
return sorted(best or [], key=lambda item: item.midi)
|
||||
|
||||
|
||||
def pitch_from_midi(midi: int) -> dict[str, int | str]:
|
||||
names = (
|
||||
("c", "n"),
|
||||
("c", "#"),
|
||||
("d", "n"),
|
||||
("e", "b"),
|
||||
("e", "n"),
|
||||
("f", "n"),
|
||||
("f", "#"),
|
||||
("g", "n"),
|
||||
("a", "b"),
|
||||
("a", "n"),
|
||||
("b", "b"),
|
||||
("b", "n"),
|
||||
)
|
||||
letter, accidental = names[mod12(midi)]
|
||||
return {"letter": letter, "accidental": accidental, "octave": midi // 12 - 1}
|
||||
|
||||
|
||||
def pitches_for_voicing(symbol: str) -> list[dict[str, int | str]]:
|
||||
return [pitch_from_midi(position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET) for position in find_playable_voicing(symbol)]
|
||||
def pitches_for_voicing(symbol: str | dict) -> list[dict[str, int | str]]:
|
||||
definition = harmony.parse_symbol(symbol) if isinstance(symbol, str) else symbol
|
||||
from generator.transposition import shift_pitch
|
||||
root = definition.get('root', {'letter':'c', 'alter':0})
|
||||
spellings = {}
|
||||
for degree, alter in harmony.degrees(definition).items():
|
||||
p = shift_pitch(root, degree-1, harmony.degree_semitones(degree, alter))
|
||||
spellings[(NATURAL[p['letter']]+p['alter']) % 12] = p
|
||||
if bass := definition.get('bass'):
|
||||
spellings[(NATURAL[bass['letter']]+bass['alter']) % 12] = bass
|
||||
result = []
|
||||
for position in find_playable_voicing(definition):
|
||||
midi = position.midi + WRITTEN_GUITAR_OCTAVE_OFFSET
|
||||
p = spellings[midi % 12].copy()
|
||||
p['octave'] = (midi - NATURAL[p['letter']] - p['alter']) // 12 - 1
|
||||
result.append(p)
|
||||
return result
|
||||
|
||||
+151
-250
@@ -1,279 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import warnings
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
|
||||
from generator.arrange.form import ArrangementSection, full_arrangement_sections
|
||||
from generator.arrange.form import full_arrangement_sections
|
||||
from generator.arrange.fretboard import pitches_for_voicing
|
||||
from generator.score import (
|
||||
QUARTER_TICKS,
|
||||
apply_source_chord_markers,
|
||||
active_chord_at_offset,
|
||||
chord_markers,
|
||||
chord_symbol_at_offset,
|
||||
copy_source_voice,
|
||||
first_chord_symbol,
|
||||
first_voice,
|
||||
leading_pickup_measure_index,
|
||||
load_json,
|
||||
make_arranged_document,
|
||||
make_measure,
|
||||
measure_capacity_ticks,
|
||||
renderable_duration,
|
||||
renderable_rest_notes,
|
||||
rest_note,
|
||||
sanitized_note,
|
||||
slash_note,
|
||||
slugify,
|
||||
source_staff,
|
||||
source_note_attack_at,
|
||||
write_json,
|
||||
)
|
||||
|
||||
PART_TYPES = ("acoustic", "electric", "vocal")
|
||||
|
||||
from generator.model import beat_lengths, fraction, key_label, make_event, meter_label, rational
|
||||
from generator.musicxml import load_musicxml
|
||||
from generator.transposition import concert_document, shift_pitch
|
||||
from generator.score import copy_source_voice, make_arranged_document, make_measure, measure_notes, reidentify, slugify, source_staff, write_json
|
||||
|
||||
PART_TYPES = ('acoustic','electric','vocal')
|
||||
FINAL_SECTION_INDEX = 6
|
||||
|
||||
|
||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[str]:
|
||||
result: list[str] = []
|
||||
current = ""
|
||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
||||
result = []
|
||||
current = None
|
||||
for measure in measures:
|
||||
current = first_chord_symbol(measure, current, include_optional=include_optional)
|
||||
result.append(current)
|
||||
for h in measure.get('harmonies', []):
|
||||
if include_optional or not h.get('chord_optional'):
|
||||
current = h
|
||||
return result
|
||||
|
||||
|
||||
def playable_chord_note(note_id: str, duration_ticks: int, voicing_chord_symbol: str, visible_chord_symbol: str = "") -> dict:
|
||||
note = {
|
||||
"id": note_id,
|
||||
"kind": "note",
|
||||
"duration_ticks": duration_ticks,
|
||||
"pitches": pitches_for_voicing(voicing_chord_symbol),
|
||||
"hidden": False,
|
||||
}
|
||||
if not note["pitches"]:
|
||||
return rest_note(note_id, duration_ticks, visible_chord_symbol)
|
||||
if visible_chord_symbol:
|
||||
note["chord_symbol"] = visible_chord_symbol
|
||||
return note
|
||||
|
||||
|
||||
def rhythm_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
|
||||
hit_offsets = (0, QUARTER_TICKS + QUARTER_TICKS // 2, QUARTER_TICKS * 2, QUARTER_TICKS * 2 + QUARTER_TICKS // 2, QUARTER_TICKS * 3 + QUARTER_TICKS // 2)
|
||||
hit_duration = QUARTER_TICKS // 2
|
||||
notes: list[dict] = []
|
||||
cursor = 0
|
||||
for offset in hit_offsets:
|
||||
if offset >= capacity:
|
||||
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
||||
active = previous
|
||||
for h in measure['harmonies']:
|
||||
if fraction(h['onset']) > onset:
|
||||
break
|
||||
if offset > cursor:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", offset - cursor, sequence)
|
||||
notes.extend(rests)
|
||||
cursor = offset
|
||||
active_chord = active_chord_at_offset(measure, offset, chord_symbol, include_optional=include_optional)
|
||||
visible_chord = chord_symbol_at_offset(measure, offset, include_optional=include_optional)
|
||||
sequence += 1
|
||||
notes.append(playable_chord_note(f"arr-note-{sequence}", min(hit_duration, capacity - offset), active_chord, visible_chord))
|
||||
cursor = offset + min(hit_duration, capacity - offset)
|
||||
if cursor < capacity:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", capacity - cursor, sequence)
|
||||
notes.extend(rests)
|
||||
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
|
||||
active = h
|
||||
return active if active and active['kind'] != 'none' else None
|
||||
|
||||
|
||||
def accompaniment_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
|
||||
notes: list[dict] = []
|
||||
lead_rest = min(QUARTER_TICKS // 2, capacity)
|
||||
if lead_rest:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", lead_rest, sequence, chord_symbol_at_offset(measure, 0, include_optional=include_optional))
|
||||
notes.extend(rests)
|
||||
if lead_rest < capacity:
|
||||
hit = min(QUARTER_TICKS // 2, capacity - lead_rest)
|
||||
visible_chord = chord_symbol_at_offset(measure, lead_rest, include_optional=include_optional)
|
||||
active_chord = active_chord_at_offset(measure, lead_rest, chord_symbol, include_optional=include_optional)
|
||||
sequence += 1
|
||||
if source_note_attack_at(measure, lead_rest):
|
||||
notes.append(rest_note(f"arr-rest-{sequence}", hit, visible_chord))
|
||||
else:
|
||||
notes.append(playable_chord_note(f"arr-note-{sequence}", hit, active_chord, visible_chord))
|
||||
cursor = lead_rest + hit
|
||||
if cursor < capacity:
|
||||
markers = [marker for marker in chord_markers(measure, include_optional=include_optional) if marker[0] >= cursor]
|
||||
if markers:
|
||||
segment_cursor = cursor
|
||||
for index, (start, _, chord) in enumerate(markers):
|
||||
if start > segment_cursor:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", start - segment_cursor, sequence)
|
||||
notes.extend(rests)
|
||||
next_start = markers[index + 1][0] if index + 1 < len(markers) else capacity
|
||||
rests, sequence = renderable_rest_notes("arr-rest", next_start - start, sequence, chord)
|
||||
notes.extend(rests)
|
||||
segment_cursor = next_start
|
||||
if segment_cursor < capacity:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", capacity - segment_cursor, sequence)
|
||||
notes.extend(rests)
|
||||
else:
|
||||
rests, sequence = renderable_rest_notes("arr-rest", capacity - cursor, sequence)
|
||||
notes.extend(rests)
|
||||
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
|
||||
|
||||
|
||||
def slash_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
|
||||
notes: list[dict] = []
|
||||
cursor = 0
|
||||
while cursor < capacity:
|
||||
sequence += 1
|
||||
duration = min(QUARTER_TICKS, capacity - cursor)
|
||||
notes.append(slash_note(f"arr-slash-{sequence}", duration))
|
||||
cursor += duration
|
||||
return apply_source_chord_markers(notes, measure, include_optional=include_optional), sequence
|
||||
|
||||
|
||||
def rest_notes(measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
capacity = measure_capacity_ticks(str(measure.get("time_signature", "4/4")))
|
||||
markers = chord_markers(measure, include_optional=include_optional)
|
||||
if not markers:
|
||||
notes, sequence = renderable_rest_notes("arr-rest", capacity, sequence)
|
||||
return notes, sequence
|
||||
|
||||
notes: list[dict] = []
|
||||
cursor = 0
|
||||
for index, (start, _, chord) in enumerate(markers):
|
||||
if start > cursor:
|
||||
if not renderable_duration(start - cursor):
|
||||
return source_rest_skeleton_notes(measure, sequence, include_optional=include_optional)
|
||||
rests, sequence = renderable_rest_notes("arr-rest", start - cursor, sequence)
|
||||
notes.extend(rests)
|
||||
next_start = markers[index + 1][0] if index + 1 < len(markers) else capacity
|
||||
if not renderable_duration(next_start - start):
|
||||
return source_rest_skeleton_notes(measure, sequence, include_optional=include_optional)
|
||||
rests, sequence = renderable_rest_notes("arr-rest", next_start - start, sequence, chord)
|
||||
notes.extend(rests)
|
||||
cursor = next_start
|
||||
if cursor < capacity:
|
||||
if not renderable_duration(capacity - cursor):
|
||||
return source_rest_skeleton_notes(measure, sequence, include_optional=include_optional)
|
||||
rests, sequence = renderable_rest_notes("arr-rest", capacity - cursor, sequence)
|
||||
notes.extend(rests)
|
||||
return notes, sequence
|
||||
|
||||
|
||||
def source_rest_skeleton_notes(measure: dict, sequence: int, *, include_optional: bool = True) -> tuple[list[dict], int]:
|
||||
notes: list[dict] = []
|
||||
for source_note in first_voice(measure).get("notes", []):
|
||||
if not isinstance(source_note, dict):
|
||||
continue
|
||||
sequence += 1
|
||||
chord = "" if not include_optional and source_note.get("chord_optional") is True else str(source_note.get("chord_symbol", ""))
|
||||
note = rest_note(f"arr-rest-{sequence}", int(source_note.get("duration_ticks", QUARTER_TICKS)), chord)
|
||||
if "display_ticks" in source_note:
|
||||
note["display_ticks"] = source_note["display_ticks"]
|
||||
if "tuplet_id" in source_note:
|
||||
note["tuplet_id"] = source_note["tuplet_id"]
|
||||
notes.append(note)
|
||||
if not notes:
|
||||
sequence += 1
|
||||
notes.append(rest_note(f"arr-rest-{sequence}", measure_capacity_ticks(str(measure.get("time_signature", "4/4")))))
|
||||
return notes, sequence
|
||||
|
||||
|
||||
def melody_voice(measure: dict, *, strip_lyrics: bool, include_optional: bool = True) -> dict:
|
||||
voice = copy_source_voice(measure, strip_lyrics=strip_lyrics, include_optional=include_optional)
|
||||
voice["id"] = "voice-arranged-source"
|
||||
voice["notes"] = [sanitized_note(note, strip_lyrics=strip_lyrics, include_optional=include_optional) for note in first_voice(measure).get("notes", []) if isinstance(note, dict)]
|
||||
return voice
|
||||
|
||||
|
||||
def apply_vocal_pickup_overlap(output: list[dict], source_measures: list[dict], section: ArrangementSection) -> None:
|
||||
pickup = leading_pickup_measure_index(source_measures)
|
||||
if pickup is None or section.source_measure_index != pickup + 1 or not output:
|
||||
return
|
||||
voice = copy_source_voice(source_measures[pickup], strip_lyrics=False)
|
||||
for note in voice["notes"]:
|
||||
if isinstance(note, dict):
|
||||
note.pop("chord_symbol", None)
|
||||
output[-1]["voices"][0]["id"] = "voice-arranged-vocal"
|
||||
output[-1]["voices"][0]["tuplets"] = voice["tuplets"]
|
||||
output[-1]["voices"][0]["notes"] = voice["notes"]
|
||||
|
||||
|
||||
def voice_for_section(section: ArrangementSection, measure: dict, chord_symbol: str, sequence: int, *, include_optional: bool = True) -> tuple[dict, int]:
|
||||
if section.role == "vocal":
|
||||
return melody_voice(measure, strip_lyrics=False, include_optional=include_optional), sequence
|
||||
if section.role == "melody":
|
||||
return melody_voice(measure, strip_lyrics=True, include_optional=include_optional), sequence
|
||||
if section.role == "rhythm":
|
||||
notes, sequence = rhythm_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
elif section.role == "accompaniment":
|
||||
notes, sequence = accompaniment_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
elif section.role == "slash":
|
||||
notes, sequence = slash_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dict]:
|
||||
length = fraction(measure['duration'])
|
||||
if role == 'rest':
|
||||
return [make_event('rest', Fraction(), length, 'rest', written={'type':'whole','dots':0}) | {'measure_rest':True}]
|
||||
if role == 'rhythm' and measure['time_signature']=='4/4' and not measure.get('attributes'):
|
||||
attacks = [(Fraction(n,2), Fraction(1,2)) for n in (0,3,4,5,7)]
|
||||
elif role == 'accompaniment' and measure['time_signature']=='4/4' and not measure.get('attributes'):
|
||||
attacks = [(Fraction(1,2),Fraction(1,2))]
|
||||
else:
|
||||
notes, sequence = rest_notes(measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
return {"id": "voice-arranged-guitar", "tuplets": [], "notes": notes}, sequence
|
||||
|
||||
|
||||
def arranged_measures(source_measures: list[dict], part_type: str) -> list[dict]:
|
||||
sections = full_arrangement_sections(source_measures, part_type)
|
||||
all_chords = propagated_chords(source_measures)
|
||||
required_chords = propagated_chords(source_measures, include_optional=False)
|
||||
output: list[dict] = []
|
||||
sequence = 0
|
||||
for section in sections:
|
||||
include_optional = section.section_index != FINAL_SECTION_INDEX
|
||||
chords = all_chords if include_optional else required_chords
|
||||
if part_type == "vocal" and section.role == "vocal":
|
||||
apply_vocal_pickup_overlap(output, source_measures, section)
|
||||
for offset in range(section.measure_count):
|
||||
source_index = section.source_measure_index + offset
|
||||
source_measure = source_measures[source_index]
|
||||
chord_symbol = chords[source_index] if source_index < len(chords) else ""
|
||||
voice, sequence = voice_for_section(section, source_measure, chord_symbol, sequence, include_optional=include_optional)
|
||||
output_measure = make_measure(
|
||||
f"measure-{len(output) + 1}",
|
||||
source_measure,
|
||||
voice.get("notes", []),
|
||||
section_label=section.label if offset == 0 else "",
|
||||
tuplets=voice.get("tuplets", []),
|
||||
)
|
||||
output_measure["voices"][0]["id"] = str(voice.get("id", output_measure["voices"][0]["id"]))
|
||||
output.append(output_measure)
|
||||
return output
|
||||
attacks = []
|
||||
states = [(Fraction(), measure['meter'])]
|
||||
states.extend((fraction(a['onset']), a['meter']) for a in measure.get('attributes', []) if 'meter' in a)
|
||||
for index, (onset, meter) in enumerate(states):
|
||||
end = states[index+1][0] if index+1 < len(states) else length
|
||||
beats = beat_lengths(meter)
|
||||
beat_index = 0
|
||||
while onset < end:
|
||||
beat = min(beats[beat_index % len(beats)] if beats else end-onset, end-onset)
|
||||
duration = beat if role == 'slash' or not beats else min(beat/2, Fraction(1,2))
|
||||
attacks.append((onset, duration))
|
||||
onset += beat
|
||||
beat_index += 1
|
||||
events = []
|
||||
cursor = Fraction()
|
||||
for onset, duration in attacks:
|
||||
if onset >= length:
|
||||
break
|
||||
if onset > cursor:
|
||||
events.append(make_event(f'rest-{len(events)}',cursor,onset-cursor,'rest'))
|
||||
duration = min(duration,length-onset)
|
||||
chord = active_chord(measure,onset,previous)
|
||||
source_attack = any(fraction(n['onset'])==onset and n['kind']=='note' and not n.get('grace') and any(not p.get('tie_from') for p in n['pitches']) for n in measure_notes(measure))
|
||||
if role=='slash':
|
||||
event = make_event(f'slash-{len(events)}',onset,duration,'note')
|
||||
event['notehead'] = 'slash'
|
||||
elif not chord or role=='accompaniment' and source_attack:
|
||||
event = make_event(f'rest-{len(events)}',onset,duration,'rest')
|
||||
else:
|
||||
pitches = pitches_for_voicing(chord)
|
||||
event = make_event(f'chord-{len(events)}',onset,duration,'note',pitches)
|
||||
if not pitches:
|
||||
warnings.warn(f'{measure["id"]}: no playable voicing for {chord["symbol"]}; using labelled rhythmic slashes', stacklevel=2)
|
||||
event['notehead'] = 'slash'
|
||||
events.append(event)
|
||||
cursor = onset + duration
|
||||
if cursor < length:
|
||||
events.append(make_event(f'rest-{len(events)}',cursor,length-cursor,'rest'))
|
||||
return events
|
||||
|
||||
|
||||
def arrange_document(document: dict, part_type: str) -> dict:
|
||||
if part_type not in PART_TYPES:
|
||||
raise ValueError(f'unknown arrangement part: {part_type}')
|
||||
document = concert_document(document)
|
||||
staff = source_staff(document)
|
||||
measures = staff.get("measures")
|
||||
if not isinstance(measures, list) or not measures:
|
||||
raise ValueError("performance arrangement requires at least one source measure")
|
||||
output = make_arranged_document(
|
||||
document,
|
||||
staff,
|
||||
staff_id="staff-arranged-vocal" if part_type == "vocal" else "staff-arranged-guitar",
|
||||
staff_name=part_type.capitalize(),
|
||||
role="vocal" if part_type == "vocal" else "guitar",
|
||||
arrangement_part_type=part_type,
|
||||
measures=arranged_measures([measure for measure in measures if isinstance(measure, dict)], part_type),
|
||||
)
|
||||
metadata = output.setdefault("metadata", {})
|
||||
if isinstance(metadata, dict):
|
||||
metadata.setdefault("tempo", 124)
|
||||
return output
|
||||
source = staff['measures']
|
||||
sections = full_arrangement_sections(source,part_type)
|
||||
output, tuplets, octave_shifts = [], [], []
|
||||
absolute = Fraction()
|
||||
source_starts = []
|
||||
source_time = Fraction()
|
||||
for m in source:
|
||||
source_starts.append(source_time)
|
||||
source_time += fraction(m['duration'])
|
||||
for section in sections:
|
||||
previous = propagated_chords(source, include_optional=section.section_index!=FINAL_SECTION_INDEX)
|
||||
segment = []
|
||||
for si in range(section.source_measure_index,section.source_measure_index+section.measure_count):
|
||||
m = copy.deepcopy(source[si])
|
||||
m['harmonies'] = [h for h in m['harmonies'] if section.section_index!=FINAL_SECTION_INDEX or not h.get('chord_optional')]
|
||||
for h in m['harmonies']:
|
||||
h.pop('chord_optional',None)
|
||||
notes = copy_source_voice(m,strip_lyrics=section.role=='melody')['notes'] if section.role in ('vocal','melody') else generated_voice(m,section.role,previous[si])
|
||||
if section.role == 'melody':
|
||||
for note in notes:
|
||||
note['pitches'] = [shift_pitch(p,7,12) for p in note['pitches']]
|
||||
result = make_measure(f'measure-{len(output)+len(segment)+1}',m,notes,section_label=section.label if not segment else '')
|
||||
if part_type!='vocal':
|
||||
result['clef'] = {'sign':'G','line':2,'octave_change':0}
|
||||
result['transpose'] = {'diatonic':0,'chromatic':0,'octave_change':-1}
|
||||
for change in result.get('attributes',[]):
|
||||
change.pop('clef',None)
|
||||
change.pop('transpose',None)
|
||||
segment.append(result)
|
||||
identity_segment = segment
|
||||
segment_length = sum((fraction(m['duration']) for m in segment),Fraction())
|
||||
if section.role in ('vocal','melody'):
|
||||
start = source_starts[section.source_measure_index]
|
||||
for shift in staff.get('octave_shifts',[]):
|
||||
low, high = max(start,fraction(shift['start'])), min(start+segment_length,fraction(shift['end']))
|
||||
if low < high:
|
||||
octave_shifts.append({**shift,'id':f'section-{section.section_index}-{shift["id"]}','start':rational(absolute+low-start),'end':rational(absolute+high-start)})
|
||||
if part_type=='vocal' and section.section_index in (1,5) and section.source_measure_index==1 and source[0].get('implicit') and output:
|
||||
pickup = copy.deepcopy(source[0])
|
||||
destination = output[-1]
|
||||
pad = fraction(destination['duration']) - fraction(pickup['duration'])
|
||||
if pad < 0:
|
||||
raise ValueError('pickup is longer than the preceding arrangement measure')
|
||||
notes = copy_source_voice(pickup,strip_lyrics=False)['notes']
|
||||
for n in notes:
|
||||
n['onset'] = rational(fraction(n['onset'])+pad)
|
||||
if pad:
|
||||
notes.insert(0,make_event('pickup-rest',Fraction(),pad,'rest'))
|
||||
destination['voices'][0]['notes'] = notes
|
||||
identity_segment = [destination, *segment]
|
||||
if pickup['harmonies']:
|
||||
destination['harmonies'] = [h for h in destination['harmonies'] if fraction(h['onset']) < pad]
|
||||
for h in pickup['harmonies']:
|
||||
h['onset'] = rational(fraction(h['onset'])+pad)
|
||||
destination['harmonies'].append(h)
|
||||
state = {k:copy.deepcopy(pickup[k]) for k in ('key','clef','meter','transpose')}
|
||||
if pad:
|
||||
destination['attributes'].append({'onset':rational(pad), **state})
|
||||
else:
|
||||
destination.update(state)
|
||||
destination['key_signature'] = key_label(state['key'])
|
||||
destination['time_signature'] = meter_label(state['meter'])
|
||||
for change in pickup.get('attributes',[]):
|
||||
destination['attributes'].append({**change,'onset':rational(fraction(change['onset'])+pad)})
|
||||
for shift in staff.get('octave_shifts',[]):
|
||||
low, high = fraction(shift['start']), min(fraction(pickup['duration']),fraction(shift['end']))
|
||||
if low < high:
|
||||
offset = absolute-fraction(pickup['duration'])
|
||||
octave_shifts.append({**shift,'id':f'pickup-{section.section_index}-{shift["id"]}','start':rational(offset+low),'end':rational(offset+high)})
|
||||
tuplets.extend(reidentify(identity_segment,staff.get('tuplets',[])))
|
||||
output.extend(segment)
|
||||
absolute += segment_length
|
||||
result = make_arranged_document(document,staff,staff_id='staff-arranged-'+part_type,staff_name=part_type.capitalize(),role='vocal' if part_type=='vocal' else 'guitar',arrangement_part_type=part_type,measures=output,tuplets=tuplets,octave_shifts=octave_shifts)
|
||||
result['metadata'].setdefault('tempo',124)
|
||||
return result
|
||||
|
||||
|
||||
def generate_performance_scores(source_dir: Path, output_dir: Path) -> int:
|
||||
if not source_dir.exists():
|
||||
raise ValueError(f"performance source directory not found: {source_dir}")
|
||||
raise ValueError(f'performance source directory not found: {source_dir}')
|
||||
count = 0
|
||||
for source_path in sorted(source_dir.glob("*.json")):
|
||||
document = load_json(source_path)
|
||||
for part_type in PART_TYPES:
|
||||
output = arrange_document(document, part_type)
|
||||
target = output_dir / f"{slugify(source_path.stem)}-{part_type}.json"
|
||||
write_json(target, output)
|
||||
for path in sorted(source_dir.glob('*.musicxml')):
|
||||
document = load_musicxml(path)
|
||||
for part in PART_TYPES:
|
||||
write_json(output_dir/f'{slugify(path.stem)}-{part}.json',arrange_document(document,part))
|
||||
count += 1
|
||||
return count
|
||||
|
||||
Reference in New Issue
Block a user