# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Name:         converter/__init__.py
# Purpose:      Specific subconverters for formats music21 should handle
#
# Authors:      Michael Scott Cuthbert
#               Christopher Ariza
#
# Copyright:    Copyright © 2009-2015 Michael Scott Cuthbert and the music21 Project
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Subconverters parse or display a single format.

Each subconverter should inherit from the base SubConverter object and have at least a
parseData method that sets self.stream.
'''
# ------------------------------------------------------------------------------
# Converters are associated classes; they are not subclasses, but most define a parseData() method,
# a parseFile() method, and a .stream attribute or property.
import base64
import io
import os
import pathlib
import subprocess
import sys
import unittest

from typing import Union

from music21 import common
from music21 import defaults
from music21 import stream
from music21 import exceptions21

from music21 import environment
_MOD = 'converter.subConverters'
environLocal = environment.Environment(_MOD)

# pylint complains when abstract methods are not overwritten, but that's okay.
# pylint: disable=abstract-method


class SubConverterException(exceptions21.Music21Exception):
    pass


class SubConverterFileIOException(SubConverterException):
    pass


class SubConverter:
    '''
    Class wrapper for parsing data or outputting data.

    All other Converter types should inherit from this and
    have ways of dealing with various data formats.

    Attributes that should be set::

        readBinary = True or False (default False)
        registerFormats = tuple of formats that can be handled; eg: ('musicxml',)
        registerShowFormats = tuple of format calls that can be handled in .show() and .write()
        registerInputExtensions = tuple of input extensions that should be handled in converter
        registerOutputExtensions = tuple of output extensions that can be written. Order matters:
            the first will be used in calls to .write()
        canBePickled = True or False (default True; does not do anything yet)
        codecWrite = True or False (default False) if encodings need to be used to write
        stringEncoding = string (default 'utf-8'). If codecWrite is True, this specifies what
            encoding to use


    '''
    readBinary = False
    canBePickled = True
    registerFormats = ()
    registerShowFormats = ()
    registerInputExtensions = ()  # if converter supports input, put something here
    registerOutputExtensions = ()  # if converter supports output put something here.
    registerOutputSubformatExtensions = {}
    launchKey = None

    codecWrite = False
    stringEncoding = 'utf-8'

    def __init__(self, **keywords):
        self._stream = stream.Score()
        self.keywords = keywords

    def parseData(self, dataString, number=None):
        '''
        Called when a string (or binary) data is encountered.

        This method MUST be implemented to do anything in parsing.

        Return self.stream in the end
        '''
        raise NotImplementedError
        # return self.stream

    def parseFile(self, filePath, number=None):
        '''
        Called when a file is encountered. If all that needs to be done is
        loading the file and putting the data into parseData then there is no need
        to do implement this method.  Just set self.readBinary to True|False.
        '''
        if self.readBinary is False:
            with open(filePath) as f:
                dataStream = f.read()
        else:
            with open(filePath, 'rb') as f:
                dataStream = f.read()

        # might raise NotImplementedError
        self.parseData(dataStream, number)

        return self.stream

    def _getStream(self):
        return self._stream

    def _setStream(self, newStream):
        self._stream = newStream

    stream = property(_getStream, _setStream, doc='''
        Returns or sets the stream in the converter.  Must be defined for subconverter to work.
        ''')

    def checkShowAbility(self, **keywords):
        '''
        return bool on whether the *system* is
        equipped to show in this format.

        Default True. Might be False if, say
        a Lilypond converter is used and Lilypond
        is not installed.
        '''
        return True

    def launch(self, filePath, fmt=None,
               options='', app=None, launchKey=None):  # pragma: no cover
        '''
        Opens the appropriate viewer for the file generated by .write()

        app is the path to an application to launch.  Specify it and/or a launchKey
        launchKey is the specific key in .music21rc (such as graphicsPath), etc.
        to search for the application.  If it's not specified then there might be
        a default one for the converter in self.launchKey.  If it can't find it
        there then environLocal.formatToApp(fmt) will be used.

        Not needed for formats for which .show() just prints to the console.
        '''
        if fmt is None and self.registerShowFormats:
            fmt = self.registerShowFormats[0]
        if app is None:
            if launchKey is not None:
                app = environLocal[launchKey]
            elif self.launchKey is not None:
                launchKey = self.launchKey
                app = environLocal[launchKey]
            else:
                launchKey = environLocal.formatToKey(fmt)
                app = environLocal.formatToApp(fmt)

        platform = common.getPlatform()
        if app is None:
            if platform == 'win':
                # no need to specify application here:
                # windows starts the program based on the file extension
                cmd = ('start', str(filePath))
            elif platform == 'darwin':
                if options:
                    cmd = ('open', options, str(filePath))
                else:
                    cmd = ('open', str(filePath))
            else:
                raise SubConverterException(
                    f'Cannot find a valid application path for format {fmt}. '
                    + 'Specify this in your Environment by calling '
                    + f"environment.set({launchKey!r}, '/path/to/application')"
                )
        elif platform in ('win', 'nix'):
            if options:
                cmd = (app, options, str(filePath))
            else:
                cmd = (app, str(filePath))
        elif platform == 'darwin':
            if options:
                cmd = ('open', '-a', str(app), options, str(filePath))
            else:
                cmd = ('open', '-a', str(app), str(filePath))
        else:
            raise SubConverterException(f'Cannot launch files on {platform}')
        subprocess.run(cmd, check=True)

    def show(self, obj, fmt, app=None, subformats=None, **keywords):
        '''
        Write the data, then show the generated data, using `.launch()` or printing
        to a console.

        Some simple formats that do not need launching, may skip .launch() and
        simply return the output.
        '''
        returnedFilePath = self.write(obj, fmt, subformats=subformats, **keywords)
        self.launch(returnedFilePath, fmt=fmt, app=app)

    def getExtensionForSubformats(self, subformats=None):
        '''
        Given a default format or subformats, give the file extension it should have:

        >>> c = converter.subConverters.ConverterMidi()
        >>> c.getExtensionForSubformats()
        '.mid'
        '''
        extensions = self.registerOutputExtensions
        if not extensions:
            raise SubConverterException(
                'This subconverter cannot show or write: '
                + 'no output extensions are registered for it')
        # start by trying the first one.
        ext = extensions[0]
        if self.registerOutputSubformatExtensions and subformats is not None:
            joinedSubformats = '.'.join(subformats)
            if joinedSubformats in self.registerOutputSubformatExtensions:
                ext = self.registerOutputSubformatExtensions[joinedSubformats]
        return '.' + ext

    def getTemporaryFile(self, subformats=None) -> pathlib.Path:
        '''
        Return a temporary file with an extension appropriate for the format.

        >>> c = corpus.parse('bwv66.6')
        >>> lpConverter = converter.subConverters.ConverterLilypond()
        >>> tf = str(lpConverter.getTemporaryFile(subformats=['png']))
        >>> tf.endswith('.png')
        True

        Changed in v.6 -- returns pathlib.Path
        '''
        ext = self.getExtensionForSubformats(subformats)
        fp = environLocal.getTempFile(ext, returnPathlib=True)
        return fp

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        '''
        Calls .writeDataStream on the repr of obj, and returns the fp returned by it.
        '''
        dataStr = repr(obj)
        fp = self.writeDataStream(fp, dataStr)
        return fp

    def writeDataStream(self, fp, dataStr):  # pragma: no cover
        '''
        Writes the data stream to `fp` or to a temporary file and returns the
        filename written.
        '''
        if fp is None:
            fp = self.getTemporaryFile()

        if self.readBinary is False:
            writeFlags = 'w'
        else:
            writeFlags = 'wb'

        if self.codecWrite is False:
            if hasattr(fp, 'write'):
                # is a file-like object
                f = fp
            else:
                fp = common.cleanpath(fp)
                f = open(fp, writeFlags)

            try:
                if isinstance(dataStr, bytes):
                    f.write(dataStr.decode('utf-8'))

                else:
                    f.write(dataStr)
            except UnicodeEncodeError:
                f.close()
                f = io.open(fp, mode=writeFlags, encoding=self.stringEncoding)
                f.write(dataStr)
                f.close()

            except TypeError as te:
                raise SubConverterException(f'Could not convert {dataStr!r} : {te!r}')
        else:
            if hasattr(fp, 'write'):
                # is a file-like object
                f = fp
            else:
                f = io.open(fp, mode=writeFlags, encoding=self.stringEncoding)
            f.write(dataStr)
            f.close()
        return fp


class ConverterIPython(SubConverter):
    '''
    Meta-subconverter for displaying image data etc. in iPython
    using either png (via MuseScore or LilyPond) or directly via
    Vexflow/music21j
    '''

    registerFormats = ('ipython',)
    registerOutputExtensions = ()
    registerOutputSubformatExtensions = {'lilypond': 'ly'}

    # def vfshow(self, s):
    #     '''
    #     pickle this object and send it to Vexflow
    #
    #     Alpha -- does not work too well.
    #     '''
    #     import random
    #     from music21.vexflow import toMusic21j
    #     from IPython.display import HTML # @UnresolvedImport
    #     vfp = toMusic21j.VexflowPickler()
    #     vfp.mode = 'jsonSplit'
    #     outputCode = vfp.fromObject(s)
    #     idName = 'canvasDiv' + str(random.randint(0, 10000))
    #     htmlBlock = '<div id="' + idName + '"><canvas/></div>'
    #     js = '''
    #     <script>
    #          data = ''' + outputCode + ''';
    #          var jpc = new music21.jsonPickle.Converter();
    #          var streamObj = jpc.run(data);
    #          streamObj.replaceCanvas("#''' + idName + '''");
    #     </script>
    #     '''
    #     return HTML(htmlBlock + js)

    def show(self, obj, fmt, app=None, subformats=None, **keywords):  # pragma: no cover
        '''
        show using the appropriate subformat.
        '''
        if subformats is None:
            subformats = ['vexflow']

        if subformats and subformats[0] == 'vexflow':
            return self.vfshow(obj)
            # subformats = ['lilypond', 'png']
        if subformats:
            helperFormat = subformats[0]
            helperSubformats = subformats[1:]
        else:
            helperFormat = 'musicxml'
            helperSubformats = []

        if not helperSubformats:
            helperSubformats = ['png']

        from music21 import converter

        helperConverter = converter.Converter()
        helperConverter.setSubconverterFromFormat(helperFormat)
        helperSubConverter = helperConverter.subConverter

        # noinspection PyPackageRequirements
        from IPython.display import Image, display, HTML  # @UnresolvedImport

        if helperFormat in ('musicxml', 'xml', 'lilypond', 'lily'):
            # hack to make musescore excerpts -- fix with a converter class in MusicXML
            from music21.ipython21 import objects as ipythonObjects

            savedDefaultTitle = defaults.title
            savedDefaultAuthor = defaults.author
            defaults.title = ''
            defaults.author = ''

            if 'Opus' not in obj.classes:
                scores = [obj]
            else:
                scores = list(obj.scores)

            for s in scores:
                fp = helperSubConverter.write(s,
                                              helperFormat,
                                              subformats=helperSubformats,
                                              **keywords
                                              )

                if helperSubformats[0] == 'png':
                    if not str(environLocal['musescoreDirectPNGPath']).startswith('/skip'):
                        ipo = ipythonObjects.IPythonPNGObject(fp)
                        # noinspection PyTypeChecker
                        display(Image(data=ipo.getData(), retina=True))
                    else:
                        # smallest transparent pixel
                        # noinspection SpellCheckingInspection
                        pngData64 = (b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA'
                                     + b'6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==')
                        pngData = base64.b64decode(pngData64)
                        # noinspection PyTypeChecker
                        display(Image(data=pngData, retina=True))

            defaults.title = savedDefaultTitle
            defaults.author = savedDefaultAuthor
            return None

        elif helperFormat == 'midi':
            assert isinstance(helperSubConverter, ConverterMidi)
            fp = helperSubConverter.write(
                obj,
                helperFormat,
                subformats=helperSubformats,
                addStartDelay=True,
            )
            with open(fp, 'rb') as f:
                binaryMidiData = f.read()

            binaryBase64 = base64.b64encode(binaryMidiData)
            s = common.SingletonCounter()
            outputId = 'midiPlayerDiv' + str(s())
            # noinspection PyTypeChecker
            display(HTML("""
                <div id='""" + outputId + """'></div>
                <link rel="stylesheet" href="//cuthbertLab.github.io/music21j/css/m21.css"
                    type="text/css" />
                <script>
                require.config({
                    paths: {'music21': '//cuthbertLab.github.io/music21j/src/music21'}
                });
                require(['music21'], function() {
                               mp = new music21.miditools.MidiPlayer();
                               mp.addPlayer('#""" + outputId + """');
                               mp.base64Load('data:audio/midi;base64,"""
                                + binaryBase64.decode('utf-8') + """');
                        });
                </script>"""))


