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.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLVoice
__all__ = ['Voice']
[docs]class Voice(MusicTree, 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)
XMLClass = XMLVoice
def __init__(self, number=None, *args, **kwargs):
super().__init__()
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()