Mercurial > ~darius > hgwebdir.cgi > musiccutter
view musiccutter.py @ 38:9e8ed92b477c
Re-jig note translation to only happen when we are going to emit a
note.
This fixes "note_off with no note_on" cases (iWriteMusic likes to emit
these for rests).
This means some messages have untransposed notes but we draw the line
because they only have to be transposed because of limitations in the
organ so before that they are are untransposed (except for the bulk
adjustment)
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Mon, 23 May 2016 22:35:44 +0930 |
parents | c490fecec0ef |
children | 86622ba474e4 |
line wrap: on
line source
#!/usr/bin/env python from IPython.core.debugger import Tracer import exceptions import itertools import math import mido import os.path import reportlab.lib.colors import reportlab.pdfgen.canvas from reportlab.lib.units import mm import sys CUT_COLOUR = reportlab.lib.colors.red ENGRAVE_COLOUR = reportlab.lib.colors.black class Stats(object): pass class EVWrap(object): def __init__(self, ev): self.ev = ev def test(filename = None, shift = 0): if filename == None: filename = 'test.midi' # Card layout from http://www.orgues-de-barbarie.com/wp-content/uploads/2014/09/format-cartons.pdf # Notes are read from right to left # Highest note is at the bottom (closest to the crank) # fold fold # in out # V ^ # +---+---+---+ lowest note # | | | | # +---+---+---+ highest note # m = Midi2PDF('notes', 120, 155, 5.5, 3.3, 6.0, 50, False, True, False, False, False, shift, 1, 30, 0.9, 'Helvetica', 12) base, ext = os.path.splitext(filename) base = os.path.basename(base) m.processMidi(filename, base + '-%02d.pdf') class Midi2PDF(object): def __init__(self, notefile, pagewidth, pageheight, pitch, slotsize, heel, leadin, timemarks, trytranspose, drawrect, notenames, notelines, noteoffset, pagesperpdf, timescale, notescale, fontname, fontsize): self.midi2note, self.note2midi = Midi2PDF.genmidi2note() self.note2slot, self.slot2note = Midi2PDF.loadnote2slot(notefile, self.note2midi) self.pagewidth = pagewidth # Dimensions are in millimetres self.pageheight = pageheight self.pitch = pitch # Distance between each slot self.slotsize = slotsize # Size of each slot cut out self.heel = heel # Bottom margin (from bottom of page to centre of slot) self.leadin = leadin # Extra at the start self.timemarks = timemarks # Draw vertical time lines self.trytranspose = trytranspose # Attempt to tranpose unplayable notes self.drawrect = drawrect # Draw rectangle around each page self.notenames = notenames # Draw note names on the right edge self.notelines = notelines # Draw line rulers self.noteoffset = noteoffset # Amount to adjust note pitches by (+12 = 1 octave) self.pagesperpdf = pagesperpdf # Number of pages to emit per PDF self.timescale = timescale # Width per second self.notescale = notescale # Multiply all note lengths by this (to get rearticulation) self.fontname = fontname self.fontsize = fontsize # Points self.pdfwidth = self.pagewidth * self.pagesperpdf def processMidi(self, midifile, outpat): stats = Stats() stats.playablecount = 0 stats.unplayablecount = 0 stats.transposeupcount = 0 stats.transposedowncount = 0 midi = mido.MidiFile(midifile) ctime = 0 channels = [] for i in range(16): channels.append({}) npages = int(math.ceil(((midi.length * self.timescale) + self.leadin) / self.pagewidth)) npdfs = int(math.ceil(float(npages) / self.pagesperpdf)) print 'npages %d, npdfs %d' % (npages, npdfs) pdfs = [] for i in range(npdfs): pdf = reportlab.pdfgen.canvas.Canvas(file(outpat % (i + 1), 'w'), pagesize = (self.pdfwidth * mm, self.pageheight * mm)) pdfs.append(pdf) title = os.path.basename(midifile) title, ext = os.path.splitext(title) for ev in midi: # Adjust pitch if hasattr(ev, 'note'): ev.note += self.noteoffset ctime += ev.time #print ctime, ev #Tracer()() if ev.type == 'text' and ctime == 0: title = ev.text if ev.type == 'note_on' and ev.velocity > 0: if ev.note in channels[ev.channel]: print 'Duplicate note_on message at %.1f sec channel %d note %d' % (ctime, ev.channel, 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 at %.1f sec for channel %d note %d' % (ctime, ev.channel, ev.note) continue else: orignote = ev.note start = channels[ev.channel][orignote] evw = EVWrap(ev) # Find a slot (plus adjust pitch, attempt transposition etc) if hasattr(ev, 'note'): self.getslotfornote(evw, stats, ctime) # Check if it was unplayable if not evw.slot: continue notelen = ctime - start #print 'Note %s (%d) at %.2f length %.2f' % (evw.notename, ev.slot, start, notelen) self.emitnote(pdfs, evw.slot, start, notelen * self.notescale) del channels[ev.channel][orignote] 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:', stats.playablecount print 'Unplayable count:', stats.unplayablecount if self.trytranspose: print 'Transpose down:', stats.transposedowncount print 'Transpose up:', stats.transposeupcount for pindx in range(npages): pdf = pdfs[pindx / self.pagesperpdf] # PDF for this page # Offset into PDF where the page starts pageofs = self.pagewidth * (self.pagesperpdf - (pindx % self.pagesperpdf) - 1) # Add title and page number Midi2PDF.textHelper(pdf, pageofs * mm, 1 * mm, ENGRAVE_COLOUR, True, self.fontname, self.fontsize, '%s (%d / %d)' % (title, pindx + 1, npages)) pdf.saveState() pdf.setLineWidth(0) # Draw time marks every 5 seconds if self.timemarks: pdfidx = pindx / self.pagesperpdf # Work out start and end times (pdf 1 is special due to leadin) tstart = self.leadin / self.timescale tend = (self.pagewidth * self.pagesperpdf) / self.timescale if pindx > 0: tsize = self.pagewidth / self.timescale # Amount of time per pdf tstart = tend + tsize * pdfidx tend = tend + tsize * (pdfidx + 1) for s in range(tstart, tend, 5): x = self.pagewidth - (float(s * self.timescale + self.leadin) % self.pagewidth) pdf.line(x * mm, self.heel, x * mm, self.pageheight * mm) Midi2PDF.textHelper(pdf, x * mm, 1 * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, str(s) + 's') # Draw rectangle around page (upper and right hand ends don't seem to render though) if self.drawrect: pdf.rect((pindx % self.pagesperpdf) * self.pagewidth * mm, 0, self.pagewidth * mm, self.pageheight * mm, fill = False, stroke = True) # Draw lines per note for slot in sorted(self.slot2note.keys()): ofs = self.pageheight - (self.heel + slot * self.pitch) - self.slotsize / 2 if self.notelines: pdf.line(0, ofs * mm, self.pdfwidth * mm, ofs * mm) # Note name if self.notenames: Midi2PDF.textHelper(pdf, (self.pdfwidth - 10) * mm, (ofs + 0.5) * mm, ENGRAVE_COLOUR, False, self.fontname, self.fontsize, self.slot2note[slot]) pdf.restoreState() for pdf in pdfs: pdf.save() def noteisplayable(self, midi): slot = None if midi in self.midi2note: notename = self.midi2note[midi] if notename in self.note2slot: slot = self.note2slot[notename] return slot def transposenote(self, evw, amount): evw.ev.note += amount evw.notename = self.midi2note[evw.ev.note] evw.slot = self.note2slot[evw.notename] def getslotfornote(self, evw, stats, ctime): evw.slot = None evw.notename = None # First off, is the note in our midi table? if evw.ev.note in self.midi2note: evw.notename = self.midi2note[evw.ev.note] # Is it playable? if self.noteisplayable(evw.ev.note) != None: evw.slot = self.note2slot[evw.notename] # Nope, maybe we can transpose? elif self.trytranspose: # Go for -3 to +3 octaves (going down first) for i in [-12, -24, -36, 12, 24, 36]: if self.noteisplayable(evw.ev.note + i) != None: self.transposenote(evw, i) if i < 0: stats.transposedowncount += 1 tmp = 'down' else: stats.transposeupcount += 1 tmp = 'up' print 'Transposed note at %.1f sec %s (%d) %s %d octave(s) to %s (%d)' % ( ctime, self.midi2note[evw.ev.note - i], evw.ev.note - i, tmp, abs(i / 12), evw.notename, evw.ev.note) break if evw.slot != None: stats.playablecount += 1 else: print 'Note at %.1f sec %d (%s) not playable' % (ctime, evw.ev.note, self.midi2note[evw.ev.note]) stats.unplayablecount += 1 else: print 'Note at %.1f sec, %d not in MIDI table' % (ctime, evw.ev.note) stats.unplayablecount += 1 # http://newt.phys.unsw.edu.au/jw/notes.html # But this seems dumb since the lowest MIDI note is 0 which would be C-1.. @staticmethod def genmidi2note(): '''Create forward & reverse tables for midi number to note name (assuming 69 = A4 = 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(12, 128): octave = midi / len(names) - 1 index = midi % len(names) name = names[index] % (octave) midi2note[midi] = name note2midi[name] = midi return midi2note, note2midi @staticmethod def loadnote2slot(fname, note2midi): note2slot = {} slot2note = {} 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 slot2note[index] = note index += 1 return note2slot, slot2note def emitnote(self, pdfs, slot, start, notelen): x = start * self.timescale + self.leadin # Convert start time to distance pdfidx = int(x / self.pdfwidth) # Determine which pdf x = x % self.pdfwidth # and where on that pdf h = self.slotsize y = self.pageheight - (self.heel + slot * self.pitch - self.slotsize / 2) - self.slotsize w = notelen * self.timescale # Convert note length in time to distance #print 'pdf = %d x = %.3f y = %.3f w = %.3f h = %.3f' % (pdfidx, x, y, w, h) w1 = w # Check if the note crosses a pdf if x + w > self.pdfwidth: w1 = self.pdfwidth - x # Crop first note w2 = w - w1 # Calculate length of second note assert w2 <= self.pdfwidth, 'note extends for more than a pdf' # Emit second half of note #print 'split note, pdf %d w2 = %.3f' % (pdfidx + 1, w2) Midi2PDF._emitnote(pdfs[pdfidx + 1], self.pdfwidth - w2, y, w2, h) Midi2PDF._emitnote(pdfs[pdfidx], self.pdfwidth - x - w1, y, w1, h) @staticmethod def _emitnote(pdf, x, y, w, h): pdf.saveState() pdf.setStrokeColor(CUT_COLOUR) pdf.setLineWidth(0) pdf.rect(x * mm, y * mm, w * mm, h * mm, fill = False, stroke = True) pdf.restoreState() @staticmethod def textHelper(pdf, x, y, colour, fill, fontname, fontsize, text): tobj = pdf.beginText() tobj.setTextOrigin(x, y) tobj.setFont(fontname, fontsize) tobj.setStrokeColor(colour) tobj.setFillColor(colour) if fill: tobj.setTextRenderMode(0) else: tobj.setTextRenderMode(1) tobj.textLine(text) pdf.drawText(tobj) if __name__ == '__main__': main()