-
Notifications
You must be signed in to change notification settings - Fork 383
/
roman.py
4592 lines (3722 loc) · 158 KB
/
roman.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Name: roman.py
# Purpose: music21 classes for doing Roman Numeral / Tonal analysis
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
#
# Copyright: Copyright © 2011-2023 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
Music21 class for dealing with Roman Numeral analysis
'''
from __future__ import annotations
import copy
import enum
import re
import typing as t
import unittest
from collections import namedtuple
from music21 import chord
from music21 import common
from music21 import defaults
from music21 import environment
from music21 import exceptions21
from music21.figuredBass import notation as fbNotation
from music21 import harmony
from music21 import interval
from music21 import key
from music21 import note
from music21 import pitch
from music21 import scale
FigureTuple = namedtuple('FigureTuple', ['aboveBass', 'alter', 'prefix'])
ChordFigureTuple = namedtuple('ChordFigureTuple', ['aboveBass', 'alter', 'prefix', 'pitch'])
environLocal = environment.Environment('roman')
# TODO: setting inversion should change the figure
T = t.TypeVar('T', bound='RomanNumeral')
# -----------------------------------------------------------------------------
SHORTHAND_RE = re.compile(r'#*-*b*o*[1-9xyz]')
ENDWITHFLAT_RE = re.compile(r'[b\-]$')
# cache all Key/Scale objects created or passed in; re-use
# permits using internally scored pitch segments
_scaleCache: dict[str, scale.ConcreteScale] = {}
_keyCache: dict[str, key.Key] = {}
# create a single notation object for RN initialization, for type-checking,
# but it will always be replaced.
_NOTATION_SINGLETON = fbNotation.Notation()
# only some figures imply a root other than the bass (e.g. "54" does not)
FIGURES_IMPLYING_ROOT: tuple[tuple[int, ...], ...] = (
# triads
(6,), (6, 3), (6, 4),
# seventh chords
(6, 5, 3), (6, 5), (6, 4, 3), (4, 3), (6, 4, 2), (4, 2), (2,),
# ninth chords
(7, 6, 5, 3), (6, 5, 4, 3), (6, 4, 3, 2), (7, 5, 3, 2),
# eleventh chords
(9, 7, 6, 5, 3), (7, 6, 5, 4, 3), (9, 6, 5, 4, 3), (9, 7, 6, 4, 3), (7, 6, 5, 4, 2),
)
def _getKeyFromCache(keyStr: str) -> key.Key:
'''
get a key from the cache if it is there; otherwise
create a new key and put it in the cache and return it.
'''
if keyStr in _keyCache:
# adding copy.copy will at least prevent small errors at a cost of only 3 nanoseconds
# of added time. A deepcopy, unfortunately, take 2.8ms, which is longer than not
# caching at all. And not caching at all really slows down things like RomanText.
# This at least will prevent what happens if `.key.mode` is changed
keyObj = copy.copy(_keyCache[keyStr])
else:
keyObj = key.Key(keyStr)
_keyCache[keyObj.tonicPitchNameWithCase] = keyObj
return keyObj
# figure shorthands for all modes.
figureShorthands = {
'53': '',
'3': '',
'63': '6',
'753': '7',
'75': '7', # controversial perhaps
'73': '7', # controversial perhaps
'9753': '9',
'975': '9', # controversial perhaps
'953': '9', # controversial perhaps
'97': '9', # controversial perhaps
'95': '9', # controversial perhaps
'93': '9', # controversial perhaps
'653': '65',
'6b53': '6b5',
'643': '43',
'642': '42',
# '6b5bb3': 'o65',
'bb7b5b3': 'o7',
'b7b5b3': 'ø7',
'bb7b53': 'o7',
'b7b53': 'ø7',
}
figureShorthandsMode: dict[str, dict] = {
'major': {
},
'minor': {
}
}
# this is sort of a crock... :-) but it's very helpful.
functionalityScores: dict[str, int] = {
'I': 100,
'i': 90,
'V7': 80,
'V': 70,
'V65': 68,
'I6': 65,
'V6': 63,
'V43': 61,
'I64': 60,
'IV': 59,
'i6': 58,
'viio7': 57,
'V42': 55,
'viio65': 53,
'viio6': 52,
'#viio65': 51,
'ii': 50,
'#viio6': 49,
'ii65': 48,
'ii43': 47,
'ii42': 46,
'IV6': 45,
'ii6': 43,
'VI': 42,
'#VI': 41,
'vi': 40,
'viio': 39,
'#viio': 38,
'iio': 37, # common in Minor
'iio42': 36,
'bII6': 35, # Neapolitan
'It6': 34,
'Ger65': 33,
'iio43': 32,
'iio65': 31,
'Fr43': 30,
'#vio': 28,
'#vio6': 27,
'III': 22,
'Sw43': 21,
'v': 20,
'VII': 19,
'VII7': 18,
'IV65': 17,
'IV7': 16,
'iii': 15,
'iii6': 12,
'vi6': 10,
}
# -----------------------------------------------------------------------------
def expandShortHand(shorthand):
'''
Expands shorthand notation into a list with all figures expanded:
>>> roman.expandShortHand('64')
['6', '4']
>>> roman.expandShortHand('973')
['9', '7', '3']
>>> roman.expandShortHand('11b3')
['11', 'b3']
>>> roman.expandShortHand('b13#9-6')
['b13', '#9', '-6']
>>> roman.expandShortHand('-')
['5', '-3']
Slashes don't matter:
>>> roman.expandShortHand('6/4')
['6', '4']
Note that this is not where abbreviations get expanded:
>>> roman.expandShortHand('')
[]
>>> roman.expandShortHand('7') # not 7, 5, 3
['7']
>>> roman.expandShortHand('4/3') # not 6, 4, 3
['4', '3']
Note that this is ['6'] not ['6', '3']:
>>> roman.expandShortHand('6')
['6']
Returns a list of individual shorthands.
'''
shorthand = shorthand.replace('/', '') # this line actually seems unnecessary.
if ENDWITHFLAT_RE.match(shorthand):
shorthand += '3'
shorthand = re.sub('11', 'x', shorthand)
shorthand = re.sub('13', 'y', shorthand)
shorthand = re.sub('15', 'z', shorthand)
shorthandGroups = SHORTHAND_RE.findall(shorthand)
if len(shorthandGroups) == 1 and shorthandGroups[0].endswith('3'):
shorthandGroups = ['5', shorthandGroups[0]]
shGroupOut = []
for sh in shorthandGroups:
sh = re.sub('x', '11', sh)
sh = re.sub('y', '13', sh)
sh = re.sub('z', '15', sh)
shGroupOut.append(sh)
return shGroupOut
def correctSuffixForChordQuality(chordObj, inversionString):
'''
Correct a given inversionString suffix given a chord of various qualities.
>>> c = chord.Chord('E3 C4 G4')
>>> roman.correctSuffixForChordQuality(c, '6')
'6'
>>> c = chord.Chord('E3 C4 G-4')
>>> roman.correctSuffixForChordQuality(c, '6')
'o6'
'''
fifthType = chordObj.semitonesFromChordStep(5)
if fifthType == 6:
qualityName = 'o'
elif fifthType == 8:
qualityName = '+'
else:
qualityName = ''
if inversionString and (inversionString.startswith('o')
or inversionString.startswith('°')
or inversionString.startswith('/o')
or inversionString.startswith('ø')):
if qualityName == 'o': # don't call viio7, viioo7.
qualityName = ''
seventhType = chordObj.semitonesFromChordStep(7)
if seventhType and fifthType == 6:
# there is a seventh and this is a diminished 5
if seventhType == 10 and qualityName == 'o':
qualityName = 'ø'
elif seventhType != 9:
pass # do something for extremely odd chords built on diminished triad.
# print(inversionString, fifthName)
return qualityName + inversionString
def postFigureFromChordAndKey(chordObj, keyObj=None):
'''
(Note: this will become a private function by v10.)
Returns the post-RN figure for a given chord in a given key.
If keyObj is none, it uses the root as a major key:
>>> roman.postFigureFromChordAndKey(
... chord.Chord(['F#2', 'D3', 'A-3', 'C#4']),
... key.Key('C'),
... )
'o6#5b3'
The function substitutes shorthand (e.g., '6' not '63')
>>> roman.postFigureFromChordAndKey(
... chord.Chord(['E3', 'C4', 'G4']),
... key.Key('C'),
... )
'6'
>>> roman.postFigureFromChordAndKey(
... chord.Chord(['E3', 'C4', 'G4', 'B-5']),
... key.Key('F'),
... )
'65'
>>> roman.postFigureFromChordAndKey(
... chord.Chord(['E3', 'C4', 'G4', 'B-5']),
... key.Key('C'),
... )
'6b5'
We reduce common omissions from seventh chords to be '7' instead
of '75', '73', etc.
>>> roman.postFigureFromChordAndKey(
... chord.Chord(['A3', 'E-4', 'G-4']),
... key.Key('b-'),
... )
'o7'
OMIT_FROM_DOCS
Fails on German Augmented 6th chords in root position. Calls them
half-diminished chords.
(This is in OMIT...)
'''
if keyObj is None:
keyObj = key.Key(chordObj.root())
chordFigureTuples = figureTuples(chordObj, keyObj)
bassFigureAlter = chordFigureTuples[0].alter
allFigureStringList = []
third = chordObj.third
fifth = chordObj.fifth
# seventh = chordObj.seventh
chordCardinality = chordObj.pitchClassCardinality
if chordCardinality != 3:
chordObjIsStandardTriad = False
isMajorTriad = False
isMinorTriad = False
else:
isMajorTriad = chordObj.isMajorTriad()
# short-circuit this expensive call if we know it's not going to be true.
isMinorTriad = False if isMajorTriad else chordObj.isMinorTriad()
chordObjIsStandardTriad = (
isMajorTriad
or isMinorTriad
or chordObj.isDiminishedTriad() # check most common first
or chordObj.isAugmentedTriad() # then least common.
)
for ft in sorted(chordFigureTuples,
key=lambda tup: (-1 * tup.aboveBass, tup.alter, tup.pitch.ps)):
# (diatonicIntervalNum, alter, alterStr, pitchObj) = figureTuple
prefix = ft.prefix
if ft.aboveBass != 1 and ft.pitch is third:
if isMajorTriad or isMinorTriad:
prefix = '' # alterStr[1:]
# elif isMinorTriad and ft.alter > 0:
# prefix = '' # alterStr[1:]
elif (ft.aboveBass != 1
and ft.pitch is fifth
and chordObjIsStandardTriad):
prefix = '' # alterStr[1:]
if ft.aboveBass == 1:
if ft.alter != bassFigureAlter and prefix != '':
# mark altered octaves as 8 not 1
figureString = prefix + '8'
if figureString not in allFigureStringList:
# filter duplicates and put at beginning
allFigureStringList.insert(0, figureString)
else:
figureString = prefix + str(ft.aboveBass)
# filter out duplicates.
if figureString not in allFigureStringList:
allFigureStringList.append(figureString)
allFigureString = ''.join(allFigureStringList)
key_mode = keyObj.mode
# first is not currently used.
if key_mode in figureShorthandsMode and allFigureString in figureShorthandsMode[key_mode]:
allFigureString = figureShorthandsMode[allFigureString]
elif allFigureString in figureShorthands:
allFigureString = figureShorthands[allFigureString]
# simplify common omissions from 7th chords
if allFigureString in ('75', '73'):
allFigureString = '7'
allFigureString = correctSuffixForChordQuality(chordObj, allFigureString)
return allFigureString
def figureTuples(chordObject: chord.Chord, keyObject: key.Key) -> list[ChordFigureTuple]:
'''
(This will become a private function in v10)
Return a set of tuplets for each pitch showing the presence of a note, its
interval above the bass its alteration (float) from a step in the given
key, an `alterationString`, and the pitch object.
Note though that for roman numerals, the applicable key is almost always
the root.
For instance, in C major, F# D A- C# would be:
>>> roman.figureTuples(
... chord.Chord(['F#2', 'D3', 'A-3', 'C#4']),
... key.Key('C'),
... )
[ChordFigureTuple(aboveBass=1, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch F#2>),
ChordFigureTuple(aboveBass=6, alter=0.0, prefix='', pitch=<music21.pitch.Pitch D3>),
ChordFigureTuple(aboveBass=3, alter=-1.0, prefix='b', pitch=<music21.pitch.Pitch A-3>),
ChordFigureTuple(aboveBass=5, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch C#4>)]
In c-minor, the A- is a normal note, so the prefix is '' not 'b'. The natural minor is used
exclusively.
>>> roman.figureTuples(
... chord.Chord(['F#2', 'D3', 'A-3', 'C#4']),
... key.Key('c'),
... )
[ChordFigureTuple(aboveBass=1, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch F#2>),
ChordFigureTuple(aboveBass=6, alter=0.0, prefix='', pitch=<music21.pitch.Pitch D3>),
ChordFigureTuple(aboveBass=3, alter=0.0, prefix='', pitch=<music21.pitch.Pitch A-3>),
ChordFigureTuple(aboveBass=5, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch C#4>)]
A C dominant-seventh chord in c minor alters the bass but not the 7th degree
>>> roman.figureTuples(
... chord.Chord(['E3', 'C4', 'G4', 'B-5']),
... key.Key('c'),
... )
[ChordFigureTuple(aboveBass=1, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch E3>),
ChordFigureTuple(aboveBass=6, alter=0.0, prefix='', pitch=<music21.pitch.Pitch C4>),
ChordFigureTuple(aboveBass=3, alter=0.0, prefix='', pitch=<music21.pitch.Pitch G4>),
ChordFigureTuple(aboveBass=5, alter=0.0, prefix='', pitch=<music21.pitch.Pitch B-5>)]
>>> roman.figureTuples(
... chord.Chord(['C4', 'E4', 'G4', 'C#4']),
... key.Key('C'),
... )
[ChordFigureTuple(aboveBass=1, alter=0.0, prefix='', pitch=<music21.pitch.Pitch C4>),
ChordFigureTuple(aboveBass=3, alter=0.0, prefix='', pitch=<music21.pitch.Pitch E4>),
ChordFigureTuple(aboveBass=5, alter=0.0, prefix='', pitch=<music21.pitch.Pitch G4>),
ChordFigureTuple(aboveBass=1, alter=1.0, prefix='#', pitch=<music21.pitch.Pitch C#4>)]
'''
result = []
bass = chordObject.bass()
for thisPitch in chordObject.pitches:
shortTuple = figureTupleSolo(thisPitch, keyObject, bass)
appendTuple = ChordFigureTuple(shortTuple.aboveBass,
shortTuple.alter,
shortTuple.prefix,
thisPitch)
result.append(appendTuple)
return result
def figureTupleSolo(
pitchObj: pitch.Pitch,
keyObj: key.Key,
bass: pitch.Pitch
) -> FigureTuple:
'''
Return a single tuple for a pitch and key showing the interval above
the bass, its alteration from a step in the given key, an alteration
string, and the pitch object.
For instance, in C major, an A-3 above an F# bass would be:
>>> roman.figureTupleSolo(
... pitch.Pitch('A-3'),
... key.Key('C'),
... pitch.Pitch('F#2'),
... )
FigureTuple(aboveBass=3, alter=-1.0, prefix='b')
These figures can be more complex in minor, so this is a good reference, showing
that natural minor is always used.
>>> c = key.Key('c')
>>> c_as_bass = pitch.Pitch('C3')
>>> for name in ('E--', 'E-', 'E', 'E#', 'A--', 'A-', 'A', 'A#', 'B--', 'B-', 'B', 'B#'):
... ft = roman.figureTupleSolo(pitch.Pitch(name + '4'), c, c_as_bass)
... print(f'{name:4s} {ft}')
E-- FigureTuple(aboveBass=3, alter=-1.0, prefix='b')
E- FigureTuple(aboveBass=3, alter=0.0, prefix='')
E FigureTuple(aboveBass=3, alter=1.0, prefix='#')
E# FigureTuple(aboveBass=3, alter=2.0, prefix='##')
A-- FigureTuple(aboveBass=6, alter=-1.0, prefix='b')
A- FigureTuple(aboveBass=6, alter=0.0, prefix='')
A FigureTuple(aboveBass=6, alter=1.0, prefix='#')
A# FigureTuple(aboveBass=6, alter=2.0, prefix='##')
B-- FigureTuple(aboveBass=7, alter=-1.0, prefix='b')
B- FigureTuple(aboveBass=7, alter=0.0, prefix='')
B FigureTuple(aboveBass=7, alter=1.0, prefix='#')
B# FigureTuple(aboveBass=7, alter=2.0, prefix='##')
Returns a namedtuple called a FigureTuple.
'''
unused_scaleStep, scaleAccidental = keyObj.getScaleDegreeAndAccidentalFromPitch(pitchObj)
thisInterval = interval.Interval(bass, pitchObj)
aboveBass = thisInterval.diatonic.generic.mod7
if scaleAccidental is None:
rootAlterationString = ''
alterDiff = 0.0
else:
alterDiff = scaleAccidental.alter
alter = int(alterDiff)
if alter < 0:
rootAlterationString = 'b' * (-1 * alter)
elif alter > 0:
rootAlterationString = '#' * alter
else:
rootAlterationString = ''
appendTuple = FigureTuple(aboveBass, alterDiff, rootAlterationString)
return appendTuple
def identifyAsTonicOrDominant(
inChord: list | tuple | chord.Chord,
inKey: key.Key
) -> str | t.Literal[False]:
'''
Returns the roman numeral string expression (either tonic or dominant) that
best matches the inChord. Useful when you know inChord is either tonic or
dominant, but only two pitches are provided in the chord. If neither tonic
nor dominant is possibly correct, False is returned
>>> roman.identifyAsTonicOrDominant(['B2', 'F5'], key.Key('C'))
'V65'
>>> roman.identifyAsTonicOrDominant(['B3', 'G4'], key.Key('g'))
'i6'
>>> roman.identifyAsTonicOrDominant(['C3', 'B-4'], key.Key('f'))
'V7'
Notice that this -- with B-natural is also identified as V7 because
it is returning the roman numeral root and the inversion name, not yet
checking for correctness.
>>> roman.identifyAsTonicOrDominant(['C3', 'B4'], key.Key('f'))
'V7'
>>> roman.identifyAsTonicOrDominant(['D3'], key.Key('f'))
False
'''
if isinstance(inChord, (list, tuple)):
inChord = chord.Chord(inChord)
elif not isinstance(inChord, chord.Chord):
raise ValueError('inChord must be a Chord or a list of strings') # pragma: no cover
pitchNameList = []
for x in inChord.pitches:
pitchNameList.append(x.name)
oneRoot = inKey.pitchFromDegree(1)
fiveRoot = inKey.pitchFromDegree(5)
oneChordIdentified = False
fiveChordIdentified = False
if oneRoot.name in pitchNameList:
oneChordIdentified = True
elif fiveRoot.name in pitchNameList:
fiveChordIdentified = True
else:
oneRomanChord = RomanNumeral('I7', inKey).pitches
fiveRomanChord = RomanNumeral('V7', inKey).pitches
onePitchNameList = []
for x in oneRomanChord:
onePitchNameList.append(x.name)
fivePitchNameList = []
for x in fiveRomanChord:
fivePitchNameList.append(x.name)
oneMatches = len(set(onePitchNameList) & set(pitchNameList))
fiveMatches = len(set(fivePitchNameList) & set(pitchNameList))
if oneMatches > fiveMatches:
oneChordIdentified = True
elif oneMatches < fiveMatches:
fiveChordIdentified = True
else: # both oneMatches and fiveMatches == 0
return False
if oneChordIdentified:
rootScaleDeg = common.toRoman(1)
if inKey.mode == 'minor':
rootScaleDeg = rootScaleDeg.lower()
else:
rootScaleDeg = rootScaleDeg.upper()
inChord.root(oneRoot)
elif fiveChordIdentified:
rootScaleDeg = common.toRoman(5)
inChord.root(fiveRoot)
else:
return False
return rootScaleDeg + romanInversionName(inChord)
def romanInversionName(inChord: chord.Chord, inv: int | None = None) -> str:
'''
Extremely similar to Chord's inversionName() method, but returns string
values and allows incomplete triads.
>>> roman.romanInversionName(chord.Chord('E4 G4 C5'))
'6'
>>> roman.romanInversionName(chord.Chord('G4 B4 C5 E5'))
'43'
Manually set the inversion to see what would happen.
>>> roman.romanInversionName(chord.Chord('C5 E5 G5'), inv=2)
'64'
>>> roman.romanInversionName(chord.Chord('C5 E5 G5'), inv=0)
''
Uncommon/unusual chords return an empty string
>>> roman.romanInversionName(chord.Chord('C5 C#4'))
''
Does not return ninth or eleventh chord figures.
'''
# TODO: add ninth and eleventh chord figures.
if inv is None:
inv = inChord.inversion()
if inChord.isSeventh() or inChord.seventh is not None:
if inv == 0:
return '7'
elif inv == 1:
return '65'
elif inv == 2:
return '43'
elif inv == 3:
return '42'
else:
return ''
elif (inChord.isTriad()
or inChord.isIncompleteMajorTriad()
or inChord.isIncompleteMinorTriad()):
if inv == 0:
return '' # not 53
elif inv == 1:
return '6'
elif inv == 2:
return '64'
else:
return ''
else:
return ''
def correctRNAlterationForMinor(
figureTuple: FigureTuple,
keyObj: key.Key
) -> FigureTuple:
'''
(This will become a private function in version 10)
Takes in a FigureTuple and a Key object and returns the same or a
new FigureTuple correcting for the fact that, for instance, Ab in c minor
is VI not vi. Works properly only if the note is the root of the chord.
Used in RomanNumeralFromChord
These return new FigureTuple objects
>>> ft5 = roman.FigureTuple(aboveBass=6, alter=-1, prefix='')
>>> ft5a = roman.correctRNAlterationForMinor(ft5, key.Key('c'))
>>> ft5a
FigureTuple(aboveBass=6, alter=-1, prefix='b')
>>> ft5a is ft5
False
>>> ft6 = roman.FigureTuple(aboveBass=6, alter=0, prefix='')
>>> roman.correctRNAlterationForMinor(ft6, key.Key('c'))
FigureTuple(aboveBass=6, alter=0, prefix='b')
>>> ft7 = roman.FigureTuple(aboveBass=7, alter=1, prefix='#')
>>> roman.correctRNAlterationForMinor(ft7, key.Key('c'))
FigureTuple(aboveBass=7, alter=0, prefix='')
Does nothing for major and passes in the original Figure Tuple unchanged:
>>> ft1 = roman.FigureTuple(aboveBass=6, alter=-1, prefix='b')
>>> ft2 = roman.correctRNAlterationForMinor(ft1, key.Key('C'))
>>> ft2
FigureTuple(aboveBass=6, alter=-1, prefix='b')
>>> ft1 is ft2
True
Does nothing for steps other than 6 or 7:
>>> ft3 = roman.FigureTuple(aboveBass=4, alter=-1, prefix='b')
>>> ft4 = roman.correctRNAlterationForMinor(ft3, key.Key('c'))
>>> ft4
FigureTuple(aboveBass=4, alter=-1, prefix='b')
>>> ft3 is ft4
True
'''
if keyObj.mode != 'minor':
return figureTuple
if figureTuple.aboveBass not in (6, 7):
return figureTuple
alter = figureTuple.alter
rootAlterationString = figureTuple.prefix
if alter == 1.0:
alter = 0
rootAlterationString = ''
elif alter == 0.0:
alter = 0 # NB! does not change!
rootAlterationString = 'b'
# more exotic:
elif alter > 1.0:
alter = alter - 1
rootAlterationString = rootAlterationString[1:]
elif alter < 0.0:
rootAlterationString = 'b' + rootAlterationString
return FigureTuple(figureTuple.aboveBass, alter, rootAlterationString)
def romanNumeralFromChord(
chordObj: chord.Chord,
keyObj: key.Key | str | None = None,
preferSecondaryDominants: bool = False,
) -> RomanNumeral:
# noinspection PyShadowingNames
'''
Takes a chord object and returns an appropriate chord name. If keyObj is
omitted, the root of the chord is considered the key (if the chord has a
major third, it's major; otherwise it's minor).
>>> rn = roman.romanNumeralFromChord(
... chord.Chord(['E-3', 'C4', 'G-6']),
... key.Key('g#'),
... )
>>> rn
<music21.roman.RomanNumeral bivo6 in g# minor>
The pitches remain the same with the same octaves:
>>> for p in rn.pitches:
... p
<music21.pitch.Pitch E-3>
<music21.pitch.Pitch C4>
<music21.pitch.Pitch G-6>
>>> romanNumeral2 = roman.romanNumeralFromChord(
... chord.Chord(['E3', 'C4', 'G4', 'B-4', 'E5', 'G5']),
... key.Key('F'),
... )
>>> romanNumeral2
<music21.roman.RomanNumeral V65 in F major>
Note that vi and vii in minor, by default, signifies what you might think of
alternatively as #vi and #vii:
>>> romanNumeral3 = roman.romanNumeralFromChord(
... chord.Chord(['A4', 'C5', 'E-5']),
... key.Key('c'),
... )
>>> romanNumeral3
<music21.roman.RomanNumeral vio in c minor>
>>> romanNumeral4 = roman.romanNumeralFromChord(
... chord.Chord(['A-4', 'C5', 'E-5']),
... key.Key('c'),
... )
>>> romanNumeral4
<music21.roman.RomanNumeral bVI in c minor>
>>> romanNumeral4.sixthMinor
<Minor67Default.CAUTIONARY: 2>
>>> romanNumeral5 = roman.romanNumeralFromChord(
... chord.Chord(['B4', 'D5', 'F5']),
... key.Key('c'),
... )
>>> romanNumeral5
<music21.roman.RomanNumeral viio in c minor>
>>> romanNumeral6 = roman.romanNumeralFromChord(
... chord.Chord(['B-4', 'D5', 'F5']),
... key.Key('c'),
... )
>>> romanNumeral6
<music21.roman.RomanNumeral bVII in c minor>
Diminished and half-diminished seventh chords can omit the third and still
be diminished: (n.b. we also demonstrate that chords can be created from a
string):
>>> romanNumeralDim7 = roman.romanNumeralFromChord(
... chord.Chord('A3 E-4 G-4'),
... key.Key('b-'),
... )
>>> romanNumeralDim7
<music21.roman.RomanNumeral viio7 in b- minor>
For reference, odder notes:
>>> romanNumeral7 = roman.romanNumeralFromChord(
... chord.Chord(['A--4', 'C-5', 'E--5']),
... key.Key('c'),
... )
>>> romanNumeral7
<music21.roman.RomanNumeral bbVI in c minor>
>>> romanNumeral8 = roman.romanNumeralFromChord(
... chord.Chord(['A#4', 'C#5', 'E#5']),
... key.Key('c'),
... )
>>> romanNumeral8
<music21.roman.RomanNumeral #vi in c minor>
>>> romanNumeral10 = roman.romanNumeralFromChord(
... chord.Chord(['F#3', 'A3', 'E4', 'C5']),
... key.Key('d'),
... )
>>> romanNumeral10
<music21.roman.RomanNumeral #iiiø7 in d minor>
Augmented 6ths without key context
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 C#5'),
... )
<music21.roman.RomanNumeral It6 in g minor>
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 B-4 C#5'),
... )
<music21.roman.RomanNumeral Ger65 in g minor>
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 A4 C#5'),
... )
<music21.roman.RomanNumeral Fr43 in g minor>
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 A#4 C#5'),
... )
<music21.roman.RomanNumeral Sw43 in g minor>
With correct key context:
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 C#5'),
... key.Key('G')
... )
<music21.roman.RomanNumeral It6 in G major>
With incorrect key context does not find an augmented 6th chord:
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 C#5'),
... key.Key('C')
... )
<music21.roman.RomanNumeral #io6b3 in C major>
Empty chords, including :class:`~music21.harmony.NoChord` objects, give empty RomanNumerals:
>>> roman.romanNumeralFromChord(harmony.NoChord())
<music21.roman.RomanNumeral>
Augmented 6th chords in other inversions do not currently find correct roman numerals
* Changed in v7: i7 is given for a tonic or subdominant minor-seventh chord in major:
>>> roman.romanNumeralFromChord(
... chord.Chord('C4 E-4 G4 B-4'),
... key.Key('C'))
<music21.roman.RomanNumeral i7 in C major>
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 B-4 C5'),
... key.Key('G'))
<music21.roman.RomanNumeral iv65 in G major>
minor-Major chords are written with a [#7] modifier afterwards:
>>> roman.romanNumeralFromChord(
... chord.Chord('C4 E-4 G4 B4'),
... key.Key('C'))
<music21.roman.RomanNumeral i7[#7] in C major>
>>> roman.romanNumeralFromChord(
... chord.Chord('E-4 G4 B4 C5'),
... key.Key('C'))
<music21.roman.RomanNumeral i65[#7] in C major>
Former bugs that are now fixed:
>>> romanNumeral11 = roman.romanNumeralFromChord(
... chord.Chord(['E4', 'G4', 'B4', 'D5']),
... key.Key('C'),
... )
>>> romanNumeral11
<music21.roman.RomanNumeral iii7 in C major>
>>> roman.romanNumeralFromChord(chord.Chord('A3 C4 E-4 G4'), key.Key('c'))
<music21.roman.RomanNumeral viø7 in c minor>
>>> roman.romanNumeralFromChord(chord.Chord('A3 C4 E-4 G4'), key.Key('B-'))
<music21.roman.RomanNumeral viiø7 in B- major>
>>> romanNumeral9 = roman.romanNumeralFromChord(
... chord.Chord(['C4', 'E5', 'G5', 'C#6']),
... key.Key('C'),
... )
>>> romanNumeral9
<music21.roman.RomanNumeral I#853 in C major>
Not an augmented 6th:
>>> roman.romanNumeralFromChord(
... chord.Chord('E4 G4 B-4 C#5')
... )
<music21.roman.RomanNumeral io6b5b3 in c# minor>
The preferSecondaryDominants option defaults to False, but if set to True,
then certain rare figures are swapped with their
more common secondary dominant equivalent to produce
Roman numerals like 'V/V' instead of 'II'.
This has no effect on most chords.
A change is triggered if and only if:
* the chord is a major triad or dominant seventh
* the chord is not diatonic to the primary key (i.e., chromatically altered)
* the root of secondary key is diatonic to the primary key.
So first without setting preferSecondaryDominants:
>>> cd = chord.Chord('D F# A')
>>> rn = roman.romanNumeralFromChord(cd, 'C')
>>> rn.figure
'II'
And now with preferSecondaryDominants=True:
>>> rn = roman.romanNumeralFromChord(cd, 'C', preferSecondaryDominants=True)
>>> rn.figure
'V/V'
Dominant sevenths must be spelt correctly
(see conditions at :meth:`~music21.chord.Chord.isDominantSeventh`).
So let's try D dominant seventh in various contexts.
>>> cd = chord.Chord('F#4 A4 C5 D5')
In G major this still comes out without recourse to a secondary,
whether preferSecondaryDominants is True or False.
>>> rn = roman.romanNumeralFromChord(cd, 'G')
>>> rn.figure
'V65'
>>> rn = roman.romanNumeralFromChord(cd, 'G', preferSecondaryDominants=True)
>>> rn.figure
'V65'
In C major it does come through as a secondary
>>> rn = roman.romanNumeralFromChord(cd, 'C', preferSecondaryDominants=True)
>>> rn.figure
'V65/V'
"German Augmented sixth" chords are left intact, without change.
This is thanks to the constraints on
spelling and on the root of the secondary degree.
>>> cd = chord.Chord('Ab4 C5 Eb5 F#5')
>>> rn = roman.romanNumeralFromChord(cd, 'C', preferSecondaryDominants=True)
>>> rn.figure
'Ger65'
Let's check that with a dominant seventh spelling and minor key context:
>>> cd = chord.Chord('Ab4 C5 Eb5 Gb5')