import numpy as np
import math
import random
from keras.models import Sequential
from keras.layers import Dense, Activation

# Chord accompaniment
# From a 4 notes melody predicts the accompanying chord
# This is a simplistic pedagogical example
# Jean-Pierre Briot, UNIRIO, 05/09/2018

# This is the one-hot encoded version

# Structures

# Input : Melody with 4 notes : One-hot encoding
# Output : Chord : represented by its root note : One-hot encoding

# Notes
# Reference : C = C3 : 60

# List of note names
note_name_list = ['DO', 'DO#', 'RE', 'RE#', 'MI', 'FA', 'FA#', 'SOL', 'SOL#', 'LA', 'LA#', 'SI']
# English notation: = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

# Dictionary making correspondence between a note name and its value
note_value_dictionary = {'DO': 0, 'DO#': 1, 'RE': 2, 'RE#': 3, 'MI': 4, 'FA': 5, 'FA#': 6, 'SOL': 7, 'SOL#': 8, 'LA': 9, 'LA#': 10, 'SI': 11}

# Note values are MIDI note numbers
# DO : C1 : 0 : 8.1757989156 Hz
# These are the lowest MIDI notes, very low ! But they are easier to understand because of the 12 modulo operation

#  List of note values
note_value_list = list(note_value_dictionary.values())			# -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

# number of notes (12)
number_notes = len(note_name_list)

# Note and melody manipulation functions

# From a symbolic note name to the note value
# Ex: note_name_to_value('A') -> (69)
def note_name_to_value(note_name):
	return(note_name_list.index(note_name))

# From a note value to the symbolic note name
# Ex: note_value_to_name(69) -> 'A'
# math.fmod is used for modulo (12)
def note_value_to_name(note_value):
	return(note_name_list[round(math.fmod(round(note_value), 12))])

# From a melody (list) of symbolic notes to the melody list of notes (pitch) values
# Ex: melody_notes_names_to_values(['D', 'FA#', 'LA', DO#']) -> [62, 66, 69, 61]
# In Python the first element of a collection (list, array...) has 0 as its index
def melody_names_to_values(melody_names):
	return([note_name_to_value(melody_names[0]), note_name_to_value(melody_names[1]), note_name_to_value(melody_names[2]), note_name_to_value(melody_names[3])])

# Example of melody serving as a basis for constructing other melodies
# Corresponds to the tetrachord (4 notes) of a DO Major chord with its major 7th (1st = DO, 3rd major = MI, 5th = SOL, 7th major = SI)
do_major_chord_melody_names = ['DO', 'MI', 'SOL', 'SI']

# Transposition function for a note name
# Ex : transpose_note_name('DO', 2) -> 'RE'
# The arithmetic and modulo operation is done on the corresponding values
def transpose_note_name(note_name, transposition_value):
	return(note_value_to_name(note_name_to_value(note_name) + transposition_value))

# Transposition function for a melody notes names
# Ex : transpose_melody_names['DO', 'MI', 'SOL', 'SI'], 2) -> ['RE', 'FA#', 'LA', 'DO#']
def transpose_melody_names(melody_names, transposition_value):
	return([transpose_note_name(melody_names[0], transposition_value), transpose_note_name(melody_names[1], transposition_value), transpose_note_name(melody_names[2], transposition_value), transpose_note_name(melody_names[3], transposition_value)])
	
# Data set construction
# For symbolic note names
# X_melody : List of melody examples - each example melody will be an input of the network
# y_chord : List of corresponding chord examples - the predictions to be made - each example chord will be an output of the network
# Both are initially empty lists
X_melody = []
y_chord = []

# Generate and append all transpositions of the do_major_chord_melody_names melody (['DO', 'MI', 'SOL', 'SI']) and of the corresponding chord ('DO')
for transposition_value in range(12):
	X_melody.append(transpose_melody_names(do_major_chord_melody_names, transposition_value))
	y_chord.append(transpose_note_name('DO', transposition_value))

# Generate and append possible permutations
# Ex : ['DO', 'MI', 'SOL', 'SI'] -> ['SOL', 'DO, 'SI', 'SOL']
# random.sample(list, n) creates a list of length n with random permutations of the elements from list
# random.sample(X_melody[i], 4) creates a random permutation of the notes names of the ith example melody
max_permutations = 2		# max : 4! = 24

for i in range(len(X_melody)):
	for n in range(max_permutations):
		X_melody.append(random.sample(X_melody[i], 4))
		y_chord.append(y_chord[i])

# One-hot encoding

# One hot encoding of a note name (into a list, not an array)
# Ex: note_name_to_one_hot_encoding('RE') -> [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def note_name_to_one_hot_encoding(note_name):
	index = note_name_list.index(note_name)
	note_one_hot_list = []
	for i in range(number_notes):
		if i == index:
			note_one_hot_list.append(1)
		else:
			note_one_hot_list.append(0)
	return(note_one_hot_list)

