# -*- coding: utf-8 -*-
"""MIDI_MIR_Statistics.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1GFqYMrhnHfOj_3UF6KnIE8FbENkYMxkZ

# MIDI MIR Statistics Notebook (ver 1.0)

***

## View detailed info and stats for the specified MIDI file

***

#### Project Los Angeles
#### Tegridy Code 2021
"""

# Commented out IPython magic to ensure Python compatibility.
#@title Install all dependencies
!git clone https://github.com/asigalov61/tegridy-tools
!pip install mido
!pip install visual_midi
!pip install pypianoroll
!git clone https://github.com/sniperwrb/python-midi
!apt-get install swig
# %cd /content/python-midi
!python setup.py install
# %cd /content/
!mkdir /content/midis/

#@title Import needed modules
import os
import numpy as np
import mido
import string
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.colors import colorConverter

import sys, math, time

import midi

from pypianoroll import Multitrack, Track

from mido import MidiFile
from operator import itemgetter

from visual_midi import Plotter
from visual_midi import Preset
from pretty_midi import PrettyMIDI


import midi
from midi.constants import NOTE_NAME_MAP_SHARP
import argparse
import networkx as nx

#@title Specify the input MIDI file to analyze
MIDI_file_to_analyze = "/content/tegridy-tools/tegridy-tools/seed.mid" #@param {type:"string"}

#@title visual_midi Bokeh Plot
preset = Preset(plot_width=850)
plotter = Plotter(preset, plot_max_length_bar=4)
pm = PrettyMIDI(MIDI_file_to_analyze)
plotter.show_notebook(pm)

