Skip to content

Commit

Permalink
Merge pull request #1716 from cuthbertLab/tsv-cleanup
Browse files Browse the repository at this point in the history
Cleanup on TSV-DCML Converter
  • Loading branch information
mscuthbert authored Jun 14, 2024
2 parents 4716deb + 3540deb commit 204e9d0
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 27 deletions.
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ disable=
consider-using-f-string, # future?
unnecessary-lambda-assignment, # opinionated
consider-using-generator, # generators are less performant for small container sizes, like most of ours

not-an-iterable, # false positives on RecursiveIterator in recent pylint versions.
unpacking-non-sequence, # also getting false positives.
# 'protected-access', # this is an important one, but for now we do a lot of
# # x = copy.deepcopy(self); x._volume = ... which is not a problem...

Expand Down
8 changes: 3 additions & 5 deletions music21/figuredBass/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def exampleA():
>>> fbRealization2 = fbLine.realize(fbRules)
>>> fbRealization2.keyboardStyleOutput = False
>>> fbRealization2.getNumSolutions()
3713168
3564440
>>> #_DOCS_SHOW fbRealization2.generateRandomRealization().show()
.. image:: images/figuredBass/fbExamples_sol2A.*
Expand Down Expand Up @@ -111,7 +111,6 @@ def exampleD():
figured bass, and fbLine is realized again. Voice overlap can be seen in the fourth
measure.
>>> fbRules.forbidVoiceOverlap = False
>>> fbRealization2 = fbLine.realize(fbRules)
>>> fbRealization2.getNumSolutions()
Expand All @@ -124,12 +123,11 @@ def exampleD():
Now, the restriction on voice overlap is reset, but the restriction on the upper parts
being within a perfect octave of each other is removed. fbLine is realized again.
>>> fbRules.forbidVoiceOverlap = True
>>> fbRules.upperPartsMaxSemitoneSeparation = None
>>> fbRealization3 = fbLine.realize(fbRules)
>>> fbRealization3.getNumSolutions()
29629539
27445876
>>> fbRealization3.keyboardStyleOutput = False
>>> #_DOCS_SHOW fbRealization3.generateRandomRealization().show()
Expand Down Expand Up @@ -177,7 +175,7 @@ def exampleB():
>>> fbRules.forbidIncompletePossibilities = False
>>> fbRealization2 = fbLine.realize(fbRules)
>>> fbRealization2.getNumSolutions()
188974
159373
>>> #_DOCS_SHOW fbRealization2.generateRandomRealization().show()
.. image:: images/figuredBass/fbExamples_sol2B.*
Expand Down
63 changes: 52 additions & 11 deletions music21/figuredBass/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
.. note:: The terminology, V43, viio, iv, etc. are explained
more fully in *The Music Theory Handbook*
by Marjorie Merryman.
.. note:: The terminology, V43, viio, iv, etc. are explained elsewhere,
such as *The Music Theory Handbook* by Marjorie Merryman.
This module contains methods which can properly resolve
`dominant seventh <https://en.wikipedia.org/wiki/Dominant_seventh_chord>`_,
Expand All @@ -26,6 +24,7 @@
'''
from __future__ import annotations

import typing as t
import unittest

from music21 import exceptions21
Expand All @@ -35,12 +34,15 @@
from music21 import stream


def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInfo=None):
def augmentedSixthToDominant(
augSixthPossib,
augSixthType: int | None = None,
augSixthChordInfo: list[pitch.Pitch | None] | None = None
) -> tuple[pitch.Pitch, ...]:
'''
Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3)
augmented sixth chords to the root position dominant triad.
Proper Italian augmented sixth resolutions not supported within this method.
>>> from music21.figuredBass import resolution
Expand Down Expand Up @@ -97,10 +99,20 @@ def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInf
elif augSixthChord.isSwissAugmentedSixth():
augSixthType = 3

