Source code for Musica.Theory.Pitch

####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

"""Classes for representing and manipulating pitches, pitch-space, and accidentals.

"""

####################################################################################################

__all__ = [
    'Accidental',
    'Pitch',
    ]

####################################################################################################

import re

from ..Locale.Unicode import to_unicode
from .Temperament import ET12

####################################################################################################

[docs]class Accidental: """Accidental class, representing the symbolic and numerical representation of pitch deviation from a pitch name (e.g. C). """ __modifier_regexp__ = re.compile('[#-]*') # Fixme: ~` __name_to_alteration__ = { 'natural': 0, # 'sharp': 1, 'double-sharp': 2, 'triple-sharp': 3, 'quadruple-sharp': 4, # 'flat': -1, 'double-flat': -2, 'triple-flat': -3, 'quadruple-flat': -4, # 'half-sharp': .5, 'one-and-a-half-sharp': 1.5, 'half-flat': .5, 'one-and-a-half-flat': -1.5, } __name_to_modifier__ = { 'natural': '', # 'sharp': '#', 'double-sharp': '##', 'triple-sharp': '###', 'quadruple-sharp': '####', # 'flat': '-', 'double-flat': '--', 'triple-flat': '---', 'quadruple-flat': '----', # 'half-sharp': '~', 'one-and-a-half-sharp': '#~', 'half-flat': '`', 'one-and-a-half-flat': '-`', } __alteration_to_name__ = {alteration:name for name, alteration in __name_to_alteration__.items()} ##############################################
[docs] @classmethod def parse_accidental(cls, value): """Return an alteration from a :class:`Alteration` instance, an alteration name or a modifier string. """ # Fixme: ~` if isinstance(value, cls): return value.alteration else: try: return cls.__name_to_alteration__[value] except KeyError: value = str(value) if not value: return 0 elif cls.__modifier_regexp__.match(value) is not None: number_of_flat = value.count('-') number_of_sharp = value.count('#') return number_of_sharp - number_of_flat else: raise ValueError("Invalid accidental {}".format(value))
############################################## def __init__(self, accidental_value): # Fixme: don't keep modifer string self.alteration = accidental_value ##############################################
[docs] def clone(self): return self.__class__(self)
############################################## @property def alteration(self): return self._alteration @alteration.setter def alteration(self, value): self._alteration = self.parse_accidental(value) ############################################## @property def is_normal(self): return self._alteration == 0 @property def is_flat(self): return self._alteration < 0 @property def is_sharp(self): return self._alteration > 0 ############################################## @property def name(self): return self.__alteration_to_name__[self._alteration] @property def unicode_name(self): return to_unicode(self.name) @property def modifier(self): return self.__name_to_modifier__[self.name]
[docs] def __str__(self): return self.modifier
##############################################
[docs] def __eq__(self, other): if other is not None: return self._alteration == other.alteration else: return False
####################################################################################################
[docs]class Pitch: """Class to represents a pitch. """ # Fixme: negative octave # use ~ for bemol : C-1 versus C~-1 #! Define an implicit octave so as to be able to define a float value __implicit_octave__ = 4 #! Assume a Twelve-tone equal temperament __temperament__ = ET12 # Fixme: note re __pitch_regexp__ = re.compile('(?P<note>[abcdefg])(?P<accidental>[#-]*)(?P<octave_sign>(/-)?)(?P<octave>\d*)') ##############################################
[docs] @classmethod def parse_pitch(cls, name, return_dict=False): _name = str(name).lower() match = cls.__pitch_regexp__.match(_name) if match is not None: note = match['note'].upper() accidental = match['accidental'] accidental = Accidental(accidental) if accidental else None octave = match['octave'] octave = int(octave) if octave else None if match['octave_sign']: octave = - octave if return_dict: return dict(note=note, accidental=accidental, octave=octave) else: return note, accidental, octave else: raise ValueError("Invalid pitch {}".format(name))
############################################## def __init__(self, name=None, **kwargs): # self._step = 'C', 'D', ... 'G' # self._step_number = 0 2 ... 11 only natural # self._accidental = Accidental() # self._octave = int self._spelling_is_inferred = False # for accidental, e.g. 1 is inferred as C# versus D- if name is not None: # Fixme. type(self) vs self.__class__ if isinstance(name, Pitch): self._init_from_clone(name) else: try: step_number = int(name) except ValueError: step_number = None if step_number is not None: self._init_from_number(step_number, kwargs) else: self._init_from_string(name, kwargs) else: self._init_from_kwargs(kwargs) # Fixme: self.step = ... ? self._step_number = self.__temperament__.name_to_number(self._step) ##############################################
[docs] def _init_from_clone(self, other): self._step = other._step self.accidental = other._accidental self._octave = other._octave
##############################################
[docs] def _init_from_string(self, name, kwargs): step, accidental, octave = self.parse_pitch(name) self._step = step self._accidental = accidental self._octave = octave if octave is None: self._octave = kwargs.get('octave', None)
##############################################
[docs] def _init_from_number(self, step_number, kwargs): try: step = self.__temperament__[step_number] except IndexError: raise ValueError('Invalid pitch number {}'.format(step_number)) if step.is_natural: self._step = step.name self._accidental = None else: self._step = step.prev_natural.name self._accidental = Accidental('#') self._spelling_is_inferred = True self._octave = kwargs.get('octave', None)
##############################################
[docs] def _init_from_kwargs(self, kwargs): if 'midi' in kwargs: pitch_int = kwargs['midi'] step_number, octave = self.__temperament__.fold_step_number(pitch_int, octave=True) octave -= 1 # C4 60 -> 0, 5 self._init_from_number(step_number, dict(octave=octave)) else: self.step = kwargs['step'] accidental = kwargs.get('accidental', None) if accidental is not None: self.accidental = Accidental(accidental) else: self._accidental = None self._octave = kwargs.get('octave', None)
##############################################
[docs] def clone(self): return self.__class__(self)
############################################## def __repr__(self): return '{} {}'.format(self.__class__.__name__, str(self)) ############################################## @property def temperament(self): return self.__temperament__ ############################################## @property def pitch_class(self): """Returns the integer value for the pitch, between 0 and 11, where C=0, C#=1, D=2, ... B=11. """ if self.alteration is None: return self._step_number else: # Fixme: cache ? return self.__temperament__.fold_step_number(self._step_number + int(self.alteration)) @pitch_class.setter def pitch_class(self, value): self._init_from_number(value) # Fixme: reset octave ! ############################################## @property def step_number(self): return self._step_number @property def step(self): """The diatonic name of the note; i.e. it does not give the accidental and octave.""" return self._step @step.setter def step(self, value): _value = value.upper() if self.__temperament__.is_valid_step_name(_value): self._step = _value self._step_number = self.__temperament__.name_to_number(self._step) else: raise ValueError("Invalid step {}".format(value)) @property def spelling_is_inferred(self): return self._spelling_is_inferred @property def degree(self): return self.__temperament__[self._step_number].degree ############################################## @property def accidental(self): return self._accidental @accidental.setter def accidental(self, value): if value is not None: self._accidental = Accidental(value) else: self._accidental = None @property def is_altered(self): return self._accidental is not None @property def alteration(self): if self._accidental is not None: return self._accidental.alteration else: return 0 ############################################## @property def octave(self): return self._octave @octave.setter def octave(self, value): # Fixme: complex accidental can alter octave too ! _value = int(value) if _value > 0: self._octave = value else: raise ValueError("Invalid octave {}".format(value)) @property def implicit_octave(self): return self.__implicit_octave__ if self._octave is None else self._octave ##############################################
[docs] def _locale(self, natural=False): translator = self.__temperament__.translator if natural: note = self._step_number else: note = self.pitch_class return translator(note)
############################################## @property def natural_locale(self): # Return :class:`Musica.Locale.Note.NoteNameTranslation` return self._locale(natural=True) @property def locale(self): # Return :class:`Musica.Locale.Note.NoteNameTranslation` return self._locale() @property def english_locale(self): return self.locale['english'] @property def french_locale(self): return self.locale['français'] ##############################################
[docs] def str(self, locale=None, latin=False, unicode=False, octave=False): if latin: locale = 'français' # or similar if locale is not None: name = self.natural_locale[locale].name # to get step without accidental else: name = self._step if self._accidental is not None: if unicode: accidental = self._accidental.unicode_name else: accidental = str(self._accidental) name += accidental if octave and self._octave is not None: if self._octave < 0: name += '/' # Fixme: ??? name += str(self._octave) return name
############################################## @property def name(self): return self.str() @property def full_name(self): return self.str(octave=True) @property def unicode_name(self): return self.str(unicode=True) @property def latin_unicode_name(self): return self.str(latin=True, unicode=True) @property def unicode_name_with_octave(self): return self.str(unicode=True, octave=True) # Fixme: shorter ??? @property def latin_unicode_name_with_octave(self): return self.str(latin=True, unicode=True, octave=True)
[docs] def __str__(self): return self.full_name
##############################################
[docs] def __eq__(self, other): # By default, __ne__() delegates to __eq__() and inverts the result return (self._step == other.step and self._accidental == other.accidental and self._octave == other.octave)
##############################################
[docs] def _compute_float_value(self, octave, add_microtone=True): value = (octave + 1) * self.__temperament__.number_of_steps value += self._step_number if self._accidental is not None: value += self._accidental.alteration if add_microtone: # if self.microtone is not None: # value += self.microtone.alter return float(value) else: return value
[docs] def __int__(self): return self._compute_float_value(self.implicit_octave, False)
[docs] def __float__(self): return float(self._compute_float_value(self.implicit_octave))
@property def midi_float(self): return float(self) ############################################## @property def midi(self): """Return the closest midi code. The MIDI specification only defines note number 60 as "Middle C" (C4, Do3), and all other notes are relative. Note are encoded by a 7-bit non signed integer, ranging from 0 to 127. Consequently, Midi map note C/-1 to 0, C#0 to 1, ... and G9 to 127. """ return int(round(float(self))) ##############################################
[docs] def __lt__(self, other): return float(self) < float(other)
[docs] def __le__(self, other): return float(self) <= float(other)
[docs] def __gt__(self, other): return float(self) > float(other)
[docs] def __ge__(self, other): return float(self) >= float(other)
############################################## @property def frequency(self): return self.__temperament__.frequency(self.implicit_octave, self.pitch_class) ##############################################
[docs] def is_enharmonic(self, other): # Fixme: _compute_float_value, same __temperament__ octave1 = self.octave is not None octave2 = other.octave is not None if octave1 == octave2: # use __implicit_octave__ if octave is None ps1 = float(self) ps2 = float(other) return ps1 == ps2 and self.alteration != other.alteration else: return False
# elif octave1 and not octave2: # ps1 = float(other) # ps2 = self._compute_float_value(self.octave) # elif not octave1 and octave2: # ps1 = self._compute_float_value(other.octave) # ps2 = float(other) ##############################################
[docs] def get_enharmonic(self): """Returns a new Pitch that is the enharmonic equivalent of this Pitch.""" # Fixme: check alteration = self.alteration if alteration == 0: return self else: temperament = self.__temperament__ step_number = self._step_number + alteration step_number, octave_offset = temperament.fold_step_number(step_number, octave=True) step = temperament[step_number] if step.is_accidental: if alteration < 0: # flat step = step.sharpen_name else: # sharp step = step.flatten_name octave = self._octave + octave_offset return self.__class__(step, octave=octave)
##############################################
[docs] def simplify_accidental(self): # Fixme: enharmonic alteration = self.alteration if alteration: temperament = self.__temperament__ step_number = self._step_number + self.alteration # Fixme: int ?, self.pitch_class ? if temperament.is_valid_step_name(step_number): return self else: step_number, octave_offset = temperament.fold_step_number(step_number, octave=True) octave = self._octave + octave_offset return self.__class__(step_number, octave=octave) else: return self # Fixme: clone ???
##############################################
[docs] def pitch_iterator(self, until=None): return PitchIterator(self, until)
##############################################
[docs] def _prev_next_pitch(self, offset): pitch = self.simplify_accidental() step_number = pitch.pitch_class + offset step_number, octave_offset = self.__temperament__.fold_step_number(step_number, octave=True) octave = pitch._octave + octave_offset return self.__class__(step_number, octave=octave)
##############################################
[docs] def next_pitch(self): return self._prev_next_pitch(1)
##############################################
[docs] def prev_pitch(self): return self._prev_next_pitch(-1)
#################################################################################################### class PitchIterator: ############################################## def __init__(self, start_pitch, stop_pitch=None): self._start = Pitch(start_pitch) if stop_pitch is not None: self._stop = Pitch(stop_pitch) else: self._stop = None ############################################## def iter(self, reverse=False, natural=False, inclusive=True): start = self._start.simplify_accidental() stop = self._stop if stop is not None: stop = stop.simplify_accidental() cls = start.__class__ fold_step_number = start.__temperament__.fold_step_number if reverse: offset = -1 else: offset = 1 step_number = start.pitch_class octave = start.octave while True: pitch = cls(step_number, octave=octave) must_stop = stop is not None and pitch == stop if inclusive and must_stop: return if not (natural and pitch.is_altered): yield pitch if not inclusive and must_stop: return # Fixme: could use Pitch API step_number += offset step_number, octave_offset = fold_step_number(step_number, octave=True) octave += octave_offset ############################################## def __iter__(self): return self.iter() #################################################################################################### class PitchInterval: ############################################## def __init__(self, lower_pitch, upper_pitch=None): if upper_pitch < lower_pitch: raise ValueError('{} < {}'.format(upper_pitch, lower_pitch)) self._lower = Pitch(lower_pitch) if upper_pitch is not None: self._upper = Pitch(upper_pitch) else: self._upper = None ############################################## def clone(self): return self.__class__(self._lower, self._upper) ############################################## @property def lower(self): return self._lower @property def upper(self): return self._upper ############################################## def __iter__(self): return PitchIterator(self._lower, self._upper).iter() # inclusive=True ############################################## def iter(self, reverse=False, natural=False): if reverse: return PitchIterator(self._upper, self._lower).iter(reverse=True, natural=natural) else: return PitchIterator(self._lower, self._upper).iter(natural=natural)