Source code for music21.musedata.base40

# ------------------------------------------------------------------------------
# Name:         base40.py
# Purpose:      Base40/Music21 Pitch/Interval Translator
#
# Authors:      Jose Cabal-Ugaz
#
# Copyright:    Copyright © 2009-2010 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Implementation of Walter Hewlett's base40 system for musical intervals.
(See Hewlett and Ann K. Blombach 1989 article)
'''
from __future__ import annotations

import typing as t

from music21 import exceptions21
from music21 import interval
from music21 import note
from music21 import pitch
from music21.interval import Interval, IntervalException

# Key => Base40 pitch number
# Value => Music21 Pitch name
base40Equivalent: dict[int, str] = {
    1: 'C--',
    2: 'C-',
    3: 'C',
    4: 'C#',
    5: 'C##',
    6: '',
    7: 'D--',
    8: 'D-',
    9: 'D',
    10: 'D#',
    11: 'D##',
    12: '',
    13: 'E--',
    14: 'E-',
    15: 'E',
    16: 'E#',
    17: 'E##',
    18: 'F--',
    19: 'F-',
    20: 'F',
    21: 'F#',
    22: 'F##',
    23: '',
    24: 'G--',
    25: 'G-',
    26: 'G',
    27: 'G#',
    28: 'G##',
    29: '',
    30: 'A--',
    31: 'A-',
    32: 'A',
    33: 'A#',
    34: 'A##',
    35: '',
    36: 'B--',
    37: 'B-',
    38: 'B',
    39: 'B#',
    40: 'B##',
}
'''
A mapping of base40 numbers to their note representations.

