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 from tkinter import * import pygame.midi import time import numpy as np from dissonant import harmonic_tone, dissonance, pitch_to_freq midiChords = [] 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, notes): newArr = [] for x in arr: if x >= notes[0] and x <= notes[1]: newArr.append(x) return newArr def gaussian(x, mu, sig): return ( 1.0 / (np.sqrt(2.0 * np.pi) * sig) * np.exp(-np.power((x - mu) / sig, 2.0) / 2) ) noPartials = 63 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))) del i, f 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 # might change this later to an array after better observation # 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(notes, notes2, p1, p2, algoType, tolerance): #p1 = #p2 = #p2 = p[note2] partialPairs = list(((x,y) for x in range(len(p1)) for y in range(len(p2)))) similarPartials = 0 similarPS = [] overallScore = 0 #print("note2",note2) if algoType == 0: if len(partialPairs) > 0: partialPairsEnum = enumerate(partialPairs) for m, partialPair in partialPairsEnum: diff = p2[partialPair[1]][0] - p1[partialPair[0]][0] #diff = abs(diff) * gaussian( p1[partialPair[1]][0], p1[partialPair[0]][0], 0.1 ) if abs(diff) <= tolerance: diff2 = (1-p1[partialPair[0]][1])*diff overallScore += abs(diff2) similarPartials += 1 # how many similar partials, with threshold, but no overlaps elif algoType == 1: if len(partialPairs) > 0: while len(partialPairs) > 0: partialScore = 8000 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]: del partialPairs[m] else: break # how many similar partials, but no threshold elif algoType == 2: if len(partialPairs) > 0: while len(partialPairs) > 0: partialScore = 8000 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: freqs, amps = harmonic_tone(pitch_to_freq(notes + 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 getLatentSpace(notes, notes2, algoType=2, tolerance=0.05): p1 = getChordPartials(notes) p2 = getChordPartials(notes2) # E3 G#3 B3 chordLen = 3 chordRange = 24 # octave noteOffset = 60 coeff = 790 lineSize, similarP = getPartialSimilarity(notes, notes2, p1, p2, algoType, tolerance) lineSize = lineSize * coeff print(lineSize) offx = 10 offy = 10 x0 = offx y0 = 800-offy chordCombinations = combinations(range(chordRange), chordLen) chordCombLen = sum(1 for ignore in chordCombinations) chordCombinations = combinations(range(chordRange), chordLen) _i=0 chords = [] hiDis = 0 allPoints = [] allDiss = [] allPSim1 = [] allPSim2 = [] allSim1 = [] allSim2 = [] def onEnter(e): global midiChords #w.focus_set() elIds = w.find_withtag("current") w.focus(elIds) if elIds: ch = chords[elIds[0]-1][0] #print(elIds,chords[elIds[0]-1]) chNotes = [midi2note(x) for x in ch] print(chNotes, chords[elIds[0]-1]) midiChords.append(ch) for n in ch: player.note_on(n, 100) time.sleep(0.3) for n in ch: player.note_off(n, 100) for ch in chordCombinations: if _i % 10 == 0: print(str(int(round(_i/chordCombLen*100))) + "%") ch2 = [x+noteOffset for x in ch] # midi vals of the notes p3 = getChordPartials(ch2) # get the harmonic distance from both p1 and p2 partialSim1, similarP1 = getPartialSimilarity(notes, list(ch), p1, p3, algoType, tolerance) partialSim2, similarP2 = getPartialSimilarity(notes2, list(ch), p2, p3, algoType, tolerance) chords.append([ch2, abs(partialSim1)*coeff, abs(partialSim2) * coeff, similarP1, similarP2]) x1 = x0 + abs(partialSim1) * coeff y1 = y0 - abs(partialSim2) * coeff freqs, amps = harmonic_tone(pitch_to_freq(ch), n_partials=noPartials) _diss = dissonance(freqs, amps, model='sethares1993') if _diss > hiDis: hiDis = _diss diss = 16 - int(math.floor( (_diss / 3.11)*16)) bgColor = "#"+hex(diss)[2:] + hex(diss)[2:] + hex(diss)[2:] o = w.create_oval(x1-3, y1-3, x1 + 3, y1 + 3, fill=bgColor, tags="t"+str(_i)) w.tag_bind("t"+str(_i), "", onEnter) allPoints.append(o) allDiss.append(_diss) allPSim1.append(partialSim1) allPSim2.append(partialSim2) allSim1.append(similarP1) allSim2.append(similarP2) w.update() _i+=1 del ch2, p3, partialSim1, partialSim2 print("Done") print(hiDis) pygame.midi.init() player = pygame.midi.Output(0) player.set_instrument(0) #del player #pygame.midi.quit() return allPoints, allDiss, allPSim1, allPSim2, allSim1, allSim2 def buttonReset(): global midiChords print("Resetting...") midiChords = [] def buttonStop(): global midiChords createMIDI(str(time.time()), midiChords) print("Midi created.") def buttonSkip(): global midiChords print("Skipping...") midiChords.append([]) midiChords.append([]) midiChords.append([]) midiChords.append([]) canvas_width = 800 canvas_height = 800 root = Tk() root.title("Points") w = Canvas(root, width=canvas_width, height=canvas_height, bg="#000") w.pack(expand=YES, fill=BOTH) bReset = Button(w, text="Reset", command=buttonReset) bStop = Button(w, text="Stop", comman=buttonStop) bSkip = Button(w, text="Skip", comman=buttonSkip) bReset.place(x=0,y=0) bStop.place(x=40,y=0) bSkip.place(x=80,y=0) root.bind("", lambda x: buttonReset()) root.bind("", lambda x: buttonStop()) root.bind("", lambda x: buttonSkip()) def main(): noteCollection = notes2midis("C3 E3 G3") noteCollection2 = notes2midis("C#3 F#3 A#3") sortByNumPartials = True points, diss, pSim1, pSim2, pNum1, pNum2 = getLatentSpace(noteCollection, noteCollection2, algoType=0, tolerance=0.02) dissMin = min(diss) dissMax = max(diss) xMax = max(pSim1) yMax = max(pSim2) sim1Max = max(pNum1) sim2Max = max(pNum2) print(sim1Max, sim2Max) pointsPre = [] dissLen = len(diss) for i in range(dissLen): pointsPre.append([pNum1[i], pNum2[i], pSim1[i], pSim2[i]]) pointsSorted = sorted(pointsPre, key=lambda x: (x[0], x[1], x[2], x[3])) xCoeff = canvas_width / xMax yCoeff = canvas_height / yMax if sortByNumPartials: xCoeff = canvas_width / sim1Max yCoeff = canvas_height / sim2Max #print(point) for i in range(len(points)): #if i > 0: # break d = (diss[i] - dissMin) / (dissMax - dissMin) if d > 0.6: w.delete(points[i]) continue brightness = int((1-d) * 255) fillColor = '#' + hex(brightness)[2:] + hex(brightness)[2:] + hex(brightness)[2:] w.itemconfig(points[i], fill=fillColor) if sortByNumPartials: w.moveto(points[i], xCoeff*(sim1Max-pNum1[i]) + pSim1[i] * xCoeff*1, canvas_height - (yCoeff*(sim2Max-pNum2[i]) + pSim2[i] * yCoeff*1)) else: w.moveto(points[i], pSim1[i] * xCoeff, canvas_height - (pSim2[i] * yCoeff)) root.after(800, main) root.mainloop()