Source code for musif.musicxml.tempo

import re
import xml.etree.ElementTree as ET
from enum import Enum
from typing import Optional

import pandas as pd


[docs] class TempoGroup2(Enum): """ Contains constants refering to grouped tempo on the second level that will be used by the tempo module. """ NA = "NA" SLOW = "Slow" MODERATE = "Moderate" FAST = "Fast"
[docs] def get_time_signature_type(time_signature: str) -> str: """ Given a time signature, it returns the correspondent type. Parameters ---------- time_signature: str Time signature in the form of string separated by '/' to obtain the time signature type. """ if time_signature in [ "1/2", "1/4", "1/8", "1/16", "2/2", "2/4", "2/8", "2/16", "4/4", "C", "4/2", "4/8", "4/16", "8/2", "8/4", "8/8", "8/16", ]: return "simple duple" elif time_signature in ["6/8", "3/8", "12/2", "12/4", "12/8", "12/16"]: return "compound duple" elif time_signature in ["3/2", "3/4", "3/16", "6/2", "6/4", "6/16"]: return "simple triple" elif time_signature in ["9/2", "9/4", "9/8", "9/16"]: return "compound triple" else: return "other"
[docs] def get_tempo_grouped_1(tempo: str) -> str: """ Returns a 1st level of grouping for the tempo markings, removing secondary labels and diminutive endings. Parameters ---------- tempo: str Tempo string in italian to be extract the group from. """ tempo = re.sub("\\W+", " ", tempo) # removes eventual special characters replacements = [ (w, "") for w in [ "molto", "poco", "un poco", "tanto", "un tanto", "assai", "meno", "più", "piuttosto", ] ] if not tempo: return "NA" for r in replacements: tempo = tempo.replace(*r) tempo = ( tempo.replace("Con brio", "brio") .replace("con brio", "brio") .replace("Con spirito", "spiritoso") .replace("con spirito", "spiritoso") ) base_important_words = [ "adagio", "allegro", "andante", "andantino", "largo", "lento", "presto", "vivace", "minueto", ] # if the tempo marking ends in -ietto and or -issimo, it retuns the marking without that ending important_words = ( base_important_words + [w[0:-1] + "etto" for w in base_important_words] + [w[0:-1] + "ietto" for w in base_important_words] + [w[0:-1] + "issimo" for w in base_important_words] + [w[0:-1] + "ssimo" for w in base_important_words] + [w[0:-1] + "hetto" for w in base_important_words] ) last_words = "" for word in tempo.split(" "): if word.lower() in important_words and ( "ma non" not in last_words or " ma " not in last_words or "Tempo di" in last_words ): return word.capitalize() last_words += " " + word ################ SECOND IMPORTANT WORDS ################ relevant_words = [ "amoroso", "affettuoso", "agitato", "arioso", "cantabile", "comodo", "brio", "spiritoso", "espressivo", "fiero", "giusto", "grave", "grazioso", "gustoso", "maestoso", "moderato", "risoluto", "sostenuto", "spiritoso", "tempo", ] words_contained = [] for word in tempo.split(" "): if word.lower() in relevant_words: words_contained.append(word) ## FINAL DECISION (Two exceptions: tempo and con brio) ## if len(words_contained) == 1: if words_contained[0].lower() not in ["tempo", "brio"]: return words_contained[0].capitalize() elif words_contained[0].lower() == "tempo": return "A tempo" else: return "Con brio" elif len(words_contained) > 1: for w in words_contained: if w[0].isupper(): if w.lower() != "tempo": return w # returns the capitalized term elif w.lower() == "tempo": return "A tempo" else: return "Con brio" if words_contained[0] != "tempo": return words_contained[0].capitalize() elif words_contained[0] == "tempo": return "A tempo" # or the first one else: return "Con brio" return "NA"
[docs] def get_tempo_grouped_2(tempo_grouped_1: str) -> str: """ Returns a 2nd level of grouping for the tempo markings. Parameters ---------- tempo_grouped_1: str Tempo string got from get_tempo_grouped_1 """ if tempo_grouped_1 is None or tempo_grouped_1.lower() == TempoGroup2.NA.value: return TempoGroup2.NA.value possible_terminations = ["ino", "etto", "ietto", "ssimo", "issimo", "hetto"] slow_basis = [ "Adagio", "Affettuoso", "Grave", "Sostenuto", "Largo", "Lento", "Sostenuto", ] slow = slow_basis + [w[:-1] + t for w in slow_basis for t in possible_terminations] moderate_basis = [ "Andante", "Arioso", "Comodo", "Cantabile", "Comodo", "Espressivo", "Grazioso", "Gustoso", "Maestoso", "Minueto", "Moderato", "Marcía", "Amoroso", ] moderate = moderate_basis + [ w[:-1] + t for w in moderate_basis for t in possible_terminations ] fast_basis = [ "Agitato", "Allegro", "Con brio", "Spiritoso", "Fiero", "Presto", "Risoluto", "Vivace", ] fast = fast_basis + [w[:-1] + t for w in fast_basis for t in possible_terminations] if tempo_grouped_1 in ["A tempo", "Giusto"]: return TempoGroup2.NA.value elif tempo_grouped_1 in slow: return TempoGroup2.SLOW.value elif tempo_grouped_1 in moderate: return TempoGroup2.MODERATE.value elif tempo_grouped_1 in fast: return TempoGroup2.FAST.value
[docs] def get_number_of_beats(time_signature: str) -> int: """ Returns the number of beats corresponding to a time signature. By default, this returns the numerator, with the exceptions when the numerator is 6, 9, or 12 and when the time signature is C or 3/8. If it is empty, it returns 1. If it is `"NA"`, it returns `pd.NA`. Parameters ---------- time_signature: str Time signature in the form of string separated by '/' to obtain the time signature type. """ # we only take the first time signature in case we receive more than one time_signature = time_signature.split(",")[0] if time_signature == "C": return 4 elif time_signature == "": return 1 elif time_signature == "NA": return pd.NA num, den = time_signature.split("/") if num == "3" and den == "8": return 1 elif num == "6": return 2 elif num == "9": return 3 elif num == "12": return 4 else: return int(num)
# raise ValueError(f"The {time_signature} is not a known time signature")
[docs] def extract_numeric_tempo(file_path: str) -> Optional[int]: """ Finds the numeric tempo in a musixml file by looking at the tempo marking in the xml code. Parameters ---------- file_path: str Path to xml file to get the tempo from. """ try: tree = ET.parse(file_path) except ET.ParseError as e: return "NA" root = tree.getroot() try: tempo = int( root.find("part") .find("measure") .find("direction") .find("sound") .get("tempo") ) except: tempo = "NA" return tempo