diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 062e39c89..2a5840999 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -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. @@ -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 diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index afa7d3c96..9a97af668 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -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) diff --git a/music21/midi/testPrimitive/test15.mid b/music21/midi/testPrimitive/test15.mid new file mode 100644 index 000000000..3b82ac96e Binary files /dev/null and b/music21/midi/testPrimitive/test15.mid differ diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 2b96c76dc..ee2660988 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -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' @@ -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' @@ -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): @@ -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() @@ -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,