class ConverterLilypond(SubConverter):
    '''
    Convert to Lilypond and from there usually to png, pdf, or svg.
    '''
    registerFormats = ('lilypond', 'lily')
    registerOutputExtensions = ('ly', 'png', 'pdf', 'svg')
    registerOutputSubformatExtensions = {'png': 'png',
                                         'pdf': 'pdf',
                                         'svg': ''}  # sic! (Why?)
    codecWrite = True

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        from music21 import lily
        conv = lily.translate.LilypondConverter()
        if 'coloredVariants' in keywords and keywords['coloredVariants'] is True:
            conv.coloredVariants = True

        if subformats is not None and 'pdf' in subformats:
            conv.loadFromMusic21Object(obj)
            convertedFilePath = conv.createPDF(fp)
            return convertedFilePath

        elif subformats is not None and 'png' in subformats:
            conv.loadFromMusic21Object(obj)
            convertedFilePath = conv.createPNG(fp)
            if 'ipython' in subformats:
                from music21.ipython21 import objects as ipythonObjects
                ipo = ipythonObjects.IPythonPNGObject(convertedFilePath)
                return ipo
            else:
                return convertedFilePath

        elif subformats is not None and 'svg' in subformats:
            conv.loadFromMusic21Object(obj)
            convertedFilePath = conv.createSVG(fp)
            return convertedFilePath

        else:
            conv.loadFromMusic21Object(obj)
            convertedFilePath = conv.writeLyFile(ext='.ly', fp=fp)
            return convertedFilePath

    def show(self, obj, fmt, app=None, subformats=None, **keywords):  # pragma: no cover
        '''
        Call .write (write out the lilypond (.ly) file; convert to .png/.pdf, etc.)
        then launch the appropriate viewer for .png/.pdf (graphicsPath) or .svg
        (vectorPath)
        '''
        if not subformats:
            subformats = ['png']
        returnedFilePath = self.write(obj, fmt, subformats=subformats, **keywords)
        if subformats is not None and subformats:
            outFormat = subformats[0]
        else:
            outFormat = 'png'

        launchKey = environLocal.formatToKey(outFormat)
        self.launch(returnedFilePath, fmt=outFormat, app=app, launchKey=launchKey)


