Skip to content

Instantly share code, notes, and snippets.

@Prince781
Last active April 9, 2019 15:48
Show Gist options
  • Save Prince781/ac6e715691126fb3a392a21643689671 to your computer and use it in GitHub Desktop.
Save Prince781/ac6e715691126fb3a392a21643689671 to your computer and use it in GitHub Desktop.
music set theory analysis
#!/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