musicdiff

View Source
# ------------------------------------------------------------------------------
# Purpose:       musicdiff is a package for comparing music scores using music21.
#
# Authors:       Greg Chapman <gregc@mac.com>
#                musicdiff is derived from:
#                   https://github.com/fosfrancesco/music-score-diff.git
#                   by Francesco Foscarin <foscarin.francesco@gmail.com>
#
# Copyright:     (c) 2022 Francesco Foscarin, Greg Chapman
# License:       MIT, see LICENSE
# ------------------------------------------------------------------------------

__docformat__ = "google"

import sys
import os
from typing import Union, List, Tuple
from pathlib import Path

import music21 as m21

from musicdiff.m21utils import M21Utils
from musicdiff.m21utils import DetailLevel
from musicdiff.annotation import AnnScore
from musicdiff.comparison import Comparison
from musicdiff.visualization import Visualization

def _getInputExtensionsList() -> [str]:
    c = m21.converter.Converter()
    inList = c.subconvertersList('input')
    result = []
    for subc in inList:
        for inputExt in subc.registerInputExtensions:
            result.append('.' + inputExt)
    return result

def _printSupportedInputFormats():
    c = m21.converter.Converter()
    inList = c.subconvertersList('input')
    print("Supported input formats are:", file=sys.stderr)
    for subc in inList:
        if subc.registerInputExtensions:
            print('\tformats   : ' + ', '.join(subc.registerFormats)
                    + '\textensions: ' + ', '.join(subc.registerInputExtensions), file=sys.stderr)

def diff(score1: Union[str, Path, m21.stream.Score],
         score2: Union[str, Path, m21.stream.Score],
         out_path1:  Union[str, Path] = None,
         out_path2:  Union[str, Path] = None,
         force_parse: bool = True,
         visualize_diffs: bool = True,
         detail: DetailLevel = DetailLevel.Default
        ) -> int:
    '''
    Compare two musical scores and optionally save/display the differences as two marked-up
    rendered PDFs.

    Args:
        score1 (str, Path, music21.stream.Score): The first music score to compare. The score
            can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,
            etc), or a music21 Score object.
        score2 (str, Path, music21.stream.Score): The second musical score to compare. The score
            can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,
            etc), or a music21 Score object.
        out_path1 (str, Path): Where to save the first marked-up rendered score PDF.
            If out_path1 is None, both PDFs will be displayed in the default PDF viewer.
            (default is None)
        out_path2 (str, Path): Where to save the second marked-up rendered score PDF.
            If out_path2 is None, both PDFs will be displayed in the default PDF viewer.
            (default is None)
        force_parse (bool): Whether or not to force music21 to re-parse a file it has parsed
            previously.
            (default is True)
        visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False,
            the only result of the call will be the return value (the number of differences).
            (default is True)
        detail (DetailLevel): What level of detail to use during the diff.  Can be
            GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
            currently equivalent to AllObjects).

    Returns:
        int: The number of differences found (0 means the scores were identical, None means the diff failed)
    '''
    badArg1: bool = False
    badArg2: bool = False

    # Convert input strings to Paths
    if isinstance(score1, str):
        try:
            score1 = Path(score1)
        except:
            print(f'score1 ({score1}) is not a valid path.', file=sys.stderr)
            badArg1 = True

    if isinstance(score2, str):
        try:
            score2 = Path(score2)
        except:
            print(f'score2 ({score2}) is not a valid path.', file=sys.stderr)
            badArg2 = True

    if badArg1 or badArg2:
        return None

    if isinstance(score1, Path):
        fileName1 = score1.name
        fileExt1 = score1.suffix

        if fileExt1 not in _getInputExtensionsList():
            print(f'score1 file extension ({fileExt1}) not supported by music21.', file=sys.stderr)
            badArg1 = True

        if not badArg1:
            # pylint: disable=broad-except
            try:
                score1 = m21.converter.parse(score1, forceSource = force_parse)
            except Exception as e:
                print(f'score1 ({fileName1}) could not be parsed by music21', file=sys.stderr)
                print(e, file=sys.stderr)
                badArg1 = True
            # pylint: enable=broad-except

    if isinstance(score2, Path):
        fileName2: str = score2.name
        fileExt2: str = score2.suffix

        if fileExt2 not in _getInputExtensionsList():
            print(f'score2 file extension ({fileExt2}) not supported by music21.', file=sys.stderr)
            badArg2 = True

        if not badArg2:
            # pylint: disable=broad-except
            try:
                score2 = m21.converter.parse(score2, forceSource = force_parse)
            except Exception as e:
                print(f'score2 ({fileName2}) could not be parsed by music21', file=sys.stderr)
                print(e, file=sys.stderr)
                badArg2 = True
            # pylint: enable=broad-except

    if badArg1 or badArg2:
        return None

    # scan each score, producing an annotated wrapper
    annotated_score1: AnnScore = AnnScore(score1, detail)
    annotated_score2: AnnScore = AnnScore(score2, detail)

    diff_list: List = None
    _cost: int = None
    diff_list, _cost = Comparison.annotated_scores_diff(annotated_score1, annotated_score2)

    numDiffs: int = len(diff_list)
    if visualize_diffs and numDiffs != 0:
        # you can change these three colors as you like...
        #Visualization.INSERTED_COLOR = 'red'
        #Visualization.DELETED_COLOR = 'red'
        #Visualization.CHANGED_COLOR = 'red'

        # color changed/deleted/inserted notes, add descriptive text for each change, etc
        Visualization.mark_diffs(score1, score2, diff_list)

        # ask music21 to display the scores as PDFs.  Composer's name will be prepended with
        # 'score1 ' and 'score2 ', respectively, so you can see which is which.
        Visualization.show_diffs(score1, score2, out_path1, out_path2)

    return numDiffs
