Source code for vis.analyzers.indexers.dissonance

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------- #
# Program Name:           vis
# Program Description:    Helps analyze music with computers.
#
# Filename:               analyzers/indexers/template.py
# Purpose:                Template indexer
#
# Copyright (C) 2013-2016 Christopher Antila, Alexander Morgan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public 
# License along with this program.  If not, see 
# <http://www.gnu.org/licenses/>.
# -------------------------------------------------------------------- #
"""
.. codeauthor:: Alexander Morgan
.. codeauthor:: Christopher Antila <christopher@antila.ca>

"""
import pandas
import numpy
from numpy import nan  # pylint: disable=no-name-in-module
from vis.analyzers import indexer
from multi_key_dict import multi_key_dict as mkd
import pdb


_d3q_label = 'Q'
_pass_dp_label = 'D'
_pass_rp_label = 'R'
_neigh_ln_label = 'L'
_neigh_un_label = 'U'
_susp_label = 'S'
_fake_susp_label = 'F'
_dim_fake_susp_label = 'f'
_ant_label = 'A'
_camb_label = 'C'
_chan_idiom_label = 'H'
_echappee = 'E'
_no_diss_label = '-'
_unexplainable = 'Z'
_only_diss_w_diss = 'O'
_weights = mkd({(
    _d3q_label, 
    _pass_dp_label, 
    _pass_rp_label, 
    _neigh_ln_label, 
    _neigh_un_label,
    _susp_label, 
    _fake_susp_label, 
    _dim_fake_susp_label, 
    _ant_label, 
    _camb_label,
    _chan_idiom_label, _echappee): 3, 
    _no_diss_label: 2, 
    _unexplainable: 1
})
_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(['Rest'])
_go_ons = set([_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'
bs_ind = u'meter.NoteBeatStrengthIndexer'
dur_ind = u'meter.DurationIndexer'
diss_types = u'dissonance.DissonanceIndexer'


[docs]class DissonanceIndexer(indexer.Indexer): """ Indexer that locates vertical dissonances between pairs of voices in a piece. It then categorizes intervals as consonant or dissonant and in the case of fourths (perfect or augmented) and diminished fifths it examines the other parts sounding with that fourth or fifth (if there are any) to see if the interval can be considered consonant. This dissonance analysis allows for the assignment of a dissonance type name or a consonance label for each voice at each offset. This last step is the DataFrame that gets returned. The score type must be a list of dataframes of the results of the following indexers (order matters): beatstrength, duration, horizontal, vertical. To simplify things, however, it is better to use the ``get_data()`` on an ``indexed_piece`` object to get results from the dissonance indexer as per the example below. The results of the disonance indexer are in the form of one cell in the resultant dataframe in each part at every offset where there is a new note, rest, or chord onset in any part. These cells contain one-character strings which are an abbreviation of what the dissonance type for that part at that moment is. The abbreviations correspoond to the following dissonance types: - 'Q': Dissonant third quarter (special type of accented passing tone) - 'D': Descending passing tone - 'R': Ascending ("rising") passing tone - 'L': Lower neighbour - 'U': Upper neighbour - 'S': Suspension - 'F': Fake suspension - 'f': Diminished fake suspension - 'A': Anticipation - 'C': Nota cambiata - 'H': Chanson idiom - 'E': Echappée (escape tone) - '-': Either no dissonance, or the part in question is not considered to be the dissonant note of the dissonance it's in **Example:** >>> from vis.models.indexed_piece import Importer >>> ip = Importer('symbolic_notation_file_location.xml') >>> ip.get_data('dissonance') """ 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 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(DissonanceIndexer, self).__init__(score) self._score = pandas.concat(score, axis=1) def _set_horiz_invl(self, indx, col_indx): """ Assigns the horizontal interval of the passed voice at the index passed. """ if self._score.iat[indx, col_indx] in _nan_rest: horiz_int = self._score.iat[indx, col_indx] else: horiz_int = int(self._score.iat[indx, col_indx], 10) return horiz_int def _is_passing_or_neigh(self, indx, pair, event, prev_event): """ A *passing tone* moves by step obliquely (i.e. the other voice stands still while this one moves) creating a dissonant interval, continues stepwise in the same direction without resting, AND satisfies one of the following: * it is on a weak half, preceded by a half or longer, and its duration is one half note, OR * it is on a weak quarter and its duration is one quarter or less, OR * it is on a weak eighth and its duration is one eighth or less. A neighbour tone moves obliquely by step creating a dissonant interval, without resting, by changing direction and returning by step to the same note that preceded it AND meets one of the above metric requirements. Passing and neighbour tone detection have been grouped to improve analysis speed because their requirements are almost identical. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. The labels assigned can be modified by changing the file-wide labels at the top of this file. The variables assigned at the beginning of this and all the other dissonance type definition functions are as follows (note that not all appear in each function): a2 == horizontal motion of the upper voice into the note or rest at a_ind a == horizontal motion of upper voice into dissonance b == horizontal motion of upper voice out of dissonance c == horizontal motion of upper voice out of the note or rest after the dissonance x2, x, y, z, z2 correspond to a2, a, b, c, d respectively but for the lower voice. ``h_upper_col``, ``d_upper_col``, and ``bs_upper_col`` correspond to the column indecies in ``self._score`` where the ``horizontal``, ``duration``, and ``beatStrength`` information of the upper voice can be found. These columns are also calculated for the lower voice, replacing 'upper' with 'lower'. 'letter'_temp == label-based index letter's row position 'letter'_ind == int-based index of letter's row position dur_'letter' == duration of note or rest at the passed position bs_'letter' == beatStrength of note or rest at the passed position :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method check_4s_5s. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or None if there was no previous event. :returns: If it finds a suspension a five-tuple with True as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with False as the argument. :rtype: tuple """ if prev_event is None: return (False,) # Upper voice variables upper = pair.split(',')[0] h_upper_col = self._score.columns.get_loc((h_ind, upper)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) dur_a = self._score.iat[a_ind, d_upper_col] dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] if dur_a < dur_b: a2_temp = self._score.iloc[:a_ind, h_upper_col].last_valid_index() a2_ind = numpy.where(self._score.index == a2_temp)[0][0] if a2_ind != None: a2 = self._set_horiz_invl(a2_ind, h_upper_col) if a2 == 1: dur_a2 = self._score.iat[a2_ind, d_upper_col] dur_a += dur_a2 # Lower voice variables lower = pair.split(',')[1] h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) dur_x = self._score.iat[x_ind, d_lower_col] dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] if dur_x < dur_y: x2_temp = self._score.iloc[:x_ind, h_lower_col].last_valid_index() x2_ind = numpy.where(self._score.index == x2_temp)[0][0] if x2_ind != None: x2 = self._set_horiz_invl(x2_ind, h_lower_col) if x2 == 1: dur_x2 = self._score.iat[x2_ind, d_lower_col] dur_x += dur_x2 # The dissonance can't be a passing tone. if prev_event not in _consonances: 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) elif a == -2: return (True, upper, _neigh_ln_label, lower, _no_diss_label) elif b == -2: if a == -2: return (True, upper, _pass_dp_label, lower, _no_diss_label) elif a == 2: return (True, upper, _neigh_un_label, lower, _no_diss_label) 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) elif x == -2: return (True, upper, _no_diss_label, lower, _neigh_ln_label) elif y == -2: if x == -2: return (True, upper, _no_diss_label, lower, _pass_dp_label) elif x == 2: return (True, upper, _no_diss_label, lower, _neigh_un_label) return (False,) # The dissonance is not a passing tone. def _is_suspension(self, indx, pair, event, prev_event): """ A note is considered a *suspension* if it is sustained or re-attacked on the same pitch while another voice enters or moves by step or by leap to create a dissonant interval AND: * its next move (the "resolution") is downwards by step without resting (it may restrike the same note before resolving, though), AND * the resolution is on a weaker beat than the dissonance, or, if the dissonant note is a whole note in duration, the resolution is on a weaker-or-equally-strong beat. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or None if there was no previous event. :returns: If it finds a suspension a five-tuple with True as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with False as the argument. :rtype: tuple """ 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)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) # NB b doesn't correspond to a note onset in upper-voice # suspensions dur_a = self._score.iat[a_ind, d_upper_col] dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] c_ind = 0 c = 0 c_temp = self._score.iloc[indx + 1:, h_upper_col].first_valid_index() if c_temp != None: c_ind = numpy.where(self._score.index == c_temp)[0][0] c = self._set_horiz_invl(c_ind, h_upper_col) bs_c = self._score.iat[c_ind, bs_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) # NB y doesn't correspond to a note onset in lower-voice # suspensions dur_x = self._score.iat[x_ind, d_lower_col] dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] z_ind = 0 z = 0 z_temp = self._score.iloc[indx + 1:, h_lower_col].first_valid_index() if z_temp != None: z_ind = numpy.where(self._score.index == z_temp)[0][0] z = self._set_horiz_invl(z_ind, h_lower_col) bs_z = self._score.iat[z_ind, bs_lower_col] # 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 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 (False,) def _is_fake_suspension(self, indx, pair, event, prev_event): """ A *fake suspension* (more accurately a "*fake preparation*") is a dissonant preparation to a suspension. It is moved to by step obliquely and becomes a dissonant suspension by being tied to a dissonant note (or followed by the same note) whose next move is down by step, with a duration of either: * a weak half, with the ensuing suspended note (or part of note) falling on the following downbeat, OR * (diminished fake suspension) a weak quarter, with the ensuing suspended note (or part of note) falling on the following strong quarter. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or None if there was no previous event. :returns: If it finds a fake suspension a five-tuple with ``True`` as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with False as the argument. :rtype: tuple """ 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)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) # NB b doesn't correspond to a note onset in upper-voice # suspensions dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] c_ind = 0 c = 0 c_temp = self._score.iloc[indx + 1:, h_upper_col].first_valid_index() if c_temp != None: c_ind = numpy.where(self._score.index == c_temp)[0][0] c = self._set_horiz_invl(c_ind, h_upper_col) lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) # NB y doesn't correspond to a note onset in lower-voice # suspensions dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] z_ind = 0 z = 0 z_temp = self._score.iloc[indx + 1:, h_lower_col].first_valid_index() if z_temp != None: z_ind = numpy.where(self._score.index == z_temp)[0][0] z = self._set_horiz_invl(z_ind, h_lower_col) 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 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 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 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 (False,) def _is_d3q(self, indx, pair, event, prev_event): """ A legal "*dissonant 3rd quarter*" is a dissonant 1 on a weak half, approached by step from above and preceded by a 2 or longer, and continuing by step in the same direction. The suspect dissonance occurs at the indx passed. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or None if there was no previous event. :returns: If it finds a dissonant 3rd quarter a five-tuple with True as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with ``False`` as the argument. :rtype: tuple """ 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)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) dur_a = self._score.iat[a_ind, d_upper_col] dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) dur_x = self._score.iat[x_ind, d_lower_col] dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] ''' .. 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) 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) else: # The dissonance is not a d3q. return (False,) def _is_anticipation(self, indx, pair, event, prev_event): """ An anticipation occurs on a weak quarter-note, is approached obliquely by step from above, and is followed immediately (i.e. on the strong quarter, which might be a downbeat or a weak half) by the same pitch. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method check_4s_5s. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or None if there was no previous event. :returns: If it finds an anticipation a five-tuple with ``True`` as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with ``False`` as the argument. :rtype: tuple """ 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)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) dur_y = self._score.iat[indx, d_lower_col] 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) elif (bs_y == .125 and x == -2 and y == 1 and dur_y == 1): return (True, upper, _no_diss_label, lower, _ant_label) return (False,) def _is_cambiata(self, indx, pair, event, prev_event): """ A *nota cambiata* figure moves obliquely by descending step to a dissonant weak half or quarter then skips down a third before ascending by step to the note skipped over. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or ``None`` if there was no previous event. :returns: If it finds a nota cambiata a five-tuple with ``True`` as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis 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 is None: return (False,) upper = pair.split(',')[0] # Upper voice variables h_upper_col = self._score.columns.get_loc((h_ind, upper)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) # NB b doesn't correspond to a note onset in upper-voice # suspensions dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] c_ind = 0 c = 0 c_temp = self._score.iloc[indx + 1:, h_upper_col].first_valid_index() if c_temp != None: c_ind = numpy.where(self._score.index == c_temp)[0][0] c = self._set_horiz_invl(c_ind, h_upper_col) lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) # NB y doesn't correspond to a note onset in lower-voice #suspensions dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] z_ind = 0 z = 0 z_temp = self._score.iloc[indx + 1:, h_lower_col].first_valid_index() if z_temp != None: z_ind = numpy.where(self._score.index == z_temp)[0][0] z = self._set_horiz_invl(z_ind, h_lower_col) 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 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 (False,) #m.136 in alto? What about diminished lengths? def _is_chanson_idiom(self, indx, pair, event, prev_event): """ The *chanson* idiom dissonance consists of a seventh or second, struck simultaneously or obliquely on the 3rd quarter of a whole note, involving a quarter in one voice ("the active voice") and a duration extending past the next downbeat in the other voice ("the inactive voice"), such that: * the active voice, at the 3rd quarter, is approached by descending step and proceeds down by step on the 4th quarter note, sounding the same pitch-class as the inactive voice, then returns upwards by step to a whole note on the downbeat * the inactive voice is extended past the downbeat, becoming a dissonant suspension that resolves down by step. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or ``None`` if there was no previous event. :returns: If it finds a chanson idiom a five-tuple with ``True`` as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with ``False`` as the argument. :rtype: tuple """ 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. upper = pair.split(',')[0] # Upper voice variables h_upper_col = self._score.columns.get_loc((h_ind, upper)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) # NB b doesn't correspond to a note onset in upper-voice # suspensions dur_a = self._score.iat[a_ind, d_upper_col] dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] c_ind = 0 c = 0 c_temp = self._score.iloc[indx + 1:, h_upper_col].first_valid_index() if c_temp != None: c_ind = numpy.where(self._score.index == c_temp)[0][0] c = self._set_horiz_invl(c_ind, h_upper_col) dur_c = self._score.iat[c_ind, d_upper_col] dur_d = 0 d_temp = self._score.iloc[c_ind + 1:, h_upper_col].first_valid_index() if d_temp != None: d_ind = numpy.where(self._score.index == d_temp)[0][0] dur_d = self._score.iat[d_ind, d_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) # NB y doesn't correspond to a note onset in lower-voice # suspensions dur_x = self._score.iat[x_ind, d_lower_col] dur_y = self._score.iat[indx, d_lower_col] bs_y = self._score.iat[indx, bs_lower_col] z_ind = 0 z = 0 z_temp = self._score.iloc[indx + 1:, h_lower_col].first_valid_index() if z_temp != None: z_ind = numpy.where(self._score.index == z_temp)[0][0] z = self._set_horiz_invl(z_ind, h_lower_col) dur_z = self._score.iat[z_ind, d_lower_col] dur_z2 = 0 z2_temp = self._score.iloc[z_ind + 1:, h_lower_col].first_valid_index() if z2_temp != None: z2_ind = numpy.where(self._score.index == z2_temp)[0][0] dur_z2 = self._score.iat[z2_ind, d_lower_col] 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 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 (False,) def _is_echappee(self, indx, pair, event, prev_event): """ A note is considered an *échappée* if it consists of a quarter-note dissonance on a weak quarter note that is approached by step and left by leap in the opposite direction. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method ``check_4s_5s``. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or ``None`` if there was no previous event. :returns: If it finds an *échappée* a five-tuple with ``True`` as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with ``False`` as the argument. :rtype: tuple """ 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)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) bs_upper_col = self._score.columns.get_loc((bs_ind, upper)) a_temp = self._score.iloc[:indx, h_upper_col].last_valid_index() a_ind = numpy.where(self._score.index == a_temp)[0][0] a = self._set_horiz_invl(a_ind, h_upper_col) b = self._set_horiz_invl(indx, h_upper_col) dur_b = self._score.iat[indx, d_upper_col] bs_b = self._score.iat[indx, bs_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) bs_lower_col = self._score.columns.get_loc((bs_ind, lower)) x_temp = self._score.iloc[:indx, h_lower_col].last_valid_index() x_ind = numpy.where(self._score.index == x_temp)[0][0] x = self._set_horiz_invl(x_ind, h_lower_col) y = self._set_horiz_invl(indx, h_lower_col) dur_y = self._score.iat[indx, d_lower_col] 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) 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 (False,) def _is_unexplainable(self, indx, pair, event, prev_event): """ Neither note in the dissonant interval can be explained as one of the above. The voice that moved to the dissonance (if only one voice moved) will be taken to be the dissonant one. If they moved together to the dissonance the note that leaves the dissonance first will be labeled as the dissonant one. If they move to and from the dissonance together they will both be labeled dissonant. This function takes four parameters to situate the analysis in a specific pair of voices at a given moment in the piece and returns the results of that analysis. By this point, none of the known dissonance types have been detected so this function determines which voice of the pair should get the dissonance label, or whether both should. For the meaning of the numerous variables assigned at the beginning of this function, please refer to the doc string of ``_is_passing_or_neigh``. The labels assigned can be modified by changing the file-wide labels at the top of this file. :param indx: The position-based (iloc) index of the dissonance being analyzed. :type indx: int :param pair: name of the voice pair in which the dissonance happens. :type pair: string with the lower numbered voice first and a comma separating the two voices. :param event: the interval that has been analyzed as a dissonance to be classified. :type event: string of dissonant interval with quality. All fourths and fifths should be dissonant fourths and fifths based on the analysis of method check_4s_5s. :param prev_event: previous event in the same voice pair which may be a consonance, a dissonance, a rest, or non-existent if there was no previous event in the pair. :type prev_event: string of previous event in the same voice pair or ``None`` if there was no previous event. :returns: A five-tuple with True as the first argument, the upper-voice number stored as a string as the second argument, the label to assign the upper voice as the third argument, the lower-voice number as a string as the fourth argument, and the label to assign the lower voice as the fifth argument. If the analysis does not find a suspension the function returns a 1-tuple with False as the argument. :rtype: tuple """ upper = pair.split(',')[0] # Upper voice variables h_upper_col = self._score.columns.get_loc((h_ind, upper)) d_upper_col = self._score.columns.get_loc((dur_ind, upper)) b = self._set_horiz_invl(indx, h_upper_col) dur_b = self._score.iat[indx, d_upper_col] lower = pair.split(',')[1] # Lower voice variables h_lower_col = self._score.columns.get_loc((h_ind, lower)) d_lower_col = self._score.columns.get_loc((dur_ind, lower)) y = self._set_horiz_invl(indx, h_lower_col) dur_y = self._score.iat[indx, d_lower_col] if b is not nan and y is nan: # Upper voice is diss return (True, upper, _unexplainable, lower, _no_diss_label) elif y is not nan and b is nan: # Lower voice is diss return (True, upper, _no_diss_label, lower, _unexplainable) if b is not nan and y is not nan: if dur_b < dur_y: # Upper voice is diss return (True, upper, _unexplainable, lower, _no_diss_label) elif dur_y < dur_b: # Lower voice is diss return (True, upper, _no_diss_label, lower, _unexplainable) return (True, upper, _unexplainable, lower, _unexplainable)
[docs] def classify(self, indx, pair, event, prev_event): """ Checks the dissonance definitions to find a suitable label for the dissonance passed. If no identifiable dissonance type matches, returns an unknown dissonance label. Omits checking the pair if either voice was previously given a known dissonance label still in vigour at the given offset. It only takes its four arguments to pass them on to the dissonance-type functions it calls. :returns: A 5-tuple that can be unpacked to assign separate labels for each voice in the pair. If none of the known dissonance types were detected the 5-tuple will have the labels to mark one or both of the voices as dissonant. :rtype: tuple """ diss_types = [ self._is_passing_or_neigh, self._is_suspension, self._is_d3q, self._is_fake_suspension, self._is_chanson_idiom, self._is_cambiata, self._is_anticipation, self._is_echappee, self._is_unexplainable ] for t in diss_types: result = t(indx, pair, event, prev_event) if result[0]: return result
[docs] def check_4s_5s(self, pair_name, iloc_indx, 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 the fourth or fifth consonant, as determined by the cons_makers list below. The function should be called once for each potentially consonant fourth or fifth. :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 iloc_indx: Pandas iloc number of interval's row in dataframe. :type iloc_indx: 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. :returns: string representing analysis of fourth or fifth in question where a 'C' or 'D' has been prepended to the interval name with quality to show that it was considered consonant or dissonant respectively. :rtype: string """ 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() if end_temp != None: # for the case where a 4th or 5th is in the last attack of # the piece. end_iloc = numpy.where(self._score.index == end_temp)[0][0] else: end_iloc = len(self._score) + 1 if '-' in suspect_diss: # set the voice that is spelled lower # as the lower voice. lower_voice = pair_name.split(',')[0] else: lower_voice = pair_name.split(',')[1] 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. 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]): cons_made = True break if cons_made: # 'C' is for consonant and it's good enough for me. return ('C' + suspect_diss) else: # 'D' shows that the fourth or fifth analyzed turned out to # be dissonant. return ('D' + suspect_diss)
[docs] def run(self): """ Make a new index of the piece which consists of a DataFrame with as many columns as there are voices in the piece. The index is the offset of the dissonance analyses. Another DataFrame (``diss_ints``) is calculated but not returned. It is 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 figures out whether fourths or diminished fifths should be considered consonant for the purposes of dissonance classification. diss_ints is not calculated separately because it essentially consists of the same loop needed for dissonance classification. :returns: A :class:`DataFrame` of the new indices. The columns have a :class:`MultiIndex`. :rtype: :class:`pandas.DataFrame` """ diss_ints = self._score[int_ind].copy(deep=True) simuls = diss_ints.ffill() 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(_no_diss_label, inplace=True) 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)) bott_voice = int(max(voices)) for i, event in enumerate(diss_ints[pair_title]): if event in _potential_consonances: # NB: all other events are definite consonances or # dissonances or don't qualify as interval onsets. event = self.check_4s_5s(pair_title, i, event, simuls) diss_ints.iat[i, col] = event # The interval must be dissonant and neither voice # should already have a dissonance label assigned. 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 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 # (_pass_rp_label, _pass_dp_label)): # prev_event = 'm3' # If there's a passing tone at the preceding note, # call the prev_event a consonance (any consonance # will do) so that there can be two passing tones in # a row. diss_analysis = self.classify(i, pair_title, event, prev_event) if (_weights[ret.iat[i, int(diss_analysis[1], 10)]] < _weights[diss_analysis[2]]): ret.iat[i, int(diss_analysis[1], 10)] = diss_analysis[2] if (_weights[ret.iat[i, int(diss_analysis[3], 10)]] < _weights[diss_analysis[4]]): ret.iat[i, int(diss_analysis[3], 10)] = diss_analysis[4] ''' # 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. for x, ndx in enumerate(unknowns[0]): passable = True for col, pair in enumerate(diss_ints.columns): v_to_check = pair.split(',') if str(unknowns[1][x]) not in v_to_check: # does this pair include the voice with the # unexplained dissonance? continue if diss_ints.iat[ndx, col] in _ignored: # go to the next line if this pair was a dissonance, # otherwise continue. continue v_to_check.remove(str(unknowns[1][x])) # remove voice with unexplained dissonance to see what # the other voice is doing. v_temp = self._score.iloc[:ndx + 1, int(v_to_check[0], 10)].last_valid_index() # since the h_ind is the first df in the concat list, a # voice's integer works to reference its horiz column. v_ndx = numpy.where(self._score.index == v_temp)[0][0] go_on = False for event in range(v_ndx, ndx + 1): if ret.iat[event, int(v_to_check[0])] not in _go_ons: go_on = True break if go_on: continue passable = False break if passable: ret.iat[ndx, unknowns[1][x]] = _only_diss_w_diss ''' return ret