cuthbertLab / music21

music21 is a Toolkit for Computational Musicology
https://www.music21.org/
Other
2.12k stars 402 forks source link

Support OSMD #306

Open supersational opened 6 years ago

supersational commented 6 years ago

I ran into this excellent library for rendering sheet music in-browser using MusicXML, and wondered if it could be used with music21: Open Sheet Music Display

Turns out it is possible with a bit of js-hackery! I've posted the script below. This makes it easy to display scores without installing any other programs (apart from a web browser).

The script requires the following file in the same directory (I can't upload .js files here, but it works fine as .txt). It's simply a compiled version of the OSMD project. opensheetmusicdisplay.min.js.txt

This should display a random score from the corpus, enjoy!

from music21 import *
import webbrowser, os, random, pathlib

osmd_js_file = os.path.join(os.path.dirname(os.path.realpath(__file__)),"opensheetmusicdisplay.min.js.txt")

if not pathlib.Path(osmd_js_file).is_file():
    print("ERROR: need ./opensheetmusicdisplay.min.js.txt at",osmd_js_file) 
    exit()
print("found required .js")

selected_piece = random.choice(corpus.getPaths())
print('loading piece', selected_piece)
b = corpus.parse(selected_piece)

def stream_to_web(b):
    html_template = """
    <html>
        <head>
            <meta http-equiv="content-type" content="text/html; charset=utf-8" />
            <title>Music21 Fragment</title>
            <script src="{osmd_js_path}"></script>
        </head>
        <body>
            <span>may take a while to load large XML...<span>
            <div id='main-div'></div>
            <button opensheetmusicdisplayClick="show_xml()">Show xml</button>
            <pre id='xml-div'></pre>
            <script>
            var data = `{data}`;
            function show_xml() {{
                document.getElementById('xml-div').textContent = data;
            }}

              var openSheetMusicDisplay = new opensheetmusicdisplay.OpenSheetMusicDisplay("main-div");
              openSheetMusicDisplay
                .load(data)
                .then(
                  function() {{
                    console.log(openSheetMusicDisplay.render());
                  }}
                );
            </script>
        </body>

    """

    osmd_js_path = pathlib.Path(osmd_js_file).as_uri()

    filename = b.write('musicxml')
    print("musicXML filename:",filename)
    if filename is not None:
        with open(filename,'r') as f:
            xmldata = f.read()
        with open(filename+'.html','w') as f_html:
            html = html_template.format(
                data=xmldata.replace('`','\\`'),
                osmd_js_path=osmd_js_path)
            f_html.write(html)

        webbrowser.open('file://' + os.path.realpath(filename+'.html'))

stream_to_web(b)
psychemedia commented 6 years ago

I've been trying a similar route in Jupyter notebooks, but for some reason can't seem to see the required opensheetmusicdisplay function?

Here's the recipe I was trying:

from music21 import *

c = chord.Chord("C4 E4 G4")
xml = open(c.write('musicxml')).read()

html='''
<div id='main-div'></div>
<script>
var openSheetMusicDisplay = new opensheetmusicdisplay.OpenSheetMusicDisplay("main-div");
openSheetMusicDisplay
    .load('{data}')
    .then(
        function() {{ openSheetMusicDisplay.render(); }} );
</script>'''.format(data=xml.replace('\n','').replace('\t','')).replace('\n','')

from IPython.display import HTML, Javascript

Javascript('https://cdn.jsdelivr.net/npm/opensheetmusicdisplay@0.3.1/build/opensheetmusicdisplay.min.js')
HTML(html)

Error is:

Javascript error adding output!
ReferenceError: opensheetmusicdisplay is not defined
See your browser Javascript console for more details.

It strikes me that opensheetmusicdisplay is a great way to go for Jupyter notebooks, perhaps implemented via a notebook extension, or even IPython magic.

I've also just come across https://github.com/akaihola/jupyter_abc, a notebook extension "for rendering ABC markup as graphical music notation in a Jupyter notebook", although it doesn't seem to work on Azure notebooks, which is where I'm trying to demo things. I'm not sure if there's a straightforward root to using this with music21 too?

supersational commented 6 years ago

Yep, unfortunately Jupyter notebooks don't have the best JavaScript debugging support.

I've now got a working script that displays most scores in notebooks too:

from IPython.core.display import display, HTML, Javascript
import json, random
def showScore(score):
    xml = open(score.write('musicxml')).read()
    showMusicXML(xml)

def showMusicXML(xml):
    DIV_ID = "OSMD-div-"+str(random.randint(0,1000000))
    print("DIV_ID", DIV_ID)
    display(HTML('<div id="'+DIV_ID+'">loading OpenSheetMusicDisplay</div>'))

    print('xml length:', len(xml))

    script = """
    console.log("loadOSMD()");
    function loadOSMD() { 
        return new Promise(function(resolve, reject){

            if (window.opensheetmusicdisplay) {
                console.log("already loaded")
                return resolve(window.opensheetmusicdisplay)
            }
            console.log("loading osmd for the first time")
            // OSMD script has a 'define' call which conflicts with requirejs
            var _define = window.define // save the define object 
            window.define = undefined // now the loaded script will ignore requirejs
            var s = document.createElement( 'script' );
            s.setAttribute( 'src', "https://cdn.jsdelivr.net/npm/opensheetmusicdisplay@0.3.1/build/opensheetmusicdisplay.min.js" );
            //s.setAttribute( 'src', "/custom/opensheetmusicdisplay.js" );
            s.onload=function(){
                window.define = _define
                console.log("loaded OSMD for the first time",opensheetmusicdisplay)
                resolve(opensheetmusicdisplay);
            };
            document.body.appendChild( s ); // browser will try to load the new script tag
        }) 
    }
    loadOSMD().then((OSMD)=>{
        console.log("loaded OSMD",OSMD)
        var div_id = "{{DIV_ID}}";
            console.log(div_id)
        window.openSheetMusicDisplay = new OSMD.OpenSheetMusicDisplay(div_id);
        openSheetMusicDisplay
            .load({{data}})
            .then(
              function() {
                console.log("rendering data")
                openSheetMusicDisplay.render();
              }
            );
    })
    """.replace('{{DIV_ID}}',DIV_ID).replace('{{data}}',json.dumps(xml))
    display(Javascript(script))
    return DIV_ID

It also returns the ID of the div element, in case we want to use it for something later.

supersational commented 6 years ago

I can't link to a notebook here, but this PDF shows what it's capable of. showScore demo.pdf

Apart from a few hiccups with lyric character-encoding it looks pretty good!

(It can also fail silently to render scores without any notes)

mscuthbert commented 6 years ago

Very cool -- do you want to try to get it into a converter - output format? if so, my comments would be: try to be deterministic on the random id so it can't possibly fail (random concat w/ time.time() is generally good). Make sure that importing still works w/o IPython (of course this won't work). Then make it invokable with c.show('ipython.osmd') -- and I'll handle making config settings for it.

supersational commented 6 years ago

Yep, was hoping to! Would you recommend using ConverterIPython as a base class?

psychemedia commented 6 years ago

@supersational Lovely... works for me w/ demo score:

import random
selected_piece = random.choice(corpus.getPaths())
score = corpus.parse(selected_piece)

I notice that the score-part ID value in the MusicXML is being displayed - and wondered if there's an easy way to hide that via an OpenSheetMusicDisplay parameter or otherwise set it via music21?

mscuthbert commented 6 years ago

Probably something that the part.partId can change.

psychemedia commented 6 years ago

I have a demo notebook on Azure notebooks here, although it's quite slow to install music21 (a prebuilt binderhub demo would be quicker) : https://notebooks.azure.com/OUsefulInfo/libraries/gettingstarted/html/4.1.0%20Music%20Notation.ipynb

supersational commented 6 years ago

@mscuthbert any tips on how to make it invokable via s.show()? I can't find where the other classes register themselves for that to work.

mscuthbert commented 6 years ago

Ah, I was going to point to http://web.mit.edu/music21/doc/usersGuide/usersGuide_54_extendingConverter.html but apparently, we haven't demonstrated an output format. I'll look into it -- but probably won't have time till the weekend at earliest (first week of university teaching starting now).

mscuthbert commented 6 years ago

