#!/usr/pkg/bin/python Copyright = """ midi.py - library of routines to manipulate Standard MIDI files Copyright (C) 2003 John Comeau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ try: import sys, os, re, time from types import * except: sys.stderr.write("Needs os and re; upgrade preferably to version 2.3\n") sys.exit(1) # Global data # define boolean True on older Python interpreters (before 2.3) try: True except: True = 1 False = 0 Debugging = False # use 'options' for debugprint() (q.v.) LongLength = 4 ShortLength = 2 Octave = 12 # 12 half-notes make an octave Twelfth = 1.0 / 12 # note frequencies differ by twelfth root of 2 NormalVelocity = 64 # use for note off events, base all notes around this ByteMask = 0xffL # must be longword to avoid warnings about left shifts DeltaZero = chr(0) # delta-time of zero prefixes many MIDI events EndOfTrack = DeltaZero + "\xff\x2f\x00" LyricEvent = DeltaZero + "\xff\x05" TextEvent = DeltaZero + "\xff\x01" CopyrightNotice = DeltaZero + "\xff\x02" RCSid = "$Id: midi.py,v 1.74 2005/01/31 02:38:49 jcomeau Exp $" MThd = "MThd" # MIDI header ID string MTrk = "MTrk" # MIDI track ID string # Subroutines def MidiInstrument(Instrument): instruments = [ # piano 'Acoustic Grand Piano', 'Bright Acoustic Piano', 'Electric Grand Piano', 'Honky-Tonk Piano', 'Electric Piano 1', 'Electric Piano 2', 'Harpsichord', 'Clavichord', # chrom. percussion 'Celesta', 'Glockenspiel', 'Music Box', 'Vibraphone', 'Marimba', 'Xylophone', 'Tubular bells', 'Santur', # organ 'Drawbar Organ', 'Percussive Organ', 'Rock Organ', 'Church Organ', 'Reed Organ', 'Accordion', 'Harmonica', 'Tango Accordion', # guitar 'Nylon String Guitar', 'Steel String Guitar', 'Electric Guitar (Jazz)', 'Electric Guitar (Clean)', 'Electric Guitar (Muted)', 'Overdriven Guitar', 'Distortion Guitar', 'Guitar Harmonics', # bass 'Acoustic Bass', 'Fingered Electric Bass', 'Picked Electric Bass', 'Fretless Bass', 'Slap Bass 1', 'Slap Bass 2', 'Synth Bass 1', 'Synth Bass 2', # strings 'Violin', 'Viola', 'Cello', 'Contrabass', 'Tremolo Strings', 'Pizzicato Strings', 'Harp', 'Timpani', # ensemble 'String Ensemble 1', 'String Ensemble 2', 'Synth Strings 1', 'Synth Strings 2', 'Choir Aahs', 'Voice Oohs', 'Synth Voice', 'Orchestra Hit', # brass 'Trumpet', 'Trombone', 'Tuba', 'Muted Trumpet', 'French Horn', 'Brass Section', 'Synth Brass 1', 'Synth Brass 2', # reeds 'Soprano Sax', 'Alto Sax', 'Tenor Sax', 'Baritone Sax', 'Oboe', 'English Horn', 'Bassoon', 'Clarinet', # pipe 'Piccolo', 'Flute', 'Recorder', 'Pan Flute', 'Blown Bottle', 'Skakuhachi', 'Whistle', 'Ocarina', # synth lead 'Square Wave', 'Sawtooth Wave', 'Synth Calliope', 'Chiff Lead', 'Charang', 'Solo Voice', '5th Sawtooth Wave', 'Bass & Lead', # synth pad 'New Age', 'Warm Pad', 'PolySynth', 'Space Choir', 'Bowed Glass', 'Metallic Pad', 'Halo Pad', 'Sweep Pad', # Synth SFX 'Rain', 'Soundtrack', 'Crystal', 'Atmosphere', 'Brightness', 'Goblins', 'Echoes', 'Science Fiction', # ethnic 'Sitar', 'Banjo', 'Shamisen', 'Koto', 'Kalimba', 'Bag Pipe', 'Fiddle', 'Shannai', # percussion 'Tinkle Bell', 'Agogo', 'Steel Drums', 'Woodblock', 'Taiko Drum', 'Melodic Tom', 'Synth Drum', 'Reverse Cymbal', # sound effects 'Guitar Fret Noise', 'Bird Tweet', 'Applause', 'Breath Noise', 'Telephone Ring', 'Gun Shot', 'Seashore', 'Helicopter', ] DebugPrint('Instrument', Instrument) if Instrument == 'all': return instruments else: try: # first assume arg is a number indexing into the list return instruments[int(Instrument)] except: # now assume arg is a name, in which case we want the index return instruments.index(Instrument) def DebugPrint(*args): "stub routine, real one in pytest.py" pass def VarLen(LongValue): Buffer, ReturnString = 0, "" try: longvalue = long(LongValue) except: DebugPrint('bad "long" value', LongValue) raise while (longvalue > 0): Buffer = Buffer + (longvalue & 0x7f) longvalue = longvalue >> 7 if (longvalue > 0): Buffer = Buffer << 8 Buffer = Buffer | 0x80 while True: ReturnString = ReturnString + chr(Buffer & ByteMask) if (Buffer & 0x80): Buffer = Buffer >> 8 else: break return (ReturnString) def NextBytes(Stream, Length): bytes = Stream.read(Length) if len(bytes) < Length: raise Exception("Unexpected EOF, requested length %d, found %s" % \ (Length, repr(bytes))) else: return bytes def ReadVarLen(Stream): testing = False if type(Stream) is StringType: # for command-line testing testing = True import StringIO Stream = StringIO.StringIO(Stream) value, byte, signal = 0L, 0, 0x80 while signal > 0: byte = ord(NextBytes(Stream, 1)) # NextBytes() will croak on EOF signal = signal & byte byte = byte & ~signal value = (value << 7) + byte if testing is True: Stream.close() return value def FixedLong(LongValue): Bytes, ReturnString = LongLength, "" for Index in range(Bytes, 0, -1): Mask = ByteMask << ((Index - 1) * 8) Byte = (LongValue & Mask) >> ((Index - 1) * 8) ReturnString = ReturnString + chr(Byte) return (ReturnString) def ReadFixedLong(Stream): Value, Long = 0, '' Long = NextBytes(Stream, LongLength) for Index in range(0, LongLength): Value = (Value << 8) + ord(Long[Index:Index + 1]) return Value def FixedShort(ShortValue): Bytes, ReturnString = ShortLength, "" for Index in range(Bytes, 0, -1): Mask = ByteMask << ((Index - 1) * 8) Byte = (ShortValue & Mask) >> ((Index - 1) * 8) ReturnString = ReturnString + chr(Byte) return (ReturnString) def ReadFixedShort(Stream): Value, Short = 0, '' Short = NextBytes(Stream, ShortLength) for Index in range(0, ShortLength): Value = (Value << 8) + ord(Short[Index:Index + 1]) return Value def Join14BitValue(array): return ord(array[0]) | (ord(array[1]) << 7) def Split14BitValue(number): number = int(number) # just in case passed as string from command line return chr(number & 0x7f) + chr((number >> 7) & 0x7f) def ReadVarLengthEvent(Event, Stream): length = ReadVarLen(Stream) return [Event, length, NextBytes(Stream, length)] def ReadSingleParameterEvent(Event, Stream): parameter = ord(NextBytes(Stream, 1)) return [Event, parameter] def ReadDualParameterEvent(Event, Stream): parameter = NextBytes(Stream, 2) return [Event, ord(parameter[0]), ord(parameter[1])] def DumpEvent(DeltaTime, Event, Stream = None): Length = ReadVarLen(Stream) print '[%d, 0x%x, %d, %s],' % (DeltaTime, Event, Length, repr(NextBytes(Stream, Length))) def DumpSysexEvent(DeltaTime, Event, Stream = None): DumpEvent(DeltaTime, Event, Stream) def DumpRealtimeEvent(DeltaTime, Event, Stream = None): wtf = ord(NextBytes(Stream, 1)) print '[%d, 0x%x, 0x%x],' % (DeltaTime, Event, wtf) def DumpChannelPressure(DeltaTime, Event, Stream = None): channel = Event % 0x10 pressure = ord(NextBytes(Stream, 1)) comment = "change channel %d pressure to %d" % (channel, pressure) print '[%d, 0x%x, 0x%x], # %s' % (DeltaTime, Event, pressure, comment) def DumpPitchWheelEvent(DeltaTime, Event, Stream = None): pitchbytes = NextBytes(Stream, 2) pitch = Join14BitValue(pitchbytes) print '[%d, 0x%x, 0x%x, 0x%x], # change pitch for channel %d to 0x%x' % \ (DeltaTime, Event, ord(pitchbytes[0]), ord(pitchbytes[1]), Event % 16, pitch) def DumpControllerEvent(DeltaTime, Event, Stream = None): controller = ord(NextBytes(Stream, 1)) channel = Event % 0x10 setting = ord(NextBytes(Stream, 1)) comment = 'set channel %d "%s" controller to %d' % \ (channel, ControllerMapping(controller), setting) print '[%d, 0x%x, 0x%x, 0x%x], # %s' % \ (DeltaTime, Event, controller, setting, comment) def DumpNoteOn(DeltaTime, Event, Stream = None): channel = Event % 0x10 note = ord(NextBytes(Stream, 1)) velocity = ord(NextBytes(Stream, 1)) comment = 'Note On, "%s", channel %d, velocity %d' % \ (NoteMapping(note), channel, velocity) print '[%d, 0x%x, 0x%x, 0x%x], # %s' % \ (DeltaTime, Event, note, velocity, comment) def DumpNoteOff(DeltaTime, Event, Stream = None): channel = Event % 0x10 note = ord(NextBytes(Stream, 1)) velocity = ord(NextBytes(Stream, 1)) comment = 'Note Off, "%s", channel %d, velocity %d' % \ (NoteMapping(note), channel, velocity) print '[%d, 0x%x, 0x%x, 0x%x], # %s' % \ (DeltaTime, Event, note, velocity, comment) def DumpAftertouchEvent(DeltaTime, Event, Stream = None): channel = Event % 0x10 note = ord(NextBytes(Stream, 1)) velocity = ord(NextBytes(Stream, 1)) comment = 'AfterTouch, "%s", channel %d, velocity %d' % \ (NoteMapping(note), channel, velocity) print '[%d, 0x%x, 0x%x, 0x%x], # %s' % \ (DeltaTime, Event, note, velocity, comment) def DumpChangeEvent(DeltaTime, Event, Stream = None): NewProgram = ord(NextBytes(Stream, 1)) print '[%d, 0x%x, %d], # change instrument for channel %d to %s' % \ (DeltaTime, Event, NewProgram, Event % 16, MidiInstrument(NewProgram)) def DumpMetaEvent(DeltaTime, Event, Stream = None): Qualifier = ord(NextBytes(Stream, 1)) Length = ReadVarLen(Stream) print '[%d, 0x%x, 0x%x, %d, %s],' % (DeltaTime, Event, Qualifier, Length, repr(NextBytes(Stream, Length))) def ReadMetaEvent(Event, Stream): qualifier = ord(NextBytes(Stream, 1)) length = ReadVarLen(Stream) bytes = NextBytes(Stream, length) return [Event, qualifier, length, bytes] def NoteOn(Delay, Channel, Note, Velocity): return VarLen(Delay) + chr(0x90 + Channel) + chr(Note) + chr(Velocity) def NoteOff(Delay, Channel, Note, Velocity): return VarLen(Delay) + chr(0x80 + Channel) + chr(Note) + chr(Velocity) def NoteDef(Value, Velocity, Duration): # for bases Channel = 0 return NoteOn(0, Channel, Value, Velocity) + \ NoteOff(Duration, Channel, Value, NormalVelocity) def NoteMapping(note = 'all', style = 'lilypond'): """map standard MIDI numeric note values to string representation use mapping of middle C is MIDI note 60 is MIDI representation C6 (C0 is MIDI note 0) is lilypond representation c' (http://lilypond.org/) (as it goes to 0: "c", "c,", "c,,", etc.) which has a frequency of about 261.625 Hz which is drawn on the first descending line below the staff (G clef)""" notes = dict() global Twelfth if style == 'frequency': notes[69] = 440 # A above middle C for number in range(68, -1, -1): notes[number] = notes[number + 1] * pow(2, -Twelfth) for number in range(70, 128): notes[number] = notes[number - 1] * pow(2, Twelfth) else: if style == 'lilypond': notename = ['c', 'df', 'd', 'ef', 'e', 'f', 'gf', 'g', 'af', 'a', 'bf', 'b'] octave = [",,,,", ",,,", ",,", ",", "", "'", "''", "'''", "''''", "'''''", "''''''"] else: # assume MIDI note representation notename = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] octave = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] for number in range (0, 128): #print "assigning string to %d" % number notes[number] = notename[number % 12] + \ octave[int(number / 12)] if note != 'all': return notes[int(note)] else: return notes def ControllerMapping(controller = 'all'): "Map controller numbers with names" controllers = { 0: 'Bank Select (coarse)', 1: 'Modulation Wheel (coarse)', 2: 'Breath Controller (coarse)', 4: 'Foot Pedal (coarse)', 5: 'Portamento Time (coarse)', 6: 'Data Entry (coarse)', 7: 'Volume (coarse)', 8: 'Balance (coarse)', 10: 'Pan Position (coarse)', 11: 'Expression (coarse)', 12: 'Effect Control 1 (coarse)', 13: 'Effect Control 2 (coarse)', 16: 'General Purpose Slider 1', 17: 'General Purpose Slider 2', 18: 'General Purpose Slider 3', 19: 'General Purpose Slider 4', 32: 'Bank Select (fine)', 33: 'Modulation Wheel (fine)', 34: 'Breath Controller (fine)', 36: 'Foot Pedal (fine)', 37: 'Portamento Time (fine)', 38: 'Data Entry (fine)', 39: 'Volume (fine)', 40: 'Balance (fine)', 42: 'Pan Position (fine)', 43: 'Expression (fine)', 44: 'Effect Control 1 (fine)', 45: 'Effect Control 2 (fine)', 64: 'Hold Pedal (on/off)', 65: 'Portamento (on/off)', 66: 'Sostenuto Pedal (on/off)', 67: 'Soft Pedal (on/off)', 68: 'Legato Pedal (on/off)', 69: 'Hold 2 Pedal (on/off)', 70: 'Sound Variation', 71: 'Sound Timbre', 72: 'Sound Release Time', 73: 'Sound Attack Time', 74: 'Sound Brightness', 75: 'Sound Control 6', 76: 'Sound Control 7', 77: 'Sound Control 8', 78: 'Sound Control 9', 79: 'Sound Control 10', 80: 'General Purpose Button 1 (on/off)', 81: 'General Purpose Button 2 (on/off)', 82: 'General Purpose Button 3 (on/off)', 83: 'General Purpose Button 4 (on/off)', 91: 'Effects Level', 92: 'Tremolo Level', 93: 'Chorus Level', 94: 'Celeste Level', 95: 'Phaser Level', 96: 'Data Button increment', 97: 'Data Button decrement', 98: 'Non-registered Parameter (fine)', 99: 'Non-registered Parameter (coarse)', 100: 'Registered Parameter (fine)', 101: 'Registered Parameter (coarse)', 120: 'All Sound Off', 121: 'All Controllers Off', 122: 'Local Keyboard (on/off)', 123: 'All Notes Off', 124: 'Omni Mode Off', 125: 'Omni Mode On', 126: 'Mono Operation', 127: 'Poly Operation', } if controller != 'all': try: return controllers[controller] except: return 'undefined controller' return controllers def Lyric(text): if len(text): return LyricEvent + VarLen(len(text)) + text else: return '' def EmbedText(text): if len(text): return TextEvent + VarLen(len(text)) + text else: return '' def EmbedCopyright(text): if len(text): return CopyrightNotice + VarLen(len(text)) + text else: return '' def DumpEventHandler(event): events = ((0xff, DumpMetaEvent), (0xf8, DumpRealtimeEvent), (0xf7, DumpSysexEvent), (0xf1, DumpEvent), (0xf0, DumpSysexEvent), (0xe0, DumpPitchWheelEvent), (0xd0, DumpChannelPressure), (0xc0, DumpChangeEvent), (0xb0, DumpControllerEvent), (0xa0, DumpAftertouchEvent), (0x90, DumpNoteOn), (0x80, DumpNoteOff), (0x00, None)) for testevent in range(0, len(events)): if event >= events[testevent][0]: return events[testevent][1] def MidiEventReader(event): events = ((0xff, ReadMetaEvent), (0xf8, ReadSingleParameterEvent), (0xf0, ReadVarLengthEvent), (0xe0, ReadDualParameterEvent), (0xc0, ReadSingleParameterEvent), (0x80, ReadDualParameterEvent), (0x00, None)) for testevent in range(0, len(events)): if event >= events[testevent][0]: return events[testevent][1] def ReadMidiFile(FileName): import StringIO end_of_track = [0xff, 0x2f, 0x00, ''] midistream = open(FileName, "rb") list = [] list.append(ReadMidiHeader(midistream)) for track in range(0, list[0][3]): timer = 0 running_status = 0 list.append(ReadTrackHeader(midistream)) current_track = track + 1 # index into list datalength = list[current_track][1] data = midistream.read(datalength) filestream = midistream midistream = StringIO.StringIO(data) eof = False while not eof: try: deltatime = ReadVarLen(midistream) status = ord(NextBytes(midistream, 1)) if midistream.tell() == datalength: raise Exception, 'EOD' except: eof = True raise handler = MidiEventReader(status) if handler is None: status = running_status handler = MidiEventReader(status) midistream.seek(-1, 1) elif status < 0xf8: # realtime messages don't affect running status if status < 0xf0: # system common messages flush running status running_status = status else: running_status = 0 timer = timer + deltatime event = handler(status, midistream) if event == end_of_track: eof = True # but don't 'break', finish adding it first event.insert(0, timer) # store absolute, not delta, time current_event = len(list[current_track]) # get index BEFORE append list[current_track].append(event) midistream.close midistream = filestream midistream.close return list def FlattenMidiFile(midifile): """flattens MIDI track data already read in by ReadMidiFile()""" if type(midifile) is StringType: midifile = ReadMidiFile(midifile) format = midifile[0][2] tracks = midifile[0][3] fixtime = 0 # only used when converting format 2 files flattened = [] for track in range(0, tracks): trackdata = midifile[track + 1] for index in range(2, len(trackdata)): # skip track header trackdata[index][0] = trackdata[index][0] + fixtime flattened.append(trackdata[index]) if format == 2: fixtime = flattened[len(flattened) - 1][0] flattened.sort(lambda a, b: cmp(a[0], b[0])) # now re-delta all the times fixtime = 0 for event in flattened: event[0] = event[0] - fixtime fixtime = fixtime + event[0] return flattened def CheckMinMax(optionInstance, option, value, parser, *arguments): (min, max) = argumentsp errormessage = "value of " + option + " (" + str(value) +\ ") must be between " + str(min) + " and " + str(max) if value < min or value > max: raise OptionValueError(errormessage) else: setattr(parser.values, optionInstance.dest, value) def midiplay_options(): try: from optparse import OptionParser, OptionValueError except: sys.stderr.write("Needs optparse, upgrade preferably to 2.3\n") sys.stderr.write("Any options on your command-line will be ignored\n") return None, sys.argv[1:] parser = OptionParser(usage = "%prog [options] INFILE[...]") parser.add_option("-t", "--transpose", type = "int", action = "callback", callback = CheckMinMax, callback_args = (-8, 8), dest = "transpose", default = 0, help = "raise or lower all notes except MIDI drums up to 8 half-steps") parser.add_option("-d", "--drop", type = "int", action = "callback", callback = CheckMinMax, callback_args = (0, 15), dest = "drop", default = None, help = "channel whose notes should not be played") parser.add_option("-v", "--verbose", action = "store_true", default = False, help = "output debugging information while processing") options, arguments = parser.parse_args() if len(arguments) < 1: parser.print_help() sys.exit(0) return options, arguments def SetDefault(dict, index, default): try: dict[index] except: dict[index] = default return dict def midinote(*args, **options): """play notes by MIDI number see NoteMapping() for documentation""" import pymidi, time midiout = pymidi.open("/dev/midi", "wb") velocity = 64 if not len(args): args = sys.argv[1:] if type(args[0]) is ListType: if type(args[-1]) is DictType: options = args[-1] args = args[0] dbgprint('midinote args', args, 'options', options) options = SetDefault(options, 'channel', 0) options = SetDefault(options, 'delay', 0.25) # default time per note options = SetDefault(options, 'legato', False) options = SetDefault(options, 'instrument', 0) dbgprint('options', options) lastnote = None pymidi.write(midiout, (0xc0 + options['channel'], options['instrument'])) for note in args: delay = options['delay'] if type(note) is ComplexType: delay = note.imag note = note.real note = int(note) #dbgprint('note: %d, channel: %d' % (note, options['channel'])) if options['legato']: if note != lastnote: if lastnote is not None: pymidi.write(midiout, (0x90 + options['channel'], lastnote, 0)) pymidi.write(midiout, (0x90 + options['channel'], note, velocity)) time.sleep(delay) # delay even if note == lastnote else: pymidi.write(midiout, (0x90 + options['channel'], note, velocity)) time.sleep(delay) pymidi.write(midiout, (0x90 + options['channel'], note, 0)) lastnote = note pymidi.write(midiout, (0x90 + options['channel'], lastnote, 0)) pymidi.close(midiout) def dbgprint(*whatever): sys.stderr.write("%s\n" % repr(whatever)) def mididrum(*args, **options): if not len(args): args = sys.argv[1:] dbgprint('mididrum args', args) try: options['channel'] = 9 # yes, midi channel 10 is drums, but zero-base it except: options = {'channel': 9} midinote(args, options) def midiplay(): import pymidi options, arguments = midiplay_options() debugprint("options: %s" % repr(options), options) os.nice(-5) # improve realtime performance for filename in arguments: file = ReadMidiFile(filename) flattened = FlattenMidiFile(file) midiout = pymidi.open("/dev/midi", "wb") timebase = 500000 # microseconds per quarter note division = file[0][4] # ticks per quarter note if (division & 0x8000) == 0x8000: # SMPTE division = (256 - (division / 0x100)) * (division & 0xff) debugprint("division: %d" % division, options) ticktime = (float(timebase) / division) / 1000000 # seconds per tick for event in flattened: delta = event[0] debugprint("sleeping %d" % delta) time.sleep(delta * ticktime) if event[1] < 0xf0: channel = event[1] % 16 try: if options.drop == channel: continue except: pass try: "channel 10 is drum set, but zero-based it's 9" if event[1] < 0xa0 and channel != 9: event[2] = event[2] + options.transpose except: pass debugprint("playing: %s" % repr(event), options) pymidi.write(midiout, tuple(event[1:])) elif event[1] == 0xff and event[2] == 0x51: debugprint("extracting new timebase from %s" % repr(event), options) timebase = 0 for byte in range(0, len(event[4])): timebase = (timebase * 0x100) + ord(event[4][byte]) ticktime = (float(timebase) / division) / 1000000 debugprint("new timebase: %d" % timebase, options) else: pass debugprint("skipping event: %s" % repr(event), options) pymidi.close(midiout) def mididump(): import StringIO for file in sys.argv[1:]: MidiStream = open(file, "rb") print "[ # midi file dump" tracks = DumpMidiHeader(MidiStream) for track in range(0, tracks): delta = 0 LastEvent = 0 print '[ # midi track %d' % (track + 1) datalength = DumpTrackHeader(MidiStream) data = MidiStream.read(datalength) FileStream = MidiStream MidiStream = StringIO.StringIO(data) eof = False while not eof: try: deltatime = ReadVarLen(MidiStream) event = ord(NextBytes(MidiStream, 1)) except: eof = True break if MidiStream.tell() == datalength: eof = True break handler = DumpEventHandler(event) if handler is None: event = LastEvent handler = DumpEventHandler(event) MidiStream.seek(-1, 1) elif event < 0xf8: "don't update LastEvent if it's a RealTime message" LastEvent = event delta = delta + deltatime handler(deltatime, event, MidiStream) MidiStream.close() debugprint("delta: %d" % delta) print "], # end midi track" MidiStream = FileStream MidiStream.close() print "] # end midi file" def midiundump(*args): for file in sys.argv[1:]: outfilename = '%s.mid' % file infile = open(file, "r") outfile = open(outfilename, "wb") midifile = ''.join(infile.readlines()) #print midifile mididata = eval(midifile) #print repr(mididata) tracks = UndumpMidiHeader(outfile, mididata[0]) for track in range(0, tracks): UndumpMidiTrack(outfile, mididata[1 + track]) def UndumpMidiHeader(outfile, header): outfile.write(header[0] + FixedLong(header[1]) + \ FixedShort(header[2]) + \ FixedShort(header[3]) + \ FixedShort(header[4])) return header[3] def UndumpMidiTrack(outfile, trackdata): LastEvent = 0 trackbytes = '' reference_bytes = trackdata[1] # use to compare with what we really get for event in trackdata[2:]: #print repr(event) trackbytes = trackbytes + VarLen(event.pop(0)) status = event.pop(0) if status < 0xf0: if status != LastEvent: trackbytes = trackbytes + chr(status) LastEvent = status elif status < 0xf8: # system common messages clear running status LastEvent = 0 trackbytes = trackbytes + chr(status) if status == 0xf7 or status == 0xf0: trackbytes = trackbytes + VarLen(event.pop(0)) elif status == 0xff: # meta-event also uses variable length arg trackbytes = trackbytes + chr(status) + chr(event.pop(0)) + \ VarLen(event.pop(0)) else: trackbytes = trackbytes + chr(status) for data in event: if type(data) is StringType: trackbytes = trackbytes + data else: trackbytes = trackbytes + chr(data) if reference_bytes != len(trackbytes): debugprint("track length modified from %d to %d" % \ (reference_bytes, len(trackbytes))) outfile.write(trackdata[0] + FixedLong(len(trackbytes)) + trackbytes) def midi(): "Default program action, should really be help text" if (len(sys.argv)) == 1: sys.stderr.write("Usage: midi COMMAND [ARG...]\n") else: sys.argv.pop(0) # drop the 'midi' and use COMMAND instead main() def MidiHeader(): ChunkLength = FixedLong(6) # 6 bytes MidiFileFormat = FixedShort(0) # 0, simplest format NumberOfTracks = FixedShort(1) # 1 track TimeDivision = FixedShort(96) # ticks per quarter note return MThd + ChunkLength + MidiFileFormat + NumberOfTracks + TimeDivision def DumpMidiHeader(*MidiStream): if type(MidiStream[0]) is FileType: MidiStream = MidiStream[0] print '[ # dump midi header' ChunkType = MidiStream.read(4) if ChunkType == MThd: print "'%s'," % MThd else: raise Exception, 'Wrong chunk type %s' % repr(ChunkType) print '%d, # chunk length' % ReadFixedLong(MidiStream) MidiFileFormat = ReadFixedShort(MidiStream) NumberOfTracks = ReadFixedShort(MidiStream) TimeDivision = ReadFixedShort(MidiStream) print '%d, # MIDI file format' % MidiFileFormat print '%d, # Tracks' % NumberOfTracks print '0x%x, # Time Division' % TimeDivision print '], # end of midi header dump' return NumberOfTracks else: print '[ # dump midi header' print "'%s'," % MidiStream[0] print '%d, # chunk length' % int(MidiStream[1]) print '%d, # MIDI file format' % int(MidiStream[2]) print '%d, # Tracks' % int(MidiStream[3]) print '0x%x, # Time Division' % int(MidiStream[4]) print '], # end of midi header dump' def ReadMidiHeader(MidiStream): chunktype = MidiStream.read(4) if chunktype != MThd: raise Exception, 'Invalid MIDI header %s' % chunktype length = ReadFixedLong(MidiStream) format = ReadFixedShort(MidiStream) tracks = ReadFixedShort(MidiStream) timedivision = ReadFixedShort(MidiStream) return [chunktype, length, format, tracks, timedivision] def DumpTrackHeader(*MidiStream): if type(MidiStream[0]) is FileType: MidiStream = MidiStream[0] ChunkType = MidiStream.read(4) if ChunkType == MTrk: print "'%s'," % MTrk else: raise Exception ChunkLength = ReadFixedLong(MidiStream) print '%d, # chunk length' % ChunkLength return ChunkLength else: print "'%s'," % MidiStream[0] print '%d, # chunk length' % int(MidiStream[1]) def ReadTrackHeader(MidiStream): chunktype = MidiStream.read(4) if chunktype != MTrk: raise Exception, 'Invalid track header %s' % chunktype length = ReadFixedLong(MidiStream) return [chunktype, length] def DumpTimeSignature(String): numerator = ord(String[0]) denominator = 2 ** ord(String[1]) clocks = ord(String[2]) # MIDI clocks in metronome tick, default 24 quarternote = ord(String[3]) # 32nd notes per 24 MIDI clocks, default 8 return "time signature %d/%d," % (numerator, denominator) + \ " %d MIDI clocks per metronome tick," % clocks + \ " %d 32nd notes per 24 MIDI clocks" % quarternote def MidiTrackCommon(Options): TimeSignaturePrefix = DeltaZero + "\xff\x58" # meta-event for time signature TimeSignatureLength = chr(4) # length of data TimeSignatureNumerator = chr(4) # for 4/4 time TimeSignatureDenominator = chr(2) # 2 ** -dd, where dd = 2, gives 1/4 TimeSignatureClocks = chr(24) # number of MIDI clocks in metronome tick TimeSignatureQuarterNote = chr(8) # 8 notated 32nd notes per 24 MIDI clocks TimeSignature = TimeSignaturePrefix + TimeSignatureLength + \ TimeSignatureNumerator + TimeSignatureDenominator + \ TimeSignatureClocks + TimeSignatureQuarterNote TempoPrefix = DeltaZero + "\xff\x51" # meta-event for tempo specification TempoLength = chr(3) # bytes for tempo data (24-bit time specification) TempoSpecification = FixedLong(500000)[1:4] # microseconds per MIDI 1/4 note Tempo = TempoPrefix + TempoLength + TempoSpecification CopyrightEvent = EmbedCopyright(Options.copyright) ProgramChange = DeltaZero + "\xc0" + chr(Options.instrument - 1) + \ DeltaZero + "\xc1" + chr(Options.instrument1 - 1) + \ DeltaZero + "\xc2" + chr(Options.instrument2 - 1) + \ DeltaZero + "\xc3" + chr(Options.instrument3 - 1) # examples: 1 = Grand Piano, 13 = Marimba return (TimeSignature + Tempo + CopyrightEvent + ProgramChange) def debugprint(text, options=None): try: if (Debugging or options.verbose) and len(text) > 0: sys.stderr.write(text + "\n") except: pass def SetChannelProgram(Handle, Channel, Instrument): """Change instrument (program) for channel instrument can be specified as a number or name""" import pymidi try: Instrument = int(Instrument) except: Instrument = MidiInstrument(Instrument) pymidi.write(Handle, (0xc0 + int(Channel), Instrument)) def EndDataTrack(Options): "Send end-of-track meta-event" return EndOfTrack def SetChannelPan(Handle, Channel, Position): """Set coarse Pan Position on a channel (left-right stereo balance) 0 is hard left, 127 is hard right. This routine sends to an already-open MIDI device""" import pymidi DebugPrint("Setting pan position for channel %s to %s" % (Channel, Position)) pymidi.write(Handle, (0xb0 + Channel, 10, Position)) def SetChannelPitch(Handle, Channel, Position): """Set pitch wheel position for channel 14-bit value from 0 to 0x2000 (center) to 0x3fff modifies pitch from -2 full notes to (just shy of?) +2 notes This is not universal though recommended by GM standard, and can be changed by a Registered Parameter Number (RPN) message""" import pymidi DebugPrint('setting pitch wheel for channel %s to 0x%x' % (Channel, Position)) value = Split14BitValue(Position) pymidi.write(Handle, (0xe0 + int(Channel), ord(value[0]), ord(value[1]))) def SetChannelPitchFrequency(Handle, Channel, Note, Frequency): """Set pitch wheel after calculating setting from Note and Frequency See notes under SetChannelPitch() This can take a while, don't use in a time-critical part of program I'm sure this can be written better but can't think how at the moment""" from arcane import NearestValueIndex global Twelfth Note, Frequency = NoteMapping(Note, 'frequency'), float(Frequency) DebugPrint('Note, Frequency:', Note, Frequency) frequencies = [] # empty list for position in range(0, 0x4000): frequencies.append(Note * pow(2, \ ((float(position) - 0x2000) / 0x800) * Twelfth)) DebugPrint('samples:', frequencies[0], frequencies[0x1000], frequencies[0x2000], frequencies[0x3000], frequencies[0x3fff]) position = NearestValueIndex(Note + Frequency, frequencies) difference = frequencies[position] - (Note + Frequency) if abs(difference) > 0.1: sys.stderr.write("Closest I can get to %s is %s\n" % (Note + Frequency, frequencies[position])) SetChannelPitch(Handle, Channel, position) def playfrequency(*args): "play specified frequency for specified duration" from arcane import NearestValueIndex while type(args[1]) is types.TupleType: args = args[1] frequency, duration = args position = NearestValueIndex(frequency, frequencies) SetChannelPitch(Handle, Channel, position) def CheckMinMax(optionInstance, option, value, parser, *arguments): (min, max) = arguments errorMessage = "value of " + option + " (" + str(value) +\ ") must be between " + str(min) + " and " + str(max) if value < min or value > max: raise OptionValueError(errorMessage) else: setattr(parser.values, optionInstance.dest, value) def program_name(program_path): "find out how this program was invoked, removing any .py extension" return re.search('([A-Za-z0-9]+)(.py)?$', program_path) def argstring(array): "return string form of array for eval()" if array is None or len(array) == 0: return "()" else: return "('" + "', '".join(array) + "')" def main(): try: "first see if program exists and run it if so" match = program_name(sys.argv[0]) program = match.group(1) if re.match('^[a-z0-9]+$', program): # lowercase, uses sys.argv eval(program + argstring(())) else: # mixed upper/lowercase, must be testing function print repr(eval(program + argstring(sys.argv[1:]))) except: raise # The following is standard; it allows the script to be used as a library # with 'import', but runs only when invoked directly if __name__ == "__main__": main()