"""
Module containing all spanners (i.e. notations that span a time-range in the score, such as slurs,
brackets, hairpins, etc.)
"""
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
# This file is part of SCAMP (Suite for Computer-Assisted Music in Python) #
# Copyright © 2020 Marc Evanstein <marc@marcevanstein.com>. #
# #
# This program is free software: you can redistribute it and/or modify it under the terms of #
# the GNU General Public License as published by the Free Software Foundation, either version #
# 3 of the License, or (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; #
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. #
# See the GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License along with this program. #
# If not, see <http://www.gnu.org/licenses/>. #
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
from __future__ import annotations
from numbers import Real
from typing import Any, Sequence
from xml.etree import ElementTree
from .enums import LineEnd, LineType, AccidentalType, HairpinType, StaffPlacement
from .score_components import StopNumberedSpanner, MidNumberedSpanner, StartNumberedSpanner
from .notations import Notation
from .directions import TextAnnotation
from pymusicxml import Direction
# octave-shift size in scale steps, keyed by how many octaves the line displaces
_OCTAVE_SHIFT_SIZE = {1: 8, 2: 15, 3: 22}
[docs]
class StopBracket(Direction, StopNumberedSpanner):
"""
End of a bracket spanner.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param line_end: Type of hook/arrow at the end of this bracket
:param end_length: Length of the hock at the end of this bracket
:param text: Any text to attach to the end of this bracket
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
def __init__(self, label: Any = 1, line_end: str | LineEnd = None, end_length: Real = None,
text: str | TextAnnotation = None, placement: str | StaffPlacement = "above",
voice: int = 1, staff: int = None):
StopNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.line_end = LineEnd(line_end) if isinstance(line_end, str) else line_end
self.end_length = end_length
self.text = TextAnnotation(text) if isinstance(text, str) else text
if self.line_end is None:
if self.text is None:
# default to a downward hook if there's no text
self.line_end = LineEnd("down")
else:
# and no end cap if there is
self.line_end = LineEnd("none")
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
bracket_dict = {"type": "stop", "number": str(self.label)}
if self.line_end is not None:
bracket_dict["line-end"] = str(self.line_end.value)
if self.end_length is not None:
bracket_dict["end-length"] = str(self.end_length)
ElementTree.SubElement(direction_type_el, "bracket", bracket_dict)
return direction_type_el,
[docs]
def render(self) -> Sequence[ElementTree.Element]:
direction = super().render()[0]
if self.text is not None:
direction.insert(0, self.text.render_direction_type()[0])
return direction,
[docs]
class StartBracket(Direction, StartNumberedSpanner):
"""
Start of a bracket spanner.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param line_end: Type of hook/arrow at the start of this bracket
:param end_length: Length of the hock at the start of this bracket
:param text: Any text to attach to the start of this bracket
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
STOP_TYPE = StopBracket
def __init__(self, label: Any = 1, line_type: str | LineType = "dashed", line_end: str | LineEnd = None,
end_length: Real = None, text: str | TextAnnotation = None,
placement: str | StaffPlacement = "above", voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.line_type = LineType(line_type) if isinstance(line_type, str) else line_type
self.line_end = LineEnd(line_end) if isinstance(line_end, str) else line_end
self.end_length = end_length
self.text = TextAnnotation(text) if isinstance(text, str) else text
if self.line_end is None:
if self.text is None:
# default to a downward hook if there's no text
self.line_end = LineEnd("down")
else:
# and no end cap if there is
self.line_end = LineEnd("none")
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
bracket_dict = {"type": "start", "number": str(self.label)}
if self.line_type is not None:
bracket_dict["line-type"] = str(self.line_type.value)
if self.line_end is not None:
bracket_dict["line-end"] = str(self.line_end.value)
if self.end_length is not None:
bracket_dict["end-length"] = str(self.end_length)
ElementTree.SubElement(direction_type_el, "bracket", bracket_dict)
return direction_type_el,
[docs]
def render(self) -> Sequence[ElementTree.Element]:
direction = super().render()[0]
if self.text is not None:
direction.insert(0, self.text.render_direction_type()[0])
return direction,
[docs]
class StopDashes(Direction, StopNumberedSpanner):
"""
End of a dashed spanner (e.g. used by a dashed "cresc." or "dim." marking)
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param text: Any text to attach to the end of this dashed spanner
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
def __init__(self, label: Any = 1, text: str | TextAnnotation = None,
placement: str | StaffPlacement = "above", voice: int = 1, staff: int = None):
StopNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.text = TextAnnotation(text) if isinstance(text, str) else text
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(direction_type_el, "dashes", {"type": "stop", "number": str(self.label)})
return direction_type_el,
[docs]
def render(self) -> Sequence[ElementTree.Element]:
direction = super().render()[0]
if self.text is not None:
direction.insert(1, self.text.render_direction_type()[0])
return direction,
[docs]
class StartDashes(Direction, StartNumberedSpanner):
"""
Start of a dashed spanner (e.g. used by a dashed "cresc." or "dim." marking)
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param dash_length: Length of the dashes
:param space_length: Length of the space between the dashes
:param text: Any text to attach to the start of this dashed spanner
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
STOP_TYPE = StopDashes
def __init__(self, label: Any = 1, dash_length: Real = None, space_length: Real = None,
text: str | TextAnnotation = None, placement: str | StaffPlacement = "above",
voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.text = TextAnnotation(text) if isinstance(text, str) else text
self.dash_length = dash_length
self.space_length = space_length
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
dash_dict = {"type": "start", "number": str(self.label)}
if self.dash_length is not None:
dash_dict["dash-length"] = str(self.dash_length)
if self.space_length is not None:
dash_dict["space-length"] = str(self.space_length)
ElementTree.SubElement(direction_type_el, "dashes", dash_dict)
return direction_type_el,
[docs]
def render(self) -> Sequence[ElementTree.Element]:
direction = super().render()[0]
if self.text is not None:
direction.insert(0, self.text.render_direction_type()[0])
return direction,
[docs]
class StopOctaveLine(Direction, StopNumberedSpanner):
"""
End of an octave-shift line (e.g. the end of an "8va" or "8vb").
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param octaves: how many octaves the line displaces; should match the associated :class:`StartOctaveLine`.
Only the magnitude matters here (a stop carries no direction): 1 = 8va/8vb, 2 = 15ma/15mb, 3 = 22ma/22mb.
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
def __init__(self, label: Any = 1, octaves: int = 1, placement: str | StaffPlacement = "above",
voice: int = 1, staff: int = None):
StopNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.octaves = octaves
@property
def size(self) -> int:
return _OCTAVE_SHIFT_SIZE[abs(self.octaves)]
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(direction_type_el, "octave-shift",
{"type": "stop", "size": str(self.size), "number": str(self.label)})
return direction_type_el,
[docs]
class StartOctaveLine(Direction, StartNumberedSpanner):
"""
Start of an octave-shift line, i.e. an "8va"/"8vb"/"15ma" bracket with a dashed line and terminal hook.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param octaves: how many octaves, and in which direction, the notes are displaced. Positive means the notes
sound higher than written (8va, drawn above): 1 = 8va, 2 = 15ma, 3 = 22ma. Negative means the notes sound
lower than written (8vb, drawn below). (Note that the resulting musicxml looks backwards because it describes
the modification of the written notes relative to how they would be written with no ottava.)
:param dash_length: Length of the dashes (in tenths). (Note: some renderers, e.g. verovio, ignore this.)
:param space_length: Length of the space between dashes (in tenths).
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
STOP_TYPE = StopOctaveLine
def __init__(self, label: Any = 1, octaves: int = 1, dash_length: Real = None, space_length: Real = None,
placement: str | StaffPlacement = "above", voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.octaves = octaves
self.dash_length = dash_length
self.space_length = space_length
@property
def size(self) -> int:
return _OCTAVE_SHIFT_SIZE[abs(self.octaves)]
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
# In MusicXML, "down" displaces pitches down to notate them, i.e. the 8va-alta drawn above the staff.
octave_dict = {"type": "down" if self.octaves > 0 else "up",
"size": str(self.size), "number": str(self.label)}
if self.dash_length is not None:
octave_dict["dash-length"] = str(self.dash_length)
if self.space_length is not None:
octave_dict["space-length"] = str(self.space_length)
ElementTree.SubElement(direction_type_el, "octave-shift", octave_dict)
return direction_type_el,
[docs]
class StopTrill(Notation, StopNumberedSpanner):
"""
Stops a trill spanner with a wavy line.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param placement: Where to place the direction in relation to the staff ("above" or "below")
"""
def __init__(self, label: Any = 1, placement: StaffPlacement | str = "above"):
self.placement = StaffPlacement(placement) if isinstance(placement, str) else placement
super().__init__(label)
[docs]
def render(self) -> Sequence[ElementTree.Element]:
ornaments_el = ElementTree.Element("ornaments")
ElementTree.SubElement(ornaments_el, "wavy-line",
{"type": "stop", "placement": self.placement.value, "number": str(self.label)})
return ornaments_el,
[docs]
class StartTrill(Notation, StartNumberedSpanner):
"""
Starts a trill spanner with a wavy line.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param accidental: Accidental annotation to go on the trill ("flat-flat", "flat", "natural", "sharp",
or "double-sharp")
"""
STOP_TYPE = StopTrill
def __init__(self, label: Any = 1, placement: StaffPlacement | str = "above",
accidental: AccidentalType | str = None):
self.placement = StaffPlacement(placement) if isinstance(placement, str) else placement
self.accidental = AccidentalType(accidental)if isinstance(accidental, str) else accidental
super().__init__(label)
[docs]
def render(self) -> Sequence[ElementTree.Element]:
ornaments_el = ElementTree.Element("ornaments")
ElementTree.SubElement(ornaments_el, "trill-mark")
if self.accidental is not None:
accidental_mark = ElementTree.SubElement(ornaments_el, "accidental-mark")
accidental_mark.text = self.accidental.value
ElementTree.SubElement(ornaments_el, "wavy-line",
{"type": "start", "placement": self.placement.value, "number": str(self.label)})
return ornaments_el,
[docs]
class StopPedal(Direction, StopNumberedSpanner):
"""
Stops a sustain pedal spanner.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param sign: whether or not to include a "*" sign
:param line: whether or not to use a line in the pedal marking
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
def __init__(self, label: Any = 1, sign: bool = False, line: bool = True,
placement: str | StaffPlacement = "below", voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.sign = sign
self.line = line
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(direction_type_el, "pedal",
{"type": "stop", "number": str(self.label), "sign": ("no", "yes")[self.sign],
"line": ("no", "yes")[self.line]})
return direction_type_el,
[docs]
class ChangePedal(Direction, MidNumberedSpanner):
"""
Pedal change in the middle of a sustain pedal spanner.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param sign: unclear what this means in the case of a change pedal
:param line: whether or not to use a line in the pedal marking
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
def __init__(self, label: Any = 1, sign: bool = True, line: bool = True,
placement: str | StaffPlacement = "below", voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.sign = sign
self.line = line
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(direction_type_el, "pedal",
{"type": "change", "number": str(self.label), "sign": ("no", "yes")[self.sign],
"line": ("no", "yes")[self.line]})
return direction_type_el,
[docs]
class StartPedal(Direction, StartNumberedSpanner):
"""
Start of a sustain pedal spanner.
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
:param sign: whether or not to include a "Ped" sign
:param line: whether or not to use a line in the pedal marking
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
STOP_TYPE = StopPedal
MID_TYPES = (ChangePedal, )
def __init__(self, label: Any = 1, sign: bool = True, line: bool = True,
placement: str | StaffPlacement = "below", voice: int = 1, staff: int = None):
StartNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.sign = sign
self.line = line
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(direction_type_el, "pedal",
{"type": "start", "number": str(self.label), "sign": ("no", "yes")[self.sign],
"line": ("no", "yes")[self.line]})
return direction_type_el,
[docs]
class StopHairpin(Direction, StopNumberedSpanner):
"""
Notation to attach to a note that ends a hairpin
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
"""
def __init__(self, label: Any = 1, spread: Real = None, placement: str | StaffPlacement = "below",
voice: int = 1, staff: int = None):
StopNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.spread = spread
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
wedge_dict = {"type": "stop", "number": str(self.label)}
if self.spread is not None:
wedge_dict["spread"] = str(self.spread)
ElementTree.SubElement(direction_type_el, "wedge", wedge_dict)
return direction_type_el,
[docs]
class StartHairpin(Direction, StartNumberedSpanner):
"""
Notation to attach to a note that starts a hairpin
:param hairpin_type: the type of hairpin ("crescendo" or "diminuendo")
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
"""
STOP_TYPE = StopHairpin
def __init__(self, hairpin_type: str | HairpinType, label: Any = 1, spread: Real = None,
placement: str | StaffPlacement = "below", niente: bool = False, voice: int = 1,
staff: int = None):
StopNumberedSpanner.__init__(self, label)
Direction.__init__(self, placement, voice, staff)
self.hairpin_type = HairpinType(hairpin_type) if isinstance(hairpin_type, str) else hairpin_type
self.spread = spread
self.niente = niente
[docs]
def render_direction_type(self) -> Sequence[ElementTree.Element]:
direction_type_el = ElementTree.Element("direction-type")
wedge_dict = {"type": self.hairpin_type.value, "number": str(self.label)}
if self.niente:
wedge_dict["niente"] = "yes"
if self.spread is not None:
wedge_dict["spread"] = str(self.spread)
ElementTree.SubElement(direction_type_el, "wedge", wedge_dict)
return direction_type_el,
[docs]
class StopSlur(Notation, StopNumberedSpanner):
"""
Notation to attach to a note that ends a slur
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
"""
[docs]
def render(self) -> Sequence[ElementTree.Element]:
return ElementTree.Element("slur", {"type": "stop", "number": str(self.label)}),
[docs]
class StartSlur(Notation, StartNumberedSpanner):
"""
Notation to attach to a note that starts a slur
:param label: See :class:`~pymusicxml.score_components.NumberedSpanner`.
"""
STOP_TYPE = StopSlur
[docs]
def render(self) -> Sequence[ElementTree.Element]:
return ElementTree.Element("slur", {"type": "start", "number": str(self.label)}),