#   def diff( score1: Union[str, pathlib.Path, music21.stream.base.Score], score2: Union[str, pathlib.Path, music21.stream.base.Score], out_path1: Union[str, pathlib.Path] = None, out_path2: Union[str, pathlib.Path] = None, force_parse: bool = True, visualize_diffs: bool = True, detail: musicdiff.m21utils.DetailLevel = <DetailLevel.AllObjects: 2> ) -> int:
View Source
def diff(score1: Union[str, Path, m21.stream.Score],
         score2: Union[str, Path, m21.stream.Score],
         out_path1:  Union[str, Path] = None,
         out_path2:  Union[str, Path] = None,
         force_parse: bool = True,
         visualize_diffs: bool = True,
         detail: DetailLevel = DetailLevel.Default
        ) -> int:
    '''
    Compare two musical scores and optionally save/display the differences as two marked-up
    rendered PDFs.

    Args:
        score1 (str, Path, music21.stream.Score): The first music score to compare. The score
            can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,
            etc), or a music21 Score object.
        score2 (str, Path, music21.stream.Score): The second musical score to compare. The score
            can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI,
            etc), or a music21 Score object.
        out_path1 (str, Path): Where to save the first marked-up rendered score PDF.
            If out_path1 is None, both PDFs will be displayed in the default PDF viewer.
            (default is None)
        out_path2 (str, Path): Where to save the second marked-up rendered score PDF.
            If out_path2 is None, both PDFs will be displayed in the default PDF viewer.
            (default is None)
        force_parse (bool): Whether or not to force music21 to re-parse a file it has parsed
            previously.
            (default is True)
        visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False,
            the only result of the call will be the return value (the number of differences).
            (default is True)
        detail (DetailLevel): What level of detail to use during the diff.  Can be
            GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is
            currently equivalent to AllObjects).

    Returns:
        int: The number of differences found (0 means the scores were identical, None means the diff failed)
    '''
    badArg1: bool = False
    badArg2: bool = False

    # Convert input strings to Paths
    if isinstance(score1, str):
        try:
            score1 = Path(score1)
        except:
            print(f'score1 ({score1}) is not a valid path.', file=sys.stderr)
            badArg1 = True

    if isinstance(score2, str):
        try:
            score2 = Path(score2)
        except:
            print(f'score2 ({score2}) is not a valid path.', file=sys.stderr)
            badArg2 = True

    if badArg1 or badArg2:
        return None

    if isinstance(score1, Path):
        fileName1 = score1.name
        fileExt1 = score1.suffix

        if fileExt1 not in _getInputExtensionsList():
            print(f'score1 file extension ({fileExt1}) not supported by music21.', file=sys.stderr)
            badArg1 = True

        if not badArg1:
            # pylint: disable=broad-except
            try:
                score1 = m21.converter.parse(score1, forceSource = force_parse)
            except Exception as e:
                print(f'score1 ({fileName1}) could not be parsed by music21', file=sys.stderr)
                print(e, file=sys.stderr)
                badArg1 = True
            # pylint: enable=broad-except

    if isinstance(score2, Path):
        fileName2: str = score2.name
        fileExt2: str = score2.suffix

        if fileExt2 not in _getInputExtensionsList():
            print(f'score2 file extension ({fileExt2}) not supported by music21.', file=sys.stderr)
            badArg2 = True

        if not badArg2:
            # pylint: disable=broad-except
            try:
                score2 = m21.converter.parse(score2, forceSource = force_parse)
            except Exception as e:
                print(f'score2 ({fileName2}) could not be parsed by music21', file=sys.stderr)
                print(e, file=sys.stderr)
                badArg2 = True
            # pylint: enable=broad-except

    if badArg1 or badArg2:
        return None

    # scan each score, producing an annotated wrapper
    annotated_score1: AnnScore = AnnScore(score1, detail)
    annotated_score2: AnnScore = AnnScore(score2, detail)

    diff_list: List = None
    _cost: int = None
    diff_list, _cost = Comparison.annotated_scores_diff(annotated_score1, annotated_score2)

    numDiffs: int = len(diff_list)
    if visualize_diffs and numDiffs != 0:
        # you can change these three colors as you like...
        #Visualization.INSERTED_COLOR = 'red'
        #Visualization.DELETED_COLOR = 'red'
        #Visualization.CHANGED_COLOR = 'red'

        # color changed/deleted/inserted notes, add descriptive text for each change, etc
        Visualization.mark_diffs(score1, score2, diff_list)

        # ask music21 to display the scores as PDFs.  Composer's name will be prepended with
        # 'score1 ' and 'score2 ', respectively, so you can see which is which.
        Visualization.show_diffs(score1, score2, out_path1, out_path2)

    return numDiffs

Compare two musical scores and optionally save/display the differences as two marked-up rendered PDFs.

Args
  • score1 (str, Path, music21.stream.Score): The first music score to compare. The score can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI, etc), or a music21 Score object.
  • score2 (str, Path, music21.stream.Score): The second musical score to compare. The score can be a file of any format readable by music21 (e.g. MusicXML, MEI, Humdrum, MIDI, etc), or a music21 Score object.
  • out_path1 (str, Path): Where to save the first marked-up rendered score PDF. If out_path1 is None, both PDFs will be displayed in the default PDF viewer. (default is None)
  • out_path2 (str, Path): Where to save the second marked-up rendered score PDF. If out_path2 is None, both PDFs will be displayed in the default PDF viewer. (default is None)
  • force_parse (bool): Whether or not to force music21 to re-parse a file it has parsed previously. (default is True)
  • visualize_diffs (bool): Whether or not to render diffs as marked up PDFs. If False, the only result of the call will be the return value (the number of differences). (default is True)
  • detail (DetailLevel): What level of detail to use during the diff. Can be GeneralNotesOnly, AllObjects, AllObjectsWithStyle or Default (Default is currently equivalent to AllObjects).
Returns

int: The number of differences found (0 means the scores were identical, None means the diff failed)