Module Documentation

WesterParse

This is the main program module.

WesterParse allows a user to test a species counterpoint exercise for conformity with the rules of line construction and voice leading laid out in Peter Westergaard’s book, An Introduction to Tonal Theory (New York, 1975).

WesterParse imports a musicxml file, converts it to a music21 stream, determines a key (unless specified by the user), and then evaluates the linear syntax or the counterpoint.

The main scripts are:

>>> evaluateLines(source)
>>> evaluateCounterpoint(source)

For more information on how to use these scripts, see User’s Guide to WesterParse.

westerparse.westerparse.evaluateLines(source, show=None, partSelection=None, partLineType=None, report=False, **kwargs)

Determine whether lines are generable using Westergaard’s rules.

Keyword arguments:

show – Determines how the output is handled.

Options

None – Default option. A text report is generated in lieu of a display in musical notation.

show – Parses will be displayed using the music notation application that the user has configured for music21.

writeToServer – Reserved for use by the WesterParse website to write parses to musicxml files, which are then displayed in the browser window.

writeToLocal – Can be used to write parses in musicxml to a user’s local directory. [Eventually the user will be able to select a directory by editing a configuration.py file.] By default, the files are written to ‘parses_from_context/’. The name for each file consists of the prefix ‘parser_output_’, a timestamp, and the suffix ‘.musicxml’.

writeToPng – Use the application MuseScore to produce png files. MuseScore first generates an xml file and then derives the png file. These are named with the prefix ‘parser_output_’, a timestamp, and the appropriate suffix. Note that Musescore inserts ‘-1’ before adding the ‘.png’ suffix. The default directory for these files is ‘tempimages/’. [This, too, can be changed by editing the configuration.py file.]

showWestergaardParse – Not yet functional. Can be used if the source consists of only one line. It will display the parse(s) of a line using Westergaard’s layered form of representation.

parsedata – Can be used to create a json data file for each viable parse of the selected parts. The data file consists of several lines of metadata, a data table for notes, and a data table for the arcs.

partSelection – Designates a line of the composition to parse.

Options

None – The default option. Selects all the lines for parsing.

0, 1, 2, …, -1 – Following the conventions of music21, lines are numbered from top to bottom, starting with 0.

partLineType – Only for use in evaluating a single line. None is the default. User may select among ‘primary’, ‘bass’, or ‘generic’.

report – True or False. Use True to see a text report. Note: If one or more lines in the source cannot be parsed (i.e., if there are syntax errors) or the show option is set to None, the program will automatically generate a text report.

Other keywords: keynote and mode – The user can use these to force the parser to interpret the input in a particular key.

For harmonically progressive species, the user can specify when the predominant and dominant spans begin, using the keywords startDominant and, if needed, startPredominant, with values

given as measure numbers.

westerparse.westerparse.evaluateCounterpoint(source, report=True, sonorityCheck=False, **kwargs)

Determine whether voice leading conforms to Westergaard’s rules.

westerparse.westerparse.makeGlobalContext(source, **kwargs)

Import a musicxml file and convert to music21 Stream. Then create a GlobalContext.

westerparse.westerparse.parseContext(cxt, show=None, partSelection=None, partLineType=None, report=False)

Run the parser for each line of a context using parsePart(). Collect error reports from the parser and to produce an error report. Create a separate report for successful parses. If the user has elected to display the results, select the preferred interpretations and display them.

  1. Create a dictionary of error reports for each part that is parsed.

  2. If the user has selected a part for evaluation, determine whether the selection is valid.

  3. If the user has selected a type of line, determine whether the selection is valid.

  4. Run the parser for the selected parts and collect errors, if any. For primary lines, check for compliance with rule G2.

  5. Determine the generability of the selected parts.

  6. If show is ‘parsedata’ and the parts are all generable, export the data for each parse as a json file.

  7. Create a parse report (text) to display to the user.

  8. Gather the sets of parses for the selected part(s). If the module variable ‘usePreferredParseSets’ is set to True, use Westergaard’s counterpoint preferences for 2- and 3-part counterpoint.

  9. [If show is ‘parsedata’ and the parts are all generable, export counterpoint data as a json file. (Not yet implemented.)]

  10. Based on the value of the ‘show’ variable, output the interpreted part(s).

westerparse.westerparse.validatePartSelection(cxt, partSelection)

Determine whether the selected part number is actually present in the score, and if so, select that part; if not, report the selection error to the user. If no part is selected by the user, all parts of the score will be parsed.

westerparse.westerparse.validateLineTypeSelection(cxt, partSelection, partLineType)

If the user has selected a line type, they must also have selected a single part for evaluation. If both selections were not made, report the error to the user.

westerparse.westerparse.parsePart(part, cxt)

Parse a given part. Create a (Parser) for the part and collect the results. Determine whether the line is generable as a primary, bass, or generic line. Compile a list of ways the line can be generated for each line type, if at all. Collect a list of parsing errors.

westerparse.westerparse.checkFinalStep(part, cxt)

Check primary lines for compliance with rule G2, which requires that at least one note in the penultimate measure has a clear step connection to the final note in the line. If no such connection is found, record the error.

