Skip to content

Commit

Permalink
Merge pull request #550 from cuthbertLab/efficient-roman
Browse files Browse the repository at this point in the history
WIP: Chord caching improvements and better Caching across M21
  • Loading branch information
mscuthbert committed May 30, 2020
2 parents 72382d4 + 3529ea2 commit 9613a55
Show file tree
Hide file tree
Showing 16 changed files with 515 additions and 242 deletions.
8 changes: 2 additions & 6 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion music21/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
Changing this number invalidates old pickles -- do it if the old pickles create a problem.
'''

__version_info__ = (6, 0, 3, 'a3')
__version_info__ = (6, 0, 5, 'a1')

v = '.'.join(str(x) for x in __version_info__[0:3])
if len(__version_info__) > 3 and __version_info__[3]:
Expand Down
2 changes: 1 addition & 1 deletion music21/alpha/analysis/fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def checkFixerHelper(self, testCase, testFixer):
self.assertTrue(isEqual, testingName + assertionCheck + reason)

# test fixing in place
fixerInPlaceResult = fixer.fix()
fixerInPlaceResult = fixer.fix(inPlace=True)
self.assertIsNone(fixerInPlaceResult, testingName)

assertionCheck = ". Expect changes in fixer's omr stream, but unequal because "
Expand Down
93 changes: 80 additions & 13 deletions music21/audioSearch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Authors: Jordi Bartolome
# Michael Scott Cuthbert
#
# Copyright: Copyright © 2011 Michael Scott Cuthbert and the music21 Project
# Copyright: Copyright © 2011-2020 Michael Scott Cuthbert and the music21 Project
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
Expand All @@ -26,6 +26,8 @@
import warnings
import unittest

from typing import List, Union

# cannot call this base, because when audioSearch.__init__.py
# imports * from base, it overwrites audioSearch!
from music21 import base
Expand Down Expand Up @@ -57,7 +59,6 @@ def histogram(data, bins):
the last element (-1) is the end of the last bin, and every remaining element (i)
is the dividing point between one bin and another.
>>> data = [1, 1, 4, 5, 6, 0, 8, 8, 8, 8, 8]
>>> outputData, bins = audioSearch.histogram(data,8)
>>> print(outputData)
Expand Down Expand Up @@ -503,25 +504,87 @@ def detectPitchFrequencies(freqFromAQList, useScale=None):
return detectedPitchesFreq


def smoothFrequencies(detectedPitchesFreq, smoothLevels=7, inPlace=True):
def smoothFrequencies(
frequencyList: List[Union[int, float]],
*,
smoothLevels=7,
inPlace=False
) -> List[int]:
'''
Smooths the shape of the signal in order to avoid false detections in the fundamental
frequency.
frequency. Takes in a list of ints or floats.
The second pitch below is obviously too low. It will be smoothed out...
>>> inputPitches = [440, 220, 440, 440, 442, 443, 441, 470, 440, 441, 440,
... 442, 440, 440, 440, 397, 440, 440, 440, 442, 443, 441,
... 440, 440, 440, 440, 440, 442, 443, 441, 440, 440]
>>> result = audioSearch.smoothFrequencies(inputPitches)
>>> print(result)
>>> result
[409, 409, 409, 428, 435, 438, 442, 444, 441, 441, 441,
441, 434, 433, 432, 431, 437, 438, 439, 440, 440, 440,
440, 440, 440, 441, 441, 441, 441, 441, 441, 441]
TODO: rename inPlace because that's not really what it does...
Original list is unchanged:
>>> inputPitches[1]
220
Different levels of smoothing have different effects. At smoothLevel=2,
the isolated 220hz sample is pulling down the samples around it:
>>> audioSearch.smoothFrequencies(inputPitches, smoothLevels=2)[:5]
[330, 275, 358, 399, 420]
Doing this enough times will smooth out a lot of inconsistencies.
>>> audioSearch.smoothFrequencies(inputPitches, smoothLevels=28)[:5]
[432, 432, 432, 432, 432]
If inPlace is True then the list is modified in place and nothing is returned:
>>> audioSearch.smoothFrequencies(inputPitches, inPlace=True)
>>> inputPitches[:5]
[409, 409, 409, 428, 435]
Note that `smoothLevels=1` is the baseline that does nothing:
>>> audioSearch.smoothFrequencies(inputPitches, smoothLevels=1) == inputPitches
True
And less than 1 raises a ValueError:
>>> audioSearch.smoothFrequencies(inputPitches, smoothLevels=0)
Traceback (most recent call last):
ValueError: smoothLevels must be >= 1
There cannot be more smoothLevels than input frequencies:
>>> audioSearch.smoothFrequencies(inputPitches, smoothLevels=40)
Traceback (most recent call last):
ValueError: There cannot be more smoothLevels (40) than inputPitches (32)
Note that the system runs on O(smoothLevels * len(frequenciesList)),
so additional smoothLevels can be costly on a large set.
This function always returns a list of ints -- rounding to the nearest
hertz (you did want it smoothed right?)
Changed in v.6 -- inPlace defaults to False (like other music21
functions) and if done in Place, returns nothing. smoothLevels and inPlace
became keyword only.
'''
dpf = detectedPitchesFreq
if smoothLevels < 1:
raise ValueError('smoothLevels must be >= 1')

numFreqs = len(frequencyList)
if smoothLevels > numFreqs:
raise ValueError(
f'There cannot be more smoothLevels ({smoothLevels}) than inputPitches ({numFreqs})'
)

dpf = frequencyList
if inPlace:
detectedPitchesFreq = dpf
else:
Expand All @@ -532,23 +595,27 @@ def smoothFrequencies(detectedPitchesFreq, smoothLevels=7, inPlace=True):
ends = 0.0

for i in range(smoothLevels):
beginning = beginning + float(detectedPitchesFreq[i])
ends = ends + detectedPitchesFreq[len(detectedPitchesFreq) - 1 - i]
beginning = beginning + detectedPitchesFreq[i]
ends = ends + detectedPitchesFreq[numFreqs - 1 - i]
beginning = beginning / smoothLevels
ends = ends / smoothLevels

for i in range(len(detectedPitchesFreq)):
for i in range(numFreqs):
if i < int(math.floor(smoothLevels / 2.0)):
detectedPitchesFreq[i] = beginning
elif i > len(detectedPitchesFreq) - int(math.ceil(smoothLevels / 2.0)) - 1:
elif i > numFreqs - int(math.ceil(smoothLevels / 2.0)) - 1:
detectedPitchesFreq[i] = ends
else:
t = 0
for j in range(smoothLevels):
t = t + detectedPitchesFreq[i + j - int(math.floor(smoothLevels / 2.0))]
detectedPitchesFreq[i] = t / smoothLevels
# return detectedPitchesFreq
return [int(round(fq)) for fq in detectedPitchesFreq]

for i in range(numFreqs):
detectedPitchesFreq[i] = int(round(detectedPitchesFreq[i]))

if not inPlace:
return detectedPitchesFreq


# ------------------------------------------------------
Expand Down
29 changes: 27 additions & 2 deletions music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<class 'music21.base.Music21Object'>
>>> music21.VERSION_STR
'6.0.3a3'
'6.0.5a1'
Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down Expand Up @@ -374,6 +374,10 @@ def __init__(self, *arguments, **keywords):
self._duration = None # type: Optional['music21.duration.Duration']
self._priority = 0 # default is zero

# store cached values here:
self._cache: Dict[str, Any] = {}


if 'id' in keywords:
self.id = keywords['id']
else:
Expand Down Expand Up @@ -441,7 +445,7 @@ def _deepcopySubclassable(self: _M21T,
TODO: move to class attributes to cache.
'''
defaultIgnoreSet = {'_derivation', '_activeSite', 'id',
'sites', '_duration', '_style'}
'sites', '_duration', '_style', '_cache'}
if ignoreAttributes is None:
ignoreAttributes = defaultIgnoreSet
else:
Expand Down Expand Up @@ -754,6 +758,26 @@ def derivation(self) -> Derivation:
def derivation(self, newDerivation: Optional[Derivation]) -> None:
self._derivation = newDerivation

def clearCache(self, **keywords):
'''
A number of music21 attributes (especially with Chords and RomanNumerals, etc.)
are expensive to compute and are therefore cached. Generally speaking
objects are responsible for making sure that their own caches are up to date,
but a power user might want to do something in an unusual way (such as manipulating
private attributes on a Pitch object) and need to be able to clear caches.
That's what this is here for. If all goes well, you'll never need to call it
unless you're expanding music21's core functionality.
**keywords is not used in Music21Object but is included for subclassing.
Look at :ref:`music21.common.decorators.cacheMethod` for the other half of this
utility.
New in v.6 -- exposes previously hidden functionality.
'''
self._cache = {}

def getOffsetBySite(self, site, stringReturns=False) -> Union[float, fractions.Fraction, str]:
'''
If this class has been registered in a container such as a Stream,
Expand Down Expand Up @@ -2434,6 +2458,7 @@ def informSites(self, changedInformation=None):
'''
for s in self.sites.get():
if hasattr(s, 'coreElementsChanged'):
# noinspection PyCallingNonCallable
s.coreElementsChanged(updateIsFlat=False, keepIndex=True)

def _getPriority(self):
Expand Down
Loading

0 comments on commit 9613a55

Please sign in to comment.