Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/C5 - Sequence Models/Week 1/Jazz improvisation with LSTM/grammar.py
Views: 4819
'''1Author: Ji-Sung Kim, Evan Chow2Project: jazzml / (used in) deepjazz3Purpose: Extract, manipulate, process musical grammar45Directly taken then cleaned up from Evan Chow's jazzml,6https://github.com/evancchow/jazzml,with permission.7'''89from collections import OrderedDict, defaultdict10from itertools import groupby11from music21 import *12import copy, random, pdb1314#from preprocess import *1516''' Helper function to determine if a note is a scale tone. '''17def __is_scale_tone(chord, note):18# Method: generate all scales that have the chord notes th check if note is19# in names2021# Derive major or minor scales (minor if 'other') based on the quality22# of the chord.23scaleType = scale.DorianScale() # i.e. minor pentatonic24if chord.quality == 'major':25scaleType = scale.MajorScale()26# Can change later to deriveAll() for flexibility. If so then use list27# comprehension of form [x for a in b for x in a].28scales = scaleType.derive(chord) # use deriveAll() later for flexibility29allPitches = list(set([pitch for pitch in scales.getPitches()]))30allNoteNames = [i.name for i in allPitches] # octaves don't matter3132# Get note name. Return true if in the list of note names.33noteName = note.name34return (noteName in allNoteNames)3536''' Helper function to determine if a note is an approach tone. '''37def __is_approach_tone(chord, note):38# Method: see if note is +/- 1 a chord tone.3940for chordPitch in chord.pitches:41stepUp = chordPitch.transpose(1)42stepDown = chordPitch.transpose(-1)43if (note.name == stepDown.name or44note.name == stepDown.getEnharmonic().name or45note.name == stepUp.name or46note.name == stepUp.getEnharmonic().name):47return True48return False4950''' Helper function to determine if a note is a chord tone. '''51def __is_chord_tone(lastChord, note):52return (note.name in (p.name for p in lastChord.pitches))5354''' Helper function to generate a chord tone. '''55def __generate_chord_tone(lastChord):56lastChordNoteNames = [p.nameWithOctave for p in lastChord.pitches]57return note.Note(random.choice(lastChordNoteNames))5859''' Helper function to generate a scale tone. '''60def __generate_scale_tone(lastChord):61# Derive major or minor scales (minor if 'other') based on the quality62# of the lastChord.63scaleType = scale.WeightedHexatonicBlues() # minor pentatonic64if lastChord.quality == 'major':65scaleType = scale.MajorScale()66# Can change later to deriveAll() for flexibility. If so then use list67# comprehension of form [x for a in b for x in a].68scales = scaleType.derive(lastChord) # use deriveAll() later for flexibility69allPitches = list(set([pitch for pitch in scales.getPitches()]))70allNoteNames = [i.name for i in allPitches] # octaves don't matter7172# Return a note (no octave here) in a scale that matches the lastChord.73sNoteName = random.choice(allNoteNames)74lastChordSort = lastChord.sortAscending()75sNoteOctave = random.choice([i.octave for i in lastChordSort.pitches])76sNote = note.Note(("%s%s" % (sNoteName, sNoteOctave)))77return sNote7879''' Helper function to generate an approach tone. '''80def __generate_approach_tone(lastChord):81sNote = __generate_scale_tone(lastChord)82aNote = sNote.transpose(random.choice([1, -1]))83return aNote8485''' Helper function to generate a random tone. '''86def __generate_arbitrary_tone(lastChord):87return __generate_scale_tone(lastChord) # fix later, make random note.888990''' Given the notes in a measure ('measure') and the chords in that measure91('chords'), generate a list of abstract grammatical symbols to represent92that measure as described in GTK's "Learning Jazz Grammars" (2009).9394Inputs:951) "measure" : a stream.Voice object where each element is a96note.Note or note.Rest object.9798>>> m199<music21.stream.Voice 328482572>100>>> m1[0]101<music21.note.Rest rest>102>>> m1[1]103<music21.note.Note C>104105Can have instruments and other elements, removes them here.1061072) "chords" : a stream.Voice object where each element is a chord.Chord.108109>>> c1110<music21.stream.Voice 328497548>111>>> c1[0]112<music21.chord.Chord E-4 G4 C4 B-3 G#2>113>>> c1[1]114<music21.chord.Chord B-3 F4 D4 A3>115116Can have instruments and other elements, removes them here.117118Outputs:1191) "fullGrammar" : a string that holds the abstract grammar for measure.120Format:121(Remember, these are DURATIONS not offsets!)122"R,0.125" : a rest element of (1/32) length, or 1/8 quarter note.123"C,0.125<M-2,m-6>" : chord note of (1/32) length, generated124anywhere from minor 6th down to major 2nd down.125(interval <a,b> is not ordered). '''126127def parse_melody(fullMeasureNotes, fullMeasureChords):128# Remove extraneous elements.x129measure = copy.deepcopy(fullMeasureNotes)130chords = copy.deepcopy(fullMeasureChords)131measure.removeByNotOfClass([note.Note, note.Rest])132chords.removeByNotOfClass([chord.Chord])133134# Information for the start of the measure.135# 1) measureStartTime: the offset for measure's start, e.g. 476.0.136# 2) measureStartOffset: how long from the measure start to the first element.137measureStartTime = measure[0].offset - (measure[0].offset % 4)138measureStartOffset = measure[0].offset - measureStartTime139140# Iterate over the notes and rests in measure, finding the grammar for each141# note in the measure and adding an abstract grammatical string for it.142143fullGrammar = ""144prevNote = None # Store previous note. Need for interval.145numNonRests = 0 # Number of non-rest elements. Need for updating prevNote.146for ix, nr in enumerate(measure):147# Get the last chord. If no last chord, then (assuming chords is of length148# >0) shift first chord in chords to the beginning of the measure.149try:150lastChord = [n for n in chords if n.offset <= nr.offset][-1]151except IndexError:152chords[0].offset = measureStartTime153lastChord = [n for n in chords if n.offset <= nr.offset][-1]154155# FIRST, get type of note, e.g. R for Rest, C for Chord, etc.156# Dealing with solo notes here. If unexpected chord: still call 'C'.157elementType = ' '158# R: First, check if it's a rest. Clearly a rest --> only one possibility.159if isinstance(nr, note.Rest):160elementType = 'R'161# C: Next, check to see if note pitch is in the last chord.162elif nr.name in lastChord.pitchNames or isinstance(nr, chord.Chord):163elementType = 'C'164# L: (Complement tone) Skip this for now.165# S: Check if it's a scale tone.166elif __is_scale_tone(lastChord, nr):167elementType = 'S'168# A: Check if it's an approach tone, i.e. +-1 halfstep chord tone.169elif __is_approach_tone(lastChord, nr):170elementType = 'A'171# X: Otherwise, it's an arbitrary tone. Generate random note.172else:173elementType = 'X'174175# SECOND, get the length for each element. e.g. 8th note = R8, but176# to simplify things you'll use the direct num, e.g. R,0.125177if (ix == (len(measure)-1)):178# formula for a in "a - b": start of measure (e.g. 476) + 4179diff = measureStartTime + 4.0 - nr.offset180else:181diff = measure[ix + 1].offset - nr.offset182183# Combine into the note info.184noteInfo = "%s,%.3f" % (elementType, nr.quarterLength) # back to diff185186# THIRD, get the deltas (max range up, max range down) based on where187# the previous note was, +- minor 3. Skip rests (don't affect deltas).188intervalInfo = ""189if isinstance(nr, note.Note):190numNonRests += 1191if numNonRests == 1:192prevNote = nr193else:194noteDist = interval.Interval(noteStart=prevNote, noteEnd=nr)195noteDistUpper = interval.add([noteDist, "m3"])196noteDistLower = interval.subtract([noteDist, "m3"])197intervalInfo = ",<%s,%s>" % (noteDistUpper.directedName,198noteDistLower.directedName)199# print "Upper, lower: %s, %s" % (noteDistUpper,200# noteDistLower)201# print "Upper, lower dnames: %s, %s" % (202# noteDistUpper.directedName,203# noteDistLower.directedName)204# print "The interval: %s" % (intervalInfo)205prevNote = nr206207# Return. Do lazy evaluation for real-time performance.208grammarTerm = noteInfo + intervalInfo209fullGrammar += (grammarTerm + " ")210211return fullGrammar.rstrip()212213''' Given a grammar string and chords for a measure, returns measure notes. '''214def unparse_grammar(m1_grammar, m1_chords):215m1_elements = stream.Voice()216currOffset = 0.0 # for recalculate last chord.217prevElement = None218for ix, grammarElement in enumerate(m1_grammar.split(' ')):219terms = grammarElement.split(',')220currOffset += float(terms[1]) # works just fine221222# Case 1: it's a rest. Just append223if terms[0] == 'R':224rNote = note.Rest(quarterLength = float(terms[1]))225m1_elements.insert(currOffset, rNote)226continue227228# Get the last chord first so you can find chord note, scale note, etc.229try:230lastChord = [n for n in m1_chords if n.offset <= currOffset][-1]231except IndexError:232m1_chords[0].offset = 0.0233lastChord = [n for n in m1_chords if n.offset <= currOffset][-1]234235# Case: no < > (should just be the first note) so generate from range236# of lowest chord note to highest chord note (if not a chord note, else237# just generate one of the actual chord notes).238239# Case #1: if no < > to indicate next note range. Usually this lack of < >240# is for the first note (no precedent), or for rests.241if (len(terms) == 2): # Case 1: if no < >.242insertNote = note.Note() # default is C243244# Case C: chord note.245if terms[0] == 'C':246insertNote = __generate_chord_tone(lastChord)247248# Case S: scale note.249elif terms[0] == 'S':250insertNote = __generate_scale_tone(lastChord)251252# Case A: approach note.253# Handle both A and X notes here for now.254else:255insertNote = __generate_approach_tone(lastChord)256257# Update the stream of generated notes258insertNote.quarterLength = float(terms[1])259if insertNote.octave < 4:260insertNote.octave = 4261m1_elements.insert(currOffset, insertNote)262prevElement = insertNote263264# Case #2: if < > for the increment. Usually for notes after the first one.265else:266# Get lower, upper intervals and notes.267interval1 = interval.Interval(terms[2].replace("<",''))268interval2 = interval.Interval(terms[3].replace(">",''))269if interval1.cents > interval2.cents:270upperInterval, lowerInterval = interval1, interval2271else:272upperInterval, lowerInterval = interval2, interval1273lowPitch = interval.transposePitch(prevElement.pitch, lowerInterval)274highPitch = interval.transposePitch(prevElement.pitch, upperInterval)275numNotes = int(highPitch.ps - lowPitch.ps + 1) # for range(s, e)276277# Case C: chord note, must be within increment (terms[2]).278# First, transpose note with lowerInterval to get note that is279# the lower bound. Then iterate over, and find valid notes. Then280# choose randomly from those.281282if terms[0] == 'C':283relevantChordTones = []284for i in range(0, numNotes):285currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())286if __is_chord_tone(lastChord, currNote):287relevantChordTones.append(currNote)288if len(relevantChordTones) > 1:289insertNote = random.choice([i for i in relevantChordTones290if i.nameWithOctave != prevElement.nameWithOctave])291elif len(relevantChordTones) == 1:292insertNote = relevantChordTones[0]293else: # if no choices, set to prev element +-1 whole step294insertNote = prevElement.transpose(random.choice([-2,2]))295if insertNote.octave < 3:296insertNote.octave = 3297insertNote.quarterLength = float(terms[1])298m1_elements.insert(currOffset, insertNote)299300# Case S: scale note, must be within increment.301elif terms[0] == 'S':302relevantScaleTones = []303for i in range(0, numNotes):304currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())305if __is_scale_tone(lastChord, currNote):306relevantScaleTones.append(currNote)307if len(relevantScaleTones) > 1:308insertNote = random.choice([i for i in relevantScaleTones309if i.nameWithOctave != prevElement.nameWithOctave])310elif len(relevantScaleTones) == 1:311insertNote = relevantScaleTones[0]312else: # if no choices, set to prev element +-1 whole step313insertNote = prevElement.transpose(random.choice([-2,2]))314if insertNote.octave < 3:315insertNote.octave = 3316insertNote.quarterLength = float(terms[1])317m1_elements.insert(currOffset, insertNote)318319# Case A: approach tone, must be within increment.320# For now: handle both A and X cases.321else:322relevantApproachTones = []323for i in range(0, numNotes):324currNote = note.Note(lowPitch.transpose(i).simplifyEnharmonic())325if __is_approach_tone(lastChord, currNote):326relevantApproachTones.append(currNote)327if len(relevantApproachTones) > 1:328insertNote = random.choice([i for i in relevantApproachTones329if i.nameWithOctave != prevElement.nameWithOctave])330elif len(relevantApproachTones) == 1:331insertNote = relevantApproachTones[0]332else: # if no choices, set to prev element +-1 whole step333insertNote = prevElement.transpose(random.choice([-2,2]))334if insertNote.octave < 3:335insertNote.octave = 3336insertNote.quarterLength = float(terms[1])337m1_elements.insert(currOffset, insertNote)338339# update the previous element.340prevElement = insertNote341342return m1_elements343344