#!/usr/bin/env python
# -*- coding: utf-8 -*-
# -------------------------------------------------------------------- #
# Program Name: vis
# Program Description: Helps analyze music with computers.
#
# Filename: analyzers/indexers/noterest.py
# Purpose: Index note and rest objects.
#
# Copyright (C) 2013, 2014, 2015 Christopher Antila, and 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:: Christopher Antila <christopher@antila.ca>
.. codeauthor:: Alexander Morgan
Index note and rest objects into pandas DataFrame(s).
"""
import six
import pandas
from music21 import pitch, note, chord
from vis.analyzers import indexer
[docs]def noterest_ind_func(event):
"""
Used internally by :class:`NoteRestIndexer`. Convert
:class:`~music21.note.Note`, :class:`~music21.note.Rest`, and
:class:`~music21.chord.Chord`objects into strings. For the chords,
only the first pitch of the chord is kept which is usually the
highest pitch. If you want to keep all the pitches in chords,
consider using the :class:`MultiStopIndexer` instead.
:param event: A music21 note, rest, or chord object which get
queried for their names.
:type event: A music21 note, rest, or chord object.
:returns: A one-tuple containing a string representation of the note
or rest, or if the event is a chord, a list of the strings of
the names of its constituent pitches.
:rtype: 1-tuple of str or list of strings
**Examples:**
>>> from noterest.py import indexer_func
>>> from music21 import note,
>>> indexer_func(note.Note('C4'))
u'C4'
>>> indexer_func(note.Rest())
u'Rest'
>>> indexer_func(chord.Chord([note.Note('E5'), note.Note('C4')]))
u'E5'
"""
if isinstance(event, float):
return event
elif event.isNote:
return six.u(event.nameWithOctave)
elif event.isRest:
return u'Rest'
else: # The event is a chord
return six.u(event.pitches[0].nameWithOctave)
[docs]def multistop_ind_func(event):
"""
Used internally by :class:`MultiStopIndexer`. Convert
:class:`~music21.note.Note` and :class:`~music21.note.Rest` objects
into a string and convert the :class:`~music21.chord.Chord` objects
into a list of the strings of their consituent pitch objects. The
results must be contained in a tuple or a list so that chords can
later be unpacked into different 1-voice strands.
:param event: A music21 note, rest, or chord object which get
queried for their names.
:type event: A music21 note, rest, or chord object.
:returns: A one-tuple containing a string representation of the note
or rest, or if the event is a chord, a list of the strings of
the names of its constituent pitches.
:rtype: 1-tuple of str or list of strings
**Examples:**
>>> from noterest.py import indexer_func
>>> from music21 import note,
>>> indexer_func(note.Note('C4'))
(u'C4',)
>>> indexer_func(note.Rest())
(u'Rest',)
>>> indexer_func(chord.Chord([note.Note('C4'), note.Note('E5')]))
[u'C4', u'E5']
"""
if isinstance(event, float):
return event
elif event.isNote:
return (six.u(event.nameWithOctave),)
elif event.isRest:
return (u'Rest',)
else: # The event is a chord
return [six.u(p.nameWithOctave) for p in event.pitches]
[docs]def unpack_chords(df):
"""
The c in nrc in methods like _get_m21_nrc_objs() stands for chord.
This method unpacks music21 chords into a list of their constituent
pitch objects. These pitch objects can be queried for their
``nameWithOctave`` in the same way that note objects can in music21.
This works by broadcasting the list of pitches in each chord object
in each part's elements to a dataframe of note, pitch, and rest
objects. So each part that had chord objects in it gets represented
as a dataframe instead of just a series. Then the series from the
parts that didn't have chords in them get concatenated with the
parts that did, resulting in potentially more columns in the final
dataframe then there are parts in the score.
"""
post = pandas.concat([pandas.DataFrame(df.iloc[:,x].dropna().tolist(),
index=df.iloc[:,x].dropna().index)
for x in range(len(df.columns))], axis=1)
return post.fillna(float('nan'))
# without this fillna call, some na values are None's.
[docs]class NoteRestIndexer(indexer.Indexer):
"""
Index :class:`~music21.note.Note` and :class:`~music21.note.Rest`
objects in a :class:`~music21.stream.Part`.
:class:`Rest` objects become ``'Rest'``, and :class:`Note` objects
become the string-format version of their
:attr:`~music21.note.Note.nameWithOctave` attribute.
This indexer is meant to be called indirectly with a call to
``get_data`` on an indexed piece in the manner of the following
example.
**Example:**
>>> from vis.models.indexed_piece import Importer
>>> ip = Importer('path_to_piece.xml')
>>> ip.get_data('noterest')
"""
required_score_type = 'pandas.DataFrame'
def __init__(self, score):
"""
:param score: A dataframe of the note, rest, and chord objects
in a piece.
:type score: pandas Dataframe
:raises: :exc:`RuntimeError` if ``score`` is not a pandas
Dataframe.
"""
super(NoteRestIndexer, self).__init__(score, None)
self._types = ('Note', 'Rest', 'Chord')
self._indexer_func = noterest_ind_func
# NB: The noterest indexer inherits the run() method from indexer.py
[docs]class MultiStopIndexer(indexer.Indexer):
"""
Index :class:`~music21.note.Note`, :class:`~music21.note.Rest`, and
:class:`~music21.chord.Chord` objects in a
:class:`~pandas.DataFrame`.
:class:`Rest` objects become ``'Rest'``, and :class:`Note` objects
become the string-format version of their
:attr:`~music21.note.Note.nameWithOctave` attribute.
:class:`~music21.chord.Chord` objects get unpacked into their
constituent pitches.
This indexer is meant to be called indirectly with a call to
``get_data`` on an indexed piece in the manner of the following
example.
**Example:**
>>> from vis.models.indexed_piece import Importer
>>> ip = Importer('path_to_piece.xml')
>>> ip.get_data('multistop')
"""
required_score_type = 'pandas.DataFrame'
def __init__(self, score):
"""
:param score: A dataframe of the note, rest, and chord objects
in a piece.
:type score: pandas Dataframe
:raises: :exc:`RuntimeError` if ``score`` is not a pandas
Dataframe.
"""
super(MultiStopIndexer, self).__init__(score, None)
self._types = ('Note', 'Rest', 'Chord')
self._indexer_func = multistop_ind_func
[docs] def run(self):
"""
Make a new index of the note and rest names in the piece. When a
single part has chord objects, those chords get separated out
into as many columns as there are notes in the chord with the
greatest number of notes. This means that there can be more
columns in this dataframe than there are parts in the piece.
:returns: A :class:`DataFrame` of the new indices.
The columns have a :class:`MultiIndex`.
:rtype: :class:`pandas.DataFrame`
"""
# This if statement is necessary because of a pandas bug, see
# pandas issue #8222.
if len(self._score.index) == 0:
# If parts have no note, rest, or chord events in them
result = self._score.copy()
else: # This is the normal case
temp = self._score.applymap(self._indexer_func)
# Do indexing.
result = unpack_chords(temp)
# Unpack chords into individual pitches.
return self.make_return([str(x)
for x in range(len(result.columns))], result)