if t.TYPE_CHECKING:
assert augSixthChordInfo is not None
if augSixthType in (1, 3):
[bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth
elif augSixthType == 2:
[bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh
else:
raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}')

if t.TYPE_CHECKING:
assert isinstance(bass, pitch.Pitch)
assert isinstance(root, pitch.Pitch)
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
Expand All @@ -111,7 +123,11 @@ def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInf
return _resolvePitches(augSixthPossib, howToResolve)


def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordInfo=None):
def augmentedSixthToMajorTonic(
augSixthPossib,
augSixthType: int | None = None,
augSixthChordInfo: list[pitch.Pitch | None] | None = None
) -> tuple[pitch.Pitch, ...]:
'''
Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3)
augmented sixth chords to the major tonic 6,4.
Expand Down Expand Up @@ -167,10 +183,21 @@ def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordI
elif augSixthChord.isSwissAugmentedSixth():
augSixthType = 3

if t.TYPE_CHECKING:
assert augSixthChordInfo is not None

if augSixthType in (1, 3):
[bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth
elif augSixthType == 2:
[bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh
else:
raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}')

if t.TYPE_CHECKING:
assert isinstance(bass, pitch.Pitch)
assert isinstance(root, pitch.Pitch)
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
Expand All @@ -182,12 +209,15 @@ def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordI
return _resolvePitches(augSixthPossib, howToResolve)


def augmentedSixthToMinorTonic(augSixthPossib, augSixthType=None, augSixthChordInfo=None):
def augmentedSixthToMinorTonic(
augSixthPossib,
augSixthType: int | None = None,
augSixthChordInfo: list[pitch.Pitch | None] | None = None
) -> tuple[pitch.Pitch, ...]:
'''
Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3)
augmented sixth chords to the minor tonic 6,4.
Proper Italian augmented sixth resolutions not supported within this method.
>>> from music21.figuredBass import resolution
Expand Down Expand Up @@ -238,10 +268,21 @@ def augmentedSixthToMinorTonic(augSixthPossib, augSixthType=None, augSixthChordI
elif augSixthChord.isSwissAugmentedSixth():
augSixthType = 3

if t.TYPE_CHECKING:
assert augSixthChordInfo is not None

if augSixthType in (1, 3):
[bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth
elif augSixthType == 2:
[bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh
else:
raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}')

if t.TYPE_CHECKING:
assert isinstance(bass, pitch.Pitch)
assert isinstance(root, pitch.Pitch)
assert isinstance(fifth, pitch.Pitch)
assert isinstance(other, pitch.Pitch)

howToResolve = [(lambda p: p.name == bass.name, '-m2'),
(lambda p: p.name == root.name, 'm2'),
Expand Down Expand Up @@ -727,14 +768,14 @@ def _transpose(samplePitch, intervalString):
return samplePitch.transpose(intervalString)


def _resolvePitches(possibToResolve, howToResolve):
def _resolvePitches(possibToResolve, howToResolve) -> tuple[pitch.Pitch, ...]:
'''
Takes in a possibility to resolve and a list of (lambda function, intervalString)
pairs and transposes each pitch by the intervalString corresponding to the lambda
function that returns True when applied to the pitch.
'''
howToResolve.append((lambda p: True, 'P1'))
resPitches = []
resPitches: list[pitch.Pitch] = []
for samplePitch in possibToResolve:
for (expression, intervalString) in howToResolve:
if expression(samplePitch):
Expand Down
21 changes: 13 additions & 8 deletions music21/romanText/tsvConverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import abc
import csv
import fractions
import pathlib
import re
import string
import types
Expand Down Expand Up @@ -266,7 +267,7 @@ def _changeRepresentation(self) -> None:
self.chord = re.sub(
r'''
(\d+) # match one or more digits
(?![\]\d]) # without a digit or a ']' to the right
(?![]\d]) # without a digit or a ']' to the right
''',
r'd\1',
self.chord,
Expand Down Expand Up @@ -521,7 +522,7 @@ class TsvHandler:
'I'
'''
def __init__(self, tsvFile: str, dcml_version: int = 1):
def __init__(self, tsvFile: str|pathlib.Path, dcml_version: int = 1):
if dcml_version == 1:
self.heading_names = HEADERS[1]
self._tab_chord_cls: type[TabChordBase] = TabChord
Expand Down Expand Up @@ -653,6 +654,7 @@ def prepStream(self) -> stream.Score:
'''
s = stream.Score()
p = stream.Part()
m: stream.Measure|None = None
if self.dcml_version == 1:
# This sort of metadata seems to have been removed altogether from the
# v2 files
Expand Down Expand Up @@ -733,7 +735,7 @@ def prepStream(self) -> stream.Score:
currentMeasureLength = newTS.barDuration.quarterLength

previousMeasure = entry.measure
if repeatBracket is not None:
if repeatBracket is not None and m is not None: # m should always be not None...
repeatBracket.addSpannedElements(m)

s.append(p)
Expand Down Expand Up @@ -762,7 +764,6 @@ class M21toTSV:
>>> tsvData[1][DCML_V2_HEADERS.index('chord')]
'I'
'''

def __init__(self, m21Stream: stream.Score, dcml_version: int = 2):
self.version = dcml_version
self.m21Stream = m21Stream
Expand Down Expand Up @@ -871,6 +872,8 @@ def _m21ToTsv_v2(self) -> list[list[str]]:
thisEntry.numeral = '@none'
thisEntry.chord = '@none'
else:
if t.TYPE_CHECKING:
assert isinstance(thisRN, roman.RomanNumeral)
local_key = localKeyAsRn(thisRN.key, global_key_obj)
relativeroot = None
if thisRN.secondaryRomanNumeral:
Expand Down Expand Up @@ -911,7 +914,7 @@ def _m21ToTsv_v2(self) -> list[list[str]]:
tsvData.append(thisInfo)
return tsvData

def write(self, filePathAndName: str):
def write(self, filePathAndName: str|pathlib.Path):
'''
Writes a list of lists (e.g. from m21ToTsv()) to a tsv file.
'''
Expand Down Expand Up @@ -974,16 +977,19 @@ def handleAddedTones(dcmlChord: str) -> str:
'Viio7[no3][no5][addb4]/V'
When in root position, 7 does not replace 8:
>>> romanText.tsvConverter.handleAddedTones('vi(#74)')
'vi[no3][add#7][add4]'
When not in root position, 7 does replace 8:
>>> romanText.tsvConverter.handleAddedTones('ii6(11#7b6)')
'ii6[no8][no5][add11][add#7][addb6]'
'0' can be used to indicate root-replacement by 7 in a root-position chord.
We need to change '0' to '7' because music21 changes the 0 to 'o' (i.e.,
a diminished chord).
>>> romanText.tsvConverter.handleAddedTones('i(#0)')
'i[no1][add#7]'
'''
Expand All @@ -1001,8 +1007,8 @@ def handleAddedTones(dcmlChord: str) -> str:
return 'Cad64' + secondary
added_tone_tuples: list[tuple[str, str, str, str]] = re.findall(
r'''
(\+|-)? # indicates whether to add or remove chord factor
(\^|v)? # indicates whether tone replaces chord factor above/below
([+\-])? # indicates whether to add or remove chord factor
([\^v])? # indicates whether tone replaces chord factor above/below
(\#+|b+)? # alteration
(1\d|\d) # figures 0-19, in practice 0-14
''',
Expand Down Expand Up @@ -1134,7 +1140,6 @@ def getLocalKey(local_key: str, global_key: str, convertDCMLToM21: bool = False)
>>> romanText.tsvConverter.getLocalKey('vii', 'a', convertDCMLToM21=True)
'g'
'''
if convertDCMLToM21:
local_key = characterSwaps(local_key, minor=isMinor(global_key[0]), direction='DCML-m21')
Expand Down
4 changes: 2 additions & 2 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4137,7 +4137,7 @@ def getElementAtOrBefore(
# TODO: allow sortTuple as a parameter (in all getElement...)

candidates = []
offset = opFrac(offset)
offset: OffsetQL = opFrac(offset)
nearestTrailSpan = offset # start with max time

sIterator = self.iter()
Expand All @@ -4147,7 +4147,7 @@ def getElementAtOrBefore(

# need both _elements and _endElements
for e in sIterator:
span = opFrac(offset - self.elementOffset(e))
span: OffsetQL = opFrac(offset - self.elementOffset(e))
# environLocal.printDebug(['e span check', span, 'offset', offset,
# 'e.offset', e.offset, 'self.elementOffset(e)', self.elementOffset(e), 'e', e])
if span < 0 or (span == 0 and _beforeNotAt):
Expand Down

0 comments on commit 204e9d0

Please sign in to comment.