From d26c59c9ce7cc9d58414022980f66ceb5f58d82f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 27 May 2020 01:05:55 -0400 Subject: [PATCH 1/7] Respect quantizePost=False when parsing raw MIDI --- music21/converter/__init__.py | 20 ++++++++++++++++++++ music21/converter/subConverters.py | 2 +- music21/midi/testPrimitive/test15.mid | Bin 0 -> 176 bytes music21/midi/translate.py | 7 +++++-- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 music21/midi/testPrimitive/test15.mid diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 062e39c89..1b86619c0 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -1833,6 +1833,26 @@ def testParseMidiQuantize(self): # midiStream.show() 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=True will make a difference + streamFpQuantized = parse(fp, forceSource=True, storePickle=False, quantizePost=True) + self.assertNotIn(0.875, streamFpQuantized.flat._uniqueOffsetsAndEndTimes()) + + streamFpNotQuantized = parse(fp, forceSource=True, storePickle=False, 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): ''' diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index afa7d3c96..ae1dd7d13 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1008,7 +1008,7 @@ def parseData(self, strData, number=None): Calls midi.translate.midiStringToStream. ''' 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): ''' diff --git a/music21/midi/testPrimitive/test15.mid b/music21/midi/testPrimitive/test15.mid new file mode 100644 index 0000000000000000000000000000000000000000..f86788cbff0344021488ee659d77a50f024639db GIT binary patch literal 176 zcmeYb$w*;fU|?flWME=p@C_--2J(~{{)eyvX(0F?!NSNS!NKr9k_p5MWM*F|-@))- zAEq>);XkKXaBgBziEn9fX0k$Xep*RzWnM{!f+53yX2wK@0}Kos*i#uca5+FQdp*Mh ioAf3yr@@8+$Zgph_SNptS&r87(pZ literal 0 HcmV?d00001 diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 2b96c76dc..08490384b 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -2194,7 +2194,7 @@ 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. @@ -2214,7 +2214,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): @@ -2251,6 +2251,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, From 122fc21ba06071c1f7945ea5e46bf88044d4215c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 27 May 2020 01:23:14 -0400 Subject: [PATCH 2/7] fix flake --- music21/converter/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 1b86619c0..160d94e98 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -1833,10 +1833,10 @@ def testParseMidiQuantize(self): # midiStream.show() 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. + 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' From 927391b9269f4db4c3f95a9eb82fbf85ae4db194 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 30 May 2020 08:57:48 -0400 Subject: [PATCH 3/7] Document quantization keywords on converters --- music21/converter/__init__.py | 5 +++++ music21/converter/subConverters.py | 10 ++++++++++ music21/midi/translate.py | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 160d94e98..6e5e67574 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. diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index ae1dd7d13..9a97af668 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1006,6 +1006,11 @@ 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.keywords) @@ -1015,6 +1020,11 @@ 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/translate.py b/music21/midi/translate.py index 08490384b..5401512c3 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -2198,6 +2198,11 @@ 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' From ec19285696e2559bcb7deacd866d175a809929b7 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 30 May 2020 09:58:33 -0400 Subject: [PATCH 4/7] Parse file once in testParseMidiNoQuantize --- music21/converter/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 6e5e67574..9f696a952 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -1846,11 +1846,11 @@ def testParseMidiNoQuantize(self): ''' fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test15.mid' - # Establish first that quantizePost=True will make a difference - streamFpQuantized = parse(fp, forceSource=True, storePickle=False, quantizePost=True) - self.assertNotIn(0.875, streamFpQuantized.flat._uniqueOffsetsAndEndTimes()) + # 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, storePickle=False, quantizePost=False) + streamFpNotQuantized = parse(fp, quantizePost=False) self.assertIn(0.875, streamFpNotQuantized.flat._uniqueOffsetsAndEndTimes()) # Also check raw data: https://github.com/cuthbertLab/music21/issues/546 From e087a48a1421bf191eb7d44c0d58b9f752a974ac Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 30 May 2020 10:50:39 -0400 Subject: [PATCH 5/7] Force source in testParseMidiNoQuantize --- music21/converter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 9f696a952..2a5840999 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -1850,7 +1850,7 @@ def testParseMidiNoQuantize(self): from music21.defaults import quantizationQuarterLengthDivisors self.assertGreater(8, max(quantizationQuarterLengthDivisors)) - streamFpNotQuantized = parse(fp, quantizePost=False) + 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 From 5777ef2efc62eae1e1ed74cc88a9b0883a9c49dd Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 30 May 2020 13:15:43 -0400 Subject: [PATCH 6/7] FIx test midi file NOTE_OFF time --- music21/midi/testPrimitive/test15.mid | Bin 176 -> 164 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/music21/midi/testPrimitive/test15.mid b/music21/midi/testPrimitive/test15.mid index f86788cbff0344021488ee659d77a50f024639db..3b82ac96eec3ed5e4c099838611806e86906c54a 100644 GIT binary patch delta 79 zcmdnMxP)^d>N;!G;0IaX{w8HaY-B9YD%~ O${HD Date: Sat, 30 May 2020 13:16:40 -0400 Subject: [PATCH 7/7] Document quantization keywords in more places --- music21/midi/translate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 5401512c3..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' @@ -2233,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()