Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions music21/converter/subConverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,13 +1089,15 @@ def write(self,
subformats=(),
*,
addStartDelay: bool = False,
addEndDelay: bool = True,
encoding: str = '',
**keywords): # pragma: no cover
from music21.midi import translate as midiTranslate
if fp is None:
fp = self.getTemporaryFile()

mf = midiTranslate.music21ObjectToMidiFile(obj, addStartDelay=addStartDelay,
addEndDelay=addEndDelay,
encoding=encoding or self.encoding)
mf.open(fp, 'wb') # write binary
mf.write()
Expand Down
21 changes: 21 additions & 0 deletions music21/midi/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,27 @@ def testMidiExportLyrics(self):
for n in m.flatten().notes:
self.assertEqual(n.lyric, lyric)

def testAddEndDelay(self):
from music21 import defaults
s = stream.Stream()
s.repeatAppend(note.Note('C4', quarterLength=1.0), 2)

# default keeps the trailing rest
mfDefault = streamToMidiFile(s)
endDtDefault = mfDefault.tracks[-1].events[-2]
self.assertEqual(endDtDefault.time, defaults.ticksAtStart)

# addEndDelay=False removes the trailing rest
mfNoDelay = streamToMidiFile(s, addEndDelay=False)
endDtNoDelay = mfNoDelay.tracks[-1].events[-2]
self.assertEqual(endDtNoDelay.time, 0)

def testGetEndEventsAddEndDelay(self):
from music21 import defaults
from music21.midi.translate import getEndEvents
self.assertEqual(getEndEvents()[0].time, defaults.ticksAtStart)
self.assertEqual(getEndEvents(addEndDelay=False)[0].time, 0)


# ------------------------------------------------------------------------------
if __name__ == '__main__':
Expand Down
37 changes: 33 additions & 4 deletions music21/midi/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ def getStartEvents(

def getEndEvents(
midiTrack: MidiTrack|None = None,
*,
addEndDelay: bool = True,
) -> list[MidiEvent|DeltaTime]:
'''
Returns a list of midi.MidiEvent objects found at the end of a track.
Expand All @@ -266,12 +268,23 @@ def getEndEvents(
[<music21.midi.DeltaTime t=10080, track=None>,
<music21.midi.MidiEvent END_OF_TRACK, track=None, data=b''>]

Set `addEndDelay` to False to omit the trailing rest before END_OF_TRACK:

>>> midi.translate.getEndEvents(addEndDelay=False)
[<music21.midi.DeltaTime (empty) track=None>,
<music21.midi.MidiEvent END_OF_TRACK, track=None, data=b''>]

Changed in v10 - getEndEvents does not take a channel

New in v10.2 - addEndDelay keyword

Note: the addEndDelay keyword and its threading through the MIDI export
functions was AI-assisted (issue #1141).
'''
events: list[MidiEvent|DeltaTime] = []

dt = DeltaTime(track=midiTrack)
dt.time = defaults.ticksAtStart
dt.time = defaults.ticksAtStart if addEndDelay else 0
events.append(dt)

me = MidiEvent(
Expand All @@ -291,26 +304,31 @@ def music21ObjectToMidiFile(
music21Object: base.Music21Object,
*,
addStartDelay=False,
addEndDelay=True,
encoding: str = 'utf-8',
) -> MidiFile:
'''
Either calls streamToMidiFile on the music21Object or
puts a copy of that object into a Stream (so as
not to change activeSites, etc.) and calls streamToMidiFile on
that object.

New in v10.2 - addEndDelay keyword
'''
if isinstance(music21Object, stream.Stream):
if music21Object.atSoundingPitch is False:
music21Object = music21Object.toSoundingPitch()

return streamToMidiFile(t.cast(stream.Stream, music21Object),
addStartDelay=addStartDelay,
addEndDelay=addEndDelay,
encoding=encoding)
else:
m21ObjectCopy = copy.deepcopy(music21Object)
s: stream.Stream = stream.Stream()
s.insert(0, m21ObjectCopy)
return streamToMidiFile(s, addStartDelay=addStartDelay,
addEndDelay=addEndDelay,
encoding=encoding)


Expand Down Expand Up @@ -1783,7 +1801,7 @@ def packetsToDeltaSeparatedEvents(
return events


def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None):
def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None, *, addEndDelay=True):
'''
Given packets already allocated with channel
and/or instrument assignments, place these in a MidiTrack.
Expand All @@ -1795,6 +1813,8 @@ def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None):
will be assigned to

Use streamToPackets to convert the Stream to the packets

New in v10.2 - addEndDelay keyword
'''
# TODO: for a given track id, need to find start/end channel
mt = MidiTrack(trackId)
Expand All @@ -1808,7 +1828,7 @@ def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None):
mt.events += packetsToDeltaSeparatedEvents(trackPackets, mt)

# must update all events with a ref to this MidiTrack
mt.events += getEndEvents(mt)
mt.events += getEndEvents(mt, addEndDelay=addEndDelay)
mt.updateEvents() # sets this track as .track for all events
return mt

Expand Down Expand Up @@ -2633,6 +2653,7 @@ def streamHierarchyToMidiTracks(
*,
acceptableChannelList=None,
addStartDelay=False,
addEndDelay=True,
encoding='utf-8',
):
'''
Expand All @@ -2659,6 +2680,7 @@ def streamHierarchyToMidiTracks(

* Changed in v6: acceptableChannelList is keyword only. addStartDelay is new.
* Changed in v6.5: Track 0 (tempo/conductor track) always exported.
* New in v10.2: addEndDelay keyword.
'''
# makes a deepcopy
s = prepareStreamForMidi(inputM21)
Expand Down Expand Up @@ -2714,7 +2736,8 @@ def streamHierarchyToMidiTracks(
mt = packetsToMidiTrack(netPackets,
trackId=trackId,
channel=initChannel,
instrumentObj=instrumentObj)
instrumentObj=instrumentObj,
addEndDelay=addEndDelay)
midiTracks.append(mt)

return midiTracks
Expand Down Expand Up @@ -2772,6 +2795,7 @@ def streamToMidiFile(
inputM21: stream.Stream,
*,
addStartDelay: bool = False,
addEndDelay: bool = True,
acceptableChannelList: list[int]|None = None,
encoding: str = 'utf-8',
) -> MidiFile:
Expand Down Expand Up @@ -2800,10 +2824,15 @@ def streamToMidiFile(
>>> #_DOCS_SHOW mf.close()

See :func:`channelInstrumentData` for documentation on `acceptableChannelList`.

Set `addEndDelay` to False to skip the trailing rest added by `getEndEvents`.

New in v10.2 - addEndDelay keyword
'''
s = inputM21
midiTracks = streamHierarchyToMidiTracks(s,
addStartDelay=addStartDelay,
addEndDelay=addEndDelay,
acceptableChannelList=acceptableChannelList,
encoding=encoding,
)
Expand Down
Loading