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