Mercurial > ~darius > hgwebdir.cgi > musiccutter
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) |