import pandas as pd
import numpy as np
import networkx as nx
import plotly.graph_objects as go
import plotly.figure_factory as ff
import plotly.express as px
import plotly.io as pio
pio.templates.default = 'plotly_white'
import webcolors as wc
import colorsys
import mido
from mido import MidiFile
rainbow = ['red','purple','slateblue','darkslateblue','royalblue','dodgerblue','mediumseagreen','yellowgreen','yellow','gold','orange','orangered']
#colormode = "sequential"
colormode = "fifths"
s = '80%'
l = '60%'
if colormode == "sequential": letters = ['C','B','A#','A','G#','G','F#','F','E','D#','D','C#']
if colormode == "fifths": letters = ['C','F','A#','D#','G#','C#','F#','B','E','A','D','G']
hues = [0,330,300,270,240,210,180,150,120,90,60,30]
huezip = dict(zip(letters,hues))
hsl = [f'hsl({h}, {s}, {l})' for h in hues]
colors = dict(zip(letters,hsl))
freq = pd.read_csv('frequencies.csv').set_index('midi').dropna()
notes = list(freq['note'])
freq['hue']=[huezip[letter] for letter in freq['letter']]
lightmap = ['10%','20%','30%','40%','50%','60%','70%','80%','90%']
freq['lightness']=[lightmap[int(octave)] for octave in freq['octave']]
freq['hsl'] = [str('hsl('+str(h)+','+str(s)+','+l+')') for h,l in zip(freq['hue'],freq['lightness'])]
songs = ['baba','badguy','canond','clair','dancingqueen','frere','furelise','happybirthday','heartsoul','littleboxes','madworld','moonlight','moonriver','ode','swanee','twinkle','vivspring']
data = {}
def parse_midi(song):
mid = MidiFile('audio/midi/'+song+'.mid')
tracks = {}
for i, track in enumerate(mid.tracks):
t = 'track_'+str(i)
df=pd.DataFrame(columns=['midi','note','time','tstep','letter'])
midi = []
time = []
for msg in track:
if msg.type == 'note_on':
midi.append(msg.note)
time.append(msg.time)
if msg.type == 'note_off':
midi.append(msg.note)
time.append(msg.time)
df['midi']=midi
df['note']=[freq['note'][m] for m in df['midi']]
df['letter']=[freq['letter'][m] for m in df['midi']]
try: df['color']=[freq[freq['note']==n]['hsl'].values[0] for n in df['note']]
except: continue
df['time']=time
df['tstep']=df['time'].cumsum()
if df.shape[0]>0:
tracks[t]=df
data[song]=tracks
for song in songs: parse_midi(song)
def vis_dist(song,stats=['letter','note']):
s = data[song]
for stat in stats:
fig = go.Figure()
for i, track in enumerate(s):
values = list(s[track][stat].unique())
counts=[]
colormap = []
for v in values:
counts.append(s[track][s[track][stat]==v].shape[0])
if stat=='note': colormap.append(freq[freq['note']==v]['hsl'].values[0])
if stat=='letter': colormap.append(colors[v])
fig.add_trace(go.Bar(x=values, y=counts,name=track,marker_color=colormap))
fig.update_layout(title=song, showlegend=False)
fig.show()
fig.write_image('charts/raster/'+song+'_'+stat+'_distribution.png')
fig.write_image('charts/vector/'+song+'_'+stat+'_distribution.svg')
for song in songs: vis_dist(song)
def vis_score(song):
s = data[song]
fig = go.Figure()
for i, track in enumerate(s):
df = s[track]
fig.add_trace(go.Scatter(x=df['tstep'], y=df['midi'],mode='lines+markers',text=df['letter'], name='track',marker_color=df['color'], line_color='lightgrey'))
fig.update_layout(showlegend=False, title=song)
fig.show()
fig.write_image('charts/raster/'+song+'_score.png')
fig.write_image('charts/vector/'+song+'_score.svg')
return
for song in songs: vis_score(song)
def vis_delta(song):
s = data[song]
fig = go.Figure()
for i, track in enumerate(s):
df = s[track]
df['delta_midi'] = df['midi'].diff().fillna(0)
vcounts = df['delta_midi'].value_counts()
categories = list(map(str, list(vcounts.index)))
fig.add_trace(go.Bar(x=categories, y=list(vcounts.values),name=track,marker_color='grey'))
fig.update_layout(title=song+' midi delta', barmode='stack', yaxis_type="log")
fig.show()
fig.write_image('charts/raster/'+song+'_midi_delta.png')
fig.write_image('charts/vector/'+song+'_midi_delta.svg')
return
for song in songs: vis_delta(song)
def draw_heatmap(matrix):
order = ['C','B','A#','A','G#','G','F#','F','E','D#','D','C#']
matrix = matrix.reindex(order, axis=1)
matrix = matrix.reindex(order, axis=1)
fig = go.Figure(data=go.Heatmap(z=matrix,x=order,y=order,colorscale = 'Viridis'))
fig.update_layout(title=song)
fig.show()
return
def calculate_network_stats(stack):
G=nx.from_pandas_edgelist(stack, 'x', 'y', ['bridges'])
network_stats=pd.DataFrame(columns=['adjacencies','centrality'],index=G.nodes())
network_stats['adjacencies']=[len(adj[1]) for node,adj in enumerate(G.adjacency())]
network_stats['centrality'] = [nx.degree_centrality(G)[n] for n in G.nodes()]
# Learn more about centrality measures in NetworkX here: https://networkx.github.io/documentation/stable/reference/algorithms/centrality.html
return network_stats
def calculate_positions(stack):
# Create a networkx graph from the list of pairs
G=nx.from_pandas_edgelist(stack, 'x', 'y', ['bridges'])
# Generate position data for each node:
#pos=layout(G)
# if weighted:
pos=nx.kamada_kawai_layout(G, weight='bridges')
# Save x, y locations of each edge
edge_x = []
edge_y = []
# Calculate x,y positions of an edge's 'start' (x0,y0) and 'end' (x1,y1) points
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.append(x0)
edge_x.append(x1)
edge_y.append(y0)
edge_y.append(y1)
# Bundle it all up in a dict:
edges = dict(x=edge_x,y=edge_y)
# Save x, y locations of each node
node_x = []
node_y = []
# Save node stats for annotation
node_name = []
node_adjacencies = []
node_centralities = []
# Calculate x,y positions of nodes
for node in G.nodes():
node_name.append(node)# Save node names
x, y = pos[node]
node_x.append(x)
node_y.append(y)
for node, adjacencies in enumerate(G.adjacency()):
node_adjacencies.append(len(adjacencies[1]))
for n in G.nodes():
node_centralities.append(nx.degree_centrality(G)[n])
# Bundle it all up in a dict:
nodes = dict(x=node_x,y=node_y,name=node_name,adjacencies=node_adjacencies,centralities=node_centralities)
return edges,nodes
def draw_graph(edges,nodes):
# Draw edges
edge_trace = go.Scatter(
x=edges['x'], y=edges['y'],
line=dict(width=1, color='#888'),
mode='lines+markers',
hoverinfo='text')
# Draw nodes
node_trace = go.Scatter(
x=nodes['x'],
y=nodes['y'],
# Optional: Add labels to points *without* hovering (can get a little messy)
mode='markers+text',
marker = dict(
size = [c*30 for c in nodes['centralities']],
color = [colors[i] for i in nodes['name']],
line = dict(width=2,color='lightgrey')
),
# ...or, just add markers (no text)
#mode='markers',
text=nodes['name'],
hoverinfo='text')
filename=song.lower().replace(" ","_")
# Draw figure
fig = go.Figure(data=[edge_trace,node_trace],
layout=go.Layout(
title=song,
titlefont_size=16,
showlegend=False,
hovermode='closest',
margin=dict(b=20,l=5,r=5,t=120),
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
template='plotly_white')
)
fig.update_traces(textposition='top center')
# Show figure
fig.show()
def draw_network(matrix, hist=False):
stack = pd.DataFrame(matrix.stack()).reset_index().rename(columns={'level_0':'x','level_1':'y',0:'bridges'})
stack = stack[stack['bridges']>0]
if hist: px.histogram(stack, x='bridges',title=song).show()
stats = calculate_network_stats(stack)
[edges,nodes] = calculate_positions(stack)
draw_graph(edges,nodes)
return
def calc_network(song, heatmap=False, network=True):
s = data[song]
letter_networks = []
for track in s:
df = s[track]
df['next_letter']=df['letter'].shift(-1)
letter_network = pd.DataFrame(0, columns=letters,index=letters)
for src in letters:
for target in letters:
bridges = df[(df['letter']==src) & (df['next_letter']==target)].shape[0]
#print(bridges)
letter_network[src][target]=bridges
letter_networks.append(letter_network)
matrix = sum(letter_networks)
if heatmap: draw_heatmap(matrix)
if network: draw_network(matrix)
return
for song in songs: calc_network(song)