comparison musiccutter.py @ 37:c490fecec0ef

Have another crack at fixing transposition. Now searches down and up 3 octaves. Refactor a bunch of code.
author Daniel O'Connor <darius@dons.net.au>
date Mon, 23 May 2016 21:11:27 +0930
parents 6874140c9c11
children 9e8ed92b477c
comparison
equal deleted inserted replaced
36:6874140c9c11 37:c490fecec0ef
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 from IPython.core.debugger import Tracer
3 import exceptions 4 import exceptions
4 import itertools 5 import itertools
5 import math 6 import math
6 import mido 7 import mido
7 import os.path 8 import os.path
11 import sys 12 import sys
12 13
13 CUT_COLOUR = reportlab.lib.colors.red 14 CUT_COLOUR = reportlab.lib.colors.red
14 ENGRAVE_COLOUR = reportlab.lib.colors.black 15 ENGRAVE_COLOUR = reportlab.lib.colors.black
15 16
16 def test(filename = None): 17 class Stats(object):
18 pass
19
20 class EVWrap(object):
21 def __init__(self, ev):
22 self.ev = ev
23
24 def test(filename = None, shift = 0):
17 if filename == None: 25 if filename == None:
18 filename = 'test.midi' 26 filename = 'test.midi'
19 # Card layout from http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf 27 # Card layout from http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf
20 # Notes are read from right to left 28 # Notes are read from right to left
21 # Highest note is at the bottom (closest to the crank) 29 # Highest note is at the bottom (closest to the crank)
24 # V ^ 32 # V ^
25 # +---+---+---+ lowest note 33 # +---+---+---+ lowest note
26 # | | | | 34 # | | | |
27 # +---+---+---+ highest note 35 # +---+---+---+ highest note
28 # 36 #
29 m = Midi2PDF('notes', 120, 155, 5.5, 3.3, 6.0, 50, False, False, False, False, False, 0, 30, 0.9, 'Helvetica', 12) 37 m = Midi2PDF('notes', 120, 155, 5.5, 3.3, 6.0, 50, False, True, False, False, False, shift, 1, 30, 0.9, 'Helvetica', 12)
30 base, ext = os.path.splitext(filename) 38 base, ext = os.path.splitext(filename)
31 base = os.path.basename(base) 39 base = os.path.basename(base)
32 m.processMidi(filename, base + '-%02d.pdf') 40 m.processMidi(filename, base + '-%02d.pdf')
33 41
34 class Midi2PDF(object): 42 class Midi2PDF(object):
54 self.fontsize = fontsize # Points 62 self.fontsize = fontsize # Points
55 63
56 self.pdfwidth = self.pagewidth * self.pagesperpdf 64 self.pdfwidth = self.pagewidth * self.pagesperpdf
57 65
58 def processMidi(self, midifile, outpat): 66 def processMidi(self, midifile, outpat):
59 playablecount = 0 67 stats = Stats()
60 unplayablecount = 0 68 stats.playablecount = 0
61 transposeupcount = 0 69 stats.unplayablecount = 0
62 transposedowncount = 0 70 stats.transposeupcount = 0
71 stats.transposedowncount = 0
63 midi = mido.MidiFile(midifile) 72 midi = mido.MidiFile(midifile)
64 ctime = 0 73 ctime = 0
65 channels = [] 74 channels = []
66 for i in range(16): 75 for i in range(16):
67 channels.append({}) 76 channels.append({})
77 pdfs.append(pdf) 86 pdfs.append(pdf)
78 87
79 title = os.path.basename(midifile) 88 title = os.path.basename(midifile)
80 title, ext = os.path.splitext(title) 89 title, ext = os.path.splitext(title)
81 for ev in midi: 90 for ev in midi:
82 # Adjust pitch of note (if it's a note) 91 evw = EVWrap(ev)
83 if hasattr(ev, 'note'): 92 # Find a slot (plus adjust pitch, attempt transposition etc)
84 ev.note += self.noteoffset 93 if hasattr(evw.ev, 'note'):
85 if ev.type == 'text' and ctime == 0: 94 self.getslotfornote(evw, stats, ctime)
86 title = ev.text 95 # Check if it was unplayable
87 96 if not evw.slot:
88 ctime += ev.time
89 if ev.type == 'note_on' or ev.type == 'note_off':
90 if ev.note not in self.midi2note:
91 print 'Input MIDI number %d out of range' % (ev.note)
92 unplayablecount += 1
93 continue 97 continue
98 if evw.ev.type == 'text' and ctime == 0:
99 title = evw.ev.text
100
101 ctime += evw.ev.time
102 #print ctime, evw.ev
103 if evw.ev.type == 'note_on' and evw.ev.velocity > 0:
104 if evw.ev.note in channels[evw.ev.channel]:
105 print 'Duplicate note_on message %d (%s)' % (evw.ev.note, evw.notename)
94 else: 106 else:
95 note = self.midi2note[ev.note] 107 channels[evw.ev.channel][evw.ev.note] = ctime
96 #print ctime, ev 108 elif evw.ev.type == 'note_off' or (evw.ev.type == 'note_on' and evw.ev.velocity == 0):
97 if ev.type == 'note_on' and ev.velocity > 0: 109 if evw.ev.note not in channels[evw.ev.channel]:
98 if ev.note in channels[ev.channel]: 110 print 'note_off with no corresponding note_on for channel %d note %d' % (evw.ev.channel, evw.ev.note)
99 print 'Duplicate note_on message %d (%s)' % (ev.note, note)
100 else: 111 else:
101 channels[ev.channel][ev.note] = ctime 112 start = channels[evw.ev.channel][evw.ev.note]
102 elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0): 113 notelen = ctime - start
103 if ev.note not in channels[ev.channel]: 114 #print 'Note %s (%d) at %.2f length %.2f' % (evw.notename, evw.ev.slot, start, notelen)
104 print 'note_off with no corresponding note_on for channel %d note %d' % (ev.channel, ev.note) 115 self.emitnote(pdfs, evw.slot, start, notelen * self.notescale)
105 else: 116
106 start = channels[ev.channel][ev.note] 117 del channels[evw.ev.channel][evw.ev.note]
107 notelen = ctime - start 118 elif evw.ev.type == 'end_of_track':
108 playable = True
109
110 if note in self.note2slot:
111 slot = self.note2slot[note]
112 elif self.trytranspose and ((ev.note - 12) in self.midi2note and self.midi2note[ev.note - 12] in self.note2slot):
113 print 'Transposing note %d (%s) down' % (ev.note, note)
114 slot = self.note2slot[self.midi2note[ev.note - 12]]
115 transposedowncount += 1
116 elif self.trytranspose and (ev.note + 12 in self.midi2note and self.midi2note[ev.note + 12] in self.note2slot):
117 print 'Transposing note %d (%s) up' % (ev.note, note)
118 slot = self.note2slot[self.midi2note[ev.note + 12]]
119 transposeupcount += 1
120 else:
121 unplayablecount += 1
122 playable = False
123
124 if playable:
125 #print 'Note %s (%d) at %.2f length %.2f' % (note, slot, start, notelen)
126 self.emitnote(pdfs, slot, start, notelen * self.notescale)
127 playablecount += 1
128
129 del channels[ev.channel][ev.note]
130 elif ev.type == 'end_of_track':
131 print 'EOT, not flushing, check for missed notes' 119 print 'EOT, not flushing, check for missed notes'
132 for chan in channels: 120 for chan in channels:
133 for ev in chan: 121 for evw.ev in chan:
134 print ev 122 print evw.ev
135 123
136 print 'Playable count:', playablecount 124 print 'Playable count:', stats.playablecount
137 print 'Unplayable count:', unplayablecount 125 print 'Unplayable count:', stats.unplayablecount
138 if self.trytranspose: 126 if self.trytranspose:
139 print 'Transpose down:', transposedowncount 127 print 'Transpose down:', stats.transposedowncount
140 print 'Transpose up:', transposeupcount 128 print 'Transpose up:', stats.transposeupcount
141 129
142 for pindx in range(npages): 130 for pindx in range(npages):
143 pdf = pdfs[pindx / self.pagesperpdf] # PDF for this page 131 pdf = pdfs[pindx / self.pagesperpdf] # PDF for this page
144 # Offset into PDF where the page starts 132 # Offset into PDF where the page starts
145 pageofs = self.pagewidth * (self.pagesperpdf - (pindx % self.pagesperpdf) - 1) 133 pageofs = self.pagewidth * (self.pagesperpdf - (pindx % self.pagesperpdf) - 1)
180 Midi2PDF.textHelper(pdf, (self.pdfwidth - 10) * mm, (ofs + 0.5) * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, self.slot2note[slot]) 168 Midi2PDF.textHelper(pdf, (self.pdfwidth - 10) * mm, (ofs + 0.5) * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, self.slot2note[slot])
181 pdf.restoreState() 169 pdf.restoreState()
182 for pdf in pdfs: 170 for pdf in pdfs:
183 pdf.save() 171 pdf.save()
184 172
173 def noteisplayable(self, midi):
174 slot = None
175 if midi in self.midi2note:
176 notename = self.midi2note[midi]
177 if notename in self.note2slot:
178 slot = self.note2slot[notename]
179
180 return slot
181
182 def transposenote(self, evw, amount):
183 evw.ev.note += amount
184 evw.notename = self.midi2note[evw.ev.note]
185 evw.slot = self.note2slot[evw.notename]
186
187 def getslotfornote(self, evw, stats, ctime):
188 evw.slot = None
189 evw.notename = None
190
191 evw.ev.note += self.noteoffset
192
193 # First off, is the note in our midi table?
194 if evw.ev.note in self.midi2note:
195 evw.notename = self.midi2note[evw.ev.note]
196 # Is it playable?
197 if self.noteisplayable(evw.ev.note) != None:
198 evw.slot = self.note2slot[evw.notename]
199 # Nope, maybe we can transpose?
200 elif self.trytranspose:
201 # Go for -3 to +3 octaves (going down first)
202 for i in [-12, -24, -36, 12, 24, 36]:
203 if self.noteisplayable(evw.ev.note + i) != None:
204 self.transposenote(evw, i)
205 if i < 0:
206 stats.transposedowncount += 1
207 tmp = 'down'
208 else:
209 stats.transposeupcount += 1
210 tmp = 'up'
211 print 'Transposed note at %.1f sec %s (%d) %s %d octave(s) to %s (%d)' % (
212 ctime, self.midi2note[evw.ev.note - i], evw.ev.note - i, tmp,
213 abs(i / 12), evw.notename, evw.ev.note)
214 break
215 if evw.slot != None:
216 stats.playablecount += 1
217 else:
218 print 'Note at %.1f sec %d (%s) not playable' % (ctime, evw.ev.note, self.midi2note[evw.ev.note])
219 stats.unplayablecount += 1
220 else:
221 print 'Note at %.1f sec %d not in MIDI table' % (ctime, evw.ev.note)
222 stats.unplayablecount += 1
223
185 # http://newt.phys.unsw.edu.au/jw/notes.html 224 # http://newt.phys.unsw.edu.au/jw/notes.html
186 @staticmethod 225 @staticmethod
187 def genmidi2note(): 226 def genmidi2note():
188 '''Create forward & reverse tables for midi number to note name (assuming 69 = A4 = A440)''' 227 '''Create forward & reverse tables for midi number to note name (assuming 69 = A4 = A440)'''
189 names = ['C%d', 'C%d#', 'D%d', 'D%d#', 'E%d', 'F%d', 'F%d#', 'G%d', 'G%d#', 'A%d', 'A%d#', 'B%d'] 228 names = ['C%d', 'C%d#', 'D%d', 'D%d#', 'E%d', 'F%d', 'F%d#', 'G%d', 'G%d#', 'A%d', 'A%d#', 'B%d']
222 x = x % self.pdfwidth # and where on that pdf 261 x = x % self.pdfwidth # and where on that pdf
223 h = self.slotsize 262 h = self.slotsize
224 y = self.pageheight - (self.heel + slot * self.pitch - self.slotsize / 2) - self.slotsize 263 y = self.pageheight - (self.heel + slot * self.pitch - self.slotsize / 2) - self.slotsize
225 w = notelen * self.timescale # Convert note length in time to distance 264 w = notelen * self.timescale # Convert note length in time to distance
226 265
227 print 'pdf = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (pdfidx, x, y, w, h) 266 #print 'pdf = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (pdfidx, x, y, w, h)
228 w1 = w 267 w1 = w
229 # Check if the note crosses a pdf 268 # Check if the note crosses a pdf
230 if x + w > self.pdfwidth: 269 if x + w > self.pdfwidth:
231 w1 = self.pdfwidth - x # Crop first note 270 w1 = self.pdfwidth - x # Crop first note
232 w2 = w - w1 # Calculate length of second note 271 w2 = w - w1 # Calculate length of second note
233 assert w2 <= self.pdfwidth, 'note extends for more than a pdf' 272 assert w2 <= self.pdfwidth, 'note extends for more than a pdf'
234 # Emit second half of note 273 # Emit second half of note
235 print 'split note, pdf %d w2 = %.3f' % (pdfidx + 1, w2) 274 #print 'split note, pdf %d w2 = %.3f' % (pdfidx + 1, w2)
236 Midi2PDF._emitnote(pdfs[pdfidx + 1], self.pdfwidth - w2, y, w2, h) 275 Midi2PDF._emitnote(pdfs[pdfidx + 1], self.pdfwidth - w2, y, w2, h)
237 276
238 Midi2PDF._emitnote(pdfs[pdfidx], self.pdfwidth - x - w1, y, w1, h) 277 Midi2PDF._emitnote(pdfs[pdfidx], self.pdfwidth - x - w1, y, w1, h)
239 278
240 @staticmethod 279 @staticmethod