Source code for musif.musicxml.common

from typing import List, Tuple

from music21.interval import Interval
from music21.note import Note
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

MUSICXML_FILE_EXTENSION = "xml"


[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 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 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 part in 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 = [measure for measure in 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( 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( 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) # p_copy = copy.deepcopy(part) # p_copy.id = p_copy.id + " " + toRoman(num) # only I or II # p_copy.partName = p_copy.partName + " " + toRoman(num) # only I or II # p_copy.elements = p.elements # final_parts.append(p_copy) 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]: # TODO: this takes > 6% of the time 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