midifile.py revision 6b065d0f7161fe54e6f58fd2b8ad6c650b2d3657
1"""
2 File:
3 midifile.py
4
5 Contents and purpose:
6 Utilities used throughout JetCreator
7
8 Copyright (c) 2008 Android Open Source Project
9
10 Licensed under the Apache License, Version 2.0 (the "License");
11 you may not use this file except in compliance with the License.
12 You may obtain a copy of the License at
13
14      http://www.apache.org/licenses/LICENSE-2.0
15
16 Unless required by applicable law or agreed to in writing, software
17 distributed under the License is distributed on an "AS IS" BASIS,
18 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 See the License for the specific language governing permissions and
20 limitations under the License.
21"""
22
23import logging
24import struct
25import copy
26import array
27
28# JET events
29JET_EVENT_MARKER = 102
30JET_MARKER_LOOP_END = 0
31JET_EVENT_TRIGGER_CLIP = 103
32
33# header definitions
34SMF_HEADER_FMT = '>4slHHH'
35SMF_RIFF_TAG = 'MThd'
36
37SMF_TRACK_HEADER_FMT = '>4sl'
38SMF_TRACK_RIFF_TAG = 'MTrk'
39
40# defaults
41DEFAULT_PPQN = 120
42DEFAULT_BEATS_PER_MEASURE = 4
43DEFAULT_TIME_FORMAT = '%03d:%02d:%03d'
44
45# force note-offs to end of list
46MAX_SEQ_NUM = 0x7fffffff
47
48# MIDI messages
49NOTE_OFF = 0x80
50NOTE_ON = 0x90
51POLY_KEY_PRESSURE = 0xa0
52CONTROL_CHANGE = 0xb0
53PROGRAM_CHANGE = 0xc0
54CHANNEL_PRESSURE = 0xd0
55PITCH_BEND = 0xe0
56
57# System common messages
58SYSEX = 0xf0
59MIDI_TIME_CODE = 0xf1
60SONG_POSITION_POINTER = 0xf2
61SONG_SELECT = 0xf3
62RESERVED_F4 = 0xf4
63RESERVED_F5 = 0xf5
64TUNE_REQUEST = 0xf6
65END_SYSEX = 0xf7
66
67# System real-time messages
68TIMING_CLOCK = 0xf8
69RESERVED_F9 = 0xf9
70START = 0xfa
71CONTINUE = 0xfb
72STOP = 0xfc
73RESERVED_FD = 0xfd
74ACTIVE_SENSING = 0xfe
75SYSTEM_RESET = 0xff
76
77ONE_BYTE_MESSAGES = (
78	TUNE_REQUEST,
79	TIMING_CLOCK,
80	RESERVED_F9,
81	START,
82	CONTINUE,
83	STOP,
84	RESERVED_FD,
85	ACTIVE_SENSING,
86	SYSTEM_RESET)
87
88THREE_BYTE_MESSAGES = (
89	NOTE_OFF,
90	NOTE_ON,
91	POLY_KEY_PRESSURE,
92	CONTROL_CHANGE,
93	PITCH_BEND)
94
95MIDI_MESSAGES = (
96	NOTE_OFF,
97	NOTE_ON,
98	POLY_KEY_PRESSURE,
99	CONTROL_CHANGE,
100	CHANNEL_PRESSURE,
101	PITCH_BEND,
102	SYSEX)
103
104# Meta-events
105META_EVENT = 0xff
106META_EVENT_SEQUENCE_NUMBER = 0x00
107META_EVENT_TEXT_EVENT = 0x01
108META_EVENT_COPYRIGHT_NOTICE = 0x02
109META_EVENT_SEQUENCE_TRACK_NAME = 0x03
110META_EVENT_INSTRUMENT_NAME = 0x04
111META_EVENT_LYRIC = 0x05
112META_EVENT_MARKER = 0x06
113META_EVENT_CUE_POINT = 0x07
114META_EVENT_MIDI_CHANNEL_PREFIX = 0x20
115META_EVENT_END_OF_TRACK = 0x2f
116META_EVENT_SET_TEMPO = 0x51
117META_EVENT_SMPTE_OFFSET = 0x54
118META_EVENT_TIME_SIGNATURE = 0x58
119META_EVENT_KEY_SIGNATURE = 0x59
120META_EVENT_SEQUENCER_SPECIFIC = 0x7f
121
122# recurring error messages
123MSG_NOT_SMF_FILE = 'Not an SMF file - aborting parse!'
124MSG_INVALID_TRACK_HEADER = 'Track header is invalid'
125MSG_TYPE_MISMATCH = 'msg_type does not match event type'
126
127LARGE_TICK_WARNING = 1000
128
129# default control values
130CTRL_BANK_SELECT_MSB = 0
131CTRL_MOD_WHEEL = 1
132CTRL_RPN_DATA_MSB = 6
133CTRL_VOLUME = 7
134CTRL_PAN = 10
135CTRL_EXPRESSION = 11
136CTRL_BANK_SELECT_LSB = 32
137CTRL_RPN_DATA_LSB = 38
138CTRL_SUSTAIN = 64
139CTRL_RPN_LSB = 100
140CTRL_RPN_MSB = 101
141CTRL_RESET_CONTROLLERS = 121
142
143RPN_PITCH_BEND_SENSITIVITY = 0
144RPN_FINE_TUNING = 1
145RPN_COARSE_TUNING = 2
146
147MONITOR_CONTROLLERS = (
148	CTRL_BANK_SELECT_MSB,
149	CTRL_MOD_WHEEL,
150	CTRL_RPN_DATA_MSB,
151	CTRL_VOLUME,
152	CTRL_PAN,
153	CTRL_EXPRESSION,
154	CTRL_BANK_SELECT_LSB,
155	CTRL_RPN_DATA_LSB,
156	CTRL_SUSTAIN,
157	CTRL_RPN_LSB,
158	CTRL_RPN_MSB)
159
160MONITOR_RPNS = (
161	RPN_PITCH_BEND_SENSITIVITY,
162	RPN_FINE_TUNING,
163	RPN_COARSE_TUNING)
164
165RPN_PITCH_BEND_SENSITIVITY = 0
166RPN_FINE_TUNING = 1
167RPN_COARSE_TUNING = 2
168
169DEFAULT_CONTROLLER_VALUES = {
170	CTRL_BANK_SELECT_MSB : 121,
171	CTRL_MOD_WHEEL : 0,
172	CTRL_RPN_DATA_MSB : 0,
173	CTRL_VOLUME : 100,
174	CTRL_PAN : 64,
175	CTRL_EXPRESSION : 127,
176	CTRL_RPN_DATA_LSB : 0,
177	CTRL_BANK_SELECT_LSB : 0,
178	CTRL_SUSTAIN : 0,
179	CTRL_RPN_LSB : 0x7f,
180	CTRL_RPN_MSB : 0x7f}
181
182DEFAULT_RPN_VALUES = {
183	RPN_PITCH_BEND_SENSITIVITY : 0x100,
184	RPN_FINE_TUNING : 0,
185	RPN_COARSE_TUNING : 1}
186
187# initialize logger
188midi_file_logger = logging.getLogger('MIDI_file')
189midi_file_logger.setLevel(logging.NOTSET)
190
191
192class trackGrid(object):
193	def __init__ (self, track, channel, name, empty):
194		self.track = track
195		self.channel = channel
196		self.name = name
197		self.empty = empty
198	def __str__ (self):
199		return "['%s', '%s', '%s']" % (self.track, self.channel, self.name)
200
201
202#---------------------------------------------------------------
203# MIDIFileException
204#---------------------------------------------------------------
205class MIDIFileException (Exception):
206	def __init__ (self, stream, msg):
207		stream.error_loc = stream.tell()
208		self.stream = stream
209		self.msg = msg
210	def __str__ (self):
211		return '[%d]: %s' % (self.stream.error_loc, self.msg)
212
213#---------------------------------------------------------------
214# TimeBase
215#---------------------------------------------------------------
216class TimeBase (object):
217	def __init__ (self, ppqn=DEFAULT_PPQN, beats_per_measure=DEFAULT_BEATS_PER_MEASURE):
218		self.ppqn = ppqn
219		self.beats_per_measure = beats_per_measure
220
221	def ConvertToTicks (self, measures, beats, ticks):
222		total_beats = beats + (measures * self.beats_per_measure)
223		total_ticks = ticks + (total_beats * self.ppqn)
224		return total_ticks
225
226	def ConvertTicksToMBT (self, ticks):
227		beats = ticks / self.ppqn
228		ticks -= beats * self.ppqn
229		measures = beats / self.beats_per_measure
230		beats -= measures * self.beats_per_measure
231		return (measures, beats, ticks)
232
233	def ConvertTicksToStr (self, ticks, format=DEFAULT_TIME_FORMAT):
234		measures, beats, ticks = self.ConvertTicksToMBT(ticks)
235		return format % (measures, beats, ticks)
236
237	def ConvertStrTimeToTuple(self, s):
238		try:
239			measures, beats, ticks = s.split(':',3)
240			return (int(measures), int(beats), int(ticks))
241		except:
242			return (0,0,0)
243
244	def ConvertStrTimeToTicks(self, s):
245		measures, beats, ticks = self.ConvertStrTimeToTuple(s)
246		return self.ConvertToTicks(measures, beats, ticks)
247
248	def MbtDifference(self, mbt1, mbt2):
249		t1 = self.ConvertToTicks(mbt1[0], mbt1[1], mbt1[2])
250		t2 = self.ConvertToTicks(mbt2[0], mbt2[1], mbt2[2])
251		return abs(t1-t2)
252
253
254#---------------------------------------------------------------
255# Helper functions
256#---------------------------------------------------------------
257def ReadByte (stream):
258	try:
259		return ord(stream.read(1))
260	except TypeError:
261		stream.error_loc = stream.tell()
262		raise MIDIFileException(stream, 'Unexpected EOF')
263
264def ReadBytes (stream, length):
265	bytes = []
266	for i in range(length):
267		bytes.append(ReadByte(stream))
268	return bytes
269
270def ReadVarLenQty (stream):
271	value = 0
272	while 1:
273		byte = ReadByte(stream)
274		value = (value << 7) + (byte & 0x7f)
275		if byte & 0x80 == 0:
276			return value
277
278def WriteByte (stream, value):
279	stream.write(chr(value))
280
281def WriteBytes (stream, bytes):
282	for byte in bytes:
283		WriteByte(stream, byte)
284
285def WriteVarLenQty (stream, value):
286	bytes = [value & 0x7f]
287	value = value >> 7
288	while value > 0:
289		bytes.append((value & 0x7f) | 0x80)
290		value = value >> 7
291	bytes.reverse()
292	WriteBytes(stream, bytes)
293
294#---------------------------------------------------------------
295# EventFilter
296#---------------------------------------------------------------
297class EventFilter (object):
298	pass
299
300class EventTypeFilter (object):
301	def __init__ (self, events, exclude=True):
302		self.events = events
303		self.exclude = exclude
304	def Check (self, event):
305		if event.msg_type in self.events:
306			return not self.exclude
307		return self.exclude
308
309class NoteFilter (EventFilter):
310	def __init__ (self, notes, exclude=True):
311		self.notes = notes
312		self.exclude = exclude
313	def Check (self, event):
314		if event.msg_type in (NOTE_ON, NOTE_OFF):
315			if event.note in self.notes:
316				return not self.exclude
317		return self.exclude
318
319class ChannelFilter (EventFilter):
320	def __init__ (self, channel, exclude=True):
321		self.channel = channel
322		self.exclude = exclude
323	def Check (self, event):
324		if event.msg_type in (NOTE_ON, NOTE_OFF, POLY_KEY_PRESSURE, CONTROL_CHANGE, CHANNEL_PRESSURE, PITCH_BEND):
325			if event.channel in self.channel:
326				return not self.exclude
327		return self.exclude
328
329#---------------------------------------------------------------
330# MIDIEvent
331#---------------------------------------------------------------
332class MIDIEvent (object):
333	"""Factory for creating MIDI events from a stream."""
334	@staticmethod
335	def ReadFromStream (stream, seq, ticks, msg_type):
336		if msg_type == SYSEX:
337			return SysExEvent.ReadFromStream(stream, seq, ticks, msg_type)
338		elif msg_type == END_SYSEX:
339			return SysExContEvent.ReadFromStream(stream, seq, ticks, msg_type)
340		elif msg_type == META_EVENT:
341			return MetaEvent.ReadFromStream(stream, seq, ticks, msg_type)
342		else:
343			high_nibble = msg_type & 0xf0
344			if high_nibble == NOTE_OFF:
345				return NoteOffEvent.ReadFromStream(stream, seq, ticks, msg_type)
346			elif high_nibble == NOTE_ON:
347				return NoteOnEvent.ReadFromStream(stream, seq, ticks, msg_type)
348			elif high_nibble == POLY_KEY_PRESSURE:
349				return PolyKeyPressureEvent.ReadFromStream(stream, seq, ticks, msg_type)
350			elif high_nibble == CONTROL_CHANGE:
351				return ControlChangeEvent.ReadFromStream(stream, seq, ticks, msg_type)
352			elif high_nibble == PROGRAM_CHANGE:
353				return ProgramChangeEvent.ReadFromStream(stream, seq, ticks, msg_type)
354			elif high_nibble == CHANNEL_PRESSURE:
355				return ChannelPressureEvent.ReadFromStream(stream, seq, ticks, msg_type)
356			elif high_nibble == PITCH_BEND:
357				return PitchBendEvent.ReadFromStream(stream, seq, ticks, msg_type)
358			else:
359				stream.Warning('Ignoring unexpected message type 0x%02x' % msg_type)
360	def WriteTicks (self, stream, track):
361		WriteVarLenQty(stream, self.ticks - track.ticks)
362		track.ticks = self.ticks
363	def WriteRunningStatus (self, stream, track, filters, msg, data1, data2=None):
364		if not self.CheckFilters(filters):
365			return
366		self.WriteTicks(stream, track)
367		status = msg + self.channel
368		if track.running_status != status:
369			WriteByte(stream, status)
370			track.running_status = status
371		WriteByte(stream, data1)
372		if data2 is not None:
373			WriteByte(stream, data2)
374	def CheckFilters (self, filters):
375		if filters is None or not len(filters):
376			return True
377
378		# never filter meta-events
379		if (self.msg_type == META_EVENT) and (self.meta_type == META_EVENT_END_OF_TRACK):
380			return True
381
382		# check all filters
383		for f in filters:
384			if not f.Check(self):
385				return False
386		return True
387
388	def TimeEventStr (self, timebase):
389		return '[%s]: %s' % (timebase.ConvertTicksToStr(self.ticks), self.__str__())
390
391#---------------------------------------------------------------
392# NoteOffEvent
393#---------------------------------------------------------------
394class NoteOffEvent (MIDIEvent):
395	def __init__ (self, ticks, seq, channel, note, velocity):
396		self.name = 'NoteOff'
397		self.msg_type = NOTE_OFF
398		self.seq = seq
399		self.ticks = ticks
400		self.channel = channel
401		self.note = note
402		self.velocity = velocity
403	@staticmethod
404	def ReadFromStream (stream, seq, ticks, msg_type):
405		ticks = ticks
406		channel = msg_type & 0x0f
407		note = ReadByte(stream)
408		velocity = ReadByte(stream)
409		if msg_type & 0xf0 != NOTE_OFF:
410			stream.seek(-2,1)
411			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
412		return NoteOffEvent(ticks, seq, channel, note, velocity)
413	def WriteToStream (self, stream, track, filters=None):
414		# special case for note-off using zero velocity
415		if self.velocity > 0:
416			self.WriteRunningStatus(stream, track, filters, NOTE_ON, self.note, self.velocity)
417		if track.running_status == (NOTE_OFF + self.channel):
418			self.WriteRunningStatus(stream, track, filters, NOTE_ON, self.note, self.velocity)
419		else:
420			self.WriteRunningStatus(stream, track, filters, NOTE_ON, self.note, 0)
421	def __str__ (self):
422		return '%s: ch=%d n=%d v=%d' % (self.name, self.channel, self.note, self.velocity)
423
424#---------------------------------------------------------------
425# NoteOnEvent
426#---------------------------------------------------------------
427class NoteOnEvent (MIDIEvent):
428	def __init__ (self, ticks, seq, channel, note, velocity, note_length, note_off_velocity):
429		self.name = 'NoteOn'
430		self.msg_type = NOTE_ON
431		self.ticks = ticks
432		self.seq = seq
433		self.channel = channel
434		self.note = note
435		self.velocity = velocity
436		self.note_length = note_length
437		self.note_off_velocity = note_off_velocity
438	@staticmethod
439	def ReadFromStream (stream, seq, ticks, msg_type):
440		channel = msg_type & 0x0f
441		note = ReadByte(stream)
442		velocity = ReadByte(stream)
443		if msg_type & 0xf0 != NOTE_ON:
444			stream.seek(-2,1)
445			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
446		if velocity == 0:
447			return NoteOffEvent(ticks, seq, channel, note, velocity)
448		return NoteOnEvent(ticks, seq, channel, note, velocity, None, None)
449	def WriteToStream (self, stream, track, filters=None):
450		self.WriteRunningStatus(stream, track, filters, NOTE_ON, self.note, self.velocity)
451	def __str__ (self):
452		if self.note_length is not None:
453			return '%s: ch=%d n=%d v=%d l=%d' % (self.name, self.channel, self.note, self.velocity, self.note_length)
454		else:
455			return '%s: ch=%d n=%d v=%d' % (self.name, self.channel, self.note, self.velocity)
456
457#---------------------------------------------------------------
458# PolyKeyPressureEvent
459#---------------------------------------------------------------
460class PolyKeyPressureEvent (MIDIEvent):
461	def __init__ (self, ticks, seq, channel, note, value):
462		self.name = 'PolyKeyPressure'
463		self.msg_type = POLY_KEY_PRESSURE
464		self.ticks = ticks
465		self.seq = seq
466		self.channel = channel
467		self.note = note
468		self.value = value
469	@staticmethod
470	def ReadFromStream (stream, seq, ticks, msg_type):
471		channel = msg_type & 0x0f
472		note = ReadByte(stream)
473		value = ReadByte(stream)
474		if msg_type & 0xf0 != POLY_KEY_PRESSURE:
475			stream.seek(-2,1)
476			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
477		return PolyKeyPressureEvent(ticks, seq, channel, note, value)
478	def WriteToStream (self, stream, track, filters=None):
479		self.WriteRunningStatus(stream, track, filters, POLY_KEY_PRESSURE, self.note, self.value)
480	def __str__ (self):
481		return '%s: ch=%d n=%d v=%d' % (self.name, self.channel, self.note, self.value)
482
483#---------------------------------------------------------------
484# ControlChangeEvent
485#---------------------------------------------------------------
486class ControlChangeEvent (MIDIEvent):
487	def __init__ (self, ticks, seq, channel, controller, value):
488		self.name = 'ControlChange'
489		self.msg_type = CONTROL_CHANGE
490		self.ticks = ticks
491		self.seq = seq
492		self.channel = channel
493		self.controller = controller
494		self.value = value
495	@staticmethod
496	def ReadFromStream (stream, seq, ticks, msg_type):
497		channel = msg_type & 0x0f
498		controller = ReadByte(stream)
499		value = ReadByte(stream)
500		if msg_type & 0xf0 != CONTROL_CHANGE:
501			stream.seek(-2,1)
502			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
503		if controller >= 120:
504			return ChannelModeEvent(ticks, seq, channel, controller, value)
505		return ControlChangeEvent(ticks, seq, channel, controller, value)
506	def WriteToStream (self, stream, track, filters=None):
507		self.WriteRunningStatus(stream, track, filters, CONTROL_CHANGE, self.controller, self.value)
508	def __str__ (self):
509		return '%s: ch=%d c=%d v=%d' % (self.name, self.channel, self.controller, self.value)
510
511#---------------------------------------------------------------
512# ChannelModeEvent
513#---------------------------------------------------------------
514class ChannelModeEvent (MIDIEvent):
515	def __init__ (self, ticks, seq, channel, controller, value):
516		self.name = 'ChannelMode'
517		self.msg_type = CONTROL_CHANGE
518		self.ticks = ticks
519		self.seq = seq
520		self.channel = channel
521		self.controller = controller
522		self.value = value
523	@staticmethod
524	def ReadFromStream (stream, seq, ticks, msg_type):
525		channel = msg_type & 0x0f
526		controller = ReadByte(stream)
527		value = ReadByte(stream)
528		if msg_type & 0xf0 != CONTROL_CHANGE:
529			stream.seek(-2,1)
530			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
531		if  controller < 120:
532			return ControlChangeEvent(ticks, seq, channel, controller, value)
533		return ChannelModeEvent(ticks, seq, channel, value)
534	def WriteToStream (self, stream, track, filters=None):
535		self.WriteRunningStatus(stream, track, filters, CONTROL_CHANGE, self.controller, self.value)
536	def __str__ (self):
537		return '%s: ch=%d c=%d v=%d' % (self.name, self.channel, self.controller, self.value)
538
539#---------------------------------------------------------------
540# ProgramChangeEvent
541#---------------------------------------------------------------
542class ProgramChangeEvent (MIDIEvent):
543	def __init__ (self, ticks, seq, channel, program):
544		self.name = 'ProgramChange'
545		self.msg_type = PROGRAM_CHANGE
546		self.ticks = ticks
547		self.seq = seq
548		self.channel = channel
549		self.program = program
550	@staticmethod
551	def ReadFromStream (stream, seq, ticks, msg_type):
552		channel = msg_type & 0x0f
553		program = ReadByte(stream)
554		if msg_type & 0xf0 != PROGRAM_CHANGE:
555			stream.seek(-1,1)
556			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
557		return ProgramChangeEvent(ticks, seq, channel, program)
558	def WriteToStream (self, stream, track, filters=None):
559		self.WriteRunningStatus(stream, track, filters, PROGRAM_CHANGE, self.program)
560	def __str__ (self):
561		return '%s: ch=%d p=%d' % (self.name, self.channel, self.program)
562
563#---------------------------------------------------------------
564# ChannelPressureEvent
565#---------------------------------------------------------------
566class ChannelPressureEvent (MIDIEvent):
567	def __init__ (self, ticks, seq, channel, value):
568		self.name = 'ChannelPressure'
569		self.msg_type = CHANNEL_PRESSURE
570		self.ticks = ticks
571		self.seq = seq
572		self.channel = channel
573		self.value = value
574	@staticmethod
575	def ReadFromStream (stream, seq, ticks, msg_type):
576		channel = msg_type & 0x0f
577		value = ReadByte(stream)
578		if msg_type & 0xf0 != CHANNEL_PRESSURE:
579			stream.seek(-1,1)
580			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
581		return ChannelPressureEvent(ticks, seq, channel, value)
582	def WriteToStream (self, stream, track, filters=None):
583		self.WriteRunningStatus(stream, track, filters, CHANNEL_PRESSURE, self.value)
584	def __str__ (self):
585		return '%s: ch=%d v=%d' % (self.name, self.channel, self.value)
586
587#---------------------------------------------------------------
588# PitchBendEvent
589#---------------------------------------------------------------
590class PitchBendEvent (MIDIEvent):
591	def __init__ (self, ticks, seq, channel, value):
592		self.name = 'PitchBend'
593		self.msg_type = PITCH_BEND
594		self.ticks = ticks
595		self.seq = seq
596		self.channel = channel
597		self.value = value
598	@staticmethod
599	def ReadFromStream (stream, seq, ticks, msg_type):
600		channel = msg_type & 0x0f
601		value = (ReadByte(stream) << 7) + ReadByte(stream) - 0x2000
602		if msg_type & 0xf0 != PITCH_BEND:
603			stream.seek(-2,1)
604			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
605		return PitchBendEvent(ticks, seq, channel, value)
606	def WriteToStream (self, stream, track, filters=None):
607		value = self.value + 0x2000
608		if value < 0:
609			value = 0
610		if value > 0x3fff:
611			value = 0x3fff
612		self.WriteRunningStatus(stream, track, filters, PITCH_BEND, value >> 7, value & 0x7f)
613	def __str__ (self):
614		return '%s: ch=%d v=%d' % (self.name, self.channel, self.value)
615
616#---------------------------------------------------------------
617# SysExEvent
618#---------------------------------------------------------------
619class SysExEvent (MIDIEvent):
620	def __init__ (self, ticks, seq, msg):
621		self.name = 'SysEx'
622		self.msg_type = SYSEX
623		self.ticks = ticks
624		self.seq = seq
625		self.length = len(msg)
626		self.msg = msg
627	@staticmethod
628	def ReadFromStream (stream, seq, ticks, msg_type):
629		pos = stream.tell()
630		length = ReadVarLenQty(stream)
631		msg = ReadBytes(stream, length)
632		if msg_type != SYSEX:
633			stream.seek(pos,0)
634			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
635		return SysExEvent(ticks, seq, msg)
636	def WriteToStream (self, stream, track, filters=None):
637		if not self.CheckFilters(filters):
638			return
639		self.WriteTicks(stream, track)
640		WriteByte(stream, SYSEX)
641		WriteVarLenQty(stream, self.length)
642		WriteBytes(stream, self.msg)
643		track.running_status = None
644	def __str__ (self):
645		fmt_str = '%s: f0' + ' %02x'*self.length
646		return fmt_str % ((self.name,) + tuple(self.msg))
647
648#---------------------------------------------------------------
649# SysExContEvent
650#---------------------------------------------------------------
651class SysExContEvent (MIDIEvent):
652	def __init__ (self, ticks, seq, msg):
653		self.name = 'SysEx+'
654		self.msg_type = END_SYSEX
655		self.ticks = ticks
656		self.seq = seq
657		self.length = len(msg)
658		self.msg = msg
659	@staticmethod
660	def ReadFromStream (stream, seq, ticks, msg_type):
661		pos = stream.tell()
662		length = ReadVarLenQty(stream)
663		msg = ReadBytes(stream, length)
664		if msg_type != END_SYSEX:
665			stream.seek(pos,0)
666			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
667		return SysExContEvent(ticks, seq, msg)
668	def WriteToStream (self, stream, track, filters=None):
669		if not self.CheckFilters(filters):
670			return
671		self.WriteTicks(stream, track)
672		WriteByte(stream, END_SYSEX)
673		WriteVarLenQty(stream, self.length)
674		WriteBytes(stream, self.msg)
675		track.running_status = None
676	def __str__ (self):
677		fmt_str = '%s:' + ' %02x'*self.length
678		return fmt_str % ((self.name,) + tuple(self.msg))
679
680#---------------------------------------------------------------
681# MetaEvent
682#---------------------------------------------------------------
683class MetaEvent (MIDIEvent):
684	def __init__ (self, ticks, seq, meta_type, msg):
685		self.name = 'MetaEvent'
686		self.msg_type = META_EVENT
687		self.ticks = ticks
688		self.seq = seq
689		self.meta_type = meta_type
690		self.length = len(msg)
691		self.msg = msg
692	@staticmethod
693	def ReadFromStream (stream, seq, ticks, msg_type):
694		pos = stream.tell()
695		meta_type = ReadByte(stream)
696		length = ReadVarLenQty(stream)
697		msg = ReadBytes(stream, length)
698		if msg_type != META_EVENT:
699			stream.seek(pos,0)
700			raise MIDIFileException(stream, MSG_TYPE_MISMATCH)
701		obj = MetaEvent(ticks, seq, meta_type, msg)
702		return obj
703	def WriteToStream (self, stream, track, filters=None):
704		if not self.CheckFilters(filters):
705			return
706		self.WriteTicks(stream, track)
707		WriteByte(stream, META_EVENT)
708		WriteByte(stream, self.meta_type)
709		WriteVarLenQty(stream, self.length)
710		WriteBytes(stream, self.msg)
711		track.running_status = None
712	def __str__ (self):
713		fmt_str = '%s: %02x' + ' %02x'*self.length
714		return fmt_str % ((self.name, self.meta_type) + tuple(self.msg))
715
716#---------------------------------------------------------------
717# MIDIControllers
718#---------------------------------------------------------------
719class MIDIControllers (object):
720	def __init__ (self):
721		self.controllers = []
722		self.rpns = []
723		for channel in range(16):
724			self.controllers.append({})
725			self.controllers[channel] = copy.deepcopy(DEFAULT_CONTROLLER_VALUES)
726			self.rpns.append({})
727			self.rpns[channel] = copy.deepcopy(DEFAULT_RPN_VALUES)
728		self.pitchbend = [0] * 16
729		self.program = [-1] * 16
730		self.pressure = [0] * 16
731
732	def __str__ (self):
733		output = []
734		for channel in range(16):
735			output.append('channel=%d' % channel)
736			output.append('  program=%d' % self.program[channel])
737			output.append('  pressure=%d' % self.pressure[channel])
738
739			output.append('  controllers')
740			for controller in self.controllers[channel].keys():
741				output.append('    %03d: %03d' % (controller, self.controllers[channel][controller]))
742
743			output.append('  rpns')
744			for rpn in self.rpns[channel].keys():
745				output.append('    %05d: %05d>' % (controller, self.rpns[channel][rpn]))
746		return '\n'.join(output)
747
748
749	def Event (self, event):
750		"""Process an event and save any changes in controller values"""
751		# process control changes
752		if event.msg_type == CONTROL_CHANGE:
753			self.ControlChange(event)
754		elif event.msg_type == CHANNEL_PRESSURE:
755			self.PressureChange(event)
756		elif event.msg_type == PROGRAM_CHANGE:
757			self.ProgramChange(event)
758		elif event.msg_type == PITCH_BEND:
759			self.PitchBendChange(event)
760
761	def PitchBendChange (self, event):
762		"""Monitor pitch bend change."""
763		self.pitchbend[event.channel] = event.value
764
765	def ProgramChange (self, event):
766		"""Monitor program change."""
767		self.program[event.channel] = event.program
768
769	def ControlChange (self, event):
770		"""Monitor control change."""
771		controller = event.controller
772		if controller in MONITOR_CONTROLLERS:
773			channel = event.channel
774			self.controllers[channel][controller] = event.value
775			if (controller == CTRL_RPN_DATA_MSB) or (controller == CTRL_RPN_DATA_LSB):
776				rpn = (self.controllers[channel][CTRL_RPN_MSB] << 7) + self.controllers[channel][CTRL_RPN_LSB]
777				if rpn in MONITOR_RPNS:
778					value = (self.controllers[channel][CTRL_RPN_DATA_MSB] << 7) + self.controllers[channel][CTRL_RPN_DATA_LSB]
779					self.rpns[channel][rpn] = value
780
781		# reset controllers
782		elif event.controller == CTRL_RESET_CONTROLLERS:
783			self.ResetControllers[event.channel]
784
785	def PressureChange (self, event):
786		"""Monitor pressure change."""
787		self.pressure[event.channel] = event.value
788
789	def ResetControllers (self, channel):
790		"""Reset controllers to default."""
791		self.controllers[channel] = DEFAULT_CONTROLLER_VALUES
792		self.rpns[channel] = DEFAULT_RPN_VALUES
793		self.pressure[channel] = 0
794
795	def GenerateEventList (self, ticks, ref_values=None):
796		"""Generate an event list based on controller differences."""
797		events = EventList()
798
799		# if no reference values, based on default values
800		if ref_values is None:
801			ref_values = MIDIControllers()
802
803		# iterate through 16 MIDI channels
804		for channel in range(16):
805
806			# generate RPN changes
807			for rpn in self.rpns[channel].keys():
808				value = self.rpns[channel][rpn]
809				if value != ref_values.rpns[channel][rpn]:
810					events.append(ControlChangeEvent(ticks, -1, channel, CTRL_RPN_MSB, rpn >> 7))
811					events.append(ControlChangeEvent(ticks, -1, channel, CTRL_RPN_LSB, rpn & 0x7f))
812					events.append(ControlChangeEvent(ticks, -1, channel, CTRL_RPN_DATA_MSB, value >> 7))
813					events.append(ControlChangeEvent(ticks, -1, channel, CTRL_RPN_DATA_LSB, value & 0x7f))
814
815			# generate controller changes
816			for controller in self.controllers[channel].keys():
817				if self.controllers[channel][controller] != ref_values.controllers[channel][controller]:
818					events.append(ControlChangeEvent(ticks, -1, channel, controller, self.controllers[channel][controller]))
819
820			# generate pressure changes
821			if self.pressure[channel] != ref_values.pressure[channel]:
822				events.append(ChannelPressureEvent(ticks, -1, channel, self.pressure[channel]))
823
824			# generate program changes
825			if self.program[channel] != ref_values.program[channel]:
826				if self.program[channel] in range(128):
827					events.append(ProgramChangeEvent(ticks, -1, channel, self.program[channel]))
828
829			# generate pitch bend changes
830			if self.pitchbend[channel] != ref_values.pitchbend[channel]:
831				if self.pitchbend[channel] in range(-8192,8191):
832					events.append(PitchBendEvent(ticks, -1, channel, self.pitchbend[channel]))
833
834		return events
835
836#---------------------------------------------------------------
837# EventList
838#---------------------------------------------------------------
839class EventList (list):
840	def __init__ (self):
841		list.__init__(self)
842
843	def FixNoteLengths (self):
844		midi_file_logger.debug('Fix note lengths')
845
846		# search for note-on's in event list
847		for index in range(len(self)):
848			event = self[index]
849			if event.msg_type == NOTE_ON:
850				note_off_ticks = event.ticks + event.note_length
851
852				# check for note-on occuring before end of current note
853				for i in range(index + 1, len(self)):
854					event_to_check = self[i]
855					if event_to_check.ticks >= note_off_ticks:
856						break
857
858					# adjust note length
859					if (event_to_check.msg_type == NOTE_ON) and (event_to_check.note == event.note):
860						midi_file_logger.debug('Adjusting note length @ %d' % event.ticks)
861						event.note_length = event_to_check.ticks - event.ticks
862						break
863
864	def ChaseControllers (self, end_seq, start_seq = 0, values = None):
865		midi_file_logger.debug('ChaseControllers from %d to %d' % (start_seq, end_seq))
866
867		# initialize controller values
868		if values is None:
869			values = MIDIControllers()
870
871		# chase controllers in track
872		for i in range(start_seq, min(end_seq, len(self))):
873			values.Event(self[i])
874
875		# return new values
876		return values
877
878	def SelectEvents (self, start, end):
879		midi_file_logger.debug('SelectEvents: %d to %d' % (start, end))
880		selected = EventList()
881		for event in self:
882			if event.ticks >= start:
883				if event.ticks >= end:
884					break
885				midi_file_logger.debug('SelectEvent: %s' % event.__str__())
886				selected.append(event)
887		return selected
888
889	def MergeEvents (self, events):
890		# copy events and sort them by ticks/sequence#
891		self.extend(events)
892		self.SortEvents()
893
894	def InsertEvents (self, events, seq):
895		self[seq:seq] = events
896		self.RenumberSeq()
897
898	def DeleteEvents (self, start_index, end_index, move_meta_events=None):
899		# default parameters
900		if start_index is None:
901			start_index = 0
902		if end_index is None:
903			end_index = len(self)
904
905		#print("\n")
906		#for evt in self[start_index:end_index]:
907		#	print("%d %s" % (evt.ticks, evt))
908
909		# delete events
910		delete_count = 0
911		move_count = 0
912		for event in self[start_index:end_index]:
913			#Bth; Added this so we always get clip end events; clips that ended on last measure wouldn't end on repeat
914			if (event.msg_type == CONTROL_CHANGE) and \
915			        (event.controller == JET_EVENT_TRIGGER_CLIP) and \
916			        ((event.value & 0x40) != 0x40):
917				pass
918			else:
919				if (move_meta_events is None) or (event.msg_type != META_EVENT):
920					self.remove(event)
921					delete_count += 1
922
923				# move meta-events
924				else:
925					event.ticks = move_meta_events
926					move_count += 1
927
928		midi_file_logger.debug('DeleteEvents: deleted %d events in range(%s:%s)' % (delete_count, start_index, end_index))
929		midi_file_logger.debug('DeleteEvents: moved %d events in range(%s:%s)' % (move_count, start_index, end_index))
930
931
932	def SeekEvent (self, pos):
933		for i in range(len(self)):
934			if self[i].ticks >= pos:
935				return i
936		return None
937
938	def RenumberSeq (self):
939		seq = 0
940		for event in self:
941			event.seq = seq
942			seq += 1
943
944	def SortEvents (self):
945		self.sort(self.EventSorter)
946		self.RenumberSeq()
947
948	@staticmethod
949	def EventSorter (x, y):
950		if x.ticks == y.ticks:
951			return cmp(x.seq, y.seq)
952		else:
953			return cmp(x.ticks, y.ticks)
954
955	def DumpEvents (self, output, timebase):
956		if output is not None:
957			for event in self:
958				output.write('%s\n' % event.TimeEventStr(timebase))
959		else:
960			for event in self:
961				midi_file_logger.debug(event.TimeEventStr(timebase))
962
963#---------------------------------------------------------------
964# MIDITrack
965#---------------------------------------------------------------
966class MIDITrack (object):
967	"""The MIDITrack class implements methods for reading, parsing,
968	modifying, and writing tracks in Standard MIDI Files (SMF).
969
970	"""
971	def __init__ (self):
972		self.length = 0
973		self.events = EventList()
974		self.end_of_track = None
975		self.channel = None
976		self.name = None
977
978	def ReadFromStream (self, stream, offset, file_size):
979		self.stream = stream
980		ticks = 0
981		seq = 0
982		running_status = None
983		tick_warning_level = stream.timebase.ppqn * LARGE_TICK_WARNING
984
985		# read the track header - verify it's an SMF track
986		stream.seek(offset)
987		bytes = stream.read(struct.calcsize(SMF_TRACK_HEADER_FMT))
988		riff_tag, track_len = struct.unpack(SMF_TRACK_HEADER_FMT, bytes)
989		midi_file_logger.debug('SMF track header\n  Tag:      %s\n  TrackLen: %d' % (riff_tag, track_len))
990		if (riff_tag != SMF_TRACK_RIFF_TAG):
991			raise MIDIFileException(stream, MSG_INVALID_TRACK_HEADER)
992		self.start = stream.tell()
993
994		# check for valid track length
995		if (self.start + track_len) > file_size:
996			stream.Warning('Ignoring illegal track length - %d exceeds length of file' % track_len)
997			track_len = None
998
999		# read the entire track
1000		note_on_list = []
1001		while 1:
1002
1003			# save current position
1004			pos = stream.tell()
1005
1006			# check for end of track
1007			if track_len is not None:
1008				if (pos - self.start) >= track_len:
1009					break
1010
1011			# are we past end of track?
1012			if self.end_of_track:
1013				stream.Warning('Ignoring data encountered beyond end-of-track meta-event')
1014				break;
1015
1016			# read delta timestamp
1017			delta = ReadVarLenQty(stream)
1018			if ticks > tick_warning_level:
1019				stream.Warning('Tick value is excessive - possibly corrupt data?')
1020			ticks += delta
1021
1022			# get the event type and process it
1023			msg_type = ReadByte(stream)
1024
1025			# if data byte, check for running status
1026			if msg_type & 0x80 == 0:
1027
1028				# use running status
1029				msg_type = running_status
1030
1031				# back up so event can process data
1032				stream.seek(-1,1)
1033
1034				# if no running status, we have a problem
1035				if not running_status:
1036					stream.Warning('Ignoring data byte received with no running status')
1037
1038			# create event type from stream
1039			event = MIDIEvent.ReadFromStream(stream, seq, ticks, msg_type)
1040
1041			if self.channel == None:
1042				try:
1043					self.channel = event.channel
1044				except AttributeError:
1045					pass
1046
1047			# track note-ons
1048			if event.msg_type == NOTE_ON:
1049
1050				"""
1051				Experimental code to clean up overlapping notes
1052				Clean up now occurs during write process
1053
1054				for note_on in note_on_list:
1055					if (event.channel == note_on.channel) and (event.note == note_on.note):
1056						stream.Warning('Duplicate note-on\'s encountered without intervening note-off')
1057						stream.Warning('  [%s]: %s' % (stream.timebase.ConvertTicksToStr(event.ticks), event.__str__()))
1058						note_on.note_length = event.ticks - note_on.ticks - 1
1059						if note_on.note_length <= 0:
1060							stream.Warning('Eliminating duplicate note-on')
1061							event.ticks = note_on.ticks
1062							self.events.remove(note_on)
1063				"""
1064
1065				note_on_list.append(event)
1066
1067			# process note-offs
1068			if event.msg_type == NOTE_OFF:
1069				for note_on in note_on_list[:]:
1070					if (event.channel == note_on.channel) and (event.note == note_on.note):
1071						note_on.note_length = event.ticks - note_on.ticks
1072						note_on.note_off_velocity = event.velocity
1073						note_on_list.remove(note_on)
1074						break
1075				#else:
1076				#	stream.Warning('Note-off encountered without corresponding note-on')
1077				#	stream.Warning('  [%s]: %s' % (stream.timebase.ConvertTicksToStr(event.ticks), event.__str__()))
1078
1079			# check for end of track
1080			elif event.msg_type == META_EVENT and event.meta_type == META_EVENT_END_OF_TRACK:
1081				self.end_of_track = event.ticks
1082
1083			# BTH; get track name
1084			elif event.msg_type == META_EVENT and event.meta_type == META_EVENT_SEQUENCE_TRACK_NAME:
1085				self.name = array.array('B', event.msg).tostring()
1086
1087			# append event to event list
1088			else:
1089				self.events.append(event)
1090				seq += 1
1091
1092			# save position for port-mortem
1093			stream.last_good_event = pos
1094
1095			# update running statusc_str(
1096			if msg_type < 0xf0:
1097				running_status = msg_type
1098			elif (msg_type < 0xf8) or (msg_type == 0xff):
1099				running_status = None
1100
1101		# check for stuck notes
1102		#if len(note_on_list):
1103		#	stream.Warning('Note-ons encountered without corresponding note-offs')
1104
1105		# check for missing end-of-track meta-event
1106		if self.end_of_track is None:
1107			self.last_tick = self.events[-1].ticks
1108			stream.Warning('End of track encountered with no end-of-track meta-event')
1109
1110		# if track length was bad, correct it
1111		if track_len is None:
1112			track_len = stream.tell() - offset - 8
1113
1114		return track_len
1115
1116	def Write (self, stream, filters=None):
1117		# save current file position so we can write header
1118		header_loc = stream.tell()
1119		stream.seek(header_loc + struct.calcsize(SMF_TRACK_HEADER_FMT))
1120
1121		# save a copy of the event list so we can restore it
1122		save_events = copy.copy(self.events)
1123
1124		# create note-off events
1125		index = 0
1126		while 1:
1127			if index >= len(self.events):
1128				break
1129
1130			# if note-on event, create a note-off event
1131			event = self.events[index]
1132			index += 1
1133			if event.msg_type == NOTE_ON:
1134				note_off = NoteOffEvent(event.ticks + event.note_length, index, event.channel, event.note, event.note_off_velocity)
1135
1136				# insert note-off in list
1137				for i in range(index, len(self.events)):
1138					if self.events[i].ticks >= note_off.ticks:
1139						self.events.insert(i, note_off)
1140						break
1141				else:
1142					self.events.append(note_off)
1143
1144		# renumber list
1145		self.events.RenumberSeq()
1146
1147		# write the events
1148		self.running_status = None
1149		self.ticks = 0
1150		for event in self.events:
1151
1152			# write event
1153			event.WriteToStream(stream, self, filters)
1154
1155		# restore original list (without note-off events)
1156		self.events = save_events
1157
1158		# write the end-of-track meta-event
1159		MetaEvent(self.end_of_track, 0, META_EVENT_END_OF_TRACK,[]).WriteToStream(stream, self, None)
1160
1161		# write track header
1162		end_of_track = stream.tell()
1163		track_len = end_of_track - header_loc - struct.calcsize(SMF_TRACK_HEADER_FMT)
1164		stream.seek(header_loc)
1165		bytes = struct.pack(SMF_TRACK_HEADER_FMT, SMF_TRACK_RIFF_TAG, track_len)
1166		stream.write(bytes)
1167		stream.seek(end_of_track)
1168
1169	def Trim (self, start, end, slide=True, chase_controllers=True, delete_meta_events=False, quantize=0):
1170		controllers = None
1171
1172		if quantize:
1173			# quantize events just before start
1174			for event in self.events.SelectEvents(start - quantize, start):
1175				midi_file_logger.debug('Trim: Moving event %s to %d' % (event.__str__(), start))
1176				event.ticks = start
1177
1178			# quantize events just before end
1179			for event in self.events.SelectEvents(end - quantize, end):
1180				midi_file_logger.debug('Trim: Moving event %s to %d' % (event.__str__(), end))
1181				event.ticks = end
1182
1183		# trim start
1184		if start:
1185
1186			# find first event inside trim
1187			start_event = self.events.SeekEvent(start)
1188			if start_event is not None:
1189
1190				# chase controllers to cut point
1191				if chase_controllers:
1192					controllers = self.events.ChaseControllers(self.events[start_event].seq)
1193					controller_events = controllers.GenerateEventList(0)
1194					midi_file_logger.debug('Trim: insert new controller events at %d:' % start)
1195					controller_events.DumpEvents(None, self.stream.timebase)
1196					self.events.InsertEvents(controller_events, start_event)
1197
1198				# delete events
1199				midi_file_logger.debug('Trim: deleting events up to event %d' % start_event)
1200				if delete_meta_events:
1201					self.events.DeleteEvents(None, start_event, None)
1202				else:
1203					self.events.DeleteEvents(None, start_event, start)
1204
1205			# delete everything except metadata
1206			else:
1207				self.events.DeleteEvents(None, None, start)
1208
1209		# trim end
1210		end_event = self.events.SeekEvent(end)
1211		if end_event is not None:
1212			midi_file_logger.debug('Trim: trimming section starting at event %d' % end_event)
1213			self.events.DeleteEvents(end_event, None)
1214
1215		# trim any notes that extend past the end
1216		for event in self.events:
1217			if event.msg_type == NOTE_ON:
1218				if (event.ticks + event.note_length) > end:
1219					midi_file_logger.debug('Trim: trimming note that extends past end %s' % event.TimeEventStr(self.stream.timebase))
1220					event.note_length = end - event.ticks
1221					if event.note_length <= 0:
1222						raise 'Error in note length - note should have been deleted'
1223
1224		midi_file_logger.debug('Trim: initial end-of-track: %d' % self.end_of_track)
1225		self.end_of_track = min(self.end_of_track, end)
1226
1227		# slide events to start of track to fill hole
1228		if slide and start:
1229			midi_file_logger.debug('Trim: sliding events: %d' % start)
1230			for event in self.events:
1231				if event.ticks > start:
1232					event.ticks -= start
1233				else:
1234					event.ticks = 0
1235			self.end_of_track = max(0, self.end_of_track - start)
1236		midi_file_logger.debug('Trim: new end-of-track: %d' % self.end_of_track)
1237
1238		self.events.RenumberSeq()
1239		self.events.FixNoteLengths()
1240
1241	def DumpEvents (self, output):
1242		self.events.DumpEvents(output, self.stream.timebase)
1243		if output is not None:
1244			output.write('[%s]: end-of-track\n' % self.stream.timebase.ConvertTicksToStr(self.end_of_track))
1245		else:
1246			midi_file_logger.debug('[%s]: end-of-track' % self.stream.timebase.ConvertTicksToStr(self.end_of_track))
1247
1248
1249#---------------------------------------------------------------
1250# MIDIFile
1251#---------------------------------------------------------------
1252class MIDIFile (file):
1253	"""The MIDIFile class implements methods for reading, parsing,
1254	modifying, and writing Standard MIDI Files (SMF).
1255
1256	"""
1257	def __init__ (self, name, mode):
1258		file.__init__(self, name, mode)
1259		self.timebase = TimeBase()
1260
1261	def ReadFromStream (self, start_offset=0, file_size=None):
1262		"""Parse the MIDI file creating a list of properties, tracks,
1263		and events based on the contents of the file.
1264
1265		"""
1266
1267		# determine file size - without using os.stat
1268		if file_size == None:
1269			self.start_offset = start_offset
1270			self.seek(0,2)
1271			file_size = self.tell() - self.start_offset
1272			self.seek(start_offset,0)
1273		else:
1274			file_size = file_size
1275
1276		# for error recovery
1277		self.last_good_event = None
1278		self.error_loc = None
1279
1280		# read the file header - verify it's an SMF file
1281		bytes = self.read(struct.calcsize(SMF_HEADER_FMT))
1282		riff_tag, self.hdr_len, self.format, self.num_tracks, self.timebase.ppqn = struct.unpack(SMF_HEADER_FMT, bytes)
1283		midi_file_logger.debug('SMF header\n  Tag:       %s\n  HeaderLen: %d\n  Format:    %d\n  NumTracks: %d\n  PPQN:      %d\n' % \
1284			(riff_tag, self.hdr_len, self.format, self.num_tracks, self.timebase.ppqn))
1285
1286		# sanity check on header
1287		if (riff_tag != SMF_RIFF_TAG) or (self.format not in range(2)):
1288			raise MIDIFileException(self, MSG_NOT_SMF_FILE)
1289
1290		# check for odd header size
1291		if self.hdr_len + 8 != struct.calcsize(SMF_HEADER_FMT):
1292			self.Warning('SMF file has unusual header size: %d bytes' % self.hdr_len)
1293
1294		# read each of the tracks
1295		offset = start_offset + self.hdr_len + 8
1296		self.tracks = []
1297		self.end_of_file = 0
1298		for i in range(self.num_tracks):
1299			#print("Track: %d" % i)
1300
1301			# parse the track
1302			track = MIDITrack()
1303			length = track.ReadFromStream(self, offset, file_size)
1304			track.trackNum = i
1305
1306			self.tracks.append(track)
1307
1308			# calculate offset to next track
1309			offset += length + 8
1310
1311			# determine time of last event
1312			self.end_of_file = max(self.end_of_file, track.end_of_track)
1313
1314		# if start_offset is zero, the final offset should match the file length
1315		if (offset - start_offset) != file_size:
1316			self.Warning('SMF file size is incorrect - should be %d, was %d' % (file_size, offset))
1317
1318	def Save (self, offset=0, filters=None):
1319		"""Save this file back to disk with modifications."""
1320		if (not 'w' in self.mode) and (not '+' in self.mode):
1321			raise MIDIFileException(self, 'Cannot write to file in read-only mode')
1322		self.Write(self, offset, filters)
1323
1324	def SaveAs (self, filename, offset=0, filters=None):
1325		"""Save MIDI data to new file."""
1326		output_file = MIDIFile(filename, 'wb')
1327		self.Write(output_file, offset, filters)
1328		output_file.close()
1329
1330	def Write (self, output_file, offset=0, filters=None):
1331		"""This function does the actual work of writing the file."""
1332		# write the file header
1333		output_file.seek(offset)
1334		bytes = struct.pack(SMF_HEADER_FMT, SMF_RIFF_TAG, struct.calcsize(SMF_HEADER_FMT) - 8, self.format, self.num_tracks, self.timebase.ppqn)
1335		output_file.write(bytes)
1336
1337		# write out the tracks
1338		for track in self.tracks:
1339			track.Write(output_file, filters)
1340
1341		# flush the data to disk
1342		output_file.flush()
1343
1344	def ConvertToType0 (self):
1345		"""Convert a file to type 0."""
1346		if self.format == 0:
1347			midi_file_logger.warning('File is already type 0 - ignoring request to convert')
1348			return
1349
1350		# convert to type 0
1351		for track in self.tracks[1:]:
1352			self.tracks[0].MergeEvents(track.events)
1353		self.tracks = self.tracks[:1]
1354		self.num_tracks = 1
1355		self.format = 0
1356
1357	def DeleteEmptyTracks (self):
1358		"""Delete any tracks that do not contain MIDI messages"""
1359		track_num = 0
1360		for track in self.tracks[:]:
1361			for event in self.tracks.events:
1362				if event.msg_type in MIDI_MESSAGES:
1363					break;
1364				else:
1365					midi_file_logger.debug('Deleting track %d' % track_num)
1366					self.tracks.remove(track)
1367			track_num += 1
1368
1369	def ConvertToTicks (self, measures, beats, ticks):
1370		return self.timebase.ConvertToTicks(measures, beats, ticks)
1371
1372	def Trim (self, start, end, quantize=0, chase_controllers=True):
1373		track_num = 0
1374		for track in self.tracks:
1375			midi_file_logger.debug('Trimming track %d' % track_num)
1376			track.Trim(start, end, quantize=quantize, chase_controllers=chase_controllers)
1377			track_num += 1
1378
1379	def DumpTracks (self, output=None):
1380		track_num = 0
1381		for track in self.tracks:
1382			if output is None:
1383				midi_file_logger.debug('*** Track %d ***' % track_num)
1384			else:
1385				output.write('*** Track %d ***' % track_num)
1386			track.DumpEvents(output)
1387			track_num += 1
1388
1389	def Warning (self, msg):
1390		midi_file_logger.warning('[%d]: %s' % (self.tell(), msg))
1391
1392	def Error (self, msg):
1393		midi_file_logger.error('[%d]: %s' % (self.tell(), msg))
1394
1395	def DumpError (self):
1396		if self.last_good_event:
1397			midi_file_logger.error('Dumping from last good event:')
1398			pos = self.last_good_event - 16
1399			length = self.error_loc - pos + 16
1400		elif self.error_loc:
1401			midi_file_logger.error('Dumping from 16 bytes prior to error:')
1402			pos = self.error_loc
1403			length = 32
1404		else:
1405			midi_file_logger.error('No dump information available')
1406			return
1407
1408		self.seek(pos, 0)
1409		for i in range(length):
1410			if i % 16 == 0:
1411				if i:
1412					midi_file_logger.error(' '.join(debug_out))
1413				debug_out = ['%08x:' % (pos + i)]
1414			byte = self.read(1)
1415			if len(byte) == 0:
1416				break;
1417			debug_out.append('%02x' % ord(byte))
1418		if i % 16 > 0:
1419			midi_file_logger.error(' '.join(debug_out))
1420
1421def GetMidiInfo(midiFile):
1422	"""Bth; Get MIDI info"""
1423
1424	class midiData(object):
1425		def __init__ (self):
1426			self.err = 1
1427			self.endMbt = "0:0:0"
1428			self.totalTicks = 0
1429			self.maxTracks = 0
1430			self.maxMeasures = 0
1431			self.maxBeats = 0
1432			self.maxTicks = 0
1433			self.totalTicks = 0
1434			self.timebase = None
1435			self.ppqn = 0
1436			self.beats_per_measure = 0
1437			self.trackList = []
1438
1439	md = midiData()
1440
1441	try:
1442		m = MIDIFile(midiFile, 'rb')
1443		m.ReadFromStream()
1444
1445		for track in m.tracks:
1446			if track.channel is not None:
1447				empty = False
1448				trk = track.channel + 1
1449			else:
1450				empty = True
1451				trk = ''
1452			md.trackList.append(trackGrid(track.trackNum, trk, track.name, empty))
1453
1454		md.endMbt = m.timebase.ConvertTicksToMBT(m.end_of_file)
1455		md.endMbtStr = "%d:%d:%d" % (md.endMbt[0], md.endMbt[1], md.endMbt[2])
1456		md.maxMeasures = md.endMbt[0]
1457		md.maxBeats = 4
1458		md.maxTicks = m.timebase.ppqn
1459		md.maxTracks = m.num_tracks
1460		md.totalTicks = m.end_of_file
1461		md.timebase = m.timebase
1462		md.ppqn = m.timebase.ppqn
1463		md.beats_per_measure = m.timebase.beats_per_measure
1464
1465		#add above if more added
1466		md.err = 0
1467
1468		m.close()
1469	except:
1470		raise
1471		pass
1472
1473	return md
1474
1475
1476
1477
1478#---------------------------------------------------------------
1479# main
1480#---------------------------------------------------------------
1481if __name__ == '__main__':
1482	sys = __import__('sys')
1483	os = __import__('os')
1484
1485	# initialize root logger
1486	root_logger = logging.getLogger('')
1487	root_logger.setLevel(logging.NOTSET)
1488
1489	# initialize console handler
1490	console_handler = logging.StreamHandler()
1491	console_handler.setFormatter(logging.Formatter('%(message)s'))
1492	console_handler.setLevel(logging.DEBUG)
1493	root_logger.addHandler(console_handler)
1494
1495	files = []
1496	dirs = []
1497	last_arg = None
1498	sysex_filter = False
1499	drum_filter = False
1500	convert = False
1501
1502	# process args
1503	for arg in sys.argv[1:]:
1504
1505		# previous argument implies this argument
1506		if last_arg is not None:
1507			if last_arg == '-DIR':
1508				dirs.append(arg)
1509				last_arg = None
1510
1511		# check for switch
1512		elif arg[0] == '-':
1513			if arg == '-DIR':
1514				last_arg = arg
1515			elif arg == '-SYSEX':
1516				sysex_filter = True
1517			elif arg == '-DRUMS':
1518				drum_filter = True
1519			elif arg == '-CONVERT':
1520				convert = True
1521			else:
1522				midi_file_logger.error('Bad option %s' % arg)
1523
1524		# must be a filename
1525		else:
1526			files.append(arg)
1527
1528	# setup filters
1529	filters = []
1530	if sysex_filter:
1531		filters.append(EventTypeFilter((SYSEX,)))
1532	if drum_filter:
1533		filters.append(ChannelFilter((9,),False))
1534
1535
1536	# process dirs
1537	for d in dirs:
1538		for root, dir_list, file_list in os.walk(d):
1539			for f in file_list:
1540				if f.endswith('.mid'):
1541					files.append(os.path.join(root, f))
1542
1543	# process files
1544	bad_files = []
1545	for f in files:
1546		midi_file_logger.info('Processing file %s' % f)
1547		midiFile = MIDIFile(f, 'rb')
1548		try:
1549			midiFile.ReadFromStream()
1550
1551			#midiFile.DumpTracks()
1552			#print('[%s]: end-of-track\n' % midiFile.timebase.ConvertTicksToStr(midiFile.end_of_file))
1553
1554			# convert to type 0
1555			if convert and (midiFile.format == 1):
1556				midiFile.Convert(0)
1557				converted = True
1558			else:
1559				converted = False
1560
1561			# write processed file
1562			if converted or len(filters):
1563				midiFile.SaveAs(f[:-4] + '-mod.mid', filters)
1564
1565		except MIDIFileException, X:
1566			bad_files.append(f)
1567			midi_file_logger.error('Error in file %s' % f)
1568			midi_file_logger.error(X)
1569			midiFile.DumpError()
1570		midiFile.close()
1571
1572	# dump problem files
1573	if len(bad_files):
1574		midi_file_logger.info('The following file(s) had errors:')
1575		for f in bad_files:
1576			midi_file_logger.info(f)
1577	else:
1578		midi_file_logger.info('All files read successfully')
1579
1580