@@ -12,10 +12,9 @@ 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')
|
||||
PART_TYPES = ('acoustic', 'electric', 'vocal')
|
||||
FINAL_SECTION_INDEX = 6
|
||||
|
||||
|
||||
def propagated_chords(measures: list[dict], *, include_optional: bool = True) -> list[dict | None]:
|
||||
result = []
|
||||
current = None
|
||||
@@ -26,7 +25,6 @@ def propagated_chords(measures: list[dict], *, include_optional: bool = True) ->
|
||||
current = h
|
||||
return result
|
||||
|
||||
|
||||
def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict | None:
|
||||
active = previous
|
||||
for h in measure['harmonies']:
|
||||
@@ -35,26 +33,25 @@ def active_chord(measure: dict, onset: Fraction, previous: dict | None) -> dict
|
||||
active = h
|
||||
return active if active and active['kind'] != 'none' else None
|
||||
|
||||
|
||||
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))]
|
||||
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:
|
||||
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
|
||||
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))
|
||||
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
|
||||
@@ -64,35 +61,34 @@ def generated_voice(measure: dict, role: str, previous: dict | None) -> list[dic
|
||||
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')
|
||||
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')
|
||||
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)
|
||||
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'))
|
||||
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)
|
||||
source = staff['measures']
|
||||
sections = full_arrangement_sections(source,part_type)
|
||||
sections = full_arrangement_sections(source, part_type)
|
||||
output, tuplets, octave_shifts = [], [], []
|
||||
absolute = Fraction()
|
||||
source_starts = []
|
||||
@@ -101,80 +97,79 @@ def arrange_document(document: dict, part_type: str) -> dict:
|
||||
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)
|
||||
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):
|
||||
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')]
|
||||
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])
|
||||
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)
|
||||
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'):
|
||||
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']))
|
||||
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:
|
||||
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']
|
||||
notes = copy_source_voice(pickup, strip_lyrics=False)['notes']
|
||||
for n in notes:
|
||||
n['onset'] = rational(fraction(n['onset'])+pad)
|
||||
n['onset'] = rational(fraction(n['onset']) + pad)
|
||||
if pad:
|
||||
notes.insert(0,make_event('pickup-rest',Fraction(),pad,'rest'))
|
||||
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)
|
||||
h['onset'] = rational(fraction(h['onset']) + pad)
|
||||
destination['harmonies'].append(h)
|
||||
state = {k:copy.deepcopy(pickup[k]) for k in ('key','clef','meter','transpose')}
|
||||
state = {k: copy.deepcopy(pickup[k]) for k in ('key', 'clef', 'meter', 'transpose')}
|
||||
if pad:
|
||||
destination['attributes'].append({'onset':rational(pad), **state})
|
||||
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']))
|
||||
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',[])))
|
||||
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)
|
||||
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}')
|
||||
count = 0
|
||||
for path in sorted(source_dir.glob('*.musicxml')):
|
||||
for path in sorted(source_dir.glob('*.xml')):
|
||||
document = load_musicxml(path)
|
||||
for part in PART_TYPES:
|
||||
write_json(output_dir/f'{slugify(path.stem)}-{part}.json',arrange_document(document,part))
|
||||
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