class ConverterBraille(SubConverter):
    registerFormats = ('braille',)
    registerOutputExtensions = ('txt',)
    codecWrite = True

    def show(self, obj, fmt, app=None, subformats=None, **keywords):  # pragma: no cover
        if not common.runningUnderIPython():
            super().show(obj, fmt, app=None, subformats=subformats, **keywords)
        else:
            from music21 import braille
            dataStr = braille.translate.objectToBraille(obj)
            print(dataStr)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        from music21 import braille
        dataStr = braille.translate.objectToBraille(obj)
        if subformats is not None and 'ascii' in subformats:
            dataStr = braille.basic.brailleUnicodeToBrailleAscii(dataStr)
        fp = self.writeDataStream(fp, dataStr)
        return fp


class ConverterVexflow(SubConverter):
    registerFormats = ('vexflow',)
    registerOutputExtensions = ('html',)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        # from music21 import vexflow
        from music21.vexflow import toMusic21j as vexflow
        if 'local' in keywords:
            local = keywords['local']
        else:
            local = False

        dataStr = vexflow.fromObject(obj, mode='html', local=local)
        fp = self.writeDataStream(fp, dataStr)
        return fp


class ConverterText(SubConverter):
    '''
    standard text presentation has line breaks, is printed.

    Two keyword options are allowed: addEndTimes=Boolean and useMixedNumerals=Boolean
    '''

    registerFormats = ('text', 'txt', 't')
    registerOutputExtensions = ('txt',)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        dataStr = obj._reprText(**keywords)
        self.writeDataStream(fp, dataStr)
        return fp

    def show(self, obj, fmt, app=None, subformats=None, **keywords):
        print(obj._reprText(**keywords))


class ConverterTextLine(SubConverter):
    '''
    a text line compacts the complete recursive representation into a
    single line of text; most for debugging. returned, not printed

    >>> s = corpus.parse('bwv66.6')
    >>> s.measures(1, 4).show('textline')
    "{0.0} <music21.stream.Part Soprano> / {0.0} <music21.instrument.Instrument '... 1'>..."
    '''
    registerFormats = ('textline',)
    registerOutputExtensions = ('txt',)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        dataStr = obj._reprTextLine()
        self.writeDataStream(fp, dataStr)
        return fp

    def show(self, obj, fmt, app=None, subformats=None, **keywords):
        return obj._reprTextLine()


class ConverterVolpiano(SubConverter):
    '''
    Reads or writes volpiano (Chant encoding).

    Normally, just use 'converter' and .show()/.write()

    >>> p = converter.parse('volpiano: 1---c-d-ef----4')
    >>> p.show('text')
    {0.0} <music21.stream.Measure 0 offset=0.0>
        {0.0} <music21.clef.TrebleClef>
        {0.0} <music21.note.Note C>
        {1.0} <music21.note.Note D>
        {2.0} <music21.note.Note E>
        {3.0} <music21.note.Note F>
        {4.0} <music21.volpiano.Neume <music21.note.Note E><music21.note.Note F>>
        {4.0} <music21.bar.Barline type=double>
    >>> p.show('volpiano')
    1---c-d-ef----4
    '''
    registerFormats = ('volpiano',)
    registerInputExtensions = ('volpiano', 'vp')
    registerOutputExtensions = ('txt', 'vp')

    def parseData(self, dataString, **keywords):
        from music21 import volpiano
        breaksToLayout = keywords.get('breaksToLayout', False)
        self.stream = volpiano.toPart(dataString, breaksToLayout=breaksToLayout)

    def getDataStr(self, obj, **keywords):
        '''
        Get the raw data, for storing as a variable.
        '''
        from music21 import volpiano
        if obj.isStream:
            s = obj
        else:
            s = stream.Stream()
            s.append(obj)

        return volpiano.fromStream(s)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        dataStr = self.getDataStr(obj, **keywords)
        self.writeDataStream(fp, dataStr)
        return fp

    def show(self, obj, fmt, app=None, subformats=None, **keywords):
        print(self.getDataStr(obj, **keywords))


