Now that you've done a decent job of getting the basic n-gram model down, you'll want to work on the offsets so things actually sound thoughtful, and not just a long stream of notes. To do this, import the data (experiment first with small # of notes, say 100) and then run KMean Clustering to find patterns. Actually, it might be useful to plot stuff first - that way you get an idea of how much notes are separated from one another.
For the offsets, you'll assume that notes only come at 0.25, 0.50, etc. instead of weird numbers like 0.682 or 1.537. This is from rounding the numbers in the N-Gram notebook.
Dependencies:
--2. N-Gram
%matplotlib inline
from collections import Counter, defaultdict
from sklearn.cluster import KMeans
from itertools import izip
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import sys, copy, random
# Read in the data and generate the offsets.
# It's okay to generate offsets here since are trigram notes.
# Recall: len = how long note lasts, offset = when it's hit (e.g. stopwatch).
numberofitems = 12345678910112
data = pd.read_csv('./oscar2ngrams.txt', names=['Note','Len'])
# toggle if want to limit to first k items
# 1078 notes in original MIDI file, so limit to first 1078 notes in ngram file
numberofitems = 1078
data = data[:numberofitems]
# Re-add offsets
totaloffset = 0
offsets = []
for i in data['Len']:
offsets.append(totaloffset)
totaloffset += i
data["Offset"] = pd.Series(offsets)
print "Shape: (%s,%s)" % (data.shape)
data.head()
Shape: (1078,3)
Note | Len | Offset | |
---|---|---|---|
0 | D5 | 0.75 | 0.00 |
1 | E4 | 0.50 | 0.75 |
2 | G#4 | 1.75 | 1.25 |
3 | A4 | 3.50 | 3.00 |
4 | G#4 | 0.25 | 6.50 |
5 rows × 3 columns
Next, let's see what our data looks like. For each note, we'll plot len (y axis) over offset (x axis).
# Plot the length over offset.
# *args is some (n, 2) array you want to plot
def plotTiming(data, labels=None, clustercenters=None):
numberofitems = len(data)
# generate colors
clusterCodes = dict()
if labels is not None:
for i in labels:
r = lambda: random.randint(0,255)
clusterCodes[i] = ('#%02X%02X%02X' % (r(),r(),r())).lower()
# Initialize the graph
dx = data['Offset']
dy = data['Len']
dn = data['Note']
plt.plot(dx, dy, 'm.--', linewidth=1.5)
for ix, (x, y) in enumerate(zip(dx, dy)):
color = 'ko'
if labels is not None:
color = clusterCodes[labels[ix]]
plt.plot(x, y, 'x', ms=15, mew=1.5, color=color)
continue
plt.plot(x, y, color)
# plot the cluster centers if available
if clustercenters is not None:
for currColorIx, i in enumerate(clustercenters):
cx = i[0]
cy = i[1]
color = clusterCodes[currColorIx]
plt.plot(cx, cy, 'ko', mew=0, ms=7.5) # plot black. same color: color=color
# plot the ticks if under certain # of points
if numberofitems <= 100:
plt.xticks(range(0, int(max(dx)) + 1))
# Annotate with note data only if under certain # of points
# (Otherwise, it gets too messy!)
if numberofitems <= 100 and labels is None:
for note, offset, length in izip(dn, dx, dy):
plt.annotate(note, xy=(offset, length), color='g')
# Enter title
plt.title('Generated N-Grams (Pruned)', fontsize=20, horizontalalignment='center')
# set fig limits, size, and other display things
fig = plt.gcf()
ax = plt.gca()
plt.ylim([0, max(dy)+ 0.25])
plt.xlim([min(dx) - 1, max(dx) + 1])
plt.ylabel('Duration', fontsize=16)
plt.xlabel('Offset', fontsize=16)
plt.grid()
fig = plt.gcf()
fig.set_size_inches(18, 6)
# plt.xkcd()
ax.xaxis.grid(False)
plotTiming(data)
plt.show()
It looks like we've got quite a few clusters! But be careful: those outliers with a higher length, for example, should really go with the previous notes (since length is the value that comes after). Also, remember that a bunch of notes all together do NOT mean that the notes are all the same -- it means the lengths are the same. Let's do some clustering and create a function to print the results: i.e. the different clusters in different colors (even just alternating between two of them).
Let's do another visualization: where the notes appear on the keyboard (x axis) by where they are in the piece (offset, y axis ascending). For this, we need to add another column to the dataframe, and write a function to assign a note a numberical value.
# Given a note, such as C5 or D#7, convert it
# into a note on the keyboard between 0 and 87 inclusive.
# Don't convert it for mingus; try to use music21 note style
# as much as possible for all this stuff.
def quantify(note):
notevals = {
'C' : 0,
'D' : 2,
'E' : 4,
'F' : 5,
'G' : 7,
'A' : 9,
'B' : 11
}
quantized = 0
octave = int(note[-1]) - 1
for i in note[:-1]:
if i in notevals: quantized += notevals[i]
if i == '-': quantized -= 1
if i == '#': quantized += 1
quantized += 12 * octave
return quantized
# Plot the notes as played by MIDI.
def plotMIDI(data, quantizednotes):
# Initialize the graph
dy = data['Offset']
dn = data['Note']
dx = quantizednotes
print len(dx), len(dy)
plt.plot(dx, dy, 'm.--', linewidth=1.5)
for x, y in zip(dx, dy):
plt.plot(x, y, 'ko')
# Plot the ticks
plt.xticks(range(0, 87))
plt.yticks(range(0, int(max(dy)+1)))
# Annotate with note data
for note, nx, ny in izip(dn, dx, dy):
plt.annotate(note, xy=(nx, ny), color='g')
# Set xticks on top
fig = plt.gcf()
ax = plt.gca()
# Set fig limits, size, and other display things
max_xax = 18
max_yax = 12
# max_yax = len(data) / 10
fig = plt.gcf()
fig.set_size_inches(max_xax, max_yax)
plt.ylim([-1, max(dy) + 1])
plt.title('Generated N-Grams', fontsize=20, horizontalalignment='center')
plt.ylabel('Offset', fontsize=16)
plt.xlabel('Keyboard', fontsize=16)
plt.grid()
ax.yaxis.grid(False)
quantizednotes = [quantify(i) for i in data['Note']]
plotMIDI(data, quantizednotes)
plt.show()
1078 1078
Ah, now we're in shape to do some clustering! We can cluster based on offset alone, or maybe also on where things are on the keyboard. Actually, let's try both: we can feed this 2-dimensional vector to KMeans, and see how things turn out.
notesX = data["Offset"].reshape(-1, 1)
notesY = data["Len"].reshape(-1, 1)
notesXY = np.concatenate((notesX, notesY), axis=1)
notenames = np.array([i for i in data["Note"]])
km = KMeans(n_clusters=int(np.sqrt(len(notesX) / 2)))
km.fit(notesXY)
kmlabels = km.labels_
plotTiming(data, labels=kmlabels, clustercenters=km.cluster_centers_)
# Hacky thing: even though you won't use the final length (for each note) in the final playback,
# still use these clusters to determine which notes go together. At this point I don't think
# it'd be best to change things around, i.e. do a new clustering method.
Here, write out the data for use with D3.js, i.e. a list in form [xi, yi] etc. Only write out the offset/length data and the keyboard data.
# First, write out offset/length data. offset = x-axis, length=y-axis.
with open("./d3js/ngram_offsetlendata.txt", 'wb') as f:
for a in data.itertuples(index=False):
f.write("[%s, %s],\n" % (a[2], a[1]))
# Next, write out the keyboard data.
quantizednotes = [quantify(i) for i in data['Note']]
with open("./d3js/ngram_keyboarddata.txt", 'wb') as f:
for x, y in zip(quantizednotes, data["Offset"]):
f.write("[%s, %s],\n" % (x, y))