# One hot encoding of a melody (list of note names) (into a list, not an array)
# Ex: melody_names_to_one_hot_encoding(['DO', 'RE', 'MI', 'FA']) -> [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]]
def melody_names_to_one_hot_encoding(melody_names):
	melody_one_hot_list = []
	for i in range(len(melody_names)):
		melody_one_hot_list.append(note_name_to_one_hot_encoding(melody_names[i]))
	return(flatten_list(melody_one_hot_list))

# Flatten the list of an encoded melody : [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ... [0, 0 ... 0]] -> [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0, 0 ... 0]
def flatten_list(list_of_list):
	return(sum(list_of_list, []))

# Undecoding the one-hot encoding
# Ex: one_hot_encoding_to_note_name(np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])) -> 'RE'
# This the deterministic interpretation : selecting the note with the highest probability
# argmax returns the index of the array with the highest value
# The argument is a numpy array because we will decode the result of the prediction which is a numpy array (np.array)
def one_hot_encoding_to_note_name(note_one_hot_encoding):
	return(note_name_list[np.argmax(note_one_hot_encoding)])

# Data set creation by transforming note names into one-hot encoding
X_train = []
y_train = []

# Create lists of one-hot encoded melodies and chords
for i in range(len(X_melody)):
	melody_names = X_melody[i]
	flattened_encoded_melody = melody_names_to_one_hot_encoding(melody_names)
	X_train.append(flattened_encoded_melody)
	chord_name = y_chord[i]
	encoded_chord = note_name_to_one_hot_encoding(chord_name)
	y_train.append(encoded_chord)

# Encoding in numpy arrays, the data type used by neural networks
X_train = np.array(X_train)
y_train = np.array(y_train)

# Input size : 4 notes melody : each one-hot encoded (list or array of size number_notes = 12)
input_size = 4 * number_notes		# 4 * 12 = 48

# Output size : Chord : Note : one-hot encoded
output_size = number_notes 		# 12

# Hidden layer sizes
# 2 hidden layers
hidden1_size = 50
hidden2_size = 50

# Definition of the model (the deep network architecture)
# Sequential : a succession of layers
model = Sequential()

# First layer
# The size of the input interface is provided
model.add(Dense(hidden1_size,
    		input_dim = input_size))
# Activation function of the first layer
model.add(Activation('sigmoid'))
# Second layer
model.add(Dense(hidden2_size))
# Activation function of the second layer
model.add(Activation('sigmoid'))
# Third (output) layer
model.add(Dense(output_size))
# Activation of the output layer is softmax : This is a classification task
model.add(Activation('softmax'))

# Creation of the model
model.compile(loss = 'categorical_crossentropy',
	optimizer = 'adam')

# Training the model

model.fit(x = X_train,
	y = y_train)

# First layer
# The size of the input interface is provided
model.add(Dense(hidden1_size,
    		input_dim = input_size))
# Activation function of the first layer
model.add(Activation('tanh'))
# Second layer
model.add(Dense(hidden2_size))
# Activation function of the second layer
model.add(Activation('tanh'))
# Third (output) layer
model.add(Dense(output_size))

# Creation of the model
model.compile(loss = 'mean_squared_error',
	optimizer = 'sgd')

# Training the model

model.fit(x = X_train,
	y = y_train,
	epochs = 100,
	steps_per_epoch = 1000)

# Prediction by the model

def predict_chord(melody_names):
# One-hot encoding of the melody
	flattened_encoded_melody = melody_names_to_one_hot_encoding(melody_names)
# predict by the model needs as an input a list of x inputs (we can simultaneously predict multiple y)
# and encoding in a numpy array
	input_list = np.array([flattened_encoded_melody])
# The model computes the list of probabilities for each note for each melody
	classification_list = model.predict(input_list)
# Retrieve the first (and only) chord array of probabilities
	probability_array = classification_list[0]
# Get the corresponding note (chord) name : Deterministic interpretation : the chord (note) with the highest probability
	return(one_hot_encoding_to_note_name(probability_array))

# Testing/examples of prediction
['DO', 'MI', 'SOL', 'SI']

def print_result(test_melody_names, expected_chord_name):
	print(str(test_melody_names) + ' -> ' + str(predict_chord(test_melody_names)) + ' (expected: ' + str(expected_chord_name) + ')')

print_result(['DO', 'MI', 'SOL', 'DO'], 'DO')

print_result(['MI', 'SI', 'DO', 'SOL'], 'DO')

print_result(['RE', 'FA#', 'LA', 'DO#'], 'RE')

print_result(['FA', 'MI', 'LA', 'DO'], 'FA')

