Source code for music21.converter.museScore

# ------------------------------------------------------------------------------
# Name:         converter/museScore.py
# Purpose:      music21 to MuseScore connection
#
# Authors:      Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2013-23 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module contains tools for using MuseScore to do conversions
from music21.  It was formerly mostly in subConverters.
'''
from __future__ import annotations
import os
import pathlib
import typing as t
import unittest

from music21 import common
from music21 import defaults
from music21 import environment
from music21.exceptions21 import SubConverterException

environLocal = environment.Environment('converter.museScore')

if t.TYPE_CHECKING:
    pass

[docs] def runThroughMuseScore( fp, subformats=(), *, dpi: int|None = None, trimEdges: bool = True, leaveMargin: int = 0, **keywords ) -> pathlib.Path: # pragma: no cover ''' Take the output of the conversion process and run it through MuseScore to convert it to a png. * dpi: specifies the dpi of the output file. If None, then the default is used. * trimEdges: if True (default) the image is trimmed to the edges of the music. * leaveMargin: if trimEdges is True, then this number of pixels is left around the trimmed image. ''' museScorePath = environLocal['musescoreDirectPNGPath'] if not museScorePath: raise SubConverterException( 'To create PNG files directly from MusicXML you need to download MuseScore and ' + 'put a link to it in your .music21rc via Environment.') if not museScorePath.exists(): raise SubConverterException( "Cannot find a path to the 'mscore' file at " + f'{museScorePath} -- download MuseScore') if not subformats: subformatExtension = 'png' else: subformatExtension = subformats[0] or 'png' fpOut = fp.with_suffix('.' + subformatExtension) museScoreRun = [str(museScorePath), fp, '-o', str(fpOut)] if trimEdges: # -T 0 = trim to zero pixel margin museScoreRun.extend(['-T', str(leaveMargin)]) if dpi is not None: museScoreRun.extend(['-r', str(dpi)]) prior_qt = os.getenv('QT_QPA_PLATFORM') prior_xdg = os.getenv('XDG_RUNTIME_DIR') if common.runningInNotebook(): if common.getPlatform() == 'nix': # provide defaults to support headless MuseScore in Google Colab # https://github.com/cuthbertLab/music21/issues/260 if prior_qt is None: os.environ['QT_QPA_PLATFORM'] = 'offscreen' if prior_xdg is None: os.environ['XDG_RUNTIME_DIR'] = str(environment.Environment().getRootTempDir()) if dpi is None: museScoreRun.extend(['-r', str(defaults.jupyterImageDpi)]) common.fileTools.runSubprocessCapturingStderr(museScoreRun) if common.runningInNotebook() and common.getPlatform() == 'nix': # Leave environment in original state if prior_qt is None: os.environ.pop('QT_QPA_PLATFORM') if prior_xdg is None: os.environ.pop('XDG_RUNTIME_DIR') if subformatExtension == 'png': return findNumberedPNGPath(fpOut) else: return fpOut
# common.cropImageFromPath(fp)
[docs] def findNumberedPNGPath(inputFp: str|pathlib.Path) -> pathlib.Path: ''' Find the first numbered file path corresponding to the provided unnumbered file path ending in ".png". Raises an exception if no file can be found. Renamed in v7. Returns a pathlib.Path ''' inputFp = str(inputFp) # not pathlib. if not inputFp.endswith('.png'): raise ValueError(f'inputFp must end with ".png"; got {inputFp}') path_without_extension = inputFp[:-1 * len('.png')] for search_extension in ('1', '01', '001', '0001', '00001'): search_path = pathlib.Path(path_without_extension + '-' + search_extension + '.png') if search_path.exists(): return search_path raise IOError( f'No png file for {inputFp} (such as {path_without_extension}-1.png) was found. ' + 'The conversion to png failed' )
[docs] def findLastPNGPath(inputFp: pathlib.Path) -> pathlib.Path: ''' Find the last numbered file path corresponding to the provided NUMBERED file path ending in ".png". Raises an exception if no file can be found. For instance, if there was a file named abc-01.png it might find abc-22.png. ''' inputFpStr = inputFp.name # not pathlib. if not inputFpStr.endswith('.png'): raise ValueError(f'inputFp must end with ".png"; got {inputFp}') dash_location = inputFpStr.rfind('-') if dash_location == -1: raise SubConverterException( f'inputFp must end with "-0001.png" or similar; got {inputFp}' ) base_name = inputFpStr[:dash_location] last_name = list(sorted(inputFp.parent.glob(f'{base_name}-*.png'), key=lambda p: p.name))[-1] return last_name
[docs] def findPNGRange( firstFp: pathlib.Path, lastFp: pathlib.Path|None = None ) -> tuple[int, int]: ''' Return a 2-tuple of the maximum PNG number and the number of digits used to specify the PNG (for MuseScore generated PNGs, for use in the widget) ''' inputFpStr = firstFp.name # not pathlib. if not inputFpStr.endswith('.png'): raise ValueError(f'firstFp must end with ".png"; got {firstFp}') if lastFp is None: lastFp = findLastPNGPath(firstFp) dash_location = inputFpStr.rfind('-') if dash_location == -1: raise ValueError( f'firstFp must end with "-0001.png" or similar; got {firstFp}' ) suffix_len = 4 # len('.png') num_digits = len(inputFpStr) - dash_location - suffix_len - 1 num_string = lastFp.name[dash_location + 1:dash_location + 1 + num_digits] # print(inputFpStr, dash_location, num_digits, lastFp, num_string) last_number = int(num_string) return (last_number, num_digits)
class Test(unittest.TestCase): def pngNumbering(self): ''' Testing findNumberedPNGPath() with files of lengths that create .png files with -1, -01, -001, and -0001 in the fp ''' env = environment.Environment() for ext_base in '1', '01', '001', '0001': png_ext = '-' + ext_base + '.png' tmp = env.getTempFile(suffix='.png', returnPathlib=False) tmpNumbered = tmp.replace('.png', png_ext) os.rename(tmp, tmpNumbered) pngFp1 = findNumberedPNGPath(tmp) self.assertEqual(str(pngFp1), tmpNumbered) os.remove(tmpNumbered) # Now with a very long path. tmp = env.getTempFile(suffix='.png', returnPathlib=False) tmpNumbered = tmp.replace('.png', '-0000001.png') os.rename(tmp, tmpNumbered) with self.assertRaises(IOError): findNumberedPNGPath(tmpNumbered) os.remove(tmpNumbered)