Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Chord caching improvements and better Caching across M21 #550

Merged
merged 5 commits into from
May 30, 2020
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
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