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