class ConverterScala(SubConverter):
    registerFormats = ('scala',)
    registerInputExtensions = ('scl',)
    registerOutputExtensions = ('scl',)


# ------------------------------------------------------------------------------
class ConverterHumdrum(SubConverter):
    '''
    Simple class wrapper for parsing Humdrum data provided in a file or in a string.
    '''
    registerFormats = ('humdrum',)
    registerInputExtensions = ('krn',)

    def __init__(self, **keywords):
        super().__init__(**keywords)
        self.data = None

    # --------------------------------------------------------------------------

    def parseData(self, humdrumString, number=None):
        '''Open Humdrum data from a string -- calls humdrum.parseData()

        >>> humData = ('**kern\\n*M2/4\\n=1\\n24r\\n24g#\\n24f#\\n24e\\n24c#\\n' +
        ...     '24f\\n24r\\n24dn\\n24e-\\n24gn\\n24e-\\n24dn\\n*-')
        >>> c = converter.subConverters.ConverterHumdrum()
        >>> s = c.parseData(humData)
        >>> c.stream.show('text')
        {0.0} <music21.metadata.Metadata object at 0x7f33545027b8>
        {0.0} <music21.stream.Part spine_0>
            {0.0} <music21.humdrum.spineParser.MiscTandem **kern humdrum control>
            {0.0} <music21.stream.Measure 1 offset=0.0>
                {0.0} <music21.meter.TimeSignature 2/4>
                {0.0} <music21.note.Rest rest>
                {0.1667} <music21.note.Note G#>
                {0.3333} <music21.note.Note F#>
                {0.5} <music21.note.Note E>
                {0.6667} <music21.note.Note C#>
                {0.8333} <music21.note.Note F>
                {1.0} <music21.note.Rest rest>
                {1.1667} <music21.note.Note D>
                {1.3333} <music21.note.Note E->
                {1.5} <music21.note.Note G>
                {1.6667} <music21.note.Note E->
                {1.8333} <music21.note.Note D>
        '''
        from music21.humdrum.spineParser import HumdrumDataCollection

        hdf = HumdrumDataCollection(humdrumString)
        hdf.parse()
        self.data = hdf
        self.stream = self.data.stream
        return self.data

    def parseFile(self, filepath, number=None):
        '''
        Open Humdrum data from a file path.

        Calls humdrum.parseFile on filepath.

        Number is ignored here.
        '''
        from music21.humdrum.spineParser import HumdrumFile
        hf = HumdrumFile(filepath)
        hf.parseFilename()
        self.data = hf
        # self.data.stream.makeNotation()

        self.stream = self.data.stream
        return self.data

# ------------------------------------------------------------------------------


class ConverterTinyNotation(SubConverter):
    '''
    Simple class wrapper for parsing TinyNotation data provided in a file or
    in a string.

    Input only for now.
    '''
    registerFormats = ('tinynotation',)
    registerInputExtensions = ('tntxt', 'tinynotation')

    def __init__(self, **keywords):
        super().__init__(**keywords)
        self.data = None

    # --------------------------------------------------------------------------
    def parseData(self, tnData, number=None):
        # noinspection PyShadowingNames
        '''
        Open TinyNotation data from a string

        >>> tnData = "3/4 E4 r f# g=lastG trip{b-8 a g} c"
        >>> c = converter.subConverters.ConverterTinyNotation()
        >>> s = c.parseData(tnData)
        >>> c.stream.show('text')
        {0.0} <music21.stream.Measure 1 offset=0.0>
            {0.0} <music21.clef.TrebleClef>
            {0.0} <music21.meter.TimeSignature 3/4>
            {0.0} <music21.note.Note E>
            {1.0} <music21.note.Rest rest>
            {2.0} <music21.note.Note F#>
        {3.0} <music21.stream.Measure 2 offset=3.0>
            {0.0} <music21.note.Note G>
            {1.0} <music21.note.Note B->
            {1.3333} <music21.note.Note A>
            {1.6667} <music21.note.Note G>
            {2.0} <music21.note.Note C>
            {2.5} <music21.bar.Barline type=final>
        '''
        if isinstance(tnData, str):
            tnStr = tnData
        else:  # assume a 2 element sequence  # pragma: no cover
            raise SubConverterException(
                'TinyNotation no longer supports two-element calls; put the time signature '
                + 'in the stream')
        from music21 import tinyNotation
        self.stream = tinyNotation.Converter(tnStr, **self.keywords).parse().stream


