In [ ]:
from __future__ import division
from itertools import chain
from random import choice, random, seed
from time import sleep, clock
import os
import math
import string
from pyknon.genmidi import Midi
from pyknon.music import Note, NoteSeq
# melody.py generates simple counterpoint melodies (cantus firmus + first species)
# Requirements:
# - pyknon (library used to generate midi tracks) - http://kroger.github.com/pyknon/
# - timidity (utility for playing midi tracks) - http://timidity.sourceforge.net/
# Helpers
def sign(x):
return math.copysign(1, x)
def remove_dupes(seq):
# f7 from http://www.peterbe.com/plog/uniqifiers-benchmark
seen = set()
seen_add = seen.add
return [ x for x in seq if x not in seen and not seen_add(x)]
# Constants for convenience
KEY_NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
KEY_NAMES_SHARP = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
P1 = C = I = Tonic = Unison = 0
m2 = Db = ii = 1
M2 = D = II = Step = 2
m3 = Eb = iii = 3
M3 = E = III = 4
P4 = F = IV = 5
d5 = Gb = Vo = Tritone = 6
P5 = G = V = 7
m6 = Ab = vi = 8
M6 = A = VI = 9
m7 = Bb = vii = 10
M7 = Bb = VII = LeadingTone = 11
P8 = O = Octave = 12
def save_midi(midi):
output_dir = os.path.dirname(os.path.abspath(__file__)) + "/static/output/"
mid_path = output_dir + "output.mid"
wav_name = ''.join(choice(string.ascii_uppercase + string.digits) for x in range(10))
wav_path = output_dir + wav_name + ".wav"
mp3_path = output_dir + wav_name + ".mp3"
try:
midi.write(mid_path)
os.system("timidity " + mid_path + " -Ow -o - >" + wav_path)
os.system("cat " + wav_path + " | ffmpeg -i - -acodec libmp3lame -ab 64k " + mp3_path + " >/dev/null 2>&1")
except Exception as e:
print e
return wav_name
def play_midi(midi):
current_dir = os.path.dirname(os.path.abspath(__file__))
mid_path = current_dir + "/output.mid"
os.system("timidity " + mid_path + " >/dev/null")
def get_runs_from_melody(melody):
# e.g.
# input: [0, 2, 4, 2, 0, 7, 5, -3, 2, 0]
# output: [[0, 2, 4], [4, 2, 0], [7, 5, -3]]
runs = []
for i in range(len(melody) - 2):
for j in range(i + 2, len(melody)):
trial_run = melody[i:j+1]
directions = [sign(trial_run[i+1] - trial_run[i]) for i in range(len(trial_run) - 1)]
all_directions_equal = (directions.count(directions[0]) == len(directions))
if all_directions_equal:
runs.append(tuple(trial_run))
runs = remove_dupes(runs)
# remove runs contained in other runs
contained_runs = []
for run1 in runs:
for run2 in runs:
if run1 != run2 and ''.join(map(str, run2)) in ''.join(map(str, run1)): # run2 contained in run1
if run2 not in contained_runs:
contained_runs.append(run2)
for run in contained_runs:
runs.remove(run)
return runs
def check_melody(m, type, verbose = False):
intervals = [m[i+1] - m[i] for i in range(len(m) - 1)]
directions = [sign(intervals[i]) for i in range(len(intervals))]
leaps = [x for x in intervals if abs(x) > Step]
notes = [x % Octave for x in m]
def no_repetition():
if type == 'cantus': # no repetition allowed in cantus firmus
if 0 not in intervals:
return True
else:
if verbose: print 'fail: no_repetition in cantus firmus: ' + str(m)
elif type == 'first_species': # one repetition allowed in first species
if intervals.count(0) <= 1:
return True
else:
if verbose: print 'fail: no_repetition in first species: ' + str(m)
def no_leaps_larger_than_octave():
# let's disallow octaves as well, because they rarely sound good
if not any([abs(x) >= P8 for x in leaps]):
return True
else:
if verbose: print 'fail: no_leaps_larger_than_octave in ' + str(m)
def no_dissonant_leaps():
consonant = [M3, P4, P5, m6, P8]
if not any([abs(x) not in consonant for x in leaps]):
return True
else:
if verbose: print 'fail: no_dissonant_leaps in ' + str(m)
def between_two_and_four_leaps():
if len(leaps) in [2, 3, 4]:
return True
else:
if verbose: print 'fail: between_two_and_four_leaps in ' + str(m)
def has_climax():
# climax can't be on tonic or leading tone
climax = max(m)
position = [i for i, j in enumerate(m) if j == climax][0]
if climax%Octave not in [Tonic, LeadingTone] and m.count(climax) == 1 and (position + 1) != (len(m) - 1):
return True
else:
if verbose: print 'fail: has_climax in ' + str(m)
def changes_direction_several_times():
directional_changes = [intervals[i+1] - intervals[i] for i in range(len(m) - 2)]
if len([x for x in directional_changes if x < 0]) >= 2:
return True
else:
if verbose: print 'fail: changes_direction_several_times in ' + str(m)
def no_note_repeated_too_often():
for note in notes:
if notes.count(note) > 3:
if verbose: print 'fail: no_note_repeated_too_often in ' + str(m)
return False
return True
def final_note_approached_by_step():
if abs(m[-1] - m[-2]) <= Step:
return True
else:
if verbose: print 'fail: final_note_approached_by_step in ' + str(m)
def larger_leaps_followed_by_change_of_direction():
for i in range(len(m) - 2):
if abs(intervals[i]) > 4 and directions[i] == directions[i + 1]:
if verbose: print 'fail: larger_leaps_followed_by_change_of_direction in ' + str(m)
return False
return True
def leading_note_goes_to_tonic():
for i in range(len(m) - 1):
if m[i] %12 == 11 and m[i+1]%12 != 0:
if verbose: print 'fail: leading_note_goes_to_tonic in ' + str(m)
return False
return True
def no_more_than_two_consecutive_leaps_in_same_direction():
for i in range(len(m) - 2):
if abs(intervals[i]) > Step and abs(intervals[i + 1]) > Step and directions[i] == directions[i + 1]:
if verbose: print 'fail: no_more_than_two_consecutive_leaps_in_same_direction in ' + str(m)
return False
return True
def no_same_two_intervals_in_a_row():
for i in range(len(m) - 2):
if intervals[i] > Step and intervals[i] == - intervals[i + 1]:
if verbose: print 'fail: no_same_two_intervals_in_a_row in ' + str(m)
return False
return True
def no_noodling():
for i in range(len(m) - 3):
if intervals[i] == - intervals[i + 1] and intervals[i + 1] == - intervals[i + 2]:
if verbose: print 'fail: no_noodling in ' + str(m)
return False
return True
def no_long_runs():
runs = get_runs_from_melody(m)
for run in runs:
if len(run) > 4:
if verbose: print 'fail: no_long_runs in ' + str(m) + ' : ' + str(runs)
return False
return True
def no_unresolved_melodic_tension():
consonant_movements = [m3, M3, P4, P5, m6, P8]
runs = get_runs_from_melody(m)
for run in runs:
movement = abs(run[0] - run[-1])
if movement not in consonant_movements:
if verbose: print 'fail: no_unresolved_melodic_tension in ' + str(m) + ' : ' + str(runs)
return False
return True
def no_sequences():
triples = [m[i:i+3] for i in range(len(m)-2)]
normalized_triples = [(0, t[1]-t[0], t[2]-t[0]) for t in triples]
if len(normalized_triples) == len(set(normalized_triples)): # no duplicates
return True
else:
if verbose: print 'fail: no_sequences in ' + str(m) + ' : ' + str(normalized_triples)
return False
return no_note_repeated_too_often() and leading_note_goes_to_tonic() and no_same_two_intervals_in_a_row() and no_repetition() and larger_leaps_followed_by_change_of_direction() and no_dissonant_leaps() and no_leaps_larger_than_octave() and no_noodling() and between_two_and_four_leaps() and has_climax() and final_note_approached_by_step() and no_more_than_two_consecutive_leaps_in_same_direction() and changes_direction_several_times() and no_long_runs() and no_unresolved_melodic_tension() and no_sequences()
def check_first_species(cantus, first_species, verbose = False):
vertical_intervals = [abs(cantus[i] - first_species[i]) for i in range(len(cantus))]
v_i = vertical_intervals
def no_dissonant_intervals():
consonant = [Unison, m3, M3, P5, m6, M6]
return not any([(x % Octave) not in consonant for x in vertical_intervals])
def no_intervals_larger_than_12th():
return not any([x > (P8 + P5) for x in vertical_intervals])
def no_parallel_fifths_or_octaves():
for i in range(len(cantus) - 1):
if (v_i[i] == P5 and v_i[i+1] == P5) or (v_i[i] == P8 and v_i[i+1] == P8):
if verbose: print 'fail: no_parallel_fifths_or_octaves in vertical intervals: ' + str(vertical_intervals)
return False
return True
def no_parallel_chains():
for i in range(len(cantus) - 1):
if v_i[i] == v_i[i+1] and v_i[i+1] == v_i[i+2] and v_i[i+2] == v_i[i+3]:
if verbose: print 'fail: no_parallel_chains in vertical intervals: ' + str(vertical_intervals)
return False
return True
return no_parallel_fifths_or_octaves() and no_parallel_chains() and no_dissonant_intervals() and no_intervals_larger_than_12th()
def pick_melody(length, start_note = I, cantus = None): # pass cantus if this is a species melody
registers = {
'major': [IV-O, V-O, VI-O, VII-O, I, II, III, IV, V, VI, VII, I+O, II+O, III+O, IV+O],
'minor': [IV-O, V-O, vi-O, vii-O, I, II, iii, IV, V, vi, vii, I+O, II+O, iii+O, IV+O]
}
tonality = 'major' # avoiding minor for now
register = registers[tonality]
melody = [start_note]
for i in range(length - 2):
if cantus:
allowed_intervals = [Unison, m2, M2, M3, P4, P5, m6, P8]
allowed_vertical_intervals = [m3, M3, P5, m6, M6]
# do some optimizing now to pass the first species tests for middle notes
available_notes = [x for x in register if abs(x - melody[-1]) in allowed_intervals
and x > cantus[i+1] # no cross-over
and abs(x - cantus[i+1]) % Octave in allowed_vertical_intervals # consonant vertical interval
and abs(x - cantus[i+1]) < (P8 + P5) # no vertical interval larger than 12th
]
if len(available_notes) > 0:
note = choice(available_notes)
else:
return False, 'none'
else:
allowed_intervals = [m2, M2, M3, P4, P5, m6]
note = choice([x for x in register if abs(x - melody[-1]) in allowed_intervals])
melody.append(note)
melody.append(choice([I, I + Octave]))
return melody, tonality
def random_transpose(melodies):
key = choice([C, D, Eb, F, G, A-O, Bb-O])
return [[x + key for x in melody] for melody in melodies], key % Octave
def display_melody(melodies, raw_melodies, tonality, time_elapsed, tries, cantus, first_species):
def pack_melody(melody, key):
return str(sum([(x + 12) * pow(24, i) for i, x in enumerate(melody + [key])]))
key = melodies[0][0] % Octave
packed_melodies = '-'.join([pack_melody(m, key) for m in raw_melodies])
print
print '=== Melody #' + packed_melodies + ' ==='
print 'Key of ' + KEY_NAMES[key] + ' ' + tonality.capitalize()
# display each melody in a separate line
for i in range(len(melodies)):
print ' '.join([KEY_NAMES[x%Octave] + str(4 + (x+3)//Octave) for x in melodies[i]]) + ' - ' + str(raw_melodies[i])
# display information about the melody
vertical_intervals = [abs(cantus[i] - first_species[i]) for i in range(len(cantus))]
print 'Vertical Intervals: ' + str(vertical_intervals)
# print 'Runs: ' + str(get_runs_from_melody(raw_melodies[0]))
# display timing information
print str(time_elapsed) + ' secs taken (' + ' + '.join(["{:,d}".format(m) for m in tries]) + ' melodies tried)'
print
def midi_from_melodies(melodies):
notes = [[Note(x%Octave, 4 + x//Octave, 1/4) for x in melody] for melody in melodies]
chords = [NoteSeq([melody_notes[i] for melody_notes in notes]) for i in range(len(melody))]
midi = Midi(tempo=120)
midi.seq_chords(chords)
return midi
random_seed = random()
print 'Seed: %.16f' % random_seed
seed(random_seed)
def generate_melody(server = False):
melodies = []
try_again = False
# start timing
start_time = clock()
tries = [0, 0]
# generate cantus firmus (base melody)
while True:
tries[0] += 1
cantus, tonality = pick_melody(10, Tonic)
if cantus and check_melody(cantus, 'cantus'):
break
melodies.append(cantus)
# generate first species above cantus firmus
attempts = set()
set_matches = 0
while True:
tries[1] += 1
if tries[1] >= 50000:
try_again = True
break
first_species, tonality = pick_melody(10, choice([P5, Octave]), cantus)
if first_species:
if tuple(first_species) not in attempts:
attempts.add(tuple(first_species))
if check_melody(first_species, 'first_species') and check_first_species(cantus, first_species):
break
else:
# if we see, say, 2000 repeated attempts, we're probably tried everything there is to try
set_matches += 1
if set_matches >= 2000:
try_again = True
break
if try_again:
if not server:
print 'No matching first species found for this cantus firmus: ' + str(cantus)
print str(clock() - start_time) + ' secs taken (' + ' + '.join(["{:,d}".format(m) for m in tries]) + ' melodies tried)'
print
return False, None
melodies.append(first_species)
# finish timing
time_elapsed = clock() - start_time
# transpose to random valid key
transposed_melodies, key = random_transpose(melodies)
# display notes and generate midi
midi = midi_from_melodies(transposed_melodies)
filename = save_midi(midi)
if server:
# return pitches and key signature, to create sheet music
# get proper enharmonics based on key signature
if 'b' in KEY_NAMES[key] or key == F:
note_names = KEY_NAMES
else:
note_names = KEY_NAMES_SHARP
pitches = []
for i in range(len(transposed_melodies)):
pitches.append([note_names[x%Octave].lower() + "/" + str(4 + x//Octave) for x in transposed_melodies[i]])
lower_voice = list(pitches[0])
upper_voice = list(pitches[1])
key_signature = KEY_NAMES[key] if tonality == 'major' else KEY_NAMES[key] + "m"
return True, (filename, lower_voice, upper_voice, key_signature, time_elapsed, tries)
else:
# display melody in console and play the MIDI file
display_melody(transposed_melodies, melodies, tonality, time_elapsed, tries, cantus, first_species)
play_midi(midi)
if __name__ == "__main__":
while True:
generate_melody()