Source code for musicscore.midi

import copy
from math import log2
from typing import Optional, Union, List

from musicscore import STANDARD, ENHARMONIC, FLAT, SHARP, FORCEFLAT, FORCESHARP
from musicscore.exceptions import AlreadyFinalizedError
from musicxml.xmlelement.xmlelement import *  # type: ignore
from musicxml.xsd.xsdsimpletype import XSDSimpleTypeNoteheadValue  # type: ignore

from musicscore.accidental import Accidental
from musicscore.musictree import MusicTree

__all__ = ['Midi', 'MidiNote', 'C', 'D', 'E', 'F', 'G', 'A', 'B', 'midi_to_frequency', 'frequency_to_midi',
           'get_accidental_mode']


[docs]def midi_to_frequency(midi, a4=440): try: midi = midi.value except AttributeError: pass f = 2 ** ((midi - 69) / 12) * a4 return f
[docs]def frequency_to_midi(frequency, a4=440): m = 69 + 12 * log2(frequency / a4) return m
[docs]def get_accidental_mode(midi_value: Union[float, int], accidental_sign: Optional[str] = None) -> str: """ :param midi_value: a valid midi value in half steps (int or float) :param accidental_sign: ``double-flat``, ``flat-flat``, ``bb``, ``ff`` – ``three-quarters-flat`` – ``flat``, ``b``, ``f`` – ``quarter-flat`` – ``None``, ``natural`` – ``quarter-sharp`` – ``sharp``, ``#``, ``s`` – ``three-quarters-sharp`` – ``double-sharp``, ``sharp-sharp``, ``x``, ``##``, ``ss`` :return: accidental_mode: ``standard``, ``enharmonic``, ``flat``, ``sharp``, ``force-flat``, ``force-sharp`` """ # midi value is valid Midi(midi_value) if accidental_sign in [None, 'natural']: accidental_value = 0 elif accidental_sign in ['quarter-sharp']: accidental_value = 0.5 elif accidental_sign in ['sharp', '#', 's']: accidental_value = 1 elif accidental_sign in ['three-quarters-sharp']: accidental_value = 1.5 elif accidental_sign in ['double-sharp', 'sharp-sharp', 'x', '##', 'ss']: accidental_value = 2 elif accidental_sign in ['quarter-flat']: accidental_value = -0.5 elif accidental_sign in ['flat', 'b', 'f']: accidental_value = -1 elif accidental_sign in ['three-quarters-flat']: accidental_value = -1.5 elif accidental_sign in ['double-flat', 'flat-flat', 'bb', 'ff']: accidental_value = -2 else: raise NotImplementedError(accidental_sign) if STANDARD[midi_value % 12][1] == accidental_value: return 'standard' elif ENHARMONIC[midi_value % 12][1] == accidental_value: return 'enharmonic' elif FLAT[midi_value % 12][1] == accidental_value: return 'flat' elif SHARP[midi_value % 12][1] == accidental_value: return 'sharp' elif FORCEFLAT[midi_value % 12][1] == accidental_value: return 'force-flat' elif FORCESHARP[midi_value % 12][1] == accidental_value: return 'force-sharp'
[docs]class Midi(MusicTree): """ Parent type: :obj:`~musicscore.note.Note` Child type: :obj:`~musicscore.accidental.Accidental` Midi is the representation of a Pitch with its midi value, and accidental sign. This object is used to create a Chord consisting of one or more pitches. The midi representation of a rest is a Midi object with value 0. """ def __init__(self, value: Union[float, int], accidental: Optional[Accidental] = None, *args, **kwargs): super().__init__(*args, **kwargs) self._value = None self._accidental = None self._notehead = None self._pitch_or_rest = None self._parent_chord = None self._parent_note = None self._ties = set() self._staff_number = None self.value = value self.accidental = accidental def _set_parent_chord(self, value): if value is not None and 'Chord' not in [cls.__name__ for cls in value.__class__.__mro__]: raise TypeError self._parent_chord = value # self._parent = value def _update_parent_note(self): if self.parent_note: self.parent_note._update_xml_pitch_or_rest() self.parent_note._update_xml_accidental() def _update_pitch_parameters(self): pitch = self.get_pitch_or_rest() if isinstance(pitch, XMLPitch): if not self.accidental.get_pitch_parameters()[1]: if pitch.xml_alter: pitch.remove(pitch.xml_alter) pitch.xml_step, pitch.xml_octave = self.accidental.get_pitch_parameters()[0], \ self.accidental.get_pitch_parameters()[2] else: pitch.xml_step, pitch.xml_alter, pitch.xml_octave = self.accidental.get_pitch_parameters() else: raise TypeError def _update_pitch_or_rest(self): if self._pitch_or_rest is None: if self.value == 0: self._pitch_or_rest = XMLRest() else: self._pitch_or_rest = XMLPitch() else: if self.value == 0: if not isinstance(self._pitch_or_rest, XMLRest): self._pitch_or_rest = XMLRest() self._update_parent_note() else: pass else: if not isinstance(self._pitch_or_rest, XMLPitch): self._pitch_or_rest = XMLPitch() self._update_parent_note() if self.accidental: self.accidental._update_xml_object() self._update_pitch_parameters() if self.up: self.up._update_xml_pitch_or_rest() # //public properties @property def accidental(self) -> "Accidental": """ Set or get :obj:`~musicscore.accidental.Accidental` object associated with this midi. If it is set to ``None`` an :obj:`~musicscore.accidental.Accidental` object will be created. """ return self._accidental @accidental.setter def accidental(self, value): if not value: value = Accidental() elif not isinstance(value, Accidental): raise TypeError(f'accidental.value must be of type Accidental not {type(value)}') self._accidental = value if value: self._accidental.parent_midi = self @property def is_tied_to_next(self) -> bool: """ :return: ``True`` if this midi adds a :obj:`~musicxml.xmlelement.xmlelement.XMLTie` object of type ``start`` :obj:`~musicscore.note.Note`. """ if 'start' in self._ties: return True else: return False @property def is_tied_to_previous(self): """ :return: ``True`` if this midi adds a :obj:`~musicxml.xmlelement.xmlelement.XMLTie` object of type ``stop`` :obj:`~musicscore.note.Note`. """ if 'stop' in self._ties: return True else: return False @property def notehead(self) -> Optional["XMLNotehead"]: """ Set or get notedhead property. This can be ``None`` or an :obj:`~musicxml.xmlelement.xmlelement.XMLNotehead` object. It is possible to set this property with a valid notehead value of type str. For permitted values see :obj:`~musicxml.xmlelement.xmlelement.XMLNotehead`. """ return self._notehead @notehead.setter def notehead(self, val): if self.parent_note: raise AlreadyFinalizedError('Cannot change notehead after finalizing.') if val is None: self._notehead = None elif not isinstance(val, XMLNotehead): self._notehead = XMLNotehead(val) else: self._notehead = val @property def octave(self) -> int: """ :return: octave number of this midi. """ return int(self.value / 12) - 1 @property def parent_chord(self) -> "Chord": """ Set or get parent :obj:`~musicscore.chord.Chord` object. """ return self._parent_chord @property def parent_note(self) -> "Note": """ Set or get parent :obj:`~musicscore.note.Note` object. """ return self._parent_note @parent_note.setter def parent_note(self, value): if value is not None and 'Note' not in [cls.__name__ for cls in value.__class__.__mro__]: raise TypeError self._parent_note = value self._parent = value @property def value(self) -> Union[float, int]: """ Set and get value of midi. A valid value must be of type ``float`` or ``int`` and can be between ``12`` and ``127``. """ return self._value @value.setter def value(self, v): if not isinstance(v, float) and not isinstance(v, int): raise TypeError(f'Midi.value must be of type float or int not{type(v)}') if v != 0 and (v < 12 or v > 127): raise ValueError( f'Midi.value {v} can be zero for a rest or must be in a range between 12 and 127 inclusively') self._value = v self._update_pitch_or_rest() @property def name(self) -> str: """ :return: a string like ``C#3`` consisting of ``step``, ``accidental sign`` (or value) and ``octave``. Midi with value 0 returns ``rest`` as its name. """ if self.value == 0: return 'rest' pitch_step = self.accidental.get_pitch_parameters()[1] if not pitch_step: accidental = '' elif pitch_step == -1.5: accidental = 'b-' elif pitch_step == -1: accidental = 'b' elif pitch_step == -0.5: accidental = '-' elif pitch_step == 0.5: accidental = '+' elif pitch_step == 1: accidental = '#' elif pitch_step == 1.5: accidental = '#+' else: accidental = str(pitch_step) return f"{self.accidental.get_pitch_parameters()[0]}{accidental}{self.octave}" # //public methods
[docs] def add_child(self, child: [Accidental]) -> Accidental: """ child's _update method will be called after adding. :param child: :return: child :rtype: :obj:`~musicscore.accidental.Accidental` """ if self.parent_chord and self.parent_chord._finalized is True: raise AlreadyFinalizedError(self, 'add_child') super().add_child(child) child._update() return child
[docs] def add_tie(self, type: str) -> None: """ A :obj:`~musicxml.xmlelement.xmlelement.XMLTie` child of type ``start`` or ``stop`` will be added to :obj:`~parent_note` object :param type: ``start``, ``stop`` :exception: ValueError """ if type not in ['start', 'stop']: raise ValueError self._ties.add(type) if self.parent_note: self.parent_note._update_ties()
[docs] def get_pitch_or_rest(self) -> Union['XMLPitch', 'XMLRest']: """ :return: :obj:`~musicxml.xmlelement.xmlelement.XMLPitch` or :obj:`~musicxml.xmlelement.xmlelement.XMLRest` object associated with this :obj:`~musicscore.midi.Midi`. """ return self._pitch_or_rest
[docs] def get_staff_number(self): """ :return: get manually set staff number (necessary for cross-staff notation) """ return self._staff_number
[docs] def remove_tie(self, type: str) -> None: """ :param type: ``start``, ``stop`` :exception: ValueError """ removed = False try: self._ties.remove(type) removed = True except KeyError: pass if removed and self.parent_note: self.parent_note._update_ties()
[docs] def set_staff_number(self, val): """ Set staff number manually (necessary for cross-staff notation). """ self._staff_number = val
[docs] def transpose(self, val: float) -> 'Midi': """ Adds val to value and returns self """ self.value += val return self
# //operators def __lt__(self, other): # For x < y return self.value < other.value def __le__(self, other): # For x <= y return self.value <= other.value def __gt__(self, other): # For x > y return self.value > other.value def __ge__(self, other): # For x >= y return self.value >= other.value def __copy__(self): copied = self.__class__(value=self.value, accidental=self.accidental) copied._ties = self._ties return copied def __deepcopy__(self, memodict={}): copied_accidental = copy.copy(self.accidental) copied = self.__class__(value=self.value, accidental=copied_accidental) copied._ties = copy.copy(self._ties) return copied def _copy_for_split(self): copied_accidental = copy.copy(self.accidental) copied = self.__class__(value=self.value, accidental=copied_accidental) copied.notehead = self.notehead return copied
[docs]class MidiNote(Midi): """ Parent class of shorthand midi names: C, D, E, F, G, A, B. Example C(4, '#') = C(4, 'sharp') = C(4, sharp) """ _VALUE = 60 def __init__(self, octave: object, accidental_sign: object = None, *args: object, **kwargs: object) -> object: self._accidental_sign = accidental_sign self._octave = octave super().__init__(value=self._get_value(), *args, **kwargs) self._set_accidental_mode() def _set_accidental_mode(self): self.accidental.mode = get_accidental_mode(self.value, self._accidental_sign) def _get_value(self): return self._VALUE + self._get_accidental_value() - (4 - self._octave) * 12 def _get_accidental_value(self): permitted = ['double-flat', 'flat-flat', 'ff', 'bb', 'three-quarters-flat', 'flat', 'f', 'b', 'quarter-flat', 'quarter-sharp', 'sharp', 's', '#', 'three-quarters-sharp', 'double-sharp', 'sharp-sharp', 'ss', '##', 'x'] if self._accidental_sign in ['double-flat', 'flat-flat', 'ff', 'bb']: return -2 elif self._accidental_sign in ['three-quarters-flat']: return -1.5 elif self._accidental_sign in ['flat', 'f', 'b']: return -1 elif self._accidental_sign in ['quarter-flat']: return -0.5 elif self._accidental_sign in [None, 'natural']: return 0 elif self._accidental_sign in ['quarter-sharp']: return 0.5 elif self._accidental_sign in ['sharp', 's', '#']: return 1 elif self._accidental_sign in ['three-quarters-sharp']: return 1.5 elif self._accidental_sign in ['double-sharp', 'sharp-sharp', 'ss', '##', 'x']: return 2 else: raise ValueError(f'accidental_sign value {self._accidental_sign} can be None or must be in {permitted}') def __repr__(self): return f"{self.name} at {id(self)}" def __copy__(self): copied = Midi(value=self.value, accidental=self.accidental) copied._ties = self._ties return copied def __deepcopy__(self, memodict={}): copied_accidental = copy.copy(self.accidental) copied = Midi(value=self.value, accidental=copied_accidental) copied._ties = copy.copy(self._ties) return copied def _copy_for_split(self): copied_accidental = copy.copy(self.accidental) copied = Midi(value=self.value, accidental=copied_accidental) copied.notehead = self.notehead return copied
[docs]class C(MidiNote): _VALUE = 60
[docs]class D(MidiNote): _VALUE = 62
[docs]class E(MidiNote): _VALUE = 64
[docs]class F(MidiNote): _VALUE = 65
[docs]class G(MidiNote): _VALUE = 67
[docs]class A(MidiNote): _VALUE = 69
[docs]class B(MidiNote): _VALUE = 71