westerparse.westerparse.writeParseDataLog(part)

Write line parse to a log file. Used for debugging.

westerparse.westerparse.getGenerability(cxt, partSelection)

Determine whether all parts are generable.

westerparse.westerparse.writeParseDataFiles(cxt)

Using the output of extractParseDataFromPart(), write json data files for each successfully parsed line, ignoring generic parses.

westerparse.westerparse.extractParseDataFromPart(cxt, part, parse)

Prepare a json data file for a particular parse. Each file contains three sets of data:

  1. Metadata: file name, part number, line type, parse label, species

  2. A data table for notes: index, rule, generative level, offset, scale degree, left paren, right paren.

  3. A data table for arcs: list of note indexes, category (basic, secondary), type (passing, neighboring, repetition, arpeggiation), direction, position in hierarchy, list of scale degrees.

westerparse.westerparse.createParseReport(cxt, generability, partsForParsing, partSelection, partLineType)

Create an optional parse report to be diplayed to the user and a required error report if errors arise.

westerparse.westerparse.gatherParseSets(cxt, partSelection=None, partLineType=None)

After parsing the individual lines, collect all the possible combinations of parses. If the module variable usePreferredParseSets is set to True, use Westergaard’s counterpoint preferences for 2- and 3-part counterpoint.

westerparse.westerparse.selectPreferredParseSets(cxt, primaryPartNum)

Negotiate the best match between the global structure of a given upper line and the global structure of the bass line. [This currently works only for two- and three-part counterpoint.]

westerparse.westerparse.extractCounterpointDataFromParseSets(cxt, parseSet, label)

Prepare a json data file for a particular set of parses. Each file contains n sets of data:

  1. Metadata: file name, species

  2. A data table for intervals.

  3. n data tables for arcs: list of note indexes, category (basic, secondary), type (passing, neighboring, repetition, arpeggiation), direction, position in hierarchy, list of scale degrees.

westerparse.westerparse.showParses(cxt, show, parseSets)

Show the interpretations. For each set of line parses, build the representation of each component line and then select the appropriate mode of representation (show).

westerparse.westerparse.assignSlurs(source, arcs, arcBasic=None)

Given a fully parsed line (an interpretation), sort through the arcs and create a music21 spanner (tie/slur) to represent each arc.

westerparse.westerparse.arcBuild(source, arc)

Translate an arc into a notated slur.

westerparse.westerparse.assignRules(source, rules)

Given a fully parsed line (an interpretation), add a lyric to each note to show the syntactic rule that generates the note. Also assigns a color to notes generated by a rule of basic structure.

westerparse.westerparse.assignParentheses(source, parentheses)

Add parentheses around notes generated as insertions. [This aspect of syntax representation cannot be fully implemented at this time, because musicxml only allows parentheses to be assigned in pairs, whereas syntax coding requires the ability to assign left and right parentheses separately.]

class westerparse.westerparse.Test(methodName='runTest')

Context

The Context module includes classes to represent both global and local contexts.

exception westerparse.context.ContextError(desc)
exception westerparse.context.EvaluationException
class westerparse.context.Context

An object for representing a span of a composition and for storing objects that represent smaller spans.

class westerparse.context.LocalContext
class westerparse.context.GlobalContext(score, **kwargs)

An object for representing a tonally unified span of a composition and for storing objects that represent local spans within the global context.

A global context consists of a music21 Score and its constituent Parts.

When a global context is created, several things happen automatically to prepare the score for evaluation.

  1. A key for the context is automatically validated or inferred using the Key Finder (keyFinder).

  2. For each part:

    • A part number is assigned.

    • The rhythmic species is identified.

    • A referential tonic scale degree (csd.value = 0) is selected.

    • A list is created to collect errors.

  3. For each note in the part:

    • A position index is assigned. This is the primary note reference used during parsing.

    • A concrete scale degree (ConcreteScaleDegree) is determined.

    • A Rule object is attached.

    • A Dependency object is attached.

    • The manner of approach and departure (Consecutions) for the note are determined.

  4. Measure-long local harmonic contexts are created, for use in parsing events in third species.

westerparse.context.validateHarmonicSegmentation(offPre, offDom, offClosTon, barDuration)

Use the offsets for the beginnings of harmonic spans and calcluate whether the segmentation conforms to the rules for harmonic species.

Key Finder

The Key Finder examines a music21 Stream and either validates a key provided by the user or infers an appropriate key.

Key inference begins by examining each part in the context to determine the scales in which the following criteria are met: first and last pitches are tonic-triad pitches, all pitches in the line belong to the scale, and at least one pitch in any leap is a triad pitch. The list of possibilities is collected in part.keyCandidatesFromScale. Then each part is examined to determine the keys in which only tonic-triad pitches are left hanging. The list of possibilities is collected in part.keyCandidatesFromHanging. The lists resulting from the first two steps are sifted see what possibilities are common to all parts. The results are collected in scoreKeyCandidates. If there are still multiple options for key, the list is winnowed using two preference rules: (a) prefer most lines to end on tonic degree, and (b) prefer major rather than minor if ambiguously mixed. If winnowed to one option, the appropriate major or melodic minor scale and key are assigned to the context, otherwise an exception is raised and the failure to find a single key is reported to the user.

