Source code for musicscore.score

from typing import Union, Optional

from musicscore import Part, Chord
from musicscore.chord import Rest
from musicscore.exceptions import AlreadyFinalizedError, ScoreMultiMeasureRestError
from musicscore.finalize import FinalizeMixin
from musicscore.layout import Scaling, PageLayout, SystemLayout, StaffLayout
from musicscore.musictree import MusicTree
from musicscore.quantize import QuantizeMixin
from musicscore.quarterduration import QuarterDuration
from musicscore.xmlwrapper import XMLWrapper
from musicxml.xmlelement.xmlelement import XMLScorePartwise, XMLPartList, XMLCredit, XMLCreditWords, XMLIdentification, \
    XMLEncoding, \
    XMLSupports, XMLScorePart, XMLPartGroup, XMLGroupSymbol, XMLGroupBarline, XMLGroupName, XMLGroupAbbreviation, \
    XMLMeasureStyle

__all__ = ['TITLE', 'SUBTITLE', 'POSSIBLE_SUBDIVISIONS', 'Score']
#:
TITLE = {'font_size': 24, 'default_x': {'A4': {'portrait': 616}}, 'default_y': {'A4': {'portrait': 1573}},
         'justify': 'center',
         'valign': 'top'}

#:
SUBTITLE = {'font_size': 18, 'default_x': {'A4': {'portrait': 616}}, 'default_y': {'A4': {'portrait': 1508}},
            'halign': 'center',
            'valign': 'top'}
#:
POSSIBLE_SUBDIVISIONS = {QuarterDuration(1, 4): [2, 3], QuarterDuration(1, 2): [2, 3, 4, 5],
                         QuarterDuration(1): [2, 3, 4, 5, 6, 7, 8]}


