Source code for musif.musicxml.common

from copy import deepcopy
from typing import List, Tuple
import itertools

from music21.interval import Interval
from music21.note import Note
from music21.repeat import RepeatMark
from music21.scale import MajorScale, MinorScale
from music21.stream.base import Measure, Part, Score, Voice
from music21.text import assembleLyrics
from roman import toRoman

from musif.cache import isinstance


[docs] def is_voice(part: Part) -> bool: """ Returns True if the part is a singer part, otherwise returns False Parameters ---------- part : Part Music21 part to check if it's a singer part """ instrument = part.getInstrument(returnDefault=False) if instrument is None or instrument.instrumentSound is None: return False return "voice" in instrument.instrumentSound
[docs] def name_parts(score: Score): """ This function assign a name to each part in the score. If a name is already present, we keep it there, otherwise, we create a name of type `missingName#` where # is an incremental number, """ i = 0 for part in score.parts: increment = False if part.partName is None: part.partName = f"NoName{i}" increment = True if part.partAbbreviation is None: part.partAbbreviation = f"NoName{i}" increment = True if increment: i += 1
[docs] def split_layers(score: Score, split_keywords: List[str]): """ Function used to split possible layers. Those instruments that include to parts in the same staff will be separated in two diferent parts so they can be treated separately. Parameters ---------- score : Score Music21 score to scan and separate parts in it according to split_keywords list split_keywords: List[str] List containing key words of instruments susceptible to be splitted. i.e. [oboe, viola] """ final_parts = [] for part_index, part in enumerate(score.parts): instrument = part.getInstrument(returnDefault=False) possible_layers = False if instrument is not None and instrument.instrumentSound is not None: for keyword in split_keywords: if keyword in instrument.instrumentSound: possible_layers = True break if possible_layers: has_voices = False for measure in part.elements: if isinstance(measure, Measure) and any( isinstance(e, Voice) for e in measure.elements ): has_voices = True break if ( has_voices ): # recorrer los compases buscando las voices y separamos en dos parts _separate_info_in_two_parts(score, final_parts, part) else: final_parts.append(part) # without any change score.remove(part) else: final_parts.append(part) # without any change score.remove(part) for idx, part in enumerate(final_parts): try: score.insert(0, part) except Exception: pass
[docs] def get_notes_and_measures( part: Part, ) -> Tuple[List[Note], List[Note], List[Measure], List[Measure]]: """ Obtains lists of notes, tied notes, measures, measures that containg notes, and notes and rests. Information that is useful in the subsequent process of extraction. Parameters ---------- part : Part Music21 part to extract the info from. """ measures = list(part.getElementsByClass(Measure)) sounding_measures = [ idx for idx, measure in enumerate(measures) if len(measure.notes) > 0 ] original_notes = [ note for measure in measures for note in measure.notes if isinstance(note, Note) ] notes_and_rests = [n for measure in measures for n in measure.notesAndRests] return original_notes, measures, sounding_measures, notes_and_rests
def _separate_info_in_two_parts(score, final_parts, part): parts_splitted = part.voicesToParts().elements num_measure = 0 for measure in part.elements: # add missing information to both parts (dynamics, text annotations, etc are # missing) if isinstance(measure, Measure): num_measure += 1 if any(not isinstance(e, Voice) for e in measure.elements): not_voices_elements = [ e for e in measure.elements if not isinstance(e, Voice) ] # elements such as clefs, dynamics, text annotations... for p in parts_splitted: if measure.measureNumber == 0 and isinstance(measure, Measure): # number = measure.measureNumber+1 # only add elements if we are in am measure if isinstance(p.elements[num_measure], Measure): p.elements[num_measure].elements += tuple( deepcopy(e) for e in not_voices_elements if e not in p.elements[num_measure].elements ) if measure.measureNumber > 0: if not isinstance(p.elements[num_measure], Measure): continue p.elements[num_measure].elements += tuple( deepcopy(e) for e in not_voices_elements if e not in p.elements[num_measure].elements ) for num, p in enumerate(parts_splitted, 1): p.id = part.id + " " + toRoman(num) # only I or II p.partName = part.partName + " " + toRoman(num) # only I or II # p.elements = p.elements final_parts.append(p) score.remove(part) def _get_part_clef(part): """ Extracts the clef in the score by checking the first measure of the part Parameters ---------- part : Part Music21 part to extract the info from """ for elem in part.elements: if isinstance(elem, Measure): if hasattr(elem, "clef"): return elem.clef.sign + "-" + str(elem.clef.line) return "" def _get_degrees_and_accidentals(key: str, notes: List[Note]) -> List[Tuple[str, str]]: if "major" in key.split(): scl = MajorScale(key.split(" ")[0]) else: scl = MinorScale(key.split(" ")[0]) degrees = [ scl.getScaleDegreeAndAccidentalFromPitch(note.pitches[0]) for note in notes ] return [(degree[0], degree[1].fullName if degree[1] else "") for degree in degrees] def _get_intervals(notes: List[Note]) -> List[Interval]: return [ Interval(notes[i].pitches[0], notes[i + 1].pitches[0]) for i in range(len(notes) - 1) ] def _contains_text(part: Part) -> bool: return assembleLyrics(part) def _get_lyrics_in_notes(notes: List[Note]) -> List[str]: lyrics = [] for note in notes: if note.lyrics is None or len(note.lyrics) == 0: continue note_lyrics = [ syllable.text for syllable in note.lyrics if syllable.text is not None ] lyrics.extend(note_lyrics) return lyrics
[docs] def fix_repeats(score: Score): """Fix the repeat sign in the score by ensuring that all the parts have the same signs""" signs = score.recurse().getElementsByClass("RepeatMark") # measure_parts is an iterator, but successive loops won't restart from index # 0 but from where the previous one was left # for this, we use itertools.chain to force the creation of a proper # iterator measure_parts = [ itertools.chain(p.getElementsByClass("Measure")) for p in score.parts ] for sign in signs: measure_sign = sign.activeSite measure_sign_offset = measure_sign.offset offset_sign = sign.offset for measures in measure_parts: for m in measures: if m.offset == measure_sign_offset: marks = m.getElementsByClass("RepeatMark") if sign.__class__ not in [mark.__class__ for mark in marks]: m.insert(offset_sign, sign) break