diff 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
line wrap: on
line diff
--- a/musiccutter.py	Tue Mar 08 00:49:54 2016 +1030
+++ b/musiccutter.py	Sat Apr 02 23:06:48 2016 +1030
@@ -2,112 +2,133 @@
 
 import exceptions
 import itertools
+import math
 import mido
 import svgwrite
 import sys
 
 def test(filename = None):
-    # http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf
-    conf = { 'notefile' : 'notes', 'pagewidth' : 20, 'pageheight' : 15.5,
-             'pitch' : 0.55, 'offset' : 0.60, 'timescale' : 1.0 }
-    midi2note, note2midi = genmidi2note()
-    note2slot = loadnote2slot(conf['notefile'], note2midi)
     if filename == None:
         filename = 'test.midi'
-    midi2svg(filename, 'test%02d.svg', midi2note, note2midi, note2slot, conf['pagewidth'], conf['pageheight'],
-        conf['pitch'], conf['offset'], conf['timescale'])
+    # Card layout from http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf
+    m = Midi2SVG('notes', 20, 15.5, 0.55, 0.6, 1.0)
+    m.processMidi(filename, 'test%02d.svg')
 
-# http://www.electronics.dit.ie/staff/tscarff/Music_technology/midi/midi_note_numbers_for_octaves.htm
-def genmidi2note():
-    '''Create forward & reverse tables for midi number to note name (assuming 69 == A440)'''
-    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']
-    midi2note = {}
-    note2midi = {}
-    for midi in range(128):
-        octave = midi / len(names)
-        index = midi % len(names)
-        name = names[index] % (octave)
-        midi2note[midi] = name
-        note2midi[name] = midi
-
-    return midi2note, note2midi
-
-def loadnote2slot(fname, note2midi):
-    svg = svgwrite.Drawing(fname)
+class Midi2SVG(object):
+    def __init__(self, notefile, pagewidth, pageheight, pitch, offset, timescale):
+        self.midi2note, self.note2midi = Midi2SVG.genmidi2note()
+        self.note2slot = Midi2SVG.loadnote2slot(notefile, self.note2midi)
+        self.pagewidth = pagewidth
+        self.pageheight = pageheight
+        self.pitch = pitch
+        self.offset = offset
+        self.timescale = timescale
 
-    note2slot = {}
-    index = 0
-
-    for note in file(fname):
-        note = note.strip()
-        if note[0] == '#':
-            continue
-        if note not in note2midi:
-            raise exceptions.ValueError('Note \'%s\' not valid' % note)
-        note2slot[note] = index
-        index += 1
+    def processMidi(self, midifile, outpat):
+        playablecount = 0
+        unplayablecount = 0
+        midi = mido.MidiFile(midifile)
+        ctime = 0
+        channels = []
+        for i in range(16):
+            channels.append({})
 
-    return note2slot
-
-def emitnote(svg, slot, start, notelen, pagewidth, pageheight, pitch, offset, timescale):
-    x = start / timescale
-    y = pageheight - (offset + slot * pitch)
-    w = notelen / timescale
-    h = pitch
-    print 'x = %.3f y = %.3f w = %.3f h = %.3f' % (x, y, w, h)
-    svg.add(svgwrite.shapes.Rect(insert = ('%.3fcm' % (x), '%.3fcm' % (y)),
-                                 size = ('%.3fcm' % (w), '%.3fcm' % (h)),
-                                stroke = 'red', stroke_width = '1px', fill = 'red'))
+        npages = int(math.ceil(midi.length / self.timescale / self.pagewidth))
+        print 'npages', npages
+        svgs = []
+        for i in range(npages):
+            svg = svgwrite.Drawing(outpat % i, profile = 'full', size = ('%.3fcm' % (self.pagewidth), '%.3fcm' % (self.pageheight)))
+            svgs.append(svg)
 
-def midi2svg(inf, outpat, midi2note, note2midi, note2slot, pagewidth, pageheight, pitch, offset, timescale):
-    playablecount = 0
-    unplayablecount = 0
-    midi = mido.MidiFile(inf)
-    ctime = 0
-    channels = []
-    svg = svgwrite.Drawing(outpat % (0), size = ('%.3fcm' % (pagewidth), '%.3fcm' % (pageheight)))
-    for i in range(16):
-        channels.append({})
-
-    pages = []
-    for ev in midi:
-        ctime += ev.time
-        #print ctime, ev
-        if ev.type == 'note_on' and ev.velocity > 0:
-            if ev.note in channels[ev.channel]:
-                print 'Duplicate note_on message %d (%s)' % (ev.note, midi2note[ev.note])
-            else:
-                channels[ev.channel][ev.note] = ctime
-        elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0):
-                if ev.note not in channels[ev.channel]:
-                    print 'note_off with no corresponding note_on for channel %d note %d' % (ev.channel, ev.note)
+        for ev in midi:
+            ctime += ev.time
+            if ev.type == 'note_on' or ev.type == 'note_off':
+                note = self.midi2note[ev.note]
+            print ctime, ev
+            if ev.type == 'note_on' and ev.velocity > 0:
+                if ev.note in channels[ev.channel]:
+                    print 'Duplicate note_on message %d (%s)' % (ev.note, note)
                 else:
