Last active
April 9, 2019 15:48
-
-
Save Prince781/ac6e715691126fb3a392a21643689671 to your computer and use it in GitHub Desktop.
music set theory analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env python3 | |
import argparse | |
import re | |
import math | |
def get_pitch_class(note): | |
try: | |
p = int(note) | |
return p | |
except: | |
if note == 't': | |
return 10 | |
if note == 'e': | |
return 11 | |
# otherwise, this is not a pitch class | |
pattern = re.compile(r'^([CDEFGABcdefgab])(b|x|bb|#|##)?(\d+)?$') | |
m = pattern.match(note) | |
if not m: | |
raise Exception(f"not {note} doesn't conform to regex {pattern}") | |
base_pitch, accidentals, octave = m.groups() | |
base_pitch = base_pitch.upper() | |
pitch = None | |
if base_pitch == 'C': | |
pitch = 0 | |
elif base_pitch == 'D': | |
pitch = 2 | |
elif base_pitch == 'E': | |
pitch = 4 | |
elif base_pitch == 'F': | |
pitch = 5 | |
elif base_pitch == 'G': | |
pitch = 7 | |
elif base_pitch == 'A': | |
pitch = 9 | |
elif base_pitch == 'B': | |
pitch = 11 | |
if accidentals == '#' or accidentals == u"\u266F": | |
pitch += 1 | |
elif accidentals == 'b' or accidentals == u"\u266D": | |
pitch -= 1 | |
elif accidentals == 'x' or accidentals == u"\U0001D12A" or accidentals == '##': | |
pitch += 2 | |
elif accidentals == 'bb' or accidentals == u"\U0001D12B": | |
pitch -= 2 | |
if octave: | |
pitch += 12 * int(octave) | |
return pitch | |
def pitch_to_note(p): | |
return ['C', 'Db/C#', 'D', 'Eb/D#', 'E', 'F', 'Gb/F#', 'G', 'Ab/G#', 'A', 'Bb/A#', 'B'][p % 12] | |
def get_pitches(string): | |
pitch_set = string | |
pitch_set = string.split(',') | |
return sorted(list({get_pitch_class(n) for n in pitch_set if n})) | |
def get_pitches2(string): | |
pitch_set = string | |
pitch_set = string.split(',') | |
return [get_pitch_class(n) for n in pitch_set if n] | |
def compare_sets(pitches1, pitches2): | |
for dyad1,dyad2 in zip(zip(pitches1, pitches1[1:] + [pitches1[0]]), zip(pitches2, pitches2[1:] + [pitches2[0]])): | |
# print(f'comparing {dyad1} with {dyad2}') | |
diff1 = (dyad1[1] - dyad1[0]) % 12 | |
diff2 = (dyad2[1] - dyad2[0]) % 12 | |
if diff2 < diff1: | |
return 1 # pitches1 > pitches2 | |
elif diff1 < diff2: | |
return -1 # pitches1 < pitches2 | |
return 0 | |
def best_span(pitches): | |
spans = [] | |
for i,j in zip(range(0,len(pitches)), range(-1,len(pitches)-1)): | |
p1 = pitches[i] | |
p2 = pitches[j] | |
spans.append((i, j, (p2 - p1) % 12)) | |
# print(f'{p1} -> {p2}: {(p2 - p1) % 12}') | |
spans = sorted(spans, key=lambda x: x[2]) | |
best_span = spans[0] | |
for span in spans[1:]: | |
a1,b1,l1 = best_span | |
a2,b2,l2 = span | |
if l2 == l1: | |
# compare intervals | |
if compare_sets(pitches[a1:] + pitches[:b1+1], pitches[a2:] + pitches[:b2+1]) > 0: | |
best_span = span | |
else: # l2 must always be greater in this case | |
break | |
return best_span | |
def transpose(pitches, ival): | |
return sorted(transpose2(pitches, ival)) | |
def transpose2(pitches, ival): | |
return [(p + ival) % 12 for p in pitches] | |
def invert(pitches, around=None): | |
return sorted(invert2(pitches, around)) | |
def invert2(pitches, around=None): | |
around = 0 if not around else around | |
return [(around-p) % 12 for p in pitches] | |
def normal_form(pitches): | |
i,j,l = best_span(pitches) | |
normal_pitches = pitches[i:] + pitches[:j+1] | |
# print(f'best span = {pitches}') | |
return normal_pitches | |
def prime_form(pitches): | |
normal_pitches = normal_form(pitches) | |
# transpose so that the first pitch is 0 | |
normal_pitches = transpose(normal_pitches, -normal_pitches[0]) | |
# invert | |
inverted_pitches = invert(normal_pitches) | |
# print(f'inverted = {inverted_pitches}') | |
inverted_pitches = normal_form(inverted_pitches) | |
# transpose so that the first pitch is 0 | |
inverted_pitches = transpose(inverted_pitches, -inverted_pitches[0]) | |
if compare_sets(normal_pitches, inverted_pitches) > 0: | |
return transpose(inverted_pitches, -inverted_pitches[0]) | |
return normal_pitches | |
def inv_relation(pitches1, pitches2): | |
sums = {} | |
if len(pitches1) != len(pitches2): | |
return None | |
for p1 in pitches1: | |
for p2 in pitches2: | |
if not (p1+p2)%12 in sums: | |
sums[(p1+p2)%12] = 1 | |
else: | |
sums[(p1+p2)%12] += 1 | |
x = [s for s,c in sums.items() if c == len(pitches1)] | |
if not x: | |
return None | |
return x[0] | |
scales = {\ | |
'oct': [[0,1,3,4,6,7,9,10], [1,2,4,5,7,8,10,11], [2,3,5,6,8,9,11,0]],\ | |
'whole': [[0,2,4,6,8,10], [1,3,5,7,9,11]],\ | |
'hex': [[0,1,4,5,8,9],[1,2,5,6,9,10],[2,3,6,7,10,11],[3,4,7,8,11,0]],\ | |
} | |
scales.update([(f'ionian-{pitch_to_note(p)}', [transpose2([0,2,4,5,7,9,11], p)]) for p in range(0,11)]) | |
scales.update([(f'dorian-{pitch_to_note(p)}', [transpose2([0,2,3,5,7,9,10], p)]) for p in range(0,11)]) | |
scales.update([(f'phrygian-{pitch_to_note(p)}', [transpose2([0,1,3,5,7,8,10], p)]) for p in range(0,11)]) | |
scales.update([(f'lydian-{pitch_to_note(p)}', [transpose2([0,2,4,6,7,9,11], p)]) for p in range(0,11)]) | |
scales.update([(f'mixolydian-{pitch_to_note(p)}', [transpose2([0,2,4,5,7,9,10], p)]) for p in range(0,11)]) | |
scales.update([(f'aeolian-{pitch_to_note(p)}', [transpose2([0,2,3,5,7,8,10], p)]) for p in range(0,11)]) | |
scales.update([(f'locrian-{pitch_to_note(p)}', [transpose2([0,1,3,5,6,8,10], p)]) for p in range(0,11)]) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('pitch_set') | |
parser.add_argument('-c','--compare') | |
parser.add_argument('-m', '--matrix', action='store_true') | |
parser.add_argument('-v', '--verbose', action='store_true') | |
args = parser.parse_args() | |
pitches = get_pitches(args.pitch_set) | |
mod_pitches = [p % 12 for p in pitches] | |
notes = get_pitches2(args.pitch_set) | |
print(f'Post-Tonal Analyses') | |
print(f'\tpitch classes: {notes}') | |
print(f'\tnormal form: {normal_form(pitches)}') | |
print(f'\tprime form: {prime_form(pitches)}') | |
if len(pitches) > 0: | |
p = sum(pitches) / len(pitches) | |
ceil = int(math.ceil(p)) | |
floor = int(math.floor(p)) | |
if ceil != floor: | |
print(f'\taxis of symmetry: {p} ({pitch_to_note(floor)} - {pitch_to_note(ceil)})') | |
else: | |
p = int(p) | |
print(f'\taxis of symmetry: {p} ({pitch_to_note(floor)})') | |
scales_text = [] | |
for _type,ls in scales.items(): | |
for s in ls: | |
name = f'{_type}({s})' | |
if set(mod_pitches) < set(s): | |
scales_text.append(f'subset of {name}') | |
elif set(mod_pitches) == set(s): | |
scales_text.append(f'equal to {name}') | |
print('\tscales: ' + '\n\t\t'.join(scales_text)) | |
print('\tnotes: ' + ','.join([pitch_to_note(p) for p in notes])) | |
if args.compare: | |
pitches2 = get_pitches(args.compare) | |
print(f'\tpitches 2 normal form: {normal_form(pitches2)}') | |
print(f'\tpitches 2 prime form: {prime_form(pitches2)}') | |
print(f'\tinversion relation: {inv_relation(pitches, pitches2)}') | |
if args.matrix: | |
mod_notes = [n % 12 for n in notes] | |
print(f'\nMatrix for {mod_notes}:') | |
pitches0 = transpose2(mod_notes, -mod_notes[0]) | |
print(' I:') | |
side_left = False | |
side_right = False | |
for p in invert2(pitches0): | |
if not side_left: | |
print('P: ', end='') | |
side_left = True | |
else: | |
print(' ', end='') | |
print(' '.join([str(n) if (n != 10 and n != 11) else ('T' if n == 10 else 'E') for n in transpose2(mod_notes, p - mod_notes[0])]), end='') | |
if not side_right: | |
print(' (R) ') | |
side_right = True | |
else: | |
print('') | |
print(' (RI)') | |
if args.verbose: | |
print('') | |
for i in range(0, 11): | |
row = transpose2(pitches0, i) | |
print(f' P{i}: {" ".join([pitch_to_note(p) for p in row])}{" (original idea)" if row == mod_notes else ""}') | |
print(f' R{row[-1]}: {" ".join([pitch_to_note(p) for p in reversed(row)])}') | |
inv = invert2(row) | |
print(f' I{inv[0]}: {" ".join([pitch_to_note(p) for p in inv])}') | |
print(f' RI{inv[-1]}: {" ".join([pitch_to_note(p) for p in reversed(inv)])}') | |
print('') | |
# TODO: analyze scales |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment