CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
amanchadha

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: amanchadha/coursera-deep-learning-specialization
Path: blob/master/C5 - Sequence Models/Week 1/Jazz improvisation with LSTM/grammar.py
Views: 4819
1
'''
2
Author: Ji-Sung Kim, Evan Chow
3
Project: jazzml / (used in) deepjazz
4
Purpose: Extract, manipulate, process musical grammar
5
6
Directly taken then cleaned up from Evan Chow's jazzml,
7
https://github.com/evancchow/jazzml,with permission.
8
'''
9
10
from collections import OrderedDict, defaultdict
11
from itertools import groupby
12
from music21 import *
13
import copy, random, pdb
14
15
#from preprocess import *
16
17
''' Helper function to determine if a note is a scale tone. '''
18
def __is_scale_tone(chord, note):
19
# Method: generate all scales that have the chord notes th check if note is
20
# in names
21
22
# Derive major or minor scales (minor if 'other') based on the quality
23
# of the chord.
24
scaleType = scale.DorianScale() # i.e. minor pentatonic
25
if chord.quality == 'major':
26
scaleType = scale.MajorScale()
27
# Can change later to deriveAll() for flexibility. If so then use list
28
# comprehension of form [x for a in b for x in a].
29
scales = scaleType.derive(chord) # use deriveAll() later for flexibility
30
allPitches = list(set([pitch for pitch in scales.getPitches()]))
31
allNoteNames = [i.name for i in allPitches] # octaves don't matter
32
33
# Get note name. Return true if in the list of note names.
34
noteName = note.name
35
return (noteName in allNoteNames)
36
37
''' Helper function to determine if a note is an approach tone. '''
38
def __is_approach_tone(chord, note):
39
# Method: see if note is +/- 1 a chord tone.
40
41
for chordPitch in chord.pitches:
42
stepUp = chordPitch.transpose(1)
43
stepDown = chordPitch.transpose(-1)
44
if (note.name == stepDown.name or
45
note.name == stepDown.getEnharmonic().name or
46
note.name == stepUp.name or
47
note.name == stepUp.getEnharmonic().name):
48
return True
49
return False
50
51
''' Helper function to determine if a note is a chord tone. '''
52
def __is_chord_tone(lastChord, note):
53
return (note.name in (p.name for p in lastChord.pitches))
54
55
''' Helper function to generate a chord tone. '''
56
def __generate_chord_tone(lastChord):
57
lastChordNoteNames = [p.nameWithOctave for p in lastChord.pitches]
58
return note.Note(random.choice(lastChordNoteNames))
59
60
''' Helper function to generate a scale tone. '''
61
def __generate_scale_tone(lastChord):
62
# Derive major or minor scales (minor if 'other') based on the quality
63
# of the lastChord.
64
scaleType = scale.WeightedHexatonicBlues() # minor pentatonic
65
if lastChord.quality == 'major':
66
scaleType = scale.MajorScale()
67
# Can change later to deriveAll() for flexibility. If so then use list
68
# comprehension of form [x for a in b for x in a].
69
scales = scaleType.derive(lastChord) # use deriveAll() later for flexibility
70
allPitches = list(set([pitch for pitch in scales.getPitches()]))
71
allNoteNames = [i.name for i in allPitches] # octaves don't matter
72
73
# Return a note (no octave here) in a scale that matches the lastChord.
74
sNoteName = random.choice(allNoteNames)
75
lastChordSort = lastChord.sortAscending()
76
sNoteOctave = random.choice([i.octave for i in lastChordSort.pitches])
77
sNote = note.Note(("%s%s" % (sNoteName, sNoteOctave)))
78
return sNote
79
80
''' Helper function to generate an approach tone. '''
81
def __generate_approach_tone(lastChord):
82
sNote = __generate_scale_tone(lastChord)
83
aNote = sNote.transpose(random.choice([1, -1]))
84
return aNote
85
86
''' Helper function to generate a random tone. '''
87
def __generate_arbitrary_tone(lastChord):
88
return __generate_scale_tone(lastChord) # fix later, make random note.
89
90
91
''' Given the notes in a measure ('measure') and the chords in that measure
92
('chords'), generate a list of abstract grammatical symbols to represent
93
that measure as described in GTK's "Learning Jazz Grammars" (2009).
94
95
Inputs:
96
1) "measure" : a stream.Voice object where each element is a
97
note.Note or note.Rest object.
98
99
>>> m1
100
<music21.stream.Voice 328482572>
101
>>> m1[0]
102
<music21.note.Rest rest>
103
>>> m1[1]
104
<music21.note.Note C>
105
106
Can have instruments and other elements, removes them here.
107
108
2) "chords" : a stream.Voice object where each element is a chord.Chord.
109
110
>>> c1
111
<music21.stream.Voice 328497548>
112
>>> c1[0]
113
<music21.chord.Chord E-4 G4 C4 B-3 G#2>
114
>>> c1[1]
115
<music21.chord.Chord B-3 F4 D4 A3>
116
117
Can have instruments and other elements, removes them here.
118
119
Outputs:
120
1) "fullGrammar" : a string that holds the abstract grammar for measure.
121
Format:
122
(Remember, these are DURATIONS not offsets!)
123
"R,0.125" : a rest element of (1/32) length, or 1/8 quarter note.
124
"C,0.125<M-2,m-6>" : chord note of (1/32) length, generated
125
anywhere from minor 6th down to major 2nd down.
126
(interval <a,b> is not ordered). '''
127
128
def parse_melody(fullMeasureNotes, fullMeasureChords):
129
# Remove extraneous elements.x
130
measure = copy.deepcopy(fullMeasureNotes)
131
chords = copy.deepcopy(fullMeasureChords)
132
measure.removeByNotOfClass([note.Note, note.Rest])
133
chords.removeByNotOfClass([chord.Chord])
134
135
# Information for the start of the measure.
136
# 1) measureStartTime: the offset for measure's start, e.g. 476.0.
137
# 2) measureStartOffset: how long from the measure start to the first element.
138
measureStartTime = measure[0].offset - (measure[0].offset % 4)
139
measureStartOffset = measure[0].offset - measureStartTime
140
141
# Iterate over the notes and rests in measure, finding the grammar for each
142
# note in the measure and adding an abstract grammatical string for it.
143
144
fullGrammar = ""
145
prevNote = None # Store previous note. Need for interval.
146
numNonRests = 0 # Number of non-rest elements. Need for updating prevNote.
147
for ix, nr in enumerate(measure):
148
# Get the last chord. If no last chord, then (assuming chords is of length
149
# >0) shift first chord in chords to the beginning of the measure.
150
try:
151
lastChord = [n for n in chords if n.offset <= nr.offset][-1]
152
except IndexError:
153
chords[0].offset = measureStartTime
154
lastChord = [n for n in chords if n.offset <= nr.offset][-1]
155
156
# FIRST, get type of note, e.g. R for Rest, C for Chord, etc.
157
# Dealing with solo notes here. If unexpected chord: still call 'C'.
158
elementType = ' '
159
# R: First, check if it's a rest. Clearly a rest --> only one possibility.
160
if isinstance(nr, note.Rest):
161
elementType = 'R'
162
# C: Next, check to see if note pitch is in the last chord.
163
elif nr.name in lastChord.pitchNames or isinstance(nr, chord.Chord):
164
elementType = 'C'
165
# L: (Complement tone) Skip this for now.
166
# S: Check if it's a scale tone.
167
elif __is_scale_tone(lastChord, nr):
168
elementType = 'S'
169
# A: Check if it's an approach tone, i.e. +-1 halfstep chord tone.
170
elif __is_approach_tone(lastChord, nr):
171
elementType = 'A'
172
# X: Otherwise, it's an arbitrary tone. Generate random note.
173
else:
174
elementType = 'X'
175
176
# SECOND, get the length for each element. e.g. 8th note = R8, but
177
# to simplify things you'll use the direct num, e.g. R,0.125
178
if (ix == (len(measure)-1)):
179
# formula for a in "a - b": start of measure (e.g. 476) + 4
180
diff = measureStartTime + 4.0 - nr.offset
181
else:
182
diff = measure[ix + 1].offset - nr.offset
183
184
# Combine into the note info.
185
noteInfo = "%s,%.3f" % (elementType, nr.quarterLength) # back to diff
186
187
# THIRD, get the deltas (max range up, max range down) based on where
188
# the previous note was, +- minor 3. Skip rests (don't affect deltas).
189
intervalInfo = ""
190
if isinstance(nr, note.Note):
191
numNonRests += 1
192
if numNonRests == 1:
193
prevNote = nr
194
else:
195
noteDist = interval.Interval(noteStart=prevNote, noteEnd=nr)
196
noteDistUpper = interval.add([noteDist, "m3"])
197
noteDistLower = interval.subtract([noteDist, "m3"])
198
intervalInfo = ",<%s,%s>" % (noteDistUpper.directedName,
199
noteDistLower.directedName)
200
# print "Upper, lower: %s, %s" % (noteDistUpper,
201
# noteDistLower)
202
# print "Upper, lower dnames: %s, %s" % (
203
# noteDistUpper.directedName,
204
# noteDistLower.directedName)
205
# print "The interval: %s" % (intervalInfo)
206
prevNote = nr
207
208
# Return. Do lazy evaluation for real-time performance.
209
grammarTerm = noteInfo + intervalInfo
210
fullGrammar += (grammarTerm + " ")
211
212
return fullGrammar.rstrip()
213
214
''' Given a grammar string and chords for a measure, returns measure notes. '''
215
def unparse_grammar(m1_grammar, m1_chords):
216
m1_elements = stream.Voice()
217
currOffset = 0.0 # for recalculate last chord.
218
prevElement = None
219
for ix, grammarElement in enumerate(m1_grammar.split(' ')):
220
terms = grammarElement.split(',')
221
currOffset += float(terms[1]) # works just fine
222
223
# Case 1: it's a rest. Just append
224
if terms[0] == 'R':
225
rNote = note.Rest(quarterLength = float(terms[1]))
226
m1_elements.insert(currOffset, rNote)
227
continue
228
229
# Get the last chord first so you can find chord note, scale note, etc.
230
try:
231
lastChord = [n for n in m1_chords if n.offset <= currOffset][-1]
232
except IndexError:
233
m1_chords[0].offset = 0.0
234
lastChord = [n for n in m1_chords if n.offset <= currOffset][-1]
235
236
# Case: no < > (should just be the first note) so generate from range
237
# of lowest chord note to highest chord note (if not a chord note, else
238
# just generate one of the actual chord notes).
239
240
# Case #1: if no < > to indicate next note range. Usually this lack of < >
241
# is for the first note (no precedent), or for rests.
242
if (len(terms) == 2): # Case 1: if no < >.
243
insertNote = note.Note() # default is C
244
245
# Case C: chord note.
246
if terms[0] == 'C':
247
insertNote = __generate_chord_tone(lastChord)
248
249
# Case S: scale note.
250
elif terms[0] == 'S':
251
insertNote = __generate_scale_tone(lastChord)
252
253
# Case A: approach note.
254
# Handle both A and X notes here for now.
255
else:
256
insertNote = __generate_approach_tone(lastChord)
257
258
# Update the stream of generated notes
259
insertNote.quarterLength = float(terms[1])
260
if insertNote.octave < 4:
261
insertNote.octave = 4
262
m1_elements.insert(currOffset, insertNote)
263
prevElement = insertNote
264
265
# Case #2: if < > for the increment. Usually for notes after the first one.
266
else:
267
# Get lower, upper intervals and notes.
268
interval1 = interval.Interval(terms[2].replace("<",''))
269
interval2 = interval.Interval(terms[3].replace(">",''))
270
if interval1.cents > interval2.cents:
271
upperInterval, lowerInterval = interval1, interval2
272
else:
273
upperInterval, lowerInterval = interval2, interval1
274
lowPitch = interval.transposePitch(prevElement.pitch, lowerInterval)
275
highPitch = interval.transposePitch(prevElement.pitch, upperInterval)
276
numNotes = int(highPitch.ps - lowPitch.ps + 1) # for range(s, e)
277
278
# Case C: chord note, must be within increment (terms[2]).
279
# First, transpose note with lowerInterval to get note that is
280
# the lower bound. Then iterate over, and find valid notes. Then
281
# choose randomly from those.
282
283
if terms[0] == 'C':
284
relevantChordTones = []
285
for i in range(0, numNotes):
286
currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())
287
if __is_chord_tone(lastChord, currNote):
288
relevantChordTones.append(currNote)
289
if len(relevantChordTones) > 1:
290
insertNote = random.choice([i for i in relevantChordTones
291
if i.nameWithOctave != prevElement.nameWithOctave])
292
elif len(relevantChordTones) == 1:
293
insertNote = relevantChordTones[0]
294
else: # if no choices, set to prev element +-1 whole step
295
insertNote = prevElement.transpose(random.choice([-2,2]))
296
if insertNote.octave < 3:
297
insertNote.octave = 3
298
insertNote.quarterLength = float(terms[1])
299
m1_elements.insert(currOffset, insertNote)
300
301
# Case S: scale note, must be within increment.
302
elif terms[0] == 'S':
303
relevantScaleTones = []
304
for i in range(0, numNotes):
305
currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())
306
if __is_scale_tone(lastChord, currNote):
307
relevantScaleTones.append(currNote)
308
if len(relevantScaleTones) > 1:
309
insertNote = random.choice([i for i in relevantScaleTones
310
if i.nameWithOctave != prevElement.nameWithOctave])
311
elif len(relevantScaleTones) == 1:
312
insertNote = relevantScaleTones[0]
313
else: # if no choices, set to prev element +-1 whole step
314
insertNote = prevElement.transpose(random.choice([-2,2]))
315
if insertNote.octave < 3:
316
insertNote.octave = 3
317
insertNote.quarterLength = float(terms[1])
318
m1_elements.insert(currOffset, insertNote)
319
320
# Case A: approach tone, must be within increment.
321
# For now: handle both A and X cases.
322
else:
323
relevantApproachTones = []
324
for i in range(0, numNotes):
325
currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())
326
if __is_approach_tone(lastChord, currNote):
327
relevantApproachTones.append(currNote)
328
if len(relevantApproachTones) > 1:
329
insertNote = random.choice([i for i in relevantApproachTones
330
if i.nameWithOctave != prevElement.nameWithOctave])
331
elif len(relevantApproachTones) == 1:
332
insertNote = relevantApproachTones[0]
333
else: # if no choices, set to prev element +-1 whole step
334
insertNote = prevElement.transpose(random.choice([-2,2]))
335
if insertNote.octave < 3:
336
insertNote.octave = 3
337
insertNote.quarterLength = float(terms[1])
338
m1_elements.insert(currOffset, insertNote)
339
340
# update the previous element.
341
prevElement = insertNote
342
343
return m1_elements
344