class ConverterNoteworthy(SubConverter):
    # noinspection SpellCheckingInspection
    '''
    Simple class wrapper for parsing NoteworthyComposer data provided in a
    file or in a string.

    Gets data with the file format .nwctxt

    Users should not need this routine.  The basic format is converter.parse('file.nwctxt')


    >>> nwcTranslatePath = common.getSourceFilePath() / 'noteworthy' #_DOCS_HIDE
    >>> paertPath = nwcTranslatePath / 'Part_OWeisheit.nwctxt' #_DOCS_HIDE
    >>> #_DOCS_SHOW paertPath = converter.parse(r'd:/desktop/arvo_part_o_weisheit.nwctxt')
    >>> paertStream = converter.parse(paertPath)
    >>> len(paertStream.parts)
    4

    For developers: see the documentation for :meth:`parseData` and :meth:`parseFile`
    to see the low-level usage.
    '''
    registerFormats = ('noteworthytext',)
    registerInputExtensions = ('nwctxt',)

    # --------------------------------------------------------------------------
    def parseData(self, nwcData):
        # noinspection PyShadowingNames
        r'''Open Noteworthy data from a string or list

        >>> nwcData = ('!NoteWorthyComposer(2.0)\n|AddStaff\n|Clef|' +
        ...     'Type:Treble\n|Note|Dur:Whole|Pos:1^')
        >>> c = converter.subConverters.ConverterNoteworthy()
        >>> c.parseData(nwcData)
        >>> c.stream.show('text')
        {0.0} <music21.stream.Part ...>
            {0.0} <music21.stream.Measure 0 offset=0.0>
                {0.0} <music21.clef.TrebleClef>
                {0.0} <music21.note.Note C>
        '''
        from music21.noteworthy import translate as noteworthyTranslate
        self.stream = noteworthyTranslate.NoteworthyTranslator().parseString(nwcData)

    def parseFile(self, fp, number=None):
        # noinspection SpellCheckingInspection
        '''
        Open Noteworthy data (as nwctxt) from a file path.

        >>> import os #_DOCS_HIDE
        >>> nwcTranslatePath = common.getSourceFilePath() / 'noteworthy' #_DOCS_HIDE
        >>> filePath = nwcTranslatePath / 'Part_OWeisheit.nwctxt' #_DOCS_HIDE
        >>> #_DOCS_SHOW paertPath = converter.parse('d:/desktop/arvo_part_o_weisheit.nwctxt')
        >>> c = converter.subConverters.ConverterNoteworthy()
        >>> c.parseFile(filePath)
        >>> #_DOCS_SHOW c.stream.show()
        '''
        from music21.noteworthy import translate as noteworthyTranslate
        self.stream = noteworthyTranslate.NoteworthyTranslator().parseFile(fp)


class ConverterNoteworthyBinary(SubConverter):
    '''
    Simple class wrapper for parsing NoteworthyComposer binary data
    provided in a file or in a string.

    Gets data with the file format .nwc

    Users should not need this routine.  Call converter.parse directly
    '''
    readBinary = True
    registerFormats = ('noteworthy',)
    registerInputExtensions = ('nwc', )

    # --------------------------------------------------------------------------
    def parseData(self, nwcData):  # pragma: no cover
        from music21.noteworthy import binaryTranslate as noteworthyBinary
        self.stream = noteworthyBinary.NWCConverter().parseString(nwcData)

    def parseFile(self, fp, number=None):
        from music21.noteworthy import binaryTranslate as noteworthyBinary
        self.stream = noteworthyBinary.NWCConverter().parseFile(fp)


# ------------------------------------------------------------------------------
class ConverterMusicXML(SubConverter):
    '''
    Converter for MusicXML using the 2015 ElementTree system.

    Users should not need this Object.  Call converter.parse directly
    '''
    registerFormats = ('musicxml', 'xml')
    registerInputExtensions = ('xml', 'mxl', 'mx', 'musicxml')
    registerOutputExtensions = ('xml', 'mxl')
    registerOutputSubformatExtensions = {'png': 'png',
                                         'pdf': 'pdf',
                                         }

    # --------------------------------------------------------------------------
    def findPNGfpFromXMLfp(self, xmlFilePath: Union[str, pathlib.Path]) -> str:
        '''
        Check whether total number of pngs is in 1-9, 10-99, or 100-999 range,
        then return appropriate fp. Raises and exception if png fp does not exist.
        '''
        xmlFilePath = str(xmlFilePath)  # not pathlib.
        path_without_extension = xmlFilePath[:-4]

        for search_extension in ('1', '01', '001', '0001', '00001'):
            search_path = path_without_extension + '-' + search_extension + '.png'
            if os.path.exists(search_path):
                return search_path

        raise SubConverterFileIOException(
            f'No png file for {xmlFilePath} (such as {path_without_extension}-1.png) was found.  '
            + 'The conversion to png failed'
        )

    def parseData(self, xmlString: str, number=None):
        '''
        Open MusicXML data from a string.
        '''
        from music21.musicxml import xmlToM21

        c = xmlToM21.MusicXMLImporter()
        c.xmlText = xmlString
        c.parseXMLText()
        self.stream = c.stream

    def parseFile(self, fp: Union[str, pathlib.Path], number=None):
        '''
        Open from a file path; check to see if there is a pickled
        version available and up to date; if so, open that, otherwise
        open source.
        '''
        # return fp to load, if pickle needs to be written, fp pickle
        # this should be able to work on a .mxl file, as all we are doing
        # here is seeing which is more recent
        from music21 import converter
        from music21.musicxml import xmlToM21

        c = xmlToM21.MusicXMLImporter()

        # here, we can see if this is a mxl or similar archive
        arch = converter.ArchiveManager(fp)
        if arch.isArchive():
            archData = arch.getData()
            c.xmlText = archData
            c.parseXMLText()
        else:  # its a file path or a raw musicxml string
            c.readFile(fp)

        # movement titles can be stored in more than one place in musicxml
        # manually insert file name as a movementName title if no titles are defined
        if c.stream.metadata.movementName is None:
            junk, fn = os.path.split(fp)
            c.stream.metadata.movementName = fn  # this should become a Path
        self.stream = c.stream

    def runThroughMusescore(self, fp, subformats=None, **keywords):  # pragma: no cover
        '''
        Take the output of the conversion process and run it through musescore to convert it
        to a png.
        '''
        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 subformats is None:
            subformatExtension = 'png'
        else:
            subformatExtension = subformats[0]

        fpOut = str(fp)[:-3]
        fpOut += subformatExtension

        musescoreRun = [str(musescorePath), fp, '-o', fpOut, '-T', '0']
        if 'dpi' in keywords:
            musescoreRun.extend(['-r', str(keywords['dpi'])])

        if common.runningUnderIPython():
            musescoreRun.extend(['-r', str(defaults.ipythonImageDpi)])

        storedStrErr = sys.stderr
        fileLikeOpen = io.StringIO()
        sys.stderr = fileLikeOpen
        subprocess.run(musescoreRun, check=False)
        fileLikeOpen.close()
        sys.stderr = storedStrErr

        if subformatExtension == 'png':
            return self.findPNGfpFromXMLfp(fpOut)
        else:
            return fpOut
        # common.cropImageFromPath(fp)

    def writeDataStream(self, fp, dataBytes: bytes):  # pragma: no cover
        if fp is None:
            fp = self.getTemporaryFile()
        else:
            fp = common.cleanpath(fp)

        writeFlags = 'wb'

        with open(fp, writeFlags) as f:
            f: io.BytesIO
            f.write(dataBytes)

        return fp

    def write(self, obj, fmt, fp=None, subformats=None,
              compress=False, **keywords):  # pragma: no cover
        '''
        Write to a .xml file.
        Set `compress=True` to immediately compress the output to a .mxl file.
        '''
        from music21.musicxml import archiveTools, m21ToXml

        savedDefaultTitle = defaults.title
        savedDefaultAuthor = defaults.author

        # hack to make musescore excerpts -- fix with a converter class in MusicXML
        if subformats is not None and 'png' in subformats:
            # do not print a title or author -- to make the PNG smaller.
            defaults.title = ''
            defaults.author = ''

        generalExporter = m21ToXml.GeneralObjectExporter(obj)
        dataBytes: bytes = generalExporter.parse()

        writeDataStreamFp = fp
        if fp is not None and subformats is not None:
            fpStr = str(fp)
            noExtFpStr = os.path.splitext(fpStr)[0]
            writeDataStreamFp = noExtFpStr + '.xml'

        xmlFp = self.writeDataStream(writeDataStreamFp, dataBytes)

        if subformats is not None and 'png' in subformats:
            defaults.title = savedDefaultTitle
            defaults.author = savedDefaultAuthor

        if (subformats is not None
                and ('png' in subformats or 'pdf' in subformats)
                and not str(environLocal['musescoreDirectPNGPath']).startswith('/skip')):
            outFp = self.runThroughMusescore(xmlFp, subformats, **keywords)
        elif compress:
            archiveTools.compressXML(xmlFp, deleteOriginal=True)
            filenameOut = os.path.splitext(str(xmlFp))[0] + '.mxl'
            outFp = common.pathTools.cleanpath(filenameOut, returnPathlib=True)
        else:
            outFp = xmlFp

        return outFp

    def show(self, obj, fmt, app=None, subformats=None, **keywords):  # pragma: no cover
        '''
        Override to do something with png...
        '''
        returnedFilePath = self.write(obj, fmt, subformats=subformats, **keywords)
        if subformats is not None:
            if 'png' in subformats:
                fmt = 'png'
            elif 'pdf' in subformats:
                fmt = 'pdf'
        self.launch(returnedFilePath, fmt=fmt, app=app)


# ------------------------------------------------------------------------------
class ConverterMidi(SubConverter):
    '''
    Simple class wrapper for parsing MIDI and sending MIDI data out.
    '''
    readBinary = True
    registerFormats = ('midi',)
    registerInputExtensions = ('mid', 'midi')
    registerOutputExtensions = ('mid',)

    def parseData(self, strData, number=None):
        '''
        Get MIDI data from a binary string representation.

        Calls midi.translate.midiStringToStream.

        Keywords to control quantization:
        `quantizePost` controls whether to quantize the output. (Default: True)
        `quarterLengthDivisors` allows for overriding the default quantization units
        in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
        '''
        from music21.midi import translate as midiTranslate
        self.stream = midiTranslate.midiStringToStream(strData, **self.keywords)

    def parseFile(self, fp, number=None, **keywords):
        '''
        Get MIDI data from a file path.

        Calls midi.translate.midiFilePathToStream.

        Keywords to control quantization:
        `quantizePost` controls whether to quantize the output. (Default: True)
        `quarterLengthDivisors` allows for overriding the default quantization units
        in defaults.quantizationQuarterLengthDivisors. (Default: (4, 3)).
        '''
        from music21.midi import translate as midiTranslate
        midiTranslate.midiFilePathToStream(fp, self.stream, **keywords)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        from music21.midi import translate as midiTranslate
        if fp is None:
            fp = self.getTemporaryFile()

        midiTranslateKeywords = {}
        if 'addStartDelay' in keywords:
            midiTranslateKeywords['addStartDelay'] = keywords['addStartDelay']

        mf = midiTranslate.music21ObjectToMidiFile(obj, **midiTranslateKeywords)
        mf.open(fp, 'wb')  # write binary
        mf.write()
        mf.close()
        return fp


# ------------------------------------------------------------------------------
class ConverterABC(SubConverter):
    '''
    Simple class wrapper for parsing ABC.

    Input only
    '''
    registerFormats = ('abc',)
    registerInputExtensions = ('abc',)

    def parseData(self, strData, number=None):
        '''
        Get ABC data, as token list, from a string representation.
        If more than one work is defined in the ABC data, a
        :class:`~music21.stream.Opus` object will be returned;
        otherwise, a :class:`~music21.stream.Score` is returned.
        '''
        from music21 import abcFormat
        af = abcFormat.ABCFile()
        # do not need to call open or close
        abcHandler = af.readstr(strData, number=number)
        # set to stream
        if abcHandler.definesReferenceNumbers():
            # this creates an Opus object, not a Score object
            self.stream = abcFormat.translate.abcToStreamOpus(abcHandler,
                                                              number=number)
        else:  # just one work
            abcFormat.translate.abcToStreamScore(abcHandler, self.stream)

    def parseFile(self, fp, number=None):
        '''
        Get ABC data from a file path. If more than one work is defined in the ABC
        data, a  :class:`~music21.stream.Opus` object will be returned;
        otherwise, a :class:`~music21.stream.Score` is returned.

        If `number` is provided, and this ABC file defines multiple works
        with a X: tag, just the specified work will be returned.
        '''
        # environLocal.printDebug(['ConverterABC.parseFile: got number', number])
        from music21 import abcFormat

        af = abcFormat.ABCFile()
        af.open(fp)
        # returns a handler instance of parse tokens
        abcHandler = af.read(number=number)
        af.close()

        # only create opus if multiple ref numbers
        # are defined; if a number is given an opus will no be created
        if abcHandler.definesReferenceNumbers():
            # this creates a Score or Opus object, depending on if a number
            # is given
            self.stream = abcFormat.translate.abcToStreamOpus(abcHandler,
                                                              number=number)
        # just get a single work
        else:
            abcFormat.translate.abcToStreamScore(abcHandler, self.stream)


