import copy
from math import log2
from typing import Optional, Union
from musicscore import STANDARD, ENHARMONIC, FLAT, SHARP, FORCEFLAT, FORCESHARP
from musicscore.exceptions import AlreadyFinalizedError
from musicxml.xmlelement.xmlelement import * # 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