-                    note = midi2note[ev.note]
-                    if note not in note2slot:
-                        print 'Skipping unplayable note %s' % (note)
-                        unplayablecount += 1
+                    channels[ev.channel][ev.note] = ctime
+            elif ev.type == 'note_off' or (ev.type == 'note_on' and ev.velocity == 0):
+                    if ev.note not in channels[ev.channel]:
+                        print 'note_off with no corresponding note_on for channel %d note %d' % (ev.channel, ev.note)
                     else:
-                        start = channels[ev.channel][ev.note]
-                        notelen = ctime - start
-                        slot = note2slot[note]
-                        #print 'Note %s (%d) at %d length %d' % (note, slot, start, notelen)
-                        emitnote(svg, slot, start, notelen, pagewidth, pageheight, pitch, offset, timescale)
-                        playablecount += 1
-                    del channels[ev.channel][ev.note]
-        elif ev.type == 'end_of_track':
-            print 'EOT, not flushing, check for missed notes'
-            for chan in channels:
-                for ev in chan:
-                    print ev
+                        if note not in self.note2slot:
+                            print 'Skipping unplayable note %s' % (note)
+                            unplayablecount += 1
+                        else:
+                            start = channels[ev.channel][ev.note]
+                            notelen = ctime - start
+                            slot = self.note2slot[note]
+                            print 'Note %s (%d) at %.2f length %.2f' % (note, slot, start, notelen)
+                            self.emitnote(svgs, slot, start, notelen)
+                            playablecount += 1
+                        del channels[ev.channel][ev.note]
+            elif ev.type == 'end_of_track':
+                print 'EOT, not flushing, check for missed notes'
+                for chan in channels:
+                    for ev in chan:
+                        print ev
+
+        print 'Playable count:', playablecount
+        print 'Unplayable count:', unplayablecount
+
+        for svg in svgs:
+            svg.save()
+
+    # http://www.electronics.dit.ie/staff/tscarff/Music_technology/midi/midi_note_numbers_for_octaves.htm
+    @staticmethod
+    def genmidi2note():
+        '''Create forward & reverse tables for midi number to note name (assuming 69 == A440)'''
+        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']
+        midi2note = {}
+        note2midi = {}
+        for midi in range(128):
+            octave = midi / len(names)
+            index = midi % len(names)
+            name = names[index] % (octave)
+            midi2note[midi] = name
+            note2midi[name] = midi
 
-    npages = int(midi.length / timescale / pagewidth + 0.5)
-    print 'npages', npages
-    for i in range(npages):
-        svg.viewbox(pagewidth / 2.54 * 96.0 * i, 0, pagewidth / 2.54 * 96.0, pageheight / 2.54 * 96.0)
-        svg.saveas(outpat % i)
+        return midi2note, note2midi
+
+    @staticmethod
+    def loadnote2slot(fname, note2midi):
+        note2slot = {}
+        index = 0
+
+        for note in file(fname):
+            note = note.strip()
+            if note[0] == '#':
+                continue
+            if note not in note2midi:
+                raise exceptions.ValueError('Note \'%s\' not valid' % note)
+            note2slot[note] = index
+            index += 1
+
+        return note2slot
 
-    print 'Playable count:', playablecount
-    print 'Unplayable count:', unplayablecount
+    def emitnote(self, svgs, slot, start, notelen):
+        startx = start / self.timescale
+        startpageidx = int(startx / self.pagewidth)
+        endx = (start + notelen) / self.timescale
+        endpageidx = int(endx / self.pagewidth)
+        startx = startx % self.pagewidth
+        y = self.pageheight - (self.offset + slot * self.pitch)
+        w = notelen / self.timescale
+        h = self.pitch
+
+        if startpageidx != endpageidx:
+            print 'page crossed from %d to %d' % (startpageidx, endpageidx)
+        print 'page = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (startpageidx, startx, y, w, h)
+        Midi2SVG._emitnote(svgs[startpageidx], startx, y, w, h)
+
+    @staticmethod
+    def _emitnote(svg, x, y, w, h):
+        svg.add(svgwrite.shapes.Rect(insert = ('%.3fcm' % (x), '%.3fcm' % (y)),
+                                     size = ('%.3fcm' % (w), '%.3fcm' % (h)),
+                                    stroke = 'red', stroke_width = '1px', fill = 'red'))
 
 if __name__ == '__main__':
     main()