neingeist
/
arduinisten
Archived
1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

192 lines
6.5 KiB
Python

# -*- coding: ISO-8859-1 -*-
# std library
from struct import unpack
# uhh I don't really like this, but there are so many constants to
# import otherwise
from constants import *
from EventDispatcher import EventDispatcher
class MidiFileParser:
"""
The MidiFileParser is the lowest level parser that see the data as
midi data. It generates events that gets triggered on the outstream.
"""
def __init__(self, raw_in, outstream):
"""
raw_data is the raw content of a midi file as a string.
"""
# internal values, don't mess with 'em directly
self.raw_in = raw_in
self.dispatch = EventDispatcher(outstream)
# Used to keep track of stuff
self._running_status = None
def parseMThdChunk(self):
"Parses the header chunk"
raw_in = self.raw_in
header_chunk_type = raw_in.nextSlice(4)
header_chunk_zise = raw_in.readBew(4)
# check if it is a proper midi file
if header_chunk_type != 'MThd':
raise TypeError, "It is not a valid midi file!"
# Header values are at fixed locations, so no reason to be clever
self.format = raw_in.readBew(2)
self.nTracks = raw_in.readBew(2)
self.division = raw_in.readBew(2)
# Theoretically a header larger than 6 bytes can exist
# but no one has seen one in the wild
# But correctly ignore unknown data if it is though
if header_chunk_zise > 6:
raw_in.moveCursor(header_chunk_zise-6)
# call the header event handler on the stream
self.dispatch.header(self.format, self.nTracks, self.division)
def parseMTrkChunk(self):
"Parses a track chunk. This is the most important part of the parser."
# set time to 0 at start of a track
self.dispatch.reset_time()
dispatch = self.dispatch
raw_in = self.raw_in
# Trigger event at the start of a track
dispatch.start_of_track(self._current_track)
# position cursor after track header
raw_in.moveCursor(4)
# unsigned long is 4 bytes
tracklength = raw_in.readBew(4)
track_endposition = raw_in.getCursor() + tracklength # absolute position!
while raw_in.getCursor() < track_endposition:
# find relative time of the event
time = raw_in.readVarLen()
dispatch.update_time(time)
# be aware of running status!!!!
peak_ahead = raw_in.readBew(move_cursor=0)
if (peak_ahead & 0x80):
# the status byte has the high bit set, so it
# was not running data but proper status byte
status = self._running_status = raw_in.readBew()
else:
# use that darn running status
status = self._running_status
# could it be illegal data ?? Do we need to test for that?
# I need more example midi files to be shure.
# Also, while I am almost certain that no realtime
# messages will pop up in a midi file, I might need to
# change my mind later.
# we need to look at nibbles here
hi_nible, lo_nible = status & 0xF0, status & 0x0F
# match up with events
# Is it a meta_event ??
# these only exists in midi files, not in transmitted midi data
# In transmitted data META_EVENT (0xFF) is a system reset
if status == META_EVENT:
meta_type = raw_in.readBew()
meta_length = raw_in.readVarLen()
meta_data = raw_in.nextSlice(meta_length)
dispatch.meta_event(meta_type, meta_data)
# Is it a sysex_event ??
elif status == SYSTEM_EXCLUSIVE:
# ignore sysex events
sysex_length = raw_in.readVarLen()
# don't read sysex terminator
sysex_data = raw_in.nextSlice(sysex_length-1)
# only read last data byte if it is a sysex terminator
# It should allways be there, but better safe than sorry
if raw_in.readBew(move_cursor=0) == END_OFF_EXCLUSIVE:
eo_sysex = raw_in.readBew()
dispatch.sysex_event(sysex_data)
# the sysex code has not been properly tested, and might be fishy!
# is it a system common event?
elif hi_nible == 0xF0: # Hi bits are set then
data_sizes = {
MTC:1,
SONG_POSITION_POINTER:2,
SONG_SELECT:1,
}
data_size = data_sizes.get(hi_nible, 0)
common_data = raw_in.nextSlice(data_size)
common_type = lo_nible
dispatch.system_common(common_type, common_data)
# Oh! Then it must be a midi event (channel voice message)
else:
data_sizes = {
PATCH_CHANGE:1,
CHANNEL_PRESSURE:1,
NOTE_OFF:2,
NOTE_ON:2,
AFTERTOUCH:2,
CONTINUOUS_CONTROLLER:2,
PITCH_BEND:2,
}
data_size = data_sizes.get(hi_nible, 0)
channel_data = raw_in.nextSlice(data_size)
event_type, channel = hi_nible, lo_nible
dispatch.channel_messages(event_type, channel, channel_data)
def parseMTrkChunks(self):
"Parses all track chunks."
for t in range(self.nTracks):
self._current_track = t
self.parseMTrkChunk() # this is where it's at!
self.dispatch.eof()
if __name__ == '__main__':
# get data
test_file = 'test/midifiles/minimal.mid'
test_file = 'test/midifiles/cubase-minimal.mid'
test_file = 'test/midifiles/Lola.mid'
# f = open(test_file, 'rb')
# raw_data = f.read()
# f.close()
#
#
# # do parsing
from MidiToText import MidiToText
from RawInstreamFile import RawInstreamFile
midi_in = MidiFileParser(RawInstreamFile(test_file), MidiToText())
midi_in.parseMThdChunk()
midi_in.parseMTrkChunks()