import math from numpy import concatenate from midiutil.MidiFile import MIDIFile from itertools import product, combinations from copy import deepcopy from re import sub, split def freq2note(f): return 12*math.log(f/440.0, 2) + 69 def note2freq(n): return math.pow(2, (n-69)/12)*440 def midi2note(m): notes = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] return notes[int(m%12)] + str(int((m//12)-2)) def notes2midis(s): spl = split("[, ]+", s) notes = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] midis = [] for s in spl: midis.append( (int(sub("[^\d-]+", "", s))+2) * 12 + notes.index( sub("-?[\d]+", "", s)) ) return midis def filterInRange(arr, filterNotes): newArr = [] for _x in arr: if _x >= filterNotes[0] and _x <= filterNotes[1]: newArr.append(_x) return newArr noPartials = 64 def createMIDI(name, notes): mf = MIDIFile(1) track = 0 time = 0 mf.addTrackName(track, time, name) mf.addTempo(track, time, 120) for i, n in enumerate(notes): if isinstance(n, int): mf.addNote(0, 0, n, i/4, 0.25, 60) elif isinstance(n, tuple) or isinstance(n, list): for _n in n: mf.addNote(0, 0, _n, i/4, 0.25, 60) # write it to disk with open(name+".mid", 'wb') as outf: mf.writeFile(outf) def getNotePartials(note): f = note2freq(note) p = [] for i in range(noPartials): p.append(freq2note(f * (i+1))) return p def getChordPartials(notes): #return concatenate([getNotePartials(n) for n in notes]) p1 = [] en = None # get the chord partials with their respective coefficients for n in notes: tempPartials = getNotePartials(n) en = enumerate(tempPartials) for i, partial in en: #if 20 <= note2freq(partial) <= 20000: p1.append([partial, getCoeffByPartial(i)]) del n, i, en p1 = sorted(p1, key=lambda x: x[0]) return p1 p = [] for i in range(127): p.append(getNotePartials(i)) # an approximate to the weight of the individual partials # this equation can be changed according to the instrument, or a database # the partial index starts from 0. def getWeightByPartial(num, partial): return num * (1 / (partial + 1)) def getCoeffByPartial(partial): return 1 / (partial + 1) def getPartialSimilarity(p1, p2, algoType, tolerance): #p2 = p[note2] partialPairs = list(((x,y) for x in range(len(p1)) for y in range(len(p2)))) similarPartials = 0 similarPS = [] overallScore = 0 if algoType == 0: if len(partialPairs) > 0: partialPairsEnum = enumerate(partialPairs) for m, partialPair in partialPairsEnum: diff = p2[partialPair[1]][0] - p1[partialPair[0]][0] if abs(diff) <= tolerance: similarPartials += 1 diff2 = (1-p1[partialPair[0]][1])*diff overallScore += abs(diff) # how many similar partials, with threshold, but no overlaps elif algoType == 1: if len(partialPairs) > 0: while len(partialPairs) > 0: partialScore = 10000 partialPairDel = None partialPairsEnum = enumerate(partialPairs) for m, partialPair in partialPairsEnum: diff = p2[partialPair[1]][0] - p1[partialPair[0]][0] if abs(diff) <= tolerance: diff2 = (1-p1[partialPair[0]][1])*diff if diff2 < partialScore: partialScore = diff2 partialPairDel = deepcopy(partialPairs[m]) if partialPairDel is not None: overallScore += abs(partialScore) similarPartials += 1 # now let's remove all the partials that share at least one of the matched group for m in range(len(partialPairs)-1, -1, -1): if partialPairs[m][0] == partialPairDel[0] \ or partialPairs[m][1] == partialPairDel[1]: #print('deleting...',m,partialPairs[m]) del partialPairs[m] else: break # how many similar partials, but no threshold elif algoType == 2: if len(partialPairs) > 0: while len(partialPairs) > 0: partialScore = 10000 partialPairDel = None for m, partialPair in enumerate(partialPairs): diff = p1[partialPair[0]][0] - p2[partialPair[1]] diff2 = (1-p1[partialPair[0]][1])*diff if abs(diff2) < abs(partialScore): partialScore = diff2 partialPairDel = deepcopy(partialPairs[m]) if partialPairDel is not None: overallScore += partialScore similarPartials += 1 print('smallest:',partialPairDel,partialScore, p1[partialPairDel[0]], p2[partialPairDel[1]]) print(partialPairDel[0]) # now let's remove all the partials that share at least one of the matched group for m in range(len(partialPairs)-1, -1, -1): if partialPairs[m][0] == partialPairDel[0] \ or partialPairs[m][1] == partialPairDel[1]: del partialPairs[m] continue if (partialPairDel[0] < partialPairDel[1] and partialPairs[m][0] < partialPairDel[0] and partialPairDel[1] < partialPairs[m][1]) \ or (partialPairDel[0] > partialPairDel[1] and partialPairs[m][0] > partialPairDel[0] and partialPairDel[0] > partialPairs[m][1]): print('deleting 2...',m,partialPairs[m]) del partialPairs[m] else: break # how many similar partials, but no threshold plus finding ones that are the most dissonant # should be the equivalent of 4 but coded version elif algoType == 3: # ???? pass elif algoType == 4: notes2 = deepcopy(notes) notes2.append(note2) freqs, amps = harmonic_tone(pitch_to_freq(notes2), n_partials=noPartials) overallScore = dissonance(freqs, amps, model='sethares1993') partialPairs = [] return overallScore, similarPartials # TYPE: how do we calculate the harmonic space? # TYPE 0: the classic - counting partial ONLY if it crosses a threshold # TYPE 1: the adjusted - like 0 + weighted # TYPE 2: the precise - no crossings, calculating all pairs by distance + weighted def getSimilarSpace(notes, algoType=2, tolerance=0.2): p1 = getChordPartials(notes) midiNotes = [] for note2 in range(127): p2 = getChordPartials([note2]) overallScore, noSamePartials = getPartialSimilarity(p1, p2, algoType, tolerance) midiNotes.append([note2, overallScore, noSamePartials]) midiNotes = sorted(midiNotes, key=lambda x: (-x[2], abs(x[1])) ) midiN = deepcopy(midiNotes) return midiN,[midi2note(midiNotes[x][0]) for x in range(len(midiNotes)-1,-1,-1)] def getNextNote(noteCollection, position): noteFilter = notes2midis("C1 C5") indx = noteCollection.index(min(noteCollection)) noteFilter3 = deepcopy(noteCollection) midiN, harmSpace = getSimilarSpace(noteCollection, algoType=0, tolerance=0.25) harmSpace_ = filterInRange(notes2midis(','.join(harmSpace)), deepcopy(noteFilter)) print(harmSpace_) harmSpace = [] # filter our the prompted notes for x in harmSpace_: if x not in noteFilter3: harmSpace.append(x) if position != -1: return harmSpace[position] else: return harmSpace def getSimSpaceWrapper(noteRange, notes, algoType=0, tolerance=0.02): #noteCollection = notes2midis("B4 E3 A3") # D#1 A#1 F2 C4 D4 G4 A4 noteFilter = noteRange noteFilter3 = notes midiN, harmSpace = getSimilarSpace(noteFilter3, algoType, tolerance) harmSpace_ = filterInRange(notes2midis(','.join(harmSpace)), deepcopy(noteFilter)) print() harmSpace = [] # filter our the prompted notes for x in harmSpace_: if x not in noteFilter3: harmSpace.append(x) print([midi2note(x) for x in harmSpace]) print("Sorted:\n") print([midi2note(x) for x in sorted(harmSpace[:len(harmSpace)//4])]) ''' Function to get notes based on "similarity" Concept that is remotely similar to the concept of resonance or consonance as described by William Sethares in his book Tuning Timbre Harmony Scale (1998) different algorithms deal with it differently: Algorithm 0 - consider two partials being "similar" when being same or smaller than a threshold this "threshold" is called tolerance, measured in semitones such that 0.02 = 2 cents Algorithm 1 - find the "most similar" pairs of partials and select them one by one until there are none left Algorithm 2 - find the "most similar" pairs of partials, but without any threshold at all Algorithm 4 - using dissonance model by Sethares ''' noteRange = notes2midis("E1 C5") notes = notes2midis("C3") getSimSpaceWrapper(noteRange, notes, algoType=0, tolerance=0.02)