1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: midi.translate.py 4# Purpose: Translate MIDI and music21 objects 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2010-2015, 2019 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12''' 13Module to translate MIDI data to music21 Streams and vice versa. Note that quantization of 14notes takes place in the :meth:`~music21.stream.Stream.quantize` method not here. 15''' 16import unittest 17import math 18import copy 19from typing import Optional, List, Tuple, Dict, Union, Any 20import warnings 21 22from music21 import chord 23from music21 import common 24from music21 import defaults 25from music21 import duration 26from music21 import note 27from music21 import exceptions21 28from music21 import environment 29from music21 import stream 30 31from music21.instrument import Conductor, deduplicate 32from music21.midi import percussion 33 34_MOD = 'midi.translate' 35environLocal = environment.Environment(_MOD) 36 37 38# ------------------------------------------------------------------------------ 39class TranslateException(exceptions21.Music21Exception): 40 pass 41 42 43class TranslateWarning(UserWarning): 44 pass 45 46# ------------------------------------------------------------------------------ 47# Durations 48 49 50def offsetToMidiTicks(o, addStartDelay=False): 51 ''' 52 Helper function to convert a music21 offset value to MIDI ticks, 53 depends on *defaults.ticksPerQuarter* and *defaults.ticksAtStart*. 54 55 Returns an int. 56 57 >>> defaults.ticksPerQuarter 58 1024 59 >>> defaults.ticksAtStart 60 1024 61 62 63 >>> midi.translate.offsetToMidiTicks(0) 64 0 65 >>> midi.translate.offsetToMidiTicks(0, addStartDelay=True) 66 1024 67 68 >>> midi.translate.offsetToMidiTicks(1) 69 1024 70 71 >>> midi.translate.offsetToMidiTicks(20.5) 72 20992 73 ''' 74 ticks = int(round(o * defaults.ticksPerQuarter)) 75 if addStartDelay: 76 ticks += defaults.ticksAtStart 77 return ticks 78 79 80def durationToMidiTicks(d): 81 # noinspection PyShadowingNames 82 ''' 83 Converts a :class:`~music21.duration.Duration` object to midi ticks. 84 85 Depends on *defaults.ticksPerQuarter*, Returns an int. 86 Does not use defaults.ticksAtStart 87 88 89 >>> n = note.Note() 90 >>> n.duration.type = 'half' 91 >>> midi.translate.durationToMidiTicks(n.duration) 92 2048 93 94 >>> d = duration.Duration('quarter') 95 >>> dReference = midi.translate.ticksToDuration(1024, inputM21DurationObject=d) 96 >>> dReference is d 97 True 98 >>> d.type 99 'quarter' 100 >>> d.type = '16th' 101 >>> d.quarterLength 102 0.25 103 >>> midi.translate.durationToMidiTicks(d) 104 256 105 ''' 106 return int(round(d.quarterLength * defaults.ticksPerQuarter)) 107 108 109def ticksToDuration(ticks, ticksPerQuarter=None, inputM21DurationObject=None): 110 # noinspection PyShadowingNames 111 ''' 112 Converts a number of MIDI Ticks to a music21 duration.Duration() object. 113 114 Optional parameters include ticksPerQuarter -- in case something other 115 than the default.ticksPerQuarter (1024) is used in this file. And 116 it can take a :class:`~music21.duration.Duration` object to modify, specified 117 as *inputM21DurationObject* 118 119 >>> d = midi.translate.ticksToDuration(1024) 120 >>> d 121 <music21.duration.Duration 1.0> 122 >>> d.type 123 'quarter' 124 125 >>> n = note.Note() 126 >>> midi.translate.ticksToDuration(3072, inputM21DurationObject=n.duration) 127 <music21.duration.Duration 3.0> 128 >>> n.duration.type 129 'half' 130 >>> n.duration.dots 131 1 132 133 More complex rhythms can also be set automatically: 134 135 >>> d2 = duration.Duration() 136 >>> d2reference = midi.translate.ticksToDuration(1200, inputM21DurationObject=d2) 137 >>> d2 is d2reference 138 True 139 >>> d2.quarterLength 140 1.171875 141 >>> d2.type 142 'complex' 143 >>> d2.components 144 (DurationTuple(type='quarter', dots=0, quarterLength=1.0), 145 DurationTuple(type='32nd', dots=0, quarterLength=0.125), 146 DurationTuple(type='128th', dots=1, quarterLength=0.046875)) 147 >>> d2.components[2].type 148 '128th' 149 >>> d2.components[2].dots 150 1 151 152 ''' 153 if inputM21DurationObject is None: 154 d = duration.Duration() 155 else: 156 d = inputM21DurationObject 157 158 if ticksPerQuarter is None: 159 ticksPerQuarter = defaults.ticksPerQuarter 160 161 # given a value in ticks 162 d.quarterLength = float(ticks) / ticksPerQuarter 163 return d 164 165 166# ------------------------------------------------------------------------------ 167# utility functions for getting commonly used event 168 169 170def getStartEvents(mt=None, channel=1, instrumentObj=None): 171 ''' 172 Returns a list of midi.MidiEvent objects found at the beginning of a track. 173 174 A MidiTrack reference can be provided via the `mt` parameter. 175 176 >>> midi.translate.getStartEvents() 177 [<music21.midi.DeltaTime (empty) track=None, channel=1>, 178 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=None, channel=1, data=b''>] 179 180 >>> midi.translate.getStartEvents(channel=2, instrumentObj=instrument.Harpsichord()) 181 [<music21.midi.DeltaTime (empty) track=None, channel=2>, 182 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=None, channel=2, data=b'Harpsichord'>, 183 <music21.midi.DeltaTime (empty) track=None, channel=2>, 184 <music21.midi.MidiEvent PROGRAM_CHANGE, track=None, channel=2, data=6>] 185 186 ''' 187 from music21 import midi as midiModule 188 events = [] 189 if isinstance(instrumentObj, Conductor): 190 return events 191 elif instrumentObj is None or instrumentObj.bestName() is None: 192 partName = '' 193 else: 194 partName = instrumentObj.bestName() 195 196 dt = midiModule.DeltaTime(mt, channel=channel) 197 events.append(dt) 198 199 me = midiModule.MidiEvent(mt, channel=channel) 200 me.type = midiModule.MetaEvents.SEQUENCE_TRACK_NAME 201 me.data = partName 202 events.append(me) 203 204 # additional allocation of instruments may happen elsewhere 205 # this may lead to two program changes happening at time zero 206 # however, this assures that the program change happens before the 207 # the clearing of the pitch bend data 208 if instrumentObj is not None and instrumentObj.midiProgram is not None: 209 sub = instrumentToMidiEvents(instrumentObj, includeDeltaTime=True, 210 channel=channel) 211 events += sub 212 213 return events 214 215 216def getEndEvents(mt=None, channel=1): 217 ''' 218 Returns a list of midi.MidiEvent objects found at the end of a track. 219 220 >>> midi.translate.getEndEvents(channel=2) 221 [<music21.midi.DeltaTime t=1024, track=None, channel=2>, 222 <music21.midi.MidiEvent END_OF_TRACK, track=None, channel=2, data=b''>] 223 ''' 224 from music21 import midi as midiModule 225 226 events = [] 227 228 dt = midiModule.DeltaTime(track=mt, channel=channel) 229 dt.time = defaults.ticksAtStart 230 events.append(dt) 231 232 me = midiModule.MidiEvent(track=mt) 233 me.type = midiModule.MetaEvents.END_OF_TRACK 234 me.channel = channel 235 me.data = '' # must set data to empty string 236 events.append(me) 237 238 return events 239 240# ------------------------------------------------------------------------------ 241# Multi-object conversion 242 243 244def music21ObjectToMidiFile( 245 music21Object, 246 *, 247 addStartDelay=False, 248) -> 'music21.midi.MidiFile': 249 ''' 250 Either calls streamToMidiFile on the music21Object or 251 puts a copy of that object into a Stream (so as 252 not to change activeSites, etc.) and calls streamToMidiFile on 253 that object. 254 ''' 255 classes = music21Object.classes 256 if 'Stream' in classes: 257 if music21Object.atSoundingPitch is False: 258 music21Object = music21Object.toSoundingPitch() 259 260 return streamToMidiFile(music21Object, addStartDelay=addStartDelay) 261 else: 262 m21ObjectCopy = copy.deepcopy(music21Object) 263 s = stream.Stream() 264 s.insert(0, m21ObjectCopy) 265 return streamToMidiFile(s, addStartDelay=addStartDelay) 266 267 268# ------------------------------------------------------------------------------ 269# Notes 270 271def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None) -> note.Note: 272 # noinspection PyShadowingNames 273 ''' 274 Convert from a list of midi.DeltaTime and midi.MidiEvent objects to a music21 Note. 275 276 The list can be presented in one of two forms: 277 278 [deltaTime1, midiEvent1, deltaTime2, midiEvent2] 279 280 or 281 282 [(deltaTime1, midiEvent1), (deltaTime2, midiEvent2)] 283 284 It is assumed, but not checked, that midiEvent2 is an appropriate Note_Off command. Thus, only 285 three elements are really needed. 286 287 The `inputM21` parameter can be a Note or None; in the case of None, a Note object is created. 288 In either case it returns a Note (N.B.: this will change soon so that None will be returned 289 if `inputM21` is given. This will match the behavior of other translate objects). 290 291 N.B. this takes in a list of music21 MidiEvent objects so see [...] on how to 292 convert raw MIDI data to MidiEvent objects 293 294 In this example, we start a NOTE_ON event at offset 1.0 that lasts 295 for 2.0 quarter notes until we 296 send a zero-velocity NOTE_ON (=NOTE_OFF) event for the same pitch. 297 298 >>> mt = midi.MidiTrack(1) 299 >>> dt1 = midi.DeltaTime(mt) 300 >>> dt1.time = 1024 301 302 >>> me1 = midi.MidiEvent(mt) 303 >>> me1.type = midi.ChannelVoiceMessages.NOTE_ON 304 >>> me1.pitch = 45 305 >>> me1.velocity = 94 306 307 >>> dt2 = midi.DeltaTime(mt) 308 >>> dt2.time = 2048 309 310 >>> me2 = midi.MidiEvent(mt) 311 >>> me2.type = midi.ChannelVoiceMessages.NOTE_ON 312 >>> me2.pitch = 45 313 >>> me2.velocity = 0 314 315 >>> n = midi.translate.midiEventsToNote([dt1, me1, dt2, me2]) 316 >>> n.pitch 317 <music21.pitch.Pitch A2> 318 >>> n.duration.quarterLength 319 1.0 320 >>> n.volume.velocity 321 94 322 323 An `inputM21` object can be given in which case it's set. 324 325 >>> m = note.Note() 326 >>> dummy = midi.translate.midiEventsToNote([dt1, me1, dt2, me2], inputM21=m) 327 >>> m.pitch 328 <music21.pitch.Pitch A2> 329 >>> m.duration.quarterLength 330 1.0 331 >>> m.volume.velocity 332 94 333 334 ''' 335 if ticksPerQuarter is None: 336 ticksPerQuarter = defaults.ticksPerQuarter 337 338 # pre sorted from a stream 339 if len(eventList) == 2: 340 tOn, eOn = eventList[0] 341 tOff, unused_eOff = eventList[1] 342 343 # a representation closer to stream 344 elif len(eventList) == 4: 345 # delta times are first and third 346 dur = eventList[2].time - eventList[0].time 347 # shift to start at zero; only care about duration here 348 tOn, eOn = 0, eventList[1] 349 tOff, unused_eOff = dur, eventList[3] 350 else: 351 raise TranslateException(f'cannot handle MIDI event list in the form: {eventList!r}') 352 353 # here we are handling an issue that might arise with double-stemmed notes 354 if (tOff - tOn) != 0: 355 if inputM21 is None: 356 n = note.Note(duration=ticksToDuration(tOff - tOn, ticksPerQuarter)) 357 else: 358 n = inputM21 359 n.duration = ticksToDuration(tOff - tOn, ticksPerQuarter, n.duration) 360 else: 361 # environLocal.printDebug(['cannot translate found midi event with zero duration:', eOn, n]) 362 # for now, substitute grace note 363 if inputM21 is None: 364 n = note.Note() 365 else: 366 n = inputM21 367 n.getGrace(inPlace=True) 368 369 n.pitch.midi = eOn.pitch 370 n.volume.velocity = eOn.velocity 371 n.volume.velocityIsRelative = False # not relative coming from MIDI 372 # n._midiVelocity = eOn.velocity 373 374 return n 375 376 377def noteToMidiEvents(inputM21, *, includeDeltaTime=True, channel=1): 378 # noinspection PyShadowingNames 379 ''' 380 Translate a music21 Note to a list of four MIDI events -- 381 the DeltaTime for the start of the note (0), the NOTE_ON event, the 382 DeltaTime to the end of the note, and the NOTE_OFF event. 383 384 If `includeDeltaTime` is not True then the DeltaTime events 385 aren't returned, thus only two events are returned. 386 387 The initial deltaTime object is always 0. It will be changed when 388 processing Notes from a Stream. 389 390 The `channel` can be specified, otherwise channel 1 is assumed. 391 392 >>> n1 = note.Note('C#4') 393 >>> eventList = midi.translate.noteToMidiEvents(n1) 394 >>> eventList 395 [<music21.midi.DeltaTime (empty) track=None, channel=1>, 396 <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=61, velocity=90>, 397 <music21.midi.DeltaTime t=1024, track=None, channel=1>, 398 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=61, velocity=0>] 399 400 >>> n1.duration.quarterLength = 2.5 401 >>> eventList = midi.translate.noteToMidiEvents(n1) 402 >>> eventList 403 [<music21.midi.DeltaTime (empty) track=None, channel=1>, 404 <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=61, velocity=90>, 405 <music21.midi.DeltaTime t=2560, track=None, channel=1>, 406 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=61, velocity=0>] 407 408 Omitting DeltaTimes: 409 410 >>> eventList2 = midi.translate.noteToMidiEvents(n1, includeDeltaTime=False, channel=9) 411 >>> eventList2 412 [<music21.midi.MidiEvent NOTE_ON, track=None, channel=9, pitch=61, velocity=90>, 413 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=9, pitch=61, velocity=0>] 414 415 Changed in v7 -- made keyword-only. 416 ''' 417 from music21 import midi as midiModule 418 419 n = inputM21 420 421 mt = None # use a midi track set to None 422 eventList = [] 423 424 if includeDeltaTime: 425 dt = midiModule.DeltaTime(mt, channel=channel) 426 # add to track events 427 eventList.append(dt) 428 429 me1 = midiModule.MidiEvent(track=mt) 430 me1.type = midiModule.ChannelVoiceMessages.NOTE_ON 431 me1.channel = channel 432 me1.pitch = n.pitch.midi 433 if not n.pitch.isTwelveTone(): 434 me1.centShift = n.pitch.getCentShiftFromMidi() 435 436 # TODO: not yet using dynamics or velocity 437 # volScalar = n.volume.getRealized(useDynamicContext=False, 438 # useVelocity=True, useArticulations=False) 439 440 # use cached realized, as realized values should have already been set 441 me1.velocity = int(round(n.volume.cachedRealized * 127)) 442 443 eventList.append(me1) 444 445 if includeDeltaTime: 446 # add note off / velocity zero message 447 dt = midiModule.DeltaTime(mt, channel=channel) 448 dt.time = durationToMidiTicks(n.duration) 449 # add to track events 450 eventList.append(dt) 451 452 me2 = midiModule.MidiEvent(track=mt) 453 me2.type = midiModule.ChannelVoiceMessages.NOTE_OFF 454 me2.channel = channel 455 me2.pitch = n.pitch.midi 456 if not n.pitch.isTwelveTone(): 457 me2.centShift = n.pitch.getCentShiftFromMidi() 458 459 me2.velocity = 0 # must be zero 460 eventList.append(me2) 461 462 # set correspondence 463 me1.correspondingEvent = me2 464 me2.correspondingEvent = me1 465 466 return eventList 467 468 469# ------------------------------------------------------------------------------ 470# Chords 471 472def midiEventsToChord(eventList, ticksPerQuarter=None, inputM21=None): 473 # noinspection PyShadowingNames 474 ''' 475 Creates a Chord from a list of :class:`~music21.midi.DeltaTime` 476 and :class:`~music21.midi.MidiEvent` objects. See midiEventsToNote 477 for details. 478 479 All DeltaTime objects except the first (for the first note on) 480 and last (for the last note off) are ignored. 481 482 >>> mt = midi.MidiTrack(1) 483 484 >>> dt1 = midi.DeltaTime(mt) 485 >>> me1 = midi.MidiEvent(mt) 486 >>> me1.type = midi.ChannelVoiceMessages.NOTE_ON 487 >>> me1.pitch = 45 488 >>> me1.velocity = 94 489 490 >>> dt2 = midi.DeltaTime(mt) 491 >>> me2 = midi.MidiEvent(mt) 492 >>> me2.type = midi.ChannelVoiceMessages.NOTE_ON 493 >>> me2.pitch = 46 494 >>> me2.velocity = 94 495 496 >>> dt3 = midi.DeltaTime(mt) 497 >>> me3 = midi.MidiEvent(mt) 498 >>> me3.type = midi.ChannelVoiceMessages.NOTE_OFF 499 >>> me3.pitch = 45 500 >>> me3.velocity = 0 501 502 >>> dt4 = midi.DeltaTime(mt) 503 >>> dt4.time = 2048 504 505 >>> me4 = midi.MidiEvent(mt) 506 >>> me4.type = midi.ChannelVoiceMessages.NOTE_OFF 507 >>> me4.pitch = 46 508 >>> me4.velocity = 0 509 510 >>> c = midi.translate.midiEventsToChord([dt1, me1, dt2, me2, dt3, me3, dt4, me4]) 511 >>> c 512 <music21.chord.Chord A2 B-2> 513 >>> c.duration.quarterLength 514 2.0 515 516 Providing fewer than four events won't work. 517 518 >>> c = midi.translate.midiEventsToChord([dt1, me1, me2]) 519 Traceback (most recent call last): 520 music21.midi.translate.TranslateException: fewer than 4 events provided to midiEventsToChord: 521 [<music21.midi.DeltaTime (empty) track=1, channel=None>, 522 <music21.midi.MidiEvent NOTE_ON, track=1, channel=None, pitch=45, velocity=94>, 523 <music21.midi.MidiEvent NOTE_ON, track=1, channel=None, pitch=46, velocity=94>] 524 525 Changed in v.7 -- Uses the last DeltaTime in the list to get the end time. 526 ''' 527 tOn: int = 0 # ticks 528 tOff: int = 0 # ticks 529 530 if ticksPerQuarter is None: 531 ticksPerQuarter = defaults.ticksPerQuarter 532 533 from music21 import pitch 534 from music21 import volume 535 pitches = [] 536 volumes = [] 537 538 # this is a format provided by the Stream conversion of 539 # midi events; it pre groups events for a chord together in nested pairs 540 # of abs start time and the event object 541 if isinstance(eventList, list) and eventList and isinstance(eventList[0], tuple): 542 # pairs of pairs 543 for onPair, offPair in eventList: 544 tOn, eOn = onPair 545 tOff, unused_eOff = offPair 546 p = pitch.Pitch() 547 p.midi = eOn.pitch 548 pitches.append(p) 549 v = volume.Volume(velocity=eOn.velocity) 550 v.velocityIsRelative = False # velocity is absolute coming from 551 volumes.append(v) 552 # assume it is a flat list 553 elif len(eventList) > 3: 554 onEvents = eventList[:(len(eventList) // 2)] 555 offEvents = eventList[(len(eventList) // 2):] 556 # first is always delta time 557 tOn = onEvents[0].time 558 # use the off time of the last chord member 559 # -1 is the event, -2 is the delta time for the event 560 tOff = offEvents[-2].time 561 # create pitches for the odd on Events: 562 for i in range(1, len(onEvents), 2): 563 p = pitch.Pitch() 564 p.midi = onEvents[i].pitch 565 pitches.append(p) 566 v = volume.Volume(velocity=onEvents[i].velocity) 567 v.velocityIsRelative = False # velocity is absolute coming from 568 volumes.append(v) 569 else: 570 raise TranslateException(f'fewer than 4 events provided to midiEventsToChord: {eventList}') 571 572 # can simply use last-assigned pair of tOff, tOn 573 if (tOff - tOn) != 0: 574 if inputM21 is None: 575 c = chord.Chord(duration=ticksToDuration(tOff - tOn, ticksPerQuarter)) 576 else: 577 c = inputM21 578 c.duration = ticksToDuration(tOff - tOn, ticksPerQuarter, c.duration) 579 else: 580 # for now, get grace 581 if inputM21 is None: 582 c = chord.Chord() 583 else: 584 c = inputM21 585 environLocal.warn(['midi chord with zero duration will be treated as grace', 586 eventList, c]) 587 c.getGrace(inPlace=True) 588 589 c.pitches = pitches 590 c.volume = volumes # can set a list to volume property 591 592 return c 593 594 595def chordToMidiEvents(inputM21, *, includeDeltaTime=True, channel=1): 596 # noinspection PyShadowingNames 597 ''' 598 Translates a :class:`~music21.chord.Chord` object to a 599 list of base.DeltaTime and base.MidiEvents objects. 600 601 The `channel` can be specified, otherwise channel 1 is assumed. 602 603 See noteToMidiEvents above for more details. 604 605 >>> c = chord.Chord(['c3', 'g#4', 'b5']) 606 >>> c.volume = volume.Volume(velocity=90) 607 >>> c.volume.velocityIsRelative = False 608 >>> eventList = midi.translate.chordToMidiEvents(c) 609 >>> eventList 610 [<music21.midi.DeltaTime (empty) track=None, channel=None>, 611 <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=48, velocity=90>, 612 <music21.midi.DeltaTime (empty) track=None, channel=None>, 613 <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=68, velocity=90>, 614 <music21.midi.DeltaTime (empty) track=None, channel=None>, 615 <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=83, velocity=90>, 616 <music21.midi.DeltaTime t=1024, track=None, channel=None>, 617 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=48, velocity=0>, 618 <music21.midi.DeltaTime (empty) track=None, channel=None>, 619 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=68, velocity=0>, 620 <music21.midi.DeltaTime (empty) track=None, channel=None>, 621 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=83, velocity=0>] 622 623 Changed in v7 -- made keyword-only. 624 ''' 625 from music21 import midi as midiModule 626 mt = None # midi track 627 eventList = [] 628 c = inputM21 629 630 # temporary storage for setting correspondence 631 noteOn = [] 632 noteOff = [] 633 634 chordVolume = c.volume # use if component volume are not defined 635 hasComponentVolumes = c.hasComponentVolumes() 636 637 for i in range(len(c)): 638 chordComponent = c[i] 639 # pitchObj = c.pitches[i] 640 # noteObj = chordComponent 641 if includeDeltaTime: 642 dt = midiModule.DeltaTime(track=mt) 643 # for a chord, only the first delta time should have the offset 644 # here, all are zero 645 # leave dt.time at zero; will be shifted later as necessary 646 # add to track events 647 eventList.append(dt) 648 649 me = midiModule.MidiEvent(track=mt) 650 me.type = midiModule.ChannelVoiceMessages.NOTE_ON 651 me.channel = 1 652 me.pitch = chordComponent.pitch.midi 653 if not chordComponent.pitch.isTwelveTone(): 654 me.centShift = chordComponent.pitch.getCentShiftFromMidi() 655 # if 'volume' in chordComponent: 656 657 if hasComponentVolumes: 658 # volScalar = chordComponent.volume.getRealized( 659 # useDynamicContext=False, 660 # useVelocity=True, useArticulations=False) 661 volScalar = chordComponent.volume.cachedRealized 662 else: 663 # volScalar = chordVolume.getRealized( 664 # useDynamicContext=False, 665 # useVelocity=True, useArticulations=False) 666 volScalar = chordVolume.cachedRealized 667 668 me.velocity = int(round(volScalar * 127)) 669 eventList.append(me) 670 noteOn.append(me) 671 672 # must create each note on in chord before each note on 673 for i in range(len(c.pitches)): 674 pitchObj = c.pitches[i] 675 676 if includeDeltaTime: 677 # add note off / velocity zero message 678 dt = midiModule.DeltaTime(track=mt) 679 # for a chord, only the first delta time should have the dur 680 if i == 0: 681 dt.time = durationToMidiTicks(c.duration) 682 eventList.append(dt) 683 684 me = midiModule.MidiEvent(track=mt) 685 me.type = midiModule.ChannelVoiceMessages.NOTE_OFF 686 me.channel = channel 687 me.pitch = pitchObj.midi 688 if not pitchObj.isTwelveTone(): 689 me.centShift = pitchObj.getCentShiftFromMidi() 690 me.velocity = 0 # must be zero 691 eventList.append(me) 692 noteOff.append(me) 693 694 # set correspondence 695 for i, meOn in enumerate(noteOn): 696 meOff = noteOff[i] 697 meOn.correspondingEvent = meOff 698 meOff.correspondingEvent = meOn 699 700 return eventList 701 702 703# ------------------------------------------------------------------------------ 704def instrumentToMidiEvents(inputM21, 705 includeDeltaTime=True, 706 midiTrack=None, 707 channel=1): 708 ''' 709 Converts a :class:`~music21.instrument.Instrument` object to a list of MidiEvents 710 711 TODO: DOCS and TESTS 712 ''' 713 from music21 import midi as midiModule 714 715 inst = inputM21 716 mt = midiTrack # midi track 717 events = [] 718 719 if isinstance(inst, Conductor): 720 return events 721 if includeDeltaTime: 722 dt = midiModule.DeltaTime(track=mt, channel=channel) 723 events.append(dt) 724 me = midiModule.MidiEvent(track=mt) 725 me.type = midiModule.ChannelVoiceMessages.PROGRAM_CHANGE 726 me.channel = channel 727 instMidiProgram = inst.midiProgram 728 if instMidiProgram is None: 729 instMidiProgram = 0 730 me.data = instMidiProgram # key step 731 events.append(me) 732 return events 733 734 735# ------------------------------------------------------------------------------ 736# Meta events 737 738def midiEventsToInstrument(eventList): 739 ''' 740 Convert a single MIDI event into a music21 Instrument object. 741 742 >>> me = midi.MidiEvent() 743 >>> me.type = midi.ChannelVoiceMessages.PROGRAM_CHANGE 744 >>> me.data = 53 # MIDI program 54: Voice Oohs 745 >>> midi.translate.midiEventsToInstrument(me) 746 <music21.instrument.Vocalist 'Voice'> 747 748 The percussion map will be used if the channel is 10: 749 750 >>> me.channel = 10 751 >>> i = midi.translate.midiEventsToInstrument(me) 752 >>> i 753 <music21.instrument.Tambourine 'Tambourine'> 754 >>> i.midiChannel # 0-indexed in music21 755 9 756 >>> i.midiProgram # 0-indexed in music21 757 53 758 ''' 759 from music21 import midi as midiModule 760 761 if not common.isListLike(eventList): 762 event = eventList 763 else: # get the second event; first is delta time 764 event = eventList[1] 765 766 from music21 import instrument 767 decoded: str = '' 768 try: 769 if isinstance(event.data, bytes): 770 # MuseScore writes MIDI files with null-terminated 771 # instrument names. Thus stop before the byte-0x0 772 decoded = event.data.decode('utf-8').split('\x00')[0] 773 decoded = decoded.strip() 774 i = instrument.fromString(decoded) 775 elif event.channel == 10: 776 pm = percussion.PercussionMapper() 777 # PercussionMapper.midiPitchToInstrument() is 1-indexed 778 i = pm.midiPitchToInstrument(event.data + 1) 779 i.midiProgram = event.data 780 else: 781 i = instrument.instrumentFromMidiProgram(event.data) 782 # Instrument.midiProgram and event.data are both 0-indexed 783 i.midiProgram = event.data 784 except UnicodeDecodeError: 785 warnings.warn( 786 f'Unable to determine instrument from {event}; getting generic Instrument', 787 TranslateWarning) 788 i = instrument.Instrument() 789 except percussion.MIDIPercussionException: 790 warnings.warn( 791 f'Unable to determine instrument from {event}; getting generic UnpitchedPercussion', 792 TranslateWarning) 793 i = instrument.UnpitchedPercussion() 794 except instrument.InstrumentException: 795 # Debug logging would be better than warning here 796 i = instrument.Instrument() 797 798 # Set MIDI channel 799 # Instrument.midiChannel is 0-indexed 800 if event.channel is not None: 801 i.midiChannel = event.channel - 1 802 803 # Set partName or instrumentName with literal value from parsing 804 if decoded: 805 # Except for lousy instrument names 806 if ( 807 decoded.lower() in ('instrument', 'inst') 808 or decoded.lower().replace('instrument ', '').isdigit() 809 or decoded.lower().replace('inst ', '').isdigit() 810 ): 811 return i 812 elif event.type == midiModule.MetaEvents.SEQUENCE_TRACK_NAME: 813 i.partName = decoded 814 elif event.type == midiModule.MetaEvents.INSTRUMENT_NAME: 815 i.instrumentName = decoded 816 return i 817 818 819def midiEventsToTimeSignature(eventList): 820 # noinspection PyShadowingNames 821 ''' 822 Convert a single MIDI event into a music21 TimeSignature object. 823 824 >>> mt = midi.MidiTrack(1) 825 >>> me1 = midi.MidiEvent(mt) 826 >>> me1.type = midi.MetaEvents.TIME_SIGNATURE 827 >>> me1.data = midi.putNumbersAsList([3, 1, 24, 8]) # 3/2 time 828 >>> ts = midi.translate.midiEventsToTimeSignature(me1) 829 >>> ts 830 <music21.meter.TimeSignature 3/2> 831 832 >>> me2 = midi.MidiEvent(mt) 833 >>> me2.type = midi.MetaEvents.TIME_SIGNATURE 834 >>> me2.data = midi.putNumbersAsList([3, 4]) # 3/16 time 835 >>> ts = midi.translate.midiEventsToTimeSignature(me2) 836 >>> ts 837 <music21.meter.TimeSignature 3/16> 838 839 ''' 840 # http://www.sonicspot.com/guide/midifiles.html 841 # The time signature defined with 4 bytes, a numerator, a denominator, 842 # a metronome pulse and number of 32nd notes per MIDI quarter-note. 843 # The numerator is specified as a literal value, but the denominator 844 # is specified as (get ready) the value to which the power of 2 must be 845 # raised to equal the number of subdivisions per whole note. For example, 846 # a value of 0 means a whole note because 2 to the power of 0 is 1 847 # (whole note), a value of 1 means a half-note because 2 to the power 848 # of 1 is 2 (half-note), and so on. 849 850 # The metronome pulse specifies how often the metronome should click in 851 # terms of the number of clock signals per click, which come at a rate 852 # of 24 per quarter-note. For example, a value of 24 would mean to click 853 # once every quarter-note (beat) and a value of 48 would mean to click 854 # once every half-note (2 beats). And finally, the fourth byte specifies 855 # the number of 32nd notes per 24 MIDI clock signals. This value is usually 856 # 8 because there are usually 8 32nd notes in a quarter-note. At least one 857 # Time Signature Event should appear in the first track chunk (or all track 858 # chunks in a Type 2 file) before any non-zero delta time events. If one 859 # is not specified 4/4, 24, 8 should be assumed. 860 from music21 import meter 861 from music21 import midi as midiModule 862 863 if not common.isListLike(eventList): 864 event = eventList 865 else: # get the second event; first is delta time 866 event = eventList[1] 867 868 # time signature is 4 byte encoding 869 post = midiModule.getNumbersAsList(event.data) 870 871 n = post[0] 872 d = pow(2, post[1]) 873 ts = meter.TimeSignature(f'{n}/{d}') 874 return ts 875 876 877def timeSignatureToMidiEvents(ts, includeDeltaTime=True): 878 # noinspection PyShadowingNames 879 ''' 880 Translate a :class:`~music21.meter.TimeSignature` to a pair of events: a DeltaTime and 881 a MidiEvent TIME_SIGNATURE. 882 883 Returns a two-element list 884 885 >>> ts = meter.TimeSignature('5/4') 886 >>> eventList = midi.translate.timeSignatureToMidiEvents(ts) 887 >>> eventList[0] 888 <music21.midi.DeltaTime (empty) track=None, channel=None> 889 >>> eventList[1] 890 <music21.midi.MidiEvent TIME_SIGNATURE, track=None, channel=1, data=b'\\x05\\x02\\x18\\x08'> 891 ''' 892 from music21 import midi as midiModule 893 894 mt = None # use a midi track set to None 895 eventList = [] 896 if includeDeltaTime: 897 dt = midiModule.DeltaTime(track=mt) 898 # dt.time set to zero; will be shifted later as necessary 899 # add to track events 900 eventList.append(dt) 901 902 n = ts.numerator 903 # need log base 2 to solve for exponent of 2 904 # 1 is 0, 2 is 1, 4 is 2, 16 is 4, etc 905 d = int(math.log2(ts.denominator)) 906 metroClick = 24 # clock signals per click, clicks are 24 per quarter 907 subCount = 8 # number of 32 notes in a quarter note 908 909 me = midiModule.MidiEvent(track=mt) 910 me.type = midiModule.MetaEvents.TIME_SIGNATURE 911 me.channel = 1 912 me.data = midiModule.putNumbersAsList([n, d, metroClick, subCount]) 913 eventList.append(me) 914 return eventList 915 916 917def midiEventsToKey(eventList) -> 'music21.key.Key': 918 # noinspection PyShadowingNames 919 r''' 920 Convert a single MIDI event into a :class:`~music21.key.KeySignature` object. 921 922 >>> mt = midi.MidiTrack(1) 923 >>> me1 = midi.MidiEvent(mt) 924 >>> me1.type = midi.MetaEvents.KEY_SIGNATURE 925 >>> me1.data = midi.putNumbersAsList([2, 0]) # d major 926 >>> ks = midi.translate.midiEventsToKey(me1) 927 >>> ks 928 <music21.key.Key of D major> 929 >>> ks.mode 930 'major' 931 932 >>> me2 = midi.MidiEvent(mt) 933 >>> me2.type = midi.MetaEvents.KEY_SIGNATURE 934 >>> me2.data = midi.putNumbersAsList([-2, 1]) # g minor 935 >>> me2.data 936 b'\xfe\x01' 937 >>> midi.getNumbersAsList(me2.data) 938 [254, 1] 939 >>> ks = midi.translate.midiEventsToKey(me2) 940 >>> ks 941 <music21.key.Key of g minor> 942 >>> ks.sharps 943 -2 944 >>> ks.mode 945 'minor' 946 ''' 947 # This meta event is used to specify the key (number of sharps or flats) 948 # and scale (major or minor) of a sequence. A positive value for 949 # the key specifies the number of sharps and a negative value specifies 950 # the number of flats. A value of 0 for the scale specifies a major key 951 # and a value of 1 specifies a minor key. 952 from music21 import key 953 from music21 import midi as midiModule 954 955 if not common.isListLike(eventList): 956 event = eventList 957 else: # get the second event; first is delta time 958 event = eventList[1] 959 post = midiModule.getNumbersAsList(event.data) 960 961 # first value is number of sharp, or neg for number of flat 962 if post[0] > 12: 963 # flip around 256 964 sharpCount = post[0] - 256 # need negative values 965 else: 966 sharpCount = post[0] 967 968 mode = 'major' 969 if post[1] == 1: 970 mode = 'minor' 971 972 # environLocal.printDebug(['midiEventsToKey', post, sharpCount]) 973 ks = key.KeySignature(sharpCount) 974 k = ks.asKey(mode) 975 976 return k 977 978 979def keySignatureToMidiEvents(ks: 'music21.key.KeySignature', includeDeltaTime=True): 980 # noinspection PyShadowingNames 981 r''' 982 Convert a single :class:`~music21.key.Key` or 983 :class:`~music21.key.KeySignature` object to 984 a two-element list of midi events, 985 where the first is an empty DeltaTime (unless includeDeltaTime is False) and the second 986 is a KEY_SIGNATURE :class:`~music21.midi.MidiEvent` 987 988 >>> ks = key.KeySignature(2) 989 >>> ks 990 <music21.key.KeySignature of 2 sharps> 991 >>> eventList = midi.translate.keySignatureToMidiEvents(ks) 992 >>> eventList 993 [<music21.midi.DeltaTime (empty) track=None, channel=None>, 994 <music21.midi.MidiEvent KEY_SIGNATURE, track=None, channel=1, data=b'\x02\x00'>] 995 996 >>> k = key.Key('b-') 997 >>> k 998 <music21.key.Key of b- minor> 999 >>> eventList = midi.translate.keySignatureToMidiEvents(k, includeDeltaTime=False) 1000 >>> eventList 1001 [<music21.midi.MidiEvent KEY_SIGNATURE, track=None, channel=1, data=b'\xfb\x01'>] 1002 ''' 1003 from music21 import midi as midiModule 1004 mt = None # use a midi track set to None 1005 eventList = [] 1006 if includeDeltaTime: 1007 dt = midiModule.DeltaTime(track=mt) 1008 # leave dt.time set to zero; will be shifted later as necessary 1009 # add to track events 1010 eventList.append(dt) 1011 sharpCount = ks.sharps 1012 if hasattr(ks, 'mode') and ks.mode == 'minor': 1013 mode = 1 1014 else: # major or None; must define one 1015 mode = 0 1016 me = midiModule.MidiEvent(track=mt) 1017 me.type = midiModule.MetaEvents.KEY_SIGNATURE 1018 me.channel = 1 1019 me.data = midiModule.putNumbersAsList([sharpCount, mode]) 1020 eventList.append(me) 1021 return eventList 1022 1023 1024def midiEventsToTempo(eventList): 1025 ''' 1026 Convert a single MIDI event into a music21 Tempo object. 1027 1028 TODO: Need Tests 1029 ''' 1030 from music21 import midi as midiModule 1031 from music21 import tempo 1032 1033 if not common.isListLike(eventList): 1034 event = eventList 1035 else: # get the second event; first is delta time 1036 event = eventList[1] 1037 # get microseconds per quarter 1038 mspq = midiModule.getNumber(event.data, 3)[0] # first data is number 1039 bpm = round(60_000_000 / mspq, 2) 1040 # post = midiModule.getNumbersAsList(event.data) 1041 # environLocal.printDebug(['midiEventsToTempo, got bpm', bpm]) 1042 mm = tempo.MetronomeMark(number=bpm) 1043 return mm 1044 1045 1046def tempoToMidiEvents(tempoIndication, includeDeltaTime=True): 1047 # noinspection PyShadowingNames 1048 r''' 1049 Given any TempoIndication, convert it to list of :class:`~music21.midi.MidiEvent` 1050 objects that signifies a MIDI tempo indication. 1051 1052 >>> mm = tempo.MetronomeMark(number=90) 1053 >>> events = midi.translate.tempoToMidiEvents(mm) 1054 >>> events 1055 [<music21.midi.DeltaTime ...>, <music21.midi.MidiEvent SET_TEMPO...>] 1056 >>> len(events) 1057 2 1058 1059 >>> events[0] 1060 <music21.midi.DeltaTime (empty) track=None, channel=None> 1061 1062 >>> evt1 = events[1] 1063 >>> evt1 1064 <music21.midi.MidiEvent SET_TEMPO, track=None, channel=1, data=b'\n,+'> 1065 >>> evt1.data 1066 b'\n,+' 1067 >>> microSecondsPerQuarterNote = midi.getNumber(evt1.data, len(evt1.data))[0] 1068 >>> microSecondsPerQuarterNote 1069 666667 1070 1071 >>> round(60_000_000 / microSecondsPerQuarterNote, 1) 1072 90.0 1073 1074 If includeDeltaTime is False then the DeltaTime object is omitted: 1075 1076 >>> midi.translate.tempoToMidiEvents(mm, includeDeltaTime=False) 1077 [<music21.midi.MidiEvent SET_TEMPO...>] 1078 1079 1080 Test round-trip. Note that for pure tempo numbers, by default 1081 we create a text name if there's an appropriate one: 1082 1083 >>> midi.translate.midiEventsToTempo(events) 1084 <music21.tempo.MetronomeMark maestoso Quarter=90.0> 1085 1086 `None` is returned if the MetronomeMark lacks a number, which can 1087 happen with metric modulation marks. 1088 1089 >>> midi.translate.tempoToMidiEvents(tempo.MetronomeMark(number=None)) is None 1090 True 1091 ''' 1092 from music21 import midi as midiModule 1093 if tempoIndication.number is None: 1094 return 1095 mt = None # use a midi track set to None 1096 eventList = [] 1097 if includeDeltaTime: 1098 dt = midiModule.DeltaTime(track=mt) 1099 eventList.append(dt) 1100 1101 me = midiModule.MidiEvent(track=mt) 1102 me.type = midiModule.MetaEvents.SET_TEMPO 1103 me.channel = 1 1104 1105 # from any tempo indication, get the sounding metronome mark 1106 mm = tempoIndication.getSoundingMetronomeMark() 1107 bpm = mm.getQuarterBPM() 1108 mspq = int(round(60_000_000 / bpm)) # microseconds per quarter note 1109 1110 me.data = midiModule.putNumber(mspq, 3) 1111 eventList.append(me) 1112 return eventList 1113 1114 1115# ------------------------------------------------------------------------------ 1116# Streams 1117 1118 1119def getPacketFromMidiEvent( 1120 trackId: int, 1121 offset: int, 1122 midiEvent: 'music21.midi.MidiEvent', 1123 obj: Optional['music21.base.Music21Object'] = None, 1124 lastInstrument: Optional['music21.instrument.Instrument'] = None 1125) -> Dict[str, Any]: 1126 ''' 1127 Pack a dictionary of parameters for each event. 1128 Packets are used for sorting and configuring all note events. 1129 Includes offset, any cent shift, the midi event, and the source object. 1130 1131 Offset and duration values stored here are MIDI ticks, not quarter lengths. 1132 1133 >>> n = note.Note('C4') 1134 >>> midiEvents = midi.translate.elementToMidiEventList(n) 1135 >>> getPacket = midi.translate.getPacketFromMidiEvent 1136 >>> getPacket(trackId=1, offset=0, midiEvent=midiEvents[0], obj=n) 1137 {'trackId': 1, 1138 'offset': 0, 1139 'midiEvent': <music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=60, velocity=90>, 1140 'obj': <music21.note.Note C>, 1141 'centShift': None, 1142 'duration': 1024, 1143 'lastInstrument': None} 1144 >>> inst = instrument.Harpsichord() 1145 >>> getPacket(trackId=1, offset=0, midiEvent=midiEvents[1], obj=n, lastInstrument=inst) 1146 {'trackId': 1, 1147 'offset': 0, 1148 'midiEvent': <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=60, velocity=0>, 1149 'obj': <music21.note.Note C>, 1150 'centShift': None, 1151 'duration': 0, 1152 'lastInstrument': <music21.instrument.Harpsichord 'Harpsichord'>} 1153 ''' 1154 from music21 import midi as midiModule 1155 post = { 1156 'trackId': trackId, 1157 'offset': offset, # offset values are in midi ticks 1158 'midiEvent': midiEvent, 1159 'obj': obj, # keep a reference to the source object 1160 'centShift': midiEvent.centShift, 1161 'duration': 0, 1162 # store last m21 instrument object, as needed to reset program changes 1163 'lastInstrument': lastInstrument, 1164 } 1165 1166 # allocate channel later 1167 # post['channel'] = None 1168 if midiEvent.type != midiModule.ChannelVoiceMessages.NOTE_OFF and obj is not None: 1169 # store duration so as to calculate when the 1170 # channel/pitch bend can be freed 1171 post['duration'] = durationToMidiTicks(obj.duration) 1172 # note offs will have the same object ref, and seem like the have a 1173 # duration when they do not 1174 1175 return post 1176 1177 1178def elementToMidiEventList( 1179 el: 'music21.base.Music21Object' 1180) -> Optional[List['music21.midi.MidiEvent']]: 1181 ''' 1182 Return a list of MidiEvents (or None) from a Music21Object, 1183 assuming that dynamics have already been applied, etc. 1184 Does not include DeltaTime objects. 1185 1186 Channel (1-indexed) is set to the default, 1. 1187 Track is not set. 1188 1189 >>> n = note.Note('C4') 1190 >>> midiEvents = midi.translate.elementToMidiEventList(n) 1191 >>> midiEvents 1192 [<music21.midi.MidiEvent NOTE_ON, track=None, channel=1, pitch=60, velocity=90>, 1193 <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=60, velocity=0>] 1194 ''' 1195 classes = el.classes 1196 if 'Rest' in classes: 1197 return 1198 elif 'Note' in classes: 1199 # get a list of midi events 1200 # using this property here is easier than using the above conversion 1201 # methods, as we do not need to know what the object is 1202 sub = noteToMidiEvents(el, includeDeltaTime=False) 1203 # TODO: unpitched 1204 elif 'Chord' in classes: 1205 # TODO: skip Harmony unless showAsChord 1206 sub = chordToMidiEvents(el, includeDeltaTime=False) 1207 elif 'Dynamic' in classes: 1208 return # dynamics have already been applied to notes 1209 elif 'TimeSignature' in classes: 1210 # return a pair of events 1211 el: 'music21.meter.TimeSignature' 1212 sub = timeSignatureToMidiEvents(el, includeDeltaTime=False) 1213 elif 'KeySignature' in classes: 1214 el: 'music21.key.KeySignature' 1215 sub = keySignatureToMidiEvents(el, includeDeltaTime=False) 1216 elif 'TempoIndication' in classes: 1217 # any tempo indication will work 1218 # note: tempo indications need to be in channel one for most playback 1219 el: 'music21.tempo.TempoIndication' 1220 sub = tempoToMidiEvents(el, includeDeltaTime=False) 1221 elif 'Instrument' in classes: 1222 # first instrument will have been gathered above with get start elements 1223 sub = instrumentToMidiEvents(el, includeDeltaTime=False) 1224 else: 1225 # other objects may have already been added 1226 return 1227 1228 return sub 1229 1230 1231def streamToPackets( 1232 s: stream.Stream, 1233 trackId: int = 1, 1234 addStartDelay: bool = False, 1235) -> List[Dict[str, Any]]: 1236 ''' 1237 Convert a (flattened, sorted) Stream to packets. 1238 1239 This assumes that the Stream has already been flattened, 1240 ties have been stripped, and instruments, 1241 if necessary, have been added. 1242 1243 In converting from a Stream to MIDI, this is called first, 1244 resulting in a collection of packets by offset. 1245 Then, packets to events is called. 1246 ''' 1247 from music21 import midi as midiModule 1248 # store all events by offset by offset without delta times 1249 # as (absTime, event) 1250 packetsByOffset = [] 1251 lastInstrument = None 1252 1253 # s should already be flat and sorted 1254 for el in s: 1255 midiEventList = elementToMidiEventList(el) 1256 if 'Instrument' in el.classes: 1257 lastInstrument = el # store last instrument 1258 1259 if midiEventList is None: 1260 continue 1261 1262 # we process midiEventList here, which is a list of midi events 1263 # for each event, we create a packet representation 1264 # all events: delta/note-on/delta/note-off 1265 # strip delta times 1266 elementPackets = [] 1267 firstNotePlayed = False 1268 for i in range(len(midiEventList)): 1269 # store offset, midi event, object 1270 # add channel and pitch change also 1271 midiEvent = midiEventList[i] 1272 if (midiEvent.type == midiModule.ChannelVoiceMessages.NOTE_ON 1273 and firstNotePlayed is False): 1274 firstNotePlayed = True 1275 1276 if firstNotePlayed is False: 1277 o = offsetToMidiTicks(s.elementOffset(el), addStartDelay=False) 1278 else: 1279 o = offsetToMidiTicks(s.elementOffset(el), addStartDelay=addStartDelay) 1280 1281 if midiEvent.type != midiModule.ChannelVoiceMessages.NOTE_OFF: 1282 # use offset 1283 p = getPacketFromMidiEvent( 1284 trackId, 1285 o, 1286 midiEvent, 1287 obj=el, 1288 lastInstrument=lastInstrument, 1289 ) 1290 elementPackets.append(p) 1291 # if its a note_off, use the duration to shift offset 1292 # midi events have already been created; 1293 else: 1294 p = getPacketFromMidiEvent( 1295 trackId, 1296 o + durationToMidiTicks(el.duration), 1297 midiEvent, 1298 obj=el, 1299 lastInstrument=lastInstrument) 1300 elementPackets.append(p) 1301 packetsByOffset += elementPackets 1302 1303 # sorting is useful here, as we need these to be in order to assign last 1304 # instrument 1305 packetsByOffset.sort( 1306 key=lambda x: (x['offset'], x['midiEvent'].sortOrder) 1307 ) 1308 # return packets and stream, as this flat stream should be retained 1309 return packetsByOffset 1310 1311 1312def assignPacketsToChannels( 1313 packets, 1314 channelByInstrument=None, 1315 channelsDynamic=None, 1316 initTrackIdToChannelMap=None): 1317 ''' 1318 Given a list of packets, assign each to a channel. 1319 1320 Do each track one at time, based on the track id. 1321 1322 Shift to different channels if a pitch bend is necessary. 1323 1324 Keep track of which channels are available. 1325 Need to insert a program change in the empty channel 1326 too, based on last instrument. 1327 1328 Insert pitch bend messages as well, 1329 one for start of event, one for end of event. 1330 1331 `packets` is a list of packets. 1332 `channelByInstrument` should be a dictionary. 1333 `channelsDynamic` should be a list. 1334 `initTrackIdToChannelMap` should be a dictionary. 1335 ''' 1336 from music21 import midi as midiModule 1337 1338 if channelByInstrument is None: 1339 channelByInstrument = {} 1340 if channelsDynamic is None: 1341 channelsDynamic = [] 1342 if initTrackIdToChannelMap is None: 1343 initTrackIdToChannelMap = {} 1344 1345 uniqueChannelEvents = {} # dict of (start, stop, usedChannel) : channel 1346 post = [] 1347 usedTracks = [] 1348 1349 for p in packets: 1350 # environLocal.printDebug(['assignPacketsToChannels', p['midiEvent'].track, p['trackId']]) 1351 # must use trackId, as .track on MidiEvent is not yet set 1352 if p['trackId'] not in usedTracks: 1353 usedTracks.append(p['trackId']) 1354 1355 # only need note_ons, as stored correspondingEvent attr can be used 1356 # to get noteOff 1357 if p['midiEvent'].type != midiModule.ChannelVoiceMessages.NOTE_ON: 1358 # set all not note-off messages to init channel 1359 if p['midiEvent'].type != midiModule.ChannelVoiceMessages.NOTE_OFF: 1360 p['midiEvent'].channel = p['initChannel'] 1361 post.append(p) # add the non note_on packet first 1362 # if this is a note off, and has a cent shift, need to 1363 # rest the pitch bend back to 0 cents 1364 if p['midiEvent'].type == midiModule.ChannelVoiceMessages.NOTE_OFF: 1365 # environLocal.printDebug(['got note-off', p['midiEvent']]) 1366 # cent shift is set for note on and note off 1367 if p['centShift']: 1368 # do not set channel, as already set 1369 me = midiModule.MidiEvent(p['midiEvent'].track, 1370 type=midiModule.ChannelVoiceMessages.PITCH_BEND, 1371 channel=p['midiEvent'].channel) 1372 # note off stores a note on for each pitch; do not invert, simply 1373 # set to zero 1374 me.setPitchBend(0) 1375 pBendEnd = getPacketFromMidiEvent( 1376 trackId=p['trackId'], 1377 offset=p['offset'], 1378 midiEvent=me, 1379 ) 1380 post.append(pBendEnd) 1381 # environLocal.printDebug(['adding pitch bend', pBendEnd]) 1382 continue # store and continue 1383 1384 # set default channel for all packets 1385 p['midiEvent'].channel = p['initChannel'] 1386 1387 # find a free channel 1388 # if necessary, add pitch change at start of Note, 1389 # cancel pitch change at end 1390 o = p['offset'] 1391 oEnd = p['offset'] + p['duration'] 1392 1393 channelExclude = [] # channels that cannot be used 1394 centShift = p['centShift'] # may be None 1395 1396 # environLocal.printDebug(['\n\n', 'offset', o, 'oEnd', oEnd, 'centShift', centShift]) 1397 1398 # iterate through all past events/channels, and find all 1399 # that are active and have a pitch bend 1400 for key in uniqueChannelEvents: 1401 start, stop, usedChannel = key 1402 # if offset (start time) is in this range of a found event 1403 # or if any start or stop is within this span 1404 # if o >= start and o < stop: # found an offset that is used 1405 1406 if ((o <= start < oEnd) 1407 or (o < stop < oEnd) 1408 or (start <= o < stop) 1409 or (start < oEnd < stop)): 1410 # if there is a cent shift active in the already used channel 1411 # environLocal.printDebug(['matchedOffset overlap']) 1412 centShiftList = uniqueChannelEvents[key] 1413 if centShiftList: 1414 # only add if unique 1415 if usedChannel not in channelExclude: 1416 channelExclude.append(usedChannel) 1417 # or if this event has shift, then we can exclude 1418 # the channel already used without a shift 1419 elif centShift: 1420 if usedChannel not in channelExclude: 1421 channelExclude.append(usedChannel) 1422 # cannot break early w/o sorting 1423 1424 # if no channels are excluded, get a new channel 1425 # environLocal.printDebug(['post process channelExclude', channelExclude]) 1426 if channelExclude: # only change if necessary 1427 ch = None 1428 # iterate in order over all channels: lower will be added first 1429 for x in channelsDynamic: 1430 if x not in channelExclude: 1431 ch = x 1432 break 1433 if ch is None: 1434 raise TranslateException( 1435 'no unused channels available for microtone/instrument assignment') 1436 p['midiEvent'].channel = ch 1437 # change channel of note off; this is used above to turn off bend 1438 p['midiEvent'].correspondingEvent.channel = ch 1439 # environLocal.printDebug(['set channel of correspondingEvent:', 1440 # p['midiEvent'].correspondingEvent]) 1441 1442 # TODO: must add program change, as we are now in a new 1443 # channel; regardless of if we have a pitch bend (we may 1444 # move channels for a different reason 1445 if p['lastInstrument'] is not None: 1446 meList = instrumentToMidiEvents(inputM21=p['lastInstrument'], 1447 includeDeltaTime=False, 1448 midiTrack=p['midiEvent'].track, 1449 channel=ch) 1450 pgmChangePacket = getPacketFromMidiEvent( 1451 trackId=p['trackId'], 1452 offset=o, # keep offset here 1453 midiEvent=meList[0], 1454 ) 1455 post.append(pgmChangePacket) 1456 1457 else: # use the existing channel 1458 ch = p['midiEvent'].channel 1459 # always set corresponding event to the same channel 1460 p['midiEvent'].correspondingEvent.channel = ch 1461 1462 # environLocal.printDebug(['assigning channel', ch, 'channelsDynamic', channelsDynamic, 1463 # 'p['initChannel']', p['initChannel']]) 1464 1465 if centShift: 1466 # add pitch bend 1467 me = midiModule.MidiEvent(p['midiEvent'].track, 1468 type=midiModule.ChannelVoiceMessages.PITCH_BEND, 1469 channel=ch) 1470 me.setPitchBend(centShift) 1471 pBendStart = getPacketFromMidiEvent( 1472 trackId=p['trackId'], 1473 offset=o, 1474 midiEvent=me, # keep offset here 1475 ) 1476 post.append(pBendStart) 1477 # environLocal.printDebug(['adding pitch bend', me]) 1478 # removal of pitch bend will happen above with note off 1479 1480 # key includes channel, so that durations can span once in each channel 1481 key = (p['offset'], p['offset'] + p['duration'], ch) 1482 if key not in uniqueChannelEvents: 1483 # need to count multiple instances of events on the same 1484 # span and in the same channel (fine if all have the same pitch bend 1485 uniqueChannelEvents[key] = [] 1486 # always add the cent shift if it is not None 1487 if centShift: 1488 uniqueChannelEvents[key].append(centShift) 1489 post.append(p) # add packet/ done after ch change or bend addition 1490 # environLocal.printDebug(['uniqueChannelEvents', uniqueChannelEvents]) 1491 1492 # this is called once at completion 1493 # environLocal.printDebug(['uniqueChannelEvents', uniqueChannelEvents]) 1494 1495 # after processing, collect all channels used 1496 foundChannels = [] 1497 for start, stop, usedChannel in list(uniqueChannelEvents): # a list 1498 if usedChannel not in foundChannels: 1499 foundChannels.append(usedChannel) 1500 # for ch in chList: 1501 # if ch not in foundChannels: 1502 # foundChannels.append(ch) 1503 # environLocal.printDebug(['foundChannels', foundChannels]) 1504 # environLocal.printDebug(['usedTracks', usedTracks]) 1505 1506 # post processing of entire packet collection 1507 # for all used channels, create a zero pitch bend at time zero 1508 # for ch in foundChannels: 1509 # for each track, places a pitch bend in its initChannel 1510 for trackId in usedTracks: 1511 if trackId == 0: 1512 continue # Conductor track: do not add pitch bend 1513 ch = initTrackIdToChannelMap[trackId] 1514 # use None for track; will get updated later 1515 me = midiModule.MidiEvent(track=trackId, 1516 type=midiModule.ChannelVoiceMessages.PITCH_BEND, 1517 channel=ch) 1518 me.setPitchBend(0) 1519 pBendEnd = getPacketFromMidiEvent( 1520 trackId=trackId, 1521 offset=0, 1522 midiEvent=me, 1523 ) 1524 post.append(pBendEnd) 1525 # environLocal.printDebug(['adding pitch bend for found channels', me]) 1526 # this sort is necessary 1527 post.sort( 1528 key=lambda x_event: (x_event['offset'], x_event['midiEvent'].sortOrder) 1529 ) 1530 1531 # TODO: for each track, add an additional silent event to make sure 1532 # entire duration gets played 1533 1534 # diagnostic display 1535 # for p in post: environLocal.printDebug(['processed packet', p]) 1536 1537 # post = packets 1538 return post 1539 1540 1541def filterPacketsByTrackId( 1542 packetsSrc: List[Dict[str, Any]], 1543 trackIdFilter: Optional[int] = None, 1544) -> List[Dict[str, Any]]: 1545 ''' 1546 Given a list of Packet dictionaries, return a list of 1547 only those whose trackId matches the filter. 1548 1549 >>> packets = [ 1550 ... {'trackId': 1, 'name': 'hello'}, 1551 ... {'trackId': 2, 'name': 'bye'}, 1552 ... {'trackId': 1, 'name': 'hi'}, 1553 ... ] 1554 >>> midi.translate.filterPacketsByTrackId(packets, 1) 1555 [{'trackId': 1, 'name': 'hello'}, 1556 {'trackId': 1, 'name': 'hi'}] 1557 >>> midi.translate.filterPacketsByTrackId(packets, 2) 1558 [{'trackId': 2, 'name': 'bye'}] 1559 1560 If no trackIdFilter is passed, the original list is returned: 1561 1562 >>> midi.translate.filterPacketsByTrackId(packets) is packets 1563 True 1564 ''' 1565 if trackIdFilter is None: 1566 return packetsSrc 1567 1568 outPackets = [] 1569 for packet in packetsSrc: 1570 if packet['trackId'] == trackIdFilter: 1571 outPackets.append(packet) 1572 return outPackets 1573 1574 1575def packetsToDeltaSeparatedEvents( 1576 packets: List[Dict[str, Any]], 1577 midiTrack: 'music21.midi.MidiTrack' 1578) -> List['music21.midi.MidiEvent']: 1579 ''' 1580 Given a list of packets (which already contain MidiEvent objects) 1581 return a list of those Events with proper delta times between them. 1582 1583 At this stage MIDI event objects have been created. 1584 The key process here is finding the adjacent time 1585 between events and adding DeltaTime events before each MIDI event. 1586 1587 Delta time channel values are derived from the previous midi event. 1588 ''' 1589 from music21.midi import DeltaTime 1590 1591 events = [] 1592 lastOffset = 0 1593 for packet in packets: 1594 midiEvent = packet['midiEvent'] 1595 t = packet['offset'] - lastOffset 1596 if t < 0: 1597 raise TranslateException('got a negative delta time') 1598 # set the channel from the midi event 1599 dt = DeltaTime(midiTrack, time=t, channel=midiEvent.channel) 1600 # environLocal.printDebug(['packetsByOffset', packet]) 1601 events.append(dt) 1602 events.append(midiEvent) 1603 lastOffset = packet['offset'] 1604 # environLocal.printDebug(['packetsToDeltaSeparatedEvents', 'total events:', len(events)]) 1605 return events 1606 1607 1608def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None): 1609 ''' 1610 Given packets already allocated with channel 1611 and/or instrument assignments, place these in a MidiTrack. 1612 1613 Note that all packets can be sent; only those with 1614 matching trackIds will be collected into the resulting track 1615 1616 The `channel` defines the channel that startEvents and endEvents 1617 will be assigned to 1618 1619 Use streamToPackets to convert the Stream to the packets 1620 ''' 1621 from music21 import midi as midiModule 1622 1623 # TODO: for a given track id, need to find start/end channel 1624 mt = midiModule.MidiTrack(trackId) 1625 # set startEvents to preferred channel 1626 mt.events += getStartEvents(mt, 1627 channel=channel, 1628 instrumentObj=instrumentObj) 1629 1630 # filter only those packets for this track 1631 trackPackets = filterPacketsByTrackId(packets, trackId) 1632 mt.events += packetsToDeltaSeparatedEvents(trackPackets, mt) 1633 1634 # must update all events with a ref to this MidiTrack 1635 mt.events += getEndEvents(mt, channel=channel) 1636 mt.updateEvents() # sets this track as .track for all events 1637 return mt 1638 1639 1640def getTimeForEvents( 1641 mt: 'music21.midi.MidiTrack' 1642) -> List[Tuple[int, 'music21.midi.MidiEvent']]: 1643 ''' 1644 Get a list of tuples of (tickTime, MidiEvent) from the events with time deltas. 1645 ''' 1646 # get an abs start time for each event, discard deltas 1647 events = [] 1648 currentTime = 0 1649 1650 # pair deltas with events, convert abs time 1651 # get even numbers 1652 # in some cases, the first event may not be a delta time, but 1653 # a SEQUENCE_TRACK_NAME or something else. thus, need to get 1654 # first delta time 1655 i = 0 1656 while i < len(mt.events): 1657 currentEvent = mt.events[i] 1658 try: 1659 nextEvent = mt.events[i + 1] 1660 except IndexError: # pragma: no cover 1661 break 1662 1663 currentDt = currentEvent.isDeltaTime() 1664 nextDt = nextEvent.isDeltaTime() 1665 1666 # in pairs, first should be delta time, second should be event 1667 # environLocal.printDebug(['midiTrackToStream(): index', 'i', i, mt.events[i]]) 1668 # environLocal.printDebug(['midiTrackToStream(): index', 'i + 1', i + 1, mt.events[i + 1]]) 1669 1670 # need to find pairs of delta time and events 1671 # in some cases, there are delta times that are out of order, or 1672 # packed in the beginning 1673 if currentDt and not nextDt: 1674 currentTime += currentEvent.time # increment time 1675 tupleAppend = (currentTime, nextEvent) 1676 events.append(tupleAppend) 1677 i += 2 1678 elif (not currentDt 1679 and not nextDt): 1680 # environLocal.printDebug(['midiTrackToStream(): got two non delta times in a row']) 1681 i += 1 1682 elif currentDt and nextDt: 1683 # environLocal.printDebug(['midiTrackToStream(): got two delta times in a row']) 1684 i += 1 1685 else: 1686 # cannot pair delta time to the next event; skip by 1 1687 # environLocal.printDebug(['cannot pair to delta time', mt.events[i]]) 1688 i += 1 1689 1690 return events 1691 1692 1693def getNotesFromEvents( 1694 events: List[Tuple[int, 'music21.midi.MidiEvent']] 1695) -> List[Tuple[Tuple[int, 'music21.midi.MidiEvent'], 1696 Tuple[int, 'music21.midi.MidiEvent']]]: 1697 ''' 1698 Returns a list of Tuples of MIDI events that are pairs of note-on and 1699 note-off events. 1700 1701 ''' 1702 notes = [] # store pairs of pairs 1703 memo = set() # store already matched note off 1704 for i, eventTuple in enumerate(events): 1705 if i in memo: 1706 continue 1707 unused_t, e = eventTuple 1708 # for each note on event, we need to search for a match in all future 1709 # events 1710 if not e.isNoteOn(): 1711 continue 1712 match = None 1713 # environLocal.printDebug(['midiTrackToStream(): isNoteOn', e]) 1714 for j in range(i + 1, len(events)): 1715 if j in memo: 1716 continue 1717 unused_tSub, eSub = events[j] 1718 if e.matchedNoteOff(eSub): 1719 memo.add(j) 1720 match = i, j 1721 break 1722 if match is not None: 1723 i, j = match 1724 pairs = (events[i], events[j]) 1725 notes.append(pairs) 1726 else: 1727 pass 1728 # environLocal.printDebug([ 1729 # 'midiTrackToStream(): cannot find a note off for a note on', e]) 1730 return notes 1731 1732 1733def getMetaEvents(events): 1734 from music21.midi import MetaEvents, ChannelVoiceMessages 1735 1736 metaEvents = [] # store pairs of abs time, m21 object 1737 last_program: int = -1 1738 for eventTuple in events: 1739 t, e = eventTuple 1740 metaObj = None 1741 if e.type == MetaEvents.TIME_SIGNATURE: 1742 # time signature should be 4 bytes 1743 metaObj = midiEventsToTimeSignature(e) 1744 elif e.type == MetaEvents.KEY_SIGNATURE: 1745 metaObj = midiEventsToKey(e) 1746 elif e.type == MetaEvents.SET_TEMPO: 1747 metaObj = midiEventsToTempo(e) 1748 elif e.type in (MetaEvents.INSTRUMENT_NAME, MetaEvents.SEQUENCE_TRACK_NAME): 1749 # midiEventsToInstrument() WILL NOT have knowledge of the current 1750 # program, so set it here 1751 metaObj = midiEventsToInstrument(e) 1752 if last_program != -1: 1753 # Only update if we have had an initial PROGRAM_CHANGE 1754 metaObj.midiProgram = last_program 1755 elif e.type == ChannelVoiceMessages.PROGRAM_CHANGE: 1756 # midiEventsToInstrument() WILL set the program on the instance 1757 metaObj = midiEventsToInstrument(e) 1758 last_program = e.data 1759 elif e.type == MetaEvents.MIDI_PORT: 1760 pass 1761 else: 1762 pass 1763 if metaObj: 1764 pair = (t, metaObj) 1765 metaEvents.append(pair) 1766 1767 return metaEvents 1768 1769def insertConductorEvents(conductorPart: stream.Part, 1770 target: stream.Part, 1771 *, 1772 isFirst: bool = False, 1773 ): 1774 ''' 1775 Insert a deepcopy of any TimeSignature, KeySignature, or MetronomeMark 1776 found in the `conductorPart` into the `target` Part at the same offset. 1777 1778 Obligatory to do this before making measures. New in v7. 1779 ''' 1780 for e in conductorPart.getElementsByClass( 1781 ('TimeSignature', 'KeySignature', 'MetronomeMark')): 1782 # create a deepcopy of the element so a flat does not cause 1783 # multiple references of the same 1784 eventCopy = copy.deepcopy(e) 1785 if 'TempoIndication' in eventCopy.classes and not isFirst: 1786 eventCopy.style.hideObjectOnPrint = True 1787 eventCopy.numberImplicit = True 1788 target.insert(conductorPart.elementOffset(e), eventCopy) 1789 1790def midiTrackToStream( 1791 mt, 1792 ticksPerQuarter=None, 1793 quantizePost=True, 1794 inputM21=None, 1795 conductorPart: Optional[stream.Part] = None, 1796 isFirst: bool = False, 1797 **keywords 1798) -> stream.Part: 1799 # noinspection PyShadowingNames 1800 ''' 1801 Note that quantization takes place in stream.py since it's useful not just for MIDI. 1802 1803 >>> fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test05.mid' 1804 >>> mf = midi.MidiFile() 1805 >>> mf.open(fp) 1806 >>> mf.read() 1807 >>> mf.close() 1808 >>> mf 1809 <music21.midi.MidiFile 1 track> 1810 >>> len(mf.tracks) 1811 1 1812 >>> mt = mf.tracks[0] 1813 >>> mt 1814 <music21.midi.MidiTrack 0 -- 56 events> 1815 >>> mt.events 1816 [<music21.midi.DeltaTime ...>, 1817 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME...>, 1818 <music21.midi.DeltaTime ...>, 1819 <music21.midi.MidiEvent NOTE_ON, track=0, channel=1, pitch=36, velocity=90>, 1820 ...] 1821 >>> p = midi.translate.midiTrackToStream(mt) 1822 >>> p 1823 <music21.stream.Part ...> 1824 >>> len(p.recurse().notesAndRests) 1825 14 1826 >>> p.recurse().notes.first().pitch.midi 1827 36 1828 >>> p.recurse().notes.first().volume.velocity 1829 90 1830 1831 Changed in v.7 -- Now makes measures 1832 1833 >>> p.show('text') 1834 {0.0} <music21.stream.Measure 1 offset=0.0> 1835 {0.0} <music21.instrument.Instrument ''> 1836 {0.0} <music21.clef.TrebleClef> 1837 {0.0} <music21.meter.TimeSignature 4/4> 1838 {0.0} <music21.note.Note C> 1839 {1.0} <music21.note.Rest quarter> 1840 {2.0} <music21.chord.Chord F3 G#4 C5> 1841 {3.0} <music21.note.Rest quarter> 1842 {4.0} <music21.stream.Measure 2 offset=4.0> 1843 {0.0} <music21.note.Rest eighth> 1844 {0.5} <music21.note.Note B-> 1845 {1.5} <music21.note.Rest half> 1846 {3.5} <music21.chord.Chord D2 A4> 1847 {8.0} <music21.stream.Measure 3 offset=8.0> 1848 {0.0} <music21.note.Rest eighth> 1849 {0.5} <music21.chord.Chord C#2 B-3 G#6> 1850 {1.0} <music21.note.Rest dotted-quarter> 1851 {2.5} <music21.chord.Chord F#3 A4 C#5> 1852 {12.0} <music21.stream.Measure 4 offset=12.0> 1853 {0.0} <music21.chord.Chord F#3 A4 C#5> 1854 {2.5} <music21.note.Rest dotted-quarter> 1855 {4.0} <music21.bar.Barline type=final> 1856 ''' 1857 # environLocal.printDebug(['midiTrackToStream(): got midi track: events', 1858 # len(mt.events), 'ticksPerQuarter', ticksPerQuarter]) 1859 1860 if inputM21 is None: 1861 s = stream.Part() 1862 else: 1863 s = inputM21 1864 1865 if ticksPerQuarter is None: 1866 ticksPerQuarter = defaults.ticksPerQuarter 1867 1868 # get events without DeltaTimes 1869 events = getTimeForEvents(mt) 1870 1871 # need to build chords and notes 1872 notes = getNotesFromEvents(events) 1873 metaEvents = getMetaEvents(events) 1874 1875 # first create meta events 1876 for t, obj in metaEvents: 1877 # environLocal.printDebug(['insert midi meta event:', t, obj]) 1878 s.coreInsert(t / ticksPerQuarter, obj) 1879 s.coreElementsChanged() 1880 deduplicate(s, inPlace=True) 1881 # environLocal.printDebug([ 1882 # 'midiTrackToStream(): found notes ready for Stream import', len(notes)]) 1883 1884 # collect notes with similar start times into chords 1885 # create a composite list of both notes and chords 1886 # composite = [] 1887 chordSub = None 1888 i = 0 1889 iGathered = [] # store a list of indexes of gathered values put into chords 1890 voicesRequired = False 1891 1892 if 'quarterLengthDivisors' in keywords: 1893 quarterLengthDivisors = keywords['quarterLengthDivisors'] 1894 else: 1895 quarterLengthDivisors = defaults.quantizationQuarterLengthDivisors 1896 1897 if len(notes) > 1: 1898 # environLocal.printDebug(['\n', 'midiTrackToStream(): notes', notes]) 1899 while i < len(notes): 1900 if i in iGathered: 1901 i += 1 1902 continue 1903 # look at each note; get on time and event 1904 on, off = notes[i] 1905 t, unused_e = on 1906 tOff, unused_eOff = off 1907 # environLocal.printDebug(['on, off', on, off, 'i', i, 'len(notes)', len(notes)]) 1908 1909 # go through all following notes; if there is only 1 note, this will 1910 # not execute; 1911 # looking for other events that start within a certain small time 1912 # window to make into a chord 1913 # if we find a note with a different end time but same start 1914 # time, throw into a different voice 1915 for j in range(i + 1, len(notes)): 1916 # look at each on time event 1917 onSub, offSub = notes[j] 1918 tSub, unused_eSub = onSub 1919 tOffSub, unused_eOffSub = offSub 1920 1921 # let tolerance for chord subbing follow the quantization 1922 if quantizePost: 1923 divisor = max(quarterLengthDivisors) 1924 # fallback: 1/16 of a quarter (64th) 1925 else: 1926 divisor = 16 1927 chunkTolerance = ticksPerQuarter / divisor 1928 # must be strictly less than the quantization unit 1929 if abs(tSub - t) < chunkTolerance: 1930 # isolate case where end time is not w/n tolerance 1931 if abs(tOffSub - tOff) > chunkTolerance: 1932 # need to store this as requiring movement to a diff 1933 # voice 1934 voicesRequired = True 1935 continue 1936 if chordSub is None: # start a new one 1937 chordSub = [notes[i]] 1938 iGathered.append(i) 1939 chordSub.append(notes[j]) 1940 iGathered.append(j) 1941 continue # keep looping through events to see 1942 # if we can add more elements to this chord group 1943 else: # no more matches; assuming chordSub tones are contiguous 1944 break 1945 # this comparison must be outside of j loop, as the case where we 1946 # have the last note in a list of notes and the j loop does not 1947 # execute; chordSub will be None 1948 if chordSub is not None: 1949 # composite.append(chordSub) 1950 c = midiEventsToChord(chordSub, ticksPerQuarter) 1951 o = notes[i][0][0] / ticksPerQuarter 1952 c.midiTickStart = notes[i][0][0] 1953 1954 s.coreInsert(o, c) 1955 # iSkip = len(chordSub) # amount of accumulated chords 1956 chordSub = None 1957 else: # just append the note, chordSub is None 1958 # composite.append(notes[i]) 1959 n = midiEventsToNote(notes[i], ticksPerQuarter) 1960 # the time is the first value in the first pair 1961 # need to round, as floating point error is likely 1962 o = notes[i][0][0] / ticksPerQuarter 1963 n.midiTickStart = notes[i][0][0] 1964 1965 s.coreInsert(o, n) 1966 # iSkip = 1 1967 # break # exit secondary loop 1968 i += 1 1969 1970 elif len(notes) == 1: # rare case of just one note 1971 n = midiEventsToNote(notes[0], ticksPerQuarter) 1972 # the time is the first value in the first pair 1973 # need to round, as floating point error is likely 1974 o = notes[0][0][0] / ticksPerQuarter 1975 n.midiTickStart = notes[0][0][0] 1976 s.coreInsert(o, n) 1977 1978 s.coreElementsChanged() 1979 # quantize to nearest 16th 1980 if quantizePost: 1981 s.quantize(quarterLengthDivisors=quarterLengthDivisors, 1982 processOffsets=True, 1983 processDurations=True, 1984 inPlace=True, 1985 recurse=False) # shouldn't be any substreams yet 1986 1987 if not notes: 1988 # Conductor track doesn't need measures made 1989 # It's an intermediate result only -- not provided to user 1990 return s 1991 1992 if conductorPart is not None: 1993 insertConductorEvents(conductorPart, s, isFirst=isFirst) 1994 1995 # Only make measures once time signatures have been inserted 1996 s.makeMeasures( 1997 meterStream=conductorPart['TimeSignature'].stream() if conductorPart else None, 1998 inPlace=True) 1999 if voicesRequired: 2000 for m in s.getElementsByClass(stream.Measure): 2001 # Gaps will be filled by makeRests, below, which now recurses 2002 m.makeVoices(inPlace=True, fillGaps=False) 2003 s.makeTies(inPlace=True) 2004 # always need to fill gaps, as rests are not found in any other way 2005 s.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True) 2006 return s 2007 2008 2009def prepareStreamForMidi(s) -> stream.Stream: 2010 # noinspection PyShadowingNames 2011 ''' 2012 Given a score, prepare it for MIDI processing, and return a new Stream: 2013 2014 1. Expand repeats. 2015 2016 2. Make changes that will let us later create a conductor (tempo) track 2017 by placing `MetronomeMark`, `TimeSignature`, and `KeySignature` 2018 objects into a new Part, and remove them from other parts. 2019 2020 3. Ensure that the resulting Stream always has part-like substreams. 2021 2022 Note: will make a deepcopy() of the stream. 2023 2024 >>> s = stream.Score() 2025 >>> p = stream.Part() 2026 >>> m = stream.Measure(number=1) 2027 >>> m.append(tempo.MetronomeMark(100)) 2028 >>> m.append(note.Note('C4', type='whole')) # MIDI 60 2029 >>> p.append(m) 2030 >>> s.append(p) 2031 >>> sOut = midi.translate.prepareStreamForMidi(s) 2032 >>> sOut.show('text') 2033 {0.0} <music21.stream.Part 0x10b0439a0> 2034 {0.0} <music21.tempo.MetronomeMark Quarter=100> 2035 {0.0} <music21.meter.TimeSignature 4/4> 2036 {0.0} <music21.stream.Part 0x10b043c10> 2037 {0.0} <music21.stream.Measure 1 offset=0.0> 2038 {0.0} <music21.note.Note C> 2039 ''' 2040 from music21 import volume 2041 2042 if s[stream.Measure]: 2043 s = s.expandRepeats() # makes a deep copy 2044 else: 2045 s = s.coreCopyAsDerivation('prepareStreamForMidi') 2046 2047 conductor = conductorStream(s) 2048 2049 if s.hasPartLikeStreams(): 2050 # process Volumes one part at a time 2051 # this assumes that dynamics in a part/stream apply to all components 2052 # of that part stream 2053 # this sets the cachedRealized value for each Volume 2054 for p in s.getElementsByClass('Stream'): 2055 volume.realizeVolume(p) 2056 2057 s.insert(0, conductor) 2058 out = s 2059 2060 else: # just a single Stream 2061 volume.realizeVolume(s) 2062 out = stream.Score() 2063 out.insert(0, conductor) 2064 out.insert(0, s) 2065 2066 return out 2067 2068 2069def conductorStream(s: stream.Stream) -> stream.Part: 2070 # noinspection PyShadowingNames 2071 ''' 2072 Strip the given stream of any events that belong in a conductor track 2073 rather than in a music track, and returns a :class:`~music21.stream.Part` 2074 containing just those events, without duplicates, suitable for being a 2075 Part to turn into a conductor track. 2076 2077 Sets a default MetronomeMark of 120 if no MetronomeMarks are present 2078 and a TimeSignature of 4/4 if not present. 2079 2080 Ensures that the conductor track always sorts before other parts. 2081 2082 Here we purposely use nested generic streams instead of Scores, Parts, etc. 2083 to show that this still works. But you should use Score, Part, Measure instead. 2084 2085 >>> s = stream.Stream(id='scoreLike') 2086 >>> p = stream.Stream(id='partLike') 2087 >>> p.priority = -2 2088 >>> m = stream.Stream(id='measureLike') 2089 >>> m.append(tempo.MetronomeMark(100)) 2090 >>> m.append(note.Note('C4')) 2091 >>> p.append(m) 2092 >>> s.insert(0, p) 2093 >>> conductor = midi.translate.conductorStream(s) 2094 >>> conductor.priority 2095 -3 2096 2097 The MetronomeMark is moved and a default TimeSignature is added: 2098 2099 >>> conductor.show('text') 2100 {0.0} <music21.instrument.Conductor 'Conductor'> 2101 {0.0} <music21.tempo.MetronomeMark Quarter=100> 2102 {0.0} <music21.meter.TimeSignature 4/4> 2103 2104 The original stream still has the note: 2105 2106 >>> s.show('text') 2107 {0.0} <music21.stream.Stream partLike> 2108 {0.0} <music21.stream.Stream measureLike> 2109 {0.0} <music21.note.Note C> 2110 ''' 2111 from music21 import tempo 2112 from music21 import meter 2113 partsList = list(s.getElementsByClass('Stream').getElementsByOffset(0)) 2114 minPriority = min(p.priority for p in partsList) if partsList else 0 2115 conductorPriority = minPriority - 1 2116 2117 conductorPart = stream.Part() 2118 conductorPart.priority = conductorPriority 2119 conductorPart.insert(0, Conductor()) 2120 2121 for klass in ('MetronomeMark', 'TimeSignature', 'KeySignature'): 2122 lastOffset = -1 2123 for el in s[klass]: 2124 # Don't overwrite an event of the same class at this offset 2125 if el.offset > lastOffset: 2126 conductorPart.coreInsert(el.offset, el) 2127 lastOffset = el.offset 2128 s.remove(el, recurse=True) 2129 2130 conductorPart.coreElementsChanged() 2131 2132 # Defaults 2133 if not conductorPart.getElementsByClass('MetronomeMark'): 2134 conductorPart.insert(tempo.MetronomeMark(number=120)) 2135 if not conductorPart.getElementsByClass('TimeSignature'): 2136 conductorPart.insert(meter.TimeSignature('4/4')) 2137 2138 return conductorPart 2139 2140 2141def channelInstrumentData( 2142 s: stream.Stream, 2143 acceptableChannelList: Optional[List[int]] = None, 2144) -> Tuple[Dict[Union[int, None], int], List[int]]: 2145 ''' 2146 Read through Stream `s` and finding instruments in it, return a 2-tuple, 2147 the first a dictionary mapping MIDI program numbers to channel numbers, 2148 and the second, a list of unassigned channels that can be used for dynamic 2149 allocation. One channel is always left unassigned for dynamic allocation. 2150 If the number of needed channels exceeds the number of available ones, 2151 any further MIDI program numbers are assigned to channel 1. 2152 2153 Substreams without notes or rests (e.g. representing a conductor track) 2154 will not consume a channel. 2155 2156 Only necessarily works if :func:`~music21.midi.translate.prepareStreamForMidi` 2157 has been run before calling this routine. 2158 2159 An instrument's `.midiChannel` attribute is observed. 2160 `None` is the default `.midiChannel` for all instruments except 2161 :class:`~music21.instrument.UnpitchedPercussion` 2162 subclasses. Put another way, the priority is: 2163 2164 - Instrument instance `.midiChannel` (set by user or imported from MIDI) 2165 - `UnpitchedPercussion` subclasses receive MIDI Channel 10 (9 in music21) 2166 - The channel mappings produced by reading from `acceptableChannelList`, 2167 or the default range 1-16. (More precisely, 1-15, since one dynamic channel 2168 is always reserved.) 2169 2170 .. warning:: 2171 2172 The attribute `.midiChannel` on :class:`~music21.instrument.Instrument` 2173 is 0-indexed, but `.channel` on :class:`~music21.midi.MidiEvent` is 1-indexed, 2174 as are all references to channels in this function. 2175 ''' 2176 # temporary channel allocation 2177 if acceptableChannelList is not None: 2178 # copy user input, because we will manipulate it 2179 acceptableChannels = acceptableChannelList[:] 2180 else: 2181 acceptableChannels = list(range(1, 10)) + list(range(11, 17)) # all but 10 2182 2183 # store program numbers 2184 # tried using set() but does not guarantee proper order. 2185 allUniqueInstruments = [] 2186 2187 channelByInstrument = {} # the midiProgram is the key 2188 channelsDynamic = [] # remaining channels 2189 # create an entry for all unique instruments, assign channels 2190 # for each instrument, assign a channel; if we go above 16, that is fine 2191 # we just cannot use it and will take modulus later 2192 channelsAssigned = set() 2193 2194 # store streams in uniform list 2195 substreamList = [] 2196 if s.hasPartLikeStreams(): 2197 for obj in s.getElementsByClass('Stream'): 2198 # Conductor track: don't consume a channel 2199 if (not obj[note.GeneralNote]) and obj[Conductor]: 2200 continue 2201 else: 2202 substreamList.append(obj) 2203 else: 2204 # should not ever run if prepareStreamForMidi() was run... 2205 substreamList.append(s) # pragma: no cover 2206 2207 # Music tracks 2208 for subs in substreamList: 2209 # get a first instrument; iterate over rest 2210 instrumentStream = subs.recurse().getElementsByClass('Instrument') 2211 setAnInstrument = False 2212 for inst in instrumentStream: 2213 if inst.midiChannel is not None and inst.midiProgram not in channelByInstrument: 2214 # Assignment Case 1: read from instrument.midiChannel 2215 # .midiChannel is 0-indexed, but MIDI channels are 1-indexed, so convert. 2216 thisChannel = inst.midiChannel + 1 2217 try: 2218 acceptableChannels.remove(thisChannel) 2219 except ValueError: 2220 # Don't warn if 10 is missing, since 2221 # we deliberately made it unavailable above. 2222 if thisChannel != 10: 2223 # If the user wants multiple non-drum programs mapped 2224 # to the same MIDI channel for some reason, solution is to provide an 2225 # acceptableChannelList containing duplicate entries. 2226 warnings.warn( 2227 f'{inst} specified 1-indexed MIDI channel {thisChannel} ' 2228 f'but acceptable channels were {acceptableChannels}. ' 2229 'Defaulting to channel 1.', 2230 TranslateWarning) 2231 thisChannel = 1 2232 channelsAssigned.add(thisChannel) 2233 channelByInstrument[inst.midiProgram] = thisChannel 2234 if inst.midiProgram not in allUniqueInstruments: 2235 allUniqueInstruments.append(inst.midiProgram) 2236 setAnInstrument = True 2237 2238 if not setAnInstrument: 2239 if None not in allUniqueInstruments: 2240 allUniqueInstruments.append(None) 2241 2242 programsStillNeeded = [x for x in allUniqueInstruments if x not in channelByInstrument] 2243 2244 for i, iPgm in enumerate(programsStillNeeded): 2245 # the key is the program number; the value is the start channel 2246 if i < len(acceptableChannels) - 1: # save at least one dynamic channel 2247 # Assignment Case 2: dynamically assign available channels 2248 # if Instrument.midiChannel was None 2249 channelByInstrument[iPgm] = acceptableChannels[i] 2250 channelsAssigned.add(acceptableChannels[i]) 2251 else: # just use 1, and deal with the mess: cannot allocate 2252 channelByInstrument[iPgm] = acceptableChannels[0] 2253 channelsAssigned.add(acceptableChannels[0]) 2254 2255 # get the dynamic channels, or those not assigned 2256 for ch in acceptableChannels: 2257 if ch not in channelsAssigned: 2258 channelsDynamic.append(ch) 2259 2260 return channelByInstrument, channelsDynamic 2261 2262 2263def packetStorageFromSubstreamList( 2264 substreamList: List[stream.Part], 2265 *, 2266 addStartDelay=False, 2267) -> Dict[int, Dict[str, Any]]: 2268 # noinspection PyShadowingNames 2269 r''' 2270 Make a dictionary of raw packets and the initial instrument for each 2271 subStream. 2272 2273 If the first Part in the list of parts is empty then a new 2274 :class:`~music21.instrument.Conductor` object will be given as the instrument. 2275 2276 >>> s = stream.Score() 2277 >>> p = stream.Part() 2278 >>> m = stream.Measure(number=1) 2279 >>> m.append(tempo.MetronomeMark(100)) 2280 >>> m.append(instrument.Oboe()) 2281 >>> m.append(note.Note('C4', type='whole')) # MIDI 60 2282 >>> p.append(m) 2283 >>> s.append(p) 2284 >>> sOut = midi.translate.prepareStreamForMidi(s) 2285 >>> partList = list(sOut.parts) 2286 >>> packetStorage = midi.translate.packetStorageFromSubstreamList(partList) 2287 >>> list(sorted(packetStorage.keys())) 2288 [0, 1] 2289 >>> list(sorted(packetStorage[0].keys())) 2290 ['initInstrument', 'rawPackets'] 2291 2292 >>> from pprint import pprint 2293 >>> pprint(packetStorage) 2294 {0: {'initInstrument': <music21.instrument.Conductor 'Conductor'>, 2295 'rawPackets': [{'centShift': None, 2296 'duration': 0, 2297 'lastInstrument': <music21.instrument.Conductor 'Conductor'>, 2298 'midiEvent': <music21.midi.MidiEvent SET_TEMPO, ... channel=1, ...>, 2299 'obj': <music21.tempo.MetronomeMark Quarter=100>, 2300 'offset': 0, 2301 'trackId': 0}, 2302 {'centShift': None, 2303 'duration': 0, 2304 'lastInstrument': <music21.instrument.Conductor 'Conductor'>, 2305 'midiEvent': <music21.midi.MidiEvent TIME_SIGNATURE, ...>, 2306 'obj': <music21.meter.TimeSignature 4/4>, 2307 'offset': 0, 2308 'trackId': 0}]}, 2309 1: {'initInstrument': <music21.instrument.Oboe 'Oboe'>, 2310 'rawPackets': [{'centShift': None, 2311 'duration': 0, 2312 'lastInstrument': <music21.instrument.Oboe 'Oboe'>, 2313 'midiEvent': <music21.midi.MidiEvent PROGRAM_CHANGE, 2314 track=None, channel=1, data=68>, 2315 'obj': <music21.instrument.Oboe 'Oboe'>, 2316 'offset': 0, 2317 'trackId': 1}, 2318 {'centShift': None, 2319 'duration': 4096, 2320 'lastInstrument': <music21.instrument.Oboe 'Oboe'>, 2321 'midiEvent': <music21.midi.MidiEvent NOTE_ON, 2322 track=None, channel=1, pitch=60, velocity=90>, 2323 'obj': <music21.note.Note C>, 2324 'offset': 0, 2325 'trackId': 1}, 2326 {'centShift': None, 2327 'duration': 0, 2328 'lastInstrument': <music21.instrument.Oboe 'Oboe'>, 2329 'midiEvent': <music21.midi.MidiEvent NOTE_OFF, 2330 track=None, channel=1, pitch=60, velocity=0>, 2331 'obj': <music21.note.Note C>, 2332 'offset': 4096, 2333 'trackId': 1}]}} 2334 ''' 2335 packetStorage = {} 2336 2337 for trackId, subs in enumerate(substreamList): # Conductor track is track 0 2338 subs = subs.flatten() 2339 2340 # get a first instrument; iterate over rest 2341 instrumentStream = subs.getElementsByClass('Instrument') 2342 2343 # if there is an Instrument object at the start, make instObj that instrument 2344 # this may be a Conductor object if prepareStreamForMidi() was run 2345 if instrumentStream and subs.elementOffset(instrumentStream[0]) == 0: 2346 instObj = instrumentStream[0] 2347 elif trackId == 0 and not subs.notesAndRests: 2348 # maybe prepareStreamForMidi() wasn't run; create Conductor instance 2349 instObj = Conductor() 2350 else: 2351 instObj = None 2352 2353 trackPackets = streamToPackets(subs, trackId=trackId, addStartDelay=addStartDelay) 2354 # store packets in dictionary; keys are trackIds 2355 packetStorage[trackId] = { 2356 'rawPackets': trackPackets, 2357 'initInstrument': instObj, 2358 } 2359 return packetStorage 2360 2361 2362def updatePacketStorageWithChannelInfo( 2363 packetStorage: Dict[int, Dict[str, Any]], 2364 channelByInstrument: Dict[Union[int, None], int], 2365) -> None: 2366 ''' 2367 Take the packetStorage dictionary and using information 2368 from 'initInstrument' and channelByInstrument, add an 'initChannel' key to each 2369 packetStorage bundle and to each rawPacket in the bundle['rawPackets'] 2370 ''' 2371 # update packets with first channel 2372 for unused_trackId, bundle in packetStorage.items(): 2373 # get instrument 2374 instObj = bundle['initInstrument'] 2375 if instObj is None: 2376 try: 2377 initCh = channelByInstrument[None] 2378 except KeyError: # pragma: no cover 2379 initCh = 1 # fallback, should not happen. 2380 elif 'Conductor' in instObj.classes: 2381 initCh = None 2382 else: # keys are midi program 2383 initCh = channelByInstrument[instObj.midiProgram] 2384 bundle['initChannel'] = initCh # set for bundle too 2385 2386 for rawPacket in bundle['rawPackets']: 2387 rawPacket['initChannel'] = initCh 2388 2389 2390def streamHierarchyToMidiTracks( 2391 inputM21, 2392 *, 2393 acceptableChannelList=None, 2394 addStartDelay=False, 2395): 2396 ''' 2397 Given a Stream, Score, Part, etc., that may have substreams (i.e., 2398 a hierarchy), return a list of :class:`~music21.midi.MidiTrack` objects. 2399 2400 acceptableChannelList is a list of MIDI Channel numbers that can be used or None. 2401 If None, then 1-9, 11-16 are used (10 being reserved for percussion). 2402 2403 In addition, if an :class:`~music21.instrument.Instrument` object in the stream 2404 has a `.midiChannel` that is not None, that channel is observed, and 2405 also treated as reserved. Only subclasses of :class:`~music21.instrument.UnpitchedPercussion` 2406 have a default `.midiChannel`, but users may manipulate this. 2407 See :func:`channelInstrumentData` for more, and for documentation on `acceptableChannelList`. 2408 2409 Called by streamToMidiFile() 2410 2411 The process: 2412 2413 1. makes a deepcopy of the Stream (Developer TODO: could this 2414 be done with a shallow copy? Not if ties are stripped and volume realized.) 2415 2416 2. we make a list of all instruments that are being used in the piece. 2417 2418 Changed in v.6 -- acceptableChannelList is keyword only. addStartDelay is new. 2419 Changed in v.6.5 -- Track 0 (tempo/conductor track) always exported. 2420 ''' 2421 # makes a deepcopy 2422 s = prepareStreamForMidi(inputM21) 2423 channelByInstrument, channelsDynamic = channelInstrumentData(s, acceptableChannelList) 2424 2425 # return a list of MidiTrack objects 2426 midiTracks = [] 2427 2428 # TODO: may need to shift all time values to accommodate 2429 # Streams that do not start at same time 2430 2431 # store streams in uniform list: prepareStreamForMidi() ensures there are substreams 2432 substreamList = [] 2433 for obj in s.getElementsByClass('Stream'): 2434 # prepareStreamForMidi() supplies defaults for these 2435 if obj.getElementsByClass(('MetronomeMark', 'TimeSignature')): 2436 # Ensure conductor track is first 2437 substreamList.insert(0, obj) 2438 else: 2439 substreamList.append(obj) 2440 2441 # strip all ties inPlace 2442 for subs in substreamList: 2443 subs.stripTies(inPlace=True, matchByPitch=False) 2444 2445 packetStorage = packetStorageFromSubstreamList(substreamList, addStartDelay=addStartDelay) 2446 updatePacketStorageWithChannelInfo(packetStorage, channelByInstrument) 2447 2448 initTrackIdToChannelMap = {} 2449 for trackId, bundle in packetStorage.items(): 2450 initTrackIdToChannelMap[trackId] = bundle['initChannel'] # map trackId to channelId 2451 2452 # combine all packets for processing of channel allocation 2453 netPackets = [] 2454 for bundle in packetStorage.values(): 2455 netPackets += bundle['rawPackets'] 2456 2457 # process all channel assignments for all packets together 2458 netPackets = assignPacketsToChannels( 2459 netPackets, 2460 channelByInstrument=channelByInstrument, 2461 channelsDynamic=channelsDynamic, 2462 initTrackIdToChannelMap=initTrackIdToChannelMap) 2463 2464 # environLocal.printDebug(['got netPackets:', len(netPackets), 2465 # 'packetStorage keys (tracks)', packetStorage.keys()]) 2466 # build each track, sorting out the appropriate packets based on track 2467 # ids 2468 for trackId in packetStorage: 2469 initChannel = packetStorage[trackId]['initChannel'] 2470 instrumentObj = packetStorage[trackId]['initInstrument'] 2471 mt = packetsToMidiTrack(netPackets, 2472 trackId=trackId, 2473 channel=initChannel, 2474 instrumentObj=instrumentObj) 2475 midiTracks.append(mt) 2476 2477 return midiTracks 2478 2479 2480def midiTracksToStreams( 2481 midiTracks: List['music21.midi.MidiTrack'], 2482 ticksPerQuarter=None, 2483 quantizePost=True, 2484 inputM21: stream.Score = None, 2485 **keywords 2486) -> stream.Stream(): 2487 ''' 2488 Given a list of midiTracks, populate either a new stream.Score or inputM21 2489 with a Part for each track. 2490 ''' 2491 # environLocal.printDebug(['midi track count', len(midiTracks)]) 2492 if inputM21 is None: 2493 s = stream.Score() 2494 else: 2495 s = inputM21 2496 2497 # conductorPart will store common elements such as time sig, key sig 2498 # from the conductor track (or any track without notes). 2499 conductorPart = stream.Part() 2500 firstTrackWithNotes = None 2501 for mt in midiTracks: 2502 # not all tracks have notes defined; only creates parts for those 2503 # that do 2504 # environLocal.printDebug(['raw midi tracks', mt]) 2505 if mt.hasNotes(): 2506 if firstTrackWithNotes is None: 2507 firstTrackWithNotes = mt 2508 streamPart = stream.Part() # create a part instance for each part 2509 s.insert(0, streamPart) 2510 else: 2511 streamPart = conductorPart 2512 2513 midiTrackToStream(mt, 2514 ticksPerQuarter, 2515 quantizePost, 2516 inputM21=streamPart, 2517 conductorPart=conductorPart, 2518 isFirst=(mt is firstTrackWithNotes), 2519 **keywords) 2520 2521 return s 2522 2523 2524def streamToMidiFile( 2525 inputM21: stream.Stream, 2526 *, 2527 addStartDelay: bool = False, 2528 acceptableChannelList: Optional[List[int]] = None, 2529) -> 'music21.midi.MidiFile': 2530 # noinspection PyShadowingNames 2531 ''' 2532 Converts a Stream hierarchy into a :class:`~music21.midi.MidiFile` object. 2533 2534 >>> s = stream.Stream() 2535 >>> n = note.Note('g#') 2536 >>> n.quarterLength = 0.5 2537 >>> s.repeatAppend(n, 4) 2538 >>> mf = midi.translate.streamToMidiFile(s) 2539 >>> mf.tracks[0].index # Track 0: conductor track 2540 0 2541 >>> len(mf.tracks[1].events) # Track 1: music track 2542 22 2543 2544 From here, you can call mf.writestr() to get the actual file info. 2545 2546 >>> sc = scale.PhrygianScale('g') 2547 >>> s = stream.Stream() 2548 >>> x=[s.append(note.Note(sc.pitchFromDegree(i % 11), quarterLength=0.25)) for i in range(60)] 2549 >>> mf = midi.translate.streamToMidiFile(s) 2550 >>> #_DOCS_SHOW mf.open('/Volumes/disc/_scratch/midi.mid', 'wb') 2551 >>> #_DOCS_SHOW mf.write() 2552 >>> #_DOCS_SHOW mf.close() 2553 2554 See :func:`channelInstrumentData` for documentation on `acceptableChannelList`. 2555 ''' 2556 from music21 import midi as midiModule 2557 2558 s = inputM21 2559 midiTracks = streamHierarchyToMidiTracks(s, 2560 addStartDelay=addStartDelay, 2561 acceptableChannelList=acceptableChannelList, 2562 ) 2563 2564 # may need to update channel information 2565 2566 mf = midiModule.MidiFile() 2567 mf.tracks = midiTracks 2568 mf.ticksPerQuarterNote = defaults.ticksPerQuarter 2569 return mf 2570 2571 2572def midiFilePathToStream( 2573 filePath, 2574 inputM21=None, 2575 **keywords 2576): 2577 ''' 2578 Used by music21.converter: 2579 2580 Take in a file path (name of a file on disk) and using `midiFileToStream`, 2581 2582 return a :class:`~music21.stream.Score` object (or if inputM21 is passed in, 2583 use that object instead). 2584 2585 Keywords to control quantization: 2586 `quantizePost` controls whether to quantize the output. (Default: True) 2587 `quarterLengthDivisors` allows for overriding the default quantization units 2588 in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)). 2589 2590 >>> sfp = common.getSourceFilePath() #_DOCS_HIDE 2591 >>> fp = str(sfp / 'midi' / 'testPrimitive' / 'test05.mid') #_DOCS_HIDE 2592 >>> #_DOCS_SHOW fp = '/Users/test/music21/midi/testPrimitive/test05.mid' 2593 >>> streamScore = midi.translate.midiFilePathToStream(fp) 2594 >>> streamScore 2595 <music21.stream.Score ...> 2596 ''' 2597 from music21 import midi as midiModule 2598 mf = midiModule.MidiFile() 2599 mf.open(filePath) 2600 mf.read() 2601 mf.close() 2602 return midiFileToStream(mf, inputM21, **keywords) 2603 2604 2605def midiAsciiStringToBinaryString( 2606 midiFormat=1, 2607 ticksPerQuarterNote=960, 2608 tracksEventsList=None 2609) -> bytes: 2610 r''' 2611 Convert Ascii midi data to a bytes object (formerly binary midi string). 2612 2613 tracksEventsList contains a list of tracks which contain also a list of events. 2614 2615 asciiMidiEventList = ['0 90 27 66', '0 90 3e 60', '3840 80 27 00', '0 80 3e 00'] 2616 2617 The format of one event is : 'aa bb cc dd':: 2618 2619 aa = delta time to last event (integer) 2620 bb = Midi event type 2621 cc = Note number (hex) 2622 dd = Velocity (integer) 2623 2624 Example: 2625 2626 >>> asciiMidiEventList = [] 2627 >>> asciiMidiEventList.append('0 90 31 15') 2628 >>> midiTrack = [] 2629 >>> midiTrack.append(asciiMidiEventList) 2630 >>> midiBinaryBytes = midi.translate.midiAsciiStringToBinaryString(tracksEventsList=midiTrack) 2631 >>> midiBinaryBytes 2632 b'MThd\x00\x00\x00\x06\x00\x01\x00\x01\x03\xc0MTrk\x00\x00\x00\x04\x00\x901\x0f' 2633 2634 Note that the name is from pre-Python 3. There is now in fact nothing called a "binary string" 2635 it is in fact a bytes object. 2636 ''' 2637 from music21 import midi as midiModule 2638 mf = midiModule.MidiFile() 2639 2640 numTracks = len(tracksEventsList) 2641 2642 if numTracks == 1: 2643 mf.format = 1 2644 else: 2645 mf.format = midiFormat 2646 2647 mf.ticksPerQuarterNote = ticksPerQuarterNote 2648 2649 if tracksEventsList is not None: 2650 for i in range(numTracks): 2651 trk = midiModule.MidiTrack(i) # sets the MidiTrack index parameters 2652 for j in tracksEventsList[i]: 2653 me = midiModule.MidiEvent(trk) 2654 dt = midiModule.DeltaTime(trk) 2655 2656 chunk_event_param = str(j).split(' ') 2657 2658 dt.channel = i + 1 2659 dt.time = int(chunk_event_param[0]) 2660 2661 me.channel = i + 1 2662 me.pitch = int(chunk_event_param[2], 16) 2663 me.velocity = int(chunk_event_param[3]) 2664 2665 valid = False 2666 if chunk_event_param[1] != 'FF': 2667 if list(chunk_event_param[1])[0] == '8': 2668 me.type = midiModule.ChannelVoiceMessages.NOTE_OFF 2669 valid = True 2670 elif list(chunk_event_param[1])[0] == '9': 2671 valid = True 2672 me.type = midiModule.ChannelVoiceMessages.NOTE_ON 2673 else: 2674 environLocal.warn(f'Unsupported midi event: 0x{chunk_event_param[1]}') 2675 else: 2676 environLocal.warn(f'Unsupported meta event: 0x{chunk_event_param[1]}') 2677 2678 if valid: 2679 trk.events.append(dt) 2680 trk.events.append(me) 2681 2682 mf.tracks.append(trk) 2683 2684 midiBinStr = b'' 2685 midiBinStr = midiBinStr + mf.writestr() 2686 2687 return midiBinStr 2688 2689 2690def midiStringToStream(strData, **keywords): 2691 r''' 2692 Convert a string of binary midi data to a Music21 stream.Score object. 2693 2694 Keywords to control quantization: 2695 `quantizePost` controls whether to quantize the output. (Default: True) 2696 `quarterLengthDivisors` allows for overriding the default quantization units 2697 in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)). 2698 2699 N.B. -- this has been somewhat problematic, so use at your own risk. 2700 2701 >>> midiBinStr = (b'MThd\x00\x00\x00\x06\x00\x01\x00\x01\x04\x00' 2702 ... + b'MTrk\x00\x00\x00\x16\x00\xff\x03\x00\x00\xe0\x00@\x00' 2703 ... + b'\x90CZ\x88\x00\x80C\x00\x88\x00\xff/\x00') 2704 >>> s = midi.translate.midiStringToStream(midiBinStr) 2705 >>> s.show('text') 2706 {0.0} <music21.stream.Part 0x108aa94f0> 2707 {0.0} <music21.stream.Measure 1 offset=0.0> 2708 {0.0} <music21.instrument.Instrument ''> 2709 {0.0} <music21.clef.TrebleClef> 2710 {0.0} <music21.meter.TimeSignature 4/4> 2711 {0.0} <music21.note.Note G> 2712 {1.0} <music21.note.Rest dotted-half> 2713 {4.0} <music21.bar.Barline type=final> 2714 ''' 2715 from music21 import midi as midiModule 2716 2717 mf = midiModule.MidiFile() 2718 # do not need to call open or close on MidiFile instance 2719 mf.readstr(strData) 2720 return midiFileToStream(mf, **keywords) 2721 2722 2723def midiFileToStream( 2724 mf: 'music21.midi.MidiFile', 2725 inputM21=None, 2726 quantizePost=True, 2727 **keywords 2728): 2729 # noinspection PyShadowingNames 2730 ''' 2731 Note: this is NOT the normal way to read a MIDI file. The best way is generally: 2732 2733 score = converter.parse('path/to/file.mid') 2734 2735 Convert a :class:`~music21.midi.MidiFile` object to a 2736 :class:`~music21.stream.Stream` object. 2737 2738 The `inputM21` object can specify an existing Stream (or Stream subclass) to fill. 2739 2740 Keywords to control quantization: 2741 `quantizePost` controls whether to quantize the output. (Default: True) 2742 `quarterLengthDivisors` allows for overriding the default quantization units 2743 in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)). 2744 2745 >>> import os 2746 >>> fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test05.mid' 2747 >>> mf = midi.MidiFile() 2748 >>> mf.open(fp) 2749 >>> mf.read() 2750 >>> mf.close() 2751 >>> len(mf.tracks) 2752 1 2753 >>> s = midi.translate.midiFileToStream(mf) 2754 >>> s 2755 <music21.stream.Score ...> 2756 >>> len(s.flatten().notesAndRests) 2757 14 2758 ''' 2759 # environLocal.printDebug(['got midi file: tracks:', len(mf.tracks)]) 2760 if inputM21 is None: 2761 s = stream.Score() 2762 else: 2763 s = inputM21 2764 2765 if not mf.tracks: 2766 raise exceptions21.StreamException('no tracks are defined in this MIDI file.') 2767 2768 if 'quantizePost' in keywords: 2769 quantizePost = keywords.pop('quantizePost') 2770 2771 # create a stream for each tracks 2772 # may need to check if tracks actually have event data 2773 midiTracksToStreams(mf.tracks, 2774 ticksPerQuarter=mf.ticksPerQuarterNote, 2775 quantizePost=quantizePost, 2776 inputM21=s, 2777 **keywords) 2778 # s._setMidiTracks(mf.tracks, mf.ticksPerQuarterNote) 2779 2780 return s 2781 2782 2783# ------------------------------------------------------------------------------ 2784class Test(unittest.TestCase): 2785 2786 def testMidiAsciiStringToBinaryString(self): 2787 from binascii import a2b_hex 2788 2789 asciiMidiEventList = [] 2790 asciiMidiEventList.append('0 90 1f 15') 2791 # asciiMidiEventList.append('3840 80 1f 15') 2792 # asciiMidiEventList.append('0 b0 7b 00') 2793 2794 # asciiMidiEventList = ['0 90 27 66', '3840 80 27 00'] 2795 # asciiMidiEventList = ['0 90 27 66', '0 90 3e 60', '3840 80 27 00', '0 80 3e 00', 2796 # '0 90 3b 60', '960 80 3b 00', '0 90 41 60', '960 80 41 00', '0 90 3e 60', 2797 # '1920 80 3e 00', '0 b0 7b 00', '0 90 24 60', '3840 80 24 00', '0 b0 7b 00'] 2798 # asciiMidiEventList = ['0 90 27 66', '0 90 3e 60', '3840 80 27 00', '0 80 3e 00', 2799 # '0 90 3b 60', '960 80 3b 00', '0 90 41 60', '960 80 41 00', 2800 # '0 90 3e 60', '1920 80 3e 00', '0 90 24 60', '3840 80 24 00'] 2801 2802 midiTrack = [] 2803 midiTrack.append(asciiMidiEventList) 2804 # midiTrack.append(asciiMidiEventList) 2805 # midiTrack.append(asciiMidiEventList) 2806 2807 midiBinStr = midiAsciiStringToBinaryString(tracksEventsList=midiTrack) 2808 2809 self.assertEqual(midiBinStr, 2810 b'MThd' + a2b_hex('000000060001000103c0') 2811 + b'MTrk' + a2b_hex('0000000400901f0f')) 2812 2813 def testNote(self): 2814 from music21 import midi as midiModule 2815 2816 n1 = note.Note('A4') 2817 n1.quarterLength = 2.0 2818 eventList = noteToMidiEvents(n1) 2819 self.assertEqual(len(eventList), 4) 2820 2821 self.assertIsInstance(eventList[0], midiModule.DeltaTime) 2822 self.assertIsInstance(eventList[2], midiModule.DeltaTime) 2823 2824 # translate eventList back to a note 2825 n2 = midiEventsToNote(eventList) 2826 self.assertEqual(n2.pitch.nameWithOctave, 'A4') 2827 self.assertEqual(n2.quarterLength, 2.0) 2828 2829 def testStripTies(self): 2830 from music21.midi import ChannelVoiceMessages 2831 from music21 import tie 2832 2833 # Stream without measures 2834 s = stream.Stream() 2835 n = note.Note('C4', quarterLength=1.0) 2836 n.tie = tie.Tie('start') 2837 n2 = note.Note('C4', quarterLength=1.0) 2838 n2.tie = tie.Tie('stop') 2839 n3 = note.Note('C4', quarterLength=1.0) 2840 n4 = note.Note('C4', quarterLength=1.0) 2841 s.append([n, n2, n3, n4]) 2842 2843 trk = streamHierarchyToMidiTracks(s)[1] 2844 mt1noteOnOffEventTypes = [event.type for event in trk.events if event.type in ( 2845 ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF)] 2846 2847 # Expected result: three pairs of NOTE_ON, NOTE_OFF messages 2848 # https://github.com/cuthbertLab/music21/issues/266 2849 self.assertListEqual(mt1noteOnOffEventTypes, 2850 [ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF] * 3) 2851 2852 # Stream with measures 2853 s.makeMeasures(inPlace=True) 2854 trk = streamHierarchyToMidiTracks(s)[1] 2855 mt2noteOnOffEventTypes = [event.type for event in trk.events if event.type in ( 2856 ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF)] 2857 2858 self.assertListEqual(mt2noteOnOffEventTypes, 2859 [ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF] * 3) 2860 2861 def testTimeSignature(self): 2862 from music21 import meter 2863 n = note.Note() 2864 n.quarterLength = 0.5 2865 s = stream.Stream() 2866 for i in range(20): 2867 s.append(copy.deepcopy(n)) 2868 2869 s.insert(0, meter.TimeSignature('3/4')) 2870 s.insert(3, meter.TimeSignature('5/4')) 2871 s.insert(8, meter.TimeSignature('2/4')) 2872 2873 mt = streamHierarchyToMidiTracks(s)[0] 2874 # self.assertEqual(str(mt.events), match) 2875 self.assertEqual(len(mt.events), 10) 2876 2877 # s.show('midi') 2878 2879 # get and compare just the conductor tracks 2880 # mtAlt = streamHierarchyToMidiTracks(s.getElementsByClass('TimeSignature').stream())[0] 2881 conductorEvents = repr(mt.events) 2882 2883 match = '''[<music21.midi.DeltaTime (empty) track=0, channel=None>, 2884 <music21.midi.MidiEvent SET_TEMPO, track=0, channel=None, data=b'\\x07\\xa1 '>, 2885 <music21.midi.DeltaTime (empty) track=0, channel=None>, 2886 <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None, 2887 data=b'\\x03\\x02\\x18\\x08'>, 2888 <music21.midi.DeltaTime t=3072, track=0, channel=None>, 2889 <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None, 2890 data=b'\\x05\\x02\\x18\\x08'>, 2891 <music21.midi.DeltaTime t=5120, track=0, channel=None>, 2892 <music21.midi.MidiEvent TIME_SIGNATURE, track=0, channel=None, 2893 data=b'\\x02\\x02\\x18\\x08'>, 2894 <music21.midi.DeltaTime t=1024, track=0, channel=None>, 2895 <music21.midi.MidiEvent END_OF_TRACK, track=0, channel=None, data=b''>]''' 2896 2897 self.assertTrue(common.whitespaceEqual(conductorEvents, match), conductorEvents) 2898 2899 def testKeySignature(self): 2900 from music21 import meter 2901 from music21 import key 2902 n = note.Note() 2903 n.quarterLength = 0.5 2904 s = stream.Stream() 2905 for i in range(20): 2906 s.append(copy.deepcopy(n)) 2907 2908 s.insert(0, meter.TimeSignature('3/4')) 2909 s.insert(3, meter.TimeSignature('5/4')) 2910 s.insert(8, meter.TimeSignature('2/4')) 2911 2912 s.insert(0, key.KeySignature(4)) 2913 s.insert(3, key.KeySignature(-5)) 2914 s.insert(8, key.KeySignature(6)) 2915 2916 conductor = streamHierarchyToMidiTracks(s)[0] 2917 self.assertEqual(len(conductor.events), 16) 2918 2919 # s.show('midi') 2920 2921 def testChannelAllocation(self): 2922 # test instrument assignments 2923 from music21 import instrument 2924 2925 iList = [instrument.Harpsichord, 2926 instrument.Viola, 2927 instrument.ElectricGuitar, 2928 instrument.Flute, 2929 instrument.Vibraphone, # not 10 2930 instrument.BassDrum, # 10 2931 instrument.HiHatCymbal, # 10 2932 ] 2933 iObjs = [] 2934 2935 s = stream.Score() 2936 for i, instClass in enumerate(iList): 2937 p = stream.Part() 2938 inst = instClass() 2939 iObjs.append(inst) 2940 p.insert(0, inst) # must call instrument to create instance 2941 p.append(note.Note('C#')) 2942 s.insert(0, p) 2943 2944 channelByInstrument, channelsDynamic = channelInstrumentData(s) 2945 2946 # Default allocations 2947 self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs)) 2948 self.assertSetEqual(set(channelByInstrument.values()), {1, 2, 3, 4, 5, 10}) 2949 self.assertListEqual(channelsDynamic, [6, 7, 8, 9, 11, 12, 13, 14, 15, 16]) 2950 2951 # Limit to given acceptable channels 2952 acl = list(range(11, 17)) 2953 channelByInstrument, channelsDynamic = channelInstrumentData(s, acceptableChannelList=acl) 2954 self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs)) 2955 self.assertSetEqual(set(channelByInstrument.values()), {10, 11, 12, 13, 14, 15}) 2956 self.assertListEqual(channelsDynamic, [16]) 2957 2958 # User specification 2959 for i, iObj in enumerate(iObjs): 2960 iObj.midiChannel = 15 - i 2961 2962 channelByInstrument, channelsDynamic = channelInstrumentData(s) 2963 2964 self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs)) 2965 self.assertSetEqual(set(channelByInstrument.values()), {11, 12, 13, 14, 15, 16}) 2966 self.assertListEqual(channelsDynamic, [1, 2, 3, 4, 5, 6, 7, 8, 9]) 2967 2968 # User error 2969 iObjs[0].midiChannel = 100 2970 want = 'Harpsichord specified 1-indexed MIDI channel 101 but ' 2971 want += r'acceptable channels were \[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16\]. ' 2972 want += 'Defaulting to channel 1.' 2973 with self.assertWarnsRegex(TranslateWarning, want): 2974 channelByInstrument, channelsDynamic = channelInstrumentData(s) 2975 self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs)) 2976 self.assertSetEqual(set(channelByInstrument.values()), {1, 11, 12, 13, 14, 15}) 2977 self.assertListEqual(channelsDynamic, [2, 3, 4, 5, 6, 7, 8, 9, 16]) 2978 2979 def testPacketStorage(self): 2980 # test instrument assignments 2981 from music21 import instrument 2982 2983 iList = [None, # conductor track 2984 instrument.Harpsichord, 2985 instrument.Viola, 2986 instrument.ElectricGuitar, 2987 instrument.Flute, 2988 None] 2989 iObjs = [] 2990 2991 substreamList = [] 2992 for i, instClass in enumerate(iList): 2993 p = stream.Part() 2994 if instClass is not None: 2995 inst = instClass() 2996 iObjs.append(inst) 2997 p.insert(0, inst) # must call instrument to create instance 2998 if i != 0: 2999 p.append(note.Note('C#')) 3000 substreamList.append(p) 3001 3002 packetStorage = packetStorageFromSubstreamList(substreamList, addStartDelay=False) 3003 self.assertIsInstance(packetStorage, dict) 3004 self.assertEqual(list(packetStorage.keys()), [0, 1, 2, 3, 4, 5]) 3005 3006 harpsPacket = packetStorage[1] 3007 self.assertIsInstance(harpsPacket, dict) 3008 self.assertSetEqual(set(harpsPacket.keys()), 3009 {'rawPackets', 'initInstrument'}) 3010 self.assertIs(harpsPacket['initInstrument'], iObjs[0]) 3011 self.assertIsInstance(harpsPacket['rawPackets'], list) 3012 self.assertTrue(harpsPacket['rawPackets']) 3013 self.assertIsInstance(harpsPacket['rawPackets'][0], dict) 3014 3015 channelInfo = { 3016 iObjs[0].midiProgram: 1, 3017 iObjs[1].midiProgram: 2, 3018 iObjs[2].midiProgram: 3, 3019 iObjs[3].midiProgram: 4, 3020 None: 5, 3021 } 3022 3023 updatePacketStorageWithChannelInfo(packetStorage, channelInfo) 3024 self.assertSetEqual(set(harpsPacket.keys()), 3025 {'rawPackets', 'initInstrument', 'initChannel'}) 3026 self.assertEqual(harpsPacket['initChannel'], 1) 3027 self.assertEqual(harpsPacket['rawPackets'][-1]['initChannel'], 1) 3028 3029 def testAnacrusisTiming(self): 3030 from music21 import corpus 3031 3032 s = corpus.parse('bach/bwv103.6') 3033 3034 # get just the soprano part 3035 soprano = s.parts['soprano'] 3036 mts = streamHierarchyToMidiTracks(soprano)[1] # get one 3037 3038 # first note-on is not delayed, even w anacrusis 3039 match = ''' 3040 [<music21.midi.DeltaTime (empty) track=1, channel=1>, 3041 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Soprano'>, 3042 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3043 <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>, 3044 <music21.midi.DeltaTime (empty) track=1, channel=1>]''' 3045 3046 self.maxDiff = None 3047 found = str(mts.events[:5]) 3048 self.assertTrue(common.whitespaceEqual(found, match), found) 3049 3050 # first note-on is not delayed, even w anacrusis 3051 match = ''' 3052 [<music21.midi.DeltaTime (empty) track=1, channel=1>, 3053 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Alto'>, 3054 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3055 <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>, 3056 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3057 <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=1, data=0>, 3058 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3059 <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=62, velocity=90>]''' 3060 3061 alto = s.parts['alto'] 3062 mta = streamHierarchyToMidiTracks(alto)[1] 3063 3064 found = str(mta.events[:8]) 3065 self.assertTrue(common.whitespaceEqual(found, match), found) 3066 3067 # try streams to midi tracks 3068 # get just the soprano part 3069 soprano = s.parts['soprano'] 3070 mtList = streamHierarchyToMidiTracks(soprano) 3071 self.assertEqual(len(mtList), 2) 3072 3073 # it's the same as before 3074 match = '''[<music21.midi.DeltaTime (empty) track=1, channel=1>, 3075 <music21.midi.MidiEvent SEQUENCE_TRACK_NAME, track=1, channel=1, data=b'Soprano'>, 3076 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3077 <music21.midi.MidiEvent PITCH_BEND, track=1, channel=1, parameter1=0, parameter2=64>, 3078 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3079 <music21.midi.MidiEvent PROGRAM_CHANGE, track=1, channel=1, data=0>, 3080 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3081 <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=66, velocity=90>, 3082 <music21.midi.DeltaTime t=512, track=1, channel=1>, 3083 <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=66, velocity=0>]''' 3084 found = str(mtList[1].events[:10]) 3085 self.assertTrue(common.whitespaceEqual(found, match), found) 3086 3087 def testMidiProgramChangeA(self): 3088 from music21 import instrument 3089 p1 = stream.Part() 3090 p1.append(instrument.Dulcimer()) 3091 p1.repeatAppend(note.Note('g6', quarterLength=1.5), 4) 3092 3093 p2 = stream.Part() 3094 p2.append(instrument.Tuba()) 3095 p2.repeatAppend(note.Note('c1', quarterLength=2), 2) 3096 3097 p3 = stream.Part() 3098 p3.append(instrument.TubularBells()) 3099 p3.repeatAppend(note.Note('e4', quarterLength=1), 4) 3100 3101 s = stream.Score() 3102 s.insert(0, p1) 3103 s.insert(0, p2) 3104 s.insert(0, p3) 3105 3106 unused_mts = streamHierarchyToMidiTracks(s) 3107 # p1.show() 3108 # s.show('midi') 3109 3110 def testMidiProgramChangeB(self): 3111 from music21 import instrument 3112 from music21 import scale 3113 import random 3114 3115 iList = [instrument.Harpsichord, 3116 instrument.Clavichord, instrument.Accordion, 3117 instrument.Celesta, instrument.Contrabass, instrument.Viola, 3118 instrument.Harp, instrument.ElectricGuitar, instrument.Ukulele, 3119 instrument.Banjo, instrument.Piccolo, instrument.AltoSaxophone, 3120 instrument.Trumpet] 3121 3122 sc = scale.MinorScale() 3123 pitches = sc.getPitches('c2', 'c5') 3124 random.shuffle(pitches) 3125 3126 s = stream.Stream() 3127 for i in range(30): 3128 n = note.Note(pitches[i % len(pitches)]) 3129 n.quarterLength = 0.5 3130 inst = iList[i % len(iList)]() # call to create instance 3131 s.append(inst) 3132 s.append(n) 3133 3134 unused_mts = streamHierarchyToMidiTracks(s) 3135 3136 # s.show('midi') 3137 3138 def testOverlappedEventsA(self): 3139 from music21 import corpus 3140 s = corpus.parse('bwv66.6') 3141 sFlat = s.flatten() 3142 mtList = streamHierarchyToMidiTracks(sFlat) 3143 self.assertEqual(len(mtList), 2) 3144 3145 # it's the same as before 3146 match = '''[<music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=66, velocity=90>, 3147 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3148 <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=61, velocity=90>, 3149 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3150 <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=58, velocity=90>, 3151 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3152 <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=54, velocity=90>, 3153 <music21.midi.DeltaTime t=1024, track=1, channel=1>, 3154 <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=66, velocity=0>, 3155 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3156 <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=61, velocity=0>, 3157 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3158 <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=58, velocity=0>, 3159 <music21.midi.DeltaTime (empty) track=1, channel=1>, 3160 <music21.midi.MidiEvent NOTE_OFF, track=1, channel=1, pitch=54, velocity=0>, 3161 <music21.midi.DeltaTime t=1024, track=1, channel=1>, 3162 <music21.midi.MidiEvent END_OF_TRACK, track=1, channel=1, data=b''>]''' 3163 3164 results = str(mtList[1].events[-17:]) 3165 self.assertTrue(common.whitespaceEqual(results, match), results) 3166 3167 def testOverlappedEventsB(self): 3168 from music21 import scale 3169 import random 3170 3171 sc = scale.MajorScale() 3172 pitches = sc.getPitches('c2', 'c5') 3173 random.shuffle(pitches) 3174 3175 dur = 16 3176 step = 0.5 3177 o = 0 3178 s = stream.Stream() 3179 for p in pitches: 3180 n = note.Note(p) 3181 n.quarterLength = dur - o 3182 s.insert(o, n) 3183 o = o + step 3184 3185 unused_mt = streamHierarchyToMidiTracks(s)[0] 3186 3187 # s.plot('pianoroll') 3188 # s.show('midi') 3189 3190 def testOverlappedEventsC(self): 3191 from music21 import meter 3192 from music21 import key 3193 3194 s = stream.Stream() 3195 s.insert(key.KeySignature(3)) 3196 s.insert(meter.TimeSignature('2/4')) 3197 s.insert(0, note.Note('c')) 3198 n = note.Note('g') 3199 n.pitch.microtone = 25 3200 s.insert(0, n) 3201 3202 c = chord.Chord(['d', 'f', 'a'], type='half') 3203 c.pitches[1].microtone = -50 3204 s.append(c) 3205 3206 pos = s.highestTime 3207 s.insert(pos, note.Note('e')) 3208 s.insert(pos, note.Note('b')) 3209 3210 unused_mt = streamHierarchyToMidiTracks(s)[0] 3211 3212 # s.show('midi') 3213 3214 def testExternalMidiProgramChangeB(self): 3215 from music21 import instrument 3216 from music21 import scale 3217 3218 iList = [instrument.Harpsichord, instrument.Clavichord, instrument.Accordion, 3219 instrument.Celesta, instrument.Contrabass, instrument.Viola, 3220 instrument.Harp, instrument.ElectricGuitar, instrument.Ukulele, 3221 instrument.Banjo, instrument.Piccolo, instrument.AltoSaxophone, 3222 instrument.Trumpet, instrument.Clarinet, instrument.Flute, 3223 instrument.Violin, instrument.Soprano, instrument.Oboe, 3224 instrument.Tuba, instrument.Sitar, instrument.Ocarina, 3225 instrument.Piano] 3226 3227 sc = scale.MajorScale() 3228 pitches = sc.getPitches('c2', 'c5') 3229 # random.shuffle(pitches) 3230 3231 s = stream.Stream() 3232 for i, p in enumerate(pitches): 3233 n = note.Note(p) 3234 n.quarterLength = 1.5 3235 inst = iList[i]() # call to create instance 3236 s.append(inst) 3237 s.append(n) 3238 3239 unused_mts = streamHierarchyToMidiTracks(s) 3240 # s.show('midi') 3241 3242 def testMicrotonalOutputA(self): 3243 s = stream.Stream() 3244 s.append(note.Note('c4', type='whole')) 3245 s.append(note.Note('c~4', type='whole')) 3246 s.append(note.Note('c#4', type='whole')) 3247 s.append(note.Note('c#~4', type='whole')) 3248 s.append(note.Note('d4', type='whole')) 3249 3250 # mts = streamHierarchyToMidiTracks(s) 3251 3252 s.insert(0, note.Note('g3', quarterLength=10)) 3253 unused_mts = streamHierarchyToMidiTracks(s) 3254 3255 def testMicrotonalOutputB(self): 3256 # a two-part stream 3257 from music21.midi import translate 3258 3259 p1 = stream.Part() 3260 p1.append(note.Note('c4', type='whole')) 3261 p1.append(note.Note('c~4', type='whole')) 3262 p1.append(note.Note('c#4', type='whole')) 3263 p1.append(note.Note('c#~4', type='whole')) 3264 p1.append(note.Note('d4', type='whole')) 3265 3266 # mts = translate.streamHierarchyToMidiTracks(s) 3267 p2 = stream.Part() 3268 p2.insert(0, note.Note('g2', quarterLength=20)) 3269 3270 # order here matters: this needs to be fixed 3271 s = stream.Score() 3272 s.insert(0, p1) 3273 s.insert(0, p2) 3274 3275 mts = translate.streamHierarchyToMidiTracks(s) 3276 self.assertEqual(mts[1].getChannels(), [1]) 3277 self.assertEqual(mts[2].getChannels(), [1, 2]) 3278 # print(mts) 3279 # s.show('midi') 3280 3281 # recreate with different order 3282 s = stream.Score() 3283 s.insert(0, p2) 3284 s.insert(0, p1) 3285 3286 mts = translate.streamHierarchyToMidiTracks(s) 3287 self.assertEqual(mts[1].getChannels(), [1]) 3288 self.assertEqual(mts[2].getChannels(), [1, 2]) 3289 3290 def testInstrumentAssignments(self): 3291 # test instrument assignments 3292 from music21 import instrument 3293 3294 iList = [instrument.Harpsichord, 3295 instrument.Viola, 3296 instrument.ElectricGuitar, 3297 instrument.Flute] 3298 3299 # number of notes, ql, pitch 3300 params = [(8, 1, 'C6'), 3301 (4, 2, 'G3'), 3302 (2, 4, 'E4'), 3303 (6, 1.25, 'C5')] 3304 3305 s = stream.Score() 3306 for i, inst in enumerate(iList): 3307 p = stream.Part() 3308 p.insert(0, inst()) # must call instrument to create instance 3309 3310 number, ql, pitchName = params[i] 3311 for j in range(number): 3312 p.append(note.Note(pitchName, quarterLength=ql)) 3313 s.insert(0, p) 3314 3315 # s.show('midi') 3316 mts = streamHierarchyToMidiTracks(s) 3317 # print(mts[0]) 3318 self.assertEqual(mts[0].getChannels(), []) # Conductor track 3319 self.assertEqual(mts[1].getChannels(), [1]) 3320 self.assertEqual(mts[2].getChannels(), [2]) 3321 self.assertEqual(mts[3].getChannels(), [3]) 3322 self.assertEqual(mts[4].getChannels(), [4]) 3323 3324 def testMicrotonalOutputD(self): 3325 # test instrument assignments with microtones 3326 from music21 import instrument 3327 from music21.midi import translate 3328 3329 iList = [instrument.Harpsichord, 3330 instrument.Viola, 3331 instrument.ElectricGuitar, 3332 instrument.Flute 3333 ] 3334 3335 # number of notes, ql, pitch 3336 params = [(8, 1, ['C6']), 3337 (4, 2, ['G3', 'G~3']), 3338 (2, 4, ['E4', 'E5']), 3339 (6, 1.25, ['C5'])] 3340 3341 s = stream.Score() 3342 for i, inst in enumerate(iList): 3343 p = stream.Part() 3344 p.insert(0, inst()) # must call instrument to create instance 3345 3346 number, ql, pitchNameList = params[i] 3347 for j in range(number): 3348 p.append(note.Note(pitchNameList[j % len(pitchNameList)], quarterLength=ql)) 3349 s.insert(0, p) 3350 3351 # s.show('midi') 3352 mts = translate.streamHierarchyToMidiTracks(s) 3353 # print(mts[1]) 3354 self.assertEqual(mts[1].getChannels(), [1]) 3355 self.assertEqual(mts[1].getProgramChanges(), [6]) # 6 = GM Harpsichord 3356 3357 self.assertEqual(mts[2].getChannels(), [2, 5]) 3358 self.assertEqual(mts[2].getProgramChanges(), [41]) # 41 = GM Viola 3359 3360 self.assertEqual(mts[3].getChannels(), [3, 6]) 3361 self.assertEqual(mts[3].getProgramChanges(), [26]) # 26 = GM ElectricGuitar 3362 # print(mts[3]) 3363 3364 self.assertEqual(mts[4].getChannels(), [4, 6]) 3365 self.assertEqual(mts[4].getProgramChanges(), [73]) # 73 = GM Flute 3366 3367 # s.show('midi') 3368 3369 def testMicrotonalOutputE(self): 3370 from music21 import corpus 3371 from music21 import interval 3372 s = corpus.parse('bwv66.6') 3373 p1 = s.parts[0] 3374 p2 = copy.deepcopy(p1) 3375 t = interval.Interval(0.5) # half sharp 3376 p2.transpose(t, inPlace=True, classFilterList=('Note', 'Chord')) 3377 post = stream.Score() 3378 post.insert(0, p1) 3379 post.insert(0, p2) 3380 3381 # post.show('midi') 3382 3383 mts = streamHierarchyToMidiTracks(post) 3384 self.assertEqual(mts[1].getChannels(), [1]) 3385 self.assertEqual(mts[1].getProgramChanges(), [0]) 3386 self.assertEqual(mts[2].getChannels(), [1, 2]) 3387 self.assertEqual(mts[2].getProgramChanges(), [0]) 3388 3389 # post.show('midi', app='Logic Express') 3390 3391 def testMicrotonalOutputF(self): 3392 from music21 import corpus 3393 from music21 import interval 3394 s = corpus.parse('bwv66.6') 3395 p1 = s.parts[0] 3396 p2 = copy.deepcopy(p1) 3397 p3 = copy.deepcopy(p1) 3398 3399 t1 = interval.Interval(12.5) # octave + half sharp 3400 t2 = interval.Interval(-12.25) # octave down minus 1/8th tone 3401 p2.transpose(t1, inPlace=True, classFilterList=('Note', 'Chord')) 3402 p3.transpose(t2, inPlace=True, classFilterList=('Note', 'Chord')) 3403 post = stream.Score() 3404 post.insert(0, p1) 3405 post.insert(0, p2) 3406 post.insert(0, p3) 3407 3408 # post.show('midi') 3409 3410 mts = streamHierarchyToMidiTracks(post) 3411 self.assertEqual(mts[1].getChannels(), [1]) 3412 self.assertEqual(mts[1].getProgramChanges(), [0]) 3413 self.assertEqual(mts[2].getChannels(), [1, 2]) 3414 self.assertEqual(mts[2].getProgramChanges(), [0]) 3415 self.assertEqual(mts[3].getChannels(), [1, 3]) 3416 self.assertEqual(mts[3].getProgramChanges(), [0]) 3417 3418 # post.show('midi', app='Logic Express') 3419 3420 def testMicrotonalOutputG(self): 3421 from music21 import corpus 3422 from music21 import interval 3423 from music21 import instrument 3424 s = corpus.parse('bwv66.6') 3425 p1 = s.parts[0] 3426 p1.remove(p1.getElementsByClass('Instrument').first()) 3427 p2 = copy.deepcopy(p1) 3428 p3 = copy.deepcopy(p1) 3429 3430 t1 = interval.Interval(12.5) # a sharp p4 3431 t2 = interval.Interval(-7.25) # a sharp p4 3432 p2.transpose(t1, inPlace=True, classFilterList=('Note', 'Chord')) 3433 p3.transpose(t2, inPlace=True, classFilterList=('Note', 'Chord')) 3434 post = stream.Score() 3435 p1.insert(0, instrument.Dulcimer()) 3436 post.insert(0, p1) 3437 p2.insert(0, instrument.Trumpet()) 3438 post.insert(0.125, p2) 3439 p3.insert(0, instrument.ElectricGuitar()) 3440 post.insert(0.25, p3) 3441 3442 # post.show('midi') 3443 3444 mts = streamHierarchyToMidiTracks(post) 3445 self.assertEqual(mts[1].getChannels(), [1]) 3446 self.assertEqual(mts[1].getProgramChanges(), [15]) 3447 3448 self.assertEqual(mts[2].getChannels(), [2, 4]) 3449 self.assertEqual(mts[2].getProgramChanges(), [56]) 3450 3451 # print(mts[3]) 3452 self.assertEqual(mts[3].getChannels(), [3, 5]) 3453 self.assertEqual(mts[3].getProgramChanges(), [26]) 3454 3455 # post.show('midi')#, app='Logic Express') 3456 3457 def testMidiTempoImportA(self): 3458 from music21 import converter 3459 3460 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3461 # a simple file created in athenacl 3462 fp = dirLib / 'test10.mid' 3463 s = converter.parse(fp) 3464 mmStream = s.flatten().getElementsByClass('MetronomeMark') 3465 self.assertEqual(len(mmStream), 4) 3466 self.assertEqual(mmStream[0].number, 120.0) 3467 self.assertEqual(mmStream[1].number, 110.0) 3468 self.assertEqual(mmStream[2].number, 90.0) 3469 self.assertEqual(mmStream[3].number, 60.0) 3470 3471 fp = dirLib / 'test06.mid' 3472 s = converter.parse(fp) 3473 mmStream = s.flatten().getElementsByClass('MetronomeMark') 3474 self.assertEqual(len(mmStream), 1) 3475 self.assertEqual(mmStream[0].number, 120.0) 3476 3477 fp = dirLib / 'test07.mid' 3478 s = converter.parse(fp) 3479 mmStream = s.flatten().getElementsByClass('MetronomeMark') 3480 self.assertEqual(len(mmStream), 1) 3481 self.assertEqual(mmStream[0].number, 180.0) 3482 3483 def testMidiTempoImportB(self): 3484 from music21 import converter 3485 3486 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3487 # a file with three tracks and one conductor track with four tempo marks 3488 fp = dirLib / 'test11.mid' 3489 s = converter.parse(fp) 3490 self.assertEqual(len(s.parts), 3) 3491 # metronome marks propagate to every staff, but are hidden on subsequent staffs 3492 self.assertEqual( 3493 [mm.numberImplicit for mm in s.parts[0].recurse().getElementsByClass('MetronomeMark')], 3494 [False, False, False, False] 3495 ) 3496 self.assertEqual( 3497 [mm.numberImplicit for mm in s.parts[1].recurse().getElementsByClass('MetronomeMark')], 3498 [True, True, True, True] 3499 ) 3500 self.assertEqual( 3501 [mm.numberImplicit for mm in s.parts[2].recurse().getElementsByClass('MetronomeMark')], 3502 [True, True, True, True] 3503 ) 3504 3505 def testMidiImportMeter(self): 3506 from music21 import converter 3507 fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid' 3508 s = converter.parse(fp) 3509 for p in s.parts: 3510 m = p.getElementsByClass('Measure').first() 3511 ts = m.timeSignature 3512 self.assertEqual(ts.ratioString, '3/4') 3513 self.assertIn(ts, m) 3514 3515 def testMidiExportConductorA(self): 3516 '''Export conductor data to MIDI conductor track.''' 3517 from music21 import meter 3518 from music21 import tempo 3519 3520 p1 = stream.Part() 3521 p1.repeatAppend(note.Note('c4'), 12) 3522 p1.insert(0, meter.TimeSignature('3/4')) 3523 p1.insert(0, tempo.MetronomeMark(number=90)) 3524 p1.insert(6, tempo.MetronomeMark(number=30)) 3525 3526 p2 = stream.Part() 3527 p2.repeatAppend(note.Note('g4'), 12) 3528 p2.insert(6, meter.TimeSignature('6/4')) 3529 3530 s = stream.Score() 3531 s.insert([0, p1, 0, p2]) 3532 3533 mts = streamHierarchyToMidiTracks(s) 3534 self.assertEqual(len(mts), 3) 3535 3536 # Tempo and time signature should be in conductor track only 3537 condTrkRepr = repr(mts[0].events) 3538 self.assertEqual(condTrkRepr.count('SET_TEMPO'), 2) 3539 self.assertEqual(condTrkRepr.count('TIME_SIGNATURE'), 2) 3540 3541 musicTrkRepr = repr(mts[1].events) 3542 self.assertEqual(musicTrkRepr.find('SET_TEMPO'), -1) 3543 self.assertEqual(musicTrkRepr.find('TIME_SIGNATURE'), -1) 3544 3545 # s.show('midi') 3546 # s.show('midi', app='Logic Express') 3547 3548 def testMidiExportConductorB(self): 3549 from music21 import tempo 3550 from music21 import corpus 3551 s = corpus.parse('bwv66.6') 3552 s.insert(0, tempo.MetronomeMark(number=240)) 3553 s.insert(4, tempo.MetronomeMark(number=30)) 3554 s.insert(6, tempo.MetronomeMark(number=120)) 3555 s.insert(8, tempo.MetronomeMark(number=90)) 3556 s.insert(12, tempo.MetronomeMark(number=360)) 3557 # s.show('midi') 3558 3559 mts = streamHierarchyToMidiTracks(s) 3560 condTrkRepr = repr(mts[0].events) 3561 self.assertEqual(condTrkRepr.count('SET_TEMPO'), 5) 3562 musicTrkRepr = repr(mts[1].events) 3563 self.assertEqual(musicTrkRepr.count('SET_TEMPO'), 0) 3564 3565 def testMidiExportConductorC(self): 3566 from music21 import tempo 3567 minTempo = 60 3568 maxTempo = 600 3569 period = 50 3570 s = stream.Stream() 3571 for i in range(100): 3572 scalar = (math.sin(i * (math.pi * 2) / period) + 1) * 0.5 3573 n = ((maxTempo - minTempo) * scalar) + minTempo 3574 s.append(tempo.MetronomeMark(number=n)) 3575 s.append(note.Note('g3')) 3576 mts = streamHierarchyToMidiTracks(s) 3577 self.assertEqual(len(mts), 2) 3578 mtsRepr = repr(mts[0].events) 3579 self.assertEqual(mtsRepr.count('SET_TEMPO'), 100) 3580 3581 def testMidiExportConductorD(self): 3582 '''120 bpm and 4/4 are supplied by default.''' 3583 s = stream.Stream() 3584 s.insert(note.Note()) 3585 mts = streamHierarchyToMidiTracks(s) 3586 self.assertEqual(len(mts), 2) 3587 condTrkRepr = repr(mts[0].events) 3588 self.assertEqual(condTrkRepr.count('SET_TEMPO'), 1) 3589 self.assertEqual(condTrkRepr.count('TIME_SIGNATURE'), 1) 3590 # No pitch bend events in conductor track 3591 self.assertEqual(condTrkRepr.count('PITCH_BEND'), 0) 3592 3593 def testMidiExportConductorE(self): 3594 '''The conductor only gets the first element at an offset.''' 3595 from music21 import converter 3596 from music21 import tempo 3597 from music21 import key 3598 3599 s = stream.Stream() 3600 p1 = converter.parse('tinynotation: c1') 3601 p2 = converter.parse('tinynotation: d2 d2') 3602 p1.insert(0, tempo.MetronomeMark(number=44)) 3603 p2.insert(0, tempo.MetronomeMark(number=144)) 3604 p2.insert(2, key.KeySignature(-5)) 3605 s.insert(0, p1) 3606 s.insert(0, p2) 3607 3608 conductor = conductorStream(s) 3609 tempos = conductor.getElementsByClass('MetronomeMark') 3610 keySignatures = conductor.getElementsByClass('KeySignature') 3611 self.assertEqual(len(tempos), 1) 3612 self.assertEqual(tempos[0].number, 44) 3613 self.assertEqual(len(keySignatures), 1) 3614 3615 def testMidiExportVelocityA(self): 3616 s = stream.Stream() 3617 for i in range(10): 3618 # print(i) 3619 n = note.Note('c3') 3620 n.volume.velocityScalar = i / 10 3621 n.volume.velocityIsRelative = False 3622 s.append(n) 3623 3624 # s.show('midi') 3625 mts = streamHierarchyToMidiTracks(s) 3626 mtsRepr = repr(mts[1].events) 3627 self.assertEqual(mtsRepr.count('velocity=114'), 1) 3628 self.assertEqual(mtsRepr.count('velocity=13'), 1) 3629 3630 def testMidiExportVelocityB(self): 3631 import random 3632 from music21 import volume 3633 3634 s1 = stream.Stream() 3635 shift = [0, 6, 12] 3636 amps = [(x / 10. + 0.4) for x in range(6)] 3637 amps = amps + list(reversed(amps)) 3638 3639 qlList = [1.5] * 6 + [1] * 8 + [2] * 6 + [1.5] * 8 + [1] * 4 3640 for j, ql in enumerate(qlList): 3641 if random.random() > 0.6: 3642 c = note.Rest() 3643 else: 3644 c = chord.Chord(['c3', 'd-4', 'g5']) 3645 vChord = [] 3646 for i, unused_cSub in enumerate(c): 3647 v = volume.Volume() 3648 v.velocityScalar = amps[(j + shift[i]) % len(amps)] 3649 v.velocityIsRelative = False 3650 vChord.append(v) 3651 c.volume = vChord # can set to list 3652 c.duration.quarterLength = ql 3653 s1.append(c) 3654 3655 s2 = stream.Stream() 3656 random.shuffle(qlList) 3657 random.shuffle(amps) 3658 for j, ql in enumerate(qlList): 3659 n = note.Note(random.choice(['f#2', 'f#2', 'e-2'])) 3660 n.duration.quarterLength = ql 3661 n.volume.velocityScalar = amps[j % len(amps)] 3662 s2.append(n) 3663 3664 s = stream.Score() 3665 s.insert(0, s1) 3666 s.insert(0, s2) 3667 3668 mts = streamHierarchyToMidiTracks(s) 3669 # mts[0] is the conductor track 3670 self.assertIn("SET_TEMPO", repr(mts[0].events)) 3671 mtsRepr = repr(mts[1].events) + repr(mts[2].events) 3672 self.assertGreater(mtsRepr.count('velocity=51'), 2) 3673 self.assertGreater(mtsRepr.count('velocity=102'), 2) 3674 # s.show('midi') 3675 3676 def testImportTruncationProblemA(self): 3677 from music21 import converter 3678 3679 # specialized problem of not importing last notes 3680 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3681 fp = dirLib / 'test12.mid' 3682 s = converter.parse(fp) 3683 3684 self.assertEqual(len(s.parts[0].flatten().notes), 3) 3685 self.assertEqual(len(s.parts[1].flatten().notes), 3) 3686 self.assertEqual(len(s.parts[2].flatten().notes), 3) 3687 self.assertEqual(len(s.parts[3].flatten().notes), 3) 3688 3689 # s.show('t') 3690 # s.show('midi') 3691 3692 def testImportChordVoiceA(self): 3693 # looking at cases where notes appear to be chord but 3694 # are better seen as voices 3695 from music21 import converter 3696 # specialized problem of not importing last notes 3697 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3698 fp = dirLib / 'test13.mid' 3699 s = converter.parse(fp) 3700 # s.show('t') 3701 self.assertEqual(len(s.flatten().notes), 7) 3702 # s.show('midi') 3703 3704 fp = dirLib / 'test14.mid' 3705 s = converter.parse(fp) 3706 # three chords will be created, as well as two voices 3707 self.assertEqual(len(s.flatten().getElementsByClass('Chord')), 3) 3708 self.assertEqual(len(s.parts.first().measure(3).voices), 2) 3709 3710 def testImportChordsA(self): 3711 from music21 import converter 3712 3713 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3714 fp = dirLib / 'test05.mid' 3715 3716 # a simple file created in athenacl 3717 s = converter.parse(fp) 3718 # s.show('t') 3719 self.assertEqual(len(s.flatten().getElementsByClass('Chord')), 5) 3720 3721 def testMidiEventsImported(self): 3722 self.maxDiff = None 3723 from music21 import corpus 3724 3725 def procCompare(mf_inner, match_inner): 3726 triples = [] 3727 for i in range(2): 3728 for j in range(0, len(mf_inner.tracks[i].events), 2): 3729 d = mf_inner.tracks[i].events[j] # delta 3730 e = mf_inner.tracks[i].events[j + 1] # events 3731 triples.append((d.time, e.type.name, e.pitch)) 3732 self.assertEqual(triples, match_inner) 3733 3734 s = corpus.parse('bach/bwv66.6') 3735 part = s.parts[0].measures(6, 9) # last measures 3736 # part.show('musicxml') 3737 # part.show('midi') 3738 3739 mf = streamToMidiFile(part) 3740 match = [(0, 'KEY_SIGNATURE', None), # Conductor track 3741 (0, 'TIME_SIGNATURE', None), 3742 (0, 'SET_TEMPO', None), 3743 (1024, 'END_OF_TRACK', None), 3744 (0, 'SEQUENCE_TRACK_NAME', None), # Music track 3745 (0, 'PITCH_BEND', None), 3746 (0, 'PROGRAM_CHANGE', None), 3747 (0, 'NOTE_ON', 69), 3748 (1024, 'NOTE_OFF', 69), 3749 (0, 'NOTE_ON', 71), 3750 (1024, 'NOTE_OFF', 71), 3751 (0, 'NOTE_ON', 73), 3752 (1024, 'NOTE_OFF', 73), 3753 (0, 'NOTE_ON', 69), 3754 (1024, 'NOTE_OFF', 69), 3755 (0, 'NOTE_ON', 68), 3756 (1024, 'NOTE_OFF', 68), 3757 (0, 'NOTE_ON', 66), 3758 (1024, 'NOTE_OFF', 66), 3759 (0, 'NOTE_ON', 68), 3760 (2048, 'NOTE_OFF', 68), 3761 (0, 'NOTE_ON', 66), 3762 (2048, 'NOTE_OFF', 66), 3763 (0, 'NOTE_ON', 66), 3764 (1024, 'NOTE_OFF', 66), 3765 (0, 'NOTE_ON', 66), 3766 (2048, 'NOTE_OFF', 66), 3767 (0, 'NOTE_ON', 66), 3768 (512, 'NOTE_OFF', 66), 3769 (0, 'NOTE_ON', 65), 3770 (512, 'NOTE_OFF', 65), 3771 (0, 'NOTE_ON', 66), 3772 (1024, 'NOTE_OFF', 66), 3773 (1024, 'END_OF_TRACK', None)] 3774 procCompare(mf, match) 3775 3776 def testMidiInstrumentToStream(self): 3777 from music21 import converter 3778 from music21 import instrument 3779 from music21.musicxml import testPrimitive 3780 3781 s = converter.parse(testPrimitive.transposing01) 3782 mf = streamToMidiFile(s) 3783 out = midiFileToStream(mf) 3784 first_instrument = out.parts.first().measure(1).getElementsByClass('Instrument').first() 3785 self.assertIsInstance(first_instrument, instrument.Oboe) 3786 self.assertEqual(first_instrument.quarterLength, 0) 3787 3788 # Unrecognized instrument 'a' 3789 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3790 fp = dirLib / 'test15.mid' 3791 s2 = converter.parse(fp) 3792 self.assertEqual(s2.parts[0].partName, 'a') 3793 3794 def testImportZeroDurationNote(self): 3795 ''' 3796 Musescore places zero duration notes in multiple voice scenarios 3797 to represent double stemmed notes. Avoid false positives for extra voices. 3798 https://github.com/cuthbertLab/music21/issues/600 3799 ''' 3800 from music21 import converter 3801 3802 dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' 3803 fp = dirLib / 'test16.mid' 3804 s = converter.parse(fp) 3805 self.assertEqual(len(s.parts.first().measure(1).voices), 2) 3806 els = s.parts.first().flatten().getElementsByOffset(0.5) 3807 self.assertSequenceEqual([e.duration.quarterLength for e in els], [0, 1]) 3808 3809 def testRepeatsExpanded(self): 3810 from music21 import converter 3811 from music21.musicxml import testPrimitive 3812 3813 s = converter.parse(testPrimitive.repeatBracketsA) 3814 num_notes_before = len(s.flatten().notes) 3815 prepared = prepareStreamForMidi(s) 3816 num_notes_after = len(prepared.flatten().notes) 3817 self.assertGreater(num_notes_after, num_notes_before) 3818 3819 def testNullTerminatedInstrumentName(self): 3820 ''' 3821 MuseScore currently writes null bytes at the end of instrument names. 3822 https://musescore.org/en/node/310158 3823 ''' 3824 from music21 import instrument 3825 from music21 import midi as midiModule 3826 3827 event = midiModule.MidiEvent() 3828 event.data = bytes('Piccolo\x00', 'utf-8') 3829 i = midiEventsToInstrument(event) 3830 self.assertIsInstance(i, instrument.Piccolo) 3831 3832 # test that nothing was broken. 3833 event.data = bytes('Flute', 'utf-8') 3834 i = midiEventsToInstrument(event) 3835 self.assertIsInstance(i, instrument.Flute) 3836 3837 def testLousyInstrumentData(self): 3838 from music21 import instrument 3839 from music21 import midi as midiModule 3840 3841 lousyNames = (' ', 'Instrument 20', 'Instrument', 'Inst 2', 'instrument') 3842 for name in lousyNames: 3843 with self.subTest(name=name): 3844 event = midiModule.MidiEvent() 3845 event.data = bytes(name, 'utf-8') 3846 event.type = midiModule.MetaEvents.INSTRUMENT_NAME 3847 i = midiEventsToInstrument(event) 3848 self.assertIsNone(i.instrumentName) 3849 3850 # lousy program change 3851 # https://github.com/cuthbertLab/music21/issues/988 3852 event = midiModule.MidiEvent() 3853 event.data = 0 3854 event.channel = 10 3855 event.type = midiModule.ChannelVoiceMessages.PROGRAM_CHANGE 3856 3857 expected = 'Unable to determine instrument from ' 3858 expected += '<music21.midi.MidiEvent PROGRAM_CHANGE, track=None, channel=10, data=0>' 3859 expected += '; getting generic UnpitchedPercussion' 3860 with self.assertWarnsRegex(TranslateWarning, expected): 3861 i = midiEventsToInstrument(event) 3862 self.assertIsInstance(i, instrument.UnpitchedPercussion) 3863 3864 def testConductorStream(self): 3865 s = stream.Stream() 3866 p = stream.Stream() 3867 p.priority = -2 3868 m = stream.Stream() 3869 m.append(note.Note('C4')) 3870 p.append(m) 3871 s.insert(0, p) 3872 conductor = conductorStream(s) 3873 self.assertEqual(conductor.priority, -3) 3874 3875 def testRestsMadeInVoice(self): 3876 from music21 import converter 3877 3878 fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid' 3879 inn = converter.parse(fp) 3880 3881 self.assertEqual( 3882 len(inn.parts[1].measure(3).voices.last().getElementsByClass('Rest')), 1) 3883 3884 def testRestsMadeInMeasures(self): 3885 from music21 import converter 3886 3887 fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test17.mid' 3888 inn = converter.parse(fp) 3889 pianoLH = inn.parts.last() 3890 m1 = pianoLH.measure(1) # quarter note, quarter note, quarter rest 3891 m2 = pianoLH.measure(2) 3892 self.assertEqual(len(m1.notesAndRests), 3) 3893 self.assertEqual(len(m1.notes), 2) 3894 self.assertEqual(m1.duration.quarterLength, 3.0) 3895 self.assertEqual(pianoLH.elementOffset(m2), 3.0) 3896 3897 for part in inn.parts: 3898 with self.subTest(part=part): 3899 self.assertEqual( 3900 sum(m.barDuration.quarterLength for m in part.getElementsByClass( 3901 stream.Measure) 3902 ), 3903 part.duration.quarterLength 3904 ) 3905 3906 def testEmptyExport(self): 3907 from music21 import instrument 3908 3909 p = stream.Part() 3910 p.insert(instrument.Instrument()) 3911 # Previously, this errored when we assumed streams lacking notes 3912 # to be conductor tracks 3913 # https://github.com/cuthbertLab/music21/issues/1013 3914 streamToMidiFile(p) 3915 3916 def testImportInstrumentsWithoutProgramChanges(self): 3917 ''' 3918 Instrument instances are created from both program changes and 3919 track or sequence names. Since we have a MIDI file, we should not 3920 rely on default MIDI programs defined in the instrument module; we 3921 should just keep track of the active program number. 3922 https://github.com/cuthbertLab/music21/issues/1085 3923 ''' 3924 from music21 import midi as midiModule 3925 3926 event1 = midiModule.MidiEvent() 3927 event1.data = 0 3928 event1.channel = 1 3929 event1.type = midiModule.ChannelVoiceMessages.PROGRAM_CHANGE 3930 3931 event2 = midiModule.MidiEvent() 3932 # This will normalize to an instrument.Soprano, but we don't want 3933 # the default midiProgram, we want 0. 3934 event2.data = b'Soprano' 3935 event2.channel = 2 3936 event2.type = midiModule.MetaEvents.SEQUENCE_TRACK_NAME 3937 3938 DUMMY_DELTA_TIME = None 3939 meta_event_pairs = getMetaEvents([(DUMMY_DELTA_TIME, event1), (DUMMY_DELTA_TIME, event2)]) 3940 # Second element of the tuple is the instrument instance 3941 self.assertEqual(meta_event_pairs[0][1].midiProgram, 0) 3942 self.assertEqual(meta_event_pairs[1][1].midiProgram, 0) 3943 3944 # Remove the initial PROGRAM_CHANGE and get a default midiProgram 3945 meta_event_pairs = getMetaEvents([(DUMMY_DELTA_TIME, event2)]) 3946 self.assertEqual(meta_event_pairs[0][1].midiProgram, 53) 3947 3948 3949# ------------------------------------------------------------------------------ 3950_DOC_ORDER = [streamToMidiFile, midiFileToStream] 3951 3952if __name__ == '__main__': 3953 import music21 3954 music21.mainTest(Test) # , runTest='testConductorStream') 3955 3956