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
25 changes: 25 additions & 0 deletions music21/converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,11 @@ def parse(value: Union[bundles.MetadataEntry, bytes, str, pathlib.Path],

`format` specifies the format to parse the line of text or the file as.

`quantizePost` specifies whether to quantize a stream resulting from MIDI conversion.
By default, MIDI streams qre quantized to the nearest sixteenth or triplet-eighth
(i.e. smaller durations will not be preserved).
`quarterLengthDivisors` sets the quantization units explicitly.

A string of text is first checked to see if it is a filename that exists on
disk. If not it is searched to see if it looks like a URL. If not it is
processed as data.
Expand Down Expand Up @@ -1834,6 +1839,26 @@ def testParseMidiQuantize(self):
for n in midiStream.recurse(classFilter='Note'):
self.assertTrue(numberTools.almostEquals(n.quarterLength % 0.5, 0.0))

def testParseMidiNoQuantize(self):
'''
Checks that quantization is not performed if quantizePost=False.
Source MIDI file contains only: 3 16th notes, 2 32nd notes.
'''
fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test15.mid'

# Establish first that quantizePost=False will make a difference given the current default
from music21.defaults import quantizationQuarterLengthDivisors
self.assertGreater(8, max(quantizationQuarterLengthDivisors))

streamFpNotQuantized = parse(fp, forceSource=True, quantizePost=False)
self.assertIn(0.875, streamFpNotQuantized.flat._uniqueOffsetsAndEndTimes())

# Also check raw data: https://github.com/cuthbertLab/music21/issues/546
with fp.open('rb') as f:
data = f.read()
streamDataNotQuantized = parse(data, quantizePost=False)
self.assertIn(0.875, streamDataNotQuantized.flat._uniqueOffsetsAndEndTimes())

def testIncorrectNotCached(self):
'''
Here is a filename with an incorrect extension (.txt for .rnText). Make sure that
Expand Down
12 changes: 11 additions & 1 deletion music21/converter/subConverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,15 +1006,25 @@ def parseData(self, strData, number=None):
Get MIDI data from a binary string representation.

Calls midi.translate.midiStringToStream.

Keywords to control quantization:
`quantizePost` controls whether to quantize the output. (Default: True)
`quarterLengthDivisors` allows for overriding the default quantization units
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
'''
from music21.midi import translate as midiTranslate
self.stream = midiTranslate.midiStringToStream(strData)
self.stream = midiTranslate.midiStringToStream(strData, **self.keywords)

def parseFile(self, fp, number=None, **keywords):
'''
Get MIDI data from a file path.

Calls midi.translate.midiFilePathToStream.

Keywords to control quantization:
`quantizePost` controls whether to quantize the output. (Default: True)
`quarterLengthDivisors` allows for overriding the default quantization units
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
'''
from music21.midi import translate as midiTranslate
midiTranslate.midiFilePathToStream(fp, self.stream, **keywords)
Expand Down
Binary file added music21/midi/testPrimitive/test15.mid
Binary file not shown.
22 changes: 20 additions & 2 deletions music21/midi/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,11 @@ def midiFilePathToStream(filePath, inputM21=None, **keywords):
return a :class:`~music21.stream.Score` object (or if inputM21 is passed in,
use that object instead).

Keywords to control quantization:
`quantizePost` controls whether to quantize the output. (Default: True)
`quarterLengthDivisors` allows for overriding the default quantization units
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).

>>> sfp = common.getSourceFilePath() #_DOCS_HIDE
>>> fp = str(sfp / 'midi' / 'testPrimitive' / 'test05.mid') #_DOCS_HIDE
>>> #_DOCS_SHOW fp = '/Users/test/music21/midi/testPrimitive/test05.mid'
Expand Down Expand Up @@ -2194,10 +2199,15 @@ def midiAsciiStringToBinaryString(midiFormat=1, ticksPerQuarterNote=960, tracksE
return midiBinStr


def midiStringToStream(strData):
def midiStringToStream(strData, **keywords):
r'''
Convert a string of binary midi data to a Music21 stream.Score object.

Keywords to control quantization:
`quantizePost` controls whether to quantize the output. (Default: True)
`quarterLengthDivisors` allows for overriding the default quantization units
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).

N.B. -- this has been somewhat problematic, so use at your own risk.

>>> midiBinStr = (b'MThd\x00\x00\x00\x06\x00\x01\x00\x01\x04\x00'
Expand All @@ -2214,7 +2224,7 @@ def midiStringToStream(strData):
mf = midiModule.MidiFile()
# do not need to call open or close on MidiFile instance
mf.readstr(strData)
return midiFileToStream(mf)
return midiFileToStream(mf, **keywords)


def midiFileToStream(mf, inputM21=None, quantizePost=True, **keywords):
Expand All @@ -2228,6 +2238,11 @@ def midiFileToStream(mf, inputM21=None, quantizePost=True, **keywords):

The `inputM21` object can specify an existing Stream (or Stream subclass) to fill.

Keywords to control quantization:
`quantizePost` controls whether to quantize the output. (Default: True)
`quarterLengthDivisors` allows for overriding the default quantization units
in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).

>>> import os
>>> fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test05.mid'
>>> mf = midi.MidiFile()
Expand All @@ -2251,6 +2266,9 @@ def midiFileToStream(mf, inputM21=None, quantizePost=True, **keywords):
if not mf.tracks:
raise exceptions21.StreamException('no tracks are defined in this MIDI file.')

if 'quantizePost' in keywords:
quantizePost = keywords.pop('quantizePost')

# create a stream for each tracks
# may need to check if tracks actually have event data
midiTracksToStreams(mf.tracks,
Expand Down