Module musx.pitch
Defines the equal tempered chromatic scale over 11 octaves and provides a mapping between alternate representations of pitch material: Pitch instances, hertz frequency, key numbers, and pitch names.
The Pitch class represent equal tempered pitches and returns information in hertz, keynum, pitch class, Pnum and pitch name formats. Pitches can be compared using standard math relations and maintain proper spelling when complemented or transposed by an Interval.
The keynum, pitch and hertz functions provide mapping between the three alternate representations of frequency:
keynum()
: converts a pitch name or hertz value into a key number.pitch()
: converts a hertz value, key number or pitch name into a Pitch.hertz()
: converts a pitch name or key number into a hertz value.
Lists and string sequences:
The functions can map individual values, lists of values, string sequences of values and lists of string sequences of values.
In a string sequence use spaces to delimit the items:
- A string sequence of key numbers: '0 40 21 33 87 12'
- A string sequence of hertz values: '440 880 220.12'
- A string sequence of pitch names: 'c4 d4 eb4 f#4 g3'
In any string sequence you can repeat an item by appending an expansion factor _n_ to it, where n is the number of times the item should occur in succession. For example, the motive of Beethoven's 5th Symphony 'G4 G4 G4 Eb4' can be written as a pitch sequence 'G43 Eb4', a key number sequence '673 63', or a hertz sequence '3923 311'.
In a string sequence of pitches octave numbers are "sticky", i.e. when an octave number is provided it remains in effect until a different octave number is specified. For example a diatonic scale from A3 to A4 is 'a3 b c4 d e f g a'.
Rests:
The special string name 'R' or 'r' represents a musical rest. The key number of a rest is -1, its hertz value is 0.0 and its Pitch is an empty pitch: Pitch().
Expand source code
###############################################################################
"""
Defines the equal tempered chromatic scale over 11 octaves and provides a
mapping between alternate representations of pitch material: Pitch instances,
hertz frequency, key numbers, and pitch names.
The Pitch class represent equal tempered pitches and returns information
in hertz, keynum, pitch class, Pnum and pitch name formats. Pitches
can be compared using standard math relations and maintain proper spelling
when complemented or transposed by an Interval.
The keynum, pitch and hertz functions provide mapping between the three
alternate representations of frequency:
* `keynum()` : converts a pitch name or hertz value into a key number.
* `pitch()` : converts a hertz value, key number or pitch name into a Pitch.
* `hertz()` : converts a pitch name or key number into a hertz value.
**Lists and string sequences:**
The functions can map individual values, lists of values, string
sequences of values and lists of string sequences of values.
In a string sequence use spaces to delimit the items:
* A string sequence of key numbers: '0 40 21 33 87 12'
* A string sequence of hertz values: '440 880 220.12'
* A string sequence of pitch names: 'c4 d4 eb4 f#4 g3'
In any string sequence you can repeat an item by appending
an expansion factor _*n_ to it, where _n_ is the number of times
the item should occur in succession. For example, the motive of
Beethoven's 5th Symphony 'G4 G4 G4 Eb4' can be written as a
pitch sequence 'G4*3 Eb4', a key number sequence '67*3 63',
or a hertz sequence '392*3 311'.
In a string sequence of pitches octave numbers are "sticky", i.e. when
an octave number is provided it remains in effect until a different octave
number is specified. For example a diatonic scale from A3 to A4 is
'a3 b c4 d e f g a'.
**Rests:**
The special string name 'R' or 'r' represents a musical rest. The key number of
a rest is -1, its hertz value is 0.0 and its Pitch is an empty pitch: Pitch().
"""
__pdoc__ = {
'parse_number_sequence': False,
'parse_pitch_sequence': False,
'chromatic_scale': False,
'build_chromatic_scale': False,
'PitchBase': False}
from random import randint
from enum import IntEnum
from collections import namedtuple
import math
from . import tools
PitchBase = namedtuple('PitchBase', ['letter', 'accidental', 'octave'])
#PitchBase.__doc__ = """Base class for the immutable implementation of Pitch."""
class Pitch (PitchBase):
"""
Creates a Pitch from a string or list, if neither is provided
an empty Pitch is returned. The legal constructor forms are:
* Pitch(string) - creates a Pitch from a pitch name string.
* Pitch([l, a, o]) - creates a Pitch from a three element
pitch list containing a letter, accidental and octave index
(see below).
* Pitch() - creates an empty Pitch.
The format of a Pitch name string is:
```
<pitch> := <letter>, [<accidental>], <octave>
<letter> := 'C' | 'D' | 'E' | 'F' | 'G' | 'A' | 'B' |
'c' | 'd' | 'e' | 'f' | 'g' | 'a' | 'b'
<accidental> := <2flat> | <flat> | <natural> | <sharp> | <2sharp>
<2flat> := 'bb' | 'ff'
<flat> := 'b' | 'f'
<natural> := ''
<sharp> := '#' | 's'
<2sharp> := '##' | 'ss'
<octave> := '00' | '0' | '1' | '2' | '3' | '4' | '5' |
'6' | '7' | '8' | '9'
```
Parameters
----------
arg : string | list | None
A pitch name string, a list of three pitch indexes, or None.
Returns
-------
A new Pitch instance.
Raises
------
* TypeError if arg is a invalid pitch list.
* TypeError if arg is an invalid pitch.
"""
# Pitch letter constants (0-6).
_let_C, _let_D, _let_E, _let_F, _let_G, _let_A, _let_B = range(7)
# Maps pitch-letter names onto zero based indexes.
_letter_map = {"C": _let_C, "D": _let_D, "E": _let_E, "F": _let_F,
"G": _let_G, "A": _let_A, "B": _let_B,
"c": _let_C, "d": _let_D, "e": _let_E, "f": _let_F,
"g": _let_G, "a": _let_A, "b": _let_B
}
# Octave constants for code readability.
_oct_00, _oct_0, _oct_1, _oct_2, _oct_3, _oct_4, _oct_5, _oct_6, _oct_7, _oct_8, _oct_9 = range(11)
# Maps octave names onto zero based indexes.
_octave_map = {"00": _oct_00, "0": _oct_0, "1": _oct_1, "2": _oct_2, "3": _oct_3, "4": _oct_4,
"5": _oct_5, "6": _oct_6, "7": _oct_7, "8": _oct_8, "9": _oct_9}
# Accidental constants for code readability.
_acc_2f, _acc_f, _acc_n, _acc_s, _acc_2s = range(5)
# Maps accidental names onto zero based indexes.
_accidental_map = {"bb": _acc_2f, "b": _acc_f, "": _acc_n, "#": _acc_s, "##": _acc_2s,
"ff": _acc_2f, "f": _acc_f, "n": _acc_n, "s": _acc_s, "ss": _acc_2s}
_enharmonic_map = [{_acc_s: 'B#', _acc_n: 'C', _acc_2f: 'Dbb'},
{_acc_2s: 'B##', _acc_s: 'C#', _acc_f: 'Db'},
{_acc_2s: 'C##', _acc_n: 'D', _acc_2f: 'Ebb'},
{_acc_s: 'D#', _acc_f: 'Eb', _acc_2f: 'Fbb'},
{_acc_2s: 'D##', _acc_n: 'E', _acc_f: 'Fb'},
{_acc_s: 'E#', _acc_n: 'F', _acc_2f: 'Gbb'},
{_acc_2s: 'E##', _acc_s: 'F#', _acc_f: 'Gb'},
{_acc_2s: 'F##', _acc_n: 'G', _acc_2f: 'Abb'},
{_acc_s: 'G#', _acc_f: 'Ab'},
{_acc_2s: 'G##', _acc_n: 'A', _acc_2f: 'Bbb'},
{_acc_s: 'A#', _acc_f: 'Bb', _acc_2f: 'Cbb'},
{_acc_2s: 'A##', _acc_n: 'B', _acc_f: 'Cb'}]
# Reverse map of pitch indexes 0-6 onto their canonical names.
_letter_names = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
# Reverse map of accidental indexes 0-4 onto their symbolic names.
_accidental_names = ['bb', 'b', '', '#', '##']
# Reverse map of pitch indexes 0-4 onto their safe names.
_accidental_safe_names = ['ff', 'f', '', 's', 'ss']
# Reverse map of pitch indexes 0-10 onto their canonical names.
_octave_names = ['00', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
# Diatonic letter distances in semitones.
_letter_spans = [0, 2, 4, 5, 7, 9, 11]
# ## The minimum pnum identifier value.
# _min_pcid = (_let_C << 4 | _acc_2f)
#
# ## The maximum pnum identifier value.
# _max_pcid = (_let_B << 4 | _acc_2s)
pnums = IntEnum('Pnum',
[(lj + aj, ((li << 4) + ai))
for li, lj in enumerate(["C", "D", "E", "F", "G", "A", "B"])
for ai, aj in enumerate(["ff", "f", "", "s", "ss"])])
"""
A class variable that holds an IntEnum of all possible letter-and-accidental
combinations Cff up to Bss. (Since the accidental character # is illegal as a
python enum name pnums use the 'safe versions' of the accidental
names: 'ff' upto 'ss'.
A pnum value is a one byte integer 'llllaaaa', where 'llll' is its
letter index 0-6, and 'aaaa' is its accidental index 0-4. Pnums can be
compared using regular math relations.
"""
def __new__(cls, arg=None):
# Check for valid types and lengths up front.
if arg is None or arg in ['R','r']:
return cls._values_to_pitch(None, None, None)
if isinstance(arg, list):
if len(arg) == 3 and all(isinstance(a, int) for a in arg):
return cls._values_to_pitch(*arg)
else:
raise TypeError(f'{arg} is an invalid pitch list.')
if isinstance(arg, str) and len(arg) >= 2:
return cls._string_to_pitch(arg)
raise TypeError(f"'{arg}' is an invalid pitch.")
@classmethod
def _string_to_pitch(cls, arg):
"""
A private method that accepts a pitch string and parses it into three
integer index values: letter, accidental, and octave. If all three values can
be parsed from the string they should then passed to the _values_to_pitch()
method to assign them to the instance's attributes. A ValueError
should be raised for any value that cannot be parsed from the string. See:
_values_to_pitch().
Parameter
---------
arg : string
The string to convert to a pitch.
Returns
-------
A new Pitch instance.
Raises
------
* ValueError is arg is not a valid pitch name.
"""
strlen = len(arg)
index = 0
letter = cls._letter_map.get(arg[index].upper())
if letter is None:
raise ValueError(f"'{arg}' is not a valid pitch name.")
while index < strlen and not arg[index].isdigit():
index += 1
if index == strlen:
raise ValueError(f"'{arg}' is not a valid pitch name.")
octave = cls._octave_map.get(arg[index::])
if octave is None:
raise ValueError(f"'{arg}' is not a valid pitch name.")
accidental = cls._acc_n # default accidental natural
if index > 1:
accidental = cls._accidental_map.get(arg[1:index])
if accidental is None:
raise ValueError(f"'{arg}' is not a valid pitch name.")
return cls._values_to_pitch(letter, accidental, octave)
@classmethod
def _values_to_pitch(cls, let, acc, ova):
"""
A private method that checks three values (letter, accidental and octave) to make
sure they are either valid index values for the letter, accidental and octave
attributes or they are None. The valid integer values are:
* A letter index 0-6 corresponding to the pitch letter names ['C', 'D', 'E', 'F', 'G', 'A', 'B'].
* An accidental index 0-4 corresponding to symbolic accidental names ['bb', 'b', '', '#', '##']
or 'safe' accidental names ['ff', 'f', 'n', 's', 'ss'].
* An octave index 0-10 corresponding to the pitch octave names ['00', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9'].
If any value is out of range the method will raise a ValueError for that value. If
all values are legal the method will make the following 'edge case' tests:
* Values cannot produce a pitch below midi key number 0 (lowest pitch is 'C00')
* Values cannot produce a pitch above midi key number 127 (highest pitches are 'G9' and 'Abb9')
If all the edge case checks pass then _values_to_pitch() should call the super's __new()__ method:
super(Pitch, cls).__new__(cls, let, acc, ova)
otherwise it should raise a ValueError for the offending values. NOTE: _values_to_pitch
should be the only method in your implementation that calls the super method.
Parameter
---------
let : string
The pitch letter string to convert to a pitch.
acc : string
The accidental string to convert to a pitch.
ova : string
The octave string to convert to a pitch.
Returns
-------
A new Pitch instance.
Raises
------
* ValueError is arg is not a valid pitch name.
"""
if let is None or let in ['R', 'r']:
return super(Pitch, cls).__new__(cls, None, None, None)
if 0 <= let <= 6:
if 0 <= acc <= 4:
if 0 <= ova <= 10:
if ova == 0 and let == cls._let_C and acc < cls._acc_n:
nam = cls._letter_names[let] + cls._accidental_names[acc] + cls._octave_names[ova]
raise ValueError(f"Pitch '{nam}': midi number below 0.")
if ova == 10 and cls._letter_spans[let] + acc-2 > 7:
nam = cls._letter_names[let] + cls._accidental_names[acc] + cls._octave_names[ova]
raise ValueError(f"Pitch '{nam}': midi number exceeds 127.")
return super(Pitch, cls).__new__(cls, let, acc, ova)
else:
raise ValueError(f"'{ova}' is not a valid pitch octave 0-10.")
else:
raise ValueError(f"'{acc}' is not a valid pitch accidental 0-4.")
else:
raise ValueError(f"'{let}' is not a valid pitch letter.")
def __repr__(self):
"""
Prints an external form that, if evaluated, will create
a Pitch with the same content as this pitch.
"""
s = self.string()
if s != "R":
return f'Pitch("{s}")'
return 'Pitch()'
def __str__(self):
return self.string()
# def __str__(self):
# """
# Returns a string displaying information about the pitch within angle
# brackets. Information includes the class name, the pitch text, and
# the id of the object. It is important that you implement the __str__
# method precisely. In particular, for __str__ you want to see
# '<', '>', '0x' in your output string. The format of your output
# strings from your version of this function must look EXACTLY the
# same as in the two examples below.
# Example
# -------
# >>> str(Pitch("C#6"))
# '<Pitch: C#6 0x7fdb17e2e950>'
# >>> str(Pitch())
# '<Pitch: empty 0x7fdb1898fa70>'
# """
# s = self.string()
# return f'<Pitch: {s if s else "empty"} {hex(id(self))}>'
def __lt__(self, other):
"""
Implements Pitch < Pitch.
This method should call self.pos() and other.pos() to get the two
values to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is less than the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() < other.pos()
raise TypeError(f'{other} is not a Pitch.')
def __le__(self, other):
"""
Implements Pitch <= Pitch.
This method should call self.pos() and other.pos() to get the values
to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is less than or equal to the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() <= other.pos()
raise TypeError(f'{other} is not a Pitch.')
def __eq__(self, other):
"""
Implements Pitch == Pitch.
This method should call self.pos() and other.pos() to get the values
to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is equal to the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() == other.pos()
raise TypeError(f'{other} is not a Pitch.')
def __ne__(self, other):
"""
Implements Pitch != Pitch.
This method should call self.pos() and other.pos() to get the values
to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is not equal to the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() != other.pos()
raise TypeError(f'{other} is not a Pitch.')
def __ge__(self, other):
"""
Implements Pitch >= Pitch.
This method should call self.pos() and other.pos() to get the values
to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is greater than or equal to the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() >= other.pos()
raise TypeError(f'{other} is not a Pitch.')
def __gt__(self, other):
"""
Implements Pitch > Pitch.
This method should call self.pos() and other.pos() to get the values
to compare.
Parameters
----------
other : Pitch
The pitch to compare with this pitch.
Returns
-------
True if this Pitch is greater than the other.
Raises
------
* TypeError if other is not a Pitch.
"""
if isinstance(other, Pitch):
return self.pos() > other.pos()
raise TypeError(f'{other} is not a Pitch.')
def pos(self):
"""
Returns a unique integer representing this pitch's position in
the octave-letter-accidental space. The expression to calculate
this value is `(octave<<8) + (letter<<4) + accidental`.
"""
return (self.octave << 8) + (self.letter << 4) + self.accidental
def is_empty(self):
"""
Returns true if the Pitch is empty. A pitch is empty if its letter,
accidental and octave attributes are None. Only one of these attributes
needs to be checked because __new__ will only create a Pitch if all
three are legal values or all three are None.
"""
return self.letter is None
def string(self):
"""
Returns a string containing the pitch name including the letter,
accidental, and octave. For example, Pitch("C#7").string() would
return 'C#7'.
"""
if self.is_empty():
return 'R'
s = self._letter_names[self.letter]
s += self._accidental_names[self.accidental]
s += self._octave_names[self.octave]
return s
def keynum(self):
"""Returns the midi key number of the Pitch."""
deg = self._letter_spans[self.letter]
# convert accidental index into semitone shift, e.g. double flat == -2.
acc = self.accidental - 2
return (12 * self.octave) + deg + acc
def pnum(self):
"""
Returns the pnum (pitch class enum) of the Pitch. Pnums enumerate and
order the letter and accidental of a Pitch so they can be compared,
e.g.: C < C# < Dbb. See also: `pnums`.
"""
return self.pnums((self.letter << 4) + self.accidental)
def pc(self):
"""Returns the pitch class (0-11) of the Pitch."""
return self.keynum() % 12
def hertz(self):
"""Returns the hertz value of the Pitch."""
k = self.keynum()
return 440.0 * math.pow(2, ((k - 69) / 12))
@classmethod
def from_keynum(cls, keynum, acci=None):
"""
A class method that creates a Pitch for the specified midi key number.
Parameters
----------
keynum : int
A valid keynum 0-127.
acci : string
The accidental to use. If no accidental is provided a default is
chosen from `C C# D Eb F F# G Ab A Bb B`
Returns
-------
A new Pitch with an appropriate spelling.
Raises
------
A ValueError if the midi key number is invalid or if the pitch requested does not support the specified accidental.
"""
if not (isinstance(keynum, int) and 0 <= keynum <= 127):
raise ValueError(f"Invalid midi key number: {keynum}.")
o, i = divmod(keynum, 12)
if acci is None:
acci = ['', '#', '', 'b', '', '', '#', '', 'b', '', 'b', ''][i]
a = cls._accidental_map.get(acci)
if a is None:
raise ValueError(f"'{acci}' is not a valid accidental.")
# s = cls._enharmonic_map[i][a]
s = cls._enharmonic_map[i].get(a)
if s is None:
raise ValueError(f'No pitch for keynum {keynum} and accidental {acci}')
if s in ['B#', 'B##']:
o -= 1
elif s in ['Cb', 'Cbb']:
o += 1
return Pitch(s + cls._octave_names[o])
@classmethod
def random(cls):
'''Returns a random Pitch spelling.'''
while True:
# Note: randint's range includes upper bound
let = randint(cls._let_C, cls._let_B)
acc = randint(cls._acc_2f, cls._acc_2s)
oct = randint(cls._oct_00, cls._oct_9)
# continue if pitch is below C00 (e.g. Cb00 or Cbb00)
if oct == cls._oct_00 and let == cls._let_C and acc < cls._acc_n:
continue
# continue if pitch is above G9 or Abb9
if oct == cls._oct_9:
if let == cls._let_G and acc > cls._acc_n:
continue
if let == cls._let_A and acc > cls._acc_2f:
continue
if let == cls._let_B:
continue
return Pitch([let, acc, oct])
# The chromatic scale and the functions pitch(), keynum() and hertz()
chromatic_scale = {}
"""
A hash table (dictionary) that maps between note names, midi key
numbers and hertz values. The table's dictionary keys consist of all
integer midi key numbers and all string pitch spellings of all midi key
numbers. See build_chromatic_scale() for more information.
"""
def keynum(ref, filt=round):
"""
Returns key numbers from a pitch name, hertz value, a list
of the same, or a string sequence of the same.
Parameters
----------
ref : string | int | float | list
A pitch name, hertz value, list of the same, or a string
containing a sequence of the same separated by spaces.
filt : function | None
A function of one argument maps a floating point key number
to an integer. The default is math.round, but math.floor and
math.ceil could also be used. If filt is None then a floating
point key number is returned. In a floating point key number, the
first two digits of the fractional portion are interpreted as
the number of cents above the integer midi value.
Returns
-------
If ref is a pitch its hash table value is returned.
If ref is a hertz value its key number is calculated, filtered
and returned (see the filt parameter).
If ref is a python list of pitch names or hertz values
then a list of key numbers is returned.
If ref is a string of hertz values
delimited by spaces then a list of key numbers is returned.
If ref is a string of pitch names then a list of key numbers
are returned. Items in the list can be directly repeated by
appending ',' to an item for each repetition. If the items are
pitch names then if a pitch does not contain an explicit octave
then it inherits the previous octave number in the list.
If no octave number is provided the the middle C octave (4)
is used as the inital octave.
Raises
------
ValueError if ref is not a pitch name, hertz value or list of the same.
Examples
--------
```python
>>> keynum('C#4')
61
>>> keynum(100)
43
>>> keynum(['Cb4', 'D#6'])
[59, 87]
>>> keynum([100, 200, 300])
[43, 55, 62]
>>> keynum('cs4 d e,, f g3 a b')
[61, 62, 64, 64, 64, 65, 55, 57, 59]
>>> keynum('100, 200, 300')
[43, 43, 55, 55, 62]
```
"""
# test if original input is a list (not a string).
refislist = isinstance(ref, list)
if isinstance(ref, str):
ref = ref.strip() # remove whitespace from start and end
if ref:
if ref[0].isalpha(): # should be a pitch
try: # try to return a single keynum
return chromatic_scale[ref][0]
except KeyError:
pass
ref = parse_pitch_sequence(ref)
for i in range(len(ref)):
ref[i] = ref[i].keynum()
if not refislist:
if len(ref) == 1: return ref[0]
return ref
elif ref[0].isdigit(): # should be hertz
# if ref contains a space then take it to be a list
# of hertz values otherwise convert to a float.
if ' ' in ref:
ref = parse_number_sequence(ref)
else:
ref = float(ref)
# ref is hertz and so not in the hashtable
if isinstance(ref, (float, int)):
keyn = 69 + math.log2(ref / 440.0) * 12
if filt:
keyn = filt(keyn)
if 0 <= keyn < 128:
return keyn #filt(keyn) if filt else keyn
if isinstance(ref, list):
return [keynum(x, filt) for x in ref]
raise ValueError(f"invalid keynum input: '{ref}'.")
def hertz(ref, filt=round):
"""
Returns hertz values from a pitch name, key number, a list
of the same, or a string sequence of the same.
Parameters
----------
ref : string, int or float
A pitch name or midi key number to convert to a hertz value.
filt : function | None
A function of one argument maps a floating point key number
to an integer.
Returns
-------
If ref is a pitch name or key number its hertz hash table
value is returned.
If ref is a python list of pitch names or key numbers
then a list of hertz values is returned.
If ref is a string of key numbers
delimited by spaces then a list of hertz values is returned.
If ref is a string of pitch names then a list of hertz values
are returned. Items in this list can be directly repeated by appending
',' to an item for each repetition. If the items are pitch names then
if a pitch does not contain an explicit octave then it inherits the
previous octave number in the list. If no octave number is provided the
the middle C octave (4) is used as the inital octave.
Raises
------
ValueError if ref is not a pitch name, key number or list of the same.
Examples
--------
```python
>>> hertz('C#4')
277.1826309768721
>>> hertz(100)
2637.02045530296
>>> hertz(['Cb4', 'D#6'])
[246.94165062806206, 1244.5079348883237]
>>> hertz([48, 60, 72])
[130.8127826502993, 261.6255653005986, 523.2511306011972]
>>> hertz('cs4 d b3')
[277.1826309768721, 293.6647679174076, 246.94165062806206]
>>> hertz('48, 60')
[130.8127826502993, 130.8127826502993, 261.6255653005986]
```
"""
refislist = isinstance(ref, list)
if isinstance(ref, str):
ref = ref.strip()
if ref:
if ref[0].isalpha(): # should be a pitch
try: # try common case of a single pitch
return chromatic_scale[ref][1] # try to returning a single hertz
except KeyError: # keep going if string isnt a pitch
pass
ref = parse_pitch_sequence(ref)
# return [p.hertz() for p in ref]
for i in range(len(ref)):
ref[i] = ref[i].hertz()
if not refislist:
if len(ref) == 1: return ref[0]
return ref
elif ref[0].isdigit(): # should be a keynum
ref = parse_number_sequence(ref)
if isinstance(ref, float):
ref = filt(ref) if filt else int(ref)
if isinstance(ref, int):
return chromatic_scale[ref][1] # KeyError if int isnt valid keynum
if isinstance(ref, list):
return [hertz(x, filt) for x in ref]
raise ValueError(f"invalid hertz input: '{ref}'.")
'''
>>> musx.pitch(["C5 D E", "F4 D E"])
[[Pitch("C5"), Pitch("D5"), Pitch("E5")], [Pitch("F4"), Pitch("D4"), Pitch("E4")]]
>>> musx.pitch("C")
Pitch("C4")
>>> musx.pitch(["C"])
[Pitch("C4")]
# correct: octave 4 resets on every new string.
>>> musx.pitch(["C","D5","E"])
[Pitch("C4"), Pitch("D5"), Pitch("E4")]
# correct: explict [] around string sequence
>>> musx.pitch(["C D5 E"])
[[Pitch("C4"), Pitch("D5"), Pitch("E5")]]
# correct
musx.pitch(["C5 D E", "F4 D E", "c3 d e f g5*2"])
[[Pitch("C5"), Pitch("D5"), Pitch("E5")], [Pitch("F4"), Pitch("D4"), Pitch("E4")], [Pitch("C3"), Pitch("D3"), Pitch("E3"), Pitch("F3"), Pitch("G5"), Pitch("G5")]]
'''
def pitch(ref, filt=round, *, hz=False, acc=[]):
"""
Returns a Pitch object given a pitch name, hertz value, key
number, a list of the same, or a string of the same.
Parameters
----------
ref : string | int | float
A pitch name, key number or hertz value. For a hertz value
you must pass True to the hz parameter.
filt : function | None
A function of one argument that maps a floating point ref
to an integer. The default filter rounds to the nearest integer.
hz : True | False
If True then ref is accepted as a hertz value otherwise it is
assumed to be a key number. The default value is False.
acc : int | list
An ordered preference list of accidentals to use in the pitch spelling.
Values range from -2 (double flat) to 2 (double sharp) with 0 being
no accidental.
Examples
--------
>>> pitch(60)
Pitch("C4")
>>> pitch(60, acc=[1])
Pitch("B#3")
>>> pitch(60, acc=[-2])
Pitch("Dbb4")
>>> pitch(440*3/2, hz=True)
Pitch("E5")
>>> pitch([48, 60, 72])
[Pitch("C3"), Pitch("C4"), Pitch("C5")]
>>> pitch("67,, 63")
[Pitch("G4"), Pitch("G4"), Pitch("G4"), Pitch("Eb4")]
"""
# cache if incoming ref is a list or not
refislist = isinstance(ref, list)
# if parsing hertz first convert to keynum or list of keynums
if hz:
ref = keynum(ref)
# ref is float keynum, convert to int
if isinstance(ref, float):
ref = filt(ref)
# ref is an integer keynum, look up the pitch
if isinstance(ref, int):
try:
data = chromatic_scale[ref]
if data:
if not acc:
# default prefers sharps for C# and F# otherwise flats.
acc = [2, 3, 1, 4, 0] if ref % 12 in [1, 6] else [2, 1, 3, 0, 4]
else:
if not isinstance(acc,list):
acc = [acc]
acc = [a + 2 for a in acc] # convert -2..2 to 0..4
try:
return next(data[0][i] for i in acc if data[0][i])
except StopIteration as err:
raise ValueError("No pitch for accidentals {acc}.") from err
except KeyError as err:
raise ValueError(f"no table entry for midi note {ref}.") from err
# ref is a string sequence of keynums, a string sequence
# of pitch names, or a pitch name.
if isinstance(ref, str):
ref = ref.strip()
if ref:
if ref[0].isalpha(): # should be a pitch
try:
return chromatic_scale[ref][2] # try to return a single pitch
except KeyError:
pass
ref = parse_pitch_sequence(ref)
# if the input was not a list, then if there is only one element
# in the list return the element without the list. if the input
# was a list then continue to the list check below.
if not refislist:
if len(ref) == 1: return ref[0]
return ref
elif ref[0].isdigit(): # should be a hertz
ref = parse_number_sequence(ref)
# ref is a list of keynums hertz values
if isinstance(ref, list):
return [pitch(x, filt, hz=False, acc=acc) for x in ref]
raise ValueError(f"invalid keynum input: '{ref}'.")
def parse_pitch_sequence(string):
seq = tools.parse_string_sequence(string)
oct = '4'
pitches = []
for i,p in enumerate(seq):
o = p[len(p.rstrip('0123456789')):]
if o:
oct = o
else:
seq[i] = p+oct # add octave to pitch
try:
pitches.append( chromatic_scale[seq[i]][2] )
except KeyError:
raise ValueError(f"invalid pitch: '{seq[i]}'.")
return pitches
def parse_number_sequence(string):
seq = tools.parse_string_sequence(string)
for i,p in enumerate(seq):
if not p[0] in '0123456789+-.':
raise ValueError(f"invalid numeric: '{p}'.")
seq[i] = float(p) if ('.' in p) else int(p)
return seq
def scale(start, length, *steps, fit=None):
"""
Returns a list of key numbers beginning on start and incremented by
successive interval increments. The step values loop if the
length of the scale is greater than the number of intervals in steps.
Parameters
----------
start : int | float
The initial key number that starts the scale.
length : int
The length of the scale including the start.
steps : ints | floats | list | tuple
An in-line (variadic) series of increments defining the
intervals beween the key numbers in the scale. This series
can also be specified as a single list or tuple of increments.
fit : None | [lb, ub, mode]
Limits placed on the range of the scale. If the value is None there
are no limits. Otherwise fit should be a list or tuple of 3
elements containing a lower bound, upper bound and string mode.
See: `musx.tools.fit()` for more information.
Returns
-------
A list of key numbers defining the scale.
Examples
--------
```python
# 16 note (3 octave) pentatonic scale on C#4
>>> scale(61, 16, 2, 3, 2, 2, 3)
[60, 62, 65, 67, 69, 72, 74, 77, 79, 81, 84, 86, 89, 91, 93, 96]
# one octave of the octotonic scale
>>> scale(60, 9, 1, 2)
[60, 61, 63, 64, 66, 67, 69, 70, 72]
# interval cycle
>>> scale(60, 12, (1, 2, 3, 4, 5))
[60, 61, 63, 66, 70, 75, 76, 78, 81, 85]
# cycle of fifths compressed to one octave
>>> pitch(scale(0, 12, 7, fit=[60, 72, 'wrap']))
['C5', 'G4', 'D4', 'A4', 'E4', 'B4', 'F#4', 'C#4', 'Ab4', 'Eb4', 'Bb4', 'F4']
```
"""
series = [start]
numsteps = len(steps)
if fit:
fit = list(fit) # copy it
series[0] = tools.fit(series[0], *fit)
# the variadic steps is a list of values or a list of one list.
if numsteps == 1 and isinstance(steps[0], (list, tuple)):
steps = steps[0]
numsteps = len(steps)
for i in range(0, length-1):
knum = series[-1] + steps[i % numsteps]
if fit:
knum = tools.fit(knum, *fit)
series.append(knum)
return series
def build_chromatic_scale():
"""
Returns a hash table (dictionary) with entries that map between
pitch names, Pitch instances, midi key numbers, and hertz values. The hash keys
are all possible MIDI key numbers 0 to 127 and all possible pitch
names for each MIDI key number. See module documentation for more information.
For a dictionary key that is a pitch name, its dictionary value will
be a two element list containing the pitch's integer keynum and its hertz
value:
`<pitch_name>: [<keynum>, <hertz>, <Pitch>]`
For a dictionary key that is an integer midi key number, its dictionary value
will be a two element list:
`<keynum> : [[<Pitch_bb>, <Pitch_b>, <Pitch>, <Pitch_#>, <Pitch_##>], <hertz>]`
The first element in the list is a sublist of length five containing all
possible Pitch objects for the given key number. The five list
locations represent accidental ordering from double-flat to double-sharp
spellings: if a pitch spelling uses one sharp it would be added at index 3,
and if a pitch spelling is not possible for a given accidental position that
position will hold an empty string. The second element in the value list is
the key number's hertz value.
To calculate a hertz value from a key number use the formula:
`hertz = 440.0 * 2 ** ((keynum - 69) / 12)`
To calculate a key number from a hertz value use the reverse formula:
`keynum = 69 + log(hertz / 440, 2) * 12`
"""
# letter names for note entries
letter_names = ["C", "D", "E", "F", "G", "A", "B"]
# symbolic accidental names for notes, "" is for a diatonic note without accidental
accidental_names = ["bb", "b", "", "#", "##"]
# safe accidental name variants
accidental_safe_name = {"bb": "ff", "b": "f", "": "", "#": "s", "##": "ss"}
# lowest octave name is 00 instead of -1.
octave_names = ["00", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
# semi-tone shifts applied to diatonic notes within the octave
letter_steps = [0, 2, 4, 5, 7, 9, 11]
# semi-tone shift applied to diatonic notes for notes with accidentals
accidental_steps = [-2, -1, 0, 1, 2]
# init table with midi keys, each holding a note array and frequency.
# the note array will hold all chromatic note names for the midi key.
table = {key: [['','','','',''], 440.0 * 2 ** ((key-69.0)/12)] for key in range(128)}
# iterate all the octave for midi key numbers 0-127.
for octave_index, octave_name in enumerate(octave_names):
# starting key number for notes in the octave
octave_midi = octave_index * 12
# iterate all the diatonic letter names for notes.
for letter_index, letter_name in enumerate(letter_names):
# the midi key number of the diatonic note
letter_midi = octave_midi + letter_steps[letter_index]
# iterate the accidentals and create all possible note names for the letter
for accidental_index, accidental_name in enumerate(accidental_names):
# get the semitone amount to shift the letter note by
accidental_step = accidental_steps[accidental_index]
# calculate the midi key number for the note
note_midi = letter_midi + accidental_step
# stop notes outside of midi range (there are only a few)
if 0 <= note_midi <= 127:
accidental_name = accidental_names[accidental_index]
# create the official note name
note_name1 = letter_name + accidental_name + octave_name
# create the Pitch for the official name.
note_pitch = Pitch(note_name1)
# create variants (lower case letter names, safe accidental names)
note_name2 = letter_name.lower() + accidental_name + octave_name
note_name3 = letter_name + accidental_safe_name[accidental_name] + octave_name
note_name4 = letter_name.lower() + accidental_safe_name[accidental_name] + octave_name
# fetch the midi data from the table
midi_data = table[note_midi]
# add the note to the note array
##midi_data[0][accidental_index] = note_name1
midi_data[0][accidental_index] = note_pitch
# get the frequency from the midi data and add it to the note data.
note_freq = table[note_midi][1]
# add the hash entry for the note name
##table[note_name1] = [note_midi, note_freq]
table[note_name1] = [note_midi, note_freq, note_pitch]
# add the variants (lower case letter, safe accidentals)
table[note_name2] = table[note_name1]
table[note_name3] = table[note_name1]
table[note_name4] = table[note_name1]
# add entries for musical rests
r = Pitch()
table['R'] = [-1, 0.0, r]
table['r'] = table['R']
#table[-1] = [[r,r,r,r,r], 0.0]
return table
# Build the hash table
chromatic_scale = build_chromatic_scale()
Functions
def hertz(ref, filt=<built-in function round>)
-
Returns hertz values from a pitch name, key number, a list of the same, or a string sequence of the same.
Parameters
ref
:string, int
orfloat
- A pitch name or midi key number to convert to a hertz value.
filt
:function | None
- A function of one argument maps a floating point key number to an integer.
Returns
If ref is a pitch name or key number its hertz hash table value is returned.
If ref is a python list of pitch names or key numbers then a list of hertz values is returned.
If ref is a string of key numbers delimited by spaces then a list of hertz values is returned.
If ref is a string of pitch names then a list of hertz values are returned. Items in this list can be directly repeated by appending ',' to an item for each repetition. If the items are pitch names then if a pitch does not contain an explicit octave then it inherits the previous octave number in the list. If no octave number is provided the the middle C octave (4) is used as the inital octave.
Raises
ValueError if ref is not a pitch name, key number or list of the same.
Examples
>>> hertz('C#4') 277.1826309768721 >>> hertz(100) 2637.02045530296 >>> hertz(['Cb4', 'D#6']) [246.94165062806206, 1244.5079348883237] >>> hertz([48, 60, 72]) [130.8127826502993, 261.6255653005986, 523.2511306011972] >>> hertz('cs4 d b3') [277.1826309768721, 293.6647679174076, 246.94165062806206] >>> hertz('48, 60') [130.8127826502993, 130.8127826502993, 261.6255653005986]
Expand source code
def hertz(ref, filt=round): """ Returns hertz values from a pitch name, key number, a list of the same, or a string sequence of the same. Parameters ---------- ref : string, int or float A pitch name or midi key number to convert to a hertz value. filt : function | None A function of one argument maps a floating point key number to an integer. Returns ------- If ref is a pitch name or key number its hertz hash table value is returned. If ref is a python list of pitch names or key numbers then a list of hertz values is returned. If ref is a string of key numbers delimited by spaces then a list of hertz values is returned. If ref is a string of pitch names then a list of hertz values are returned. Items in this list can be directly repeated by appending ',' to an item for each repetition. If the items are pitch names then if a pitch does not contain an explicit octave then it inherits the previous octave number in the list. If no octave number is provided the the middle C octave (4) is used as the inital octave. Raises ------ ValueError if ref is not a pitch name, key number or list of the same. Examples -------- ```python >>> hertz('C#4') 277.1826309768721 >>> hertz(100) 2637.02045530296 >>> hertz(['Cb4', 'D#6']) [246.94165062806206, 1244.5079348883237] >>> hertz([48, 60, 72]) [130.8127826502993, 261.6255653005986, 523.2511306011972] >>> hertz('cs4 d b3') [277.1826309768721, 293.6647679174076, 246.94165062806206] >>> hertz('48, 60') [130.8127826502993, 130.8127826502993, 261.6255653005986] ``` """ refislist = isinstance(ref, list) if isinstance(ref, str): ref = ref.strip() if ref: if ref[0].isalpha(): # should be a pitch try: # try common case of a single pitch return chromatic_scale[ref][1] # try to returning a single hertz except KeyError: # keep going if string isnt a pitch pass ref = parse_pitch_sequence(ref) # return [p.hertz() for p in ref] for i in range(len(ref)): ref[i] = ref[i].hertz() if not refislist: if len(ref) == 1: return ref[0] return ref elif ref[0].isdigit(): # should be a keynum ref = parse_number_sequence(ref) if isinstance(ref, float): ref = filt(ref) if filt else int(ref) if isinstance(ref, int): return chromatic_scale[ref][1] # KeyError if int isnt valid keynum if isinstance(ref, list): return [hertz(x, filt) for x in ref] raise ValueError(f"invalid hertz input: '{ref}'.")
def keynum(ref, filt=<built-in function round>)
-
Returns key numbers from a pitch name, hertz value, a list of the same, or a string sequence of the same.
Parameters
ref
:string | int | float | list
- A pitch name, hertz value, list of the same, or a string containing a sequence of the same separated by spaces.
filt
:function | None
- A function of one argument maps a floating point key number to an integer. The default is math.round, but math.floor and math.ceil could also be used. If filt is None then a floating point key number is returned. In a floating point key number, the first two digits of the fractional portion are interpreted as the number of cents above the integer midi value.
Returns
If ref is a pitch its hash table value is returned.
If ref is a hertz value its key number is calculated, filtered and returned (see the filt parameter).
If ref is a python list of pitch names or hertz values then a list of key numbers is returned.
If ref is a string of hertz values delimited by spaces then a list of key numbers is returned.
If ref is a string of pitch names then a list of key numbers are returned. Items in the list can be directly repeated by appending ',' to an item for each repetition. If the items are pitch names then if a pitch does not contain an explicit octave then it inherits the previous octave number in the list. If no octave number is provided the the middle C octave (4) is used as the inital octave.
Raises
ValueError if ref is not a pitch name, hertz value or list of the same.
Examples
>>> keynum('C#4') 61 >>> keynum(100) 43 >>> keynum(['Cb4', 'D#6']) [59, 87] >>> keynum([100, 200, 300]) [43, 55, 62] >>> keynum('cs4 d e,, f g3 a b') [61, 62, 64, 64, 64, 65, 55, 57, 59] >>> keynum('100, 200, 300') [43, 43, 55, 55, 62]
Expand source code
def keynum(ref, filt=round): """ Returns key numbers from a pitch name, hertz value, a list of the same, or a string sequence of the same. Parameters ---------- ref : string | int | float | list A pitch name, hertz value, list of the same, or a string containing a sequence of the same separated by spaces. filt : function | None A function of one argument maps a floating point key number to an integer. The default is math.round, but math.floor and math.ceil could also be used. If filt is None then a floating point key number is returned. In a floating point key number, the first two digits of the fractional portion are interpreted as the number of cents above the integer midi value. Returns ------- If ref is a pitch its hash table value is returned. If ref is a hertz value its key number is calculated, filtered and returned (see the filt parameter). If ref is a python list of pitch names or hertz values then a list of key numbers is returned. If ref is a string of hertz values delimited by spaces then a list of key numbers is returned. If ref is a string of pitch names then a list of key numbers are returned. Items in the list can be directly repeated by appending ',' to an item for each repetition. If the items are pitch names then if a pitch does not contain an explicit octave then it inherits the previous octave number in the list. If no octave number is provided the the middle C octave (4) is used as the inital octave. Raises ------ ValueError if ref is not a pitch name, hertz value or list of the same. Examples -------- ```python >>> keynum('C#4') 61 >>> keynum(100) 43 >>> keynum(['Cb4', 'D#6']) [59, 87] >>> keynum([100, 200, 300]) [43, 55, 62] >>> keynum('cs4 d e,, f g3 a b') [61, 62, 64, 64, 64, 65, 55, 57, 59] >>> keynum('100, 200, 300') [43, 43, 55, 55, 62] ``` """ # test if original input is a list (not a string). refislist = isinstance(ref, list) if isinstance(ref, str): ref = ref.strip() # remove whitespace from start and end if ref: if ref[0].isalpha(): # should be a pitch try: # try to return a single keynum return chromatic_scale[ref][0] except KeyError: pass ref = parse_pitch_sequence(ref) for i in range(len(ref)): ref[i] = ref[i].keynum() if not refislist: if len(ref) == 1: return ref[0] return ref elif ref[0].isdigit(): # should be hertz # if ref contains a space then take it to be a list # of hertz values otherwise convert to a float. if ' ' in ref: ref = parse_number_sequence(ref) else: ref = float(ref) # ref is hertz and so not in the hashtable if isinstance(ref, (float, int)): keyn = 69 + math.log2(ref / 440.0) * 12 if filt: keyn = filt(keyn) if 0 <= keyn < 128: return keyn #filt(keyn) if filt else keyn if isinstance(ref, list): return [keynum(x, filt) for x in ref] raise ValueError(f"invalid keynum input: '{ref}'.")
def pitch(ref, filt=<built-in function round>, *, hz=False, acc=[])
-
Returns a Pitch object given a pitch name, hertz value, key number, a list of the same, or a string of the same.
Parameters
ref
:string | int | float
- A pitch name, key number or hertz value. For a hertz value you must pass True to the hz parameter.
filt
:function | None
- A function of one argument that maps a floating point ref to an integer. The default filter rounds to the nearest integer.
hz
:True | False
- If True then ref is accepted as a hertz value otherwise it is assumed to be a key number. The default value is False.
acc
:int | list
- An ordered preference list of accidentals to use in the pitch spelling. Values range from -2 (double flat) to 2 (double sharp) with 0 being no accidental.
Examples
>>> pitch(60) Pitch("C4") >>> pitch(60, acc=[1]) Pitch("B#3") >>> pitch(60, acc=[-2]) Pitch("Dbb4") >>> pitch(440*3/2, hz=True) Pitch("E5") >>> pitch([48, 60, 72]) [Pitch("C3"), Pitch("C4"), Pitch("C5")] >>> pitch("67,, 63") [Pitch("G4"), Pitch("G4"), Pitch("G4"), Pitch("Eb4")]
Expand source code
def pitch(ref, filt=round, *, hz=False, acc=[]): """ Returns a Pitch object given a pitch name, hertz value, key number, a list of the same, or a string of the same. Parameters ---------- ref : string | int | float A pitch name, key number or hertz value. For a hertz value you must pass True to the hz parameter. filt : function | None A function of one argument that maps a floating point ref to an integer. The default filter rounds to the nearest integer. hz : True | False If True then ref is accepted as a hertz value otherwise it is assumed to be a key number. The default value is False. acc : int | list An ordered preference list of accidentals to use in the pitch spelling. Values range from -2 (double flat) to 2 (double sharp) with 0 being no accidental. Examples -------- >>> pitch(60) Pitch("C4") >>> pitch(60, acc=[1]) Pitch("B#3") >>> pitch(60, acc=[-2]) Pitch("Dbb4") >>> pitch(440*3/2, hz=True) Pitch("E5") >>> pitch([48, 60, 72]) [Pitch("C3"), Pitch("C4"), Pitch("C5")] >>> pitch("67,, 63") [Pitch("G4"), Pitch("G4"), Pitch("G4"), Pitch("Eb4")] """ # cache if incoming ref is a list or not refislist = isinstance(ref, list) # if parsing hertz first convert to keynum or list of keynums if hz: ref = keynum(ref) # ref is float keynum, convert to int if isinstance(ref, float): ref = filt(ref) # ref is an integer keynum, look up the pitch if isinstance(ref, int): try: data = chromatic_scale[ref] if data: if not acc: # default prefers sharps for C# and F# otherwise flats. acc = [2, 3, 1, 4, 0] if ref % 12 in [1, 6] else [2, 1, 3, 0, 4] else: if not isinstance(acc,list): acc = [acc] acc = [a + 2 for a in acc] # convert -2..2 to 0..4 try: return next(data[0][i] for i in acc if data[0][i]) except StopIteration as err: raise ValueError("No pitch for accidentals {acc}.") from err except KeyError as err: raise ValueError(f"no table entry for midi note {ref}.") from err # ref is a string sequence of keynums, a string sequence # of pitch names, or a pitch name. if isinstance(ref, str): ref = ref.strip() if ref: if ref[0].isalpha(): # should be a pitch try: return chromatic_scale[ref][2] # try to return a single pitch except KeyError: pass ref = parse_pitch_sequence(ref) # if the input was not a list, then if there is only one element # in the list return the element without the list. if the input # was a list then continue to the list check below. if not refislist: if len(ref) == 1: return ref[0] return ref elif ref[0].isdigit(): # should be a hertz ref = parse_number_sequence(ref) # ref is a list of keynums hertz values if isinstance(ref, list): return [pitch(x, filt, hz=False, acc=acc) for x in ref] raise ValueError(f"invalid keynum input: '{ref}'.")
def scale(start, length, *steps, fit=None)
-
Returns a list of key numbers beginning on start and incremented by successive interval increments. The step values loop if the length of the scale is greater than the number of intervals in steps.
Parameters
start
:int | float
- The initial key number that starts the scale.
length
:int
- The length of the scale including the start.
steps
:ints | floats | list | tuple
- An in-line (variadic) series of increments defining the intervals beween the key numbers in the scale. This series can also be specified as a single list or tuple of increments.
fit
:None | [lb, ub, mode]
- Limits placed on the range of the scale. If the value is None there
are no limits. Otherwise fit should be a list or tuple of 3
elements containing a lower bound, upper bound and string mode.
See:
fit()
for more information.
Returns
A list of key numbers defining the scale.
Examples
# 16 note (3 octave) pentatonic scale on C#4 >>> scale(61, 16, 2, 3, 2, 2, 3) [60, 62, 65, 67, 69, 72, 74, 77, 79, 81, 84, 86, 89, 91, 93, 96] # one octave of the octotonic scale >>> scale(60, 9, 1, 2) [60, 61, 63, 64, 66, 67, 69, 70, 72] # interval cycle >>> scale(60, 12, (1, 2, 3, 4, 5)) [60, 61, 63, 66, 70, 75, 76, 78, 81, 85] # cycle of fifths compressed to one octave >>> pitch(scale(0, 12, 7, fit=[60, 72, 'wrap'])) ['C5', 'G4', 'D4', 'A4', 'E4', 'B4', 'F#4', 'C#4', 'Ab4', 'Eb4', 'Bb4', 'F4']
Expand source code
def scale(start, length, *steps, fit=None): """ Returns a list of key numbers beginning on start and incremented by successive interval increments. The step values loop if the length of the scale is greater than the number of intervals in steps. Parameters ---------- start : int | float The initial key number that starts the scale. length : int The length of the scale including the start. steps : ints | floats | list | tuple An in-line (variadic) series of increments defining the intervals beween the key numbers in the scale. This series can also be specified as a single list or tuple of increments. fit : None | [lb, ub, mode] Limits placed on the range of the scale. If the value is None there are no limits. Otherwise fit should be a list or tuple of 3 elements containing a lower bound, upper bound and string mode. See: `musx.tools.fit()` for more information. Returns ------- A list of key numbers defining the scale. Examples -------- ```python # 16 note (3 octave) pentatonic scale on C#4 >>> scale(61, 16, 2, 3, 2, 2, 3) [60, 62, 65, 67, 69, 72, 74, 77, 79, 81, 84, 86, 89, 91, 93, 96] # one octave of the octotonic scale >>> scale(60, 9, 1, 2) [60, 61, 63, 64, 66, 67, 69, 70, 72] # interval cycle >>> scale(60, 12, (1, 2, 3, 4, 5)) [60, 61, 63, 66, 70, 75, 76, 78, 81, 85] # cycle of fifths compressed to one octave >>> pitch(scale(0, 12, 7, fit=[60, 72, 'wrap'])) ['C5', 'G4', 'D4', 'A4', 'E4', 'B4', 'F#4', 'C#4', 'Ab4', 'Eb4', 'Bb4', 'F4'] ``` """ series = [start] numsteps = len(steps) if fit: fit = list(fit) # copy it series[0] = tools.fit(series[0], *fit) # the variadic steps is a list of values or a list of one list. if numsteps == 1 and isinstance(steps[0], (list, tuple)): steps = steps[0] numsteps = len(steps) for i in range(0, length-1): knum = series[-1] + steps[i % numsteps] if fit: knum = tools.fit(knum, *fit) series.append(knum) return series
Classes
class Pitch (arg=None)
-
Creates a Pitch from a string or list, if neither is provided an empty Pitch is returned. The legal constructor forms are:
- Pitch(string) - creates a Pitch from a pitch name string.
- Pitch([l, a, o]) - creates a Pitch from a three element pitch list containing a letter, accidental and octave index (see below).
- Pitch() - creates an empty Pitch.
The format of a Pitch name string is:
<pitch> := <letter>, [<accidental>], <octave> <letter> := 'C' | 'D' | 'E' | 'F' | 'G' | 'A' | 'B' | 'c' | 'd' | 'e' | 'f' | 'g' | 'a' | 'b' <accidental> := <2flat> | <flat> | <natural> | <sharp> | <2sharp> <2flat> := 'bb' | 'ff' <flat> := 'b' | 'f' <natural> := '' <sharp> := '#' | 's' <2sharp> := '##' | 'ss' <octave> := '00' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
Parameters
arg
:string | list | None
- A pitch name string, a list of three pitch indexes, or None.
Returns
A new Pitch instance.
Raises
- TypeError if arg is a invalid pitch list.
- TypeError if arg is an invalid pitch.
Expand source code
class Pitch (PitchBase): """ Creates a Pitch from a string or list, if neither is provided an empty Pitch is returned. The legal constructor forms are: * Pitch(string) - creates a Pitch from a pitch name string. * Pitch([l, a, o]) - creates a Pitch from a three element pitch list containing a letter, accidental and octave index (see below). * Pitch() - creates an empty Pitch. The format of a Pitch name string is: ``` <pitch> := <letter>, [<accidental>], <octave> <letter> := 'C' | 'D' | 'E' | 'F' | 'G' | 'A' | 'B' | 'c' | 'd' | 'e' | 'f' | 'g' | 'a' | 'b' <accidental> := <2flat> | <flat> | <natural> | <sharp> | <2sharp> <2flat> := 'bb' | 'ff' <flat> := 'b' | 'f' <natural> := '' <sharp> := '#' | 's' <2sharp> := '##' | 'ss' <octave> := '00' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ``` Parameters ---------- arg : string | list | None A pitch name string, a list of three pitch indexes, or None. Returns ------- A new Pitch instance. Raises ------ * TypeError if arg is a invalid pitch list. * TypeError if arg is an invalid pitch. """ # Pitch letter constants (0-6). _let_C, _let_D, _let_E, _let_F, _let_G, _let_A, _let_B = range(7) # Maps pitch-letter names onto zero based indexes. _letter_map = {"C": _let_C, "D": _let_D, "E": _let_E, "F": _let_F, "G": _let_G, "A": _let_A, "B": _let_B, "c": _let_C, "d": _let_D, "e": _let_E, "f": _let_F, "g": _let_G, "a": _let_A, "b": _let_B } # Octave constants for code readability. _oct_00, _oct_0, _oct_1, _oct_2, _oct_3, _oct_4, _oct_5, _oct_6, _oct_7, _oct_8, _oct_9 = range(11) # Maps octave names onto zero based indexes. _octave_map = {"00": _oct_00, "0": _oct_0, "1": _oct_1, "2": _oct_2, "3": _oct_3, "4": _oct_4, "5": _oct_5, "6": _oct_6, "7": _oct_7, "8": _oct_8, "9": _oct_9} # Accidental constants for code readability. _acc_2f, _acc_f, _acc_n, _acc_s, _acc_2s = range(5) # Maps accidental names onto zero based indexes. _accidental_map = {"bb": _acc_2f, "b": _acc_f, "": _acc_n, "#": _acc_s, "##": _acc_2s, "ff": _acc_2f, "f": _acc_f, "n": _acc_n, "s": _acc_s, "ss": _acc_2s} _enharmonic_map = [{_acc_s: 'B#', _acc_n: 'C', _acc_2f: 'Dbb'}, {_acc_2s: 'B##', _acc_s: 'C#', _acc_f: 'Db'}, {_acc_2s: 'C##', _acc_n: 'D', _acc_2f: 'Ebb'}, {_acc_s: 'D#', _acc_f: 'Eb', _acc_2f: 'Fbb'}, {_acc_2s: 'D##', _acc_n: 'E', _acc_f: 'Fb'}, {_acc_s: 'E#', _acc_n: 'F', _acc_2f: 'Gbb'}, {_acc_2s: 'E##', _acc_s: 'F#', _acc_f: 'Gb'}, {_acc_2s: 'F##', _acc_n: 'G', _acc_2f: 'Abb'}, {_acc_s: 'G#', _acc_f: 'Ab'}, {_acc_2s: 'G##', _acc_n: 'A', _acc_2f: 'Bbb'}, {_acc_s: 'A#', _acc_f: 'Bb', _acc_2f: 'Cbb'}, {_acc_2s: 'A##', _acc_n: 'B', _acc_f: 'Cb'}] # Reverse map of pitch indexes 0-6 onto their canonical names. _letter_names = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] # Reverse map of accidental indexes 0-4 onto their symbolic names. _accidental_names = ['bb', 'b', '', '#', '##'] # Reverse map of pitch indexes 0-4 onto their safe names. _accidental_safe_names = ['ff', 'f', '', 's', 'ss'] # Reverse map of pitch indexes 0-10 onto their canonical names. _octave_names = ['00', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # Diatonic letter distances in semitones. _letter_spans = [0, 2, 4, 5, 7, 9, 11] # ## The minimum pnum identifier value. # _min_pcid = (_let_C << 4 | _acc_2f) # # ## The maximum pnum identifier value. # _max_pcid = (_let_B << 4 | _acc_2s) pnums = IntEnum('Pnum', [(lj + aj, ((li << 4) + ai)) for li, lj in enumerate(["C", "D", "E", "F", "G", "A", "B"]) for ai, aj in enumerate(["ff", "f", "", "s", "ss"])]) """ A class variable that holds an IntEnum of all possible letter-and-accidental combinations Cff up to Bss. (Since the accidental character # is illegal as a python enum name pnums use the 'safe versions' of the accidental names: 'ff' upto 'ss'. A pnum value is a one byte integer 'llllaaaa', where 'llll' is its letter index 0-6, and 'aaaa' is its accidental index 0-4. Pnums can be compared using regular math relations. """ def __new__(cls, arg=None): # Check for valid types and lengths up front. if arg is None or arg in ['R','r']: return cls._values_to_pitch(None, None, None) if isinstance(arg, list): if len(arg) == 3 and all(isinstance(a, int) for a in arg): return cls._values_to_pitch(*arg) else: raise TypeError(f'{arg} is an invalid pitch list.') if isinstance(arg, str) and len(arg) >= 2: return cls._string_to_pitch(arg) raise TypeError(f"'{arg}' is an invalid pitch.") @classmethod def _string_to_pitch(cls, arg): """ A private method that accepts a pitch string and parses it into three integer index values: letter, accidental, and octave. If all three values can be parsed from the string they should then passed to the _values_to_pitch() method to assign them to the instance's attributes. A ValueError should be raised for any value that cannot be parsed from the string. See: _values_to_pitch(). Parameter --------- arg : string The string to convert to a pitch. Returns ------- A new Pitch instance. Raises ------ * ValueError is arg is not a valid pitch name. """ strlen = len(arg) index = 0 letter = cls._letter_map.get(arg[index].upper()) if letter is None: raise ValueError(f"'{arg}' is not a valid pitch name.") while index < strlen and not arg[index].isdigit(): index += 1 if index == strlen: raise ValueError(f"'{arg}' is not a valid pitch name.") octave = cls._octave_map.get(arg[index::]) if octave is None: raise ValueError(f"'{arg}' is not a valid pitch name.") accidental = cls._acc_n # default accidental natural if index > 1: accidental = cls._accidental_map.get(arg[1:index]) if accidental is None: raise ValueError(f"'{arg}' is not a valid pitch name.") return cls._values_to_pitch(letter, accidental, octave) @classmethod def _values_to_pitch(cls, let, acc, ova): """ A private method that checks three values (letter, accidental and octave) to make sure they are either valid index values for the letter, accidental and octave attributes or they are None. The valid integer values are: * A letter index 0-6 corresponding to the pitch letter names ['C', 'D', 'E', 'F', 'G', 'A', 'B']. * An accidental index 0-4 corresponding to symbolic accidental names ['bb', 'b', '', '#', '##'] or 'safe' accidental names ['ff', 'f', 'n', 's', 'ss']. * An octave index 0-10 corresponding to the pitch octave names ['00', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']. If any value is out of range the method will raise a ValueError for that value. If all values are legal the method will make the following 'edge case' tests: * Values cannot produce a pitch below midi key number 0 (lowest pitch is 'C00') * Values cannot produce a pitch above midi key number 127 (highest pitches are 'G9' and 'Abb9') If all the edge case checks pass then _values_to_pitch() should call the super's __new()__ method: super(Pitch, cls).__new__(cls, let, acc, ova) otherwise it should raise a ValueError for the offending values. NOTE: _values_to_pitch should be the only method in your implementation that calls the super method. Parameter --------- let : string The pitch letter string to convert to a pitch. acc : string The accidental string to convert to a pitch. ova : string The octave string to convert to a pitch. Returns ------- A new Pitch instance. Raises ------ * ValueError is arg is not a valid pitch name. """ if let is None or let in ['R', 'r']: return super(Pitch, cls).__new__(cls, None, None, None) if 0 <= let <= 6: if 0 <= acc <= 4: if 0 <= ova <= 10: if ova == 0 and let == cls._let_C and acc < cls._acc_n: nam = cls._letter_names[let] + cls._accidental_names[acc] + cls._octave_names[ova] raise ValueError(f"Pitch '{nam}': midi number below 0.") if ova == 10 and cls._letter_spans[let] + acc-2 > 7: nam = cls._letter_names[let] + cls._accidental_names[acc] + cls._octave_names[ova] raise ValueError(f"Pitch '{nam}': midi number exceeds 127.") return super(Pitch, cls).__new__(cls, let, acc, ova) else: raise ValueError(f"'{ova}' is not a valid pitch octave 0-10.") else: raise ValueError(f"'{acc}' is not a valid pitch accidental 0-4.") else: raise ValueError(f"'{let}' is not a valid pitch letter.") def __repr__(self): """ Prints an external form that, if evaluated, will create a Pitch with the same content as this pitch. """ s = self.string() if s != "R": return f'Pitch("{s}")' return 'Pitch()' def __str__(self): return self.string() # def __str__(self): # """ # Returns a string displaying information about the pitch within angle # brackets. Information includes the class name, the pitch text, and # the id of the object. It is important that you implement the __str__ # method precisely. In particular, for __str__ you want to see # '<', '>', '0x' in your output string. The format of your output # strings from your version of this function must look EXACTLY the # same as in the two examples below. # Example # ------- # >>> str(Pitch("C#6")) # '<Pitch: C#6 0x7fdb17e2e950>' # >>> str(Pitch()) # '<Pitch: empty 0x7fdb1898fa70>' # """ # s = self.string() # return f'<Pitch: {s if s else "empty"} {hex(id(self))}>' def __lt__(self, other): """ Implements Pitch < Pitch. This method should call self.pos() and other.pos() to get the two values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is less than the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() < other.pos() raise TypeError(f'{other} is not a Pitch.') def __le__(self, other): """ Implements Pitch <= Pitch. This method should call self.pos() and other.pos() to get the values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is less than or equal to the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() <= other.pos() raise TypeError(f'{other} is not a Pitch.') def __eq__(self, other): """ Implements Pitch == Pitch. This method should call self.pos() and other.pos() to get the values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is equal to the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() == other.pos() raise TypeError(f'{other} is not a Pitch.') def __ne__(self, other): """ Implements Pitch != Pitch. This method should call self.pos() and other.pos() to get the values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is not equal to the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() != other.pos() raise TypeError(f'{other} is not a Pitch.') def __ge__(self, other): """ Implements Pitch >= Pitch. This method should call self.pos() and other.pos() to get the values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is greater than or equal to the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() >= other.pos() raise TypeError(f'{other} is not a Pitch.') def __gt__(self, other): """ Implements Pitch > Pitch. This method should call self.pos() and other.pos() to get the values to compare. Parameters ---------- other : Pitch The pitch to compare with this pitch. Returns ------- True if this Pitch is greater than the other. Raises ------ * TypeError if other is not a Pitch. """ if isinstance(other, Pitch): return self.pos() > other.pos() raise TypeError(f'{other} is not a Pitch.') def pos(self): """ Returns a unique integer representing this pitch's position in the octave-letter-accidental space. The expression to calculate this value is `(octave<<8) + (letter<<4) + accidental`. """ return (self.octave << 8) + (self.letter << 4) + self.accidental def is_empty(self): """ Returns true if the Pitch is empty. A pitch is empty if its letter, accidental and octave attributes are None. Only one of these attributes needs to be checked because __new__ will only create a Pitch if all three are legal values or all three are None. """ return self.letter is None def string(self): """ Returns a string containing the pitch name including the letter, accidental, and octave. For example, Pitch("C#7").string() would return 'C#7'. """ if self.is_empty(): return 'R' s = self._letter_names[self.letter] s += self._accidental_names[self.accidental] s += self._octave_names[self.octave] return s def keynum(self): """Returns the midi key number of the Pitch.""" deg = self._letter_spans[self.letter] # convert accidental index into semitone shift, e.g. double flat == -2. acc = self.accidental - 2 return (12 * self.octave) + deg + acc def pnum(self): """ Returns the pnum (pitch class enum) of the Pitch. Pnums enumerate and order the letter and accidental of a Pitch so they can be compared, e.g.: C < C# < Dbb. See also: `pnums`. """ return self.pnums((self.letter << 4) + self.accidental) def pc(self): """Returns the pitch class (0-11) of the Pitch.""" return self.keynum() % 12 def hertz(self): """Returns the hertz value of the Pitch.""" k = self.keynum() return 440.0 * math.pow(2, ((k - 69) / 12)) @classmethod def from_keynum(cls, keynum, acci=None): """ A class method that creates a Pitch for the specified midi key number. Parameters ---------- keynum : int A valid keynum 0-127. acci : string The accidental to use. If no accidental is provided a default is chosen from `C C# D Eb F F# G Ab A Bb B` Returns ------- A new Pitch with an appropriate spelling. Raises ------ A ValueError if the midi key number is invalid or if the pitch requested does not support the specified accidental. """ if not (isinstance(keynum, int) and 0 <= keynum <= 127): raise ValueError(f"Invalid midi key number: {keynum}.") o, i = divmod(keynum, 12) if acci is None: acci = ['', '#', '', 'b', '', '', '#', '', 'b', '', 'b', ''][i] a = cls._accidental_map.get(acci) if a is None: raise ValueError(f"'{acci}' is not a valid accidental.") # s = cls._enharmonic_map[i][a] s = cls._enharmonic_map[i].get(a) if s is None: raise ValueError(f'No pitch for keynum {keynum} and accidental {acci}') if s in ['B#', 'B##']: o -= 1 elif s in ['Cb', 'Cbb']: o += 1 return Pitch(s + cls._octave_names[o]) @classmethod def random(cls): '''Returns a random Pitch spelling.''' while True: # Note: randint's range includes upper bound let = randint(cls._let_C, cls._let_B) acc = randint(cls._acc_2f, cls._acc_2s) oct = randint(cls._oct_00, cls._oct_9) # continue if pitch is below C00 (e.g. Cb00 or Cbb00) if oct == cls._oct_00 and let == cls._let_C and acc < cls._acc_n: continue # continue if pitch is above G9 or Abb9 if oct == cls._oct_9: if let == cls._let_G and acc > cls._acc_n: continue if let == cls._let_A and acc > cls._acc_2f: continue if let == cls._let_B: continue return Pitch([let, acc, oct])
Ancestors
- musx.pitch.PitchBase
- builtins.tuple
Class variables
var pnums
-
A class variable that holds an IntEnum of all possible letter-and-accidental combinations Cff up to Bss. (Since the accidental character # is illegal as a python enum name pnums use the 'safe versions' of the accidental names: 'ff' upto 'ss'.
A pnum value is a one byte integer 'llllaaaa', where 'llll' is its letter index 0-6, and 'aaaa' is its accidental index 0-4. Pnums can be compared using regular math relations.
Static methods
def from_keynum(keynum, acci=None)
-
A class method that creates a Pitch for the specified midi key number.
Parameters
keynum
:int
- A valid keynum 0-127.
acci
:string
- The accidental to use. If no accidental is provided a default is
chosen from
C C# D Eb F F# G Ab A Bb B
Returns
A new Pitch with an appropriate spelling.
Raises
A ValueError if the midi key number is invalid or if the pitch requested does not support the specified accidental.
Expand source code
@classmethod def from_keynum(cls, keynum, acci=None): """ A class method that creates a Pitch for the specified midi key number. Parameters ---------- keynum : int A valid keynum 0-127. acci : string The accidental to use. If no accidental is provided a default is chosen from `C C# D Eb F F# G Ab A Bb B` Returns ------- A new Pitch with an appropriate spelling. Raises ------ A ValueError if the midi key number is invalid or if the pitch requested does not support the specified accidental. """ if not (isinstance(keynum, int) and 0 <= keynum <= 127): raise ValueError(f"Invalid midi key number: {keynum}.") o, i = divmod(keynum, 12) if acci is None: acci = ['', '#', '', 'b', '', '', '#', '', 'b', '', 'b', ''][i] a = cls._accidental_map.get(acci) if a is None: raise ValueError(f"'{acci}' is not a valid accidental.") # s = cls._enharmonic_map[i][a] s = cls._enharmonic_map[i].get(a) if s is None: raise ValueError(f'No pitch for keynum {keynum} and accidental {acci}') if s in ['B#', 'B##']: o -= 1 elif s in ['Cb', 'Cbb']: o += 1 return Pitch(s + cls._octave_names[o])
def random()
-
Returns a random Pitch spelling.
Expand source code
@classmethod def random(cls): '''Returns a random Pitch spelling.''' while True: # Note: randint's range includes upper bound let = randint(cls._let_C, cls._let_B) acc = randint(cls._acc_2f, cls._acc_2s) oct = randint(cls._oct_00, cls._oct_9) # continue if pitch is below C00 (e.g. Cb00 or Cbb00) if oct == cls._oct_00 and let == cls._let_C and acc < cls._acc_n: continue # continue if pitch is above G9 or Abb9 if oct == cls._oct_9: if let == cls._let_G and acc > cls._acc_n: continue if let == cls._let_A and acc > cls._acc_2f: continue if let == cls._let_B: continue return Pitch([let, acc, oct])
Methods
def hertz(self)
-
Returns the hertz value of the Pitch.
Expand source code
def hertz(self): """Returns the hertz value of the Pitch.""" k = self.keynum() return 440.0 * math.pow(2, ((k - 69) / 12))
def is_empty(self)
-
Returns true if the Pitch is empty. A pitch is empty if its letter, accidental and octave attributes are None. Only one of these attributes needs to be checked because new will only create a Pitch if all three are legal values or all three are None.
Expand source code
def is_empty(self): """ Returns true if the Pitch is empty. A pitch is empty if its letter, accidental and octave attributes are None. Only one of these attributes needs to be checked because __new__ will only create a Pitch if all three are legal values or all three are None. """ return self.letter is None
def keynum(self)
-
Returns the midi key number of the Pitch.
Expand source code
def keynum(self): """Returns the midi key number of the Pitch.""" deg = self._letter_spans[self.letter] # convert accidental index into semitone shift, e.g. double flat == -2. acc = self.accidental - 2 return (12 * self.octave) + deg + acc
def pc(self)
-
Returns the pitch class (0-11) of the Pitch.
Expand source code
def pc(self): """Returns the pitch class (0-11) of the Pitch.""" return self.keynum() % 12
def pnum(self)
-
Returns the pnum (pitch class enum) of the Pitch. Pnums enumerate and order the letter and accidental of a Pitch so they can be compared, e.g.: C < C# < Dbb. See also:
pnums
.Expand source code
def pnum(self): """ Returns the pnum (pitch class enum) of the Pitch. Pnums enumerate and order the letter and accidental of a Pitch so they can be compared, e.g.: C < C# < Dbb. See also: `pnums`. """ return self.pnums((self.letter << 4) + self.accidental)
def pos(self)
-
Returns a unique integer representing this pitch's position in the octave-letter-accidental space. The expression to calculate this value is
(octave<<8) + (letter<<4) + accidental
.Expand source code
def pos(self): """ Returns a unique integer representing this pitch's position in the octave-letter-accidental space. The expression to calculate this value is `(octave<<8) + (letter<<4) + accidental`. """ return (self.octave << 8) + (self.letter << 4) + self.accidental
def string(self)
-
Returns a string containing the pitch name including the letter, accidental, and octave. For example, Pitch("C#7").string() would return 'C#7'.
Expand source code
def string(self): """ Returns a string containing the pitch name including the letter, accidental, and octave. For example, Pitch("C#7").string() would return 'C#7'. """ if self.is_empty(): return 'R' s = self._letter_names[self.letter] s += self._accidental_names[self.accidental] s += self._octave_names[self.octave] return s