├── .gitignore ├── ChordSpaces.py ├── Chunk.py ├── ClassicalFG.py ├── Constraints.py ├── EuterpeaShorthands.py ├── ExampleGUI.py ├── Examples.py ├── FromMidi2.py ├── KulittaTests.py ├── MidiFuns.py ├── MidiReader.py ├── MusicGrammars.py ├── MusicPlayer.py ├── PEConstants.py ├── PTGG.py ├── PostProc.py ├── PythonEuterpea.py ├── QuotientSpaces.py ├── README.md ├── Search.py ├── __init__.py ├── cleanup.bat ├── doc └── PythonEuterpea_reference.pdf └── test.mid /.gitignore: -------------------------------------------------------------------------------- 1 | # Not sure what this is 2 | *.swp 3 | 4 | # Persistence Files 5 | *.p 6 | 7 | # Mac-specific 8 | .DS_Store 9 | 10 | # gitignore settings for IDEs, Python, and R projects 11 | 12 | ### IDEs 13 | 14 | # PyCharm project settings 15 | .idea 16 | 17 | # Eclipse project settings 18 | .project 19 | .pydevproject 20 | 21 | # Spyder project settings 22 | .spyderproject 23 | 24 | # Rope project settings 25 | .ropeproject 26 | 27 | 28 | ### Haskell projects 29 | 30 | *.hi 31 | 32 | 33 | ### Python projects 34 | 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | *.o 43 | 44 | # Distribution / packaging 45 | .Python 46 | env/ 47 | build/ 48 | develop-eggs/ 49 | dist/ 50 | downloads/ 51 | eggs/ 52 | .eggs/ 53 | lib/ 54 | lib64/ 55 | parts/ 56 | sdist/ 57 | var/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | 62 | # PyInstaller 63 | # Usually these files are written by a python run_experiment_script from a template 64 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 65 | *.manifest 66 | *.spec 67 | 68 | # Installer logs 69 | pip-log.txt 70 | pip-delete-this-directory.txt 71 | 72 | # Unit test / coverage reports 73 | htmlcov/ 74 | .tox/ 75 | .coverage 76 | .coverage.* 77 | .cache 78 | nosetests.xml 79 | coverage.xml 80 | *,cover 81 | .hypothesis/ 82 | 83 | # Translations 84 | *.mo 85 | *.pot 86 | 87 | # Django stuff: 88 | *.log 89 | local_settings.py 90 | 91 | # Flask stuff: 92 | instance/ 93 | .webassets-cache 94 | 95 | # Scrapy stuff: 96 | .scrapy 97 | 98 | # Sphinx documentation 99 | docs/_build/ 100 | 101 | # PyBuilder 102 | target/ 103 | 104 | # IPython Notebook 105 | .ipynb_checkpoints 106 | 107 | # pyenv 108 | .python-version 109 | 110 | # celery beat schedule file 111 | celerybeat-schedule 112 | 113 | # dotenv 114 | .env 115 | 116 | # virtualenv 117 | venv/ 118 | ENV/ 119 | 120 | 121 | ### R projects 122 | 123 | # History files 124 | .Rhistory 125 | .Rapp.history 126 | 127 | # Session Data files 128 | .RData 129 | 130 | # Example code in package build process 131 | *-Ex.R 132 | 133 | # Output files from R CMD build 134 | /*.tar.gz 135 | 136 | # Output files from R CMD check 137 | /*.Rcheck/ 138 | 139 | # RStudio files 140 | .Rproj.user/ 141 | 142 | # produced vignettes 143 | vignettes/*.html 144 | vignettes/*.pdf 145 | 146 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 147 | .httr-oauth 148 | 149 | # knitr and R markdown default cache directories 150 | /*_cache/ 151 | /cache/ 152 | 153 | # Temporary files created by R markdown 154 | *.utf8.md 155 | *.knit.md 156 | -------------------------------------------------------------------------------- /ChordSpaces.py: -------------------------------------------------------------------------------- 1 | # Chord Spaces Implementation for Python 2 | # Ported from Haskell implementation 3 | # Author: Donya Quck 4 | # Last modified: 28-Oct-2014 5 | # 6 | # This file implements the functions from the original 7 | # ChordSpaces.lhs associated with Kulitta (Haskell) in 8 | # Python. A 1:1 implementation was possible in most 9 | # cases. An additional in-place randomization method 10 | # is also included. 11 | 12 | 13 | #===================================================== 14 | # PITCH SPACE GENERATION 15 | 16 | from itertools import product 17 | 18 | 19 | def makeRange(ranges): 20 | def f(a): return list(range(a[0],a[1]+1)) 21 | fullRanges = list(map (f, ranges)) 22 | return list(map (list, list(product(*fullRanges)))) 23 | 24 | # Generating filtered space 25 | 26 | def nextValue(x,ranges): 27 | def updatePoint(x,i,ranges): 28 | if i >= len(x) : 29 | x.append(-1) 30 | elif x[i] == ranges[i][1]: 31 | x[i] = ranges[i][0] 32 | updatePoint(x, i+1, ranges) 33 | else: 34 | x[i] = x[i]+1 35 | updatePoint(x,0,ranges) 36 | 37 | def searchRange(f, ranges): 38 | def searchFrom(f,x,ranges,xs): 39 | while len(x) == len(ranges): 40 | if f(x): xs.append(list(x)) 41 | nextValue(x,ranges) 42 | return xs 43 | x0 = list(map(lambda x: x[0], ranges)) 44 | return list(searchFrom(f, x0, ranges, [])) 45 | 46 | 47 | 48 | #===================================================== 49 | # OPTIC RELATIONS 50 | 51 | # Atomic operations 52 | # Note: these are not in place 53 | 54 | def o(chord,octs): 55 | return list(map (lambda a, b: a+12*b, chord, octs)) 56 | 57 | def t(chord, k): 58 | return list(map (lambda c: c+k, chord)) 59 | 60 | 61 | # Normalizations 62 | # Note: these are not in place 63 | 64 | def normO(chord): 65 | return list(map (lambda x: x % 12, chord)) 66 | 67 | def normT(chord): 68 | if chord == []: 69 | return [] 70 | else: 71 | return t(chord,-chord[0]) 72 | 73 | def normP(chord): return sorted(chord) 74 | 75 | def normC(chord): return list(set(chord)) 76 | 77 | def normOP(chord): return normP(normO(chord)) 78 | def normOC(chord): return normC(normO(chord)) 79 | def normOT(chord): return normO(normT(chord)) 80 | def normPT(chord): return normT(normP(chord)) 81 | def normPC(chord): return normC(normP(chord)) 82 | def normTC(chord): return normC(normT(chord)) 83 | def normOPC(chord): return normC(normOP(chord)) 84 | 85 | 86 | # Equivalence relations based on normalizations 87 | 88 | def normToEqRel(n,a,b): return n(a) == n(b) 89 | 90 | def oEq(c1,c2): return normToEqRel(normO,c1,c2) 91 | def pEq(c1,c2): return normToEqRel(normP,c1,c2) 92 | def tEq(c1,c2): return normToEqRel(normT,c1,c2) 93 | def cEq(c1,c2): return normToEqRel(normC,c1,c2) 94 | def opEq(c1,c2): return normToEqRel(normOP,c1,c2) 95 | def ocEq(c1,c2): return normToEqRel(normOC,c1,c2) 96 | def otEq(c1,c2): return normToEqRel(normOT,c1,c2) 97 | def ptEq(c1,c2): return normToEqRel(normPT,c1,c2) 98 | def pcEq(c1,c2): return normToEqRel(normPC,c1,c2) 99 | def tcEq(c1,c2): return normToEqRel(normTC,c1,c2) 100 | def opcEq(c1,c2): return normToEqRel(normOPC, c1, c2) 101 | 102 | # Equivalence relations requiring other algorithms 103 | 104 | # [TO-DO: OPT] 105 | # [TO-DO: OPTC] 106 | 107 | #===================================================== 108 | # QUOTIENT SPACE IMPLEMENTATION 109 | 110 | # Quotient space partition implementation mirroring the Haskell definition. 111 | # This has a worst case of O(n^2). 112 | def partitionOld(eqRel,items): 113 | if items==[]: 114 | return [] 115 | else: 116 | eqClass, otherItems = split(lambda x: eqRel(items[0],x), items) 117 | if otherItems==[]: 118 | return [eqClass] 119 | else: 120 | otherClasses = partition(eqRel, otherItems) 121 | return [eqClass]+otherClasses 122 | 123 | # A non-recursive approach to partitioning the quotient space 124 | def partition(eqRel, items): 125 | eqClasses = [] 126 | for x in items: 127 | if len(eqClasses) == 0: 128 | eqClasses.append([x]) 129 | #print(x, "added to its own class") 130 | else: 131 | i = 0 132 | n = len(eqClasses) 133 | while i=0: 134 | if eqRel(x, eqClasses[i][0]): 135 | eqClasses[i].append(x) 136 | #print(x, "added to equivalence class ", i) 137 | i = -1 # exit condition 1: less than zero, class found 138 | else: 139 | i += 1 # exi condition 2: exceed n (no class found) 140 | if i>=n: 141 | eqClasses.append([x]) 142 | #print (x, "added to its own class") 143 | return eqClasses 144 | 145 | def split(pred, items): 146 | predYes = [] 147 | predNo = [] 148 | for x in items: 149 | if pred(x): 150 | predYes.append(x) 151 | else: 152 | predNo.append(x) 153 | return predYes, predNo 154 | 155 | def eqClass(eqRel,qSpace,x): 156 | return next((y for y in qSpace if eqRel(x,y[0])),None) 157 | 158 | #===================================================== 159 | # RANDOMIZATION 160 | 161 | from random import seed, shuffle 162 | 163 | # The "randomize" function just becomes "shuffle" 164 | # Note: user has to choose in place or using a copy. 165 | # To return a list,x to its sorted order, use x.sort() 166 | 167 | def randomizeInPlace(items,rSeed): 168 | seed(rSeed) 169 | shuffle(items) 170 | 171 | # To maintain consistency with the Haskell version, 172 | # the main "randomize" definition copies the list rather 173 | # than sorting it in-place. 174 | 175 | def randomize(items,rSeed): 176 | newItems = list(items) 177 | randomizeInPlace(newItems, rSeed) 178 | return newItems 179 | 180 | 181 | 182 | #===================================================== 183 | # TESTING 184 | 185 | #x = [0,0,4,7] 186 | #y = [0,3,7] 187 | #z = [60,64,67] 188 | #def f(a,b): return a==b 189 | #def g(a): return a<2 190 | 191 | #q = partition(oEq, [x,y,z]) 192 | #foo = [12,16,19] 193 | 194 | 195 | #s = makeRange([(0,50),(0,50),(0,50),(0,50)]) 196 | 197 | #def opEqTo(aSet,x): 198 | # xs = filter(lambda y: opEq(x,y), aSet) 199 | # return list(xs) 200 | 201 | #import time 202 | 203 | #def benchTest(set): 204 | # t1 = time.time() 205 | # print(len(set)) 206 | # t2 = time.time() 207 | # # print(len(opEqTo(s,x))) 208 | # t3 = time.time(); 209 | # print (t2-t1) 210 | # print (t3-t2) 211 | 212 | #def bench2(): 213 | # t1 = time.time() 214 | # xs = searchRange((lambda w: opEq(w,[0,0,4,7])), [(0,30),(0,30),(0,30),(0,30)]) 215 | # print (xs) 216 | # t2 = time.time() 217 | # print (t2-t1) 218 | 219 | #bench2() 220 | -------------------------------------------------------------------------------- /Chunk.py: -------------------------------------------------------------------------------- 1 | from PythonEuterpea import * 2 | 3 | class Chunk: 4 | 5 | def __init__(self, kind, value): 6 | self.kind = kind 7 | self.value = value 8 | self.onset = 0 9 | self.end = 0 10 | self.dur = 0 11 | 12 | if (kind=="R"): # value is a tuple of onset and duration 13 | self.onset = value[0] 14 | self.dur = value[1] 15 | self.end = self.onset + self.dur 16 | elif (kind=="E"): # value is an MEvent 17 | self.onset = self.value.eTime 18 | self.dur = self.value.dur 19 | self.end = self.onset + self.dur 20 | elif (kind=="Seq"): # value is a list of back-to-back chunks sorted by onset 21 | if (len(self.value) > 0): 22 | self.onset = self.value[0].onset 23 | self.dur = sum (map (lambda x: x.dur, self.value)) 24 | self.end = self.value[-1].onset + self.value[-1].dur 25 | elif (kind=="Chord"): # value is a list of chunks with identical onsets and durs 26 | if (len(self.value) > 0): 27 | self.onset = self.value[0].onset 28 | self.dur = self.value[0].dur 29 | self.end = self.value[0].end 30 | elif (kind=="Par"): # value is a list of potentially overlapping chunks 31 | if (len(self.value) > 0): 32 | self.onset = min(map(lambda x:x.onset, self.value)) 33 | self.end = min(map (lambda x:x.end, self.value)) 34 | self.dur = self.end - self.onset 35 | else: 36 | raise Exception("Unrecognized Chunk kind: "+kind) 37 | 38 | def __str__(self): 39 | if (self.kind=="E"): 40 | return self.kind + " "+str(self.value.eTime)+" "+str(self.value.pitch)+" "+str(self.value.dur) 41 | else: 42 | return self.kind + " "+str(self.value) 43 | 44 | def __repr__(self): 45 | return str(self) 46 | 47 | def toMusic(self): 48 | if (self.kind=="R"): 49 | return Rest(self.value[1]) 50 | elif (self.kind=="E"): 51 | return Note(self.value.pitch, self.value.dur, self.value.vol) 52 | elif (self.kind=="Seq"): 53 | return line(map(lambda x: x.toMusic(), self.value)) 54 | elif (self.kind=="Chord"): 55 | return chord(map(lambda x: x.toMusic(), self.value)) 56 | elif (self.kind=="Par"): 57 | return chord(map(lambda x: line([Rest(x.onset), x.toMusic()]), self.value)) 58 | else: 59 | raise Exception("Unrecognized Chunk kind: "+self.kind) -------------------------------------------------------------------------------- /ClassicalFG.py: -------------------------------------------------------------------------------- 1 | import ChordSpaces 2 | import Search 3 | import Constraints 4 | import PTGG 5 | 6 | def pianoFilter(chords): # [[0, 1]] 7 | res = [] 8 | for chd in chords: 9 | if not len(set(chd)) == len(chd): 10 | continue 11 | new_chd = sorted(chd) 12 | if new_chd not in res: 13 | res.append(new_chd) 14 | return res 15 | 16 | 17 | 18 | def testPred (a, b): 19 | for idx in range(len(a)): 20 | if abs(a[idx] - b[idx]) > 7: 21 | return False 22 | return True 23 | 24 | def testPred2(a, b): 25 | return True 26 | 27 | def classicalCS2(tchords): 28 | allChords = pianoFilter(ChordSpaces.makeRange([(47, 67), (52, 76), (60, 81)])) 29 | #print("Total number of possible chords: ", len(allChords)) 30 | # print(allChords[:10]) 31 | qSpace = ChordSpaces.partition(ChordSpaces.opEq, allChords) 32 | # print(qSpace) 33 | chords = map(lambda x: x.absChord, tchords) 34 | # print(chords) 35 | newChords = Search.greedyProg(qSpace, ChordSpaces.opEq, testPred, Search.nearFall, chords) 36 | print(newChords) 37 | for i in range(len(tchords)): 38 | tchords[i].absChord = [] + newChords[i] 39 | 40 | def classicalCS2WithRange(tchords, voiceRange = [(47, 67), (52, 76), (60, 81)]): 41 | #allChords = pianoFilter(ChordSpaces.makeRange(voiceRange)) 42 | allChords = PTGG.filter(ChordSpaces.makeRange(voiceRange), Constraints.satbFilter) 43 | #print("Total number of possible chords: ", len(allChords)) 44 | # print(allChords[:10]) 45 | qSpace = ChordSpaces.partition(ChordSpaces.opcEq, allChords) 46 | print(qSpace) 47 | chords = map(lambda x: x.absChord, tchords) 48 | newChords = Search.greedyProg(qSpace, ChordSpaces.opcEq, testPred, Search.nearFall, chords) 49 | #print("New Chords: ", newChords) 50 | print(newChords) 51 | for i in range(len(tchords)): 52 | tchords[i].absChord = [] + newChords[i] 53 | 54 | -------------------------------------------------------------------------------- /Constraints.py: -------------------------------------------------------------------------------- 1 | import Search 2 | import math 3 | import sys 4 | from ChordSpaces import * 5 | 6 | def sorted(chord): 7 | return all(chord[i] <= chord[i + 1] for i in xrange(len(chord) - 1)) 8 | 9 | def spaced(lims, chord): 10 | cPairs = zip(chord[0:len(chord)-2], chord[1:]) 11 | def f(lim,pair): 12 | diff = abs(pair[1] - pair[0]) 13 | return (diff >= lim[0] and diff <=lim[1]) 14 | return all([f(a,b) for (a,b) in zip(lims,cPairs)]) 15 | 16 | # doubled :: [AbsChord] -> Predicate AbsChord 17 | # doubled templates x = elem (normOP x) allTriads where 18 | # allTriads = concatMap (\c -> map (normOP . t c) templates) [0..11] 19 | 20 | 21 | triads = [[0, 0, 4, 7], [0, 4, 7, 7], [0, 0, 3, 7], [0, 3, 7, 7], [0, 0, 3, 6]] 22 | 23 | def flatten(x): 24 | return [val for sublist in x for val in sublist] 25 | 26 | def doubled(templates, x): 27 | allTriads = flatten(map(lambda c: map(lambda v: normOP(t(v,c)), templates), range(0,12))) 28 | xn = normOP(x) 29 | return xn in allTriads 30 | 31 | 32 | # satbFilter x = and $ map ($x) [sorted, spaced satbLimits, doubled triads] 33 | satbLimits = [(3,12), (3,12), (3,12), (3,12)] 34 | def satbFilter(x): return sorted(x) and doubled(triads,x) and spaced(satbLimits,x) 35 | 36 | # check if parrallel exist 37 | def hNotPar1(chord1, chord2): 38 | if not len(chord1) == len(chord2): 39 | print("the length of chords are different!") 40 | return True #??? 41 | diff = [chord1[i2] - chord2[i2] for i2 in range(len(chord1))] 42 | return len(set(diff)) == len(diff) 43 | 44 | # check if rank is the same 45 | def hNotCross(chord1, chord2): 46 | if not len(chord1) == len(chord2): 47 | print("the length of chords are different!") 48 | return True #??? 49 | rank1 = sorted(range(len(chord1)), key=lambda k: chord1[k]) 50 | rank2 = sorted(range(len(chord2)), key=lambda k: chord2[k]) 51 | return rank1 == rank2 52 | 53 | def euclideanDist(chord1, chord2): 54 | if not len(chord1) == len(chord2): 55 | print("the length of chords are different!") 56 | return sys.maxint 57 | return math.sqrt(sum((a - b) ** 2 for a,b in zip(chord1, chord2))) 58 | 59 | def maxStepDist(chord1, chord2): 60 | if not len(chord1) == len(chord2): 61 | print("the length of chords are different!") 62 | return sys.maxint 63 | return max(abs(a - b) for a, b in zip(chord1, chord2)) 64 | # parameters 65 | # dist function, constraints, chord1, chrod2 66 | def distClass(distFun, predicate, chord1, chord2): 67 | # return predicate (distFun, chord1, chord2) 68 | pass 69 | 70 | # # ======= Test Case for hNotPar1 ====== 71 | # c1 = [60, 64, 67] 72 | # c2 = [61, 70, 68] 73 | # print("hNotPar1 should return False") 74 | # print(hNotPar1(c1, c2)) 75 | # 76 | # # ======= Test Case for hNotCross ====== 77 | # 78 | # c3 = [60, 64, 67] 79 | # c4 = [66, 65, 67] 80 | # print("hNotCross should return False") 81 | # print(hNotCross(c3, c4)) 82 | -------------------------------------------------------------------------------- /EuterpeaShorthands.py: -------------------------------------------------------------------------------- 1 | from PythonEuterpea import * 2 | 3 | # Capital letters had to be used here to avoid conflict with "as" to mean a-sharp 4 | 5 | def pcToNote(pcnum, oct, dur, vol=100): 6 | return Note(pcnum+(oct+1)*12, dur, vol) 7 | 8 | def Cf(oct, dur, vol=100): 9 | return pcToNote(-1, oct, dur, vol) 10 | 11 | def C(oct, dur, vol=100): 12 | return pcToNote(0, oct, dur, vol) 13 | 14 | def Cs(oct, dur, vol=100): 15 | return pcToNote(1, oct, dur, vol) 16 | 17 | def Df(oct, dur, vol=100): 18 | return pcToNote(1, oct, dur, vol) 19 | 20 | def D(oct, dur, vol=100): 21 | return pcToNote(2, oct, dur, vol) 22 | 23 | def Ds(oct, dur, vol=100): 24 | return pcToNote(3, oct, dur, vol) 25 | 26 | def Ef(oct, dur, vol=100): 27 | return pcToNote(3, oct, dur, vol) 28 | 29 | def E(oct, dur, vol=100): 30 | return pcToNote(4, oct, dur, vol) 31 | 32 | def F(oct, dur, vol=100): 33 | return pcToNote(5, oct, dur, vol) 34 | 35 | def Fs(oct, dur, vol=100): 36 | return pcToNote(6, oct, dur, vol) 37 | 38 | def Gf(oct, dur, vol=100): 39 | return pcToNote(6, oct, dur, vol) 40 | 41 | def G(oct, dur, vol=100): 42 | return pcToNote(7, oct, dur, vol) 43 | 44 | def Gs(oct, dur, vol=100): 45 | return pcToNote(8, oct, dur, vol) 46 | 47 | def Af(oct, dur, vol=100): 48 | return pcToNote(8, oct, dur, vol) 49 | 50 | def A(oct, dur, vol=100): 51 | return pcToNote(9, oct, dur, vol) 52 | 53 | def As(oct, dur, vol=100): 54 | return pcToNote(10, oct, dur, vol) 55 | 56 | def Bf(oct, dur, vol=100): 57 | return pcToNote(10, oct, dur, vol) 58 | 59 | def B(oct, dur, vol=100): 60 | return pcToNote(11, oct, dur, vol) 61 | 62 | def Bs(oct, dur, vol=100): 63 | return pcToNote(12, oct, dur, vol) 64 | 65 | -------------------------------------------------------------------------------- /ExampleGUI.py: -------------------------------------------------------------------------------- 1 | from Tkinter import Tk, Frame, Button, Label, Entry, Text, INSERT, IntVar, Radiobutton, RIGHT,W 2 | from Tkinter import BooleanVar, BOTH, Menu, Menubutton, RAISED, DISABLED, ACTIVE, OptionMenu, StringVar 3 | from Examples import genMusic 4 | import tkMessageBox as mbox 5 | import sys 6 | import midi 7 | 8 | 9 | class MusicGUI(Frame): 10 | 11 | def __init__(self, parent): 12 | Frame.__init__(self, parent) 13 | 14 | self.parent = parent 15 | self.centerWindow() 16 | self.initUI() 17 | 18 | def centerWindow(self): 19 | w = 500 20 | h = 500 21 | 22 | sw = self.parent.winfo_screenwidth() 23 | sh = self.parent.winfo_screenheight() 24 | 25 | x = (sw - w) / 2 26 | y = (sh - h) / 2 27 | self.parent.geometry('%dx%d+%d+%d' % (w, h, x, y)) 28 | 29 | def initUI(self): 30 | 31 | self.parent.title("Python-Kullita") 32 | 33 | self.pack(fill=BOTH, expand=True) 34 | # self.var = BooleanVar() 35 | L1 = Label(self, text="Midi File Name:") 36 | L1.place(x = 30, y = 450) 37 | self.fileEntry = Entry(self) 38 | self.fileEntry.place(x = 150, y = 450) 39 | genButton = Button(self, text="Genrate Music!", 40 | command=self.onClick) 41 | genButton.place(x=350, y=450) 42 | self.initLog() 43 | self.init3Voices() 44 | 45 | def init3Voices(self): 46 | # [(47, 67), (52, 76), (60, 81)]) 47 | # (B,2) - 20 48 | # (E,3) - 24 49 | # (C,4) - 25 50 | self.note_name = ['C','Cs','D','Ds','E','F','Fs','G','Gs','A','As','B'] 51 | self.pitchC1 = StringVar() 52 | self.pitchC2 = StringVar() 53 | self.pitchC3 = StringVar() 54 | self.octave1 = IntVar() 55 | self.octave2 = IntVar() 56 | self.octave3 = IntVar() 57 | self.range1 = IntVar() 58 | self.range2 = IntVar() 59 | self.range3 = IntVar() 60 | 61 | 62 | v1Label =Label(self, text="Voice 1 start:") 63 | v1Label.place(x = 30, y = 25) 64 | self.w1 = OptionMenu(self, self.pitchC1, *self.note_name) 65 | self.w1.place(x = 135, y = 25) 66 | self.pitchC1.set(self.note_name[11]) 67 | self.oc1 = OptionMenu(self, self.octave1, *range(1, 9)) 68 | self.oc1.place(x=200, y=25) 69 | self.octave1.set(2) 70 | v1_rangeLabel = Label(self, text="Range:") 71 | v1_rangeLabel.place(x=300, y=25) 72 | self.rangeoption1 = OptionMenu(self, self.range1, *range(18, 26)) 73 | self.rangeoption1.place(x=360, y=25) 74 | self.range1.set(20) 75 | 76 | 77 | v2Label = Label(self, text="Voice 2 start:") 78 | v2Label.place(x=30, y=50) 79 | self.w2 = OptionMenu(self, self.pitchC2, *self.note_name) 80 | self.w2.place(x=135, y=50) 81 | self.pitchC2.set(self.note_name[4]) 82 | self.oc2 = OptionMenu(self, self.octave2, *range(1, 9)) 83 | self.oc2.place(x=200, y=25 * 2) 84 | self.octave2.set(3) 85 | v2_rangeLabel = Label(self, text="Range:") 86 | v2_rangeLabel.place(x=300, y=25 * 2) 87 | self.rangeoption2 = OptionMenu(self, self.range2, *range(18, 26)) 88 | self.rangeoption2.place(x=360, y=25 * 2) 89 | self.range2.set(24) 90 | 91 | 92 | v3Label = Label(self, text="Voice 3 start:") 93 | v3Label.place(x=30, y=75) 94 | self.w3 = OptionMenu(self, self.pitchC3, *self.note_name) 95 | self.w3.place(x=135, y=75) 96 | self.pitchC3.set(self.note_name[0]) 97 | self.oc3 = OptionMenu(self, self.octave3, *range(1, 9)) 98 | self.oc3.place(x=200, y=25 * 3) 99 | self.octave3.set(4) 100 | v3_rangeLabel = Label(self, text="Range:") 101 | v3_rangeLabel.place(x=300, y=25 * 3) 102 | self.rangeoption3 = OptionMenu(self, self.range3, *range(18, 26)) 103 | self.rangeoption3.place(x=360, y=25 * 3) 104 | self.range3.set(25) 105 | 106 | # def pitchClick(self, voiceStr, key): 107 | # print("hello!") 108 | # vMap = {'1': self.pitchC1, '2': self.pitchC2, '3': self.pitchC3} 109 | # txt = voiceStr + ": " + self.note_name[vMap[key].get()] 110 | # txt += "\n" 111 | # self.logText.insert(INSERT, txt) 112 | def initLog(self): 113 | self.logText = Text(self, width = 61, height =21) 114 | self.logText.insert(INSERT, "Hello.....") 115 | self.logText.insert(INSERT, "Midi Log:\n") 116 | self.logText.place(x = 30, y = 110) 117 | self.logText.config(highlightbackground='blue') 118 | 119 | def onClick(self): 120 | self.filename = self.fileEntry.get() 121 | if self.filename == "": 122 | mbox.showerror("Error", "No File Name!") 123 | return 124 | temp = sys.stdout 125 | sys.stdout = open('log.txt', 'w') 126 | startPitch = [eval("midi." + self.pitchC1.get() + "_" + str(self.octave1.get())), 127 | eval("midi." + self.pitchC2.get() + "_" + str(self.octave2.get())), 128 | eval("midi." + self.pitchC3.get() + "_" + str(self.octave3.get())), 129 | ] 130 | print("Start Pitch Value:") 131 | print(startPitch) 132 | voiceRange = [(startPitch[0], startPitch[0] + self.range1.get()), 133 | (startPitch[1], startPitch[1] + self.range2.get()), 134 | (startPitch[2], startPitch[2] + self.range3.get()), 135 | ] 136 | print("3 Voices Pitch Range:") 137 | print(voiceRange) 138 | genMusic(self.filename, voiceRange) 139 | sys.stdout.close() 140 | sys.stdout = temp 141 | with open("log.txt", "r") as myfile: 142 | data = myfile.readlines() 143 | content = "" 144 | for line in data: 145 | content += line 146 | self.logText.insert(INSERT, content) 147 | mbox.showinfo("Music Generation", self.filename + ".midi File completed") 148 | 149 | 150 | def main(): 151 | 152 | root = Tk() 153 | # root.geometry("250x150+300+300") 154 | app = MusicGUI(root) 155 | root.mainloop() 156 | # print eval('midi.G_3') 157 | 158 | 159 | if __name__ == '__main__': 160 | main() -------------------------------------------------------------------------------- /Examples.py: -------------------------------------------------------------------------------- 1 | # Python Kulitta examples 2 | # Authors: Wen Sheng and Donya Quick 3 | 4 | from MusicGrammars import * 5 | from PTGG import * 6 | import ClassicalFG 7 | import PostProc 8 | import MidiFuns 9 | 10 | r1 = (0.3, (CType.I, lambda p: [v(h(p)), i(h(p))])) 11 | r2 = (0.6, (CType.I, lambda p:[i(h(p)), i(h(p))])) 12 | r3 = (0.1, (CType.I, lambda p:[i(p)])) 13 | r4 = (0.5, (CType.V, lambda p:[iv(h(p)), v(h(p))])) 14 | r5 = (0.4, (CType.V, lambda p:[v(h(p)), v(h(p))])) 15 | r6 = (0.1, (CType.V, lambda p:[v(p)])) 16 | r7 = (0.8, (CType.IV, lambda p:[iv(h(p)), iv(h(p))])) 17 | r8 = (0.2, (CType.IV, lambda p:[iv(p)])) 18 | rs = [r1, r2, r3, r4, r5, r6, r7, r8] 19 | 20 | rs2 = [(1.0, (CType.I, lambda p: [i(h(p)), i(h(p))]))] 21 | 22 | def smallerThanQN(dur): 23 | return dur < 0.5 24 | 25 | def mapRules(fnDur, rs): 26 | res = [] 27 | for r in rs: 28 | # res.append((r[0], toRelDur(fnDur, r[1]))) 29 | res.append((r[0], fnDur, r[1])) 30 | return res 31 | rules = mapRules(smallerThanQN, rs) 32 | 33 | 34 | def genMusic(filename = "kullita-test", voiceRange = [(47, 67), (52, 76), (60, 81)], printSteps=False): 35 | startSym = [i(MP(4 * Dur.WN, Mode.MAJOR, 0, 0, 4 * Dur.WN))] 36 | testMP = MP(16 * Dur.WN, Mode.MAJOR, 0, 0, 0) 37 | h(testMP) 38 | absStruct = gen(normalize(rs), startSym, 4) 39 | if printSteps: print "absStruct: ", absStruct 40 | chords = PostProc.toAbsChords(absStruct) 41 | if printSteps: print "TChords: ", chords 42 | ClassicalFG.classicalCS2WithRange(chords, voiceRange) 43 | if printSteps: print "New TChords: ", chords 44 | MidiFuns.tChordsToMidi(chords, filename) 45 | if printSteps: print "Done." 46 | 47 | genMusic("test", [(47, 67), (52, 72), (60, 80), (65,83)], True) 48 | -------------------------------------------------------------------------------- /FromMidi2.py: -------------------------------------------------------------------------------- 1 | from Chunk import * 2 | from PythonEuterpea import * 3 | from GMInstruments import * 4 | from copy import deepcopy 5 | from MidiReader import * 6 | 7 | 8 | def extractChord(startChunk, chunks): 9 | ''' 10 | Locate well-formed chords of Chunks with the same onsets and durations 11 | :param startChunk: Chunk to compare against 12 | :param chunks: All the other chunks to check 13 | :return: A chord, if one is found (otherwise startChunk is returned), and leftover chunks 14 | ''' 15 | cChunks = [startChunk] 16 | for c in chunks: 17 | if c.onset == startChunk.onset and c.dur == startChunk.dur: 18 | cChunks.append(c) 19 | r = range(len(chunks)) 20 | r.reverse() 21 | for i in r: 22 | if chunks[i] in cChunks: 23 | chunks.remove(chunks[i]) 24 | if len(cChunks) > 1: 25 | return Chunk("Chord", cChunks), chunks 26 | else: 27 | return startChunk, chunks 28 | 29 | def extractMel(startChunk, chunks): 30 | ''' 31 | Locate sequential patterns of back-to-back chunks 32 | :param startChunk: The Chunk to compare against 33 | :param chunks: All Chunks to check. 34 | :return: A sequential pattern, if found (otherwise startChunk is returned), and any leftover chunks 35 | ''' 36 | mChunks = [startChunk] 37 | for c in chunks: 38 | if c.onset == mChunks[-1].end: 39 | mChunks.append(c) 40 | r = range(len(chunks)) 41 | r.reverse() 42 | for i in r: 43 | if chunks[i] in mChunks: 44 | chunks.remove(chunks[i]) 45 | if len(mChunks)>1: 46 | return Chunk("Seq", mChunks), chunks 47 | else: 48 | return startChunk, chunks 49 | 50 | def extractSeqs(startChunk, chunks): 51 | ''' 52 | Find sequential patterns that may include gaps 53 | :param startChunk: First Chunk to start comparing against 54 | :param chunks: All other Chunks to check 55 | :return: A new sequence, if found (otherwise startChunk is returned), and then any leftover chunks 56 | ''' 57 | sChunks = [startChunk] 58 | inds = [] 59 | for i in range(len(chunks)): 60 | c = chunks[i] 61 | if c.onset == sChunks[-1].end: 62 | sChunks.append(c) 63 | inds.append(i) 64 | elif c.onset > sChunks[-1].end: 65 | sChunks.append(Chunk("R", (sChunks[-1].end, c.onset - sChunks[-1].end))) 66 | sChunks.append(c) 67 | inds.append(i) 68 | print c 69 | inds.reverse() 70 | for i in inds: 71 | chunks.remove(chunks[i]) 72 | if len(sChunks)>1: 73 | return Chunk("Seq", sChunks), chunks 74 | else: 75 | return startChunk, chunks 76 | 77 | def chunkByFun(chunks, f): 78 | ''' 79 | Apply a chunk-merging function, f, left to right over a list of input chunks 80 | :param chunks: 81 | :param f: 82 | :return: Reformatted Chunks 83 | ''' 84 | fChunks = list() 85 | while len(chunks) > 0: 86 | c = chunks.pop(0) 87 | cChunks, chunks = f(c, chunks) 88 | fChunks.append(cChunks) 89 | return fChunks 90 | 91 | def chunkByChords(chunks0): 92 | return chunkByFun(chunks0, extractChord) 93 | 94 | def chunkByMel(chunks0): 95 | return chunkByFun(chunks0, extractMel) 96 | 97 | def seqWithRests(chunks0): 98 | return chunkByFun(chunks0, extractSeqs) 99 | 100 | def initChunk(evs): 101 | return map(lambda e: Chunk("E", e), evs) 102 | 103 | def chunkEvents(evs): 104 | ''' 105 | Given a list of MEvents, apply the algorithms above to turn it into a single Par Chunk. 106 | :param evs0: 107 | :return: 108 | ''' 109 | init = initChunk(evs) 110 | chords = chunkByChords(init) 111 | mels = chunkByMel(chords) 112 | seqs = seqWithRests(mels) # This step is still broken 113 | return Chunk("Par", seqs) 114 | 115 | def restructure(inMusic): 116 | ''' 117 | Restructure a PythonEuterpea music value using the algorithms here. 118 | :param inMusic: 119 | :return: A new music structure 120 | ''' 121 | evs = musicToMEvents(inMusic) 122 | evsP = splitByPatch(evs) 123 | parts = [] 124 | for e in evsP: 125 | p = checkPatch(e) 126 | chunk = chunkEvents(e) 127 | em = removeZeros(chunk.toMusic()) 128 | if (p[0]>=0): 129 | em = Modify(Instrument(p[0], p[1]), em) 130 | parts.append(em) 131 | return Music(chord(parts), 120) 132 | 133 | def midiToMusic2(filename): 134 | m = midiToMusic(filename) 135 | return restructure(m) -------------------------------------------------------------------------------- /KulittaTests.py: -------------------------------------------------------------------------------- 1 | # Some Simple Kulitta Tests 2 | # Wen Sheng 3 | # Last modified: 13-April-2016 4 | # =================================== 5 | import Search 6 | import Constraints 7 | 8 | # test pitch case a, b is same 9 | def testEq (a, b): 10 | return a % 12 == b % 12 11 | testSpace = [[i2, i2 + 12] for i2 in range(12)] 12 | testMel = [0, 4, 7, 2] 13 | testMelBuckets = [testSpace[i2] for i2 in testMel] 14 | 15 | # print testSpace 16 | # print testMel 17 | # print testMelBuckets 18 | 19 | # ============Testing allSolns and versions of pairProg============ 20 | 21 | 22 | def testPred (a, b): 23 | return abs(a - b) <= 7 24 | 25 | print "test all solns" 26 | print "expected:" 27 | # expetedAllSolns = [[0,4,7,2],[0,4,7,14],[0,4,19,2],[0,4,19,14],[0,16,7,2],[0,16,7,14],[0,16,19,2],[0,16,19,14],[12,4,7,2],[12,4,7,14],[12,4,19,2],[12,4,19,14],[12,16,7,2],[12,16,7,14],[12,16,19,2],[12,16,19,14]] 28 | print Search.allSols(testSpace, testEq, testMel) 29 | print Search.pairProg(testSpace, testEq, testPred, testMel) 30 | 31 | 32 | # ============Testing greedyProg and fallBack ============ 33 | 34 | # def testFallBack(bucket, g, x): 35 | # if bucket is None or len(bucket) == 0: 36 | # raise ("error!", "empty") 37 | # else: 38 | # return (g, bucket[0]) 39 | print "greedyProg" 40 | print Search.greedyProg(testSpace, testEq, testPred, Search.nearFall, testMel) -------------------------------------------------------------------------------- /MidiFuns.py: -------------------------------------------------------------------------------- 1 | # Score-level representations and MIDI conversion for some Kulita features 2 | # Code by Wen Sheng and Donya Quick 3 | # 4 | # This file requires the pythonmidi library: 5 | # https://github.com/vishnubob/python-midi 6 | # 7 | # Notable changes from previous versions: 8 | # 9 | # - Order of arguments to TNote and TChord constructors changed. The new order 10 | # works better with Python's ability to set default argument values. 11 | # 12 | # - Voice class removed (old code that wasn't being used). A voice is now just 13 | # a list of TNotes. 14 | # 15 | # - More values are now stored in TNote and TChord (for Haskell compat.) 16 | # 17 | # - More interface options added for working with lists of pitch numbers. 18 | # 19 | # --------------------------------------- 20 | # 21 | # On the to-do list: 22 | # - Fix bug in converting from TChords to voices 23 | # - Introduce handling for rests as pitch -1 24 | # - Handle onset information in an event list format. 25 | # 26 | # --------------------------------------- 27 | # 28 | # Important terms: 29 | # 30 | # - "Absolute pitch" or abspitch/AbsPitch is a pitch number. The Euterpea 31 | # library used by the Haskell version of Kulitta uses Euterpea's 32 | # AbsPitch type, and we use the term here for consistency. 33 | # 34 | # - "Absolute chord" is just a chord consisting of abspitches. Chords are 35 | # vectors mathematically and implemented as lists in Python (and Haskell). 36 | # 37 | # - A "voice" is essentially a melody: a list of notes where each note starts 38 | # as soon as the previous one ends. Rests are currently not permitted. 39 | # 40 | # --------------------------------------- 41 | # 42 | # Important constants and bounds: 43 | # 44 | # - Pitches must be within the range 0-127, where pitch number 0 is C_0 in 45 | # the python-midi library. All pitches are integers. 46 | # 47 | # - The smallest time increment in MIDI is the tick. A lot of software uses 96 48 | # ticks per beat, so that is used here by default. However, some software 49 | # uses a lot more. For example, Cakewalk Sonar uses 960. 50 | # 51 | # - Durations in Kulitta are in terms of whole notes in 4/4. A duration of 1.0 52 | # indicates a whole note, which at 120bpm lasts 2 seconds. 53 | # 54 | # ============================================================================= 55 | 56 | import midi 57 | import itertools 58 | 59 | # Tick resolution constant 60 | RESOLUTION = 96 61 | 62 | 63 | # Mode definition borrowed from PythonKulitta's MusicGrammars.py 64 | # It defines constants for reference in the form Mode.MAJOR, etc. 65 | class Mode: 66 | MAJOR = "Major" 67 | MINOR = "Minor" 68 | MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] 69 | MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10] 70 | 71 | # A Key holds a root (absolute pitch) and a mode. 72 | class Key: 73 | def __init__(self, absPitch, mode_name): 74 | self.absPitch = absPitch 75 | self.mode = mode_name 76 | 77 | DEFAULT_KEY = Key(0, Mode.MAJOR) 78 | 79 | 80 | # A TNote is a "temporal note" with two pieces of information: 81 | # a pitch number (abspitch) and a duration. 82 | class TNote: 83 | def __init__(self, absPitch, dur=1.0, vol=100, key=DEFAULT_KEY, onset=0): 84 | # deep copy of key 85 | self.key = Key(key.absPitch, key.mode) 86 | self.dur = dur 87 | self.absPitch = absPitch 88 | self.vol = vol 89 | self.onset = onset 90 | def __str__(self): 91 | return "("+str(self.absPitch)+","+str(self.dur)+")" 92 | def __repr__(self): 93 | return str(self) 94 | 95 | # A TChord is a "temporal chord." All of the notes have the same 96 | # duration and are assumed to play simultaneously. 97 | class TChord: 98 | def __init__(self, absChord, dur=1.0, vol=100, key=DEFAULT_KEY, onset=0): 99 | # deep copy of abschord, key 100 | self.key = Key(key.absPitch, key.mode) 101 | self.dur = dur 102 | self.absChord = [] + absChord 103 | self.onset = onset 104 | self.vol = vol 105 | def __str__(self): 106 | return "("+str(self.absChord)+","+str(self.dur)+")" 107 | def __repr__(self): 108 | return str(self) 109 | 110 | # A Voice is a list of TNotes - basically a melody. Each note is 111 | # assumed to start immediately after the previous note ends. 112 | #class Voice: 113 | # def __init__(self, tnotes): 114 | # # deep copy 115 | # self.tNotes = [TNote(tnote.absPitch, tnote.dur, tnote.vol, tnote.key, tnote.onset) for tnote in tnotes] 116 | 117 | #Transforms over TNotes for compatibility with Haskell implementation 118 | 119 | def tnK(tNote): # fetch the key information 120 | return tNote.key 121 | 122 | def tnD(tNote): # fetch the duration 123 | return tNote.dur 124 | 125 | def tnP(tNote): # fetch the pitch 126 | return tNote.absPitch 127 | 128 | def newP (tNote, p1): # change just the pitch 129 | tNote.absPitch = p1 130 | 131 | def trans_p(key, p1): 132 | key.absPitch = (key.absPitch + p1) % 12 133 | 134 | 135 | # Converting TChords to a MIDI file 136 | def tChordsToMidi(tChords, filename): 137 | # Instantiate a MIDI Pattern (contains a list of tracks) 138 | pattern = midi.Pattern() 139 | # Set the tick per beat resolution 140 | pattern.resolution = RESOLUTION 141 | # Instantiate a MIDI Track (contains a list of MIDI events) 142 | track = midi.Track() 143 | # Append the track to the pattern 144 | pattern.append(track) 145 | # Iterate through each chord and write it to a track 146 | for tChord in tChords: 147 | writeChord(tChord, track) 148 | eot = midi.EndOfTrackEvent(tick=1) 149 | track.append(eot) 150 | # Save the pattern to disk 151 | midi.write_midifile(filename + '.mid', pattern) 152 | 153 | def voiceToMidi(voice, filename): 154 | # Instantiate a MIDI Pattern (contains a list of tracks) 155 | pattern = midi.Pattern() 156 | # Set the tick per beat resolution 157 | pattern.resolution = RESOLUTION 158 | # Instantiate a MIDI Track (contains a list of MIDI events) 159 | track = midi.Track() 160 | #track.append(midi.SetTempoEvent()) 161 | # Append the track to the pattern 162 | pattern.append(track) 163 | # Iterate through each chord and write it to a track 164 | for tnote in voice: 165 | writeNote(tnote, track) 166 | eot = midi.EndOfTrackEvent(tick=1) 167 | track.append(eot) 168 | # Save the pattern to disk 169 | midi.write_midifile(filename + '.mid', pattern) 170 | 171 | # toVoices converts chords to voices. Mathematically, this is essentially 172 | # just a matrix transposition of the pitches. Durations for each chord are 173 | # copied into each of their resulting TNotes. The resulting music will 174 | # sound exactly the same. 175 | def toVoices(tChords): 176 | tNoteChords = map(lambda tc: toTNotes(tc), tChords) 177 | return map(list, zip(*tNoteChords)) 178 | 179 | # toTNotes converts a single chord to a list of TNotes. 180 | def toTNotes(tChord): 181 | notes = map (lambda p: TNote(p, tChord.dur, tChord.vol, tChord.key, tChord.onset), tChord.absChord) 182 | return notes 183 | 184 | # toPitch converts a pitch number into a tuple represetation of 185 | # a pitch class and an octave. 186 | def toPitch(absPitch): 187 | note_idx = absPitch % 12 188 | octave_idx = absPitch / 12 189 | return eval("midi.%s_%d" % (midi.NOTE_NAMES[note_idx], octave_idx)) 190 | 191 | # Auxilliary function to add a chord to a MIDI track 192 | def writeChord(tChord, track): 193 | if len(tChord.absChord) == 0: 194 | return 195 | absChd = tChord.absChord 196 | dur = tChord.dur 197 | tickDur = toMidiTick(dur) 198 | for a in absChd: 199 | track.append(midi.NoteOnEvent(tick=0, velocity=tChord.vol, pitch=toMidiPitch(a))) 200 | track.append(midi.NoteOffEvent(tick = tickDur, pitch = toMidiPitch(absChd[0]))) 201 | for a in absChd[1:]: 202 | track.append(midi.NoteOffEvent(tick=0, pitch=toMidiPitch(a))) # Bug fix 26-July-2016 203 | 204 | def writeNote(tNote, track): 205 | track.append(midi.NoteOnEvent(tick=0, velocity=tNote.vol, pitch=toMidiPitch(tNote.absPitch))) 206 | track.append(midi.NoteOffEvent(tick=toMidiTick(tNote.dur), pitch=toMidiPitch(tNote.absPitch))) 207 | 208 | # Conversion from Kulitta's durations to MIDI ticks 209 | def toMidiTick(dur): 210 | ticks = int(round(dur * RESOLUTION * 4)) # bug fix 26-June-2016 211 | return ticks 212 | 213 | # Check whether a pitch is in the appropriate range 214 | def toMidiPitch(pitchVal): 215 | # Euterpea start from 10 Cff 216 | if pitchVal < 0: 217 | raise Exception("Negative pitch value not supported: "+str(pitchVal)) 218 | elif pitchVal > 127: 219 | raise Exception("Pitch value too large: "+str(pitchVal)+" (must be 0-127)") 220 | # 0 - 127 221 | # 0 = C_0 222 | return pitchVal 223 | 224 | # pitchesToMidi writes a list of pitch numbers (0-127 each) to a file with 225 | # default durations and volumes. 226 | def pitchesToMidi(ps, filename, defaultDur=0.25, defaultVol=100): 227 | v = [] 228 | for p in ps: 229 | v.append(TNote(p, defaultDur)) 230 | voiceToMidi(v, filename) 231 | 232 | # This version uses two lists: one for pitches and one for durations. 233 | def pitchesDursToMidi1(ps, durs, filename, defaultVol=100): 234 | v = [] 235 | for p,d in itertools.izip(ps,durs): 236 | v.append(TNote(p, d)) 237 | voiceToMidi(v, filename) 238 | 239 | # This version uses a list of pairs of the format: [(p1, d1), (p2, d2), ...] 240 | def pdPairsToMidi(pds, filename, defaultVol=100): 241 | v = [] 242 | for p,d in pds: 243 | v.append(TNote(p, d)) 244 | voiceToMidi(v, filename) 245 | 246 | 247 | # ============================================================================= 248 | # Test area 249 | 250 | #x = TChord([40,45,47], 0.25, 100, Key(0, Mode.MAJOR)) 251 | #print x 252 | #y = toTNotes(x) 253 | #print y 254 | 255 | #x = [TChord([40,45,47], 0.25, 100, Key(0, Mode.MAJOR)), TChord([60,63,67], 0.25, 70, Key(0, Mode.MAJOR))] 256 | #print x 257 | #y = toVoices(x) 258 | #print y 259 | 260 | 261 | #x = [TNote(60, 0.25, 100, Key(0, Mode.MAJOR)) , TNote(67, 0.25, 70, Key(0, Mode.MAJOR))] 262 | #voiceToMidi(x, "test") 263 | 264 | #ps = [60, 64, 67] 265 | #pitchesToMidi(ps, "test2") 266 | 267 | #ds = [0.25, 0.25, 0.5] 268 | #pitchesDursToMidi1(ps, ds, "test3") 269 | 270 | #pds = [(60, 0.25), (64, 0.25), (67, 0.5)] 271 | #pdPairsToMidi(pds, "test4") -------------------------------------------------------------------------------- /MidiReader.py: -------------------------------------------------------------------------------- 1 | ''' 2 | MIDI to PythonEuterpea Conversion 3 | Donya Quick 4 | 5 | Assumptions: 6 | - No mixed-channel tracks (but there can be more than one track with the same channel) 7 | - Ticks per beat = 96 8 | 9 | Output format: 10 | - Outermost Par(s) are by instrument 11 | - Within an instrument's subtree, everything is still parallelized - each note is 12 | simply offset by a rest representing its onset time. 13 | - All "expression" events like aftertouch, modulation, etc. are ignored. 14 | 15 | Things not yet supported: 16 | - Tempo change events (currently everything is 120bpm with on changes) 17 | - ProgramChange events ocurring in the middle of a track. Currently assuming 18 | one track per instrument per channel. 19 | ''' 20 | 21 | from midi import * 22 | from PythonEuterpea import * 23 | from MusicPlayer import * 24 | 25 | def findNoteDuration(pitch, events): 26 | ''' 27 | Scan through a list of MIDI events looking for a matching note-off. 28 | A note-on of the same pitch will also count to end the current note, 29 | assuming an instrument can't play the same note twice simultaneously. 30 | If no note-off is found, the end of the track is used to truncate 31 | the current note. 32 | :param pitch: 33 | :param events: 34 | :return: 35 | ''' 36 | sumTicks = 0 37 | for e in events: 38 | sumTicks = sumTicks + e.tick 39 | c = e.__class__.__name__ 40 | if c == "NoteOffEvent" or c == "NoteOnEvent": 41 | if e.data[0] == pitch: 42 | return sumTicks 43 | return sumTicks 44 | 45 | 46 | def toPatch(channel, patch): 47 | ''' 48 | Convert MIDI patch info to PythonEuterpea's version. 49 | :param channel: if this is 9, it's the drum track 50 | :param patch: 51 | :return: a tuple of an int and boolean, (patch, isDrums) 52 | ''' 53 | if channel==9: 54 | return (patch, PERC) 55 | else: 56 | return (patch, INST) 57 | 58 | 59 | def tickToDur(ticks): 60 | ''' 61 | Convert from pythonmidi ticks back to PythonEuterpea durations 62 | :param ticks: 63 | :return: 64 | ''' 65 | return float(ticks) / float((RESOLUTION * 4)) 66 | 67 | def getChannel(track): 68 | ''' 69 | Determine the channel assigned to a track. 70 | ASSUMPTION: all events in the track should have the same channel. 71 | :param track: 72 | :return: 73 | ''' 74 | if len(track) > 0: 75 | e = track[0] 76 | if (e.__class__.__name__ == "EndOfTrackEvent"): 77 | return -1 78 | return track[0].channel 79 | return -1 80 | 81 | def trackToMEvents(track): 82 | ''' 83 | Turn a pythonmidi track (list of events) into MEvents 84 | :param track: 85 | :return: 86 | ''' 87 | currTicks = 0 88 | currPatch = -1 89 | channel = getChannel(track); 90 | mevs = [] 91 | for i in range(0,len(track)): 92 | e = track[i] 93 | c = e.__class__.__name__ 94 | currTicks = currTicks + e.tick # add time after last event 95 | if e.__class__.__name__ == "ProgramChangeEvent": 96 | print "INSTRUMENT", e 97 | # assign new instrument 98 | currPatch = e.data[0] 99 | elif c == "NoteOnEvent": 100 | # 1. find matching noteOn event OR another note on with the same pitch & channel 101 | noteDur = findNoteDuration(e.data[0], track[(i+1):]) 102 | # 2. create an MEvent for the note 103 | #print "Note on ", currTicks, e.data, noteDur 104 | n = MEvent(tickToDur(currTicks), e.data[0], tickToDur(noteDur), e.data[1], patch=toPatch(channel, currPatch)) 105 | mevs.append(n) 106 | elif c == "SetTempoEvent": 107 | print "Tempo change ignored (not supported yet): ", e 108 | elif c == "EndOfTrackEvent" or c == "NoteOffEvent": 109 | pass # nothing to do here 110 | else: 111 | print "Unsupported event type (ignored): ", e 112 | return mevs 113 | 114 | def checkPatch(mevs): 115 | ''' 116 | Determines the PythonEuterpea patch for a collection of MEvents. 117 | :param mevs: 118 | :return: 119 | ''' 120 | retVal = (-1,INST) 121 | if len(mevs) > 0: 122 | retVal = mevs[0].patch 123 | return retVal 124 | 125 | def mEventsToMusic(mevs): 126 | ''' 127 | Convert a list of MEvents to a Music value 128 | :param mevs: 129 | :return: 130 | ''' 131 | mVals = [] 132 | patch = checkPatch(mevs) # NEED TO UPDATE THIS 133 | for e in mevs: 134 | n = Note(e.pitch, e.dur, e.vol) 135 | r = Rest(e.eTime) 136 | mVals.append(line([r,n])) 137 | mTotal = chord(mVals) 138 | if (patch[0] >= 0): 139 | i = Instrument(patch[0], patch[1]) 140 | print "Instrument: ", i 141 | mTotal = Modify(i, mTotal) 142 | return mTotal 143 | 144 | def midiToMusic(filename): 145 | ''' 146 | Read a MIDI file and convert it to a Music structure. 147 | :param filename: 148 | :return: 149 | ''' 150 | pattern = read_midifile(filename) # a list of tracks 151 | mVals = [] 152 | for t in pattern: 153 | evs = trackToMEvents(t) 154 | if len(evs) > 0: 155 | mVals.append(mEventsToMusic(evs)) 156 | return Music(chord(mVals), 120) 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /MusicGrammars.py: -------------------------------------------------------------------------------- 1 | # Python port of MusicGrammars.lhs 2 | # Authors: Wen Sheng and Donya Quick 3 | 4 | from fractions import Fraction 5 | from PTGG import NT 6 | from copy import deepcopy 7 | 8 | class Mode: 9 | MAJOR = "Major" 10 | MINOR = "Minor" 11 | MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] 12 | MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10] 13 | 14 | 15 | class CType: 16 | I, II, III, IV, V, VI, VII = range(7) 17 | ALL_CHORD = [I, II, III, IV, V, VI, VII] 18 | 19 | 20 | 21 | 22 | 23 | class Dur(object): 24 | WN = 1.0 25 | HN = 0.5 26 | QN = 0.25 27 | EN = 0.125 28 | SN = 0.0625 29 | TN = 0.03125 30 | dur_dict = {"WN": WN, 31 | "HN": HN, 32 | "QN": QN, 33 | "EN": EN, 34 | "SN": SN, 35 | "TN": TN, 36 | WN:"WN", HN:"HN", QN: "QN", EN: "EN", SN: "SN", TN: "TN" 37 | } 38 | 39 | 40 | 41 | class MP: 42 | def __init__(self, dur=Dur.WN, mode=Mode.MAJOR, key=0, onset=0, sDur=Dur.WN): 43 | self.dur = dur # float 44 | self.mode = mode # str 45 | self.sDur = sDur 46 | # later defined 47 | self.key = key 48 | self.onset = onset 49 | # print("dur:", self.dur) 50 | def __str__(self): 51 | myStr = "("+str(self.dur)+")" 52 | return myStr 53 | def __repr__(self): 54 | return str(self) 55 | 56 | 57 | def isMaj(mode): 58 | return mode == Mode.MAJOR 59 | 60 | 61 | def isMin(mode): 62 | return mode == Mode.MINOR 63 | 64 | 65 | 66 | def dFac(x, mp): 67 | mp2 = deepcopy(mp) 68 | mp2.dur = mp2.dur * x 69 | return mp2 70 | 71 | def getScale(mode): 72 | if mode == Mode.MINOR: 73 | return Mode.MINOR_SCALE 74 | elif mode == Mode.MAJOR: 75 | return Mode.MAJOR_SCALE 76 | else: 77 | return [] 78 | # i, ii, iii, iv, v, vi, vii = [(lambda p: NT((c, p)) for c in CType.ALL_CHORD)] 79 | i = lambda p: NT((CType.I, p)) 80 | ii = lambda p: NT((CType.II, p)) 81 | iii = lambda p: NT((CType.III, p)) 82 | iv = lambda p: NT((CType.IV, p)) 83 | v = lambda p: NT((CType.V, p)) 84 | vi = lambda p: NT((CType.VI, p)) 85 | vii = lambda p: NT((CType.VII, p)) 86 | 87 | 88 | def h(p): 89 | return dFac(0.5, p) 90 | 91 | # # test 92 | # p = MP() 93 | # print(h(p).dur) 94 | 95 | 96 | def toRelDur(fnDur, rule): # rule is a tuple like (CType.I, rfun) 97 | left, right = rule 98 | newRight = lambda p:[NT((left, p))] if fnDur(p.dur) else right(p) 99 | return (left, newRight) 100 | 101 | def toRelDur2(fnDur, rule): 102 | # left, right = rule 103 | # newRight = 0 104 | # return(left,newRight) 105 | pass 106 | 107 | -------------------------------------------------------------------------------- /MusicPlayer.py: -------------------------------------------------------------------------------- 1 | import pygame.midi as pm 2 | import PythonEuterpea as pe 3 | import time 4 | 5 | class MusicPlayer: 6 | def __init__(self, output_port=0, init=True, startupDelay = 0.2): 7 | self.output_port = output_port 8 | self.buffer = list() 9 | self.model = None 10 | self.is_recording = False 11 | self.run_relay = True 12 | self.startupDelay = startupDelay # Needed to avoid messed up timing on first note played 13 | if (init): 14 | self.initialize() 15 | self.outDev = pm.Output(self.output_port) 16 | 17 | 18 | def initialize(self): 19 | pm.init() 20 | if self.startupDelay > 0: 21 | time.sleep(self.startupDelay) 22 | 23 | def close(self): 24 | self.outDev.close() 25 | 26 | def setOutputDevice(self, output_port): 27 | self.output_port = output_port 28 | self.outDev = pm.Output(output_port) 29 | 30 | 31 | def interactiveSetup(self): 32 | self.printDevices() 33 | 34 | # get an input ID from the console 35 | #self.input_port = pm.Input(int(input("Enter input device number: "))) 36 | 37 | # get an output ID from the console 38 | self.output_port = pm.Output(int(input("Enter output device number: "))) 39 | 40 | def printDevices(self): 41 | inputs, outputs = list(), list() 42 | bad_devices = list() # TODO: Dead code consider revising or adding items to catch 43 | for i in range(0, pm.get_count()): 44 | info = pm.get_device_info(i) 45 | if info[1] not in bad_devices: 46 | target = inputs if info[2] > 0 else outputs 47 | target.append((i, info[1])) 48 | 49 | # print list of input devices 50 | #print "Input devices: " 51 | #for i in range(len(inputs)): 52 | # print inputs[i] 53 | 54 | # print list of output devices 55 | print "Output devices: " 56 | for i in range(len(outputs)): 57 | print outputs[i] 58 | 59 | def playMusic(self, music): 60 | """ 61 | NOTE: this implementation will have a "time leak" - if the CPU gets 62 | delayed, that time is lost and everything is pushed back. Generally 63 | we don't want this, but it protects against "note avalanches" in the 64 | event the CPU gets horribly delayed (so it can be useful for testing). 65 | """ 66 | self.initialize() 67 | mEvs = pe.musicToMEvents(music) # convert to note event representation 68 | onOffs = pe.mEventsToOnOff(mEvs) # convert to on-off pairs 69 | pe.onOffToRelDur(onOffs) # convert durations to relative durs 70 | onsetToSeconds(onOffs) # convert Euterpea time to seconds 71 | patches = pe.eventPatchList(mEvs) # which Instruments are in use? 72 | pmap = pe.linearPatchMap(patches) # build a patch map 73 | for p in pmap: # for each patch that needs to be assigned... 74 | if p[0][0] > 0: 75 | self.outDev.set_instrument(p[0][0], p[1]) # set instrument for channel (instID, chan) 76 | for e in onOffs: # for each on/off event... 77 | time.sleep(e.eTime) # wait until the event should occur 78 | chanID = findChannel(pmap, e.patch) # which channel are we on? 79 | if e.eType == pe.ON: # turn a note on? 80 | self.outDev.note_on(e.pitch, e.vol, chanID) # need to update channel 81 | elif e.eType == pe.OFF: # turn a note off? 82 | self.outDev.note_off(e.pitch, e.vol, chanID) # need to update channel 83 | 84 | 85 | def playMusicS(self, music): 86 | """ 87 | This version of the playback function tries to correct for lost time 88 | by shifting future events forward in time if the current one gets delayed. 89 | """ 90 | mEvs = pe.musicToMEvents(music) # convert to note event representation 91 | onOffs = pe.mEventsToOnOff(mEvs) # convert to on-off pairs 92 | pe.onOffToRelDur(onOffs) # convert durations to relative durs 93 | onsetToSeconds(onOffs) # convert Euterpea time to seconds 94 | patches = pe.eventPatchList(mEvs) # which Instruments are in use? 95 | pmap = pe.linearPatchMap(patches) # build a patch map 96 | for p in pmap: # for each patch that needs to be assigned... 97 | if p[0][0] > 0: 98 | self.outDev.set_instrument(p[0][0], p[1]) # set instrument for channel (instID, chan) 99 | t = time.time() # get the current time 100 | tDelta = 0 101 | for e in onOffs: # for each on/off event... 102 | # debugging: print 'time, delta:', e.eTime, tDelta, e.eTime - tDelta 103 | time.sleep(max(0, e.eTime - tDelta)) # wait until the event should occur, correcting for lateness 104 | chanID = findChannel(pmap, e.patch) # which channel are we on? 105 | if e.eType == pe.ON: # turn a note on? 106 | self.outDev.note_on(e.pitch, e.vol, chanID) # need to update channel 107 | elif e.eType == pe.OFF: # turn a note off? 108 | self.outDev.note_off(e.pitch, e.vol, chanID) # need to update channel 109 | tActual = time.time() - t # how long did we actually sleep? 110 | tDelta = tActual - e.eTime # max(0, tActual - e.eTime) # if we slept too long, speed up the next event 111 | t = time.time() # update our last timestamp 112 | 113 | 114 | 115 | def onsetToSeconds(events): 116 | for e in events: 117 | e.eTime = e.eTime * 2 # 1 measure in Euterpea is 2 seconds at 120bpm 118 | 119 | 120 | def findChannel(pmap, patch): 121 | i = 0 122 | while i < len(pmap): 123 | if patch == pmap[i][0]: 124 | return pmap[i][1] 125 | i += 1 126 | return -1 -------------------------------------------------------------------------------- /PEConstants.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # DURATION CONSTANTS 3 | # ================================================================= 4 | 5 | WN = 1.0 # whole note = one measure in 4/4 6 | DHN = 0.75 # dotted half 7 | HN = 0.5 # half note 8 | DQN = 0.375 # dotted quarter 9 | QN = 0.25 # quarter note 10 | DEN = 0.1875 # dotted eighth 11 | EN = 0.125 # eighth note 12 | DSN = 0.09375 # dotted sixteenth 13 | SN = 0.0625 # sixteenth note 14 | DTN = 0.046875 # dotted thirtysecond 15 | TN = 0.03125 # thirtysecond note 16 | 17 | 18 | # ================================================================= 19 | # List of GM instrument names in order of patch number. 20 | # ================================================================= 21 | 22 | gmNames = [ 23 | "ACOUSTIC_GRAND_PIANO", "BRIGHT_PIANO", "ELECTRIC_PIANO", 24 | "HONKY_TONK_PIAO", "RHODES_PIANO", "CHORUSED_PIANO", 25 | "HARPSICHORD", "CLAVINET", "CELESTA", 26 | "GLOCKENSPIEL", "MUSIC_BOX", "VIBRAPHONE", 27 | "MARIMBA", "XYLOPHONE", "TUBULAR_BELLS", 28 | "DULCIMER", "HAMMOND_ORGAN", "PERCUSSIVE_ORGAN", 29 | "ROCK_ORGAN", "CHURCH_ORGAN", "REED_ORGAN", 30 | "ACCORDION", "HARMONICA", "TANGO_ACCORDION", 31 | "ACOUSTIC_NYLON_GUITAR", "ACOUSTIC_STEEL_GUITAR", "ELECTRIC_JAZZ_GUITAR", 32 | "ELECTRIC_CLEAN_GUITAR", "ELECTRIC_MUTED_GUITAR", "OVERDRIVE_GUITAR", 33 | "DISTORTED_GUITAR", "HARMONIC_GUITAR", "ACOUSTIC_BASS", 34 | "FINGERED_ELECTRIC_BASS", "PICKED_ELECTRIC_BASS", "FRETLESS_BASS", 35 | "SLAP_BASS1", "SLAP_BASS2", "SYNTH_BASS1", 36 | "SYNTH_BASS2", "VIOLIN", "VIOLA", 37 | "CELLO", "CONTRABASS", "TREMOLO_STRINGS", 38 | "PIZZICATO_STRINGS", "HARP", "TIMPANI", 39 | "STRINGS1", "STRINGS2", "SYNTH_STRINGS1", 40 | "SYNTH_STRINGS2", "CHOIR_AAHS", "VOICE_OOHS", 41 | "SYNTH_VOICE", "ORCHESTRA_HIT", "TRUMPET", 42 | "TROMBONE", "TUBA", "MUTED_TRUMPET", 43 | "FRENCH_HORN", "BRASS_SECTION", "SYNTH_BRASS1", 44 | "SYNTH_BRASS2", "SOPRANO_SAX", "ALTO_SAX", 45 | "TENOR_SAX", "BARITONE_SAX", "OBOE", 46 | "BASSOON", "ENGLISH_HORN", "CLARINET", 47 | "PICCOLO", "FLUTE", "RECORDER", 48 | "PAN_FLUTE", "BLOWN_BOTTLE", "SHAKUHACHI", 49 | "WHISTLE", "OCARINA", "SQUARE_LEAD", 50 | "SAW_LEAD", "CALIOPE_LEAD", "CHIFF_LEAD", 51 | "CHARANG_LEAD", "VOICE_LEAD", "FIFTHS_LEAD", 52 | "BASS_LEAD", "NEW_AGE_PAD", "WARM_PAD", 53 | "POLYSYNTH", "CHOIR_PAD", "BOWED_PAD", 54 | "METALLIC_PAD", "HALO_PAD", "SWEEP_PAD", 55 | "TRAIN_FX", "SOUNDTRACK_FX", "CRYSTAL_FX", 56 | "ATMOSPHERE_FX", "BRIGHTNESS_FX", "GOBLIN_FX", 57 | "ECHO_FX", "SCI_FI_FX" "SITAR", 58 | "BANJO", "SHAMISEN", "KOTO", 59 | "KALIMBA", "BAGPIPE", "FIDDLE", 60 | "SHANAI", "TINKLE_BELL", "AGOGO", 61 | "STEEL_DRUM", "WOODBLOCK", "TAIKO_DRUM", 62 | "MELODIC_DRUM", "SYNTH_DRUM", "REVERSE_CYMBAL", 63 | "FRET_NOISE", "BREATH_NOISE", "SEASHORE", 64 | "BIRD_TWEET", "TELEPHONE", "HELICOPTER", 65 | "APPLAUSE", "GUNSHOT"] 66 | -------------------------------------------------------------------------------- /PTGG.py: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | ## This module is a Python version of Kulitta's PTGG module, which 3 | ## was originally written in Haskell. 4 | ## 5 | ## Author: Donya Quick 6 | ## Last modified: 28-Nov-2015 7 | ## Python 2 and 3 compatible version of PTGG generative functions 8 | ##################################################################### 9 | 10 | from random import * 11 | from copy import * 12 | 13 | # Nonterminal class 14 | class NT: 15 | def __init__(self, val): 16 | self.val=val 17 | def __str__(self): 18 | return ('NT '+str(self.val)) 19 | def __repr__(self): 20 | return str(self) 21 | 22 | # Let statement class to handle statements of the form: let x = A in exp 23 | class Let: 24 | def __init__(self, x, val, exp): 25 | self.x=x 26 | self.val=val 27 | self.exp=exp 28 | def __str__(self): 29 | return ('Let '+str(self.x)+' = '+str(self.val)+' in '+str(self.exp)) 30 | def __repr__(self): 31 | return str(self) 32 | # Variable class for handling instances of variables within expressions 33 | class Var: 34 | def __init__(self, name): 35 | self.name=name # this is assumed to be a string 36 | def __str__(self): 37 | return ('Var '+self.name) 38 | def __repr__(self): 39 | return str(self) 40 | 41 | 42 | 43 | # This version of applyRule assumes that we have already 44 | # picked an appropriate rule. This is the version that 45 | # will be used normally. It has a probabilistic version 46 | # as well that allows for rules that have a probability 47 | # attached as (p, (lhs, rhs)). 48 | 49 | def applyRule(r,sym): 50 | return (r[1](sym[1])) 51 | 52 | def applyRuleP(r,sym): 53 | return (r[1][1](sym[1])) 54 | 55 | # For choose(xs), we assume that xs is a list of tuples. 56 | # The first element in each tuple is the probability. We 57 | # assume that probabilities sum to 1.0. 58 | 59 | def choose(xs): 60 | n = len(xs) 61 | if n> 0: # Do we have things to choose from? 62 | r = random() # in [0.0, 1.0) 63 | i = 0 64 | while i < n: # Search through the items 65 | p = xs[i][0] 66 | if p >= r: # Have we used up the probability mass? 67 | return xs[i][1] # Yes - pick current element 68 | r = r-p # Subtract some probability mass 69 | i=i+1 # go to next index 70 | return xs[n-1] # Catch-all case for bad prob. mass distribution 71 | else: 72 | raise Exception('Empty list supplied to choose function') 73 | 74 | # A Python version of the filter function from Haskell 75 | 76 | def filter(seq, f): 77 | return [elem for elem in seq if f(elem)] 78 | 79 | # Rule matching functions. 80 | 81 | def sameLhs(x,r): # assumes r=(lhs,rhs) 82 | return (x==r[0]) 83 | 84 | def sameLhsP(x,r): # assumes r=(p,(lhs,rhs)) 85 | return (x==r[1][0]) 86 | 87 | def findRules(rules,x): # assumes rules have form (lhs, rhs) 88 | xRules = list(filter(rules, lambda r: sameLhs(x,r))) 89 | return xRules 90 | 91 | def findRulesP(prules,x): # assumes rules have form (p,(lhs,rhs)) 92 | xRules = filter(prules, lambda r: sameLhsP(x,r)) 93 | return xRules 94 | 95 | # Update function to apply rules left to right over 96 | # a sequence of symbols. 97 | 98 | def update(prules, seq): 99 | newSeq = [] # new sequence 100 | for x in seq: # update each symbol in the sequence 101 | if (x.__class__.__name__ == 'Var'): 102 | newSeq.append(x) 103 | elif (x.__class__.__name__ == 'Let'): 104 | newVal = update(prules, x.val) 105 | newExp = update(prules, x.exp) 106 | newSeq = [Let(x.x, newVal, newExp)] 107 | elif (x.__class__.__name__ == 'NT'): 108 | okRs = findRulesP(prules, x.val[0]) # which rules can we use? 109 | if len(okRs) > 0: # did we find any rules? 110 | r = choose(okRs) # pick a rule stochastically 111 | newX = applyRule(r,x.val) # apply the rule 112 | # print(newSeq) 113 | # print(type(list(newX))) 114 | newSeq = newSeq+newX # grow the new sequence 115 | else: # no rules available - symbol is a terminal. 116 | newSeq.append(x) 117 | else: 118 | raise Exception("Unrecognized symbol: " + str(x)+"Type: "+type(x).__class__.__name__) 119 | return newSeq 120 | 121 | # The gen function for n iterations 122 | 123 | def gen(prules, seq, n): 124 | if n<=0: # are we done? 125 | return seq 126 | else: # not done, so generate one more level 127 | newSeq = update(prules, seq) 128 | return gen(prules, newSeq, n-1) 129 | 130 | # Wraps a list of pairs with the NT constructor. 131 | def toNT(seq): 132 | for x in seq: 133 | yield (NT(x)) 134 | 135 | # expand instantiates all Lets. Either an existing environment 136 | # can be supplied or [] for statements containing all necessary 137 | # definitions as Lets. 138 | def expand(env, seq): 139 | newSeq = [] # create a new local sequence to build 140 | for x in seq: 141 | if (x.__class__.__name__ == 'Var'): 142 | xVal = lookupLast(env,x.name) # find variable definition 143 | newSeq = newSeq +xVal # add its definition to the new sequence 144 | elif (x.__class__.__name__ == 'Let'): 145 | env.append((x.x, x.val)) # add x's definition 146 | newXs = expand(env,x.exp) # recurse into the expression 147 | newSeq = newSeq+newXs # add result to new sequence 148 | env.pop() # remove x's definition 149 | elif (x.__class__.__name__ == 'NT'): 150 | newSeq.append(x) # just add the symbol 151 | else: 152 | raise Exception("Unrecognized symbol: " + str(x)+"Type: "+type(x).__class__.__name__) 153 | return newSeq 154 | 155 | def lookupLast(env, v): 156 | n = len(env) 157 | i = n-1 158 | while i>=0: # walk backwards through list 159 | if (v==env[i][0]): # found a match? 160 | return env[i][1] 161 | i = i-1 162 | raise Exception('No table entry for variable name '+v) 163 | 164 | 165 | # The toPairs function expands the term and strips NT constructors 166 | def toPairs(seq): 167 | newSeq = [] 168 | for x in seq: 169 | if (x.__class__.__name__ == 'Var'): 170 | raise Exception('No definition for variable '+x.name) 171 | elif (x.__class__.__name__ == 'Let'): 172 | newSeq = newSeq + expand([],x) 173 | elif (x.__class__.__name__ == 'NT'): 174 | newSeq.append(x.val) 175 | else: 176 | raise Exception("Unrecognized symbol: " + str(x)+"Type: "+type(x).__class__.__name__) 177 | return newSeq 178 | 179 | # tMap transforms the data values in a term (operates on NT and Let). 180 | # The original value is unaffected; a copy is made before any changes. 181 | def tMap(f, seq0): 182 | seq = deepcopy(seq0) 183 | for x in seq: 184 | if (x.__class__.__name__ == 'Let'): 185 | x.val = tMap(f,x.val) 186 | x.exp = tMap(f,x.exp) 187 | elif (x.__class__.__name__ == 'NT'): 188 | x.val = f(x.val) 189 | elif (x.__class__.__name__ != 'Var'): 190 | raise Exception("Unrecognized symbol: " + str(x)+"Type: "+type(x).__class__.__name__) 191 | return seq 192 | 193 | # normalize fixes the probability distribution for a rule set. 194 | # The original value is unaffected; a copy is made before any changes. 195 | def normalize(prules0): 196 | if len(prules0) <= 0: 197 | return [] 198 | else: 199 | prules = deepcopy(prules0) 200 | x0 = prules[0][1][0] 201 | rules1 = fixProbs (findRulesP(prules,x0)) 202 | rules2 = normalize(findRulesPNot(prules, x0)) 203 | return (rules1 + rules2) 204 | 205 | # fixProbs is one step of normalization for rules with the same lhs. 206 | # The original value is unaffected; a copy is made before any changes. 207 | def fixProbs(prules): 208 | s = sum (map (lambda r: r[0], prules)) 209 | newRules = [] 210 | for r in prules: 211 | newRules.append((r[0]/s,r[1])) 212 | return newRules 213 | 214 | # The opposite of fineRulesP (finds non-matching lhs rules) 215 | def findRulesPNot(prules,x): # assumes rules have form (p,(lhs,rhs)) 216 | xRules = list(filter(prules, lambda r: not(sameLhsP(x,r)))) 217 | return xRules 218 | 219 | #================================ 220 | # TESTING 221 | 222 | # Testing with arbitrary numbers 223 | 224 | rules2 = [(0.5, (0, lambda p: [NT ((0,p)), NT((0,p+1))])), 225 | (0.5, (0, lambda p: [NT ((1,p))])), 226 | (3.0, (1, lambda p: [NT ((1,p))])), # not normalized (for testing purposes) 227 | (6.0, (1, lambda p: [NT ((2,p))]))] # not normalized (for testing purposes) 228 | 229 | def foo(x): # for testing tMap 230 | if x[0] == 0: 231 | return ('a',x[1]) # map 0 to a 232 | else: 233 | return ('b',x[1]) # map 1 to b 234 | 235 | def testIt2(seedVal, n): 236 | seed(seedVal) 237 | x0 = [Let('x', [NT((0,0))], [Var('x'), Var('x')])] # starting value 238 | xn = gen(normalize(rules2), x0, n) # test gen 239 | xe = toPairs(expand ([], xn)) # test toPairs 240 | xf = tMap(foo,xn) # test tMap 241 | return (x0, xn, xf, xe) 242 | 243 | # print(testIt2(5,4)) 244 | 245 | # Testing with chord-based PTGG prototype 246 | 247 | I = 0 248 | V = 4 249 | 250 | rules3 = [(0.5, (I, lambda p: [NT((V,p/2)), NT((I,p/2))])), # I^t --> V^(t/2) I^(t/2) 251 | (0.5, (I, lambda p: [NT((I,p/2)), NT((I,p/2))])), # I^t --> I^(t/2) I^(t/2) 252 | (1.0, (V, lambda p: [NT((V,p))]))] # V^t --> V^t 253 | 254 | def testIt3(seedVal, n): 255 | seed(seedVal) 256 | x0 = [Let('x', [NT((I,4.0))], [Var('x'), Var('x')])] # test case 1 (lets) 257 | #x0 = [NT((I,4.0))] # test case 2 (no lets) 258 | xn = gen(normalize(rules3), x0, n) # test gen 259 | xe = toPairs(expand ([], xn)) # test toPairs 260 | return (x0, xn, xe) 261 | 262 | 263 | -------------------------------------------------------------------------------- /PostProc.py: -------------------------------------------------------------------------------- 1 | import ChordSpaces 2 | import midi 3 | from MusicGrammars import * 4 | import ChordSpaces 5 | import PTGG 6 | from MidiFuns import * 7 | 8 | def toAs(ctype, mode): 9 | scale = None 10 | if mode == Mode.MAJOR: 11 | scale = Mode.MAJOR_SCALE + map(lambda x: x + 12, Mode.MAJOR_SCALE) 12 | else: 13 | scale = Mode.MINOR_SCALE + map(lambda x: x + 12, Mode.MINOR_SCALE) 14 | return [scale[ctype], scale[ctype + 2], scale[ctype + 4]] 15 | 16 | 17 | #===================For Testing ToAs function========== 18 | # for i2 in CType.ALL_CHORD: 19 | # print(toAs(i2, Mode.MINOR)) 20 | 21 | 22 | 23 | class RChord: 24 | def __init__(self, key, dur, c, v=100, o=0): 25 | self.key = Key(key.absPitch, key.mode) 26 | self.dur = dur 27 | self.ctype = c 28 | self.vol = v 29 | self.onset = 0 30 | def __str__(self): 31 | return "("+str(self.ctype)+","+str(self.dur)+")" 32 | def __repr__(self): 33 | return str(self) 34 | 35 | 36 | 37 | def toChords(terms): 38 | xe = PTGG.toPairs(PTGG.expand([], terms)) 39 | res = [] 40 | for x in xe: 41 | # print x 42 | a, p = x 43 | # print p.key 44 | # print p.mode 45 | key = Key(p.key, p.mode) 46 | rchord = RChord(key, p.dur, a) 47 | res.append(rchord) 48 | return res 49 | 50 | def toAbsChords(terms): 51 | rchords = toChords(terms) 52 | print "Rchords", rchords 53 | return map(toAbsChord, rchords) 54 | 55 | 56 | def toAbsChord(rchord): 57 | to_as_res = toAs(rchord.ctype, rchord.key.mode) 58 | absChd = ChordSpaces.t(to_as_res, rchord.key.absPitch) 59 | key = rchord.key 60 | return TChord(absChd, rchord.dur) #, rchord.vol, Key(key.absPitch, key.mode), rchord.onset) 61 | 62 | 63 | #def atTrans(p1, tChords): 64 | # for tChord in tChords: 65 | # trans_p(tChord.key, p1) 66 | # ChordSpaces.t(tChord.absChord, p1) 67 | 68 | -------------------------------------------------------------------------------- /PythonEuterpea.py: -------------------------------------------------------------------------------- 1 | # =============================================================================== 2 | # PythonEuterpea: a Python port of Haskell Euterpea's core score-level features. 3 | # Author: Donya Quick 4 | # Last modified: 30-Dec-2016 5 | # Last changes: in-place music manipulation functions can now be used as: x = f(x) 6 | # 7 | # This file requires GMInstruments.py and the python-midi library: 8 | # https://github.com/vishnubob/python-midi 9 | # 10 | # Python-midi can be installed with: pip install python-midi 11 | # 12 | # Euterpea is a library for music representation and creation in the Haskell 13 | # programming language. This file represents a port of the "core" features 14 | # of Euterpea's score-level or note-level features. This includes classes that 15 | # mirror the various constructors of the Music data type as well as functions 16 | # for conversion to MIDI. 17 | # 18 | # Haskell Euterpea's "Music a" polymorphism is captured in the Note class by 19 | # way of optional parameters like vol (volume) and params (any type). There is 20 | # also an optional params field that is not used by the MIDI export backend. 21 | # =============================================================================== 22 | 23 | from copy import deepcopy 24 | import midi # This is the python-midi library 25 | from GMInstruments import * # Bring in a bunch of GM instrument names 26 | 27 | 28 | class EuterpeaException(Exception): 29 | """ 30 | For throwing errors 31 | """ 32 | def __init__(self, value): 33 | self.parameter = value 34 | 35 | def __str__(self): 36 | return repr(self.parameter) 37 | 38 | 39 | # ================================================================= 40 | # DURATION CONSTANTS 41 | # ================================================================= 42 | 43 | WN = 1.0 # whole note = one measure in 4/4 44 | DHN = 0.75 # dotted half 45 | HN = 0.5 # half note 46 | DQN = 0.375 # dotted quarter 47 | QN = 0.25 # quarter note 48 | DEN = 0.1875 # dotted eighth 49 | EN = 0.125 # eighth note 50 | DSN = 0.09375 # dotted sixteenth 51 | SN = 0.0625 # sixteenth note 52 | DTN = 0.046875 # dotted thirtysecond 53 | TN = 0.03125 # thirtysecond note 54 | 55 | 56 | # ================================================================= 57 | # MUSICAL STRUCTURE REPRESENTATIONS 58 | # Haskell Euterpea features a type called Music, that is polymorphic 59 | # and has several constructors. In Python, these constructors are 60 | # represented as different classes that can (but do not have to) 61 | # fall under an umbrella Music class to store everything. 62 | # ================================================================= 63 | 64 | class Music: 65 | """ 66 | A piece of music consists of a tree of musical structures interpreted within 67 | a particular base or reference tempo, the default for which is 120bpm. 68 | """ 69 | def __init__(self, tree, bpm=120): 70 | self.tree = tree 71 | self.bpm = bpm 72 | 73 | def __str__(self): 74 | return 'Music(' + str(self.tree) + ', ' + str(self.bpm)+' bpm)' 75 | 76 | def __repr__(self): 77 | return str(self) 78 | 79 | 80 | class Note: 81 | """ 82 | A Euterpea Note has a pitch, duration, volume, and other possible parameters. 83 | (these other parameters are application-specific) 84 | """ 85 | def __init__(self, pitch, dur=0.25, vol=100, params=None): 86 | self.pitch = pitch 87 | self.dur = dur 88 | self.vol = vol 89 | self.params = params 90 | 91 | def __str__(self): 92 | return 'Note' + str((self.pitch, self.dur, self.vol)) 93 | 94 | def __repr__(self): 95 | return str(self) 96 | 97 | 98 | class Rest: 99 | """ 100 | A Euterpea Rest has just a duration. It's a temporal place-holder just like a 101 | rest on a paper score. 102 | """ 103 | def __init__(self, dur=0.25, params=None): 104 | self.dur = dur 105 | self.params = params 106 | 107 | def __str__(self): 108 | return 'Rest(' + str(self.dur) + ')' 109 | 110 | def __repr__(self): 111 | return str(self) 112 | 113 | 114 | class Seq: 115 | """ 116 | Seq is equivalent to Haskell Euterpexa's (:+:) operator. It composes two 117 | musical objects in sequence: left then right. 118 | """ 119 | def __init__(self, left, right): 120 | self.left = left 121 | self.right = right 122 | 123 | def __str__(self): 124 | return '(' + str(self.left) + ') :+: (' + str(self.right) + ')' 125 | 126 | def __repr__(self): 127 | return str(self) 128 | 129 | 130 | class Par: # For composing two things in parallel 131 | """ 132 | Par is equivalent to Haskell Euterpea's (:=:) operator. It composes two 133 | musical objects in parallel: left and right happen starting at the same 134 | time. 135 | """ 136 | def __init__(self, top, bot): 137 | self.top = top 138 | self.bot = bot 139 | 140 | def __str__(self): 141 | return '(' + str(self.top) + ') :=: (' + str(self.bot) + ')' 142 | 143 | def __repr__(self): 144 | return str(self) 145 | 146 | 147 | class Modify: 148 | """ 149 | Modify is equivalent to Haskell Euterpea's Modify constructor and allows 150 | alterations to a musical tree. Which modifiers are allowed are application 151 | specific. 152 | """ 153 | def __init__(self, modifier, tree): 154 | self.mod = modifier 155 | self.tree = tree 156 | 157 | def __str__(self): 158 | return 'Mod(' + str(self.mod) + ', ' + str(self.tree)+')' 159 | 160 | def __repr__(self): 161 | return str(self) 162 | 163 | 164 | class Tempo: 165 | """ 166 | A Tempo class to be used with the Modify class. 167 | Tempos are scaling factors, not bpm. If the current tempo is 120bpm 168 | and a Tempo(2.0) is applied, it results in 240bpm. 169 | """ 170 | def __init__(self, value): 171 | self.value = value 172 | 173 | def __str__(self): 174 | return 'Tempo(' + str(self.value) + ')' 175 | 176 | def __repr__(self): 177 | return str(self) 178 | 179 | 180 | # Constants for instrument creation 181 | PERC = True 182 | INST = False 183 | 184 | 185 | class Instrument: 186 | """ 187 | An Instrument class to be used with the Modify class. 188 | """ 189 | def __init__(self, value, itype=False): 190 | if isinstance(value, int): 191 | self.patch = (value, itype) 192 | self.name = gmName(self.patch) # need to update this - should look up from patch 193 | elif isinstance(value, basestring): 194 | self.name = value 195 | if self.name=="DRUMS": 196 | self.patch = (0, True) 197 | else: 198 | self.patch = (gmNames.index(self.name), itype) 199 | else: 200 | print "Unrecognized Instrument value: ", value 201 | self.value = "" 202 | self.patch = (0, False) 203 | 204 | def __str__(self): 205 | return self.name + '(' + str(self.patch) + ')' 206 | 207 | def __repr__(self): 208 | return str(self) 209 | 210 | 211 | def gmName(patch): 212 | if patch[0] < 0 or patch[0] > 127: 213 | return "NO_INSTRUMENT" 214 | elif patch[1]: 215 | return "DRUMS" 216 | else: 217 | return gmNames[patch[0]] 218 | 219 | 220 | # ================================================================= 221 | # OPERATIONS ON MUSICAL STRUCTURES 222 | # Haskell Euterpea provides a number of basic operations on the 223 | # Music type. Only a few of them are presented here. 224 | # ================================================================= 225 | 226 | def dur(x): 227 | """ 228 | Computes the duration of a music tree. Values are relative to the overall 229 | bpm for the entire tree, such that 0.25 is a quarter note. 230 | :param x: the music structure 231 | :return: the duration of x in whole notes (wn = 1.0) 232 | """ 233 | if (x.__class__.__name__ == 'Music'): 234 | d = dur(x.tree) 235 | return d * (120/x.tempo) 236 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 237 | return x.dur 238 | elif (x.__class__.__name__ == 'Seq'): 239 | return (dur(x.left) + dur(x.right)) 240 | elif (x.__class__.__name__ == 'Par'): 241 | return max(dur(x.top), dur(x.bot)) 242 | elif (x.__class__.__name__ == 'Modify'): 243 | if (x.mod.__class__.__name__ == 'Tempo'): 244 | d = dur(x.tree) 245 | return d / x.mod.value 246 | else: 247 | return dur(x.tree) 248 | else: 249 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) 250 | 251 | 252 | def line(musicVals): 253 | """ 254 | The line function build a "melody" with Seq constructors 255 | out of a list of music substructures. Values are NOT copied. 256 | :param musicVals: a list of musical structures 257 | :return: the sequential composition of the input list 258 | """ 259 | tree = None 260 | for m in musicVals: 261 | if tree is None: tree = m 262 | else: tree = Seq(tree, m) 263 | return tree 264 | 265 | 266 | 267 | def chord(musicVals): 268 | """ 269 | The chord function build a "chord" with Par constructors 270 | out of a list of music substructures. Values are NOT copied. 271 | :param musicVals: a list of music structures 272 | :return: the parallel composition of the input 273 | """ 274 | tree = None 275 | for m in musicVals: 276 | if tree is None: tree = m 277 | else: tree = Par(tree, m) 278 | return tree 279 | 280 | 281 | def mMap(f, x): 282 | """ 283 | The mMap function maps a function over the Notes in a Music value. 284 | :param f: Function to map over Notes 285 | :param x: the music structure to operate on 286 | :return: an in-place modification of the music structure 287 | """ 288 | if (x.__class__.__name__ == 'Music'): 289 | mMap(f, x.tree) 290 | return x 291 | elif (x.__class__.__name__ == 'Note'): 292 | f(x) 293 | return x 294 | elif (x.__class__.__name__ == 'Rest'): 295 | return x 296 | elif (x.__class__.__name__ == 'Seq'): 297 | mMap(f, x.left) 298 | mMap(f, x.right) 299 | return x 300 | elif (x.__class__.__name__ == 'Par'): 301 | mMap(f, x.top) 302 | mMap(f, x.bot) 303 | return x 304 | elif (x.__class__.__name__ == 'Modify'): 305 | mMap(f, x.tree) 306 | return x 307 | else: 308 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) 309 | 310 | 311 | 312 | def mMapAll(f, x): 313 | """ 314 | The mMapDur function is not found in Haskell Euterpea but may prove useful. 315 | It maps a function over Notes and Rests and applies it to the entire musical 316 | structure. Note: the function MUST handle the constructors directly if using 317 | something other than dur. 318 | :param f: The function to apply to durations (v.dur for a Note or Rest) 319 | :param x: the music structure to traverse 320 | :return: an in-place altered version of the music structure 321 | """ 322 | if (x.__class__.__name__ == 'Music'): 323 | mMapAll(f, x.tree) 324 | return x 325 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 326 | f(x) 327 | return x 328 | elif (x.__class__.__name__ == 'Seq'): 329 | mMapAll(f, x.left) 330 | mMapAll(f, x.right) 331 | return x 332 | elif (x.__class__.__name__ == 'Par'): 333 | mMapAll(f, x.top) 334 | mMapAll(f, x.bot) 335 | return x 336 | elif (x.__class__.__name__ == 'Modify'): 337 | mMapAll(f, x.tree) 338 | return x 339 | else: 340 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) 341 | 342 | 343 | def transpose(x, amount): 344 | """ 345 | transpose directly alters the Notes of the supplied structure. 346 | Each Note's pitch number has amount added to it. 347 | :param x: 348 | :param amount: 349 | :return: 350 | """ 351 | def f(xNote): xNote.pitch = xNote.pitch+amount 352 | mMap(f, x) 353 | return x 354 | 355 | # The following volume-related functions deviate slightly from 356 | # Haskell Euterpea's methods of handling volume. This is because 357 | # the volume is stored directly in the Note class in Python, which 358 | # is not the case in Haskell Euterpea. Note: volumes are not 359 | # guaranteed to be integers with scaleVolume. You sould use 360 | # intVolume before converting to MIDI. You may wish to use 361 | # scaleVolumeInt instead. 362 | 363 | def setVolume(x, volume): # set everything to a constant volume 364 | def f(xNote): xNote.vol = volume 365 | ret = mMap(f,x) 366 | return ret 367 | 368 | def scaleVolume(x, factor): # multiply all volumes by a factor 369 | def f(xNote): xNote.vol = xNote.vol * factor 370 | ret = mMap (f,x) 371 | return ret 372 | 373 | def scaleVolumeInt(x, factor): # multiply but then round to an integer 374 | def f(xNote): xNote.vol = int(round(xNote.vol * factor)) 375 | ret = mMap (f,x) 376 | return ret 377 | 378 | def adjustVolume(x, amount): # add a constant amount to all volumes 379 | def f(xNote): xNote.vol = xNote.vol + amount 380 | ret = mMap (f,x) 381 | return ret 382 | 383 | 384 | def checkMidiCompatible(x): 385 | """ 386 | Check whether pitch and volume values are within 0-127. 387 | If they are not, an exception is thrown. There is no return value. 388 | :param x: the music structure to search through 389 | :return: nothing if successful - otherwise an exception is thrown. 390 | """ 391 | def f(xNote): 392 | if xNote.vol < 0 or xNote.vol > 127: 393 | raise EuterpeaException("Invalid volume found: "+str(xNote.vol)) 394 | if xNote.pitch < 0 or xNote.pitch > 127: 395 | raise EuterpeaException("Invalid pitch found: "+str(xNote.pitch)) 396 | mMap (f,x) 397 | 398 | 399 | def forceMidiCompatible(x): 400 | """ 401 | Check whether pitch and volume values are within 0-127. 402 | Values <0 are converted to 0 and those >127 become 127. 403 | :param x: the music structure to alter 404 | :return: a MIDI-compatible version of the input 405 | """ 406 | def f(xNote): 407 | if xNote.vol < 0: xNote.vol = 0 408 | elif xNote.vol > 127: xNote.vol = 127 409 | if xNote.pitch <0: xNote.pitch = 0 410 | elif xNote.pitch >127: xNote.pitch = 127 411 | ret = mMap (f,x) 412 | return ret 413 | 414 | 415 | def reverse(x): 416 | """ 417 | Reverse a musical structure in place (last note is first, etc.) 418 | :param x: the music structure to reverse. 419 | :return: the reversal of the input. 420 | """ 421 | if (x.__class__.__name__ == 'Music'): 422 | reverse(x.tree) 423 | return x 424 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 425 | return x # nothing to do 426 | elif (x.__class__.__name__ == 'Seq'): 427 | temp = x.left 428 | x.left = x.right 429 | x.right = temp 430 | reverse(x.left) 431 | reverse(x.right) 432 | return x 433 | elif (x.__class__.__name__ == 'Par'): 434 | reverse(x.top) 435 | reverse(x.bot) 436 | dTop = dur(x.top) 437 | dBot = dur(x.bot) 438 | # reversal affects relative start time of each section. Must add rests to correct. 439 | if dTop < dBot: 440 | x.top = Seq(Rest(dBot-dTop), x.top) 441 | elif dBot < dTop: 442 | x.bot = Seq(Rest(dTop-dBot), x.bot) 443 | return x 444 | elif (x.__class__.__name__ == 'Modify'): 445 | reverse(x.tree) 446 | return x 447 | else: raise EuterpeaException("Unrecognized musical structure: "+str(x)) 448 | 449 | 450 | def times(music, n): 451 | """ 452 | Returns a new value that is n repetitions of the input musical structure. 453 | Deep copy is used, so there will be no shared references between the input 454 | and the output. 455 | :param music: the music structure to repeat 456 | :param n: how many times to repeat? 457 | :return: a new structure (so this should be called as a = times(b,n) 458 | """ 459 | if n <= 0: return Rest(0) 460 | else: 461 | m = deepcopy(music) 462 | return Seq(m, times(music, n-1)) 463 | 464 | 465 | def cut(x, amount): 466 | """ 467 | Keeps only the first duration amount of a musical structure. The amount 468 | is in measures at the reference duration, which is 120bpm unless specified 469 | by the Music constructor. Note that this operation is messy - it can leave 470 | a lot of meaningless structure in place, with leaves occupied by Rest(0). 471 | :param x: the music value to alter 472 | :param amount: how many whole notes worth to take. 473 | :return: the furst amount of the music structure by time (whole note = 1.0) 474 | """ 475 | if (x.__class__.__name__ == 'Music'): 476 | cut(x.tree, amount) 477 | return x 478 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 479 | if amount <= x.dur: 480 | x.dur = amount 481 | return x 482 | elif (x.__class__.__name__ == 'Seq'): 483 | dLeft = dur(x.left) 484 | if dLeft >= amount: # do we have enough duration on the left? 485 | cut(x.left, amount) 486 | x.right = Rest(0) # right side becomes nonexistent 487 | elif dLeft+dur(x.right) >= amount: # do we have enough duration on the right? 488 | cut(x.right, amount-dLeft) 489 | return x 490 | elif (x.__class__.__name__ == 'Par'): 491 | cut(x.top, amount) 492 | cut(x.bot, amount) 493 | return x 494 | elif (x.__class__.__name__ == 'Modify'): 495 | if (x.mod.__class__.__name__ == 'Tempo'): 496 | cut(x.tree, amount*x.mod.value) 497 | else: 498 | cut(x.tree, amount) 499 | return x 500 | else: raise EuterpeaException("Unrecognized musical structure: " + str(x)) 501 | 502 | 503 | def remove(x, amount): 504 | """ 505 | The opposite of "cut," chopping away the first amount. Note that this 506 | operation is messy - it can leave a lot of meaningless structure in 507 | place, with leaves occupied by Rest(0). 508 | :param x: the music structure to alter 509 | :param amount: how much to cut off of the beginning? 510 | :return: 511 | """ 512 | if amount<=0: pass # nothing to remove! 513 | elif (x.__class__.__name__ == 'Music'): 514 | remove(x.tree, amount) 515 | return x 516 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 517 | if amount >= x.dur: 518 | x.dur = 0 519 | if amount < x.dur: 520 | x.dur = x.dur - amount 521 | return x 522 | elif (x.__class__.__name__ == 'Seq'): 523 | dLeft = dur(x.left) 524 | if dLeft >= amount: 525 | remove(x.left, amount) 526 | elif dLeft + dur(x.right) >= amount: 527 | x.left = Rest(0) # remove all of the left side 528 | remove(x.right, amount-dLeft) 529 | return x 530 | elif (x.__class__.__name__ == 'Par'): 531 | remove(x.top, amount) 532 | remove(x.bot, amount) 533 | return x 534 | elif (x.__class__.__name__ == 'Modify'): 535 | if (x.mod.__class__.__name__ == 'Tempo'): 536 | remove(x.tree, amount*x.mod.value) 537 | else: 538 | remove(x.tree, amount) 539 | return x 540 | else: raise EuterpeaException("Unrecognized musical structure: " + str(x)) 541 | 542 | 543 | def mFold(x, noteOp, restOp, seqOp, parOp, modOp): 544 | """ 545 | The mFold operation traverses a music value with a series of operations 546 | for the various constructors. noteOp takes a Note, restOp takes a Rest, 547 | seqOp and parOp take the RESULTS of mFolding over their arguments, and 548 | modOp takes a modifier (x.mod) and the RESULT of mFolding over its 549 | tree (x.tree). 550 | :param x: 551 | :param noteOp: 552 | :param restOp: 553 | :param seqOp: 554 | :param parOp: 555 | :param modOp: 556 | :return: 557 | """ 558 | if (x.__class__.__name__ == 'Music'): 559 | return mFold(x.tree, noteOp, restOp, seqOp, parOp, modOp) 560 | elif (x.__class__.__name__ == 'Note'): 561 | return noteOp(x) 562 | elif (x.__class__.__name__ == 'Rest'): 563 | return restOp(x) 564 | elif (x.__class__.__name__ == 'Seq'): 565 | leftVal = mFold(x.left, noteOp, restOp, seqOp, parOp, modOp) 566 | rightVal = mFold(x.right, noteOp, restOp, seqOp, parOp, modOp) 567 | return seqOp(leftVal, rightVal) 568 | elif (x.__class__.__name__ == 'Par'): 569 | topVal = mFold(x.top, noteOp, restOp, seqOp, parOp, modOp) 570 | botVal = mFold(x.bot, noteOp, restOp, seqOp, parOp, modOp) 571 | return parOp(topVal, botVal) 572 | elif (x.__class__.__name__ == 'Modify'): 573 | val = mFold(x.tree, noteOp, restOp, seqOp, parOp, modOp) 574 | return modOp(x.mod, val) 575 | else: raise EuterpeaException("Unrecognized musical structure: " + str(x)) 576 | 577 | 578 | def firstPitch(x): 579 | """ 580 | The firstPitch function returns the first pitch in the Music value. 581 | None is returned if there are no notes. Preference is lef tand top. 582 | :param x: 583 | :return: 584 | """ 585 | if (x.__class__.__name__ == 'Music'): 586 | return firstPitch(x.tree) 587 | elif (x.__class__.__name__ == 'Note'): 588 | return x.pitchf 589 | elif (x.__class__.__name__ == 'Rest'): 590 | return None 591 | elif (x.__class__.__name__ == 'Seq'): 592 | leftVal = firstPitch(x.left) 593 | if leftVal==None: return firstPitch(x.right) 594 | else: return leftVal 595 | elif (x.__class__.__name__ == 'Par'): 596 | topVal = firstPitch(x.top) 597 | if topVal==None: return firstPitch(x.bot) 598 | else: return topVal 599 | elif (x.__class__.__name__ == 'Modify'): 600 | return firstPitch(x.tree) 601 | else: raise EuterpeaException("Unrecognized musical structure: " + str(x)) 602 | 603 | 604 | def getPitches(m): 605 | """ 606 | An application of mFold to extract all pitches in the music 607 | structure as a list. 608 | :param m: 609 | :return: 610 | """ 611 | def fn(n): return [n.pitch] 612 | def fr(r): return [] 613 | def fcat(a,b): return a+b 614 | def fm(m,t): return t 615 | return mFold(m, fn, fr, fcat, fcat, fm) 616 | 617 | 618 | def invertAt(m, pitchRef): 619 | """ 620 | Musical inversion around a reference pitch. Metrical structure 621 | is preserved; only pitches are altered. 622 | :param m: 623 | :param pitchRef: 624 | :return: 625 | """ 626 | def f(aNote): aNote.pitch = 2 * pitchRef - aNote.pitch 627 | ret = mMap(f, m) 628 | return ret 629 | 630 | 631 | def invert(m): 632 | """ 633 | Musical inversion around the first pitch in a musical structure. 634 | :param m: 635 | :return: 636 | """ 637 | p = firstPitch(m) 638 | ret = invertAt(m, p) 639 | return ret 640 | 641 | 642 | def instrument(m, value): 643 | """ 644 | Shorthand for setting an instrument. 645 | :param m: 646 | :param value: 647 | :return: 648 | """ 649 | return Modify(Instrument(value), m) 650 | 651 | 652 | def removeInstruments(x): 653 | """ 654 | Remove Instrument modifiers from a musical structure 655 | :param x: 656 | :return: 657 | """ 658 | def checkInstMod(x): # function to get rid of individual nodes 659 | if x.__class__.__name__ == 'Modify': 660 | if x.mod.__class__.__name__ == 'Instrument': return x.tree 661 | else: return x 662 | else: return x 663 | if x.__class__.__name__ == 'Music': 664 | tNew = checkInstMod(x.tree) 665 | removeInstruments(x.tree) 666 | return x 667 | elif x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest': 668 | return x 669 | elif x.__class__.__name__ == 'Seq': 670 | x.left = checkInstMod(x.left) 671 | x.right = checkInstMod(x.right) 672 | removeInstruments(x.left) 673 | removeInstruments(x.right) 674 | return x 675 | elif x.__class__.__name__ == 'Par': 676 | x.top = checkInstMod(x.top) 677 | x.bot = checkInstMod(x.bot) 678 | removeInstruments(x.top) 679 | removeInstruments(x.bot) 680 | return x 681 | elif x.__class__.__name__ == 'Modify': 682 | xNew = checkInstMod(x) 683 | return xNew 684 | else: raise EuterpeaException("Unrecognized musical structure: " + str(x)) 685 | 686 | 687 | def changeInstrument(m, value): 688 | x = removeInstruments(m) 689 | x1 = instrument(value, x) 690 | return x1 691 | 692 | 693 | # Scale all durations in a music structure by the same amount. 694 | def scaleDurations(m, factor): 695 | def f(x): x.dur = x.dur*factor 696 | x = mMapAll(f, m) 697 | return x 698 | 699 | 700 | # ================================================================= 701 | # EVENT-STYLE REPRESENTATION 702 | # Euterpea features an event-based representation of music called 703 | # MEvent. Conversion from Music to MEvent requires processing of 704 | # certain modifiers, such as Tempo. 705 | # ================================================================= 706 | 707 | def applyTempo(x, tempo=1.0): 708 | """ 709 | applyTempo copies its input and interprets its Tempo modifiers. This 710 | scales durations in the tree and removes Modify nodes for Tempo. The 711 | original input structure, however, is left unchanged. 712 | :param x: 713 | :param tempo: 714 | :return: 715 | """ 716 | y = deepcopy(x) 717 | y = applyTempoInPlace(y, tempo) 718 | return y 719 | 720 | 721 | def applyTempoInPlace(x, tempo=1.0): 722 | """ 723 | applyTempoInPlace performs in-place interpretation of Tempo modifiers. 724 | However, it still has to be used as: foo = applyTempoInPace(foo) 725 | :param x: 726 | :param tempo: 727 | :return: 728 | """ 729 | if (x.__class__.__name__ == 'Music'): 730 | x.tree = applyTempo(x.tree, 120/x.bpm) 731 | x.bpm = 120 732 | return x 733 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 734 | x.dur = x.dur / tempo 735 | return x 736 | elif (x.__class__.__name__ == 'Seq'): 737 | x.left = applyTempo(x.left, tempo) 738 | x.right = applyTempo(x.right, tempo) 739 | return x 740 | elif (x.__class__.__name__ == 'Par'): 741 | x.top = applyTempo(x.top, tempo) 742 | x.bot = applyTempo(x.bot, tempo) 743 | return x 744 | elif (x.__class__.__name__ == 'Modify'): 745 | if (x.mod.__class__.__name__ == 'Tempo'): 746 | x.tree = applyTempo(x.tree, x.mod.value) 747 | return x.tree 748 | else: 749 | x.tree = applyTempo(x.tree, tempo) 750 | return x 751 | else: 752 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) 753 | 754 | 755 | class MEvent: 756 | """ 757 | MEvent is a fairly direct representation of Haskell Euterpea's MEvent type, 758 | which is for event-style reasoning much like a piano roll representation. 759 | eTime is absolute time for a tempo of 120bpm. So, 0.25 is a quarter note at 760 | 128bpm. The patch field should be a patch number, like the patch field of 761 | the Instrument class. 762 | """ 763 | def __init__(self, eTime, pitch, dur, vol=100, patch=(-1, INST)): 764 | self.eTime=eTime 765 | self.pitch=pitch 766 | self.dur=dur 767 | self.vol=vol 768 | self.patch=patch 769 | def __str__(self): 770 | return "MEvent("+str(self.eTime)+","+str(self.pitch)+","+str(self.dur) +","+str(self.patch)+")" 771 | def __repr__(self): 772 | return str(self) 773 | 774 | 775 | def musicToMEvents(x, currentTime=0, currentInstrument=(-1,INST)): 776 | """ 777 | The musicToMEvents function converts a tree of Notes and Rests into an 778 | event structure. 779 | :param x: 780 | :param currentTime: 781 | :param currentInstrument: 782 | :return: 783 | """ 784 | if (x.__class__.__name__ == 'Music'): 785 | y = applyTempo(x) # interpret all tempo scaling factors before continuing 786 | return musicToMEvents(y.tree, 0, (-1, INST)) 787 | elif (x.__class__.__name__ == 'Note'): 788 | if x.dur > 0: 789 | return [MEvent(currentTime, x.pitch, x.dur, x.vol, currentInstrument)] # one note = one event 790 | else: # when duration is <0, there should be no event. 791 | return [] 792 | elif (x.__class__.__name__ == 'Rest'): 793 | return [] # rests don't contribute to an event representation 794 | elif (x.__class__.__name__ == 'Seq'): 795 | leftEvs = musicToMEvents(x.left, currentTime, currentInstrument) 796 | rightEvs = musicToMEvents(x.right, currentTime+dur(x.left), currentInstrument) 797 | return leftEvs + rightEvs # events can be concatenated, doesn't require sorting 798 | elif (x.__class__.__name__ == 'Par'): 799 | topEvs = musicToMEvents(x.top, currentTime, currentInstrument) 800 | botEvs = musicToMEvents(x.bot, currentTime, currentInstrument) 801 | return sorted(topEvs+botEvs, key=lambda e: e.eTime) # need to sort events by onset 802 | elif (x.__class__.__name__ == 'Modify'): 803 | if (x.mod.__class__.__name__ == 'Tempo'): 804 | y = applyTempo(x) 805 | return musicToMEvents(y, currentTime, currentInstrument) 806 | elif (x.mod.__class__.__name__ == 'Instrument'): 807 | return musicToMEvents(x.tree, currentTime, x.mod.patch) 808 | else: 809 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) 810 | 811 | 812 | # ================================================================= 813 | # MIDI CONVERSION BACKEND 814 | # From this point onwards, we deviate from the Haskell Euterpea and 815 | # provide classes and functions specific to peforming conversion to 816 | # MIDI in Python. 817 | # ================================================================= 818 | 819 | #First, some constants: 820 | ON = 1 # note on event type 821 | OFF = 0 # note off event type 822 | 823 | class MEventMidi: 824 | """ 825 | This is an intermediate type to aid in conversion to MIDI. A single 826 | MEvent will get split into two events, an on and off event. These 827 | will need to be sorted by event time (eTime) in larger lists. 828 | Field information: 829 | - eTime will be either relative to the last event depending on the 830 | current step on the way to conversion to MIDI. 831 | - eType should be either ON=1 or OFF=0. 832 | """ 833 | def __init__(self, eTime, eType, pitch, vol=100, patch=-1): 834 | self.eTime = eTime 835 | self.eType = eType 836 | self.pitch = pitch 837 | self.vol = vol 838 | self.patch = patch 839 | def typeStr(self): 840 | return ["OFF", "ON"][self.eType] 841 | def __str__(self): 842 | return "MEMidi("+str(self.eTime)+","+str(self.pitch)+","+self.typeStr()+")" 843 | def __repr__(self): 844 | return str(self) 845 | 846 | 847 | def mEventsToOnOff(mevs): 848 | """ 849 | This function is an intermediate on the way from the MEvent-style 850 | representation to MIDI format. 851 | :param mevs: 852 | :return: 853 | """ 854 | def f(e): 855 | return [MEventMidi(e.eTime, ON, e.pitch, e.vol, e.patch), 856 | MEventMidi(e.eTime+e.dur, OFF, e.pitch, e.vol, e.patch)] 857 | onOffs = [] 858 | for e in mevs: 859 | onOffs = onOffs + f(e) 860 | return sorted(onOffs, key=lambda e: e.eTime) 861 | 862 | 863 | def onOffToRelDur(evs): 864 | """ 865 | This function will convert an event sequence with an eTime field into 866 | a relative time stamp format (time since the last event). This is intended 867 | for use with the MEventMidi type. 868 | :param evs: 869 | :return: 870 | """ 871 | durs = [0] + map (lambda e: e.eTime, evs) 872 | for i in range(0, len(evs)): 873 | evs[i].eTime -= durs[i] 874 | 875 | 876 | def eventPatchList(mevs): 877 | patches = map (lambda e: e.patch, mevs) # extract just the patch from each note 878 | return list(set(patches)) # remove duplicates 879 | 880 | 881 | def linearPatchMap(patchList): 882 | """ 883 | A linearPatchMap assigns channels to instruments exculsively and top to bottom 884 | with the exception of percussion, which will always fall on channel 9. Note that 885 | this implementation allows the creation of tracks without any program changes 886 | through the use of negative numbers. 887 | :param patchList: 888 | :return: 889 | """ 890 | currChan = 0 # start from channel 0 and work upward 891 | pmap = [] # initialize patch map to be empty 892 | for p in patchList: 893 | if p[1]: # do we have percussion? 894 | pmap.append((p,9)) 895 | else: 896 | if currChan==15: 897 | print "ERROR: too many instruments. Only 15 unique instruments with percussion (channel 9) is allowed in MIDI." 898 | else: 899 | pmap.append((p,currChan)) # update channel map 900 | if currChan==8: currChan = 10 # step over percussion channel 901 | else: currChan = currChan+1 # increment channel counter 902 | return sorted(pmap, key = lambda x: x[1]) 903 | 904 | 905 | def splitByPatch(mevs, pListIn=[]): 906 | """ 907 | This function splits a list of MEvents (or MEventMidis) by their 908 | patch number. 909 | :param mevs: 910 | :param pListIn: 911 | :return: 912 | """ 913 | pList = [] 914 | # did we already get a patch list? 915 | if len(pListIn)==0: pList = eventPatchList(mevs) # no - need to build it 916 | else: pList = pListIn # use what we already were supplied 917 | evsByPatch = [] # list of lists to sort events 918 | unsorted = mevs # our starting list to work with 919 | for p in pList: # for each patch... 920 | pEvs = [x for x in unsorted if x.patch == p] # fetch the list of matching events 921 | evsByPatch.append(pEvs) # add them to the outer list of lists 922 | unsorted = [x for x in unsorted if x not in pEvs] # which events are left over? 923 | return evsByPatch 924 | 925 | 926 | # Tick resolution constant 927 | RESOLUTION = 96 928 | 929 | 930 | # Conversion from Kulitta's durations to MIDI ticks 931 | def toMidiTick(dur): 932 | ticks = int(round(dur * RESOLUTION * 4)) # bug fix 26-June-2016 933 | return ticks 934 | 935 | 936 | # Create a pythonmidi event from an MEventMidi value. 937 | def toMidiEvent(onOffMsg, chan): 938 | m = None 939 | ticks = toMidiTick(onOffMsg.eTime) 940 | p = int(onOffMsg.pitch) 941 | v = int(onOffMsg.vol) 942 | if onOffMsg.eType==ON: m = midi.NoteOnEvent(tick=ticks, velocity=v, pitch=p, channel=chan) 943 | else: m = midi.NoteOffEvent(tick=ticks, velocity=v, pitch=p) 944 | return m 945 | 946 | 947 | def mEventsToPattern(mevs): 948 | """ 949 | Converting MEvents to a MIDI file. The following function takes a music structure 950 | (Music, Seq, Par, etc.) and converts it to a pythonmidi Pattern. File-writing is 951 | not performed at this step. 952 | :param mevs: 953 | :return: 954 | """ 955 | pattern = midi.Pattern() # Instantiate a MIDI Pattern (contains a list of tracks) 956 | pattern.resolution = RESOLUTION # Set the tick per beat resolution 957 | pList = eventPatchList(mevs) # get list of active patches 958 | pmap = linearPatchMap(pList) # linear patch/channel assignment 959 | usedChannels = map(lambda p: p[1], pmap) # which channels are we using? (Important for drum track) 960 | mevsByPatch = splitByPatch(mevs, pList) # split event list by patch 961 | chanInd = 0; 962 | for i in range(0,16): 963 | track = midi.Track() 964 | if i in usedChannels: # are we using this channel? 965 | # if yes, then we add events to it 966 | mevsP = mevsByPatch[chanInd] # get the relevant collection of events 967 | if pmap[chanInd][0][0] >= 0: # are we assigning an instrument? 968 | track.append(midi.ProgramChangeEvent(value=pmap[chanInd][0][0], channel = i)) # set the instrument 969 | mevsOnOff = mEventsToOnOff(mevsP) # convert to on/off messages 970 | onOffToRelDur(mevsOnOff) # convert to relative timestamps 971 | for e in mevsOnOff: # for each on/off event... 972 | m = toMidiEvent(e, i) # turn it into a pythonmidi event 973 | track.append(m) # add that event to the track 974 | chanInd = chanInd+1; 975 | track.append(midi.EndOfTrackEvent(tick=1)) # close the track (not optional!) 976 | pattern.append(track) # add the track to the pattern 977 | return pattern 978 | 979 | 980 | def musicToMidi(filename, music): 981 | """ 982 | musicToMidi takes a filename (which must end in ".mid") and a music structure and writes 983 | a MIDI file. 984 | :param filename: 985 | :param music: 986 | :return: 987 | """ 988 | checkMidiCompatible(music) # are the volumes and pitches within 0-127? 989 | e = musicToMEvents(music) # convert to MEvents 990 | p = mEventsToPattern(e) # convert to a pythonmidi Pattern 991 | midi.write_midifile(filename, p) # write the MIDI file 992 | 993 | 994 | # ============================================================================================= 995 | # Some extra supporting functions for compatibility with more pure vector/list 996 | # representations of melodies and chords. 997 | 998 | 999 | # Convert a pitch number to a single note. 1000 | def pitchToNote(p, defDur=0.25, defVol=100): 1001 | if p==None: 1002 | return Rest(defDur) 1003 | else: 1004 | return Note(p, defDur, defVol) 1005 | 1006 | 1007 | # Convert a list of pitches to a melody using a default note duration. 1008 | def pitchListToMusic(ps, defDur=0.25, defVol=100): 1009 | ns = map(lambda p: pitchToNote(p, defDur, defVol), ps) 1010 | return line(ns) 1011 | 1012 | 1013 | # Synonym for consistency with some other naming schemes 1014 | # This does the same thing as pitchListToMusic 1015 | def pitchListToMelody(ps, defDur=0.25, defVol=100): 1016 | return pitchListToMusic(ps, defDur, defVol) 1017 | 1018 | def pdPairsToMusic(pds, defVol=100): 1019 | """ 1020 | Convert a list of pitch+duration pairs to a melody (a bunch of Notes in sequence). 1021 | pdPair = pitch-duration pair 1022 | :param pds: pairs of pitch and duration: [(p1,d1), (p2,d2), ...] 1023 | :param defVol: default volume 1024 | :return: music structure as a melody 1025 | """ 1026 | ns = map(lambda x: Note(x[0], x[1], defVol), pds) 1027 | return line(ns) 1028 | 1029 | def pdPairsToMelody(pds, defVol=100): # This is just a synonym for pdPairsToMusic 1030 | return pdPairsToMusic(pds, defVol) 1031 | 1032 | def pdPairsToChord(pds, defVol=100): 1033 | """ 1034 | Convert a list of pitch+duration pairs to a chord (a bunch of Notes in parallel). 1035 | NOTE: start times will be the same for all pitches, but end times will be based on 1036 | the duration of the notes - so they may end at different times! 1037 | pdPair = pitch-duration pair 1038 | :param pds: pairs of pitch and duration: [(p1,d1), (p2,d2), ...] 1039 | :param defVol: default volume 1040 | :return: music structure as a chord 1041 | """ 1042 | ns = map(lambda x: Note(x[0], x[1], defVol), pds) 1043 | return chord(ns) 1044 | 1045 | 1046 | # Convert a list of pitches to a chord (a bunch of Notes in parallel). 1047 | def pitchListToChord(ps, defDur=0.25, defVol=100): 1048 | if ps == None: 1049 | return Rest(defDur) 1050 | else: 1051 | ns = map (lambda p: Note(p, defDur, defVol), ps) 1052 | return chord(ns) 1053 | 1054 | 1055 | # Convert a list of chords (a list of lists of pitches) to a music structure. 1056 | def chordListToMusic(chords, defDur=0.25, defVol=100): 1057 | cList = map(lambda x: pitchListToChord(x, defDur, defVol), chords) 1058 | return line(cList) 1059 | 1060 | 1061 | # Remove zero-duration entities 1062 | 1063 | def removeZeros(x): 1064 | if (x.__class__.__name__ == 'Music'): 1065 | x.tree = removeZeros(x.tree) 1066 | return x 1067 | elif (x.__class__.__name__ == 'Note' or x.__class__.__name__ == 'Rest'): 1068 | return x # can't remove at this stage 1069 | elif (x.__class__.__name__ == 'Seq'): 1070 | x.left = removeZeros(x.left) 1071 | x.right = removeZeros(x.right) 1072 | if (dur(x.left) <= 0): 1073 | return x.right 1074 | elif(dur(x.right) <=0): 1075 | return x.left 1076 | else: 1077 | return x 1078 | elif (x.__class__.__name__ == 'Par'): 1079 | x.top = removeZeros(x.top) 1080 | x.bot = removeZeros(x.bot) 1081 | if (dur(x.top) <= 0): 1082 | return x.bot 1083 | elif(dur(x.bot) <=0): 1084 | return x.top 1085 | else: 1086 | return x 1087 | elif (x.__class__.__name__ == 'Modify'): 1088 | x.tree = removeZeros(x.tree) 1089 | return x 1090 | else: 1091 | raise EuterpeaException("Unrecognized musical structure: "+str(x)) -------------------------------------------------------------------------------- /QuotientSpaces.py: -------------------------------------------------------------------------------- 1 | def eqClass(qspace, eqrel, val): 2 | for q in qspace: 3 | if len(q) > 0 and eqrel(val,q[0]) is True: 4 | return q 5 | return [] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PythonKulitta 2 | Python version of Kulitta. This project also contains a Python port of the core features of the Euterpea library. 3 | -------------------------------------------------------------------------------- /Search.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | import QuotientSpaces 4 | def nextSol(buckets, pre): 5 | code = None 6 | res = [] 7 | codes = [] 8 | # print(pre) 9 | if pre is None: 10 | codes = (['1'] * len(buckets)) 11 | else: 12 | pre_codes = pre.split("-") 13 | carry = 1 14 | for i2 in range(len(pre_codes)): 15 | if carry == 1 and int(pre_codes[i2]) <= len(buckets[i2]) - 1: 16 | codes.append(str(int(pre_codes[i2]) + 1)) 17 | carry = 0 18 | elif carry == 1 and int(pre_codes[i2]) == len(buckets[i2]): 19 | codes.append(str(1)) 20 | else: 21 | codes.append(pre_codes[i2]) 22 | 23 | for i2 in range(len(buckets)): 24 | res.append(buckets[i2][int(codes[i2]) - 1]) 25 | codes = "-".join(codes) 26 | return codes, res 27 | 28 | 29 | def allSols(qspace, eqrel, bucket): 30 | targets = [] 31 | for val in bucket: 32 | tar = QuotientSpaces.eqClass(qspace, eqrel, val) 33 | if len(tar) == 0: 34 | raise Exception('error','No class for'+str(val)) 35 | targets.append(tar) 36 | return allSolsHelper(targets) 37 | 38 | def allSolsHelper(buckets): 39 | num = 1 40 | for b in buckets: 41 | num *= len(b) 42 | pre = None 43 | res = [] 44 | for iter in range(num): 45 | pre, cur = nextSol(buckets, pre) 46 | res.append(cur) 47 | # print(res) 48 | return res 49 | 50 | class Predicate: # f(a) # f(a, b) 51 | def __init__(self, fn, single=False): 52 | self.fn = fn 53 | self.single = single 54 | 55 | def is_single(self): 56 | return self.single 57 | 58 | def compare(self, a, b= None): 59 | if self.single is True: 60 | return self.fn(a) 61 | else: 62 | return self.fn(a,b) 63 | def compareAll(self, arr): 64 | if self.single is True: 65 | for i2 in arr: 66 | if self.fn(i2) is False: 67 | return False 68 | else: 69 | for i2 in range(len(arr) - 1): 70 | if self.fn(arr[i2], arr[i2+1]) is False: 71 | return False 72 | return True 73 | def pairProg(qspace, eqrel, constraint, bucket): 74 | targets = [] 75 | for val in bucket: 76 | tar = QuotientSpaces.eqClass(qspace, eqrel, val) 77 | if len(tar) == 0: 78 | raise Exception('error', 'No class for' + str(val)) 79 | targets.append(tar) 80 | res = [] 81 | pairProgHelper(targets, constraint, 0, [], res) 82 | return res 83 | 84 | def pairProgHelper(buckets, constraint, ith, pre, res): 85 | if ith == len(buckets): 86 | res.append(pre) 87 | return 88 | for candidate in buckets[ith]: 89 | if len(pre) == 0 or constraint(pre[-1], candidate) is True: 90 | pairProgHelper(buckets, constraint, ith + 1, pre + [candidate], res) 91 | 92 | 93 | # def pairProg(buckets, c): 94 | # cur = None 95 | # num = 1 96 | # for b in buckets: 97 | # num *= len(b) 98 | # res = [] 99 | # next = None 100 | # for i in range(num): 101 | # next, arr = nextSol(buckets, cur) 102 | # cur = next 103 | # suc = c.compareAll(arr) 104 | # if suc is True: 105 | # res.append(arr) 106 | # if len(res) == 1: 107 | # print("No solutions that satisfy the consraints!") 108 | # else: 109 | # return res 110 | 111 | class Fallback: 112 | def __init__(self, fn = None): 113 | self.fn = fn 114 | def gen(self, bucket, pre = None): 115 | if self.fn is None or pre is None: 116 | return random.choice(bucket) 117 | return self.fn(bucket, pre) 118 | # def greedyProg(buckets, fallback, c): 119 | # sol = [] 120 | # pre = None 121 | # for idx in range(len(buckets)): 122 | # # idx = 0 random choose 123 | # # choose candidates from pre, if null then use falback 124 | # if idx == 0: 125 | # pre = random.choice(buckets[0]) 126 | # sol.append(pre) 127 | # continue 128 | # can = [] 129 | # for val in buckets[idx]: 130 | # if c.is_single() is True: 131 | # if c.compare(val) is True: 132 | # can.append(val) 133 | # elif c.is_single() is False: 134 | # if c.compare(val, pre) is True: 135 | # can.append(val) 136 | # # print(pre, can) 137 | # if len(can): 138 | # pre = random.choice(can) 139 | # else: 140 | # pre = fallback.gen(buckets[idx], pre) 141 | # sol.append(pre) 142 | # return sol 143 | def greedyProg(qspace, eqrel, constraint, fallback, bucket): 144 | sol = [] 145 | pre = None 146 | for idx in range(len(bucket)): 147 | val = bucket[idx] 148 | tar = QuotientSpaces.eqClass(qspace, eqrel, val) 149 | can = [] 150 | if idx != 0: 151 | for t in tar: 152 | if constraint(pre, t) is True: 153 | can.append(t) 154 | else: 155 | can = tar 156 | if len(can) == 0: 157 | pre = fallback(tar, pre) 158 | else: 159 | pre = random.choice(can) 160 | sol.append(pre) 161 | return sol 162 | def nearFall(bucket, pre): 163 | # abs(i - pre) 164 | minV = sys.maxint 165 | minAbs = sys.maxint 166 | for val in bucket: 167 | if abs(val - pre) < minAbs: 168 | minAbs = abs(val - pre) 169 | minV = val 170 | return minV 171 | 172 | # # for testing: 173 | # A = [1, 2] 174 | # B = [3, 4] 175 | # # C = [1,0, 5, 6] 176 | # buckets = [A, B] 177 | # # code, arr = nextSol(buckets, "0-0") 178 | # # print(code) 179 | # # print(arr) 180 | # print("all solution:") 181 | # print(allSols(buckets)) 182 | # 183 | # def absSmaller2(a, b): 184 | # return abs(a - b) <= 2 185 | # p = Predicate(absSmaller2, False) 186 | # a = 10 187 | # b = 6 188 | # print(p.compare(a, b)) 189 | # res = pairProg(buckets, p) 190 | # print(res) 191 | # 192 | # print("Test for nearFall:") 193 | # print("closest to 1 is:") 194 | # print(nearFall([12, 0, 5], 1)) 195 | # 196 | # print("Test for greedy Prog") 197 | # buckets = [[2, 4], [5, 4]] 198 | # def absSmaller1(a, b): 199 | # return abs(a - b) <= 1 200 | # fallback = Fallback(nearFall) 201 | # p2 = Predicate(absSmaller2, False) 202 | # print(greedyProg(buckets, fallback, p2)) 203 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import PythonEuterpea 2 | import PEConstants -------------------------------------------------------------------------------- /cleanup.bat: -------------------------------------------------------------------------------- 1 | del *.pyc 2 | del *.mid -------------------------------------------------------------------------------- /doc/PythonEuterpea_reference.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donya/PythonKulitta/c67a077c8b87d94070a8286e17b767301cd14f68/doc/PythonEuterpea_reference.pdf -------------------------------------------------------------------------------- /test.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donya/PythonKulitta/c67a077c8b87d94070a8286e17b767301cd14f68/test.mid --------------------------------------------------------------------------------