# smidi.py: A simple midi-file library # make a single file version for midi-outfile only. # Original code is from Max M's site here (GPL) # http://www.mxm.dk/products/public/pythonmidi """ include source codes from - constants.py - DataTypeConverters.py - RawOutstreamFile.py - MidiOutStream.py - MidiOutFile.py """ from struct import pack, unpack from types import StringType from cStringIO import StringIO #=================== constants.py ===================================# ################################################### ## Definitions of the different midi events ################################################### ## Midi channel events (The most usual events) ## also called "Channel Voice Messages" NOTE_OFF = 0x80 # 1000cccc 0nnnnnnn 0vvvvvvv (channel, note, velocity) NOTE_ON = 0x90 # 1001cccc 0nnnnnnn 0vvvvvvv (channel, note, velocity) AFTERTOUCH = 0xA0 # 1010cccc 0nnnnnnn 0vvvvvvv (channel, note, velocity) CONTINUOUS_CONTROLLER = 0xB0 # see Channel Mode Messages!!! # 1011cccc 0ccccccc 0vvvvvvv (channel, controller, value) PATCH_CHANGE = 0xC0 # 1100cccc 0ppppppp (channel, program) CHANNEL_PRESSURE = 0xD0 # 1101cccc 0ppppppp (channel, pressure) PITCH_BEND = 0xE0 # 1110cccc 0vvvvvvv 0wwwwwww (channel, value-lo, value-hi) ################################################### ## Channel Mode Messages (Continuous Controller) ## They share a status byte. ## The controller makes the difference here # High resolution continuous controllers (MSB) BANK_SELECT = 0x00 MODULATION_WHEEL = 0x01 BREATH_CONTROLLER = 0x02 FOOT_CONTROLLER = 0x04 PORTAMENTO_TIME = 0x05 DATA_ENTRY = 0x06 CHANNEL_VOLUME = 0x07 BALANCE = 0x08 PAN = 0x0A EXPRESSION_CONTROLLER = 0x0B EFFECT_CONTROL_1 = 0x0C EFFECT_CONTROL_2 = 0x0D GEN_PURPOSE_CONTROLLER_1 = 0x10 GEN_PURPOSE_CONTROLLER_2 = 0x11 GEN_PURPOSE_CONTROLLER_3 = 0x12 GEN_PURPOSE_CONTROLLER_4 = 0x13 # High resolution continuous controllers (LSB) BANK_SELECT = 0x20 MODULATION_WHEEL = 0x21 BREATH_CONTROLLER = 0x22 FOOT_CONTROLLER = 0x24 PORTAMENTO_TIME = 0x25 DATA_ENTRY = 0x26 CHANNEL_VOLUME = 0x27 BALANCE = 0x28 PAN = 0x2A EXPRESSION_CONTROLLER = 0x2B EFFECT_CONTROL_1 = 0x2C EFFECT_CONTROL_2 = 0x2D GENERAL_PURPOSE_CONTROLLER_1 = 0x30 GENERAL_PURPOSE_CONTROLLER_2 = 0x31 GENERAL_PURPOSE_CONTROLLER_3 = 0x32 GENERAL_PURPOSE_CONTROLLER_4 = 0x33 # Switches SUSTAIN_ONOFF = 0x40 PORTAMENTO_ONOFF = 0x41 SOSTENUTO_ONOFF = 0x42 SOFT_PEDAL_ONOFF = 0x43 LEGATO_ONOFF = 0x44 HOLD_2_ONOFF = 0x45 # Low resolution continuous controllers SOUND_CONTROLLER_1 = 0x46 # (TG: Sound Variation; FX: Exciter On/Off) SOUND_CONTROLLER_2 = 0x47 # (TG: Harmonic Content; FX: Compressor On/Off) SOUND_CONTROLLER_3 = 0x48 # (TG: Release Time; FX: Distortion On/Off) SOUND_CONTROLLER_4 = 0x49 # (TG: Attack Time; FX: EQ On/Off) SOUND_CONTROLLER_5 = 0x4A # (TG: Brightness; FX: Expander On/Off)75 SOUND_CONTROLLER_6 (TG: Undefined; FX: Reverb OnOff) SOUND_CONTROLLER_7 = 0x4C # (TG: Undefined; FX: Delay OnOff) SOUND_CONTROLLER_8 = 0x4D # (TG: Undefined; FX: Pitch Transpose OnOff) SOUND_CONTROLLER_9 = 0x4E # (TG: Undefined; FX: Flange/Chorus OnOff) SOUND_CONTROLLER_10 = 0x4F # (TG: Undefined; FX: Special Effects OnOff) GENERAL_PURPOSE_CONTROLLER_5 = 0x50 GENERAL_PURPOSE_CONTROLLER_6 = 0x51 GENERAL_PURPOSE_CONTROLLER_7 = 0x52 GENERAL_PURPOSE_CONTROLLER_8 = 0x53 PORTAMENTO_CONTROL = 0x54 # (PTC) (0vvvvvvv is the source Note number) (Detail) EFFECTS_1 = 0x5B # (Ext. Effects Depth) EFFECTS_2 = 0x5C # (Tremelo Depth) EFFECTS_3 = 0x5D # (Chorus Depth) EFFECTS_4 = 0x5E # (Celeste Depth) EFFECTS_5 = 0x5F # (Phaser Depth) DATA_INCREMENT = 0x60 # (0vvvvvvv is n/a; use 0) DATA_DECREMENT = 0x61 # (0vvvvvvv is n/a; use 0) NON_REGISTERED_PARAMETER_NUMBER = 0x62 # (LSB) NON_REGISTERED_PARAMETER_NUMBER = 0x63 # (MSB) REGISTERED_PARAMETER_NUMBER = 0x64 # (LSB) REGISTERED_PARAMETER_NUMBER = 0x65 # (MSB) # Channel Mode messages - (Detail) ALL_SOUND_OFF = 0x78 RESET_ALL_CONTROLLERS = 0x79 LOCAL_CONTROL_ONOFF = 0x7A ALL_NOTES_OFF = 0x7B OMNI_MODE_OFF = 0x7C # (also causes ANO) OMNI_MODE_ON = 0x7D # (also causes ANO) MONO_MODE_ON = 0x7E # (Poly Off; also causes ANO) POLY_MODE_ON = 0x7F # (Mono Off; also causes ANO) ################################################### ## System Common Messages, for all channels SYSTEM_EXCLUSIVE = 0xF0 # 11110000 0iiiiiii 0ddddddd ... 11110111 MTC = 0xF1 # MIDI Time Code Quarter Frame # 11110001 SONG_POSITION_POINTER = 0xF2 # 11110010 0vvvvvvv 0wwwwwww (lo-position, hi-position) SONG_SELECT = 0xF3 # 11110011 0sssssss (songnumber) #UNDEFINED = 0xF4 ## 11110100 #UNDEFINED = 0xF5 ## 11110101 TUNING_REQUEST = 0xF6 # 11110110 END_OFF_EXCLUSIVE = 0xF7 # terminator # 11110111 # End of system exclusive ################################################### ## Midifile meta-events SEQUENCE_NUMBER = 0x00 # 00 02 ss ss (seq-number) TEXT = 0x01 # 01 len text... COPYRIGHT = 0x02 # 02 len text... SEQUENCE_NAME = 0x03 # 03 len text... INSTRUMENT_NAME = 0x04 # 04 len text... LYRIC = 0x05 # 05 len text... MARKER = 0x06 # 06 len text... CUEPOINT = 0x07 # 07 len text... PROGRAM_NAME = 0x08 # 08 len text... DEVICE_NAME = 0x09 # 09 len text... MIDI_CH_PREFIX = 0x20 # MIDI channel prefix assignment (unofficial) MIDI_PORT = 0x21 # 21 01 port, legacy stuff but still used END_OF_TRACK = 0x2F # 2f 00 TEMPO = 0x51 # 51 03 tt tt tt (tempo in us/quarternote) SMTP_OFFSET = 0x54 # 54 05 hh mm ss ff xx TIME_SIGNATURE = 0x58 # 58 04 nn dd cc bb KEY_SIGNATURE = 0x59 # ??? len text... SPECIFIC = 0x7F # Sequencer specific event FILE_HEADER = 'MThd' TRACK_HEADER = 'MTrk' ################################################### ## System Realtime messages ## I don't supose these are to be found in midi files?! TIMING_CLOCK = 0xF8 # undefined = 0xF9 SONG_START = 0xFA SONG_CONTINUE = 0xFB SONG_STOP = 0xFC # undefined = 0xFD ACTIVE_SENSING = 0xFE SYSTEM_RESET = 0xFF ################################################### ## META EVENT, it is used only in midi files. ## In transmitted data it means system reset!!! META_EVENT = 0xFF # 11111111 ################################################### ## Helper functions def is_status(byte): return (byte & 0x80) == 0x80 # 1000 0000 #===================== DataTypeConverters.py ========================# """ This module contains functions for reading and writing the special data types that a midi file contains. """ """ nibbles are four bits. A byte consists of two nibles. hiBits==0xF0, loBits==0x0F Especially used for setting channel and event in 1. byte of musical midi events """ def getNibbles(byte): """ Returns hi and lo bits in a byte as a tuple Asserts byte <= 255 """ return (byte >> 4 & 0xF, byte & 0xF) def setNibbles(hiNibble, loNibble): """ returns byte with value set according to hi and lo bits Asserts hiNibble <= 15 and loNibble <= 15 """ return (hiNibble << 4) + loNibble def readBew(value): "Reads string as big endian word, (asserts len(value) in [1,2,4])" return unpack('>%s' % {1:'B', 2:'H', 4:'L'}[len(value)], value)[0] def writeBew(value, length): "Write int as big endian formatted string, (asserts length in [1,2,4])" return pack('>%s' % {1:'B', 2:'H', 4:'L'}[length], value) """ Variable Length Data (varlen) is a data format sprayed liberally throughout a midi file. It can be anywhere from 1 to 4 bytes long. If the 8'th bit is set in a byte another byte follows. The value is stored in the lowest 7 bits of each byte. So max value is 4x7 bits = 28 bits. """ def readVar(value): """ Converts varlength format to integer. Just pass it 0 or more chars that might be a varlen and it will only use the relevant chars. use varLen(readVar(value)) to see how many bytes the integer value takes. asserts len(value) >= 0 """ sum = 0 for byte in unpack('%sB' % len(value), value): sum = (sum << 7) + (byte & 0x7F) if not 0x80 & byte: break # stop after last byte return sum def varLen(value): """ Returns the the number of bytes an integer will be when converted to varlength """ if value <= 127: return 1 elif value <= 16383: return 2 elif value <= 2097151: return 3 else: return 4 def writeVar(value): "Converts an integer to varlength format" sevens = to_n_bits(value, varLen(value)) for i in range(len(sevens)-1): sevens[i] = sevens[i] | 0x80 return fromBytes(sevens) def to_n_bits(value, length=1, nbits=7): "returns the integer value as a sequence of nbits bytes" bytes = [(value >> (i*nbits)) & 0x7F for i in range(length)] bytes.reverse() return bytes def toBytes(value): "Turns a string into a list of byte values" return unpack('%sB' % len(value), value) def fromBytes(value): "Turns a list of bytes into a string" if not value: return '' return pack('%sB' % len(value), *value) #=================== RawOutstreamFile.py ============================# class RawOutstreamFile: """ Writes a midi file to disk. """ def __init__(self, outfile=''): self.buffer = StringIO() self.outfile = outfile # native data reading functions def writeSlice(self, str_slice): "Reads the next text slice from the raw data, with length" self.buffer.write(str_slice) def writeBew(self, value, length=1): "Writes a value to the file as big endian word" self.writeSlice(writeBew(value, length)) def writeVarLen(self, value): "Writes a variable length word to the file" var = self.writeSlice(writeVar(value)) def write(self): "Writes to disc" if self.outfile: if isinstance(self.outfile, StringType): outfile = open(self.outfile, 'wb') outfile.write(self.getvalue()) outfile.close() else: self.outfile.write(self.getvalue()) def getvalue(self): return self.buffer.getvalue() #=================== MidiOutStream.py ===============================# class MidiOutStream: """ MidiOutstream is Basically an eventhandler. It is the most central class in the Midi library. You use it both for writing events to an output stream, and as an event handler for an input stream. This makes it extremely easy to take input from one stream and send it to another. Ie. if you want to read a Midi file, do some processing, and send it to a midiport. All time values are in absolute values from the opening of a stream. To calculate time values, please use the MidiTime and MidiDeltaTime classes. """ def __init__(self): # the time is rather global, so it needs to be stored # here. Otherwise there would be no really simple way to # calculate it. The alternative would be to have each event # handler do it. That sucks even worse! self._absolute_time = 0 self._relative_time = 0 self._current_track = 0 self._running_status = None # time handling event handlers. They should overwritten with care def update_time(self, new_time=0, relative=1): """ Updates the time, if relative is true, new_time is relative, else it's absolute. """ if relative: self._relative_time = new_time self._absolute_time += new_time else: self._relative_time = new_time - self._absolute_time self._absolute_time = new_time def reset_time(self): """ reset time to 0 """ self._relative_time = 0 self._absolute_time = 0 def rel_time(self): "Returns the relative time" return self._relative_time def abs_time(self): "Returns the absolute time" return self._absolute_time # running status methods def reset_run_stat(self): "Invalidates the running status" self._running_status = None def set_run_stat(self, new_status): "Set the new running status" self._running_status = new_status def get_run_stat(self): "Set the new running status" return self._running_status # track handling event handlers def set_current_track(self, new_track): "Sets the current track number" self._current_track = new_track def get_current_track(self): "Returns the current track number" return self._current_track ##################### ## Midi events def channel_message(self, message_type, channel, data): """The default event handler for channel messages""" pass def note_on(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ pass def note_off(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ pass def aftertouch(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ pass def continuous_controller(self, channel, controller, value): """ channel: 0-15 controller, value: 0-127 """ pass def patch_change(self, channel, patch): """ channel: 0-15 patch: 0-127 """ pass def channel_pressure(self, channel, pressure): """ channel: 0-15 pressure: 0-127 """ pass def pitch_bend(self, channel, value): """ channel: 0-15 value: 0-16383 """ pass ##################### ## System Exclusive def system_exclusive(self, data): """ data: list of values in range(128) """ pass ##################### ## Common events def song_position_pointer(self, value): """ value: 0-16383 """ pass def song_select(self, songNumber): """ songNumber: 0-127 """ pass def tuning_request(self): """ No values passed """ pass def midi_time_code(self, msg_type, values): """ msg_type: 0-7 values: 0-15 """ pass ######################### # header does not really belong here. But anyhoo!!! def header(self, format=0, nTracks=1, division=96): """ format: type of midi file in [1,2] nTracks: number of tracks division: timing division """ pass def eof(self): """ End of file. No more events to be processed. """ pass ##################### ## meta events def meta_event(self, meta_type, data): """ Handles any undefined meta events """ pass def start_of_track(self, n_track=0): """ n_track: number of track """ pass def end_of_track(self): """ n_track: number of track """ pass def sequence_number(self, value): """ value: 0-16383 """ pass def text(self, text): """ Text event text: string """ pass def copyright(self, text): """ Copyright notice text: string """ pass def sequence_name(self, text): """ Sequence/track name text: string """ pass def instrument_name(self, text): """ text: string """ pass def lyric(self, text): """ text: string """ pass def marker(self, text): """ text: string """ pass def cuepoint(self, text): """ text: string """ pass def midi_ch_prefix(self, channel): """ channel: midi channel for subsequent data (deprecated in the spec) """ pass def midi_port(self, value): """ value: Midi port (deprecated in the spec) """ pass def tempo(self, value): """ value: 0-2097151 tempo in us/quarternote (to calculate value from bpm: int(60,000,000.00 / BPM)) """ pass def smtp_offset(self, hour, minute, second, frame, framePart): """ hour, minute, second: 3 bytes specifying the hour (0-23), minutes (0-59) and seconds (0-59), respectively. The hour should be encoded with the SMPTE format, just as it is in MIDI Time Code. frame: A byte specifying the number of frames per second (one of : 24, 25, 29, 30). framePart: A byte specifying the number of fractional frames, in 100ths of a frame (even in SMPTE-based tracks using a different frame subdivision, defined in the MThd chunk). """ pass def time_signature(self, nn, dd, cc, bb): """ nn: Numerator of the signature as notated on sheet music dd: Denominator of the signature as notated on sheet music The denominator is a negative power of 2: 2 = quarter note, 3 = eighth, etc. cc: The number of MIDI clocks in a metronome click bb: The number of notated 32nd notes in a MIDI quarter note (24 MIDI clocks) """ pass def key_signature(self, sf, mi): """ sf: is a byte specifying the number of flats (-ve) or sharps (+ve) that identifies the key signature (-7 = 7 flats, -1 = 1 flat, 0 = key of C, 1 = 1 sharp, etc). mi: is a byte specifying a major (0) or minor (1) key. """ pass def sequencer_specific(self, data): """ data: The data as byte values """ pass ##################### ## realtime events def timing_clock(self): """ No values passed """ pass def song_start(self): """ No values passed """ pass def song_stop(self): """ No values passed """ pass def song_continue(self): """ No values passed """ pass def active_sensing(self): """ No values passed """ pass def system_reset(self): """ No values passed """ pass #============================ MidiOutFile.py ========================# class MidiOutFile(MidiOutStream): """ MidiOutFile is an eventhandler that subclasses MidiOutStream. """ def __init__(self, raw_out=''): self.raw_out = RawOutstreamFile(raw_out) MidiOutStream.__init__(self) def write(self): self.raw_out.write() def event_slice(self, slc): """ Writes the slice of an event to the current track. Correctly inserting a varlen timestamp too. """ trk = self._current_track_buffer trk.writeVarLen(self.rel_time()) trk.writeSlice(slc) ##################### ## Midi events def note_on(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ slc = fromBytes([NOTE_ON + channel, note, velocity]) self.event_slice(slc) def note_off(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ slc = fromBytes([NOTE_OFF + channel, note, velocity]) self.event_slice(slc) def aftertouch(self, channel=0, note=0x40, velocity=0x40): """ channel: 0-15 note, velocity: 0-127 """ slc = fromBytes([AFTERTOUCH + channel, note, velocity]) self.event_slice(slc) def continuous_controller(self, channel, controller, value): """ channel: 0-15 controller, value: 0-127 """ slc = fromBytes([CONTINUOUS_CONTROLLER + channel, controller, value]) self.event_slice(slc) # These should probably be implemented # http://users.argonet.co.uk/users/lenny/midi/tech/spec.html#ctrlnums def patch_change(self, channel, patch): """ channel: 0-15 patch: 0-127 """ slc = fromBytes([PATCH_CHANGE + channel, patch]) self.event_slice(slc) def channel_pressure(self, channel, pressure): """ channel: 0-15 pressure: 0-127 """ slc = fromBytes([CHANNEL_PRESSURE + channel, pressure]) self.event_slice(slc) def pitch_bend(self, channel, value): """ channel: 0-15 value: 0-16383 """ msb = (value>>7) & 0xFF lsb = value & 0xFF slc = fromBytes([PITCH_BEND + channel, msb, lsb]) self.event_slice(slc) ##################### ## System Exclusive # def sysex_slice(sysex_type, data): # "" # sysex_len = writeVar(len(data)+1) # self.event_slice(SYSTEM_EXCLUSIVE + sysex_len + data + END_OFF_EXCLUSIVE) # def system_exclusive(self, data): """ data: list of values in range(128) """ sysex_len = writeVar(len(data)+1) self.event_slice(chr(SYSTEM_EXCLUSIVE) + sysex_len + data + chr(END_OFF_EXCLUSIVE)) ##################### ## Common events def midi_time_code(self, msg_type, values): """ msg_type: 0-7 values: 0-15 """ value = (msg_type<<4) + values self.event_slice(fromBytes([MIDI_TIME_CODE, value])) def song_position_pointer(self, value): """ value: 0-16383 """ lsb = (value & 0x7F) msb = (value >> 7) & 0x7F self.event_slice(fromBytes([SONG_POSITION_POINTER, lsb, msb])) def song_select(self, songNumber): """ songNumber: 0-127 """ self.event_slice(fromBytes([SONG_SELECT, songNumber])) def tuning_request(self): """ No values passed """ self.event_slice(chr(TUNING_REQUEST)) ######################### # header does not really belong here. But anyhoo!!! def header(self, format=0, nTracks=1, division=96): """ format: type of midi file in [0,1,2] nTracks: number of tracks. 1 track for type 0 file division: timing division ie. 96 ppq. """ raw = self.raw_out raw.writeSlice('MThd') bew = raw.writeBew bew(6, 4) # header size bew(format, 2) bew(nTracks, 2) bew(division, 2) def eof(self): """ End of file. No more events to be processed. """ # just write the file then. self.write() ##################### ## meta events def meta_slice(self, meta_type, data_slice): "Writes a meta event" slc = fromBytes([META_EVENT, meta_type]) + \ writeVar(len(data_slice)) + data_slice self.event_slice(slc) def meta_event(self, meta_type, data): """ Handles any undefined meta events """ self.meta_slice(meta_type, fromBytes(data)) def start_of_track(self, n_track=0): """ n_track: number of track """ self._current_track_buffer = RawOutstreamFile() self.reset_time() self._current_track += 1 def end_of_track(self): """ Writes the track to the buffer. """ raw = self.raw_out raw.writeSlice(TRACK_HEADER) track_data = self._current_track_buffer.getvalue() # wee need to know size of track data. eot_slice = writeVar(self.rel_time()) + fromBytes([META_EVENT, END_OF_TRACK, 0]) raw.writeBew(len(track_data)+len(eot_slice), 4) # then write raw.writeSlice(track_data) raw.writeSlice(eot_slice) def sequence_number(self, value): """ value: 0-65535 """ self.meta_slice(meta_type, writeBew(value, 2)) def text(self, text): """ Text event text: string """ self.meta_slice(TEXT, text) def copyright(self, text): """ Copyright notice text: string """ self.meta_slice(COPYRIGHT, text) def sequence_name(self, text): """ Sequence/track name text: string """ self.meta_slice(SEQUENCE_NAME, text) def instrument_name(self, text): """ text: string """ self.meta_slice(INSTRUMENT_NAME, text) def lyric(self, text): """ text: string """ self.meta_slice(LYRIC, text) def marker(self, text): """ text: string """ self.meta_slice(MARKER, text) def cuepoint(self, text): """ text: string """ self.meta_slice(CUEPOINT, text) def midi_ch_prefix(self, channel): """ channel: midi channel for subsequent data (deprecated in the spec) """ self.meta_slice(MIDI_CH_PREFIX, chr(channel)) def midi_port(self, value): """ value: Midi port (deprecated in the spec) """ self.meta_slice(MIDI_CH_PREFIX, chr(value)) def tempo(self, value): """ value: 0-2097151 tempo in us/quarternote (to calculate value from bpm: int(60,000,000.00 / BPM)) """ hb, mb, lb = (value>>16 & 0xff), (value>>8 & 0xff), (value & 0xff) self.meta_slice(TEMPO, fromBytes([hb, mb, lb])) def smtp_offset(self, hour, minute, second, frame, framePart): """ hour, minute, second: 3 bytes specifying the hour (0-23), minutes (0-59) and seconds (0-59), respectively. The hour should be encoded with the SMPTE format, just as it is in MIDI Time Code. frame: A byte specifying the number of frames per second (one of : 24, 25, 29, 30). framePart: A byte specifying the number of fractional frames, in 100ths of a frame (even in SMPTE-based tracks using a different frame subdivision, defined in the MThd chunk). """ self.meta_slice(SMTP_OFFSET, fromBytes([hour, minute, second, frame, framePart])) def time_signature(self, nn, dd, cc, bb): """ nn: Numerator of the signature as notated on sheet music dd: Denominator of the signature as notated on sheet music The denominator is a negative power of 2: 2 = quarter note, 3 = eighth, etc. cc: The number of MIDI clocks in a metronome click bb: The number of notated 32nd notes in a MIDI quarter note (24 MIDI clocks) """ self.meta_slice(TIME_SIGNATURE, fromBytes([nn, dd, cc, bb])) def key_signature(self, sf, mi): """ sf: is a byte specifying the number of flats (-ve) or sharps (+ve) that identifies the key signature (-7 = 7 flats, -1 = 1 flat, 0 = key of C, 1 = 1 sharp, etc). mi: is a byte specifying a major (0) or minor (1) key. """ self.meta_slice(KEY_SIGNATURE, fromBytes([sf, mi])) def sequencer_specific(self, data): """ data: The data as byte values """ self.meta_slice(SEQUENCER_SPECIFIC, data) #============================ convenient function ===================# # play a simple track (no pause, no overlap) on pys60 # track is in the form [(note1, dur1), (note2, dur2), ...] def play(track): m = MidiOutFile('C:\\out.mid') m.header() m.start_of_track() for note, dur in track: m.update_time(0) m.note_on(note=note) m.update_time(dur) m.note_off(note=note) m.update_time(0) m.end_of_track() m.eof() #os.startfile('C:\\out.mid') # for PC import audio, e32 s = audio.Sound.open('C:\\out.mid') s.play() while s.state()==2: # playing e32.ao_sleep(0.1) s.close()