Previously, your non-iPython script oscar.py gave you a text file oscar2chords.txt, in the form:
Chord{....}..., c minor 7, 0.125, 0.800
In this notebook, you extract this information and build a possible "chord bank" of possible chords and what triggers there are (e.g. certain note sequences) to play them. This is necessary because you need to get a unique collection of chords Oscar Peterson played, and your text file oscar2chords.txt is insufficient because it lists all chords that are played rather than only the unique ones.
Output: a chord bank as a .txt file that you can use with the clustering algorithms and playback in the other notesbooks.
Dependencies:
--2. N-Gram
from collections import Counter, defaultdict
from sklearn.cluster import KMeans
from mingus.midi import fluidsynth
from mingus.containers import NoteContainer
from mingus.containers.Bar import Bar
import mingus.core.value as value
import pandas as pd
import numpy as np
import sys, re, itertools, random
sys.path.append('C:/Python27/Lib/site-packages')
fluidsynth.init('/usr/share/sounds/sf2/FluidR3_GM.sf2',"alsa")
True
# Import the chord data.
allchords = pd.read_csv('oscar2chords.txt', skiprows=2)[:].sort("Offset")
allchords.index = xrange(1, len(allchords) + 1)
with open('oscar2chords.txt', 'rb') as f:
metmark = float(f.readline())
tsig_num, tsig_den = [i for i in f.readline().replace(' /', '').split()]
print "Metronome, Timesig Numerator, Timesig Denominator, # chords played"
print metmark, tsig_num, tsig_den, len(allchords)
allchords.sort(columns="Offset", ascending=True)[:10]
allchords.head()
Metronome, Timesig Numerator, Timesig Denominator, # chords played 176.0 4 4 297
FullName | CommonName | Len | Offset | |
---|---|---|---|---|
1 | Chord {D in octave 5 | C in octave 4 | E in oc... | A6-perfect-fourth minor tetrachord | 1.125000 | 8.000 |
2 | Chord {A in octave 3 | G in octave 3 | E in oc... | A3-incomplete dominant-seventh chord | 1.250000 | 8.000 |
3 | Chord {E in octave 6 | E in octave 4 | D in oc... | D6-quartal trichord | 1.375000 | 9.625 |
4 | Chord {C in octave 4 | A in octave 5} Dotted Q... | A5-interval class 3 | 1.500000 | 9.625 |
5 | Chord {G in octave 3 | A in octave 3} Quarter ... | A3-interval class 2 | 1.666667 | 9.625 |
5 rows × 4 columns
# Convert music21 note to mingus note.
# This version (different from that in 3. Play Notes)
# doesn't return a Note object: returns a string.
def mingifytext(note):
accidental = re.compile("[A-Z](-|#)[0-9]")
if accidental.match(note):
if '-' not in note: note = "%s%s-%s" % (note[0], note[1], note[2])
else: note = note.replace('-', 'b-')
else: note = "%s-%s" % (note[0], note[1])
return note
# Given a MUSIC21 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
# Extract notes in chords.
# Shorter single-note chords: lowest prob of being played
def getChords(allchords, mingify=True):
chords_poss = []
for chordname in allchords['FullName']:
notenames = re.findall("[CDEFGAB]+[-]*[sharp|flat]*[in octave]*[1-9]", chordname)
for ix in xrange(len(notenames)):
notenames[ix] = notenames[ix].replace(" in octave ", '').replace("-sharp","#").replace("-flat","-")
if mingify==True:
notenames = [mingifytext(note) for note in notenames]
else:
notenames = [note for note in notenames]
toDel = [ix for ix in xrange(len(notenames)) if "6" in notenames[ix]
or "5" in notenames[ix]] # rm chords with notes too high, e.g. oct == 6 or 5
notenames = [i for ix, i in enumerate(notenames) if ix not in toDel]
if len(notenames) > 2: # min num of notes in valid chord = 3. Can change this
chords_poss.append(sorted(notenames)) # important to sort, else can't find duplicates
result = sorted(list(chords_poss for chords_poss,_ in itertools.groupby(chords_poss)))
result = list(result for result,_ in itertools.groupby(result))
return result
oscarchords = getChords(allchords)
print len(oscarchords)
oscarchords[:10]
40
[['A-2', 'A-3', 'E-3'], ['A-3', 'A-4', 'C-4'], ['A-3', 'Bb-3', 'D-4'], ['A-3', 'Bb-3', 'D-4', 'F-4'], ['A-3', 'C#-3', 'E-4'], ['A-3', 'C#-4', 'F#-4'], ['A-3', 'C-3', 'C-4'], ['A-3', 'C-3', 'E-4'], ['A-3', 'C-4', 'D-4'], ['A-3', 'C-4', 'E-4', 'G-3']]
# Write chords out into cleaned-up version of Oscar's chords
with open("oscar2chords_extract.txt", 'wb') as f:
for chord in oscarchords:
for n in chord:
f.write(n)
f.write(' ')
f.write('\n')
# Test audio. First populate bars.
# Maybe this is a better way to play notes rather than one huge bar ---> same duration/each note.
# allbars = []
# for chord in uniquechords:
# b = Bar()
# b.set_meter((4, 4))
# b.place_notes(chord, 4)
# allbars.append(b)
# for bar in allbars:
# fluidsynth.play_Bar(bar, 1, 300)
Okay, now that you have a bunch of unique musical chords, the next step is to build a predictive model that takes in a sequence of notes (assume offsets all equal), and generates chords to accompany them. For example:
$Notes:\:\:\:D\:\:\:F\:\:\:A\:\:\:C\:\:|\:B\:\:\:G\:\:\:F\:\:\:E-\:\:\:|\:E \\Chords:\:D-7\:\:\:\:\:\:\:\:\:\:\:\:\:|\:\:\:\:\:\:\:G7\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:\:|\:CMaj7$
Doesn't have to be perfect. Also, just assume 4/4 time, for putting the notes into bars later you'll just use 4/4 time for each and a note for each beat, this keeps things simpler. Can still have randomly generated gaps between clusters of course, but you assume that the improviser just plays fast straight 8ths for each cluster or musical idea.
# Read in notes and convert into bitwise frame. But might be expensive.
# note sequence is a list in music 21 style, (D/D-)
# note that chordbank and notesequence should be in same format (mingus/m21)
# Returns default dict with notes in whatever music21/mingus style chordbank is already in.
from sklearn.cluster import Ward
def comp(notesequence, chordbank):
""" Problem: how to determine whether chord "fits" note or not? """
# Cluster notes into chunks
quantizednotes = np.array([quantify(note) for note in notesequence]).reshape(-1, 1)
wardclf = Ward()
wardclf.fit(quantizednotes)
# get indices of first note in each cluster
firstixs = [0]
firstnotes = [notesequence[0]]
currLabel = 0
for ix, label in enumerate(wardclf.labels_):
if ix == 0: continue
if label != currLabel:
currLabel = label
firstixs.append(ix)
firstnotes.append(notesequence[ix])
# For each start note of cluster, find random chord that also starts with that note.
allmatches = defaultdict()
for ix, note in zip(firstixs, firstnotes):
matching = [chord for chord in chordbank if chord[0][0] == note[0]] # null if no matches
if len(matching) >= 1:
allmatches[ix] = random.choice(matching)
""" Revise above chord-choosing section. Need to choose better chords -- maybe bigram or trigram model
with backoff? """
return allmatches
testnotes = ['D4','F4','A4','C5','E5','E-5','D-5','D5','C5','A4','A#4','B4','D5','F5',
'E5','E-5','D-5','D5','C5','A4','A#4','A-5','C5','A4','A#4','B4','D5',
'G5','G-5','F5','D5','C5','A4','A#4','A-5','C5','E-5','E5']
chordmatches = comp(testnotes, oscarchords)
chordmatches
defaultdict(None, {0: ['D-4', 'F-4', 'G#-3'], 32: ['A-3', 'C-4', 'D-4'], 34: ['A-2', 'A-3', 'E-3'], 3: ['C-4', 'F#-3', 'F-4'], 9: ['A-3', 'Bb-3', 'D-4'], 12: ['D-4', 'F-4', 'G#-3'], 19: ['A-3', 'C-3', 'E-4'], 21: ['A-3', 'Bb-3', 'D-4'], 23: ['A-3', 'C-4', 'E-4', 'G-3'], 26: ['D-4', 'F-4', 'G#-3']})
# Test doing notes with chords
# fullbars = []
# for ix, note in enumerate(testnotes):
# b = Bar()
# b.set_meter((4,4))
# if ix in chordmatches.keys():
# allbarnotes = chordmatches[ix]
# allbarnotes.append(mingifytext(note))
# b.place_notes(allbarnotes, 4)
# fullbars.append(b)
# continue
# b.place_notes(mingifytext(note), 4)
# fullbars.append(b)
# for bar in fullbars:
# fluidsynth.play_Bar(bar, 1, 500)