class ConverterRomanText(SubConverter):
    '''Simple class wrapper for parsing roman text harmonic definitions.
    '''
    registerFormats = ('romantext', 'rntext')
    registerInputExtensions = ('rntxt', 'rntext', 'romantext', 'rtxt')
    registerOutputExtensions = ('rntxt',)

    def parseData(self, strData, number=None):
        from music21.romanText import rtObjects
        from music21.romanText import translate as romanTextTranslate
        rtf = rtObjects.RTFile()  # not sure why -- @UndefinedVariable
        rtHandler = rtf.readstr(strData)
        if rtHandler.definesMovements():
            # this re-defines Score as an Opus
            self.stream = romanTextTranslate.romanTextToStreamOpus(rtHandler)
        else:
            romanTextTranslate.romanTextToStreamScore(rtHandler, self.stream)

    def parseFile(self, fp, number=None):
        from music21.romanText import rtObjects
        from music21.romanText import translate as romanTextTranslate
        rtf = rtObjects.RTFile()  # not sure why -- @UndefinedVariable
        rtf.open(fp)
        # returns a handler instance of parse tokens
        rtHandler = rtf.read()
        rtf.close()
        romanTextTranslate.romanTextToStreamScore(rtHandler, self.stream)

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        '''
        Writes 'RomanText' files (using the extension .rntxt) from a music21.stream.
        '''

        from music21.romanText import writeRoman
        if fp is None:
            fp = self.getTemporaryFile()

        with open(fp, 'w') as text_file:
            for entry in writeRoman.RnWriter(obj).combinedList:
                text_file.write(entry + '\n')

        return fp


class ConverterClercqTemperley(SubConverter):
    '''
    Wrapper for parsing harmonic definitions in Trevor de Clercq and
    David Temperley's format.
    '''
    registerFormats = ('cttxt', 'har')
    registerInputExtensions = ('cttxt', 'har')

    def parseData(self, strData, number=None):
        from music21.romanText import clercqTemperley
        ctSong = clercqTemperley.CTSong(strData)
        self.stream = ctSong.toScore()

    def parseFile(self, fp, number=None):
        self.parseData(fp)


class ConverterCapella(SubConverter):
    '''
    Simple class wrapper for parsing Capella .capx XML files.  See capella/fromCapellaXML.
    '''
    registerFormats = ('capella',)
    registerInputExtensions = ('capx',)

    def parseData(self, strData, number=None):
        '''
        parse a data stream of uncompressed capella xml

        N.B. for web parsing, it gets more complex.
        '''
        from music21.capella import fromCapellaXML
        ci = fromCapellaXML.CapellaImporter()
        ci.parseXMLText(strData)
        scoreObj = ci.systemScoreFromScore(self.mainDom.documentElement)
        partScore = ci.partScoreFromSystemScore(scoreObj)
        self.stream = partScore

    def parseFile(self, fp, number=None):
        '''
        Parse a Capella file
        '''
        from music21.capella import fromCapellaXML
        ci = fromCapellaXML.CapellaImporter()
        self.stream = ci.scoreFromFile(fp)


# ------------------------------------------------------------------------------
class ConverterMuseData(SubConverter):
    '''Simple class wrapper for parsing MuseData.
    '''
    registerFormats = ('musedata',)
    registerInputExtensions = ('md', 'musedata', 'zip')

    def parseData(self, strData, number=None):  # pragma: no cover
        '''Get musedata from a string representation.

        '''
        from music21 import musedata as musedataModule
        from music21.musedata import translate as musedataTranslate

        if isinstance(strData, str):
            strDataList = [strData]
        else:
            strDataList = strData

        mdw = musedataModule.MuseDataWork()

        for strDataInner in strDataList:
            mdw.addString(strDataInner)

        musedataTranslate.museDataWorkToStreamScore(mdw, self.stream)

    def parseFile(self, fp, number=None):
        '''
        parse fp (a pathlib.Path()) and number
        '''
        if not isinstance(fp, pathlib.Path):
            fp = pathlib.Path(fp)

        from music21 import converter
        from music21 import musedata as musedataModule
        from music21.musedata import translate as musedataTranslate

        mdw = musedataModule.MuseDataWork()

        af = converter.ArchiveManager(fp)

        # environLocal.printDebug(['ConverterMuseData: parseFile', fp, af.isArchive()])
        # for dealing with one or more files
        if fp.suffix == '.zip' or af.isArchive():
            # environLocal.printDebug(['ConverterMuseData: found archive', fp])
            # get data will return all data from the zip as a single string
            for partStr in af.getData(dataFormat='musedata'):
                # environLocal.printDebug(['partStr', len(partStr)])
                mdw.addString(partStr)
        else:
            if fp.is_dir:
                mdd = musedataModule.MuseDataDirectory(fp)
                fpList = mdd.getPaths()
            elif not common.isListLike(fp):
                fpList = [fp]
            else:
                fpList = fp

            for fpInner in fpList:
                mdw.addFile(fpInner)

        # environLocal.printDebug(['ConverterMuseData: mdw file count', len(mdw.files)])

        musedataTranslate.museDataWorkToStreamScore(mdw, self.stream)


class ConverterMEI(SubConverter):
    '''
    Converter for MEI. You must use an ".mei" file extension for MEI files because music21 will
    parse ".xml" files as MusicXML.
    '''

    registerFormats = ('mei',)
    registerInputExtensions = ('mei',)
    # NOTE: we're only working on import for now
    # registerShowFormats = ('mei',)
    # registerOutputExtensions = ('mei',)

    def parseData(self, dataString: str, number=None) -> 'music21.stream.Stream':
        '''
        Convert a string with an MEI document into its corresponding music21 elements.

        * dataString: The string with XML to convert.

        * number: Unused in this class. Default is ``None``.

        Returns the music21 objects corresponding to the MEI file.
        '''
        from music21 import mei
        if dataString.startswith('mei:'):
            dataString = dataString[4:]

        self.stream = mei.MeiToM21Converter(dataString).run()

        return self.stream

    def parseFile(
        self,
        filePath: Union[str, pathlib.Path],
        number=None
    ) -> 'music21.stream.Stream':
        '''
        Convert a file with an MEI document into its corresponding music21 elements.

        * filePath: Full pathname to the file containing MEI data as a string or Path.

        * number: Unused in this class. Default is ``None``.

        Returns the music21 objects corresponding to the MEI file.
        '''
        # In Python 3 we try the two most likely encodings to work. (UTF-16 is outputted from
        # "sibmei", the Sibelius-to-MEI exporter).
        try:
            with open(filePath, 'rt', encoding='utf-8') as f:
                dataStream = f.read()
        except UnicodeDecodeError:
            with open(filePath, 'rt', encoding='utf-16') as f:
                dataStream = f.read()

        self.parseData(dataStream, number)

        return self.stream

    def checkShowAbility(self, **keywords):
        '''
        MEI export is not yet implemented.
        '''
        return False

    # def launch(self, filePath, fmt=None, options='', app=None, key=None):
    #     raise NotImplementedError('MEI export is not yet implemented.')

    def show(self, obj, fmt, app=None, subformats=None, **keywords):  # pragma: no cover
        raise NotImplementedError('MEI export is not yet implemented.')

    # def getTemporaryFile(self, subformats=None):
    #     raise NotImplementedError('MEI export is not yet implemented.')

    def write(self, obj, fmt, fp=None, subformats=None, **keywords):  # pragma: no cover
        raise NotImplementedError('MEI export is not yet implemented.')

    # def writeDataStream(self, fp, dataStr):
    #     raise NotImplementedError('MEI export is not yet implemented.')


