-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed: DissonanceLocator now producing a dataframe results in the sam…
…e format as IntervalIndexer. The dissonance class can now accurately classify fourths and fifths as consonant or dissonant based on what is going on in the other voices.
- Loading branch information
1 parent
8cc8202
commit 96ab239
Showing
1 changed file
with
52 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,74 +26,14 @@ | |
.. codeauthor:: Alexander Morgan | ||
.. codeauthor:: Christopher Antila <christopher@antila.ca> | ||
""" | ||
import six | ||
import pandas | ||
from numpy import nan, isnan # pylint: disable=no-name-in-module | ||
from six.moves import range, xrange # pylint: disable=import-error,redefined-builtin | ||
This comment has been minimized.
Sorry, something went wrong. |
||
from music21 import stream | ||
from vis.analyzers import indexer | ||
|
||
def indexer_func(obj): ##TODO: Ryan, should this function now be deleted? | ||
""" | ||
The function that indexes. | ||
:param obj: The simultaneous event(s) to use when creating this index. (For indexers using a | ||
:class:`Series`). | ||
:type obj: :class:`pandas.Series` of strings | ||
:returns: "-" if the interval in question is regular a consonance, "C4" if a consonant fourth, | ||
"Cd5" if a consonant diminished fifth, or "D" if any other dissonance. | ||
:rtype: str | ||
""" | ||
def indexer_func(obj): | ||
This comment has been minimized.
Sorry, something went wrong.
crantila
Member
|
||
return None | ||
|
||
|
||
class SimulIndexer(indexer.Indexer): | ||
""" | ||
Used internally by the :class:`DissonanceIndexer`. This indexer forward fills the results of the | ||
interval indexer so that the dissonance indexer knows what intervals sound together. This | ||
information is used to classify fourths and fifths as consonant or dissonant. | ||
""" | ||
required_score_type = 'pandas.DataFrame' | ||
|
||
def __init__(self, score, settings=None): | ||
""" | ||
:param score: The output from :class:`~vis.analyzers.indexers.interval.IntervalIndexer`. | ||
You must include interval quality and must use simple intervals. | ||
:type score: :class:`pandas.DataFrame`. | ||
:param settings: This indexer uses no settings, so this is ignored. | ||
:type settings: NoneType | ||
:raises: :exc:`RuntimeError` if ``score`` is the wrong type. | ||
:raises: :exc:`RuntimeError` if ``score`` is not a list of the same types. | ||
""" | ||
super(SimulIndexer, self).__init__(score, None) | ||
self._indexer_func = indexer_func | ||
|
||
def run(self): | ||
""" | ||
Make a new index of the piece in the same format as the :class:`IntervalIndexer` (i.e. a | ||
DataFrame of Series where each series corresponds to the intervals in a given voice pair). | ||
The difference between this and the interval indexer is that this one forward fills the | ||
interval results in order to make the simultaneities at any given offset easily accessible | ||
even if intervals that sound together don't have the same onset or same duration. | ||
:returns: A :class:`DataFrame` of the new indices. The columns have a :class:`MultiIndex`. | ||
:rtype: :class:`pandas.DataFrame` | ||
""" | ||
# Copied from diss_sigs.py script and dissonance indexer in diss_sigs branch. | ||
setts = {u'quality': True, 'simple or compound': u'simple'} | ||
ffilled_intervals = the_piece.get_data([noterest.NoteRestIndexer, interval.IntervalIndexer], setts) | ||
ffilled_intervals = ffilled_intervals.T | ||
new_ints = ffilled_intervals.loc['interval.IntervalIndexer'].fillna(method='ffill', axis=1) | ||
new_multiindex = [('interval.IntervalIndexer', x) for x in list(new_ints.index)] | ||
new_ints.index = pandas.MultiIndex.from_tuples(new_multiindex) | ||
ffilled_intervals.update(new_ints) | ||
ffilled_intervals = ffilled_intervals.T | ||
del new_ints | ||
return ffilled_intervals | ||
|
||
|
||
class DissonanceIndexer(indexer.Indexer): | ||
""" | ||
Indexer that locates vertical dissonances between pairs of voices in a piece. Used internally by | ||
|
@@ -114,10 +54,23 @@ def __init__(self, score, settings=None): | |
:raises: :exc:`RuntimeError` if ``score`` is the wrong type. | ||
:raises: :exc:`RuntimeError` if ``score`` is not a list of the same types. | ||
""" | ||
super(DissonanceIndexer, self).__init__(score, None) | ||
setts = {'quality': True, 'simple or compound': 'simple'} | ||
super(DissonanceIndexer, self).__init__(score, setts) | ||
self._indexer_func = indexer_func | ||
|
||
def check_4s_5s(self, pair_name, start_ind, suspect_diss): | ||
def simul_ints(self, intervalDataFrame): | ||
""" | ||
Creates a 'forwardfilled' version of the passed dataframe and returns it. Eventually this | ||
could be its own class, but since the dissonance the function check_4s_5s is the only place | ||
that should use forward filling (it interferes with results in most other contexts) it is | ||
just a function. | ||
""" | ||
ints = intervalDataFrame.copy(deep=True) | ||
ffilled_ints = ints.ffill() | ||
del ints | ||
return ffilled_ints | ||
|
||
def check_4s_5s(self, pair_name, event_num, start_off, suspect_diss, simuls): | ||
""" | ||
This function evaluates whether P4's, A4's, and d5's should be considered consonant based | ||
whether or not the lower voice of the suspect_diss forms an interval that causes us to deem | ||
|
@@ -127,38 +80,45 @@ def check_4s_5s(self, pair_name, start_ind, suspect_diss): | |
:param pair_name: Name of pair that has the potentially consonant fourth or fifth. | ||
:type pair_name: String in the format '0,2' if the pair in question is S and T in an | ||
SATB texture. | ||
:param start_ind: Index of 4th or 5th being analyzed taken from the index of its voice | ||
pair. | ||
:type start_ind: Integer. | ||
:param event_num: Pandas iloc number of interval's row in dataframe. | ||
:type event_num: Integer. | ||
:param start_off: Beginning offset of 4th or 5th being analyzed. | ||
:type start_off: Integer. | ||
:param suspect_diss: Interval name with quality and direction (i.e. nothing or '-') that | ||
corresponds to the fourth or fifth to be examined. | ||
:type suspect_diss: String. | ||
""" | ||
cons_makers = {'P4':[u'm3', u'M3', u'P5'], 'd5':[u'M6'], 'A4':[u'm3'], | ||
'-P4':[u'm3', u'M3', u'P5'], '-d5':[u'M6'], '-A4':[u'm3']} | ||
diss_dura = interval.IntervalIndexer[voice_pair_index][start_ind].duration.quarterLength | ||
end_ind = start_ind + diss_dura | ||
cons_makers = {'P4':[u'm3', u'M3', u'P5'], 'd5':[u'M6'], 'A4':[u'm3'], '-P4':[u'm3', u'M3', u'P5'], '-d5':[u'M6'], '-A4':[u'm3']} | ||
Xed_makers = {'P4':[u'-m3', u'-M3', u'-P5'], 'd5':[u'-M6'], 'A4':[u'-m3'],'-P4':[u'-m3', u'-M3', u'-P5'], '-d5':[u'-M6'], '-A4':[u'-m3']} | ||
cons_made = False | ||
end_off = start_off # Find the offset of the next event in the voice pair to know when the interval ends. | ||
for index, row in self._score['interval.IntervalIndexer'][start_off:].iterrows(): | ||
if end_off != start_off: | ||
break | ||
elif end_off == start_off: | ||
end_off = self._score['interval.IntervalIndexer'].index[len(self._score['interval.IntervalIndexer'])-1] # for the case where a 4th or 5th is in the last attack of the piece. | ||
elif type(row[pair_name]) is unicode: | ||
end_off = self._score['interval.IntervalIndexer'].index[event_num + index] | ||
|
||
if '-' in suspect_diss: | ||
lower_voice = pair_name.split(',')[0] | ||
else: | ||
lower_voice = pair_name.index.split(',')[1] | ||
lower_voice = pair_name.split(',')[1] | ||
|
||
for cn, voice_combo in enumerate(interval.IntervalIndexer.columns): | ||
if lower_voice == voice_combo[1].split(',')[0] and voice_combo[1] != pair_name: # look at other pairs that have lower_voice as their upper voice. Could be optimized. | ||
if any(SimulIndexer[SimulIndexer.columns[cn]][start_ind:end_ind] in cons_makers[suspect_diss]): | ||
for voice_combo in self._score['interval.IntervalIndexer']: | ||
if lower_voice == voice_combo.split(',')[0] and voice_combo != pair_name: # look at other pairs that have lower_voice as their upper voice. Could be optimized. | ||
if simuls[voice_combo][start_off:end_off].any() in cons_makers[suspect_diss]: | ||
cons_made = True | ||
break | ||
elif lower_voice == voice_combo[1].split(',')[1] and voice_combo[1] != pair_name: # look at other pairs that have lower_voice as their lower voice. Could be optimized. | ||
if any(SimulIndexer[SimulIndexer.columns[cn]][start_ind:end_ind][1:] in cons_makers[suspect_diss]): | ||
elif lower_voice == voice_combo.split(',')[1] and voice_combo != pair_name: # look at other pairs that have lower_voice as their lower voice. Could be optimized. | ||
if simuls[voice_combo][start_off:end_off].any() in Xed_makers[suspect_diss]: | ||
cons_made = True | ||
break | ||
|
||
if cons_made: # 'C' is for consonant and it's good enough for me. | ||
return ('C' + suspect_diss) | ||
else: # This 'D' shows that the fourth or fifth analyzed turned out to be truly dissonant. | ||
return ('D' + suspect_diss) | ||
else: # 'D' shows that the fourth or fifth analyzed turned out to be dissonant. | ||
return ('D' + suspect_diss) | ||
|
||
def run(self): | ||
""" | ||
|
@@ -171,21 +131,17 @@ def run(self): | |
:returns: A :class:`DataFrame` of the new indices. The columns have a :class:`MultiIndex`. | ||
:rtype: :class:`pandas.DataFrame` | ||
""" | ||
# To calculate all 2-part combinations: | ||
combinations = self._score['interval.IntervalIndexer'] # TODO: Check with Ryan. Should I pass the required settings here? | ||
consonances = [u'Rest', u'P1', u'm3', u'M3', u'P5', u'm6', u'M6', u'P8', | ||
u'-m3', u'-M3', u'-P5', u'-m6', u'-M6', u'-P8'] | ||
results = self._score['interval.IntervalIndexer'].copy(deep=True) | ||
potential_consonances = [u'P4', u'-P4', u'A4', u'-A4', u'd5', u'-d5'] | ||
|
||
for pair_index in combinations.columns: | ||
for j, event in enumerate(combinations[pair_index]): | ||
if event in consonances: | ||
combinations[pair_index].iloc[j] = nan | ||
elif event in potential_consonances: | ||
combinations[pair_index[j]] = self.check_4s_5s(pair_index[1], combinations.index[j], event) | ||
# NB: all other events are either dissonant or don't qualify as interval onsets. | ||
|
||
# This method returns once all computation is complete. The results are returned as a list | ||
# of Series objects in the same order as the "combinations" argument. | ||
results = self._do_multiprocessing(combinations) | ||
return self.make_return([six.u(x)[1:-1] for x in combinations], results) | ||
simuls = self.simul_ints(self._score['interval.IntervalIndexer']) | ||
for pair_title in results: | ||
for j, event in enumerate(results[pair_title]): | ||
if event in potential_consonances: # NB: all other events are definite consonances or dissonances or don't qualify as interval onsets. | ||
results[pair_title].iloc[j] = self.check_4s_5s(pair_title, j, results.index[j], event, simuls) | ||
|
||
results = results.T # Reapply the multi-index to the results dataframe | ||
new_multiindex = [('dissonance.DissonanceLocator', x) for x in list(results.index)] | ||
results.index = pandas.MultiIndex.from_tuples(new_multiindex) | ||
results = results.T | ||
|
||
return results |
I'd be wary of removing the range/xrange import from six unless you're 100% sure you'll never use those functions in this file. (Even though you aren't using them now).