comparison musiccutter.py @ 7:31db42ce72b8

Rework to be class-y Need to handle page bridging notes.
author Daniel O'Connor <darius@dons.net.au>
date Sat, 02 Apr 2016 23:06:48 +1030
parents af683606184e
children f07a997e9f79
comparison
equal deleted inserted replaced
6:2de05d714e5c 7:31db42ce72b8
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 import exceptions 3 import exceptions
4 import itertools 4 import itertools
5 import math
5 import mido 6 import mido
6 import svgwrite 7 import svgwrite
7 import sys 8 import sys
8 9
9 def test(filename = None): 10 def test(filename = None):
10 # http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf
11 conf = { 'notefile' : 'notes', 'pagewidth' : 20, 'pageheight' : 15.5,
12 'pitch' : 0.55, 'offset' : 0.60, 'timescale' : 1.0 }
13 midi2note, note2midi = genmidi2note()
14 note2slot = loadnote2slot(conf['notefile'], note2midi)
15 if filename == None: 11 if filename == None:
16 filename = 'test.midi' 12 filename = 'test.midi'
17 midi2svg(filename, 'test%02d.svg', midi2note, note2midi, note2slot, conf['pagewidth'], conf['pageheight'], 13 # Card layout from http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf
18 conf['pitch'], conf['offset'], conf['timescale']) 14 m = Midi2SVG('notes', 20, 15.5, 0.55, 0.6, 1.0)
15 m.processMidi(filename, 'test%02d.svg')
19 16
20 # http://www.electronics.dit.ie/staff/tscarff/Music_technology/midi/midi_note_numbers_for_octaves.htm 17 class Midi2SVG(object):
21 def genmidi2note(): 18 def __init__(self, notefile, pagewidth, pageheight, pitch, offset, timescale):
22 '''Create forward & reverse tables for midi number to note name (assuming 69 == A440)''' 19 self.midi2note, self.note2midi = Midi2SVG.genmidi2note()
23 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'] 20 self.note2slot = Midi2SVG.loadnote2slot(notefile, self.note2midi)
24 midi2note = {} 21 self.pagewidth = pagewidth
25 note2midi = {} 22 self.pageheight = pageheight
26 for midi in range(128): 23 self.pitch = pitch
27 octave = midi / len(names) 24 self.offset = offset
28 index = midi % len(names) 25 self.timescale = timescale
29 name = names[index] % (octave)
30 midi2note[midi] = name
31 note2midi[name] = midi
32 26
33 return midi2note, note2midi 27 def processMidi(self, midifile, outpat):
28 playablecount = 0
29 unplayablecount = 0
30 midi = mido.MidiFile(midifile)
31 ctime = 0
32 channels = []
33 for i in range(16):
34 channels.append({})
34 35
35 def loadnote2slot(fname, note2midi): 36 npages = int(math.ceil(midi.length / self.timescale / self.pagewidth))
36 svg = svgwrite.Drawing(fname) 37 print 'npages', npages
38 svgs = []
39 for i in range(npages):
40 svg = svgwrite.Drawing(outpat % i, profile = 'full', size = ('%.3fcm' % (self.pagewidth), '%.3fcm' % (self.pageheight)))
41 svgs.append(svg)
37 42
38 note2slot = {} 43 for ev in midi:
39 index = 0 44 ctime += ev.time
45 if ev.type == 'note_on' or ev.type == 'note_off':
46 note = self.midi2note[ev.note]
47 print ctime, ev
48 if ev.type == 'note_on' and ev.velocity > 0:
49 if ev.note in channels[ev.channel]:
50 print 'Duplicate note_on message %d (%s)' % (ev.note, note)
51 else:
52 channels[ev.channel][ev.note] = ctime
53 elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0):
54 if ev.note not in channels[ev.channel]:
55 print 'note_off with no corresponding note_on for channel %d note %d' % (ev.channel, ev.note)
56 else:
57 if note not in self.note2slot:
58 print 'Skipping unplayable note %s' % (note)
59 unplayablecount += 1
60 else:
61 start = channels[ev.channel][ev.note]
62 notelen = ctime - start
63 slot = self.note2slot[note]
64 print 'Note %s (%d) at %.2f length %.2f' % (note, slot, start, notelen)
65 self.emitnote(svgs, slot, start, notelen)
66 playablecount += 1
67 del channels[ev.channel][ev.note]
68 elif ev.type == 'end_of_track':
69 print 'EOT, not flushing, check for missed notes'
70 for chan in channels:
71 for ev in chan:
72 print ev
40 73
41 for note in file(fname): 74 print 'Playable count:', playablecount
42 note = note.strip() 75 print 'Unplayable count:', unplayablecount
43 if note[0] == '#':
44 continue
45 if note not in note2midi:
46 raise exceptions.ValueError('Note \'%s\' not valid' % note)
47 note2slot[note] = index
48 index += 1
49 76
50 return note2slot 77 for svg in svgs:
78 svg.save()
51 79
52 def emitnote(svg, slot, start, notelen, pagewidth, pageheight, pitch, offset, timescale): 80 # http://www.electronics.dit.ie/staff/tscarff/Music_technology/midi/midi_note_numbers_for_octaves.htm
53 x = start / timescale 81 @staticmethod
54 y = pageheight - (offset + slot * pitch) 82 def genmidi2note():
55 w = notelen / timescale 83 '''Create forward & reverse tables for midi number to note name (assuming 69 == A440)'''
56 h = pitch 84 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']
57 print 'x = %.3f y = %.3f w = %.3f h = %.3f' % (x, y, w, h) 85 midi2note = {}
58 svg.add(svgwrite.shapes.Rect(insert = ('%.3fcm' % (x), '%.3fcm' % (y)), 86 note2midi = {}
59 size = ('%.3fcm' % (w), '%.3fcm' % (h)), 87 for midi in range(128):
60 stroke = 'red', stroke_width = '1px', fill = 'red')) 88 octave = midi / len(names)
89 index = midi % len(names)
90 name = names[index] % (octave)
91 midi2note[midi] = name
92 note2midi[name] = midi
61 93
62 def midi2svg(inf, outpat, midi2note, note2midi, note2slot, pagewidth, pageheight, pitch, offset, timescale): 94 return midi2note, note2midi
63 playablecount = 0
64 unplayablecount = 0
65 midi = mido.MidiFile(inf)
66 ctime = 0
67 channels = []
68 svg = svgwrite.Drawing(outpat % (0), size = ('%.3fcm' % (pagewidth), '%.3fcm' % (pageheight)))
69 for i in range(16):
70 channels.append({})
71 95
72 pages = [] 96 @staticmethod
73 for ev in midi: 97 def loadnote2slot(fname, note2midi):
74 ctime += ev.time 98 note2slot = {}
75 #print ctime, ev 99 index = 0
76 if ev.type == 'note_on' and ev.velocity > 0:
77 if ev.note in channels[ev.channel]:
78 print 'Duplicate note_on message %d (%s)' % (ev.note, midi2note[ev.note])
79 else:
80 channels[ev.channel][ev.note] = ctime
81 elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0):
82 if ev.note not in channels[ev.channel]:
83 print 'note_off with no corresponding note_on for channel %d note %d' % (ev.channel, ev.note)
84 else:
85 note = midi2note[ev.note]
86 if note not in note2slot:
87 print 'Skipping unplayable note %s' % (note)
88 unplayablecount += 1
89 else:
90 start = channels[ev.channel][ev.note]
91 notelen = ctime - start
92 slot = note2slot[note]
93 #print 'Note %s (%d) at %d length %d' % (note, slot, start, notelen)
94 emitnote(svg, slot, start, notelen, pagewidth, pageheight, pitch, offset, timescale)
95 playablecount += 1
96 del channels[ev.channel][ev.note]
97 elif ev.type == 'end_of_track':
98 print 'EOT, not flushing, check for missed notes'
99 for chan in channels:
100 for ev in chan:
101 print ev
102 100
103 npages = int(midi.length / timescale / pagewidth + 0.5) 101 for note in file(fname):
104 print 'npages', npages 102 note = note.strip()
105 for i in range(npages): 103 if note[0] == '#':
106 svg.viewbox(pagewidth / 2.54 * 96.0 * i, 0, pagewidth / 2.54 * 96.0, pageheight / 2.54 * 96.0) 104 continue
107 svg.saveas(outpat % i) 105 if note not in note2midi:
106 raise exceptions.ValueError('Note \'%s\' not valid' % note)
107 note2slot[note] = index
108 index += 1
108 109
109 print 'Playable count:', playablecount 110 return note2slot
110 print 'Unplayable count:', unplayablecount 111
112 def emitnote(self, svgs, slot, start, notelen):
113 startx = start / self.timescale
114 startpageidx = int(startx / self.pagewidth)
115 endx = (start + notelen) / self.timescale
116 endpageidx = int(endx / self.pagewidth)
117 startx = startx % self.pagewidth
118 y = self.pageheight - (self.offset + slot * self.pitch)
119 w = notelen / self.timescale
120 h = self.pitch
121
122 if startpageidx != endpageidx:
123 print 'page crossed from %d to %d' % (startpageidx, endpageidx)
124 print 'page = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (startpageidx, startx, y, w, h)
125 Midi2SVG._emitnote(svgs[startpageidx], startx, y, w, h)
126
127 @staticmethod
128 def _emitnote(svg, x, y, w, h):
129 svg.add(svgwrite.shapes.Rect(insert = ('%.3fcm' % (x), '%.3fcm' % (y)),
130 size = ('%.3fcm' % (w), '%.3fcm' % (h)),
131 stroke = 'red', stroke_width = '1px', fill = 'red'))
111 132
112 if __name__ == '__main__': 133 if __name__ == '__main__':
113 main() 134 main()