Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup on TSV-DCML Converter #1716

Merged
merged 9 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading