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