This is the documentation for the VIS music analysis framework, made available as free software according to the terms of the Affero General Public Licence, version 3 or any later. VIS is a Python package that uses the music21 and pandas libraries to build a ridiculously flexible and preposterously easy system for writing computer music analysis programs. The developers hope to lower the barrier to empirical music analysis to the point that North American music theorists feel compelled to use techniques common among scientists since the 1980s. The VIS framework was produced by the McGill University team of the ELVIS Project. Learn more about the project at our website, http://elvisproject.ca.
If you are a programmer who wishes to use the vis Python objects directly, to use the framework as part of another project, or to add functionality to your own instance of the vis Web app, you should read this document.
If you are not a programmer, this document is probably not for you, since we assume readers are fluent with Python. You may still wish to read the Design Principles section to get an idea of how to use our framework, but it is still quite technical. If you wish to learn about computer-driven music analysis, if you are looking for help using the ELVIS counterpoint Web app, or if you are looking for research findings produced by the ELVIS Project, you will find these resources on the ELVIS website when they become available.
The VIS API documentation explains the framework’s architecture, how to install and use the framework, and how to use the built-in analyzer modules.
If you are new to VIS, we recommend you read Design Principles and continue through both tutorials.
If you forgot the basics, we recommend you read this paragraph. The VIS framework uses two models (IndexedPiece and AggregatedPieces) to fetch results for one or multiple pieces, respectively. Use metadata() to fetch metadata and get_data() to run analyses and fetch results. Call get_data() with a list of analyzers to run, and a dictionary with the settings they use. Learn what an analyzer does, what it requires for input, and what settings it uses by reading the class documentation. Learn the output format of an analyzer by reading the run() documentation.
If you have a question about an analyzer, learn what it does, what it requires for input, and what settings it uses by reading the class documentation. Learn the output format by reading the run() documentation.
Important
Remember to have a lot of fun.
The vis framework has a simple design: write an analyzer to make an analytic judgment about a piece, then use the built-in models to ensure analyzers are run in the right order, with the right inputs and settings. Since music analysis is a complex task (really, a complicated complex of tasks), and the vis framework is highly abstracted, our simple design requires much explanation.
There are two types of analyzers: indexers and experimenters. Indexers take a music21.stream.Score or the result of another indexer, perform a calculation, then produce output that can sensibly be attached to a specific moment of a piece. Indexers may be relatively simple, like the IntervalIndexer, which accepts an index of the notes and rests in a piece and outputs an index of the intervals between all possible part pairs. Indexers may be complicated, like the NGramIndexer, which accepts at least one index of anything, and outputs an index of successions found therein. An indexer might tell you the scale degrees in a part, or the harmonic function of chords in a score.
Experimenters always accept the result of an indexer or another experimenter, perform a culculation, then produce results that cannot be sensibly attached to a specific moment of a piece. Experimenters may be relatively simple, like the FrequencyExperimenter, which accepts any index and counts the number of occurrences of unique objects found within. Experimenters may be complicated, like one that accepts the result of the FrequencyExperimenter, then outputs a Markov transition model.
The vis framework ships analyzers for various tasks, but we think most users will extend the framework with their own analyzers.
The vis framework has two data models. Use IndexedPiece for a single piece and AggregatedPieces for pieces as a group. In a typical application, you will write analyzers but never use them directly, and never modify but always use the models. The models know how to run analyzers on the piece or pieces they hold, how to import pieces safely and efficiently, and how to find and access metadata. In the future, the models may support storing results from analyzers in a database so they need not be recalculated for future analyses, use multiprocessing to speed up analyses on multi-core computers, or facilitate transit to and from analyzers in other languages like Haskell. We recommend you use the models to benefit from new features without modifying your programs, since they (should not) change the API.
After you install the framework, we recommend you begin with the two tutorials below (refer to Tutorial: Make a new Workflow and Tutorial: Use the WorkflowManager). When you wish to write a new analyzer, refer to the documentation and source code for the TemplateIndexer and TemplateExperimenter.
You must install the VIS Framework before you use it. If you will not write extensions for the Framework, you may use pip to install the package from the Python Package Index (PyPI—https://pypi.python.org/pypi/vis-framework/). Run this command:
$ pip install vis-framework
You may also wish to install some or all of the optional dependencies:
- numexpr and bottleneck, which speed up pandas.
- openpyxl, which allows pandas to export Excel-format spreadsheets.
- cython and tables, which allow pandas to export HDF5-format binary files.
You may install optional dependencies in the same ways as VIS itself. For example:
$ pip install numexpr bottleneck
If you wish to install the VIS Framework for development work, we recommend you clone our Git repository from https://github.com/ELVIS-Project/vis/, or even make your own fork on GitHub. You may also wish to checkout a particular version for development with the “checkout” command, as git checkout tags/vis-framework-1.2.3 or git checkout master.
If you installed git, but you need help to clone a repository, you may find useful information in the git documentation.
After you clone the vis repository, you should install its dependencies (currently music21, pandas, and mock), for which we recommend you use pip. From the main VIS directory, run pip install -r requirements.txt to automatically download and install the library dependencies as specified in our requirements.txt file. We also recommend you run pip install -r optional_requirements.txt to install several additional packages that improve the speed of pandas and allow additional output formats (Excel, HDF5). You may need to use sudo or su to run pip with the proper privileges. If you do not have pip installed, use your package manager (the package is probably called python-pip—at least for users of Fedora, Ubuntu, and openSUSE). If you are one of those unfortunate souls who uses Windows, or worse, Mac OS X, then clearly we come from different planets. The pip documentation may help you.
During development, you should usually start python (or ipython, etc.) from within the main “vis” directory to ensure proper importing.
After you install the VIS Framework, we recommend you run our automated tests. From the main vis directory, run python run_tests.py. Python prints . for every test that passes, and a large error or warning for every test that fails. Certain versions of music21 may cause tests to fail; refer to Known Issues and Limitations for more information.
The WorkflowManager is not required for the framework’s operation. We recommend you use the WorkflowManager directly or as an example to write new applications. The vis framework gives you tools to answer a wide variety of musical questions. The WorkflowManager uses the framework to answer specific questions. Please refer to Tutorial: Use the WorkflowManager for more information. If you will not use the WorkflowManager, we recommend you delete it: remove the workflow.py and other_tests/test_workflow.py files.
If you wish to produce graphs with the VIS Framework, you must install an R interpreter and the “ggplot2” library. We use the version 3.0.x series of R.
If you use a “Windows” or “OS X” computer, download a pre-compiled binary from http://cran.r-project.org. If you use a “Linux” computer (or “BSD,” etc.), check your package manager for R 3.0.x. You may have a problem if you search for “R,” since it is a common letter, so we recommend you assume the package is called “R” and try to search only if that does not work. If your distribution does not provide an R binary, or provides an older version than 3.0.0, install R from source code: http://cran.r-project.org/doc/manuals/r-release/R-admin.html.
In all cases, if you encounter a problem, the R manuals are extensive, but require careful attention.
Your distribution may provide a package for “ggplot” or “ggplot2.” The following instructions work for all operating systems:
Start R (with superuser privileges, if not on Windows).
Run the following command to install ggplot:
install.packages("ggplot2")
Run the following commands to test R and ggplot:
huron <- data.frame(year=1875:1972, level=as.vector(LakeHuron))
library(plyr)
huron$decade <- round_any(huron$year, 10, floor)
library(ggplot)
h <- ggplot(huron, aes(x=year))
h + geom_ribbon(aes(ymin=level-1, ymax=level+1))
Expect to see a chart like this:
Image credit: taken from the “ggplot2” documentation on 26 November 2013; reused here under the GNU General Public License, version 2.
Quit R. You do not need to save your workspace:
q()
Once you understand our framework’s architecture (explained in Design Principles), you can start to design a new workflow to ask your own queries.
Pretend you are me. I want to describe what distinguishes the melodic styles of two composers. I have already chosen the composers and the pieces I will use to compare them, trying to make the test sets as similar as possible except for the different composers. I want to use the vis framework, and I want to be as lazy as possible, so I will try to avoid adding new analyzers.
For my preliminary investigation, I will consider only patterns of melodic motion, since all required indexers and experimenters are already included with the vis framework. The NGramIndexer provides vis with pattern-finding functionality, so to run my query I must consider two questions: (1) what does the NGramIndexer need in order to find melodic patterns? and (2) how shall I aggregate the melodic patterns across pieces?
After the preliminary investigation, I would make my query more useful by using the “horizontal” and “vertical” functionality of the NGramIndexer to coordinate disparate musical elements that make up melodic identity. Writing a new Indexer to help combine melodic intervals with the duration of the note preceding the interval would be relatively easy, since music21 knows the duration of every note. A more subtle, but possibly more informative, query would combine melodic intervals with the scale degree of the preceding note. This is a much more complicated query, since it would require an indexer to find the key at a particular moment (an extremely complicated question) and an indexer that knows the scale degree of a note.
I start by reading and understanding the documentation for the NGramIndexer. This indexer’s power means it can be difficult to use in subtle and unexpected ways. For this simple preliminary investigation, we need only provide the melodic intervals of every part in an IndexedPiece. The melodic intervals will be the “vertical” events; there will be no “horizontal” events. We can change the “mark singles” and “continuer” settings any time as we please. We will probably want to try many different pattern lengths by changing the “n” setting. If we do not wish our melodic patterns to include rests, we can set “terminator” to [u'Rest'].
Thus the only information NGramIndexer requires from another analyzer is the melodic intervals, produced by HorizontalIntervalIndexer, which will confusingly be the “vertical” event. As specified in its documentation, the HorizontalIntervalIndexer requires the output of the NoteRestIndexer, which operates directly on the music21 Score.
The first part of our query looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from vis.analyzers.indexers import noterest, interval, ngram
from vis.models.indexed_piece import IndexedPiece
# prepare inputs and output-collectors
pathnames = [list_of_pathnames_here]
ind_ps = [IndexedPiece(x) for x in pathnames]
interval_settings = {'quality': True}
ngram_settings = {'vertical': 0, 'n': 3} # change 'n' as required
ngram_results = []
# prepare for and run the NGramIndexer
for piece in ind_ps:
intervals = piece.get_data([noterest.NoteRestIndexer, interval.HorizontalIntervalIndexer], interval_settings)
for part in intervals:
ngram_results.append(piece.get_data([ngram.NGramIndexer], ngram_settings, [part])
|
After the imports, we start by making a list of all the pathnames to use in this query, then use a Python list comprehension to make a list of IndexedPiece objcects for each file. We make the settings dictionaries to use for the interval then n-gram indexers on lines 7 and 8, but note we have not included all possible settings. The empty ngram_results list will store results from the NGramIndexer.
The loop started on line 12 is a little confusing: why not use an AggregatedPieces object to run the NGramIndexer on all pieces with a single call to get_data()? The reason is the inner loop, started on line 14: if we run the NGramIndexer on an IndexedPiece once, we can only index a single part, but we want results from all parts. This is the special burden of using the NGramIndexer, which is flexible but not (yet) intelligent. In order to index the melodic intervals in every part using the get_data() call on line 15, we must add the nested loops.
For this analysis, I will simply count the number of occurrences of each harmonic interval pattern, which is called the “frequency.” It makes sense to calculate each piece separately, then combine the results across pieces. We’ll use the FrequencyExperimenter and ColumnAggregator experimenters for these tasks. The FrequencyExperimenter counts the number of occurrences of every unique token in another index into a pandas.Series, and the ColumnAggregator combines results across a list of Series or a DataFrame (which it treats as a list of Series) into a single Series.
With these modifications, our program looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from vis.analyzers.indexers import noterest, interval, ngram
from vis.analyzers.experimenters import frequency, aggregator
from vis.models.indexed_piece import IndexedPiece
from vis.models.aggregated_pieces import AggregatedPieces
from pandas import DataFrame
# prepare inputs and output-collectors
pathnames = [list_of_pathnames_here]
ind_ps = [IndexedPiece(x) for x in pathnames]
interval_settings = {'quality': True}
ngram_settings = {'vertical': [0], 'n': 3} # change 'n' as required
ngram_freqs = []
# prepare for and run the NGramIndexer
for piece in ind_ps:
intervals = piece.get_data([noterest.NoteRestIndexer, interval.HorizontalIntervalIndexer], interval_settings)
for part in intervals:
ngram_freqs.append(piece.get_data([ngram.NGramIndexer, frequency.FrequencyExperimenter], ngram_settings, [part]))
# aggregate results of all pieces
agg_p = AggregatedPieces(ind_ps)
result = agg_p.get_data([aggregator.ColumnAggregator], [], {}, ngram_freqs)
result = DataFrame({'Frequencies': result})
|
The first thing to note is that I modified the loop from the previous step by adding the FrequencyExperimenter to the get_data() call on line 18 that uses the NGramIndexer. As you can see, the aggregation step is actually the easiest; it simply requires we create an AggregatedPieces object and call its get_data() method with the appropriate input, which is the frequency data we collected in the loop.
On line 22, result holds a Series with all the information we need! To export your data to one of the supported formats (CSV, Excel, etc.) you must create a DataFrame and use one of the methods described in the pandas documentation. The code on line 23 “converts” result into a DataFrame by giving the Series to the DataFrame constructor in a dictionary. The key is the name of the column, which you can change to any value valid as a Python dictionary key. Since the Series holds the frequencies of melodic interval patterns, it makes sense to call the column 'Frequencies' in this case. You may also wish to sort the results by running result.sort() before you “convert” to a DataFrame. You can sort in descending order (with the most common events at the top) with result.sort(ascending=False).
The script developed in Tutorial: Make a new Workflow is suitable for users comfortable with an interactive Python shell. Application developers making a graphical interface—whether on the Web or in a desktop application—can take advantage of a further layer of abstraction offered by our WorkflowManager. Since developers often prefer to separate their user interface code from any of the so-called “business logic,” the WorkflowManager provides the means by which to connect the “dumb” user interface with the highly-abstracted vis framework. You can think of the WorkflowManager as the true back-end component of your application, and you should expect to rewrite it with every application you develop.
The WorkflowManager‘s documentation describes its functionality:
Parameters: | pathnames (list of basestring) – A list of pathnames. |
---|
The WorkflowManager automates several common music analysis patterns for counterpoint. Use the WorkflowManager with these four tasks:
Before you analyze, you may wish to use these methods:
You may also treat a WorkflowManager as a container:
>>> wm = WorkflowManager('piece1.mxl', 'piece2.krn')
>>> len(wm)
2
>>> ip = wm[1]
>>> type(ip)
<class 'vis.models.indexed_piece.IndexedPiece'>
If I want to port the Tutorial: Make a new Workflow query to the WorkflowManager, I need to fit its functionality into the existing methods. The load(), output(), and export() methods are all related to preparing IndexedPiece objects for analysis and saving or outputting results. Since my query requires no special treatment in these areas, I will not modify those methods, and all of my changes will be in the run() method.
Since my new program only requires one query, I can make a very simple run() method and remove the other hidden methods (_intervs(), _interval_ngrams(), _variable_part_modules(), _two_part_modules(), and _all_part_modules()). Of course, you may wish to use those methods for inspiration when you build your own queries. When I add my new query’s logic to the run() method, I get this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def run(self):
ngram_settings = {'vertical': [0], 'n': self.settigns(None, 'n')}
ngram_freqs = []
for i, piece in enumerate(self._data):
interval_settings = {'quality': self.settings(i, 'interval quality')}
intervals = piece.get_data( \
[noterest.NoteRestIndexer, interval.HorizontalIntervalIndexer], \
interval_settings)
for part in intervals:
ngram_freqs.append( \
piece.get_data([ngram.NGramIndexer, frequency.FrequencyExperimenter], \
ngram_settings, \
[part]))
agg_p = AggregatedPieces(ind_ps)
self._result = agg_p.get_data([aggregator.ColumnAggregator], [], {}, ngram_freqs)
|
I made the following changes:
I could also use the WorkflowManager.settings() method to get other settings by piece or shared across all pieces, like 'simple intervals', which tells the HorizontalIntervalIndexer whether to display all intervals as their single-octave equivalents.
To run the same analysis as in Tutorial: Make a new Workflow, use the WorkflowManager like this:
1 2 3 4 5 6 7 8 9 | from vis.workflow import WorkflowManager
pathnames = [list_of_pathnames]
work = WorkflowManager(pathnames)
work.load('pieces')
for i in xrange(len(work)):
work.settings(i, 'quality', True)
work.run()
work.export('CSV', 'output_filename.csv')
|
This script actually does more than the program in Tutorial: Make a new Workflow because export() “converts” the results to a DataFrame, sorts, and outputs the results.
Indexers produce an “index” of a symbolic musical score. Each index provides one type of data, and each event in can be attached to a particular moment in the original score. Some indexers produce their index directly from the score, like the NoteRestIndexer, which describes pitches. Others create new information by analyzing another index, like the IntervalIndexer, which describes harmonic intervals between two-part combinations in the score, or the FilterByRepeatIndexer, which removes consecutive identical events, leaving only the first.
Analysis modules that subclass Experimenter, by contrast, produce results that cannot be attached to a moment in a score.
Indexers work only on single IndexedPiece instances. To analyze many IndexedPiece objects together, use an experimenter with an AggregatedPieces object.
This module outlines the Experimenter base class, which helps with transforming time-attached analytic information to other types.
Bases: object
Run an experiment on an IndexedPiece.
Use the “Experimenter.required_indices” attribute to know which Indexer subclasses should be provided to this Experimenter’s constructor. If the list is None or [], use the “Experimenter.required_experiments” attribute to know which Experimenter should be provided to the constructor.
The controllers that deal with indexing data from music21 Score objects.
Bases: object
Create an index of a music21 stream.
Use the requires_score attribute to know whether __init__() requires a list of Part objects. If False, read the documentation to know which indexers’ results it requires.
The name of the indexer, as stored in an IndexedPiece, is the unicode-format version of the class name.
Make a new index of the piece.
Returns: | A list of the new indices. The index of each Series corresponds to the index of the Part or Series used to generate it, as given to the constructor. Each element in each Series is a basestring (unless specified otherwise in a subclass). |
---|---|
Return type: | list of pandas.Series |
For a set of streams, find the offsets at which events begin. Used by mp_indexer.
Parameters: | streams (list of music21.stream.Stream) – A list of Streams in which to find the offsets at which events begin. |
---|---|
Returns: | A list of floating-point numbers representing offsets at which a new event begins in any of the streams. Offsets are sorted from lowest to highest (start to end). |
Return type: | list of float |
When there is more than one event at an offset, call this method to ensure parsing simultaneities.
Example: Transforms this... [[1, 2, 3], [1, 2, 3], [1, 2]] ... into this... [[1, 1, 1], [2, 2, 2], [3, 3]]
Perform the indexation of a part or part combination. This is a module-level function designed to ease implementation of multiprocessing with the MPController module.
If your Indexer has settings, use the indexer_func() to adjust for them.
Parameters: |
|
---|---|
Returns: | The new index where each element is a unicode object and the “index” of the pandas object corresponds to the offset at which each event begins. Index 0 is the argument “pipe_index” unchanged. |
Return type: | 2-tuple with “pipe_index” and pandas.Series or pandas.DataFrame |
Raises: | ValueError, if there are multiple events at an offset in any of the inputted Series. |
Perform the indexation of a part or part combination. This is a module-level function designed to ease implementation of multiprocessing.
If your Indexer has settings, use the indexer_func() to adjust for them.
If an offset has multiple events of the correct type, only the “first” discovered results will be included in the output. This may produce misleading results when, for example, a double-stop was imported as two Note objects in the same Part, rather than as a Chord.
Parameters: |
|
---|---|
Returns: | The “pipe_index” argument and the new index. The new index is a pandas.Series where every element is a unicode object. The Series’ index corresponds to the quarterLength offset of the event in the input Stream. |
Return type: | 2-tuple of any and pandas.Series |
Aggregating experimenters.
Bases: vis.analyzers.experimenter.Experimenter
Experiment that aggregates data from all columns of a DataFrame, a list of DataFrame objects, or a list of Series, into a single Series. Aggregation is done through addition. If a DataFrame has a column with the name u'all', it will not be included in the aggregation.
Run the ColumnAggregator experiment.
Returns: | A Series with an index that is the combination of all indices of the provided pandas objects, and the value is the sum of all values in the pandas objects. |
---|---|
Return type: | pandas.Series |
Experimenters that deal with the frequencies (number of occurrences) of events.
Bases: vis.analyzers.experimenter.Experimenter
Calculate the number of occurrences of things found in an index.
Run the FrequencyExperimenter.
Returns: | The result of the experiment. Data is stored such that column labels correspond to the part (combinations) totalled in the column, and row labels correspond to a type of the kind of objects found in the given index. Note that all columns are totalled in the “all” column, and that not every part combination will have every interval; in case an interval does not appear in a part combination, the value is numpy.NaN. |
---|---|
Return type: | pandas.DataFrame |
Used by the FrequencyExperimenter to calculate the frequencies of things in an index.
Parameters: | obj (tuple of (anything, pandas.Series)) – An identifier plus the results of an indexer. |
---|---|
Returns: | An identifier plus the result of this indexation. In the series, the index is the names of objects found in the inputted series, and the value is the number of occurrences. The first element is the first element given here, used for identification purposes. |
Return type: | tuple of (anything, pandas.Series) |
Template for writing a new experimenter. Use this class to help write a new :class`Experimenter` subclass. The TemplateExperimenter does nothing, and should only be used by programmers.
Bases: vis.analyzers.experimenter.Experimenter
Template for an Experimenter subclass.
The default values for settings named in possible_settings. If a setting doesn’t have a value in this constant, then it must be specified to the constructor at runtime, or the constructor should raise a RuntimeException.
This is a list of basestrings that are the names of the settings used in this experimenter. Specify the types and reasons for each setting as though it were an argument list, like this:
Parameters: | u’fake_setting’ (boolean) – This is a fake setting. |
---|
Run an experiment on a piece.
Returns: | The result of the experiment. Each experiment should describe its data storage. |
---|---|
Return type: | pandas.Series or pandas.DataFrame |
Index intervals. Use the IntervalIndexer to find vertical (harmonic) intervals between two parts. Use the HorizontalIntervalIndexer to find horizontal (melodic) intervals in the same part.
Bases: vis.analyzers.indexers.interval.IntervalIndexer
Use music21.interval.Interval to create an index of the horizontal (melodic) intervals in a single part.
You should provide the result of NoteRestIndexer.
Make a new index of the piece.
Returns: | A list of the new indices. The index of each Series corresponds to the index it has in the list of Series given to the constructor. |
---|---|
Return type: | list of pandas.Series |
Bases: vis.analyzers.indexer.Indexer
Use music21.interval.Interval to create an index of the vertical (harmonic) intervals between two-part combinations.
You should provide the result of NoteRestIndexer.
A dict of default settings for the IntervalIndexer.
A list of possible settings for the IntervalIndexer.
Parameters: |
|
---|
The IntervalIndexer requires a list of Series as input. These should be the result of NoteRestIndexer.
alias of Series
Make a new index of the piece.
Returns: | A dictionary of the new interval indices. Find part combinations by using the index of the parts as provided to the __init__() method, set as a string with a comma. Refer to the “Example” below. |
---|---|
Return type: | dict of pandas.Series |
** Example **
To access the intervals between the two highest parts in a score:
>>> result = an_interval_indexer.run()
>>> result['0,1']
Series([], type: object) # whatever intervals
Used internally by the IntervalIndexer and HorizontalIntervalIndexer.
Call real_indexer() with settings to print compound intervals without quality.
Used internally by the IntervalIndexer and HorizontalIntervalIndexer.
Call real_indexer() with settings to print simple intervals without quality.
Used internally by the IntervalIndexer and HorizontalIntervalIndexer.
Call real_indexer() with settings to print compound intervals with quality.
Used internally by the IntervalIndexer and HorizontalIntervalIndexer.
Call real_indexer() with settings to print simple intervals with quality.
Transforms a key in the results of IntervalIndexer.run() into a 2-tuple with the indices of the parts held therein.
Parameters: | key (unicode string) – The key from IntervalIndexer. |
---|---|
Returns: | The indices of parts referred to by the key. |
Return type: | tuple of int |
>>> key_to_tuple(u'5,6')
(5, 6)
>>> key_to_tuple(u'234522,98100')
(234522, 98100)
>>> var = key_to_tuple(u'1,2')
>>> key_to_tuple(str(var[0]) + u',' + str(var[1]))
(1, 2)
Used internally by the IntervalIndexer and HorizontalIntervalIndexer.
Turn a notes-and-rests simultaneity into the name of the interval it represents. Note that, because of the 'Rest' strings, you can compare the duration of the piece in which the two parts do or do not have notes sounding together.
Parameters: |
|
---|---|
Returns: | u'Rest' if one or more of the parts is u'Rest'; otherwise, the interval between parts. |
Return type: | unicode string |
Indexer to find k-part any-object n-grams.
Bases: vis.analyzers.indexer.Indexer
Indexer that finds k-part n-grams from other indices.
The indexer requires at least one “vertical” index, and supports “horizontal” indices that seem to “connect” instances in the vertical indices. Although we use “vertical” and “horizontal” to describe these index types, because the class is an abstraction of two-part interval n-grams, you can supply any information as either type of index. If you want one-part melodic n-grams for example, you should supply the relevant interval information as the “vertical” component.
There is no relationship between the number of index types, though there must be at least one “vertical” index.
The settings given to __init__() specify which index values in score are horizontal or vertical intervals. They will be added in the n-gram in the order specified, so if the u'vertical' setting is [4, 1, 3] for lists of intervals, then for each vertical event, objects will be listed in that order.
In the output, groups of “vertical” events are enclosed in brackets, while groups of “horizontal” events are enclosed in parentheses.
For cases where there is only one index in a particular direction, you can avoid printing the brackets or parentheses by setting the u'mark singles' setting to False (though the default is True).
If you want n-grams to terminate when finding one or several particular values, you can specify this with the u'terminator' setting.
To show that a horizontal event continues, we use u'_' by default, but you can set this separately, for example to u'P1' u'0', as seems appropriate. Note that the default WorkflowManager is set to override this setting by dynamically adjusting to whether interval quality is set to True or False or if the user chooses to pass a custom string for this setting.
You can also use the NGramIndexer to collect “stacks” of single vertical events. If you provide indices of intervals above a lowest part, for example, these “stacks” become the figured bass signature of a single moment. Set u'n' to 1 for this feature. Horizontal events are obviously ignored.
A dict of default settings for the NGramIndexer.
A list of possible settings for the NGramIndexer.
Parameters: |
|
---|
The NGramIndexer requires pandas.Series as input.
alias of Series
Make an index of k-part n-grams of anything.
Returns: | A single-item list with the new index. |
---|---|
Return type: | list of pandas.Series |
Index note and rest objects.
Bases: vis.analyzers.indexer.Indexer
Index music21.note.Note and Rest objects found in a music21.stream.Part.
Rest objects are indexed as u'Rest', and Note objects as the unicode-format version of their pitchWithOctave attribute.
The NoteRestIndexer uses Part objects directly.
alias of Part
Make a new index of the piece.
Returns: | A list of the new indices. The index of each Series corresponds to the index of the Part used to generate it, in the order specified to the constructor. Each element in the Series is a unicode. |
---|---|
Return type: | list of pandas.Series |
Used internally by NoteRestIndexer. Convert objects from the music21.note module into a unicode.
Parameters: | obj (list of music21.note.Note or music21.note.Rest) – A list with the object to convert. |
---|---|
Returns: | If the first object in the list is a music21.note.Rest, the string u'Rest'; otherwise the nameWithOctave attribute, which is the pitch class and octave of the music21.note.Note. |
Return type: | unicode |
Indexers that modify the “offset” values (floats stored as the “index” of a pandas.Series), potentially adding repetitions of or removing pre-existing events, without modifying the events themselves.
Bases: vis.analyzers.indexer.Indexer
Indexer that regularizes the “offset” values of observations from other indexers.
The Indexer regularizes observations from offsets spaced any, possibly irregular, quarterLength durations apart, so they are instead observed at regular intervals. This has two effects:
Since elements’ durations are not recorded, the last observation in a Series will always be included in the results. If it does not start on an observed offset, it will be included as the next observed offset—again, whether or not this is true in the actual music. However, the last observation will only ever be counted once, even if a part ends before others in a piece with many parts. See the doctests for examples.
Examples. For all, the quarterLength is 1.0.
When events in the input already appear at intervals of quarterLength, input and output are identical.
offset | 0.0 | 1.0 | 2.0 |
---|---|---|---|
input | a | b | c |
output | a | b | c |
When events in the input appear at intervals of quarterLength, but there are additional elements between the observed offsets, those additional elements are removed.
offset | 0.0 | 0.5 | 1.0 | 2.0 |
---|---|---|---|---|
input | a | A | b | c |
output | a | b | c |
offset | 0.0 | 0.25 | 0.5 | 1.0 | 2.0 |
---|---|---|---|---|---|
input | a | z | A | b | c |
output | a | b | c |
When events in the input appear at intervals of quarterLength, but not at every observed offset, the event from the previous offset is repeated.
offset | 0.0 | 1.0 | 2.0 |
---|---|---|---|
input | a | c | |
output | a | a | c |
When events in the input appear at offsets other than those observed by the specified quarterLength, the “most recent” event will appear.
offset | 0.0 | 0.25 | 0.5 | 1.0 | 2.0 |
---|---|---|---|---|---|
input | a | z | A | c | |
output | a | A | c |
When the final event does not appear at an observed offset, it will be included in the output at the next offset that would be observed, even if this offset does not appear in the score file to which the results correspond.
offset | 0.0 | 1.0 | 1.5 | 2.0 |
---|---|---|---|---|
input | a | b | d | |
output | a | b | d |
The behaviour in this last example can create a potentially misleading result for some analytic situations that consider metre. It avoids another potentially misleading situation where the final chord of a piece would appear to be dissonant because of a suspension. We chose to lose metric and rythmic precision, which would be more profitably analyzed with indexers built for that purpose. Consider this illustration, where the numbers correspond to scale degrees.
offset | 410.0 | 411.0 | 411.5 | 412.0 |
---|---|---|---|---|
in-S | 2 | 1 | ||
in-A | 7 | 5 | ||
in-T | 4 ———– | 3 | ||
in-B | 5 | 1 | ||
out-S | 2 | 1 | 1 | |
out-A | 7 | 5 | 5 | |
out-T | 4 | 4 | 3 | |
out-B | 5 | 1 | 1 |
If we left out the note event appear in the in-A part at offset 411.5, the piece would appear to end with a dissonant sonority!
A list of possible settings for the FilterByOffsetIndexer.
Parameters: |
|
---|
The FilterByOffsetIndexer uses pandas.Series objects.
alias of Series
Regularize the observed offsets for the inputted Series.
Returns: | A DataFrame with the indices for all the inputted parts, where the “index” value for each part is the same as in the list in which they were submitted to the constructor. The “index” for each member Series is the same, starting at 0.0 then at every “quarterLength” after, until either the last observation in the piece, or the nearest multiple before. |
---|---|
Return type: | pandas.DataFrame |
Indexers that consider repetition in any way.
Bases: vis.analyzers.indexer.Indexer
If the same event occurs many times in a row, remove all occurrences but the one with the lowest offset value (i.e., the “first” event).
The FilterByRepeatIndexer requires pandas.Series as input.
alias of Series
Make a new index of the piece, removing any event that is identical to the preceding.
Returns: | A list of the new indices. The index of each Series corresponds to the index of the Part used to generate it, in the order specified to the constructor. Each element in the Series is a basestring. |
---|---|
Return type: | list of pandas.Series |
Template for writing a new indexer. Use this class to help write a new :class`Indexer` subclass. The TemplateIndexer does nothing, and should only be used by programmers.
Bases: vis.analyzers.indexer.Indexer
Template for an Indexer subclass.
The default values for settings named in possible_settings. If a setting doesn’t have a value in this constant, then it must be specified to the constructor at runtime, or the constructor should raise a RuntimeException.
This is a list of basestrings that are the names of the settings used in this indexer. Specify the types and reasons for each setting as though it were an argument list, like this:
Parameters: | u’fake_setting’ (boolean) – This is a fake setting. |
---|
Depending on how this Indexer works, you’ll either need to provide music21.stream.Part, music21.stream.Score, or pandas.Series.
alias of Part
Make a new index of the piece.
Returns: | A list of the new indices. The index of each Series corresponds to the index of the Part used to generate it, in the order specified to the constructor. Each element in the Series is a basestring. |
---|---|
Return type: | list of pandas.Series |
The function that indexes.
Parameters for Indexers Using a Score :param obj: The simultaneous event(s) to use when creating this index. :type obj: list of the types stored in self._types
Parameters for Indexers Using a Series :param obj: The simultaneous event(s) to use when creating this index. :type obj: pandas.Series of unicode
Returns: | The value to store for this index at this offset. |
---|---|
Return type: | unicode |
The model representing data from multiple IndexedPiece instances.
Bases: object
Hold data from multiple IndexedPiece instances.
Bases: object
Used internally by AggregatedPieces ... at least for now.
Hold aggregated metadata about the IndexedPieces in an AggregatedPiece. Every list has no duplicate entries.
Get the results of an Experimenter run on all the IndexedPiece objects. You must specify all indexers and experimenters to be run to get the results you want.
The same settings dict will be given to all experiments and indexers.
If you want the results from all IndexedPiece objects separately, provide an empty list as the aggregated_experiments argument.
Either the first analyzer in independent_analyzers should use a music21.stream.Score or you must provide an argument for data that is the output from a previous call to this instance’s get_data() method.
Examples
Run analyzer C then D on each piece individually, then provide a list of those results to Experimenter A then B.
>>> pieces.get_data([A, B], [C, D])
Run analyzer C then D on each piece individually, then return a list of those results.
>>> pieces.get_data([], [C, D])
Run analyzer A then B on the results of a previous get_data() call.
>>> piece.get_data([A, B], [], data=previous_results)
Parameters: |
|
---|---|
Returns: | Either one DataFrame with all experimental results or a list of DataFrame objects, each with the experimental results for one piece. |
Return type: | pandas.DataFrame or list of pandas.Series or pandas.DataFrame |
Raises: | TypeError if the analyzer_cls is invalid or cannot be found. |
Get a metadatum about the IndexedPieces stored in this AggregatedPieces.
If only some of the stored IndexedPieces have had their metadata initialized, this method returns incompelete metadata. Missing data will be represented as None in the list, but it will not appear in date_range unless there are no dates. If you need full metadata, we recommend running an Indexer that requires a Score object on all the IndexedPieces (like vis.analyzers.indexers.noterest.NoteRestIndexer).
Valid fields are:
Parameters: | field (basestring) – The name of the field to be accessed or modified. |
---|---|
Returns: | The value of the requested field or None, if accessing a non-existant field or a field that has not yet been initialized in the IndexedPieces. |
Return type: | object or None |
Raises: | TypeError if field is not a basestring. |
This model represents an indexed and analyzed piece of music.
Bases: object
Hold indexed data from a musical score.
Get the results of an Experimenter or Indexer run on this IndexedPiece.
Parameters: |
|
---|---|
Returns: | Results of the analyzer. |
Return type: | pandas.DataFrame or list of pandas.Series |
Raises: | TypeError if the analyzer_cls is invalid or cannot be found. |
Raises: | RuntimeError if the first analyzer class in analyzer_cls does not use Score objects, and data is None. |
Raises: | OpusWarning if the file imports as a music21.stream.Opus object and known_opus is False. |
Raises: | OpusWarning if known_opus is True but the file does not import as an Opus. |
Note about Opus Objects
Correctly importing Opus objects is a little awkward because we only know a file imports to an Opus after we import it, but an Opus should be treated as multiple IndexedPiece objects.
We recommend you handle Opus objects like this:
Refer to the source code for vis.workflow.WorkflowManager.load() for an example implementation.
Get or set metadata about the piece.
Note
Some metadata fields may not be available for all pieces. The available metadata fields depend on the specific file imported. Unavailable fields return None. We guarantee real values for pathname, title, and parts.
Parameters: |
|
---|---|
Returns: | The value of the requested field or None, if assigning, or if accessing a non-existant field or a field that has not yet been initialized. |
Return type: | object or None (usually a basestring) |
Raises: | TypeError if field is not a basestring. |
Raises: | AttributeError if accessing an invalid field (see valid fields below). |
Metadata Field Descriptions
All fields are taken directly from music21 unless otherwise noted.
Metadata Field | Description |
---|---|
alternativeTitle | A possible alternate title for the piece; e.g. Bruckner’s Symphony No. 8 in C minor is known as “The German Michael.” |
anacrusis | The length of the pick-up measure, if there is one. This is not determined by music21. |
composer | The author of the piece. |
composers | If the piece has multiple authors. |
date | The date that the piece was composed or published. |
localeOfComposition | Where the piece was composed. |
movementName | If the piece is part of a larger work, the name of this subsection. |
movementNumber | If the piece is part of a larger work, the number of this subsection. |
number | Taken from music21. |
opusNumber | Number assigned by the composer to the piece or a group containing it, to help with identification or cataloguing. |
parts | A list of the parts in a multi-voice work. This is determined partially by music21. |
pathname | The filesystem path to the music file encoding the piece. This is not determined by music21. |
title | The title of the piece. This is determined partially by music21. |
Examples
>>> piece = IndexedPiece('a_sibelius_symphony.mei')
>>> piece.metadata('composer')
u'Jean Sibelius'
>>> piece.metadata('date', 1919)
>>> piece.metadata('date')
1919
>>> piece.metadata('parts')
[u'Flute 1', u'Flute 2', u'Oboe 1', u'Oboe 2', u'Clarinet 1', u'Clarinet 2', ... ]
Bases: exceptions.RuntimeWarning
The OpusWarning is raised by IndexedPiece.get_data() when known_opus is False but the file imports as a music21.stream.Opus object, and when known_opus is True but the file does not import as a music21.stream.Opus object.
Internally, the warning is actually raised by IndexedPiece._import_score().
The workflow module holds the WorkflowManager, which automates several common music analysis patterns for counterpoint. The TemplateWorkflow class is a template for writing new WorkflowManager classes.
Bases: object
Parameters: | pathnames (list of basestring) – A list of pathnames. |
---|
The WorkflowManager automates several common music analysis patterns for counterpoint. Use the WorkflowManager with these four tasks:
Before you analyze, you may wish to use these methods:
You may also treat a WorkflowManager as a container:
>>> wm = WorkflowManager('piece1.mxl', 'piece2.krn')
>>> len(wm)
2
>>> ip = wm[1]
>>> type(ip)
<class 'vis.models.indexed_piece.IndexedPiece'>
Warning
This method is deprecated and will be removed in VIS 2. Please use output().
Save data from the most recent result of run() to a file.
Parameters: |
|
---|---|
Returns: | The pathname of the outputted file. |
Return type: | |
Raises: | RuntimeError for unrecognized instructions. |
Raises: | RuntimeError if run() has never been called. |
Formats:
Import analysis data from long-term storage on a filesystem. This should primarily be used for the u'pieces' instruction, to control when the initial music21 import happens.
Use load() with an instruction other than u'pieces' to load results from a previous analysis run by run().
Note
If one of the files imports as a music21.stream.Opus, the number of pieces and their order will change.
Parameters: |
|
---|---|
Raises: | RuntimeError if the instruction is not recognized. |
Instructions
Note
only u'pieces' is implemented at this time.
Get or set a metadata field. The valid field names are determined by IndexedPiece (refer to the documentation for metadata()).
A metadatum is a salient musical characteristic of a particular piece, and does not change across analyses.
Parameters: |
|
---|---|
Returns: | The value of the requested field or None, if assigning, or if accessing a non-existant field or a field that has not yet been initialized. |
Return type: | |
Raises: | TypeError if field is not a basestring. |
Raises: | AttributeError if accessing an invalid field. |
Raises: | IndexError if index is invalid for this WorkflowManager. |
Output the results of the most recent call to run(), saved in a file. This method handles both visualizations and symbolic output formats.
Note
For LiliyPond output, you must have called run() with count frequency set to False.
Parameters: |
|
---|---|
Returns: | The pathname(s) of the outputted visualization(s). Requesting a histogram always returns a single string; requesting a score (or some scores) always returns a list. |
Return type: | basestring or list of basestring |
Raises: | RuntimeError for unrecognized instructions. |
Raises: | RuntimeError if run() has never been called. |
Raises: | RuntiemError if a call to R encounters a problem. |
Raises: | RuntimeError with LilyPond output, if we think you called run() with count frequency set to True. |
Instructions:
'histogram': a histogram. Currently equivalent to the 'R histogram' instruction.
'LilyPond': each score with annotations for analyzed objects.
'histogram' instruction. In the future, this will be used to distinguish histograms produced with R from those produced with other libraries, like matplotlib or bokeh.
- u'CSV': output a Series or DataFrame to a CSV file.
'Stata': output a Stata file for importing to R.
'Excel': output an Excel file for Peter Schubert.
'HTML': output an HTML table, as used by the VIS Counterpoint Web App.
Note
We try to prevent you from requesting LilyPond output if you called run() with count frequency set to True by raising a RuntimeError if count frequency is True, or the number of pieces is not the same as the number of results. It is still possible to call run() with count frequency set to True in a way we will not detect. However, this always causes output() to fail. The error will probably be a TypeError that says object of type 'numpy.float64' has no len().
Run an experiment’s workflow. Remember to call load() before this method.
Parameters: | instruction (basestring) – The experiment to run (refer to “List of Experiments” below). |
---|---|
Returns: | The result of the experiment. |
Return type: | pandas.Series or pandas.DataFrame or a list of lists of pandas.Series. If u'count frequency' is set to False, the return type will be a list of lists of series wherein the containing list has each piece in the experiment as its elements (even if there is only one piece in the experiment, this will be a list of length one). The contained lists contain the results of the experiment for each piece where each element in the list corresponds to a unique voice combination in an unlabelled and unpredictable fashion. Finally each series corresponds the experiment results for a given voice combination in a given piece. |
Raises: | RuntimeError if the instruction is not valid for this WorkflowManager. |
Raises: | RuntimeError if you have not called load(). |
Raises: | ValueError if the voice-pair selection is invalid or unset. |
List of Experiments
Get or set a value related to analysis. The valid values are listed below.
A setting is related to this particular analysis, and is not a salient musical feature of the work itself.
Refer to run() for a list of settings required or used by each experiment.
Parameters: |
|
---|---|
Returns: | The value of the requested field or None, if assigning, or if accessing a non-existant field or a field that has not yet been initialized. |
Return type: | object or None |
Raises: | AttributeError if accessing an invalid field (see valid fields below). |
Raises: | IndexError if index is invalid for this WorkflowManager. |
Raises: | ValueError if index and value are both None. |
Piece-Specific Settings
Pieces do not share these settings.
setting. To avoid running the FilterByOffsetIndexer, set this to 0. setting that will become the quarterLength duration between observed offsets.
filter repeats: If you want to run the FilterByRepeatIndexer, set this setting to True.
voice combinations: If you want to consider certain specific voice combinations, set this setting to a list of a list of iterables. The following value would analyze the highest three voices with each other: '[[0,1,2]]' while this would analyze the every part with the lowest for a four-part piece: '[[0, 3], [1, 3], [2, 3]]'. This should always be a basestring that nominally represents a list (except the special values for 'all' parts at once or 'all pairs').
Shared Settings
All pieces share these settings. The value of index is ignored for shared settings, so it can be anything.