# HG changeset patch # User Daniel O'Connor # Date 1459600608 -37800 # Node ID 31db42ce72b8b44f6ca6e9ff8a7259620854d95f # Parent 2de05d714e5c617e9e0b89c4e444aec1cfcf9fa3 Rework to be class-y Need to handle page bridging notes. diff -r 2de05d714e5c -r 31db42ce72b8 musiccutter.py --- 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()