Source code for musicscore.note

from typing import Optional

from musicscore.musictree import MusicTree
from musicscore.exceptions import (
    NoteHasNoParentChordError,
    NoteMidiHasNoParentChordError,
)
from musicscore.midi import Midi
from musicscore.quarterduration import QuarterDurationMixin
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")