>>> musedata.base40.base40Equivalent(34)
'A##'
'''

# Key => Music21 Pitch name
# Value => Base40 pitch number
base40Representation: dict[str, int] = {
    'C--': 1,
    'C-': 2,
    'C': 3,
    'C#': 4,
    'C##': 5,
    # 6
    'D--': 7,
    'D-': 8,
    'D': 9,
    'D#': 10,
    'D##': 11,
    # 12
    'E--': 13,
    'E-': 14,
    'E': 15,
    'E#': 16,
    'E##': 17,
    'F--': 18,
    'F-': 19,
    'F': 20,
    'F#': 21,
    'F##': 22,
    # 23
    'G--': 24,
    'G-': 25,
    'G': 26,
    'G#': 27,
    'G##': 28,
    # 29
    'A--': 30,
    'A-': 31,
    'A': 32,
    'A#': 33,
    'A##': 34,
    # 35
    'B--': 36,
    'B-': 37,
    'B': 38,
    'B#': 39,
    'B##': 40,
}


# Key => Base40 delta (difference between two Base40 pitch numbers)
# Value => Corresponding music21 Interval
base40IntervalTable: dict[int, str] = {
    0: 'P1',
    1: 'A1',

    4: 'd2',
    5: 'm2',
    6: 'M2',
    7: 'A2',

    10: 'd3',
    11: 'm3',
    12: 'M3',
    13: 'A3',

    16: 'd4',
    17: 'P4',
    18: 'A4',

    22: 'd5',
    23: 'P5',
    24: 'A5',

    27: 'd6',
    28: 'm6',
    29: 'M6',
    30: 'A6',

    33: 'd7',
    34: 'm7',
    35: 'M7',
    36: 'A7',

    39: 'd8',
    40: 'P8',  # wraps around
}


[docs] def base40DeltaToInterval(delta: int) -> Interval: ''' Returns a music21 Interval between two Base40 pitch numbers given the delta (difference) between them. Raises a ``Base40Exception`` if the interval is not handled by Base40. Base40 can only handle major, minor, perfect, augmented, and diminished intervals. Although not for certain, it seems that the engineers that designed this system assumed that other intervals (doubly augmented intervals, for instance) would be of a very rare occurrence, and extreme intervals which would trigger an incorrect answer (C-- to C##, for instance, would return a diminished second, even though it's a quadruplely-augmented unison) just would not occur. >>> musedata.base40.base40DeltaToInterval(4) <music21.interval.Interval d2> >>> musedata.base40.base40DeltaToInterval(11) <music21.interval.Interval m3> >>> musedata.base40.base40DeltaToInterval(23) <music21.interval.Interval P5> >>> musedata.base40.base40DeltaToInterval(-23) <music21.interval.Interval P-5> >>> musedata.base40.base40DeltaToInterval(52) <music21.interval.Interval M10> >>> musedata.base40.base40DeltaToInterval(-52) <music21.interval.Interval M-10> >>> musedata.base40.base40DeltaToInterval(77) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Interval not handled by Base40 37 ''' direction = 1 if delta < 0: direction = -1 simpleDelta = abs(delta) % 40 try: simpleIntervalName = base40IntervalTable[simpleDelta] simpleInterval = interval.Interval(simpleIntervalName) except KeyError: raise Base40Exception('Interval not handled by Base40 ' + str(simpleDelta)) numOctaves = abs(delta) // 40 sgi = simpleInterval.generic # Simple generic interval # Compound generic interval cgi = interval.GenericInterval(direction * (sgi.value + 7 * numOctaves)) sdi = simpleInterval.diatonic # Simple diatonic interval newInterval = str(sdi.specifier) + str(cgi.value) return interval.Interval(newInterval)
[docs] def base40ToPitch(base40Num: int) -> pitch.Pitch: ''' Converts a Base40 pitch number into a music21 Pitch. The Base40 number is octave specific. Raises a Base40 Exception if the Base40 pitch number given doesn't have an associated pitch name. There is one unassigned number each time the interval between two letters is a whole step. >>> musedata.base40.base40ToPitch(1) <music21.pitch.Pitch C--1> >>> musedata.base40.base40ToPitch(40) <music21.pitch.Pitch B##1> >>> musedata.base40.base40ToPitch(23) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Pitch name not assigned to this Base40 number 23 >>> musedata.base40.base40ToPitch(186) <music21.pitch.Pitch G5> ''' p = pitch.Pitch() p.octave = ((base40Num - 1) / 40) + 1 tableNum = base40Num - 40 * (p.implicitOctave - 1) pitchName = base40Equivalent.get(tableNum, '') if pitchName: p.name = pitchName return p raise Base40Exception('Pitch name not assigned to this Base40 number ' + str(base40Num))
[docs] def pitchToBase40(pitchToConvert: str|pitch.Pitch) -> int: ''' Converts a pitch string or a music21 Pitch into a Base40 pitch number. The Base40 number is octave specific. Raises a Base40 Exception if the pitch to convert is outside the set of pitches that Base40 can handle; for example, half flats and half sharps or triple flats and triple sharps. >>> musedata.base40.pitchToBase40(pitch.Pitch('C--5')) 161 >>> musedata.base40.pitchToBase40('F##4') 142 >>> musedata.base40.pitchToBase40('F###4') Traceback (most recent call last): music21.musedata.base40.Base40Exception: Base40 cannot handle this pitch F###4 ''' pitchObj: pitch.Pitch if isinstance(pitchToConvert, str): pitchObj = pitch.Pitch(pitchToConvert) else: pitchObj = pitchToConvert if pitchObj.name in base40Representation: tableNum = base40Representation[pitchObj.name] base40Num = (40 * (pitchObj.implicitOctave - 1)) + tableNum return base40Num raise Base40Exception('Base40 cannot handle this pitch ' + pitchObj.nameWithOctave)
[docs] def base40Interval(base40NumA: int, base40NumB: int) -> Interval: ''' Returns a music21 Interval between two base40 pitch numbers, using their delta (difference) as defined in Base40. The interval provided is without direction. Raises a Base40 Exception if the delta doesn't correspond to an interval in Base40, or if either base40 pitch number doesn't correspond to a pitch name. >>> musedata.base40.base40Interval(163, 191) <music21.interval.Interval m6> >>> musedata.base40.base40Interval(186, 174) # Descending M3 <music21.interval.Interval M-3> Base40 has limitations for intervals smaller than diminished or bigger than augmented. >>> musedata.base40.base40Interval(1, 5) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Base40 cannot compute interval between 1 and 5. >>> musedata.base40.base40Interval(1, 3) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Interval not handled by Base40 2 >>> musedata.base40.base40Interval(2, 6) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Pitch name not assigned to this Base40 number 6 Interval does not exist >>> musedata.base40.base40Interval(12, 6) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Pitch name not assigned to these Base40 numbers 12 and 6 Interval does not exist ''' pitchA = base40Equivalent.get((base40NumA - 1) % 40 + 1, '') pitchB = base40Equivalent.get((base40NumB - 1) % 40 + 1, '') delta = base40NumB - base40NumA if not pitchA and not pitchB: raise Base40Exception('Pitch name not assigned to these Base40 numbers ' + str(base40NumA) + ' and ' + str(base40NumB) + ' Interval does not exist') if not pitchA: raise Base40Exception('Pitch name not assigned to this Base40 number ' + str(base40NumA) + ' Interval does not exist') if not pitchB: raise Base40Exception('Pitch name not assigned to this Base40 number ' + str(base40NumB) + ' Interval does not exist') if delta > 3 and pitchA[0] == pitchB[0]: raise Base40Exception('Base40 cannot compute interval between ' + str(base40NumA) + ' and ' + str(base40NumB) + '.') return base40DeltaToInterval(delta)
[docs] def base40ActualInterval(base40NumA: int, base40NumB: int) -> Interval: ''' Calculates a music21 Interval between two Base40 pitch numbers, as calculated using the music21.interval module. Raises a Base40 Exception if (a) Either of the Base40 pitch numbers does not correspond to a pitch name or (b) If an unusual interval is encountered that can't be handled by music21. >>> musedata.base40.base40ActualInterval(163, 191) <music21.interval.Interval m6> >>> musedata.base40.base40ActualInterval(186, 174) # Descending M3 <music21.interval.Interval M-3> >>> musedata.base40.base40ActualInterval(1, 5) <music21.interval.Interval AAAA1> >>> musedata.base40.base40ActualInterval(1, 3) <music21.interval.Interval AA1> >>> musedata.base40.base40ActualInterval(2, 6) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Pitch name not assigned to this Base40 number 6 OMIT_FROM_DOCS >>> musedata.base40.base40ActualInterval(12, 6) Traceback (most recent call last): music21.musedata.base40.Base40Exception: Pitch name not assigned to this Base40 number 12 ''' pitchA = base40ToPitch(base40NumA) pitchB = base40ToPitch(base40NumB) noteA = note.Note() noteA.pitch = pitchA noteB = note.Note() noteB.pitch = pitchB try: return interval.Interval(noteA, noteB) except IntervalException: # pragma: no cover raise Base40Exception('Unusual interval - Limitation of music21.interval')
def _quickEnharmonicString( nameStr: str, direction: t.Literal['up', 'down'] = 'up', allowDoubleAccidentals: bool = True, ) -> list[str]: ''' Helper function for quickHigherEnharmonicString and quickLowerEnharmonicString ''' if direction == 'up': addNum = 4 elif direction == 'down': addNum = -4 else: raise Base40Exception(f'Not a valid direction, {direction}') base40num = base40Representation.get(nameStr, None) if not base40num: return [] enharmonics: list[str] = [] max_loops = 100 while max_loops: max_loops -= 1 base40num = (base40num + addNum) % 40 if base40num == 0: base40num = 40 base40str = base40Equivalent.get(base40num, '') if allowDoubleAccidentals is False and len(base40str) > 2: base40str = '' if not base40str: return enharmonics if base40str[0] == nameStr[0]: return enharmonics for e in enharmonics: if base40str[0] == e[0]: return enharmonics enharmonics.append(base40str) # pragma: no cover raise ValueError('Did not ever leave')
[docs] def quickHigherEnharmonicString(nameStr: str, allowDoubleAccidentals: bool = True): ''' Takes a name of a string and returns a list of the quick higher enharmonics, limited to double sharps and double flats (or to single sharps and single flats if allowDoubleAccidentals is False) >>> musedata.base40.quickHigherEnharmonicString('F#') ['G-'] >>> musedata.base40.quickHigherEnharmonicString('C##') ['D', 'E--'] >>> musedata.base40.quickHigherEnharmonicString('C##', allowDoubleAccidentals=False) ['D'] >>> musedata.base40.quickHigherEnharmonicString('B#') ['C', 'D--'] ''' return _quickEnharmonicString(nameStr, direction='up', allowDoubleAccidentals=allowDoubleAccidentals)
[docs] def quickLowerEnharmonicString(nameStr: str, allowDoubleAccidentals: bool = True): ''' Takes a name of a string and returns a list of the quick lower enharmonics, limited to double sharps and double flats (or to single sharps and single flats if allowDoubleAccidentals is False) >>> musedata.base40.quickLowerEnharmonicString('B-') ['A#'] >>> musedata.base40.quickLowerEnharmonicString('G-') ['F#', 'E##'] >>> musedata.base40.quickLowerEnharmonicString('G-', allowDoubleAccidentals=False) ['F#'] >>> musedata.base40.quickLowerEnharmonicString('C-') ['B', 'A##'] ''' return _quickEnharmonicString(nameStr, direction='down', allowDoubleAccidentals=allowDoubleAccidentals)
[docs] def quickEnharmonicString(nameStr: str, allowDoubleAccidentals: bool = True): ''' Takes a name of a string and returns a list of the quick lower and higher enharmonics, limited to double sharps and double flats (or to single sharps and single flats if allowDoubleAccidentals is False) >>> musedata.base40.quickEnharmonicString('C') ['B#', 'D--'] >>> musedata.base40.quickEnharmonicString('C', allowDoubleAccidentals=False) ['B#'] >>> musedata.base40.quickEnharmonicString('G') ['F##', 'A--'] ''' harmonicString = quickLowerEnharmonicString(nameStr, allowDoubleAccidentals) harmonicString.extend(quickHigherEnharmonicString(nameStr, allowDoubleAccidentals)) return harmonicString
class Base40Exception(exceptions21.Music21Exception): pass # ------------------------------------------------------------------------------ _DOC_ORDER = [ base40ActualInterval, quickEnharmonicString, quickHigherEnharmonicString, quickLowerEnharmonicString, ] if __name__ == '__main__': import music21 music21.mainTest()