comparison musiccutter.py @ 42:3925ac56d99e

Allow flushing notes. Requires refactoring a bunch of crap though and makes some of the functions know a bit bunch..
author Daniel O'Connor <darius@dons.net.au>
date Tue, 24 May 2016 19:42:02 +0930
parents 21da8af1cdd2
children c69e53b8917c
comparison
equal deleted inserted replaced
41:21da8af1cdd2 42:3925ac56d99e
80 80
81 self.midi2note, self.note2midi = Midi2PDF.genmidi2note() 81 self.midi2note, self.note2midi = Midi2PDF.genmidi2note()
82 self.note2slot, self.slot2note = Midi2PDF.loadnote2slot(cp.get('default', 'notes').split(), self.note2midi) 82 self.note2slot, self.slot2note = Midi2PDF.loadnote2slot(cp.get('default', 'notes').split(), self.note2midi)
83 83
84 def processMidi(self, midifile, outpat): 84 def processMidi(self, midifile, outpat):
85 stats = Stats() 85 self.stats = Stats()
86 stats.playablecount = 0 86 self.stats.playablecount = 0
87 stats.unplayablecount = 0 87 self.stats.unplayablecount = 0
88 stats.transposeupcount = 0 88 self.stats.transposeupcount = 0
89 stats.transposedowncount = 0 89 self.stats.transposedowncount = 0
90 midi = mido.MidiFile(midifile) 90 midi = mido.MidiFile(midifile)
91 ctime = 0 91 self.ctime = 0
92 channels = [] 92 self.channels = []
93 for i in range(16): 93 for i in range(16): # 16 is the maximum number of MIDI channels
94 channels.append({}) 94 self.channels.append({})
95 95
96 npages = int(math.ceil(((midi.length * self.timescale) + self.leadin) / self.pagewidth)) 96 npages = int(math.ceil(((midi.length * self.timescale) + self.leadin) / self.pagewidth))
97 npdfs = int(math.ceil(float(npages) / self.pagesperpdf)) 97 npdfs = int(math.ceil(float(npages) / self.pagesperpdf))
98 print 'npages %d, npdfs %d' % (npages, npdfs) 98 print 'npages %d, npdfs %d' % (npages, npdfs)
99 99
100 pdfs = [] 100 self.pdfs = []
101 for i in range(npdfs): 101 for i in range(npdfs):
102 pdf = reportlab.pdfgen.canvas.Canvas(file(outpat % (i + 1), 'w'), 102 pdf = reportlab.pdfgen.canvas.Canvas(file(outpat % (i + 1), 'w'),
103 pagesize = (self.pdfwidth * mm, self.pageheight * mm)) 103 pagesize = (self.pdfwidth * mm, self.pageheight * mm))
104 pdfs.append(pdf) 104 self.pdfs.append(pdf)
105 105
106 title = os.path.basename(midifile) 106 title = os.path.basename(midifile)
107 title, ext = os.path.splitext(title) 107 title, ext = os.path.splitext(title)
108 for ev in midi: 108 for ev in midi:
109 # Adjust pitch 109 # Adjust pitch
110 if hasattr(ev, 'note'): 110 if hasattr(ev, 'note'):
111 ev.note += self.noteoffset 111 ev.note += self.noteoffset
112 ctime += ev.time 112 self.ctime += ev.time
113 #print ctime, ev 113 print '%.03f: %s' % (self.ctime, str(ev))
114 #Tracer()() 114 #Tracer()()
115 115
116 if ev.type == 'text' and ctime == 0: 116 if ev.type == 'text' and self.ctime == 0:
117 title = ev.text 117 title = ev.text
118 if ev.type == 'note_on' and ev.velocity > 0: 118 if ev.type == 'note_on' and ev.velocity > 0:
119 if ev.note in channels[ev.channel]: 119 if ev.note in self.channels[ev.channel]:
120 print 'Duplicate note_on message at %.1f sec channel %d note %d' % (ctime, ev.channel, ev.note) 120 print 'Duplicate note_on message at %.1f sec channel %d note %d' % (self.ctime, ev.channel, ev.note)
121 else: 121 else:
122 channels[ev.channel][ev.note] = ctime 122 # Store start time
123 self.channels[ev.channel][ev.note] = self.ctime
124 # note_on with velocity of 0 is a note_off
123 elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0): 125 elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0):
124 if ev.note not in channels[ev.channel]: 126 if ev.note not in self.channels[ev.channel]:
125 # These can be rests (iWriteMusic) 127 # These can be rests (iWriteMusic)
126 print 'note_off with no corresponding note_on at %.1f sec for channel %d note %d' % (ctime, ev.channel, ev.note) 128 print 'note_off with no corresponding note_on at %.1f sec for channel %d note %d' % (self.ctime, ev.channel, ev.note)
127 continue 129 continue
128 else: 130 else:
129 orignote = ev.note 131 orignote = self.emitnote(ev)
130 start = channels[ev.channel][orignote] 132 del self.channels[ev.channel][orignote]
131 evw = EVWrap(ev)
132 # Find a slot (plus adjust pitch, attempt transposition etc)
133 if hasattr(ev, 'note'):
134 self.getslotfornote(evw, stats, ctime)
135 # Check if it was unplayable
136 if not evw.slot:
137 continue
138 notelen = ctime - start
139 #print 'Note %s (%d) at %.2f length %.2f' % (evw.notename, ev.slot, start, notelen)
140 self.emitnote(pdfs, evw.slot, start, notelen * self.notescale)
141
142 del channels[ev.channel][orignote]
143 elif ev.type == 'end_of_track': 133 elif ev.type == 'end_of_track':
144 print 'EOT, not flushing, check for missed notes' 134 for chan in self.channels:
145 for chan in channels:
146 for ev in chan: 135 for ev in chan:
136 print 'Flushed', ev
137 self.emitnote(ev)
147 print ev 138 print ev
148 139
149 print 'Playable count:', stats.playablecount 140 print 'Playable count:', self.stats.playablecount
150 print 'Unplayable count:', stats.unplayablecount 141 print 'Unplayable count:', self.stats.unplayablecount
151 if self.trytranspose: 142 if self.trytranspose:
152 print 'Transpose down:', stats.transposedowncount 143 print 'Transpose down:', self.stats.transposedowncount
153 print 'Transpose up:', stats.transposeupcount 144 print 'Transpose up:', self.stats.transposeupcount
154 145
155 # Do per-page things 146 # Do per-page things
156 for pindx in range(npages): 147 for pindx in range(npages):
157 pdf = pdfs[pindx / self.pagesperpdf] # PDF for this page 148 pdf = self.pdfs[pindx / self.pagesperpdf] # PDF for this page
158 # Offset into PDF where the page starts 149 # Offset into PDF where the page starts
159 pageofs = self.pagewidth * (self.pagesperpdf - (pindx % self.pagesperpdf) - 1) 150 pageofs = self.pagewidth * (self.pagesperpdf - (pindx % self.pagesperpdf) - 1)
160 # Add title and page number 151 # Add title and page number
161 Midi2PDF.textHelper(pdf, pageofs * mm, 1 * mm, 152 Midi2PDF.textHelper(pdf, pageofs * mm, 1 * mm,
162 ENGRAVE_COLOUR, True, self.fontname, self.fontsize, 153 ENGRAVE_COLOUR, True, self.fontname, self.fontsize,
191 pdf.line(0, ofs * mm, self.pdfwidth * mm, ofs * mm) 182 pdf.line(0, ofs * mm, self.pdfwidth * mm, ofs * mm)
192 # Note name 183 # Note name
193 if self.notenames: 184 if self.notenames:
194 Midi2PDF.textHelper(pdf, (self.pdfwidth - 10) * mm, (ofs + 0.5) * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, self.slot2note[slot]) 185 Midi2PDF.textHelper(pdf, (self.pdfwidth - 10) * mm, (ofs + 0.5) * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, self.slot2note[slot])
195 pdf.restoreState() 186 pdf.restoreState()
196 for pdf in pdfs: 187 for pdf in self.pdfs:
197 pdf.save() 188 pdf.save()
198 189
199 def noteisplayable(self, midi): 190 def noteisplayable(self, midi):
200 slot = None 191 #Tracer()()
201 if midi in self.midi2note: 192 if midi in self.midi2note:
202 notename = self.midi2note[midi] 193 notename = self.midi2note[midi]
203 if notename in self.note2slot: 194 if notename in self.note2slot:
204 slot = self.note2slot[notename] 195 return True
205 196
206 return slot 197 return False
207 198
208 # Check if the organ can play the note 199 # Check if the organ can play the note
209 def transposenote(self, evw, amount): 200 def transposenote(self, evw, amount):
210 evw.ev.note += amount 201 evw.ev.note += amount
211 evw.notename = self.midi2note[evw.ev.note] 202 evw.notename = self.midi2note[evw.ev.note]
212 evw.slot = self.note2slot[evw.notename] 203 evw.slot = self.note2slot[evw.notename]
213 204
214 # Work out which slot to use for the note, transpose if desired 205 # Work out which slot to use for the note, transpose if desired
215 def getslotfornote(self, evw, stats, ctime): 206 def getslotfornote(self, evw):
216 evw.slot = None 207 evw.slot = None
217 evw.notename = None 208 evw.notename = None
218 209
219 # First off, is the note in our midi table? 210 # First off, is the note in our midi table?
220 if evw.ev.note in self.midi2note: 211 if evw.ev.note in self.midi2note:
221 evw.notename = self.midi2note[evw.ev.note] 212 evw.notename = self.midi2note[evw.ev.note]
222 # Is it playable? 213 # Is it playable?
223 if self.noteisplayable(evw.ev.note) != None: 214 if self.noteisplayable(evw.ev.note):
224 evw.slot = self.note2slot[evw.notename] 215 evw.slot = self.note2slot[evw.notename]
225 # Nope, maybe we can transpose? 216 # Nope, maybe we can transpose?
226 elif self.trytranspose: 217 elif self.trytranspose:
227 # Go for -3 to +3 octaves (going down first) 218 # Go for -3 to +3 octaves (going down first)
228 for i in [-12, -24, -36, 12, 24, 36]: 219 for i in [-12, -24, -36, 12, 24, 36]:
229 if self.noteisplayable(evw.ev.note + i) != None: 220 if self.noteisplayable(evw.ev.note + i):
230 self.transposenote(evw, i) 221 self.transposenote(evw, i)
231 if i < 0: 222 if i < 0:
232 stats.transposedowncount += 1 223 self.stats.transposedowncount += 1
233 tmp = 'down' 224 tmp = 'down'
234 else: 225 else:
235 stats.transposeupcount += 1 226 self.stats.transposeupcount += 1
236 tmp = 'up' 227 tmp = 'up'
237 print 'Transposed note at %.1f sec %s (%d) %s %d octave(s) to %s (%d)' % ( 228 print 'Transposed note at %.1f sec %s (%d) %s %d octave(s) to %s (%d)' % (
238 ctime, self.midi2note[evw.ev.note - i], evw.ev.note - i, tmp, 229 self.ctime, self.midi2note[evw.ev.note - i], evw.ev.note - i, tmp,
239 abs(i / 12), evw.notename, evw.ev.note) 230 abs(i / 12), evw.notename, evw.ev.note)
240 break 231 break
241 if evw.slot != None: 232 if evw.slot != None:
242 stats.playablecount += 1 233 self.stats.playablecount += 1
243 else: 234 else:
244 print 'Note at %.1f sec %d (%s) not playable' % (ctime, evw.ev.note, self.midi2note[evw.ev.note]) 235 print 'Note at %.1f sec %d (%s) not playable' % (self.ctime, evw.ev.note, self.midi2note[evw.ev.note])
245 stats.unplayablecount += 1 236 self.stats.unplayablecount += 1
246 else: 237 else:
247 print 'Note at %.1f sec, %d not in MIDI table' % (ctime, evw.ev.note) 238 print 'Note at %.1f sec, %d not in MIDI table' % (self.ctime, evw.ev.note)
248 stats.unplayablecount += 1 239 self.stats.unplayablecount += 1
249 240
250 # http://newt.phys.unsw.edu.au/jw/notes.html 241 # http://newt.phys.unsw.edu.au/jw/notes.html
251 # But this seems dumb since the lowest MIDI note is 0 which would be C-1.. 242 # But this seems dumb since the lowest MIDI note is 0 which would be C-1..
252 @staticmethod 243 @staticmethod
253 def genmidi2note(): 244 def genmidi2note():
280 slot2note[index] = note 271 slot2note[index] = note
281 index += 1 272 index += 1
282 273
283 return note2slot, slot2note 274 return note2slot, slot2note
284 275
285 def emitnote(self, pdfs, slot, start, notelen): 276 def emitnote(self, ev):
277 orignote = ev.note
278 start = self.channels[ev.channel][orignote]
279 evw = EVWrap(ev)
280 # Find a slot (plus adjust pitch, attempt transposition etc)
281 if hasattr(ev, 'note'):
282 self.getslotfornote(evw)
283 # Check if it was unplayable
284 if evw.slot == None:
285 return orignote
286 notelen = self.ctime - start
287 print 'Note %s (%d) at %.2f length %.2f' % (evw.notename, evw.slot, start, notelen)
286 x = start * self.timescale + self.leadin # Convert start time to distance 288 x = start * self.timescale + self.leadin # Convert start time to distance
287 pdfidx = int(x / self.pdfwidth) # Determine which pdf 289 pdfidx = int(x / self.pdfwidth) # Determine which pdf
288 x = x % self.pdfwidth # and where on that pdf 290 x = x % self.pdfwidth # and where on that pdf
289 h = self.slotsize 291 h = self.slotsize
290 y = self.pageheight - (self.heel + slot * self.pitch - self.slotsize / 2) - self.slotsize 292 y = self.pageheight - (self.heel + evw.slot * self.pitch - self.slotsize / 2) - self.slotsize
291 w = notelen * self.timescale # Convert note length in time to distance 293 w = notelen * self.timescale # Convert note length in time to distance
292 294
293 #print 'pdf = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (pdfidx, x, y, w, h) 295 #print 'pdf = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (pdfidx, x, y, w, h)
294 w1 = w 296 w1 = w
295 # Check if the note crosses a pdf 297 # Check if the note crosses a pdf
297 w1 = self.pdfwidth - x # Crop first note 299 w1 = self.pdfwidth - x # Crop first note
298 w2 = w - w1 # Calculate length of second note 300 w2 = w - w1 # Calculate length of second note
299 assert w2 <= self.pdfwidth, 'note extends for more than a pdf' 301 assert w2 <= self.pdfwidth, 'note extends for more than a pdf'
300 # Emit second half of note 302 # Emit second half of note
301 #print 'split note, pdf %d w2 = %.3f' % (pdfidx + 1, w2) 303 #print 'split note, pdf %d w2 = %.3f' % (pdfidx + 1, w2)
302 Midi2PDF._emitnote(pdfs[pdfidx + 1], self.pdfwidth - w2, y, w2, h) 304 Midi2PDF._emitnote(self.pdfs[pdfidx + 1], self.pdfwidth - w2, y, w2, h)
303 305
304 Midi2PDF._emitnote(pdfs[pdfidx], self.pdfwidth - x - w1, y, w1, h) 306 Midi2PDF._emitnote(self.pdfs[pdfidx], self.pdfwidth - x - w1, y, w1, h)
307 return orignote
305 308
306 @staticmethod 309 @staticmethod
307 def _emitnote(pdf, x, y, w, h): 310 def _emitnote(pdf, x, y, w, h):
308 pdf.saveState() 311 pdf.saveState()
309 pdf.setStrokeColor(CUT_COLOUR) 312 pdf.setStrokeColor(CUT_COLOUR)