Skip to content

Commit

Permalink
WIP: Improvement in dissonance detection, not finished.
Browse files Browse the repository at this point in the history
This is a step in the right direction but still not done. An
_explainable label has been added to distinguish between notes
that have not been analyzed and those that have been deemed the
"consonant" note of a dissonant interval. Related to #425.
  • Loading branch information
alexandermorgan committed Sep 13, 2016
1 parent b39fab0 commit a499f0d
Showing 1 changed file with 45 additions and 42 deletions.
87 changes: 45 additions & 42 deletions vis/analyzers/indexers/dissonance.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from numpy import nan # pylint: disable=no-name-in-module
from vis.analyzers import indexer


_d3q_label = 'Q'
_pass_dp_label = 'D'
_pass_rp_label = 'R'
Expand All @@ -44,15 +45,18 @@
_chan_idiom_label = 'H'
_echappee = 'E'
_no_diss_label = '-'
_explainable = '+'
_unexplainable = 'Z'
_only_diss_w_diss = 'O'
_consonances = set(['P1', 'm3', 'M3', 'CP4', 'CA4', 'Cd5', 'P5', 'm6', 'M6', 'P8', '-m3', '-M3', 'C-P4',
'C-A4', 'C-d5', '-P5', '-m6', '-M6', '-P8'])
_potential_consonances = set([u'P4', u'-P4', u'A4', u'-A4', u'd5', u'-d5'])
_cons_makers = {'P4':set([u'm3', u'M3', u'P5']), 'd5':[u'M6'], 'A4':[u'm3'], '-P4':set([u'm3', u'M3', u'P5']), '-d5':[u'M6'], '-A4':[u'm3']}
_Xed_makers = {'P4':set([u'-m3', u'-M3', u'-P5']), 'd5':[u'-M6'], 'A4':[u'-m3'],'-P4':set([u'-m3', u'-M3', u'-P5']), '-d5':[u'-M6'], '-A4':[u'-m3']}
_nan_rest = set([nan, 'Rest'])
_ignored = _consonances.union(_nan_rest)
_go_ons = set([_no_diss_label, _unexplainable])
_passes = set(('n', _no_diss_label, _unexplainable))
_passes = set((nan, _no_diss_label, _unexplainable))
int_ind = u'interval.IntervalIndexer'
diss_ind = u'dissonance.DissonanceLocator'
h_ind = u'interval.HorizontalIntervalIndexer'
Expand Down Expand Up @@ -162,7 +166,7 @@ def _is_passing_or_neigh(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand Down Expand Up @@ -204,33 +208,33 @@ def _is_passing_or_neigh(self, indx, pair, event, prev_event):
dur_x2 = self._score.iat[x2_ind, d_lower_col]
dur_x += dur_x2

if prev_event not in _consonances: # The dissonance is can't be a passing tone.
if prev_event not in _consonances: # The dissonance can't be a passing tone.
return (False,)
elif (((dur_b == 2 and bs_b == .25) or (dur_b <= 1 and bs_b == .125) or
(dur_b <= .5 and bs_b == .0625)) and dur_a >= dur_b and (y is nan or x == 1)):
if b == 2:
if a == 2:
return (True, upper, _pass_rp_label, lower, _no_diss_label)
return (True, upper, _pass_rp_label, lower, _explainable)
elif a == -2:
return (True, upper, _neigh_ln_label, lower, _no_diss_label)
return (True, upper, _neigh_ln_label, lower, _explainable)
elif b == -2:
if a == -2:
return (True, upper, _pass_dp_label, lower, _no_diss_label)
return (True, upper, _pass_dp_label, lower, _explainable)
elif a == 2:
return (True, upper, _neigh_un_label, lower, _no_diss_label)
return (True, upper, _neigh_un_label, lower, _explainable)

elif (((dur_y == 2 and bs_y == .25) or (dur_y <= 1 and bs_y == .125)
or (dur_y <= .5 and bs_y == .0625)) and dur_x >= dur_y and (b is nan or a == 1)):
if y == 2:
if x == 2:
return (True, upper, _no_diss_label, lower, _pass_rp_label)
return (True, upper, _explainable, lower, _pass_rp_label)
elif x == -2:
return (True, upper, _no_diss_label, lower, _neigh_ln_label)
return (True, upper, _explainable, lower, _neigh_ln_label)
elif y == -2:
if x == -2:
return (True, upper, _no_diss_label, lower, _pass_dp_label)
return (True, upper, _explainable, lower, _pass_dp_label)
elif x == 2:
return (True, upper, _no_diss_label, lower, _neigh_un_label)
return (True, upper, _explainable, lower, _neigh_un_label)


return (False,) # The dissonance is not a passing tone.
Expand Down Expand Up @@ -269,7 +273,7 @@ def _is_suspension(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand Down Expand Up @@ -311,9 +315,9 @@ def _is_suspension(self, indx, pair, event, prev_event):

# NB this may need to be tweaked for the edge case where a consonant 4th becomes a dissonant fourth suspension without being restruck.
if (c != 0 and ((a == -2 and b is nan) or (a == 1 and b == -2)) and (bs_y > bs_c or (dur_a == 4 and bs_y >= bs_c))):
return (True, upper, _susp_label, lower, _no_diss_label) # Susp in upper voice
return (True, upper, _susp_label, lower, _explainable) # Susp in upper voice
elif (z != 0 and ((x == -2 and y is nan) or (x == 1 and y == -2)) and (bs_b > bs_z or (dur_x == 4 and bs_b >= bs_z))):
return (True, upper, _no_diss_label, lower, _susp_label) # Susp in lower voice
return (True, upper, _explainable, lower, _susp_label) # Susp in lower voice
return (False,)

def _is_fake_suspension(self, indx, pair, event, prev_event):
Expand Down Expand Up @@ -352,7 +356,7 @@ def _is_fake_suspension(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand Down Expand Up @@ -390,14 +394,14 @@ def _is_fake_suspension(self, indx, pair, event, prev_event):

if a == 2 or a == -2:
if bs_b == .25 and ((b == -2 and dur_b > 2) or (b == 1 and dur_b == 2 and c == -2)):
return (True, upper, _fake_susp_label, lower, _no_diss_label) # Fake susp in upper voice
return (True, upper, _fake_susp_label, lower, _explainable) # Fake susp in upper voice
elif bs_b == .125 and ((b == -2 and dur_b > 1) or (b == 1 and dur_b == 1 and c == -2)):
return (True, upper, _dim_fake_susp_label, lower, _no_diss_label) # Diminished fake susp in upper voice
return (True, upper, _dim_fake_susp_label, lower, _explainable) # Diminished fake susp in upper voice
elif x == 2 or x == -2:
if bs_y == .25 and ((y == -2 and dur_y > 2) or (y == 1 and dur_y == 2 and z == -2)):
return (True, upper, _no_diss_label, lower, _fake_susp_label) # Fake susp in lower voice
return (True, upper, _explainable, lower, _fake_susp_label) # Fake susp in lower voice
elif bs_y == .125 and ((y == -2 and dur_y > 1) or (y == 1 and dur_y == 1 and z == -2)):
return (True, upper, _no_diss_label, lower, _dim_fake_susp_label) # Diminished fake susp in lower voice
return (True, upper, _explainable, lower, _dim_fake_susp_label) # Diminished fake susp in lower voice
return (False,)

def _is_d3q(self, indx, pair, event, prev_event):
Expand Down Expand Up @@ -431,7 +435,7 @@ def _is_d3q(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand Down Expand Up @@ -459,9 +463,9 @@ def _is_d3q(self, indx, pair, event, prev_event):

# TODO: make the beatstrength requirements dependent on the detected meter. Right now it is hard-coded for 4/2 meter.
if bs_b == .25 and dur_a >= 2 and dur_b == 1 and a == -2 and b == -2: # Upper voice is d3q
return (True, upper, _d3q_label, lower, _no_diss_label)
return (True, upper, _d3q_label, lower, _explainable)
elif bs_y == .25 and dur_x >= 2 and dur_y == 1 and x == -2 and y == -2: # Lower voice is d3q
return (True, upper, _no_diss_label, lower, _d3q_label)
return (True, upper, _explainable, lower, _d3q_label)
else: # The dissonance is not a d3q.
return (False,)

Expand Down Expand Up @@ -496,7 +500,7 @@ def _is_anticipation(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand All @@ -521,9 +525,9 @@ def _is_anticipation(self, indx, pair, event, prev_event):
bs_y = self._score.iat[indx, bs_lower_col]

if (bs_b == .125 and a == -2 and b == 1 and dur_b == 1):
return (True, upper, _ant_label, lower, _no_diss_label)
return (True, upper, _ant_label, lower, _explainable)
elif (bs_y == .125 and x == -2 and y == 1 and dur_y == 1):
return (True, upper, _no_diss_label, lower, _ant_label)
return (True, upper, _explainable, lower, _ant_label)
return (False,)

def _is_cambiata(self, indx, pair, event, prev_event):
Expand Down Expand Up @@ -556,7 +560,7 @@ def _is_cambiata(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
""" # QUESTION: is b on a weak half even if it lasts a quarter note?
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand Down Expand Up @@ -594,9 +598,9 @@ def _is_cambiata(self, indx, pair, event, prev_event):


if (a == -2 and ((dur_b == 2 and bs_b == .25) or (dur_b == 1 and bs_b == .125) and b == -3 and c == 2)):
return (True, upper, _camb_label, lower, _no_diss_label) # Cambiata in upper voice
return (True, upper, _camb_label, lower, _explainable) # Cambiata in upper voice
elif (x == -2 and ((dur_y == 2 and bs_y == .25) or (dur_y == 1 and bs_y == .125) and y == -3 and z == 2)):
return (True, upper, _no_diss_label, lower, _camb_label) # Cambiata in lower voice
return (True, upper, _explainable, lower, _camb_label) # Cambiata in lower voice
return (False,)

def _is_chanson_idiom(self, indx, pair, event, prev_event): #m.136 in alto? What about diminished lengths?
Expand Down Expand Up @@ -636,7 +640,7 @@ def _is_chanson_idiom(self, indx, pair, event, prev_event): #m.136 in alto? What
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
diss = int(''.join(dig for dig in event if dig.isdigit()), 10) # delete all non-digit characters from event string and convert to int.

Expand Down Expand Up @@ -690,10 +694,10 @@ def _is_chanson_idiom(self, indx, pair, event, prev_event): #m.136 in alto? What

if ((diss == 2 or diss == -7) and dur_b == 1 and ((y == -2 and dur_y > 2) or (y == 1 and dur_y == 2)) and
a == -2 and b == -2 and c == 2 and dur_c == 1 and dur_d >= 2):
return (True, upper, _chan_idiom_label, lower, _no_diss_label) # Chanson idiom in upper voice
return (True, upper, _chan_idiom_label, lower, _explainable) # Chanson idiom in upper voice
if ((diss == -2 or diss == 7) and dur_y == 1 and ((b == -2 and dur_b > 2) or (b == 1 and dur_b == 2)) and
x == -2 and y == -2 and z == 2 and dur_z == 1 and dur_z2 >= 2):
return (True, upper, _no_diss_label, lower, _chan_idiom_label) # Chanson idiom in lower voice
return (True, upper, _explainable, lower, _chan_idiom_label) # Chanson idiom in lower voice
return (False,)

def _is_echappee(self, indx, pair, event, prev_event):
Expand Down Expand Up @@ -726,7 +730,7 @@ def _is_echappee(self, indx, pair, event, prev_event):
does not find a suspension the function returns a 1-tuple with False as the argument.
:rtype: tuple
"""
if prev_event == None:
if prev_event is None:
return (False,)
upper = pair.split(',')[0] # Upper voice variables
h_upper_col = self._score.columns.get_loc((h_ind, upper))
Expand All @@ -751,9 +755,9 @@ def _is_echappee(self, indx, pair, event, prev_event):
bs_y = self._score.iat[indx, bs_lower_col]

if bs_b == .125 and ((a == 2 and b < -2) or (a == -2 and b > 2)): # Upper note échappée
return (True, upper, _echappee, lower, _no_diss_label)
return (True, upper, _echappee, lower, _explainable)
if bs_y == .125 and ((x == 2 and y < -2) or (x == -2 and y > 2)): # Lower note échappée
return (True, upper, _no_diss_label, lower, _echappee)
return (True, upper, _explainable, lower, _echappee)
return (False,)


Expand Down Expand Up @@ -865,8 +869,6 @@ def check_4s_5s(self, pair_name, iloc_indx, suspect_diss, simuls):
consonant or dissonant respectively.
:rtype: string
"""
cons_makers = {'P4':set([u'm3', u'M3', u'P5']), 'd5':[u'M6'], 'A4':[u'm3'], '-P4':set([u'm3', u'M3', u'P5']), '-d5':[u'M6'], '-A4':[u'm3']}
Xed_makers = {'P4':set([u'-m3', u'-M3', u'-P5']), 'd5':[u'-M6'], 'A4':[u'-m3'],'-P4':set([u'-m3', u'-M3', u'-P5']), '-d5':[u'-M6'], '-A4':[u'-m3']}
cons_made = False
# Find the offset of the next event in the voice pair to know when the interval ends.
end_temp = self._score.loc[:, (int_ind, pair_name)].iloc[iloc_indx +1:].first_valid_index()
Expand All @@ -882,11 +884,11 @@ def check_4s_5s(self, pair_name, iloc_indx, suspect_diss, simuls):

for voice_combo in simuls:
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].iloc[iloc_indx:end_iloc].any() in cons_makers[suspect_diss]: # this chained-indexing is actually faster than the alternative.
if simuls[voice_combo].iloc[iloc_indx:end_iloc].any() in _cons_makers[suspect_diss]: # this chained-indexing is actually faster than the alternative.
cons_made = True
break
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].iloc[iloc_indx:end_iloc].any() in Xed_makers[suspect_diss]:
if simuls[voice_combo].iloc[iloc_indx:end_iloc].any() in _Xed_makers[suspect_diss]:
cons_made = True
break

Expand Down Expand Up @@ -916,8 +918,7 @@ def run(self):
iterables = [[diss_types], self._score[dur_ind].columns]
d_types_multi_index = pandas.MultiIndex.from_product(iterables, names = ['Indexer', 'Parts'])
ret = pandas.DataFrame(index=self._score.index, columns=d_types_multi_index)
ret.fillna('n', inplace=True) # replace NaNs with 'n's so that they can be checked for more easily


for col, pair_title in enumerate(diss_ints.columns):
voices = pair_title.split(',') # assign top and bottom voices as integers
top_voice = int(min(voices))
Expand All @@ -931,7 +932,7 @@ def run(self):
if (event not in _ignored and ret.iat[i, top_voice] in _passes
and ret.iat[i, bott_voice] in _passes):
prev_event = diss_ints[pair_title].iloc[:i].last_valid_index()
if prev_event != None:
if prev_event is not None:
prev_event = diss_ints.at[prev_event, pair_title]
# if prev_event not in _consonances and i > 0 and (ret.iat[i-1, top_voice] in
# (_pass_rp_label, _pass_dp_label) or ret.iat[i-1, bott_voice] in
Expand All @@ -940,7 +941,9 @@ def run(self):
diss_analysis = self.classify(i, pair_title, event, prev_event)
ret.iat[i, int(diss_analysis[1], 10)] = diss_analysis[2]
ret.iat[i, int(diss_analysis[3], 10)] = diss_analysis[4]
ret.replace('n', _no_diss_label, inplace=True)

ret.replace(_explainable, _no_diss_label, inplace=True)
ret.fillna(_no_diss_label, inplace=True)

# Remove lingering unexplainable labels from notes that are only dissonant against identifiable dissonances.
unknowns = numpy.where(ret.values == _unexplainable) # 2-tuple of a list of iloc indecies and a list of corresponding voice integers.
Expand Down

0 comments on commit a499f0d

Please sign in to comment.