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