Source code for vis.analyzers.indexers.contour
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------- #
# Program Name: vis
# Program Description: Helps analyze music with computers.
#
# Filename: analyzers/indexers/contour.py
# Purpose: Contour Indexer
#
# Copyright (C) 2016 Marina Borsodi-Benson
#
# 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:: Marina Borsodi-Benson <marinaborsodibenson@gmail.com>
.. todo:: Properly document the ``COM_matrix`` and ``compare``
functions.
"""
from vis.analyzers import indexer
import music21
import pandas
[docs]def COM_matrix(contour):
"""
Creates a matrix representing the contour given.
"""
com = []
contour = contour.replace(' ', '')
contour = contour.replace('[', '')
contour = contour.replace(']', '')
contour = contour.split(',')
for x in contour:
com.append([])
for i in range(len(contour)):
for x in range(len(contour)):
if contour[i] == contour[x]:
com[i].append("0")
elif contour[i] > contour[x]:
com[i].append("-")
else:
com[i].append("+")
return com
[docs]def getContour(notes):
"""
Method used internally by the ``ContourIndexer`` class to convert
pitches into contour numbers.
"""
contour = list(map(music21.note.Note, notes))
cseg = [0] * len(contour)
for i in range(len(contour)):
notes = []
for x in range(len(contour)):
if ((music21.interval.getAbsoluteHigherNote(contour[i], contour[x]) == contour[i]) and
(contour[i].nameWithOctave != contour[x].nameWithOctave) and
(contour[x].nameWithOctave not in notes)):
notes.append(contour[x].nameWithOctave)
cseg[i] += 1
return str(cseg)
[docs]def compare(contour1, contour2):
"""
Additional method to compare ``COM_matrices``.
"""
count = 0
l = len(contour1)
for row in range(l):
for c1, c2 in zip(contour1[row], contour2[row]):
if c1 == c2:
count += 1
count = float((count - l) / 2)
total = float((l * (l - 1)) / 2)
return count / total
[docs]class ContourIndexer(indexer.Indexer):
"""
Indexes the contours of a given length in a piece, where contour is
a way of numbering the relative heights of pitches, beginning at 0
for the lowest pitch.
Call this indexer via the ``get_data()`` method of either an
``indexed_piece`` object or an ``aggregated_pieces`` object (see
example below). If nothing is passed in the 'data' argument of the
call to ``get_data()``, then the default is to process the
``NoteRestIndexer`` results of the ``indexed_piece`` in question.
You can pass some other DataFrame to the 'data' argument, but it is
discouraged.
:keyword 'length': This is the length of the contour you want to
look at.
:type 'length': int
**Example:**
Prepare an indexed piece:
>>> from vis.models.indexed_piece import Importer
>>> ip = Importer('path_to_piece.xml')
Get the ``ContourIndexer`` results with specified settings and
processing the notes and rests:
>>> notes = ip.get_data('noterest')
>>> contour_setts = {'length': 3}
>>> ip.get_data('contour', data=notes, settings=contour_setts)
"""
required_score_type = 'pandas.DataFrame'
possible_settings = ['length']
_MISSING_LENGTH = 'ContourIndexer requires "length" setting.'
_LOW_LENGTH = 'Setting "length" must have a value of at least 1.'
def __init__(self, score, settings=None):
"""
:param score: The input from which to produce the contour
indexer results.
:type score: :class:`pandas.DataFrame`
:param settings: All the settings required by this indexer.
:type settings: dict or None
:raises: :exc:`TypeError` if the ``score`` argument is the wrong
type.
:raises: :exc:`RuntimeError` if the required settings are not
present in the ``settings`` argument.
:raises: :exc:`RuntimeError` if the value of 'length' is below 1
"""
if settings is None or 'length' not in settings:
raise RuntimeError(self._MISSING_LENGTH)
elif settings['length'] < 1:
raise RuntimeError(self._LOW_LENGTH)
else:
self.settings = settings
self.score = score
self.parts = len(self.score.columns.values)
super(ContourIndexer, self).__init__(score, None)
[docs] def run(self):
"""
Makes a new index of the contours in the piece.
:returns: A :class:`DataFrame` of the contours.
:rtype: :class:`pandas.DataFrame`
"""
contours = []
for v, voice in enumerate(self.score.columns.values):
part = self.score[voice].tolist()
index = self.score.index.tolist()
new_index = []
voice_con = []
for x in range(len(part)-self.settings['length']+1):
cont = []
if part[x] == 'Rest' or type(part[x]) == float:
pass
else:
cont.append(part[x])
y = 1
while (len(cont) < self.settings['length']) and (x+y < len(part)):
if part[x+y] == 'Rest' or type(part[x+y]) == float:
pass
else:
cont.append(part[x+y])
y+=1
if len(cont) == self.settings['length']:
voice_con.append(getContour(cont))
new_index.append(index[x])
voice = pandas.Series(voice_con, index=new_index, name=str(v))
contours.append(voice)
result = pandas.concat(contours, axis=1)
return self.make_return(result.columns, [result[name] for name in result.columns])