from fractions import Fraction
from typing import Optional, Union, List
from musicscore.beat import Beat
from musicscore.chord import GraceChord, Chord
from musicscore.exceptions import (
VoiceHasNoBeatsError,
VoiceHasNoParentError,
VoiceIsFullError,
AddChordError,
AlreadyFinalizedError,
)
from musicscore.musictree import MusicTree
from musicscore.finalize import FinalizeMixin
from musicscore.quantize import QuantizeMixin
from musicscore.quarterduration import QuarterDuration
from musicscore.tuplet import SimplifiedSextuplets
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLVoice
__all__ = ["Voice"]
[docs]class Voice(MusicTree, SimplifiedSextuplets, QuantizeMixin, FinalizeMixin, XMLWrapper):
"""
Parent type: :obj:`~musicscore.staff.Staff`
Child type: :obj:`~musicscore.beat.Beat`
"""
_ATTRIBUTES = {"number", "leftover_chord", "is_filled"}
_ATTRIBUTES = _ATTRIBUTES.union(MusicTree._ATTRIBUTES)
_ATTRIBUTES = _ATTRIBUTES.union(QuantizeMixin._ATTRIBUTES)
_ATTRIBUTES = _ATTRIBUTES.union(SimplifiedSextuplets._ATTRIBUTES)
XMLClass = XMLVoice
def __init__(
self,
number=None,
simplified_sextuplets=None,
get_quantized=None,
*args,
**kwargs,
):
super().__init__(
simplified_sextuplets=simplified_sextuplets, get_quantized=get_quantized
)
self._xml_object = self.XMLClass(value_="1", *args, **kwargs)
self._number = None
self.number = number
self._current_beat_index = None
self._leftover_chord = None
self._final_updated = False
def _add_chord(self, chord: "Chord") -> List["Chord"]:
"""
:param chord: :obj:`~musicscore.chord.Chord`, required
:return: added chord or a list of split chords
"""
if not self.get_children():
raise VoiceHasNoBeatsError
try:
current_beat = self.get_children()[self.get_current_beat_index()]
except IndexError:
raise VoiceIsFullError(
f"Voice number {self.value_} of Measure number {self.up.up.number} is full."
)
if isinstance(chord, GraceChord) and chord.position == "after":
return current_beat.add_child(chord)
if current_beat.is_filled:
self._current_beat_index += 1
return self._add_chord(chord)
else:
return current_beat.add_child(chord)
@property
def is_filled(self) -> bool:
"""
:return: ``True`` if voice has :obj:`~musicscore.beat.Beat` children and the last child is filled, else ``False``
"""
if self.get_children():
return self.get_children()[-1].is_filled
else:
return False
@property
def leftover_chord(self) -> Optional["Chord"]:
"""
:return: None or a :obj:`~musicscore.chord.Chord` which is left over after adding a chord to the voice.
"""
return self._leftover_chord
@leftover_chord.setter
def leftover_chord(self, val):
self._leftover_chord = val
@property
def number(self) -> Optional[int]:
"""
:type: ``None`` or ``int``. If ``None`` number is set to 1.
:return: ``positive int`` or ``None``
"""
if self._number is None:
return None
object_value = self.xml_object.value_
if object_value is not None:
return int(object_value)
@number.setter
def number(self, val):
self._number = val
if val is not None:
self.xml_object.value_ = str(val)
else:
self.xml_object.value_ = "1"
[docs] def add_beat(
self,
beat_quarter_duration: Optional[
Union[QuarterDuration, Fraction, int, float]
] = 1,
) -> Beat:
"""
Creates and adds a :obj:`~musicscore.beat.Beat` to voice
:param beat_quarter_duration: if None beat_quarter_duration is set to 1.
:return: :obj:`~musicscore.beat.Beat`
"""
if self._finalized is True:
raise AlreadyFinalizedError(self, "add_beat")
if beat_quarter_duration is None:
beat_quarter_duration = 1
return self.add_child(Beat(beat_quarter_duration))
def add_chord(self, *args, **kwargs):
raise AddChordError()
[docs] def add_child(self, child: Beat) -> Beat:
"""
Check and add child to list of children. Child's parent is set to self.
:param child: :obj:`~musicscore.beat.Beat`
:return: child
:rtype: :obj:`~musicscore.beat.Beat`
"""
if self._finalized is True:
raise AlreadyFinalizedError(self, "add_child")
if not self.up:
raise VoiceHasNoParentError(
"A child Beat can only be added to a Voice if voice has a Staff parent."
)
return super().add_child(child)
[docs] def fill_with_rests(self):
if not self.is_filled:
if not self.get_children():
self.update_beats()
self._add_chord(
Chord(
0,
sum([b.quarter_duration for b in self.get_beats()])
- sum([ch.quarter_duration for ch in self.get_chords()]),
)
)
[docs] def get_current_beat(self) -> "Beat":
"""
:return: First not completely filled child of type :obj:`~musicscore.beat.Beat`
:exception: :obj:`~musicscore.exceptions.VoiceIsFullError` is raised if all beats are already filled.
"""
try:
current_beat = self.get_children()[self.get_current_beat_index()]
except IndexError:
raise VoiceIsFullError()
if current_beat.is_filled:
self._current_beat_index += 1
return self.get_current_beat()
return current_beat
[docs] def get_current_beat_index(self) -> int:
"""
:return: Index of first not completely filled child of type :obj:`~musicscore.beat.Beat`
"""
if not self.get_children():
raise ValueError("Voice has no beats.")
else:
if not self._current_beat_index:
self._current_beat_index = 0
return self._current_beat_index
[docs] def update_beats(self, *quarter_durations) -> Optional[List[Beat]]:
"""
Creates and adds or replaces Beats.
:param quarter_durations: if None and a measure as ancestor exists, this measure's
:obj:`musicscore.time.Time.get_beats_quarter_durations()` method is called.
:return: None if quarter_durations is None and no measures as ancestor exists, else list of created beats.
"""
if not quarter_durations:
if self.up and self.up.up:
quarter_durations = self.up.up.time.get_beats_quarter_durations()
else:
return
else:
if len(quarter_durations) == 1 and hasattr(
quarter_durations[0], "__iter__"
):
quarter_durations = quarter_durations[0]
self.remove_children()
for quarter_duration in quarter_durations:
self.add_child(Beat(quarter_duration))
return self.get_children()