[docs]class Score(MusicTree, QuantizeMixin, FinalizeMixin, XMLWrapper): """ Parent type: ``None`` Child type: :obj:`~musicscore.part.Part` """ _ATTRIBUTES = {'version', 'title', 'subtitle', 'scaling', 'page_layout', 'system_layout', 'staff_layout', 'new_system'} _ATTRIBUTES = _ATTRIBUTES.union(MusicTree._ATTRIBUTES) _ATTRIBUTES = _ATTRIBUTES.union(QuantizeMixin._ATTRIBUTES) XMLClass = XMLScorePartwise def __init__(self, version='4.0', title=None, subtitle=None, get_quantized=False, new_system=False, *args, **kwargs): super().__init__(get_quantized=get_quantized) self._xml_object = self.XMLClass(*args, **kwargs) self._update_xml_object() self._version = None self._title = None self._subtitle = None self._page_layout = None self._system_layout = None self._staff_layout = None self._scaling = None self._new_system = None self.scaling = Scaling() self.page_layout = PageLayout() self.system_layout = SystemLayout() self.version = version self.title = title self.subtitle = subtitle self.new_system = new_system self._possible_subdivisions = POSSIBLE_SUBDIVISIONS.copy() self._measure_numbers_within_multi_measure_rests = set() self._final_updated = False def _create_missing_measures(self): number_of_measures = max([len(p.get_children()) for p in self.get_children()]) longest_parts = [p for p in self.get_children() if len(p.get_children()) == number_of_measures] for p in set(self.get_children()).difference(set(longest_parts)): for measure_number in range(len(p.get_children()) + 1, number_of_measures + 1): p.add_chord(Rest(longest_parts[0].get_measure(measure_number).quarter_duration)) def _get_title_attributes(self): output = TITLE.copy() output['default_x'] = TITLE['default_x']['A4']['portrait'] output['default_y'] = TITLE['default_y']['A4']['portrait'] return output def _get_subtitle_attributes(self): output = SUBTITLE.copy() output['default_x'] = SUBTITLE['default_x']['A4']['portrait'] output['default_y'] = SUBTITLE['default_y']['A4']['portrait'] return output def _set_last_barline(self): last_measures = [p.get_children()[-1] for p in self.get_children() if p.get_children()] try: if not last_measures[0].get_barline(): for m in last_measures: m.set_barline(style='light-heavy') except IndexError: pass def _set_missing_barlines(self): for measures in zip(*[p.get_children() for p in self.get_children()]): right_barline = None left_barline = None for m in measures: if m.get_barline(location='right'): right_barline = m.get_barline(location='right') break for m in measures: if m.get_barline(location='left'): left_barline = m.get_barline(location='left') break if right_barline: for m in measures: m._barlines['right'] = right_barline if left_barline: for m in measures: m._barlines['left'] = left_barline def _update_xml_object(self): self.xml_object.xml_part_list = XMLPartList() self.xml_object.xml_identification = XMLIdentification() encoding = self.xml_object.xml_identification.xml_encoding = XMLEncoding() encoding.add_child(XMLSupports(element='accidental', type='yes')) encoding.add_child(XMLSupports(element='beam', type='yes')) encoding.add_child(XMLSupports(element='stem', type='yes')) @property def new_system(self) -> bool: """ Set or get new_system property. It will be automatically set to ``True`` if a :obj:`~musicscore.measure.Measure` sets its :obj:`musicscore.measure.Measure.new_system` to ``True``. ``<encoding>`` in :obj:`~musicxml.xmlelement.xmlelement.XMLScorePartwise`\'s ``<identification>`` will have a ``<support>`` child with attribute ``new-system`` if :obj:`musicscore.score.Score.new_system` is set to ``True``. """ return self._new_system @new_system.setter def new_system(self, val): if isinstance(val, bool): self._new_system = val else: raise TypeError(f"new_system {val} must be of type bool and not {val.__class__}") encoding = self.xml_object.xml_identification.xml_encoding new_system_supports = [sup for sup in encoding.get_children_of_type(XMLSupports) if sup.attribute == 'new-system'] if self.new_system: if not new_system_supports: self.xml_object.xml_identification.xml_encoding.add_child( XMLSupports(attribute='new-system', element='print', type='yes', value='yes')) else: if new_system_supports: self.xml_object.xml_identification.xml_encoding.remove(new_system_supports[0]) @property def page_layout(self) -> PageLayout: """ Set and get page layout. After setting value, page layout's parent is set to self. :type: :obj:`~musicscore.pagelayout.PageLayout` :return: :obj:`~musicscore.pagelayout.PageLayout` """ return self._page_layout @page_layout.setter def page_layout(self, val): if not isinstance(val, PageLayout): raise TypeError self._page_layout = val self.page_layout.parent = self @property def scaling(self) -> Scaling: """ :type: :obj:`~musicscore.scaling.Scaling` :return: :obj:`~musicscore.scaling.Scaling` """ return self._scaling @scaling.setter def scaling(self, val): if not isinstance(val, Scaling): raise TypeError val.score = self self._scaling = val @property def staff_layout(self): """ Set and get staff layout. After setting value, staff layout's parent is set to self. :type: :obj:`~musicscore.stafflayout.StaffLayout` :return: :obj:`~musicscore.stafflayout.StaffLayout` """ return self._staff_layout @staff_layout.setter def staff_layout(self, val): if not isinstance(val, StaffLayout): raise TypeError self._staff_layout = val self.staff_layout.parent = self @property def subtitle(self): """ - If val is ``None`` and a subtitle object as credit already exists, this object will be removed. - If val is not ``None``: - if a subtitle exists its ``value_`` will be replaced. - else a subtitle as credit will be added. :type: Optional[str] :return: Optional[str] """ return self._subtitle @subtitle.setter def subtitle(self, val): if val is not None: if self._subtitle is None: credit = self.xml_object.add_child(XMLCredit(page=1)) credit.xml_credit_type = 'subtitle' self._subtitle = credit.add_child(XMLCreditWords(value_=val, **self._get_subtitle_attributes())) else: self._subtitle.value_ = val else: if self._subtitle is None: pass else: credit = self._subtitle.up credit.up.remove(credit) self._subtitle = None @property def system_layout(self): """ Set and get system layout. After setting value, system layout's parent is set to self. :type: :obj:`~musicscore.systemlayout.SystemLayout` :return: :obj:`~musicscore.systemlayout.SystemLayout` """ return self._system_layout @system_layout.setter def system_layout(self, val): if not isinstance(val, SystemLayout): raise TypeError self._system_layout = val self.system_layout.parent = self @property def title(self): """ - If val is ``None`` and a title object as credit already exists, this object will be removed. - If val is not ``None``: - if a title exists its ``value_`` will be replaced. - else a title as credit will be added. :type: Optional[str] :return: Optional[str] """ return self._title @title.setter def title(self, val): if val is not None: if self._title is None: credit = self.xml_object.add_child(XMLCredit(page=1)) credit.xml_credit_type = 'title' self._title = credit.add_child(XMLCreditWords(value_=val, **self._get_title_attributes())) else: self._title.value_ = val else: if self._title is None: pass else: credit = self._title.up credit.up.remove(credit) self._title = None @property def version(self) -> str: """ :type: Any :return: xml_object's version :rtype: str """ return self._version @version.setter def version(self, val): self._version = str(val) self.xml_object.version = self.version
[docs] def add_child(self, child: 'Part') -> 'Part': """ - Check and add child to list of children. Child's parent is set to self. - Part's ``xml_object`` is add to score's ``xml_object`` as child - Part's ``score_part.xml_object`` is added to score's ``xml_part_list`` :param child: :obj:`~musicscore.part.Part`, required :return: child :rtype: :obj:`~musicscore.part.Part` """ if self._finalized is True: raise AlreadyFinalizedError(self, 'add_child') super().add_child(child) self.xml_object.add_child(child.xml_object) if not self.xml_part_list: self.xml_part_list.xml_score_part = child.score_part.xml_object else: self.xml_part_list.add_child(child.score_part.xml_object) return child
[docs] def add_part(self, id: str) -> 'Part': """ Creates and adds part :param id: part's id :return: part """ if self._finalized is True: raise AlreadyFinalizedError(self, 'add_part') p = Part(id) return self.add_child(p)
[docs] def export_xml(self, path: 'pathlib.Path') -> None: """ Creates a musicxml file :param path: Output xml file :return: None """ with open(path, '+w') as f: f.write("""<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> """) f.write(self.to_string())
[docs] def finalize(self) -> None: self._create_missing_measures() self._set_missing_barlines() self._set_last_barline() super().finalize() for measure_number in self._measure_numbers_within_multi_measure_rests: for part in self.get_children(): measure = part.get_measure(measure_number) for ch in measure.get_chords(): ch.notes[0].xml_rest.measure = 'yes'
[docs] def group_parts(self, number: Union[int, str], start_part_number: int, end_part_number: int, symbol: str = 'square', name: Optional[str] = None, abbreviation: Optional[str] = None, **kwargs) -> None: """ Adds a XMLPartList child :param number: sets number property of :obj:`~musicxml.xmlelement.xmlelement.XMLPartGroup` :param start_part_number: number of part to start the group :param end_part_number: number of part to end the group :param symbol: symbol property of :obj:`~musicxml.xmlelement.xmlelement.XMLGroupSymbol`:'none', 'brace', 'line', 'bracket', 'square'; default: 'square' :param name: value of :obj:`~musicxml.xmlelement.xmlelement.XMLGroupName` :param abbreviation: value of :obj:`~musicxml.xmlelement.xmlelement.XMLGroupAbbreviation` :param kwargs: kwargs of :obj:`~musicxml.xmlelement.xmlelement.XMLGroupSymbol` :return: None """ parts = self.get_children_of_type(Part) for part_number in [start_part_number, end_part_number]: if not 0 < part_number <= len(parts): raise ValueError(f'Wrong part number {part_number}. Permitted between 1 and {len(parts)}') if not start_part_number < end_part_number: raise ValueError( f'Wrong part numbers. start_part_number {start_part_number} must be smaller than end_part_number {end_part_number}') new_xml_part_list = XMLPartList(xsd_check=False) for child in self.xml_part_list.get_children(): if isinstance(child, XMLScorePart) and child.id == parts[start_part_number - 1].id_.value: pg = new_xml_part_list.add_child(XMLPartGroup(number=str(number), type='start')) pg.add_child(XMLGroupSymbol(symbol, **kwargs)) pg.add_child(XMLGroupBarline('yes')) if name: pg.add_child(XMLGroupName(name)) if abbreviation: pg.add_child(XMLGroupAbbreviation(abbreviation)) new_xml_part_list.add_child(child) if isinstance(child, XMLScorePart) and child.id == parts[end_part_number - 1].id_.value: new_xml_part_list.add_child(XMLPartGroup(number=str(number), type='stop')) self.xml_part_list = new_xml_part_list
[docs] def set_multi_measure_rest(self, first_measure_number: int, last_measure_number: int) -> None: """ Creates a multi measure rest :param first_measure_number: number of measure to start the multi measure rest :param last_measure_number: number of measure to end the multi measure rest :return: None """ if len(self.get_children()) == 0: raise ScoreMultiMeasureRestError(f'score has no parts.') if last_measure_number <= first_measure_number: raise ScoreMultiMeasureRestError( f'last_measure_number {last_measure_number} must be larger than first_measure_number {first_measure_number}') for x in range(first_measure_number, last_measure_number + 1): if x in self._measure_numbers_within_multi_measure_rests: raise ScoreMultiMeasureRestError(f'measure number {x} is already part of a multiple measure reset') for part in self.get_children(): for _ in range(last_measure_number - len(part.get_children())): m = part.add_measure() part.add_chord(Chord(0, m.quarter_duration)) for measure in part.get_children()[first_measure_number - 1:last_measure_number]: for ch in measure.get_chords(): if not ch.is_rest: raise ScoreMultiMeasureRestError( f'Measures contain not rest chords') first_measure = part.get_measure(first_measure_number) if not first_measure.xml_attributes.xml_measure_style: first_measure.xml_attributes.xml_measure_style = XMLMeasureStyle() first_measure.xml_attributes.xml_measure_style.xml_multiple_rest = last_measure_number - first_measure_number + 1 if part == self.get_children()[0]: self._measure_numbers_within_multi_measure_rests.update( {x for x in range(first_measure_number, last_measure_number + 1)})
[docs] def write(self, *args, **kwargs): """ Not implemented. Use Score.export_xml instead! """ raise NotImplementedError('Use Score.export_xml instead!')