from typing import Optional, List
from musicscore.musictree import MusicTree
from musicscore.exceptions import NoteTypeError, NoteHasNoParentChordError, NoteMidiHasNoParentChordError
from musicscore.midi import Midi
from musicscore.quarterduration import QuarterDurationMixin
from musicscore.util import note_types
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLNote, XMLDot, XMLGrace, XMLRest, XMLTie, XMLNotations, XMLTied, XMLBeam
__all__ = ['Note', 'tie', 'untie']
[docs]def tie(*notes):
"""
Tie notes.
"""
if hasattr(notes[0], '__iter__'):
notes = notes[0]
notes[0].start_tie()
if len(notes) > 1:
for note in notes[1:-1]:
note.stop_tie()
note.start_tie()
notes[-1].stop_tie()
[docs]def untie(*notes):
"""
Untie notes.
"""
if hasattr(notes[0], '__iter__'):
notes = notes[0]
notes[0].remove_tie('start')
if len(notes) > 1:
for note in notes[1:-1]:
note.remove_tie('stop')
note.remove_tie('start')
notes[-1].remove_tie('stop')
[docs]class Note(MusicTree, XMLWrapper, QuarterDurationMixin):
"""
Parent type: :obj:`~musicscore.chord.Chord`
Child type: :obj:`~musicscore.midi.Midi`
"""
_ATTRIBUTES = {'midi', 'quarter_duration', 'parent_chord', 'is_tied', 'is_tied_to_previous'}
XMLClass = XMLNote
def __init__(self, midi, quarter_duration=None, *args, **kwargs):
self._midi = None
self._parent_chord = midi.parent_chord
self._xml_object = self.XMLClass(*args, **kwargs)
super().__init__(quarter_duration=quarter_duration)
self.midi = midi
self._parent = self.parent_chord
self._update_xml_notehead()
self._update_xml_voice()
self._update_xml_staff()
self._update_xml_dots()
self._update_xml_time_modification()
self._update_xml_tuplet()
self._update_xml_beams()
@staticmethod
def _check_xml_duration_value(duration):
if int(duration) != duration:
raise ValueError(f'product of quarter_duration and divisions {duration} must be an integer')
if duration < 0:
raise ValueError
def _add_child(self, child: Midi) -> Midi:
"""
Check and add child to list of children. Child's parent is set to self.
:param child: :obj:`~musicscore.midi.Midi`
:return: child
:rtype: :obj:`~musicscore.midi.Midi`
"""
return super().add_child(child)
def _set_xml_tied(self, val):
if not self.xml_notations:
self.xml_notations = XMLNotations()
tied_xml_objects = self.xml_notations.find_children('XMLTied')
tied_xml_types = [t.type for t in tied_xml_objects]
if val in tied_xml_types:
pass
elif val == 'stop' and 'start' in tied_xml_types:
tied_xml_objects[0].type = 'stop'
self.xml_notations.add_child(XMLTied(type='start'))
else:
self.xml_notations.add_child(XMLTied(type=val))
def _update_ties(self):
if 'stop' in self.midi._ties:
self.stop_tie()
else:
self.remove_tie('stop')
if 'start' in self.midi._ties:
self.start_tie()
else:
self.remove_tie('start')
def _update_xml_accidental(self):
self.xml_object.xml_accidental = self.midi.accidental.xml_object
def _update_xml_beams(self):
if self.parent_chord and self.parent_chord.beams is not None:
for number, value in self.parent_chord.beams.items():
if value in ['forward', 'backward']:
value += ' hook'
self.xml_object.add_child(XMLBeam(number=number, value_=value))
def _update_xml_dots(self):
number_of_dots = self.parent_chord.number_of_dots
if number_of_dots is None:
number_of_dots = 0
dots = self.xml_object.find_children('XMLDot')
if number_of_dots > len(dots):
diff = number_of_dots - len(dots)
while diff:
self.xml_object.add_child(XMLDot())
diff -= 1
elif number_of_dots < len(dots):
for dot in dots[number_of_dots:]:
dot.get_parent().remove(dot)
else:
pass
def _update_xml_duration(self):
duration = float(self.quarter_duration) * self.get_parent_measure().get_divisions()
self._check_xml_duration_value(duration)
duration = int(duration)
if duration == 0:
if self.midi and self.midi.value == 0:
raise ValueError('A rest cannot be a grace note.')
self.xml_object.xml_duration = None
if not self.xml_object.xml_grace:
self.xml_object.xml_grace = XMLGrace()
else:
self.xml_object.xml_grace = None
self.xml_object.xml_duration = duration
def _update_xml_pitch_or_rest(self):
if self.midi.value == 0 and self.quarter_duration == 0:
raise ValueError('A rest cannot be a grace note.')
pitch_or_rest = self.midi.get_pitch_or_rest()
if isinstance(pitch_or_rest, XMLRest):
if self.xml_object.xml_pitch:
self.xml_object.xml_pitch = None
self.xml_object.xml_rest = pitch_or_rest
self.xml_object.xml_notehead = None
else:
if self.xml_object.xml_rest:
self.xml_object.xml_rest = None
self.xml_object.xml_pitch = pitch_or_rest
def _update_xml_staff(self):
self.xml_object.xml_staff = self.get_staff_number()
def _update_xml_type(self):
self.xml_type = self.parent_chord.type
def _update_xml_voice(self):
self.xml_object.xml_voice = str(self.get_voice_number())
def _update_xml_notations(self):
"""
If ``self.xml_object.xml_notations`` has children of types :obj:`~musicxml.xmlelement.xmlelement.XMLArticulation` oder
:obj:`~musicxml.xmlelement.xmlelement.XMLTechnical`, :obj:`~musicxml.xmlelement.xmlelement.XMLOrnaments`,
:obj:`~musicxml.xmlelement.xmlelement.XMLDynamics`
which have no children themselves, these will be removed.
``self.xml_object.xml_notations`` will be removed itself if it has no children.
:return: None
"""
if self.xml_object.xml_notations:
if self.xml_object.xml_notations.xml_articulations and not self.xml_object.xml_notations.xml_articulations.get_children():
self.xml_object.xml_notations.remove(self.xml_object.xml_notations.xml_articulations)
if self.xml_object.xml_notations.xml_technical and not self.xml_object.xml_notations.xml_technical.get_children():
self.xml_object.xml_notations.remove(self.xml_object.xml_notations.xml_technical)
if self.xml_object.xml_notations.xml_ornaments and not self.xml_object.xml_notations.xml_ornaments.get_children():
self.xml_object.xml_notations.remove(self.xml_object.xml_notations.xml_ornaments)
if self.xml_object.xml_notations.xml_dynamics and not self.xml_object.xml_notations.xml_dynamics.get_children():
self.xml_object.xml_notations.remove(self.xml_object.xml_notations.xml_dynamics)
if not self.xml_object.xml_notations.get_children():
self.xml_object.remove(self.xml_object.xml_notations)
def _update_xml_notehead(self):
self.xml_object.xml_notehead = self.midi.notehead
def _update_xml_time_modification(self):
if self.parent_chord.tuplet:
self.xml_time_modification = self.parent_chord.tuplet.get_xml_time_modification()
def _update_xml_tuplet(self):
try:
xml_tuplet = self.parent_chord.tuplet.get_xml_tuplet()
if xml_tuplet:
if not self.xml_notations:
self.xml_notations = XMLNotations()
self.xml_notations.xml_tuplet = xml_tuplet
except AttributeError:
pass
@property
def is_tied(self) -> bool:
"""
:return: True if an element :obj:`~musicxml.xmlelement.xmlelement.XMLTie` with type 'start' is under note's xml_object children.
:rtype: bool
"""
type_types = [t.type for t in self.xml_object.find_children('XMLTie')]
if 'start' in type_types:
return True
else:
return False
@property
def is_tied_to_next(self) -> bool:
"""
:return: same as :obj:`~musicscore.musicscore.Note.is_tied`
"""
return self.is_tied
@property
def is_tied_to_previous(self) -> bool:
"""
:return: True if an element :obj:`~musicxml.xmlelement.xmlelement.XMLTie` with type 'stop' is under note's xml_object children.
:rtype: bool
"""
type_types = [t.type for t in self.find_children('XMLTie')]
if 'stop' in type_types:
return True
else:
return False
@property
def midi(self) -> Midi:
"""
:obj:`~musicscore.chord.Chord`.midi property must be a :obj:`~musicscore.midi.Midi` object with a parent :obj:`~musicscore.chord.Chord`. :obj:`~musicscore.midi.Midi` with value `0` means rest.
Setting this property will set :obj:`~musicscore.midi.Midi`\s :obj:`~musicscore.midi.Midi.parent_note` to self.
:return: note's :obj:`~musicscore.midi.Midi`.
"""
return self._midi
@midi.setter
def midi(self, value):
if not isinstance(value, Midi):
raise TypeError('Note.midi property must be of type Midi')
if not value.parent_chord:
raise NoteMidiHasNoParentChordError
self._midi = value
self._midi.parent_note = self
self._update_xml_pitch_or_rest()
self._update_xml_accidental()
@property
def parent_chord(self) -> 'Chord':
"""
:return: notes parent. Same as self.up
"""
return self._parent_chord
@QuarterDurationMixin.quarter_duration.setter
def quarter_duration(self, value):
if value is not None:
if not self.parent_chord:
raise NoteHasNoParentChordError()
self._set_quarter_duration(value)
self._update_xml_duration()
self._update_xml_type()
# self._update_xml_dots()
else:
self.xml_object.xml_duration = None
[docs] def get_parent_chord(self):
"""
returns :obj:`~parent_chord`
"""
return self.parent_chord
[docs] def get_or_create_xml_notations(self) -> 'XMLNotations':
"""
If note's ``xml_object`` has no :obj:`~musicxml.xmlelement.xmlelement.XMLNotations` as child this child will be created.
:return: :obj:`~musicxml.xmlelement.xmlelement.XMLNotations`
"""
if not self.xml_object.xml_notations:
self.xml_object.xml_notations = XMLNotations()
return self.xml_object.xml_notations
[docs] def get_parent_measure(self) -> 'Measure':
"""
:return: parent :obj:`~musicscore.measure.Measure`
"""
return self.parent_chord.get_parent_measure()
[docs] def get_staff_number(self) -> int:
"""
:return: number of parent :obj:`~musicscore.staff.Staff`
"""
midi_staff_number = self.midi.get_staff_number()
if midi_staff_number:
return midi_staff_number
return self.parent_chord.get_staff_number()
[docs] def get_voice_number(self) -> int:
"""
:return: number of parent :obj:`~musicscore.voice.Voice`
"""
return self.get_parent_chord().get_voice_number()
[docs] def remove_tie(self, type: Optional[str] = None) -> None:
"""
:param type: 'start', 'stop', None: if None and note has :obj:`~musicxml.xmlelement.xmlelement.XMLTie` objects with both types
ValueError is raised.
:return: None
"""
ties = self.find_children('XMLTie')
tie_to_be_removed = None
if len(ties) == 0:
pass
elif len(ties) == 1:
if type is None:
tie_to_be_removed = ties[0]
else:
tie_to_be_removed = ties[0] if ties[0].type == type else None
elif len(ties) == 2:
if type is None:
raise ValueError(
'Note has stop and start ties. Specify type=start or type=stop to decide which one should be removed')
else:
tie_to_be_removed = [t for t in ties if t.type == type]
tie_to_be_removed = None if not tie_to_be_removed else tie_to_be_removed[0]
else:
raise NotImplementedError
if tie_to_be_removed:
try:
self.midi._ties.remove(tie_to_be_removed.type)
except KeyError:
pass
tied_to_be_removed = \
[t for t in self.xml_notations.find_children('XMLTied') if t.type == tie_to_be_removed.type][0]
tie_to_be_removed.up.remove(tie_to_be_removed)
xml_notations = tied_to_be_removed.up
xml_notations.remove(tied_to_be_removed)
if not xml_notations.get_children():
xml_notations.up.remove(xml_notations)
[docs] def start_tie(self) -> None:
"""
Start a tie if not already started.
"""
if not self.is_tied:
self.xml_object.add_child(XMLTie(type='start'))
self._set_xml_tied('start')
self.midi._ties.add('start')
[docs] def stop_tie(self) -> None:
"""
Stop a tie if not already stopped.
"""
if self.is_tied_to_previous:
pass
elif self.is_tied:
self.find_children('XMLTie')[0].type = 'stop'
self.xml_object.add_child(XMLTie(type='start'))
self._set_xml_tied('stop')
else:
self.xml_object.add_child(XMLTie(type='stop'))
self._set_xml_tied('stop')
self.midi._ties.add('stop')