#@title Plot Piano Roll
# inherit the origin mido class
class MidiFile(mido.MidiFile):

    def __init__(self, filename):

        mido.MidiFile.__init__(self, filename)
        self.sr = 10
        self.meta = {}
        self.events = self.get_events()

    def get_events(self):
        mid = self
        print(mid)

        # There is > 16 channel in midi.tracks. However there is only 16 channel related to "music" events.
        # We store music events of 16 channel in the list "events" with form [[ch1],[ch2]....[ch16]]
        # Lyrics and meta data used a extra channel which is not include in "events"

        events = [[] for x in range(16)]

        # Iterate all event in the midi and extract to 16 channel form
        for track in mid.tracks:
            for msg in track:
                try:
                    channel = msg.channel
                    events[channel].append(msg)
                except AttributeError:
                    try:
                        if type(msg) != type(mido.UnknownMetaMessage):
                            self.meta[msg.type] = msg.dict()
                        else:
                            pass
                    except:
                        print("error",type(msg))

        return events

    def get_roll(self):
        events = self.get_events()
        # Identify events, then translate to piano roll
        # choose a sample ratio(sr) to down-sample through time axis
        sr = self.sr

        # compute total length in tick unit
        length = self.get_total_ticks()

        # allocate memory to numpy array
        roll = np.zeros((16, 128, length // sr), dtype="int8")

        # use a register array to save the state(no/off) for each key
        note_register = [int(-1) for x in range(128)]

        # use a register array to save the state(program_change) for each channel
        timbre_register = [1 for x in range(16)]


        for idx, channel in enumerate(events):

            time_counter = 0
            volume = 100
            # Volume would change by control change event (cc) cc7 & cc11
            # Volume 0-100 is mapped to 0-127

            print("channel", idx, "start")
            for msg in channel:
                if msg.type == "control_change":
                    if msg.control == 7:
                        volume = msg.value
                        # directly assign volume
                    if msg.control == 11:
                        volume = volume * msg.value // 127
                        # change volume by percentage
                    # print("cc", msg.control, msg.value, "duration", msg.time)

                if msg.type == "program_change":
                    timbre_register[idx] = msg.program
                    #print("channel", idx, "pc", msg.program, "time", time_counter, "duration", msg.time)



                if msg.type == "note_on":
                    #print("on ", msg.note, "time", time_counter, "duration", msg.time, "velocity", msg.velocity)
                    note_on_start_time = time_counter // sr
                    note_on_end_time = (time_counter + msg.time) // sr
                    intensity = volume * msg.velocity // 127



					# When a note_on event *ends* the note start to be play 
					# Record end time of note_on event if there is no value in register
					# When note_off event happens, we fill in the color
                    if note_register[msg.note] == -1:
                        note_register[msg.note] = (note_on_end_time,intensity)
                    else:
					# When note_on event happens again, we also fill in the color
                        old_end_time = note_register[msg.note][0]
                        old_intensity = note_register[msg.note][1]
                        roll[idx, msg.note, old_end_time: note_on_end_time] = old_intensity
                        note_register[msg.note] = (note_on_end_time,intensity)


                if msg.type == "note_off":
                  try:
                    #print("off", msg.note, "time", time_counter, "duration", msg.time, "velocity", msg.velocity)
                    note_off_start_time = time_counter // sr
                    note_off_end_time = (time_counter + msg.time) // sr
                    note_on_end_time = note_register[msg.note][0]
                    intensity = note_register[msg.note][1]
					# fill in color
                    roll[idx, msg.note, note_on_end_time:note_off_end_time] = intensity

                    note_register[msg.note] = -1  # reinitialize register
                  except:
                    continue
                time_counter += msg.time

                # TODO : velocity -> done, but not verified
                # TODO: Pitch wheel
                # TODO: Channel - > Program Changed / Timbre catagory
                # TODO: real time scale of roll

            # if there is a note not closed at the end of a channel, close it
            for key, data in enumerate(note_register):
                if data != -1:
                    note_on_end_time = data[0]
                    intensity = data[1]
                    # print(key, note_on_end_time)
                    note_off_start_time = time_counter // sr
                    roll[idx, key, note_on_end_time:] = intensity
                note_register[idx] = -1

        return roll

    def get_roll_image(self):
        roll = self.get_roll()
        plt.ioff()

        K = 16

        transparent = colorConverter.to_rgba('black')
        colors = [mpl.colors.to_rgba(mpl.colors.hsv_to_rgb((i / K, 1, 1)), alpha=1) for i in range(K)]
        cmaps = [mpl.colors.LinearSegmentedColormap.from_list('my_cmap', [transparent, colors[i]], 128) for i in
                 range(K)]

        for i in range(K):
            cmaps[i]._init()  # create the _lut array, with rgba values
            # create your alpha array and fill the colormap with them.
            # here it is progressive, but you can create whathever you want
            alphas = np.linspace(0, 1, cmaps[i].N + 3)
            cmaps[i]._lut[:, -1] = alphas

        fig = plt.figure(figsize=(12, 9))
        a1 = fig.add_subplot(111)
        a1.axis("equal")
        a1.set_facecolor("black")

        array = []

        for i in range(K):
            try:
                img = a1.imshow(roll[i], interpolation='nearest', cmap=cmaps[i], aspect='auto')
                array.append(img.get_array())
            except IndexError:
                pass
        return array

    def draw_roll(self):


        roll = self.get_roll()

        # build and set fig obj
        plt.ioff()
        fig = plt.figure(figsize=(12, 9))
        a1 = fig.add_subplot(111)
        a1.axis("equal")
        a1.set_facecolor("black")

        # change unit of time axis from tick to second
        tick = self.get_total_ticks()
        second = mido.tick2second(tick, self.ticks_per_beat, self.get_tempo())
        #print(second)
        if second > 10:
            x_label_period_sec = second // 10
        else:
            x_label_period_sec = second / 10  # ms
        #print(x_label_period_sec)
        x_label_interval = mido.second2tick(x_label_period_sec, self.ticks_per_beat, self.get_tempo()) / self.sr
        #print(x_label_interval)
        plt.xticks([int(x * x_label_interval) for x in range(20)], [round(x * x_label_period_sec, 2) for x in range(20)])

        # change scale and label of y axis
        plt.yticks([y*16 for y in range(8)], [y*16 for y in range(8)])

        # build colors
        channel_nb = 16
        transparent = colorConverter.to_rgba('black')
        colors = [mpl.colors.to_rgba(mpl.colors.hsv_to_rgb((i / channel_nb, 1, 1)), alpha=1) for i in range(channel_nb)]
        cmaps = [mpl.colors.LinearSegmentedColormap.from_list('my_cmap', [transparent, colors[i]], 128) for i in
                 range(channel_nb)]

        # build color maps
        for i in range(channel_nb):
            cmaps[i]._init()
            # create your alpha array and fill the colormap with them.
            alphas = np.linspace(0, 1, cmaps[i].N + 3)
            # create the _lut array, with rgba values
            cmaps[i]._lut[:, -1] = alphas


        # draw piano roll and stack image on a1
        for i in range(channel_nb):
            try:
                a1.imshow(roll[i], origin="lower", interpolation='nearest', cmap=cmaps[i], aspect='auto')
            except IndexError:
                pass

        # draw color bar

        colors = [mpl.colors.hsv_to_rgb((i / channel_nb, 1, 1)) for i in range(channel_nb)]
        cmap = mpl.colors.LinearSegmentedColormap.from_list('my_cmap', colors, 16)
        a2 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
        cbar = mpl.colorbar.ColorbarBase(a2, cmap=cmap,
                                        orientation='horizontal',
                                        ticks=list(range(16)))

        # show piano roll
        plt.draw()
        plt.ion()
        plt.show(block=True)
        #plt.savefig('test.jpg', dpi=300)

    def get_tempo(self):
        try:
            return self.meta["set_tempo"]["tempo"]
        except:
            return 500000

    def get_total_ticks(self):
        max_ticks = 0
        for channel in range(16):
            ticks = sum(msg.time for msg in self.events[channel])
            if ticks > max_ticks:
                max_ticks = ticks
        return max_ticks


mid = MidiFile(MIDI_file_to_analyze)

# get the list of all events
# events = mid.get_events()

# get the np array of piano roll image
roll = mid.get_roll()

# draw piano roll by pyplot
mid.draw_roll()

#@title MIDI Stats 1

# https://github.com/LiuFang816/SALSTM_py_data/blob/d494b3041069d377d6a7a9c296a14334f2fa5acc/python/olofmogren_c-rnn-gan/c-rnn-gan-master/midi_statistics.py
# https://github.com/gbramleysimmons/musictransformers

def msg2dict(msg):
    result = dict()
    if 'note_on' in msg:
        on_ = True
    elif 'note_off' in msg:
        on_ = False
    else:
        on_ = None
    result['time'] = int(msg[msg.rfind('time'):].split(' ')[0].split('=')[1].translate(
        str.maketrans({a: None for a in string.punctuation})))

    if on_ is not None:
        for k in ['note', 'velocity']:
            result[k] = int(msg[msg.rfind(k):].split(' ')[0].split('=')[1].translate(
                str.maketrans({a: None for a in string.punctuation})))
    return [result, on_]


def switch_note(last_state, note, velocity, on_=True):
    # piano has 88 notes, corresponding to note id 21 to 108, any note out of this range will be ignored
    result = [0] * 88 if last_state is None else last_state.copy()
    if 21 <= note <= 108:
        result[note-21] = velocity if on_ else 0
    return result


def get_new_state(new_msg, last_state):
    new_msg, on_ = msg2dict(str(new_msg))
    new_state = switch_note(last_state, note=new_msg['note'], velocity=new_msg['velocity'], on_=on_) if on_ is not None else last_state
    return [new_state, new_msg['time']]


def track2seq(track):
    # piano has 88 notes, corresponding to note id 21 to 108, any note out of the id range will be ignored
    result = []
    last_state, last_time = get_new_state(str(track[0]), [0]*88)
    for i in range(1, len(track)):
        new_state, new_time = get_new_state(track[i], last_state)
        if new_time > 0:
            result += [last_state]*new_time
        last_state, last_time = new_state, new_time
    return result


def mid2arry(mid, min_msg_pct=0.1):
    tracks_len = [len(tr) for tr in mid.tracks]
    min_n_msg = max(tracks_len) * min_msg_pct
    # convert each track to nested list
    all_arys = []
    for i in range(len(mid.tracks)):
        if len(mid.tracks[i]) > min_n_msg:
            ary_i = track2seq(mid.tracks[i])
            all_arys.append(ary_i)
    # make all nested list the same length
    max_len = max([len(ary) for ary in all_arys])
    for i in range(len(all_arys)):
        if len(all_arys[i]) < max_len:
            all_arys[i] += [[0] * 88] * (max_len - len(all_arys[i]))
    all_arys = np.array(all_arys)
    all_arys = all_arys.max(axis=0)
    # trim: remove consecutive 0s in the beginning and at the end
    sums = all_arys.sum(axis=1)
    ends = np.where(sums > 0)[0]
    return all_arys[min(ends): max(ends)]


def print_array(midi_array):
    
    plt.plot(range(midi_array.shape[0]), np.multiply(np.where(midi_array > 0, 1, 0), range(1, 89)), marker='.',
             markersize=1, linestyle='')
    #plt.show()
    plt.savefig('MIDI Stats Graph.png', dpi=300)


def main():
    # Need to change filepath to midi path
    filepath = MIDI_file_to_analyze
    midi_file = mido.MidiFile(filepath, clip=True)
    midi_array = mid2arry(midi_file)
    print_array(midi_array)


if __name__ == '__main__':
    main()

#@title MIDI Stats 2
# Tools to load and save midi files for the rnn-gan-project.
# https://github.com/LiuFang816/SALSTM_py_data/blob/d494b3041069d377d6a7a9c296a14334f2fa5acc/python/olofmogren_c-rnn-gan/c-rnn-gan-master/midi_statistics.py
# 
# https://github.com/TimStroup/Classical-Music-Generator/blob/847a410acb7e993c33e2116af45a7ccdb6b1a9e9/midi_statistics.py
#
# Written by Olof Mogren, http://mogren.one/
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================


GENRE      = 0
COMPOSER   = 1
SONG_DATA  = 2

# INDICES IN BATCHES:
LENGTH     = 0
FREQ       = 1
VELOCITY   = 2
TICKS_FROM_PREV_START      = 3

# INDICES IN SONG DATA (NOT YET BATCHED):
BEGIN_TICK = 3
CHANNEL    = 4

debug = ''
#debug = 'overfit'


base_tones = {'C':   0,
              'C#':  1, 
              'D':   2,
              'D#':  3,
              'E':   4,
              'F':   5,
              'F#':  6,
              'G':   7,
              'G#':  8,
              'A':   9,
              'A#': 10,
              'B':  11}

scale = {}
#Major scale:
scale['major'] = [0,2,4,5,7,9,11]
#(W-W-H-W-W-W-H)
#(2 2 1 2 2 2 1)

#Natural minor scale:
scale['natural_minor'] = [0,2,3,5,7,8,10]
#(W-H-W-W-H-W-W)
#(2 1 2 2 1 2 2)
 
#Harmonic minor scale:
scale['harmonic_minor'] = [0,2,3,5,7,8,11]
#(W-H-W-W-H-WH-H)
#(2 1 2 2 1 3 1)

tone_names = {}
for tone_name in base_tones:
  tone_names[base_tones[tone_name]] = tone_name


def get_tones(midi_pattern):
  """
  returns a dict of statistics, keys: [scale_distribution,
  """
  
  tones = []
  
  for track in midi_pattern:
    for event in track:
      if type(event) == midi.events.SetTempoEvent:
        pass # These are currently ignored
      elif (type(event) == midi.events.NoteOffEvent) or \
           (type(event) == midi.events.NoteOnEvent and \
            event.velocity == 0):
        pass # not needed here
      elif type(event) == midi.events.NoteOnEvent:
        tones.append(event.data[0])
  return tones 

def detect_beat(midi_pattern):
  """
  returns a dict of statistics, keys: [scale_distribution,
  """
  
  abs_ticks = []
  
  # Tempo:
  ticks_per_quarter_note = float(midi_pattern.resolution)
  
  for track in midi_pattern:
    abs_tick=0
    for event in track:
      abs_tick += event.tick
      if type(event) == midi.events.SetTempoEvent:
        pass # These are currently ignored
      elif (type(event) == midi.events.NoteOffEvent) or \
           (type(event) == midi.events.NoteOnEvent and \
            event.velocity == 0):
        pass
      elif type(event) == midi.events.NoteOnEvent:
        abs_ticks.append(abs_tick)
  stats = {}
  for quarter_note_estimate in range(int(ticks_per_quarter_note), int(0.75*ticks_per_quarter_note), -1):
    #print('est: {}'.format(quarter_note_estimate))
    avg_ticks_off = []
    for begin_tick in range(quarter_note_estimate):
      ticks_off = []
      for abs_tick in abs_ticks:
        #print('abs_tick: {} % {}'.format(abs_tick, quarter_note_estimate/4))
        sixteenth_note_estimate = quarter_note_estimate/4
        ticks_off_sixteenths = int((begin_tick+abs_tick)%sixteenth_note_estimate)
        if ticks_off_sixteenths > sixteenth_note_estimate/2:
          # off, but before beat 
          ticks_off_sixteenths = -(ticks_off_sixteenths-sixteenth_note_estimate)
        #print('ticks_off: {}'.format(ticks_off_sixteenths))
        ticks_off.append(ticks_off_sixteenths)
      avg_ticks_off.append(float(sum(ticks_off))/float(len(ticks_off)))
      #print('avg_ticks_off: {}. min: {}.'.format(avg_ticks_off, min(avg_ticks_off)))
    stats[quarter_note_estimate] = min(avg_ticks_off)
  return stats

def get_abs_ticks(midi_pattern):
  abs_ticks = []
  for track in midi_pattern:
    abs_tick=0
    for event in track:
      abs_tick += event.tick
      if type(event) == midi.events.SetTempoEvent:
        pass # These are currently ignored
      elif (type(event) == midi.events.NoteOffEvent) or \
           (type(event) == midi.events.NoteOnEvent and \
            event.velocity == 0):
        pass
      elif type(event) == midi.events.NoteOnEvent:
        abs_ticks.append(abs_tick)
  abs_ticks.sort()
  return abs_ticks

def get_top_k_intervals(midi_pattern, k):
  """
  returns a fraction of the noteon events in midi_pattern that are polyphonous
  (several notes occurring at the same time).
  Here, two note on events are counted as the same event if they
  occur at the same time, and in this case it is considered a polyphonous event.
  """
  intervals = {}
  abs_ticks = get_abs_ticks(midi_pattern)
  accumulator = 0
  last_abs_tick = 0
  for abs_tick in abs_ticks:
    interval = abs_tick-last_abs_tick
    if interval not in intervals:
      intervals[interval] = 0
    intervals[interval] += 1
    accumulator += 1
    last_abs_tick = abs_tick
  intervals_list = [(interval, intervals[interval]/float(accumulator)) for interval in intervals]
  intervals_list.sort(key=lambda i: i[1], reverse=True)
  return intervals_list[:k]


def get_polyphony_score(midi_pattern):
  """
  returns a fraction of the noteon events in midi_pattern that are polyphonous
  (several notes occurring at the same time).
  Here, two note on events are counted as the same event if they
  occur at the same time, and in this case it is considered a polyphonous event.
  """
  
  abs_ticks = get_abs_ticks(midi_pattern)
  monophonous_events = 0
  polyphonous_events = 0
  
  last_abs_tick = 0
  tones_in_current_event = 0
  for abs_tick in abs_ticks:
    if abs_tick == last_abs_tick:
      tones_in_current_event += 1
    else:
      if tones_in_current_event == 1:
        monophonous_events += 1
      elif tones_in_current_event > 1:
        polyphonous_events += 1
      tones_in_current_event = 1
    last_abs_tick = abs_tick
    if tones_in_current_event == 1:
      monophonous_events += 1
    elif tones_in_current_event > 1:
      polyphonous_events += 1
  if polyphonous_events == 0:
    return 0.0
  return float(polyphonous_events)/(polyphonous_events+monophonous_events)


def get_rhythm_stats(midi_pattern):
  """
  returns a dict of statistics, keys: [scale_distribution,
  """
  
  abs_ticks = []
  
  # Tempo:
  ticks_per_quarter_note = float(midi_pattern.resolution)
  
  # Multiply with output_ticks_pr_input_tick for output ticks.
  for track in midi_pattern:
    abs_tick=0
    for event in track:
      abs_tick += event.tick
      if type(event) == midi.events.SetTempoEvent:
        pass # These are currently ignored
      elif (type(event) == midi.events.NoteOffEvent) or \
           (type(event) == midi.events.NoteOnEvent and \
            event.velocity == 0):
        pass
      elif type(event) == midi.events.NoteOnEvent:
        abs_ticks.append(abs_tick)
  stats = {}
  for abs_tick in abs_ticks:
    ticks_since_quarter_note = int(abs_tick%ticks_per_quarter_note)
    if ticks_since_quarter_note not in stats:
      stats[ticks_since_quarter_note] = 1
    else:
      stats[ticks_since_quarter_note] += 1
  return stats


def get_intensities(midi_pattern):
  """
  returns a dict of statistics, keys: [scale_distribution,
  """
  
  intensities = []
  
  for track in midi_pattern:
    abs_tick=0
    for event in track:
      abs_tick += event.tick
      if type(event) == midi.events.SetTempoEvent:
        pass # These are currently ignored
      elif (type(event) == midi.events.NoteOffEvent) or \
           (type(event) == midi.events.NoteOnEvent and \
            event.velocity == 0):
        pass
      elif type(event) == midi.events.NoteOnEvent:
        intensities.append(event.velocity)
  return (min(intensities), max(intensities))


def get_midi_pattern(filename):
  try:
    return midi.read_midifile(filename)
  except:
    print ('Error reading {}'.format(filename))
    return None

def tones_to_scales(tones):
  """
   Midi to tone name (octave: -5):
   0: C
   1: C#
   2: D
   3: D#
   4: E
   5: F
   6: F#
   7: G
   8: G#
   9: A
   10: A#
   11: B
   Melodic minor scale is ignored.
   One octave is 12 tones.
  """
  counts = {}
  for base_tone in base_tones:
    counts[base_tone] = {}
    counts[base_tone]['major'] = 0
    counts[base_tone]['natural_minor'] = 0
    counts[base_tone]['harmonic_minor'] = 0

  if not len(tones):
    frequencies = {}
    for base_tone in base_tones:
      frequencies[base_tone] = {}
      for scale_label in scale:
        frequencies[base_tone][scale_label] = 0.0
    return frequencies
  for tone in tones:
    for base_tone in base_tones:
      for scale_label in scale:
        if tone%12-base_tones[base_tone] in scale[scale_label]:
          counts[base_tone][scale_label] += 1
  frequencies = {}
  for base_tone in counts:
    frequencies[base_tone] = {}
    for scale_label in counts[base_tone]:
      frequencies[base_tone][scale_label] = float(counts[base_tone][scale_label])/float(len(tones))
  return frequencies

def repetitions(tones):
  rs = {}
  #print(tones)
  #print(len(tones)/2)
  for l in range(2, min(len(tones)/2, 10)):
    #print (l)
    rs[l] = 0
    for i in range(len(tones)-l*2):
      for j in range(i+l,len(tones)-l):
        #print('comparing \'{}\' and \'{}\''.format(tones[i:i+l], tones[j:j+l]))
        if tones[i:i+l] == tones[j:j+l]:
          rs[l] += 1
  rs2 = {}
  for r in rs:
    if rs[r]:
      rs2[r] = rs[r]
  return rs2
      

def tone_to_tone_name(tone):
  """
   Midi to tone name (octave: -5):
   0: C
   1: C#
   2: D
   3: D#
   4: E
   5: F
   6: F#
   7: G
   8: G#
   9: A
   10: A#
   11: B
   One octave is 12 tones.
  """

  base_tone = tone_names[tone%12]
  octave = tone/12-5
  return '{} {}'.format(base_tone, octave)

def max_likelihood_scale(tones):
  scale_statistics = tones_to_scales(tones) 
  stat_list = []
  for base_tone in scale_statistics:
    for scale_label in scale_statistics[base_tone]:
      stat_list.append((base_tone, scale_label, scale_statistics[base_tone][scale_label]))
  stat_list.sort(key=lambda e: e[2], reverse=True)
  return (stat_list[0][0]+' '+stat_list[0][1], stat_list[0][2])

def tone_to_freq(tone):
  """
    returns the frequency of a tone. 
    formulas from
      * https://en.wikipedia.org/wiki/MIDI_Tuning_Standard
      * https://en.wikipedia.org/wiki/Cent_(music)
  """
  return math.pow(2, ((float(tone)-69.0)/12.0)) * 440.0

def freq_to_tone(freq):
  """
    returns a dict d where
    d['tone'] is the base tone in midi standard
    d['cents'] is the cents to make the tone into the exact-ish frequency provided.
               multiply this with 8192 to get the midi pitch level.
    formulas from
      * https://en.wikipedia.org/wiki/MIDI_Tuning_Standard
      * https://en.wikipedia.org/wiki/Cent_(music)
  """
  if freq == 0.0:
    return None
  float_tone = (69.0+12*math.log(float(freq)/440.0, 2))
  int_tone = int(float_tone)
  cents = int(1200*math.log(float(freq)/tone_to_freq(int_tone), 2))
  return {'tone': int_tone, 'cents': cents}

def cents_to_pitchwheel_units(cents):
  return int(40.96*(float(cents)))

def get_all_stats(midi_pattern):
  stats = {}
  if not midi_pattern:
    print('Failed to read midi pattern.')
    return None
  tones = get_tones(midi_pattern)
  if len(tones) == 0:
    print('This is an empty song.')
    return None
  stats['num_tones'] = len(tones)
  stats['tone_min'] = min(tones)
  stats['freq_min'] = tone_to_freq(min(tones))
  stats['tone_max'] = max(tones)
  stats['freq_max'] = tone_to_freq(max(tones))
  stats['tone_span'] = max(tones)-min(tones)
  stats['freq_span'] = tone_to_freq(max(tones))-tone_to_freq(min(tones))
  stats['tones_unique'] = len(set(tones))
  rs = repetitions(tones)
  for r in range(2,10):
    if r in rs:
      stats['repetitions_{}'.format(r)] = rs[r]
    else:
      stats['repetitions_{}'.format(r)] = 0
  
  ml = max_likelihood_scale(tones)
  stats['scale'] = ml[0]
  stats['scale_score'] = ml[1]
  
  beat_stats = detect_beat(midi_pattern)
  minval = float(midi_pattern.resolution)
  argmin = -1
  for beat in beat_stats:
    #print('Looking at beat: {}. Avg ticks off: {}.'.format(beat, beat_stats[beat]))
    if beat_stats[beat] < minval:
      minval = beat_stats[beat]
      argmin = beat
  stats['estimated_beat'] = argmin
  stats['estimated_beat_avg_ticks_off'] = minval
  (min_int, max_int) = get_intensities(midi_pattern)
  stats['intensity_min'] = min_int
  stats['intensity_max'] = max_int
  stats['intensity_span'] = max_int-min_int

  stats['polyphony_score'] = get_polyphony_score(midi_pattern)
  stats['top_10_intervals'] = get_top_k_intervals(midi_pattern, 10)
  stats['top_2_interval_difference'] = 0.0
  if len(stats['top_10_intervals']) > 1:
    stats['top_2_interval_difference'] = abs(stats['top_10_intervals'][1][0]-stats['top_10_intervals'][0][0])
  stats['top_3_interval_difference'] = 0.0
  if len(stats['top_10_intervals']) > 2:
    stats['top_3_interval_difference'] = abs(stats['top_10_intervals'][2][0]-stats['top_10_intervals'][0][0])

  return stats

def get_gnuplot_line(midi_patterns, i, showheader=True):
  stats = []
  print('#getting stats...')
  stats_time = time.time()
  for p in midi_patterns:
    stats.append(get_all_stats(p))
  print('done. time: {}'.format(time.time()-stats_time))
  #print(stats)
  stats_keys_string = ['scale']
  stats_keys = ['scale_score', 'tone_min', 'tone_max', 'tone_span', 'freq_min', 'freq_max', 'freq_span', 'tones_unique', 'repetitions_2', 'repetitions_3', 'repetitions_4', 'repetitions_5', 'repetitions_6', 'repetitions_7', 'repetitions_8', 'repetitions_9', 'estimated_beat', 'estimated_beat_avg_ticks_off', 'intensity_min', 'intensity_max', 'intensity_span', 'polyphony_score', 'top_2_interval_difference', 'top_3_interval_difference', 'num_tones']
  gnuplotline = ''
  if showheader:
    gnuplotline = '# global-step {} {}\n'.format(' '.join([s.replace(' ', '_') for s in stats_keys_string]), ' '.join(stats_keys))
  gnuplotline += '{} {} {}\n'.format(i, ' '.join(['{}'.format(stats[0][key].replace(' ', '_')) for key in stats_keys_string]), ' '.join(['{:.3f}'.format(sum([s[key] for s in stats])/float(len(stats))) for key in stats_keys]))
  return gnuplotline



def main():
  if len(sys.argv) > 2 and sys.argv[1] == '--gnuplot':
    #number = sys.argv[2]
    patterns = []
    for i in range(3,len(sys.argv)):
      #print(i)
      filename = sys.argv[i]
      print('#File: {}'.format(filename))
      #patterns.append(get_midi_pattern(filename))
      print(get_gnuplot_line([get_midi_pattern(filename)], i, showheader=(i==0)))
    
  else:
    for i in range(1,len(sys.argv)):
      #filename = sys.argv[i]
      filename = MIDI_file_to_analyze
      print('File: {}'.format(filename))
      midi_pattern = get_midi_pattern(filename)
      stats = get_all_stats(midi_pattern)
      if stats is None:
        print('Could not extract stats.')
      else:
        print ('ML scale estimate: {}: {:.2f}'.format(stats['scale'], stats['scale_score']))
        print ('Min tone: {}, {:.1f} Hz.'.format(tone_to_tone_name(stats['tone_min']), stats['freq_min']))
        print ('Max tone: {}, {:.1f} Hz.'.format(tone_to_tone_name(stats['tone_max']), stats['freq_max']))
        print ('Span: {} tones, {:.1f} Hz.'.format(stats['tone_span'], stats['freq_span']))
        print ('Unique tones: {}'.format(stats['tones_unique']))
        for r in range(2,10):
          print('Repetitions of len {}: {}'.format(r, stats['repetitions_{}'.format(r)]))
        print('Estimated beat: {}. Avg ticks off: {:.2f}.'.format(stats['estimated_beat'], stats['estimated_beat_avg_ticks_off']))
        print('Intensity: min: {}, max: {}.'.format(stats['intensity_min'], stats['intensity_max']))
        print('Polyphonous events: {:.2f}.'.format(stats['polyphony_score']))
        print('Top intervals:')
        for interval,score in stats['top_10_intervals']:
          print('{}: {:.2f}.'.format(interval,score))
        print('Top 2 interval difference: {}.'.format(stats['top_2_interval_difference']))
        print('Top 3 interval difference: {}.'.format(stats['top_3_interval_difference']))


if __name__ == "__main__":
  main()

#@title MIDI Stats 3 (Ground Truth)
# https://github.com/mbereket/music-transcription

# Comverts MIDI file pattern to representation of notes events in absolute time
class NoteEvents:
    def __init__(self, pattern, note_tracks=None, start_on_note=True):
        self._event_list = []
        self.note_time_list = []
        pattern.make_ticks_abs()
        self.pattern = pattern
        self.ticks_per_beat = pattern.resolution
        self.numNotes = 88
        # offset between note index and MIDI note number
        self.noteOffset = 9
        # list of track names to include notes from
        self.note_tracks = note_tracks
        self.names = self._name_tracks()
        self.start_on_note = start_on_note
        self._parse_events()

    def _name_tracks(self):
        names = [None] * len(self.pattern)
        for i in range(len(self.pattern)):
            for event in self.pattern[i]:
                if type(event) == midi.events.TrackNameEvent:
                    names[i] = event.text
                break
        return names

    def _parse_events(self):
        for i in range(len(self.pattern)):
            for event in self.pattern[i]:
                if type(event) in (midi.events.NoteOnEvent, midi.events.NoteOffEvent):
                    if self.note_tracks == None or self.names[i] in self.note_tracks:
                        self._event_list.append(event)
                elif type(event) == midi.events.SetTempoEvent:
                    self._event_list.append(event)
                elif type(event) == midi.events.EndOfTrackEvent and event.tick != 0:
                    self._event_list.append(event)
        self._event_list = sorted(self._event_list, key=lambda x: x.tick)
        self._event_list_timed()


    def _event_list_timed(self):
        assert(type(self._event_list[0]) == midi.events.SetTempoEvent)
        microseconds_per_beat = self._event_list[0].get_mpqn()
        prev_time = 0
        prev_tick = 0
        microseconds_per_tick = float(microseconds_per_beat) / self.ticks_per_beat
        for event in self._event_list:
            tick_diff = event.tick - prev_tick
            curr_time = prev_time + (tick_diff * microseconds_per_tick)
            if type(event) != midi.events.SetTempoEvent:
                self.note_time_list.append((event, curr_time))
                prev_time = curr_time
                prev_tick = event.tick
            else:
                prev_time = curr_time
                prev_tick = event.tick
                microseconds_per_beat = event.get_mpqn()
                microseconds_per_tick = float(microseconds_per_beat) / self.ticks_per_beat
        start_time = self.note_time_list[0][1]

        if self.start_on_note:
            for i, tup in enumerate(self.note_time_list):
                self.note_time_list[i] = (tup[0],tup[1]-start_time)
        self._last_event_time = self.note_time_list[-1][1]

    def _note_off(self, note_event):
        return ((type(note_event) == midi.events.NoteOnEvent) and (note_event.get_velocity() == 0)) \
                    or type(note_event) == midi.events.NoteOffEvent

    # returns index of first slice at or after given time
    # time in microseconds
    def time_to_slice(self, t, slices_per_second):
        microseconds_per_slice = 1e6 / slices_per_second
        return np.ceil(float(t) / microseconds_per_slice).astype(int)

    # duration in seconds
    def get_ground_truth(self, slices_per_second, duration=None):
        microseconds_per_slice = 1e6 / slices_per_second
        number_slices = np.ceil(self._last_event_time / microseconds_per_slice).astype(int)
        ground_truth = np.zeros(self.numNotes * number_slices).reshape(self.numNotes, number_slices)
        template = np.zeros(self.numNotes).reshape(self.numNotes,1)
        prev_time = 0
        for note, curr_time in self.note_time_list:
            if prev_time != curr_time:
                prev_time_slice = self.time_to_slice(prev_time, slices_per_second)
                curr_time_slice = self.time_to_slice(curr_time, slices_per_second)
                #make all slices in [prev_time, curr_time) equal to current template
                ground_truth[:,prev_time_slice:curr_time_slice] = template.repeat(curr_time_slice - prev_time_slice, axis=1)
            if type(note) == midi.events.EndOfTrackEvent:
                break
            pitch_index = note.get_pitch() - self.noteOffset
            if pitch_index >= 0 and pitch_index < self.numNotes:
                if self._note_off(note):
                    template[pitch_index] = 0
                else:
                    template[pitch_index] = 1
            prev_time = curr_time
        if duration != None:
            ground_truth = ground_truth[:,:self.time_to_slice(1e6 * duration, slices_per_second)]
        return ground_truth



if __name__ == '__main__':
    pattern = midi.read_midifile(MIDI_file_to_analyze)
    events = NoteEvents(pattern)
    truth = events.get_ground_truth(31.25, 15)
    plt.matshow(truth)
    plt.show()

#@title MIDI Stats 4
# https://github.com/Astner/exjobb/blob/1abc750a7a4d9af52e273ef24c2c32d0e431e1a6/concepts-master/experimental/higherorder/formats/midi/miditocontext/midiutils.py
# 

# Copyright 2015 Daniel Gillblad (dgi@sics.se).

# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Selecte active channels
# Here, all channels except 10 are used, as this MIDI channel is used for percussion only
# in General MIDI files (note that we index channels from 0, not 1)
activeChannels = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15)

# Time during which events are considered simultaneous
simTimeLimit = 50

# Fraction of time notes need to be overlapping to be considered played together
overlapFracLimit = 0.95

# Print info on specified MIDI file
def printMidiFileInfo(midifile):
	mid = MidiFile(midifile)
	print("File:", os.path.basename(midifile),
	      "Type:", mid.type,
	      "Length: ",
	      end = "")
	if mid.type < 2:
		print(mid.length)
	else:
		print("N/A")

# Print messages in MIDI file
def printMidiMessages(midifile):
	mid = MidiFile(midifile)
	for i, track in enumerate(mid.tracks):
		print('Track {}: {}'.format(i, track.name))
		for message in track:
			print(message)

# Simple class for representing current note state,
# given a series of messages given in order to the update method.
class NoteState:
	nstate = [-1] * 128

	def printState(self):
		print('|', end = '')
		for i in self.nstate[22:116]:
			if i >= 0:
				print('*', end = '')
			else:
				print('-', end = '')
		print('|')

	def update(self, message, time = 0):
		if message.type == 'all_notes_off':
			self.nstate = [-1] * 128
		elif message.type == 'note_off' or (message.type == 'note_on' and message.velocity <= 0):
			rmsg = [(self.nstate[message.note], time, message.note)]
			self.nstate[message.note] = -1
			return rmsg
		elif message.type == 'note_on':
			self.nstate[message.note] = time
			return []
		return []

# Determines if a message is a "note" message: note on, note off, or all notes off
def isNoteMessage(msg):
	if msg.type == 'note_on' or msg.type == 'note_off' or msg.type == 'all_notes_off':
		return True
	else:
		return False

# Determines if a message is a "note" message on a specific channel
def isNoteMessageOnChannel(msg, ch):
	if isNoteMessage(msg) and msg.channel == ch:
		return True
	else:
		return False

# Convert a MIDI file to a list of note events, each note given by
# (start-time, end-time, note number). Returns a nested list with
# one list per channel per track.
def fileToNoteList(midifile):
	mid = MidiFile(midifile)
	allnotes = []
	for i, track in enumerate(mid.tracks):
		tracknotes = []
		for channel in activeChannels:
			channelnotes = []
			state = NoteState()
			cTime = 0
			for message in track:
				cTime += message.time
				if isNoteMessageOnChannel(message, channel):
					channelnotes += state.update(message, cTime)
			if len(channelnotes) >= 1:
				tracknotes += [sorted(channelnotes, key=itemgetter(0,1))]
		if len(tracknotes) >= 1:
			allnotes += [tracknotes]
	return allnotes

# Convert a note list to a list of notes played together.
def playedTogether(notelist):
	together = []
	for i, note in enumerate(notelist):
		# Find overlaps
		for fnote in notelist[i + 1:]:
			if fnote[0] > note[1]:
				break
			overlap_a = (note[1] - fnote[0]) / (note[1] - note[0])
			overlap_b = (fnote[1] - note[1]) / (fnote[1] - fnote[0])
			if overlap_a >= overlapFracLimit or overlap_b >= overlapFracLimit:
				together += [[note[2], fnote[2]]]
	return together

# Find all notes played together in midifile, separated by track and channel
def allNotesPlayedTogether(midifile):
	notelists = fileToNoteList(midifile)
	alltogether = []
	for t in notelists:
		for c in t:
			alltogether += playedTogether(c)
	return alltogether

# Print a graphic representation of all note messages in a MIDI file
# on a (reasonable) easy to read format.
def printNoteMessages(midifile):
	mid = MidiFile(midifile)
	for i, track in enumerate(mid.tracks):
		print('Track {}: {}'.format(i, track.name))
		for channel in activeChannels:
			print('Channel:', channel)
			state = NoteState()
			cTime = 0
			simTime = 0
			for message in track:
				cTime += message.time
				if isNoteMessageOnChannel(message, channel):
					# If message is outside what would be considered simultaneous,
					# emit last state and reset simultaneous time counter
					if (simTime + message.time) >= simTimeLimit:
						print(cTime, end = '')
						state.printState()
						simTime = 0
					else:
						simTime += message.time
					state.update(message, cTime)

# Print a graphic representation of all note messages in a MIDI file
# on a (reasonable) easy to read format, grouping near simultaneous note events
# for readability.
def printGroupedNoteOnMessages(midifile):
	mid = MidiFile(midifile)
	for i, track in enumerate(mid.tracks):
		print('Track {}: {}'.format(i, track.name))
		for channel in activeChannels:
			print('Channel:', channel)
			cTime = 0
			gTime = 0
			nSet = set()
			for message in track:
				cTime += message.time
				if message.type == 'note_on' and message.velocity > 0 and message.channel == channel:
					if (cTime - gTime) >= simTimeLimit:
						print(cTime, nSet)
						gTime = cTime
						nSet.clear()
						nSet.add(message.note)
					else:
						nSet.add(message.note)

# Collect a set of statistics for a specified MIDI file object.
def collectMidiNoteStatistics(midifileobj, channel_activity, played_notes, note_offs,
                              running_status, all_notes_off):
	for i, track in enumerate(midifileobj.tracks):
		for channel in activeChannels:
			for message in track:
				if(isNoteMessage(message)):
					channel_activity[message.channel] += 1
					if message.type == 'note_on' and message.velocity > 0:
						played_notes[message.note] += 1
					if message.type == 'note_off':
						note_offs[message.channel] += 1
					if message.type == 'note_on' and message.velocity <= 0:
						running_status[message.channel] += 1
					if message.type == 'all_notes_off':
						all_notes_off[message.channel] += 1

# Print MIDI file statistics compiled for all MIDI files found in the specified directory
# and all sub-directories recursively.
def printMidiFileStatistics(root_dir):
    nr_files = 0
    nr_file_types = [0] * 3
    nr_length_files = 0
    total_length = 0
    channel_activity = [0] * 16
    played_notes = [0] * 127
    note_offs = [0] * 16 # Per channel
    running_status = [0] * 16 # Per channel
    all_notes_off = [0] * 16 # Per channel

    mid = MidiFile(MIDI_file_to_analyze)
    nr_files += 1
    nr_file_types[mid.type] += 1
    if mid.type < 2:
      nr_length_files += 1
      total_length += mid.length
    collectMidiNoteStatistics(mid, channel_activity, played_notes, note_offs,
                              running_status, all_notes_off)

    print('--------------------------------------------------------------------------')
    print('Number of .mid files:', nr_files, '      Type 0:', nr_file_types[0],
          ' Type 1:', nr_file_types[1], ' Type 2:', nr_file_types[2])
    print('Average length:       {:.1f}'.format(total_length / nr_length_files))
    print('--------------------------------------------------------------------------')
    print('Note on messages:    ', sum(played_notes))
    print('Note off messages:   ', sum(note_offs))
    print('Running status off:  ', sum(running_status))
    print('All notes off:       ', sum(all_notes_off))
    print('--------------------------------------------------------------------------')
    print('Channel activity distribution:')
    total_activity = sum(channel_activity)
    for i, act in enumerate(channel_activity):
      if act > 0:
        print('{:2d}: {:.1%} '.format(i + 1, act / total_activity))
    print('--------------------------------------------------------------------------')
    print('Note distribution:')
    total_notes = sum(played_notes)
    for i, nt in enumerate(played_notes):
      if nt > 0:
        print('{:2d}: {:.2%} '.format(i, nt / total_notes))
    print('--------------------------------------------------------------------------')


# Visit files recursively from a root directory.
def visitFilesRecursively(root_dir, extension, apply_func, verbose = True):
	base_dir = os.path.abspath(root_dir)
	for root, subdirs, files in os.walk(base_dir):
		if verbose:
			print('# Directory: ' + root)
		for filename in files:
			if os.path.splitext(filename)[1] == extension:
				if verbose:
					print('  - %s ' % (filename))
				file_path = os.path.join(root, filename)
				res = apply_func(file_path)
    
printMidiFileStatistics('/content/')

#@title Time varying key of a MIDI file
# Identifies the time varying key of a MIDI file
# Hilary Mogul
# hilary.mogul@gmail.com
# May 2014
# Version 0.0.1
from pretty_midi import PrettyMIDI
import numpy as np
import midi
import os
import sys

def make_chroma_vector(chroma_slice):
  """Returns chroma vector of sums from starting time to ending time"""
  chroma_vector = np.zeros((12,1))
  chroma_vector[0] = np.sum(chroma_slice[11,])
  for i in range(1,12):
    chroma_vector[i] = np.sum(chroma_slice[11-i])
  return chroma_vector

def get_approx_key(chroma_vector, keys):
  """Returns index of approximated key, and the score that this approximated key got"""
  chroma_vector = chroma_vector[:]
  scores = []
  end = keys.shape[0] -1
  for i in range(0,end):
    key_vector = keys[i,:]
    score = np.dot(key_vector,chroma_vector)
    scores.append(score)
  key_index = scores.index(max(scores))
  arr = np.array([[key_index, max(scores)]])
  return arr

def get_key_score(chroma_vector, keys, key_index):
  """Returns the score of an approximated key, given the index of the key weights to try out"""
  chroma_vector = np.rot90(chroma_vector,3)
  chroma_vector = chroma_vector[0,:]
  key_vector = keys[key_index,:]
  score = np.dot(key_vector,chroma_vector)
  return score


def load_keys():
  """ Returns arrays of the weighted keys, and the corresponding names of them """
  #Building of weighted vectors
  if not os.path.exists("key_base_info.npz"):
    print("Building default array- major keys only")
    key_weight = np.array([[ 3, -1,  1, -1,  2,  1, -1,  2, -1,  1, -1,  2]])
    key_weight = np.vstack((key_weight,np.array([[2,  3, -1,  1, -1,  2,  1, -1,  2, -1,  1, -1]])))
    key_weight = np.vstack((key_weight,np.array([[-1,  2,  3, -1,  1, -1,  2,  1, -1,  2, -1,  1]])))
    key_weight = np.vstack((key_weight,np.array([[1, -1,  2,  3, -1,  1, -1,  2,  1, -1,  2, -1]])))
    key_weight = np.vstack((key_weight,np.array([[-1,  1, -1,  2,  3, -1,  1, -1,  2,  1, -1,  2]])))
    key_weight = np.vstack((key_weight,np.array([[2, -1,  1, -1,  2,  3, -1,  1, -1,  2,  1, -1]])))
    key_weight = np.vstack((key_weight,np.array([[-1,  2, -1,  1, -1,  2,  3, -1,  1, -1,  2,  1]])))
    key_weight = np.vstack((key_weight,np.array([[1, -1,  2, -1,  1, -1,  2,  3, -1,  1, -1,  2]])))
    key_weight = np.vstack((key_weight,np.array([[2,  1, -1,  2, -1,  1, -1,  2,  3, -1,  1, -1]])))
    key_weight = np.vstack((key_weight,np.array([[-1,  2,  1, -1,  2, -1,  1, -1,  2,  3, -1,  1]])))
    key_weight = np.vstack((key_weight,np.array([[1, -1,  2,  1, -1,  2, -1,  1, -1,  2,  3, -1]])))
    key_weight = np.vstack((key_weight,np.array([[-1,  1, -1,  2,  1, -1,  2, -1,  1, -1,  2,  3]])))
    names = np.array([['C-Maj', 'C#-Maj', 'D-Maj', 'D#-Maj', 'E-Maj', 'F-Maj','F#-Maj','G-Maj','G#-Maj', 'A-Maj', 'A#-Maj','B-Maj']])
    np.savez("key_base_info", key_weight, names)
  npzfile = np.load("key_base_info.npz")
  key_weight = npzfile['arr_0']
  names = npzfile['arr_1']
  #vector of key names (This version will use only major keys)

  return key_weight, names


#Main function of program:
def identify_key(midi_filename, command_line_print = True, save_results = True, measure_value = 1):
  """ Runs key identification algorithm
  :parameters:
    - midi_filename : String of name of existing midi file to process.
    - command_line_print : Boolean to allow for printing results to command line.
    - save_results : Boolean to allow saving the approximated key and the names of those keys to a .npz file matching the midi files name
    - measure_value : int > 0 that sets how many beats the program will process per approximation.
    """

  song = PrettyMIDI((midi_filename))

  #get beat locations for slices
  beats = song.get_beats() #output is an np.ndarray
  times = beats.flatten()
  sectionBeats = True
  #create slices of chroma data to process including summing them up
  #output: every "measure" of beats

  if measure_value< 1 or measure_value >times.size or measure_value == None:
    sectionBeats = False
    print ("WARNING: measure_value selected less than 1 or greater than beat size.  Will do one approximation for the whole song.")



  key_weight, names = load_keys()
  #get Chroma features
  chroma = song.get_chroma()
  #normalize chroma features
  chroma /= (chroma.max( axis = 0 ) + (chroma.max( axis = 0 ) == 0))

  # first case
  if sectionBeats:
    chroma_slice = chroma[:,0:round(times[0])*100]
  else:
    chroma_slice = chroma
  # Sum rows to find intensity of each note.
  vec = np.sum(chroma_slice, axis=1)
  keys_approx = get_approx_key(vec, key_weight)

  #for each slice, get approximated key and score into 2 column array (key, score)
  #possiblymay need to use indices of key names instead of actual keys
  #chroma time indices have a resolution of 10 ms

  times_used = np.array([[times[0]]])

  if sectionBeats:
    for t in range(1, times.size-1,measure_value):
    #make chroma slice based on time
      if times.size -t < measure_value:
        chroma_slice = chroma[:,round(times[t]*100):round(times[t+1]*100)]
      # print chroma_slice
      # vec = make_chroma_vector(chroma_slice)
        vec = np.sum(chroma_slice, axis=1)
      # vec = vec[::-1]
      else:
        chroma_slice = chroma[:,round(times[t]*100):round(times[t+1]*100)]
      # print chroma_slice
      # vec = make_chroma_vector(chroma_slice)
        vec = np.sum(chroma_slice, axis=1)
      # vec = vec[::-1]
      apr = get_approx_key(vec, key_weight)
      #if the score isn't 0 (which happens in silence), add the approximated key to the list
      if not apr[0,1] == 0:
        keys_approx = np.vstack((keys_approx, apr))
        times_used = np.vstack((times_used, np.array([[times[t]]])))


  # DUMMIED OUT CODE FOR FUTURE IMPLEMENTATION
  # final_keys = np.array([ [ keys_approx[0,0],times[0,0] ] ]) #mark first
  # print final_keys
  #
  # #iterate through rows of array- if there is a change, get % difference in scores for each key and use threshold to figure
  # #if it is a key change.  mark key change in final 2 column array of (key, time start)
  # threshold = .15 #experimental value
  #
  # if times.size > 1:
  #   print "going thru removal loop"
  #   for t in range (1, keys_approx.shape[0]):
  #     current = keys_approx[t,0]
  #     prev = keys_approx[t-1,0]
  #     if not current == prev: #if not equal to previous, check % difference of scores
  #       print "In key change check"
  #       score1 = keys_approx[t,1] #score of key of this time slice
  #       # print score1
  #       vec = make_chroma_vector(chroma, round(times[0,t])*100,round(times[0,t+1])*100 )
  #       # print vec
  #       score2 = get_key_score(vec, key_weight, prev) #score it would have gotten with last input key
  #       # print score2
  #       diff = abs(score1-score2)/(score1+score2)
  #       print diff
  #       if diff > threshold:
  #         arr = np.array([[keys_approx[t,0], times[t,0] ]])
  #         # print arr
  #         print "Key change at index: ", times[t,0]
  #         final_keys = np.vstack((final_keys, arr))
  #       else:
  #         print "difference not large enough to constitute key change "

  #keys_approx = final_keys
  e = keys_approx.shape[0]
  if command_line_print:
    for i in range(0, e):
      key_int = int(keys_approx[i,0])
      print("In key: ",names[0,key_int],"at time: ", times_used[i,0])
  if save_results:
    filename = os.path.basename(midi_filename)
    filename_raw = os.path.splitext(filename)[0]
    # if not os.path.exists(directory):
    dirname = "Results_of_key_approximation"
    if not os.path.exists(dirname):
      os.makedirs(dirname)
    np.savez(dirname+"/"+filename_raw+"_key_approx-vars", keys_approx = keys_approx, names = names)
    file_results = open(dirname+"/"+filename_raw+"_key_approx-text_form.txt",'w')
    file_results.write(filename_raw+"\n")
    for i in range(0, e):
      key_int = int(keys_approx[i,0])
      file_results.write("In key: "+names[0,key_int]+" at time: "+ str(times_used[i,0])+"\n")
    file_results.close()
  return keys_approx, names

x = identify_key(MIDI_file_to_analyze)

# Commented out IPython magic to ensure Python compatibility.
#@title Detailed chords analysis
# %cd /content/
# https://github.com/realbchan/MidiAnalysis

import bisect
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np

class Utils:
	# initialize, build mapping from a note number to a note letter
	def __init__(self):
		self.number_to_letter_mappings = dict()
		self.pattern = ['A', 'A#/Bb', 'B', 'C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab']
		self.build_number_letter_mapping()

	# this method maps a number to a note letter and it's octave
	# notes on my keyboard is from note=21 to note=108 == 88 keys
	# I dont think the piano can represent anything out of this range
	def build_number_letter_mapping(self):
		octave = 0
		for num_note in range(21, 109):
			letter = self.pattern[(num_note - 21) % len(self.pattern)]
			if letter == 'C':
				octave += 1
			note = (self.pattern[(num_note - 21) % len(self.pattern)], octave)

			self.number_to_letter_mappings[num_note] = note

	# get method, retrieve note letter from note number
	def note_number_to_letter(self, num_note):
		return self.number_to_letter_mappings[num_note]

# each note or chord is considered a "Tone"
# This is the parent class for Note and Chord
class Tone:
	def __init__(self):
		# this threshold limit determines how "close"
		# a note is to another note in seconds, this can be tuned
		self.threshold_limit = .25

	def is_note(self):
		return False
	def is_chord(self):
		return False
	# must be implemented in Note and Chord
	def is_close(self, other_note):
		raise NotImplementedError("To be implemented")

# I define a Note to be a single musical note played
# If there are 1 or 2 notes "close" by, it is considered a Chord
class Note(Tone):
	# takes in parameters from message, calculates duration
	def __init__(self, absoluteStart, absoluteStop, notePlayed, velocityStart, velocityStop):
		super().__init__()
		self.absoluteStart = absoluteStart
		self.absoluteStop = absoluteStop
		self.duration = self.absoluteStop - self.absoluteStart
		self.notePlayed = notePlayed
		self.velocityStart = velocityStart
		self.velocityStop = velocityStop

	# def __key(self):
	# 	return (self.notePlayed[0], self.notePlayed[1])

	# def __hash__(self):
	# 	return hash(self.__key())

	def __lt__(self, other_note):
		note_letter, note_octave = self.notePlayed[0], self.notePlayed[1]
		other_note_letter, other_note_octave = other_note.notePlayed[0], other_note.notePlayed[1]
		if note_octave < other_note_octave:
			return True
		elif note_octave == other_note_octave:
			return other_note_letter < note_letter
		else:
			return False

	def __str__(self):
		return str(self.notePlayed)

	def __repr__(self):
		return self.__str__()

	def is_note(self):
		return True
	# uses self.threshold to determine if a note is close
	def is_close(self, other_note):
		start_difference = abs(self.absoluteStart - other_note.absoluteStart)
		stop_difference = abs(self.absoluteStop - other_note.absoluteStop)

		if start_difference <= self.threshold_limit and stop_difference <= self.threshold_limit:
			return True
		return False

# a chord is considered 1 or 2 notes that are close by
class Chord(Tone):
	# we need to keep track of the average parameters of a chord
	def __init__(self):
		super().__init__()
		self.notes_in_chord = tuple()
		self.num_chords = 0
		self.average_duration = 0
		self.average_start_time = 0
		self.average_stop_time = 0

	def __str__(self):
		return str(self.notes_in_chord)

	# def __key(self):
	# 	return tuple(note.__hash__() for note in self.notes_in_chord)

	# def __hash__(self):
	# 	return hash(self.__key())


	def is_chord(self):
		return True

	# on adding a note, recalculate averages
	def add_note(self, note):
		self.average_duration = self.get_new_average(self.average_duration, note.duration)
		self.average_start_time = self.get_new_average(self.average_start_time, note.absoluteStart)
		self.average_stop_time = self.get_new_average(self.average_stop_time, note.absoluteStop)

		self.num_chords += 1

		note_letter, note_octave = note.notePlayed

		list_notes_in_chord = list(self.notes_in_chord)
		if len(self.notes_in_chord) > 0:
			bisect.insort_left(list_notes_in_chord, note)
		else:
			list_notes_in_chord.append(note)
		self.notes_in_chord = tuple(list_notes_in_chord)
		# print(self.notes_in_chord)




	# calculate new arbitrary average
	def get_new_average(self, previous_cumulation, next_number):
		previous_sum = previous_cumulation * self.num_chords
		current_sum = previous_sum + next_number
		return current_sum / (self.num_chords + 1)

	# similar to is_close for Note
	def is_close(self, other_note):
		start_difference = abs(self.average_start_time - other_note.absoluteStart)
		stop_difference = abs(self.average_stop_time - other_note.absoluteStop)
		

		if start_difference <= self.threshold_limit and stop_difference <= self.threshold_limit:
			return True
		return False

# a Song is a list of Tones
# it represents a midi track as a whole
class Song:
	def __init__(self):
		self.tones = list()

	def __str__(self):

		return ''.join(str(tone) for tone in self.tones)

	# sorts tones by start
	def sort_by_start_of_tone(self):
		# sorting comparator, probably could refactor
		def get_start(tone):
			if tone.is_note():
				return tone.absoluteStart
			else:
				return tone.average_start_time

		self.tones.sort(key=get_start)
	# add note to song, will detect if needs to make a chord
	def add_note(self, note):
		
		if len(self.tones) == 0:
			self.tones.append(note)

		else:
			for previous_tone in self.tones[::-1]:
				# retrieve previous tone's end, chord or note
				previous_tone_end = None
				if previous_tone.is_chord():
					previous_tone_end = previous_tone.average_stop_time
				else:
					previous_tone_end = previous_tone.absoluteStop

				# check if the previous' end time is close to current note
				# if it's not in threshold, this is just a singular note
				if abs(previous_tone_end - note.absoluteStop) <= note.threshold_limit:
					# current note's end is close to previous end, check if both start
					# and end are close
					if previous_tone.is_close(note):
						# in the event that we need to make a new chord,
						# we need to save the index of the previous_tone,
						#create a chord, and insert it back to original position
						if previous_tone.is_note():
							chord = Chord()
							chord.add_note(previous_tone)
							chord.add_note(note)
							saveIndex = self.tones.index(previous_tone)
							self.tones.pop(saveIndex)
							self.tones.insert(saveIndex, chord)
							break
						else:
							previous_tone.add_note(note)
							break
					continue
				else:
					self.tones.append(note)
					break

class ToneFrequency:
	def __init__(self, song):
		self.song = song
		self.frequency_of_all_notes = self.get_all_note_frequency()
		self.frequency_of_only_chords = self.get_frequency_only_chords()
		self.plot_get_frequency_only_chords()

	def get_all_note_frequency(self):
		frequencies = defaultdict(int)
		for tone in self.song.tones:
			if tone.is_note():
				frequencies[tone.notePlayed] += 1
			else:
				for note in tone.notes_in_chord:
					frequencies[note.notePlayed] += 1
		return frequencies


	def get_frequency_only_chords(self):
		frequencies = defaultdict(int)
		for tone in self.song.tones:
			if tone.is_chord():
				notes = tuple(note.notePlayed for note in tone.notes_in_chord)
				frequencies[notes] += 1

		return frequencies


	def plot_get_frequency_only_chords(self):
		D = self.frequency_of_only_chords

		plt.bar(range(len(D)), list(D.values()), align='center')
		plt.xticks(range(len(D)), list(D.keys()))
		# # for python 2.x:
		# plt.bar(range(len(D)), D.values(), align='center')  # python 2.x
		# plt.xticks(range(len(D)), D.keys())  # in python 2.x

		plt.show()





from mido import MidiFile
from mido.midifiles.units import tempo2bpm, bpm2tempo, tick2second, second2tick
# from midiUtils import Utils, Tone, Note, Chord, Song
#from toneFrequency import ToneFrequency


util = Utils()

mid = MidiFile(MIDI_file_to_analyze)
# mid = MidiFile('./whenTheSaintsLeftHandMelody/wtsgmi5.mid')
print((mid.ticks_per_beat))
# print(mid.ticks_per_beat, 1000)
# print(mid.tracks)
# exit()
tempoPerMinute = 500000
totalNotes = list()
song = Song()
for i, track in enumerate(mid.tracks):
    print('Track {}: {}'.format(i, track.name))
    cumulative_time = 0
    currently_played_notes = dict()

    for msg in track:
      try: 
        if not msg.is_meta:
          cumulative_time += tick2second(msg.time, mid.ticks_per_beat, tempoPerMinute)
          message_note = util.note_number_to_letter(msg.note)

          if message_note in currently_played_notes:
            absoluteStart, velocityStart = currently_played_notes.pop(message_note)
            note = Note(absoluteStart, cumulative_time, message_note, velocityStart, msg.velocity)
            song.add_note(note)
          else:
            currently_played_notes[message_note] = (cumulative_time, msg.velocity)
      except:
        continue  
    # print(len(song.tones))
    # song.sort_by_start_of_tone()
    for tone in song.tones:
    	print(tone)
    frequencies = ToneFrequency(song)
    print(frequencies.frequency_of_all_notes)
    print(frequencies.frequency_of_only_chords)

#@title Pydot Graph
pattern = midi.read_midifile(MIDI_file_to_analyze)

notes = {NOTE_NAME_MAP_SHARP[name]: name.replace('s', '#').replace('_', '')
         for name in NOTE_NAME_MAP_SHARP}

l = pattern[0]

tracks = []

for track in pattern:
    x = list()
    y = list()
    t = 0

    for ev in track:
        t += ev.tick
        if type(ev) == midi.events.NoteOnEvent:
            pitch, vel = ev.data
            if vel > 0:
                y.append(pitch)
                x.append(t)

    if len(x) > 0:
        tracks.append((x, y))


colors = ['orange', 'green', 'red']
n = 0
for track in tracks:
    G = nx.DiGraph()
    for i in range(len(track[1])-1):
        G.add_edge(notes[track[1][i]],
                   notes[track[1][i+1]],
                   color=colors[n])

        G.add_node(notes[track[1][i]],
                   color=colors[n])

        G.add_node(notes[track[1][i+1]],
                   color=colors[n])

    nx.drawing.nx_pydot.write_dot(G, "g%s.dot" % n)
    n += 1

nx.draw_networkx(G)
#plt.figure(figsize=(19, 10), dpi=300)
plt.savefig('Dot_Graph.jpg')

#@title Generate and plot MIDI file's Parson's code and contour.
length_of_the_code_and_contour_in_notes = 70 #@param {type:"slider", min:10, max:2000, step:10}
code_and_contour_start_offset_in_notes = 0 #@param {type:"slider", min:0, max:1000, step:10}

#!/usr/bin/env python

# https://github.com/snus-kin/parsons-code

"""
    Take a midi file and convert it to parsons code, with a limit and offset
"""

def midi_to_parsons(midifile, limit=code_and_contour_start_offset_in_notes + length_of_the_code_and_contour_in_notes, offset=code_and_contour_start_offset_in_notes):
    """
        Input: midifile (string) = A midi file path
               limit    (int)    = How long is the parsons code
               offset   (int)    = How many notes in do we start
        Output: parsons (string) = A string containing a parsons code length
                                   limit at an offset from the start of the file
    """
    count = 0
    parsons = ""
    for message in mido.MidiFile(midifile):
        if "note_on" in str(message) and offset == 0:
            if parsons == "":
                # initialise list
                prev = message.note
                parsons += "*"

            # simple comparison
            elif message.note > prev:
                parsons += "U"
                prev = message.note
            elif message.note < prev:
                parsons += "D"
                prev = message.note
            elif message.note == prev:
                parsons += "R"
                prev = message.note

            #increment count
            count += 1
            if count >= limit:
                break
        elif "note_on" in str(message):
            offset -= 1

    return parsons

code = midi_to_parsons(MIDI_file_to_analyze)

#print(code)

""" Parsons code to contour plot """

def contour(code):
    print('Song Parson Code is:')
    print(code)
    print('')
    if code[0] != "*":
        raise ValueError("Parsons Code must start with '*'")

    contour_dict = {}
    pitch = 0
    index = 0

    maxp = 0
    minp = 0

    contour_dict[(pitch, index)] = "*"

    for point in code:
        if point == "R":
            index += 1
            contour_dict[(pitch, index)] = "-"

            index += 1
            contour_dict[(pitch, index)] = "*"
        elif point == "U":
            index += 1
            pitch -= 1
            contour_dict[(pitch, index)] = "/"

            index += 1
            pitch -= 1
            contour_dict[(pitch, index)] = "*"

            if pitch < maxp:
                maxp = pitch
        elif point == "D":
            index += 1
            pitch += 1
            contour_dict[(pitch, index)] = "\\"

            index += 1
            pitch += 1
            contour_dict[(pitch, index)] = "*"

            if pitch > minp:
                minp = pitch

    for pitch in range(maxp, minp+1):
        line = [" " for _ in range(index + 1)]
        for pos in range(index + 1):
            if (pitch, pos) in contour_dict:
                line[pos] = contour_dict[(pitch, pos)]


        print("".join(line))


contour(code)