from __future__ import annotations import copy import generator.harmony import generator.model def shift_pitch(pitch: dict, steps: int, semitones: int) -> dict: output = copy.deepcopy(pitch) if 'unpitched' in pitch: return output original_octave = pitch.get('octave', 4) diatonic = original_octave * 7 + generator.model.LETTERS.index(pitch['letter']) + steps octave, index = divmod(diatonic, 7) output['letter'] = generator.model.LETTERS[index] output['octave'] = octave output['alter'] = generator.model.rational(generator.model.pitch_number({**pitch, 'octave': original_octave}) + semitones - (12 * (octave + 1) + generator.model.NATURAL[output['letter']])) if 'octave' not in pitch: output.pop('octave') if 'accidental_display' in output: output['accidental_display']['name'] = generator.model.accidental_name(generator.model.alteration(output['alter'])) return output def shift_key(key: dict, steps: int, semitones: int) -> dict: tonic = shift_pitch(key['tonic'], steps, semitones) if key.get('tonic') else None alterations = [0] * 7 for index, letter in enumerate(generator.model.LETTERS): p = shift_pitch({'letter': letter, 'alter': key['alterations'][index]}, steps, semitones) alterations[generator.model.LETTERS.index(p['letter'])] = p['alter'] result = {**copy.deepcopy(key), 'tonic': tonic, 'alterations': alterations} if 'steps' in key: result['steps'] = [shift_pitch(step, steps, semitones) for step in key['steps']] if not key.get('custom') and tonic: for fifths in range(-14, 15): candidate = generator.model.key_from_fifths(fifths, key['mode']) if candidate['tonic'] == tonic and candidate['alterations'] == alterations: result['fifths'] = fifths break else: result['custom'] = True if 'cancel' in key: # A cancellation references the old signature, not a count of naturals. canceled = shift_key(generator.model.key_from_fifths(key['cancel']), steps, semitones) if canceled.get('custom'): raise ValueError('transposed key cancellation cannot be represented by fifths') result['cancel'] = canceled['fifths'] return result def concert_document(document: dict, *, octave_transposing: bool = False) -> dict: """Resolve source instrument pitch once, before arranging in concert pitch.""" output = copy.deepcopy(document) zero = {'diatonic': 0, 'chromatic': 0, 'octave_change': 0, 'explicit': True} for staff in output['staves']: for measure in staff['measures']: initial = {key: measure[key] for key in ('transpose', 'clef')} changes = copy.deepcopy(measure.get('attributes', [])) def interval(onset): state = dict(initial) for change in changes: if generator.model.fraction(change['onset']) <= onset: state.update({key: change[key] for key in initial if key in change}) transpose, clef = state['transpose'], state['clef'] steps = transpose['diatonic'] + 7 * transpose['octave_change'] semitones = transpose['chromatic'] + 12 * transpose['octave_change'] if octave_transposing and not transpose.get('explicit') and not (steps or semitones) and not clef['octave_change']: return -7, -12 return steps, semitones measure['key'] = shift_key(measure['key'], *interval(0)) measure['key_signature'] = generator.model.key_label(measure['key']) for n in (n for v in measure['voices'] for n in v['notes']): n['pitches'] = [shift_pitch(p, *interval(generator.model.fraction(n['onset']))) for p in n['pitches']] for h in measure['harmonies']: for field in ('root', 'bass'): if field in h: h[field] = shift_pitch(h[field], *interval(generator.model.fraction(h['onset']))) h['symbol'] = generator.harmony.symbol(h) for change in measure.get('attributes', []): if 'key' in change: change['key'] = shift_key(change['key'], *interval(generator.model.fraction(change['onset']))) if 'transpose' in change: change['transpose'] = copy.deepcopy(zero) measure['transpose'] = copy.deepcopy(zero) first = output['staves'][0]['measures'][0] output['metadata']['key'] = copy.deepcopy(first['key']) output['metadata']['key_signature'] = first['key_signature'] return output def sounding_pitch(pitch: dict, state: dict): if 'unpitched' in pitch: raise ValueError('unpitched notes have no fretted pitch interpretation') transpose = state['transpose'] return generator.model.pitch_number(pitch) + transpose['chromatic'] + 12 * transpose['octave_change']