Validation of a user-provided key involves two steps: the name of the key is tested for validity (‘Q# diminished’ is not a valid option) and the validated name is then tested using the same criteria as in key inference.

exception westerparse.keyFinder.KeyFinderError(desc)
westerparse.keyFinder.testKey(score, knote=None, kmode=None, kharm=False)

Validate and test a key provided by the user.

westerparse.keyFinder.inferKey(score)

Infer a key from the parts.

class westerparse.keyFinder.Test(methodName='runTest')

Parser

A transition-based dependency parser. The WesterParse parser analyzes the syntax of a rhythmically simple melodic line and produces a set of valid interpretations.

Procedure:

  1. Accept a part from a context.

  2. Infer the possible line type(s) if not given in advance.

  3. Parse the part for each possible line type.

  4. Return a set of parses and errors.

The machinery consists of a buffer, a stack, and a scanner. At initialization, the notes of the line are read into the buffer. The scanner then shifts notes onto the stack one by one. With each shift, the transition is evaluated in light of the previously analyzed line. The new note may be evaluated as (a) dependent upon an earlier note, (b) extending a dependency from an earlier note, (c) resolving an earlier note, or (d) independent. As the scanning proceeds, the parser maintains lists of open heads, open transitions, and syntactic units (arcs). These lists shrink and grow as the interpretive process unfolds. When an arc is formed (e.g., a passing or neighboring motion), a tuple of note positions is placed in the list of arcs. Meanwhile, dependent elements within the arc are removed from the list of open transitions, but structural heads are retained in the list of open heads for subsequent attachment. The parser has a limited ability to backtrack and reinterpret segments of a line.

The first stage of parsing ends when the buffer is exhausted. Interpretation then continues by line type.

The parser compiles lists of all the valid interpretations. The parser also records errors that arise.

class westerparse.parser.Parser(part, context, **kwargs)

The main engine of the parser.

The bulk of the parser’s work is done by parseTransition(). After a preliminary parse of the line, the parser decides on a set of possible structural interpretations and creates a Parse object to store each interpretation.

Upon initialization, the Parser automatically parses the line, using the following procedure:

  • Prepare placeholders for parses and errors.

  • Accept a line type if provided, otherwise infer the set of possible types.

  • Operate the preliminary parser: preParseLine().

  • Interrupt the parser if preliminary parsing is unsuccessful and report errors.

  • Determine the set of possible basic structures and test for each possibility: prepareParses().

  • Gather all the valid interpretations of the part by line type: collectParses().

  • Reduce the set of interpretations using preference rules: selectPreferredParses().

The individual parses are contained in a Parse. These are created by prepareParses().

inferLineTypes()

If the line type is not specified, infer a set of possibilities.

preParseLine()

Conduct a preliminary parse of the line. Initialize the buffer, stack, and arcs. Initialize the lists of open heads and transitions. Set the global harmonic referents. Run the scanner, parsing each transition.

parseTransition(stack, buffer, part, i, j, harmonyStart, harmonyEnd, openHeads, openTransitions, arcs)

Asks a series of questions at the transition from note i to note j.

  • Do i and j belong to the harmony of the context (tonic, in the case of global contexts)?

  • What is the intervallic relation between i and j (step or skip)?

  • How does j connect, if at all, with notes in the dynamic lists of open heads and transitions?

Based on the answers, the parser assigns dependency relations, creates arcs where warranted, or returns error messages if the line is syntactically malformed.

The specific cases are as follows:

  1. Both pitches are harmonic

    • If i and j are the same pitch, generate a repetition.

    • If there are open transitions, see whether j resolves a transition, starting with the most recent.

  2. Step from the harmony of this bar to the harmony of the next

    • If i is an open local transition, end the transition at j.

  3. Step from harmonic to nonharmonic pitch

    • If there are open transitions, see whether j continues a transition, starting with the most recent.

    • If there are no open transitions but there are open heads, try to attach j to an open head, starting with the most recent (i).

  4. Step from nonharmonic to harmonic pitch

    • In third species, add j to the local harmony if needed.

    • If there are no open transitions, see whether the directionality of i matches the direction of the step.

      • If so, make i the lefthead of j.

      • Otherwise, add i to the list of open transitions.

    • If there are open transitions, see whether j resolves a transition, starting with the most recent.

  5. Step from nonharmonic to nonharmonic

    • If the directionality of i and j match or i is bidirectional and j is ascending, …

    • If i is ascending and j is descending, …

    • If i is ascending and j is bidirectional, …

    • If i is bidirectional and j is descending, …

  6. Consonant skip from nonharmonic to harmonic

    • if i and j are linearly consonant …

  7. Consonant skip from harmonic to nonharmonic

    • This is one of the more complex cases, often necessitating revision of the interpretation in order to connect the new nonharmonic pitch to a previous pitch.

    • If there are open transitions:

      • See whether j continues a transition in progress.

      • If not, see whether j connects to a head that precedes the open transitions.

      • If neither of these works, return an error: j appears out of the blue and cannot be generated.

    • If there are open heads:

      • Look for an open head to attach to j.

      • If that fails, search for possible step-related antecedent (head or transition).

        • Look in reverse at the terminals of current arcs and select the most recent that is step-related.

        • Look for possible step-related transition that was previously integrated into a neighbor arc.

        • Look for possible step-related insertion that was embedded in another arc.

      • If neither of these works, return an error: j appears out of the blue and cannot be generated.

  8. Consonant skip from nonharmonic to nonharmonic

    • Return an error.

  9. Linear unison between nonharmonic pitches

    • Return an error.

  10. Dissonant skip

    • Return an error.

  11. Skip larger than an octave

    • Return an error.

prepareMonotriadicParses()

After preliminary parsing is completed, determines possibilities for basic structures in a monotriadic line based on available line types and parses the line using each candidate for basic structure. The results are collected in self.parses.

If the line type is bass, the function verifies that the line begins and ends on a tonic degree (rules S1 and S2) and then assembles a list of notes that could complete the basic arpeggiation (rule S3) and builds a Parse for each S3 candidate. (See parseBass().)

If the line type is primary, the function verifies that the line ends on a tonic degree (rule S1) and then assembles a list of notes that could initiate a basic step motion (rule S2). The function uses eight different methods to determine whether a valid basic step motion exists for each S2 candidate (see parsePrimary()) and attempts to build a Parse using each method; not every method yields a result.

If the line type is generic, the function verifies that the line begins and ends on triad pitches (rules S1 and S2) and then looks for a possible step connection between these terminal pitches (see parseGeneric()).

prepareHarmonicParses()

After preliminary parsing is completed, determine possibiities for basic structures in a harmonically progressive line using each structural head candidate. The results are collected in self.parses.

buildParse(cand, lineType, parsecounter, buildErrors, method=None, s4cand=None)

Sets up the basic features of the parse object and then executes the parsing process. Uses deep copies of the arcs and notes, as the list of arcs and the properties of notes will be altered during the process.

class Parse

An object for holding one interpretation of a line’s syntactic structure.

The object’s attributes include S1Index, S2Degree, S2Index, S2Value, S3Degree, S3Final, S3Index, S3Indexes, S3Initial, S3PenultCands, S3Value, label, arcs, tonic, mode, and arcBasic.

performLineParse()

Create a complete interpretation of the line, using the following procedure:

  • Construct an arc for the basic structure, given the line type and a specific option for the basic structure.

  • Assign rules to notes in secondary structures.

  • Test for resolution of local insertions in third species.

  • Consolidate arcs into longer passing motions, if possible

  • Assemble lists for rule labels and parentheses, to be used when generating representations of the interpretation.

  • Set the dependency level of each note [if function enabled].

arcMerge(arc1, arc2)

Combine two passing motions that share an inner node and direction.

arcEmbed(arc1, arc2)

Embed a repetition inside a passing motion.

arcExtend(arc, extension)

Lengthen an arc by adding a note at extension index to the right or left.

parsePrimary()

For monotriadic lines, use one of eight methods to find a basic step motion from a potential S2 (Kopfton):

  1. Look for one existing basic step motion arc that starts from S2.

  2. Look for an existing basic step motion arc that can be attached to S2 (repetition + passing)

  3. Look for two arcs that can be merged into a basic step motion (passing + passing).

  4. Look for three arcs that can be merged into a basic step motion (passing + passing + passing).

  5. Take an existing 5-4-3 arc (the longest spanned, if more than one) and try to find a connection -2-1 to complete a basic arc.

  6. Look for a nonfinal arc from S2 whose terminus == S1.csd.value, and extend the arc to end on S1Index if possible.

  7. Reinterpret the line, looking for a descending step motion from S2 and then parsing the remaining notes. The least reliable method.

For harmonic lines, use one of a dozen methods to merge a series of arcs into a basic step motion that connects with the final tonic.

parseBass()

Test whether a specific dominant pitch can function as S3.

parseGeneric()

The line has already passed the generic test, so all that is to be done is: determine whether there is a basic step motion connecting the first and last notes.

attachOpenheadsToStructuralLefthead(structuralLefthead, rightLimit)

Examine the span between a structural lefthead and a righthand limit, looking for notes that are either head of an arc (left or right) or not embedded in an arc, and can be taken as a repetition of the structural lefthead. This function increases the coherence of a parse.

integrateSecondaryWithPrimary()

Revise an intepretation to make it tighter, more efficient, more coherent. Connect secondary structures to elements of the basic structure where possible. [Not yet implemented.]

assignSecondaryRules()

Find any note that does not yet have a rule assigned and determine the appropriate rule based on its dependency.

testLocalResolutions()

For any note identified as a local insertion (rule L3), determine whether it is subsequently displaced stepwise to a tonic triad pitch. Record an error if not.

setDependencyLevels()

Review a completed parse and determine the structural level of each note.

  1. Assign levels to notes in the basic arc.

  2. Set the level of the first note if not in the basic arc.

  3. Collect all the secondary arcs. Examine each arc and make a list of all the spans filled with notes that are not components of the arc.

  4. Look at every span in the list, and see whether a dependent arc fits into it. This is the core of the function. Give priority to an arc that connects both edges of the span, then one that is tethered to the left edge, then the right edge, and then any that lie independently within the span. Give preference to the longer arcs. Once a choice is made, assign generative levels to the dependent arc components, revise the span list, and continue.

  5. Process any spans that contains only inserted pitches and no arcs, testing to ensure compliance with the restrictions of rule E3.

displayWestergaardParse()

Create a multileveled illustration of a parse of the sort used in Westergaard’s book. Activate by setting global variable showWestergaardInterpretations to True. Will eventually be added as a parse display option.

testGenerabilityFromLevels()

Given a parse in which rule levels have been assigned (perhaps by the student), determine whether the line is generable in that way. [Under development]

collectParses()

Collect all the attempted parses of a line in the Parser and discard any that have errors and thus failed. Also removes parses of primary lines if the same basic arc was produced by a more reliable method.

selectPreferredParses()

Given a list of successful interpretations from Parser, remove those that do not conform to cognitive preference rules.

  • Primary line preferences

    • For parses that share the same S2 scale degree, prefer the parse in which S2 occurs earliest.

  • Bass Lines

    • Prefer parses in which S3 occurs after the midpoint.

    • Prefer parses in which S3 occurs on the beat.

    • If there are two candidates for S3 and the second can be interpreted as a direct repetition of the first, prefer the parse that interprets the first candidate as S3.

westerparse.parser.isEmbeddedInOtherArc(arc, arcs, startIndex=0, stopIndex=-1)

Check whether an arc is embedded within another arc between two indices.

westerparse.parser.conflictsWithOtherArc(arc, arcs)

Check whether an arc is in conflict with any existing arc. Five types of relationship:

  1. overlap, always illegal

  2. congruence, always illegal

  3. containment, requires nesting test

  4. co-iniation, requires nesting test

  5. co-termination, requires nesting test

class westerparse.parser.Test(methodName='runTest')

Voice Leading Checker

The Voice Leading Checker module takes a score with two or more parts (lines) and examines the local voice leading for conformity with Westergaard’s rules of species counterpoint.

The module divides the score into small bits for analysis, bits such as pairs of simultaneous notes, complete verticalities, and voice-leading quartets. The voice-leading checker then analyzes these bits of data.

The base functions consist of the following:

getOffsetList(score)() - Compiles a list of timepoints when events are initiated in a score. Accepts as input either a duet of parts extracted from a score or all of the score parts.

getVerticalityContentDictFromDuet(duet, offset)() - Given a duet and a particular offset, this function constructs a dictionary of the notes still sounding or starting to sound at that offset. The dictionary is arranged by part number.

getVerticalPairs(duet)() - Uses getOffsetList(score)() to construct an ordered list of all the pairs of simultaneous notes occurring between the parts of the duet. Useful for checking the control of dissonance.

getAllVLQsFromDuet(duet)() - Uses getOffsetList(score)() to extract an ordered list of the VoiceLeadingQuartet objects in the duet. A voice-leading quartet (VLQ) consists of pairs of simultaneous notes in two parts:

  • v1: n1, n2

  • v2: n1, n2

VLQs are useful for checking forbidden forms of motion.

Modified versions of these functions are used to extra vertical pairs and VLQs that involve nonconsecutive simulataneities (see below).

Westergaard’s rules for combining lines cover four areas:

  • intervals between consecutive notes

  • intervals between simultaneous notes: dissonance

  • intervals between simultaneous notes: sonority

  • motion between pairs of simultaneously sounding notes

While most of the rules for intervals between consecutive notes are handled by the rules of linear syntax, there is one such rule that has a contrapuntal component: the rule that prohibits the implication of a six-four chord by controlling the situations in which the bass leaps a perfect fourth. (Another rule in this implementation (but not found in Westeraard) also has a contrapuntal aspect: the global rule that ensures a step connection from the penultimate measure to the final pitch of a primary line.)

The rules controlling leaps of a fourth in the bass, dissonance, and motion are absolutes, hence any infractions automatically yield an error report. The rules for controlling sonority, on the other hand, are rules of advice. Nonconformity with the sonority rules is only reported on demand [this option is not yet available].

The rules for forbidden forms of motion vary with the rhythmic situation:

  • on the beat to on the beat

  • on the beat to next on the beat

  • on the beat to immediately following off the beat

  • off the beat to immediately following on the beat

  • off the beat to next but not immediately following on the beat

  • off the beat to immediately following off the beat

  • off the beat to off the beat in the next bar (fourth species)

Many of the rules apply across species. For example, the rules of first species, originally formulated for consecutive beats (on the beat to on the beat) apply in all species whenever both lines move to new notes on the beat. This happens regularly in first, second, and third species, and at the end of fourth species, where the syncopations are broken. A single function (forbiddenMotionsOntoBeatWithoutSyncope()) checks these situations. Each species also has a set of rules that are peculiar to it, so there are separate functions for each of these.

Control of dissonance is checked by one of two functions: :py:func:’checkControlOfDissonance’ for first, second, and third species, or :py:func:’fourthSpeciesControlOfDissonance’.

[Etc.]

westerparse.vlChecker.checkCounterpoint(context, report=True)

This is the main script.

It creates a duet for each pair of parts in the score and then checks every duet for conformity with the rules that control dissonance and the rules that prohibit certain forms of motion. A separate function checks for the rules that control leaps of a fourth in the bass.

westerparse.vlChecker.checkDuet(context, duet)

Check the voice-leading of each duet, depending upon which simple species the pair represents (e.g., first, second, third, fourth). The function is not yet able to evaluate combined species.

westerparse.vlChecker.checkFirstSpecies(context, duet)

Check a duet, where both lines are in first species. Evaluate control of dissonance and forbidden forms of motion.

westerparse.vlChecker.checkSecondSpecies(context, duet)

Check a duet, where one line is in first species and the other is in second species. Check the intervals between consecutive notes (no local repetitions in the second species line). Evaluate control of dissonance and forbidden forms of motion, including the rules for nonconsecutive unisons and octaves.

westerparse.vlChecker.checkThirdSpecies(context, duet)

Check a duet, where one line is in first species and the other is in third species. Check the intervals between consecutive notes (no local repetitions in the third species line). Evaluate control of dissonance and forbidden forms of motion.

westerparse.vlChecker.checkFourthSpecies(context, duet)

Check a duet, where one line is in first species and the other is in fourth species. Check the intervals between consecutive notes (no local repetitions in the fourth species line). Evaluate control of dissonance and forbidden forms of motion.

westerparse.vlChecker.checkControlOfDissonance(context, duet, VLQs)

Check a duet for conformity with the rules that control dissonance in first, second, or third species. Requires access not only to notes in the duet but also the bass line, if not included in the duet.

On the beat: notes must be consonant.

Off the beat: notes may be dissonant but only if approached and left by step.

Off the beat: consecutive dissonances must be approached and left by step in the same direction.

westerparse.vlChecker.checkFourthSpeciesControlOfDissonance(context, duet, VLQs)

Check the duet for conformity the rules that control dissonance in fourth species.

westerparse.vlChecker.checkForbiddenMotionsOntoBeatWithoutSyncope(context, duet, vlq)

Check a voice-leading quartet for conformity with the rules that prohibit or restrict certain kinds of motion onto the beat:

  • similar motion to or from a unison

  • similar motion to an octave

  • similar motion to a fifth

  • parallel motion to unison, octave, or fifth

  • voice crossing, voice overlap, cross relation

westerparse.vlChecker.checkFirstSpeciesForbiddenMotions(context, duet, VLQs)

Check the forbidden forms of motion for a duet in first species. Essentially: forbiddenMotionsOntoBeatWithoutSyncope() and :py:funct:`checkFirstSpeciesNonconsecutiveParallels`.

westerparse.vlChecker.checkSecondSpeciesForbiddenMotions(context, duet, VLQs)

Check the forbidden forms of motion for a duet in second species. Use forbiddenMotionsOntoBeatWithoutSyncope() to check motion across the barline and then check motion from beat to beat.

westerparse.vlChecker.checkThirdSpeciesForbiddenMotions(context, duet, VLQs)

Check the forbidden forms of motion for a duet in third species. Use forbiddenMotionsOntoBeatWithoutSyncope() to check motion across the barline and then check motion from beat to beat, from off the beat to next but not immediately following on the beat.

westerparse.vlChecker.checkFourthSpeciesForbiddenMotions(context, duet, VLQs)

Check the forbidden forms of motion for a duet in fourth species. Mostly limited to looking for parallel unisons and octaves in consecutive meausures. Use forbiddenMotionsOntoBeatWithoutSyncope() to check motion across the barline whenever the syncopations are broken.

westerparse.vlChecker.checkFirstSpeciesNonconsecutiveParallels(context, duet)

Check for restrictions on nonconsecutive parallel unisons and octaves in first species.

westerparse.vlChecker.checkSecondSpeciesNonconsecutiveUnisons(duet)

Check for restrictions on nonconsecutive parallel unisons.

westerparse.vlChecker.checkSecondSpeciesNonconsecutiveOctaves(duet)

Check for restrictions on nonconsecutive parallel octaves.

westerparse.vlChecker.checkConsecutions(context)

Check the intervals between consecutive notes. If the line is in second or third species, confirm that there are no direct repetitions. If the line is in fourth species, confirm that the pitches of tied-over notes match and that there are no direct repetitions.

westerparse.vlChecker.checkFourthLeapsInBass(context)

Check fourth leaps in the bass to ensure that there is no implication of a six-four chord during the meausure in which the lower note of the fourth occurs.

westerparse.vlChecker.getAllPartNumPairs(score)

Assemble a list of the pairwise combinations of parts in a score. Adopted from music21’s theory analyzer

westerparse.vlChecker.getOffsetList(score)

Get a list of note/rest offsets for all event initiations in a score. Accepts a stream as input: duet, context.score Use as input to building the context dictionary of verticals

westerparse.vlChecker.getOnbeatOffsetList(score)

Get a list of offsets for all downbeats in a score. Accepts a stream as input: duet, context.score.

westerparse.vlChecker.getOffbeatOffsetList(score)

Get a list of offsets for all offbeats in a score. Accepts a stream as input: duet, context.score.

westerparse.vlChecker.getOnbeatIntervals(duet)

Extract an ordered list of onbeat intervals in the duet.

westerparse.vlChecker.getOffbeatIntervals(duet)

Extract an ordered list of offbeat intervals in the duet.

westerparse.vlChecker.getGenericKlangs(score)

Extract a list of generic intervals above the bass for a score with any number of parts

westerparse.vlChecker.getVerticalityContentDictFromDuet(duet, offset)

Assume that the parts in a duet contain a single note or rest each at any given offset. Get the content of each part at the offset and construct a content dictionary: the keys are part numbers in the duet and the values are notes (rests).

westerparse.vlChecker.getVerticalContentDictionariesList(score, offsets='all')

Generate an offset list for a score, then construct a content dictionary for the parts at every offset: the keys are part numbers in the duet and the values are notes (rests). Accepts as input: duet, context.score.

westerparse.vlChecker.getVerticalPairs(duet)

Generate an offset list for the duet and then construct a list of all the simultaneities (vertical pairs of notes) occurring between the parts of the duet.

westerparse.vlChecker.getAllVLQsFromDuet(duet)

Generate an offset list for the duet, make a pairwise list of offsets, and then construct a list of all the VoiceLeadingQuartet objects in the duet.

westerparse.vlChecker.getOnbeatVLQs(duet)

Generate an offset list of downbeats in the duet, make a pairwise list from it, and then construct a list of VoiceLeadingQuartet objects.

westerparse.vlChecker.getNonconsecutiveOffbeatToOnbeatVLQs(duet)

Generate an offset list for the duet in which the first offset is off the beat and the second is on the next downbeat, and then construct a list of VoiceLeadingQuartet objects.

westerparse.vlChecker.makeVLQFromVPairList(vPairList)

Given a list of simultaneous note pairs, create a voice-leading quartet for each consecutive pair of pairs and return the list of VLQs.

westerparse.vlChecker.makeVLQfromVertPairs(vpair1, vpair2)

Given a pair of simultaneous note pairs, create a voice-leading quartet.

westerparse.vlChecker.getFourthLeapsInBassDict(context)

Identify P4 intervals in the bass part and make a list of the note pairs.

westerparse.vlChecker.isConsonanceAboveBass(b, u)

Input two notes with pitch, a bass note and an upper note, and determine whether the pair forms a vertical consonance. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P1’, ‘m3’, ‘M3’, ‘P5’, ‘m6’, ‘M6’. Equivalent to music21.Interval.isConsonant().

westerparse.vlChecker.isThirdOrSixthAboveBass(b, u)

Input two notes with pitch, a bass note and an upper note, and determine whether the pair forms a vertical third or sixth. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘m3’, ‘M3’, ‘m6’, ‘M6’.

westerparse.vlChecker.isConsonanceBetweenUpper(u1, u2)

Input two notes with pitch, two upper-line notes, and determine whether the pair forms a vertical consonance. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P1’, ‘m3’, ‘M3’, ‘P4’, ‘P5’, ‘m6’, ‘M6’.

westerparse.vlChecker.isPermittedDissonanceBetweenUpper(u1, u2)

Input two notes with pitch, two upper-line notes, and determine whether the pair forms a permitted vertical dissonance. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P4’, ‘A4’, ‘d5’. Each note requires additional test with bass: isThirdOrSixthAboveBass().

westerparse.vlChecker.isTriadicConsonance(n1, n2)

Input two notes, from any context, and determine whether the pair forms a triadic interval in a consonant triad (major or minor). The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P1’, ‘m3’, ‘M3’, ‘P4’, ‘P5’, ‘m6’, ‘M6’.

westerparse.vlChecker.isTriadicInterval(n1, n2)

Input two notes, from any context, and determine whether the pair forms a triadic interval in any type of triad (major, minor, diminished, augmented). The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P1’, ‘m3’, ‘M3’, ‘P4’, ‘A4’, ‘d5’, ‘P5’, ‘m6’, ‘M6’.

westerparse.vlChecker.isPerfectVerticalConsonance(n1, n2)

Input two simultaneous notes with pitch and determine whether the pair forms a perfect vertical consonance. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘P1’, ‘P5’, ‘P8’.

westerparse.vlChecker.isImperfectVerticalConsonance(n1, n2)

Input two simultaneous notes with pitch and determine whether the pair forms an imperfect vertical consonance. The test determines whether the simple interval equivalent of the actual interval is in the list: ‘m3’, ‘M3’, ‘m6’, ‘M6’.

westerparse.vlChecker.isVerticalDissonance(n1, n2)

Input two simultaneous notes with pitch and determine whether the pair forms a vertical dissonance. The test determines whether the simple interval equivalent of the actual interval is not in the list: ‘P1’, ‘P5’, ‘P8’, ‘m3’, ‘M3’, ‘m6’, ‘M6’.

westerparse.vlChecker.isDiatonicStep(n1, n2)

Input two notes with pitch and determine whether the pair forms a diatonic step. The test determines whether the actual interval is in the list: ‘m2’, ‘M2’.

westerparse.vlChecker.isUnison(n1, n2)

Input two notes with pitch and determine whether the pair forms a unison. The test determines whether the actual interval is in the list: ‘P1’.

westerparse.vlChecker.isOctave(n1, n2)

Input two notes with pitch and determine whether the pair forms an octave. The test determines whether the actual interval is in the list: ‘P8’, ‘P15’, ‘P22’.

westerparse.vlChecker.isSimilarUnison(vlq)

Input a VLQ and determine whether there is similar motion to a unison.

westerparse.vlChecker.isSimilarFromUnison(vlq)

Input a VLQ and determine whether there is similar motion from a unison.

westerparse.vlChecker.isSimilarFifth(vlq)

Input a VLQ and determine whether there is similar motion to a perfect fifth (simple or compound).

westerparse.vlChecker.isSimilarOctave(vlq)

Input a VLQ and determine whether there is similar motion to an octave (simple or compound).

westerparse.vlChecker.isParallelUnison(vlq)

Input a VLQ and determine whether there is parallel motion from one unison to another.

westerparse.vlChecker.isParallelFifth(vlq)

Input a VLQ and determine whether there is parallel motion to a perfect fifth (the first fifth need not be perfect).

westerparse.vlChecker.isParallelOctave(vlq)

Input a VLQ and determine whether there is parallel motion from one octave (simple or compound) to another.

westerparse.vlChecker.isVoiceOverlap(vlq)
Input a VLQ and determine whether the voices overlap:

v1n1 >= v2n1 and v1n2 >= v2n2, and either v1n2 < v2n1 or v2n2 > v1n1.

westerparse.vlChecker.isVoiceCrossing(vlq)

Input a VLQ and determine whether the voices cross: v1n1 >= v2n1, and v1n2 < v2n2.

westerparse.vlChecker.isCrossRelation(vlq)

Input a VLQ and determine whether the there is a cross relation. The test determines whether the simple interval of either contiguous interval is in the list: ‘d1’, ‘A1’.

westerparse.vlChecker.isDisplaced(vlq)
Input a VLQ and determine whether either of the pitches in the first

verticality is displaced by a pitch in the second verticality. The test determines whether the simple interval of any contiguous or consecutive interval is not in the list: ‘P1’, ‘m3’, ‘M3’, ‘P4’, ‘P5’, ‘m6’, ‘M6’.

westerparse.vlChecker.isOnbeat(note)

Tests whether a note is initiated on the downbeat.

westerparse.vlChecker.isSyncopated(score, note)

Test whether a note is syncopated. [Not yet functional]

class westerparse.vlChecker.Test(methodName='runTest')

Dependency

The Dependency class stores syntactic dependency relationships among notes in the form of references to a lefthand head, a righthand head, and dependents.

A passing tone in the space of a third, for example, stores a reference to a lefthead and a righthead, but not to any dependents. The note that initiates the passing motion, by contrast, has one dependent (the passing tone) but no left- or righthead. An anticipation stores a reference to a righthead, but has no lefthead and no dependents.

exception westerparse.dependency.DependencyException
class westerparse.dependency.Dependency(*args, **kwargs)

An object for storing the dependencies of a note in a line

Rule

The Rule class stores information about a note’s syntactic function.

class westerparse.rule.Rule(name=None, lineType=None, scope=None, level=None, index=None)

A rule that can be assigned as an attribute to a Note in a Line. A rule has a name (e.g., ‘S2’) and a level. For display purposes, rule names are appended to notes as lyrics.

Concrete Scale Degree (CSD)

exception westerparse.csd.CSDError(desc)
class westerparse.csd.ConcreteScaleDegree(p, scale)

A scale degree value based on an actual pitch object. Tonic = 0. Leading tone = -1. The upper octave = 7. The fifth above = 4. Scale degree residue classes are easily inferred from these values using mod7.

Arc

The Arc class stores information about an arc’s syntactic function.

class westerparse.arc.Arc(arc=None)

An arc is a syntactic unit that comprises at least two notes. The main content of an arc is a list of note indexes. Arcs are generated by the syntax parser. Types include passing, neighboring, repetition, and arpeggiation.

Consecutions

The Consecutions class stores information about how a note in a line is approached and left. This should be converted to a method.

class westerparse.consecutions.Consecutions(targetNote, leftNote=None, rightNote=None)

An object holding the generic types of melodic consecution for a note to the left and right (approach and departure): the interval, its direction, (-1, 0, 1) and the consecution type (‘same’, ‘step’, ‘skip’, None) are calculated from a three-note linear segment.

westerparse.consecutions.getConsecutions(idx, part)

Given a note index in a part, set the note’s Consecution object if not already extant.

class westerparse.consecutions.Test(methodName='runTest')
westerparse.utilities.pairwise(span)

s -> (s0, s1), (s1, s2), (s2, s3), …

westerparse.utilities.pairwiseFromLists(list1, list2)

return permutations from two lists