class Test(unittest.TestCase):

    def testSimpleTextShow(self):
        from music21 import note
        n = note.Note()
        s = stream.Stream()
        s.append(n)
        unused_x = s.show('textLine')

    def testImportMei1(self):
        '''
        When the string starts with "mei:"
        '''
        from unittest import mock
        with mock.patch('music21.mei.MeiToM21Converter') as mockConv:
            testConverter = ConverterMEI()
            testConverter.parseData('mei: <?xml><mei><note/></mei>')
            mockConv.assert_called_once_with(' <?xml><mei><note/></mei>')

    def testImportMei2(self):
        '''
        When the string doesn't start with "mei:"
        '''
        from unittest import mock
        with mock.patch('music21.mei.MeiToM21Converter') as mockConv:
            testConverter = ConverterMEI()
            testConverter.parseData('<?xml><mei><note/></mei>')
            mockConv.assert_called_once_with('<?xml><mei><note/></mei>')

    def testImportMei3(self):
        '''
        When the file uses UTF-16 encoding rather than UTF-8 (which happens if
        it was exported from
        the "sibmei" plug-in for Sibelius.
        '''
        from unittest import mock  # pylint: disable=no-name-in-module
        with mock.patch('music21.mei.MeiToM21Converter') as mockConv:
            testPath = common.getSourceFilePath() / 'mei' / 'test' / 'notes_in_utf16.mei'
            testConverter = ConverterMEI()
            testConverter.parseFile(testPath)
            self.assertEqual(1, mockConv.call_count)

    def testImportMei4(self):
        '''
        For the sake of completeness, this is the same as testImportMei3() but with a UTF-8 file.
        '''
        from unittest import mock  # pylint: disable=no-name-in-module
        with mock.patch('music21.mei.MeiToM21Converter') as mockConv:
            testPath = common.getSourceFilePath() / 'mei' / 'test' / 'notes_in_utf8.mei'
            testConverter = ConverterMEI()
            testConverter.parseFile(testPath)
            self.assertEqual(1, mockConv.call_count)

    def testXMLtoPNG(self):
        '''
        testing the findPNGfpFromXMLfp method with three different files of lengths
        that create .png files with -1, -01, and -001 in the fp
        '''
        env = environment.Environment()
        for ext_base in '1', '01', '001':
            png_ext = '-' + ext_base + '.png'

            tempFp1 = str(env.getTempFile())
            xmlFp1 = tempFp1 + '.xml'
            os.rename(tempFp1, tempFp1 + png_ext)
            tempFp1 += png_ext
            xmlConverter1 = ConverterMusicXML()
            pngFp1 = xmlConverter1.findPNGfpFromXMLfp(xmlFp1)
            self.assertEqual(pngFp1, tempFp1)


    def testXMLtoPNGTooLong(self):
        '''
        testing the findPNGfpFromXMLfp method with a file that is obscenely long
        '''
        env = environment.Environment()
        tempFp = str(env.getTempFile())
        xmlFp = tempFp + '.xml'
        os.rename(tempFp, tempFp + '-0000001.png')
        tempFp += '-0000001.png'
        xmlConverter = ConverterMusicXML()
        self.assertRaises(SubConverterFileIOException, xmlConverter.findPNGfpFromXMLfp, xmlFp)

    def testWriteMXL(self):
        from music21 import converter
        from music21.musicxml import testPrimitive

        s = converter.parseData(testPrimitive.multiDigitEnding)
        mxlPath = s.write('mxl')
        self.assertTrue(str(mxlPath).endswith('.mxl'))


class TestExternal(unittest.TestCase):  # pragma: no cover

    def testXMLShow(self):
        from music21 import corpus
        c = corpus.parse('bwv66.6')
        c.show()  # musicxml

    def testWriteLilypond(self):
        from music21 import note
        n = note.Note()
        n.duration.type = 'whole'
        s = stream.Stream()
        s.append(n)
        s.show('lily.png')
        print(s.write('lily.png'))

    def testMultiPageXMlShow1(self):
        '''
        tests whether show() works for music that is 10-99 pages long
        '''
        from music21 import omr, converter
        K525 = omr.correctors.K525groundTruthFilePath
        K525 = converter.parse(K525)
        K525.show('musicxml.png')
        print(K525.write('musicxml.png'))

    # def testMultiPageXMlShow2(self):
    #     '''
    #      tests whether show() works for music that is 100-999 pages long.
    #      Currently takes way too long to run.
    #      '''
    #     from music21 import stream, note
    #     biggerStream = stream.Stream()
    #     note1 = note.Note('C4')
    #     note1.duration.type = 'whole'
    #     biggerStream.repeatAppend(note1, 10000)
    #     biggerStream.show('musicxml.png')
    #     biggerStream.show()
    #     print(biggerStream.write('musicxml.png'))


if __name__ == '__main__':
    import music21
    # import sys
    # sys.argv.append('SimpleTextShow')
    music21.mainTest(Test)
    # run command below to test commands that open musescore, etc.
    # music21.mainTest(TestExternal)
