"""
Module containing all non-spanner subclasses of the :class:`~pymusicxml.score_components.Direction` type.
"""
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
# 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 typing import Sequence
from xml.etree import ElementTree
from pymusicxml.enums import StaffPlacement
from pymusicxml.score_components import Duration, Direction
[docs]class MetronomeMark(Direction):
"""
Class representing a tempo-specifying metronome mark
:param beat_length: length, in quarters, of the note that takes the beat
:param bpm: beats per minute
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
:param placement: Where to place the direction in relation to the staff ("above" or "below")
:param other_attributes: any other attributes to assign to the metronome mark, e.g. parentheses="yes" or
font_size="5"
"""
def __init__(self, beat_length: float, bpm: float, placement: str | StaffPlacement = "above",
voice: int = 1, staff: int = None, **other_attributes):
super().__init__(placement, voice, staff)
try:
self.beat_unit = Duration.from_written_length(beat_length)
except ValueError:
# fall back to quarter note tempo if the beat length is not expressible as a single notehead
self.beat_unit = Duration.from_written_length(1.0)
bpm /= beat_length
self.bpm = bpm
self.other_attributes = {key.replace("_", "-"): value for key, value in other_attributes.items()}
[docs] def render_direction_type(self) -> Sequence[ElementTree.Element]:
type_el = ElementTree.Element("direction-type")
metronome_el = ElementTree.SubElement(type_el, "metronome", self.other_attributes)
metronome_el.extend(self.beat_unit.render_to_beat_unit_tags())
ElementTree.SubElement(metronome_el, "per-minute").text = str(self.bpm)
return type_el,
[docs]class TextAnnotation(Direction):
"""
Class representing text that is attached to the staff
:param text: the text of the annotation
:param font_size: the font size of the text
:param italic: whether or not the text is italicized
: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
:param kwargs: any extra properties of the musicXML "words" tag aside from font-size and italics can be
passed to kwargs
"""
def __init__(self, text: str, font_size: float = None, italic: bool = False, bold: bool = False,
placement: str | StaffPlacement = "above", voice: int = 1, staff: int = None, **kwargs):
super().__init__(placement, voice, staff)
self.text = text
self.text_properties = kwargs
if font_size is not None:
self.text_properties["font-size"] = font_size
if italic:
self.text_properties["font-style"] = "italic"
if bold:
self.text_properties["font-weight"] = "bold"
[docs] def render_direction_type(self) -> Sequence[ElementTree.Element]:
type_el = ElementTree.Element("direction-type")
ElementTree.SubElement(type_el, "words", self.text_properties).text = self.text
return type_el,
[docs]class Dynamic(Direction):
"""
Class representing a dynamic that is attached to the staff
:param dynamic_text: the text of the dynamic, e.g. "mf"
:param voice: Which voice to attach to
:param staff: Which staff to attach to if the part has multiple staves
"""
STANDARD_TYPES = ("f", "ff", "fff", "ffff", "fffff", "ffffff", "fp", "fz", "mf", "mp", "p", "pp", "ppp", "pppp",
"ppppp", "pppppp", "rf", "rfz", "sf", "sffz", "sfp", "sfpp", "sfz")
def __init__(self, dynamic_text: str, placement: str | StaffPlacement = "below",
voice: int = 1, staff: int = None):
self.dynamic_text = dynamic_text
super().__init__(placement, voice, staff)
[docs] def render_direction_type(self) -> Sequence[ElementTree.Element]:
type_el = ElementTree.Element("direction-type")
dynamics_el = ElementTree.SubElement(type_el, "dynamics")
if self.dynamic_text in Dynamic.STANDARD_TYPES:
ElementTree.SubElement(dynamics_el, self.dynamic_text)
else:
ElementTree.SubElement(dynamics_el, "other-dynamics").text = self.dynamic_text
return type_el,
[docs]class Harmony(Direction):
"""
Class representing harmonic notation.
"""
KINDS = ("augmented", "augmented-seventh", "diminished",
"diminished-seventh", "dominant", "dominant-11th", "dominant-13th",
"dominant-ninth", "French", "German", "half-diminished", "Italian",
"major", "major-11th", "major-13th", "major-minor", "major-ninth",
"major-seventh", "major-sixth", "minor", "minor-11th", "minor-13th",
"minor-ninth", "minor-seventh", "minor-sixth", "Neapolitan", "none",
"other", "pedal", "power", "suspended-fourth", "suspended-second",
"Tristan")
def __init__(self, root_letter: str, root_alter: int, kind: str,
use_symbols: bool = False, degrees: Sequence[Degree] = (),
placement: str | StaffPlacement = "above"):
if kind not in self.KINDS:
raise ValueError(f"Chord {kind} of invalid kind. Allowed values: {self.KINDS}")
self.root_letter = root_letter
self.root_alter = root_alter
self.kind = kind
self.use_symbols = use_symbols
self.degrees = list(degrees)
super().__init__(placement, 1, None)
[docs] def render(self) -> Sequence[ElementTree.Element]:
harmony_el = ElementTree.Element("harmony")
root_el = ElementTree.SubElement(harmony_el, "root")
ElementTree.SubElement(root_el, "root-step").text = str(self.root_letter)
ElementTree.SubElement(root_el, "root-alter").text = str(self.root_alter)
ElementTree.SubElement(harmony_el, "kind").text = str(self.kind)
for d in self.degrees:
harmony_el.extend(d.render())
return harmony_el,
[docs] def render_direction_type(self) -> Sequence[ElementTree.Element]:
return self.render(),
[docs]class Degree:
"""
The <degree> element is used to add, alter, or subtract individual notes in the chord.
:param value: The number of the degree, a positive integer.
:param alter: An integer meaning alteration by semitones.
:param degree_type: Type of alteration. A positive alter + 'subtract' = semitone down.
:param print_object: Whether to print the degree or not.
"""
DEGREE_TYPES = ("add", "alter", "subtract")
def __init__(self, value: int, alter: int, degree_type: str = "alter", print_object: bool = True):
assert degree_type in self.DEGREE_TYPES
self.value = value
self.alter = alter
self.degree_type = degree_type
self.print_object = print_object
[docs] def render(self) -> Sequence[ElementTree.Element]:
degree_element = ElementTree.Element("degree", {"print-object": "yes" if self.print_object else "no"})
ElementTree.SubElement(degree_element, "degree-value").text = str(self.value)
ElementTree.SubElement(degree_element, "degree-alter").text = str(self.alter)
ElementTree.SubElement(degree_element, "degree-type").text = str(self.degree_type)
return degree_element,