The weekend came very fast. Added to converter.subConverters.py -- a way of getting to the Chant-notation parser in music21.volpiano. It was already done, but I hadn't integrated it to converter.

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 style=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, *args, **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, *args, **keywords):
        print(self.getDataStr(obj, *args, **keywords))

once it's set, there are a few tests in converter/__init__.py that will need to pass.

willingc commented 6 years ago

@mscuthbert @psychemedia @supersational I saw some of the discussion on the mailing list re: Azure notebooks, cloud. I decided to give this a go using Binder (https://mybinder.org).

I've got all running but the rendering of the sheetmusic. Though I am able to install musescore and lilypond into the container, I'm getting an error

SubConverterException: To create PNG files directly from MusicXML you need to download MuseScore and put a link to it in your .music21rc via Environment.

Here's the branch from my fork: https://github.com/willingc/music21/tree/binderize Press the Launch Binder button in the README or click here https://mybinder.org/v2/gh/willingc/music21/binderize

supersational commented 6 years ago

hey @willingc, this is fantastic! I've got it up and running in a binder of my repo.

Still finishing off the PR for this, but it's working. You can create a new notebook in the root directory and execute the following:

from music21 import *
s = converter.parse("tinyNotation: 3/4 E4 r f# g=lastG trip{b-8 a g} c4~ c")
s.show('osmd')

Hope this is the right URL for sharing: https://mybinder.org/v2/gh/supersational/music21/opensheetmusicdisplay

P.S. the nice thing about this is you shouldn't have to install either musescore or lilypond for this to work, it's pure musicXML -> JS rendering!

psychemedia commented 6 years ago

@supersational - that suggested demo not working for me? I see an output message of the form:

OSMD-div-RANDOM-ID

but no notation display?

supersational commented 6 years ago

@psychemedia my bad, should work now with the latest commits (use the same link)

mscuthbert commented 6 years ago

Just wondering if this is still actively being worked on? Thanks!

supersational commented 6 years ago

@mscuthbert I'll have another go at finishing this over the next few days.. greatly appreciate the detailed comments you've made on the pull request, but have simply been busy.

psychemedia commented 6 years ago

Wondering if this stalled again?

willingc commented 6 years ago

@psychemedia I'm not sure if it has. The general build of the repo works on Binder: Binder

I'm not sure if which elements in the repo do not render correctly.

psychemedia commented 6 years ago

@willingc if I use that Binder, and try the following (taken from https://github.com/cuthbertLab/music21/pull/326#issuecomment-429867351) in a notebook:

!pip install matplotlib
import matplotlib
%matplotlib inline

import music21
s = music21.converter.parse("tinyNotation: 3/4 E4 r f# g=lastG trip{b-8 a g} c4~ c")
s.show('osmd')

I get Music21ObjectException: cannot support showing in this format yet: osmd (the PR is yet to be accepted...).

supersational commented 6 years ago

The PR is currently working fine but I don't have the time to finish the testing/refactoring unfortunately.

Link to it directly and it does work using the above code (matplotlib no longer needed): https://mybinder.org/v2/gh/supersational/music21.git/opensheetmusicdisplay

psychemedia commented 6 years ago

@supersational Okay - thanks, will do... My own music related demo notebooks are in various stages of broken and I was hoping to try to tidy them up this weekend with this renderer at the core.

willingc commented 6 years ago

@supersational If you give us push access to your branch for the PR, we could likely clean up the PR with tests/refactor.

supersational commented 6 years ago

@willingc that would be fantastic! Feel free to make any changes as you see fit, you should both have push access now.