Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easy way to view musicXML in a browser #306

Open
supersational opened this issue May 31, 2018 · 24 comments

Comments

@supersational
Copy link

@supersational supersational commented May 31, 2018

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

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Aug 1, 2018

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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Member

@mscuthbert mscuthbert commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 28, 2018

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

@psychemedia

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Aug 28, 2018

@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

This comment has been minimized.

Copy link
Member

@mscuthbert mscuthbert commented Aug 28, 2018

Probably something that the part.partId can change.

@psychemedia

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 28, 2018

@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

This comment has been minimized.

Copy link
Member

@mscuthbert mscuthbert commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Member

@mscuthbert mscuthbert commented Aug 28, 2018

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

This comment has been minimized.

Copy link
Contributor

@willingc willingc commented Aug 29, 2018

@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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 29, 2018

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

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Aug 30, 2018

@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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Aug 30, 2018

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

@mscuthbert

This comment has been minimized.

Copy link
Member

@mscuthbert mscuthbert commented Oct 13, 2018

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

@supersational

This comment has been minimized.

Copy link
Author

@supersational supersational commented Oct 15, 2018

@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

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Nov 13, 2018

Wondering if this stalled again?

@willingc

This comment has been minimized.

Copy link
Contributor

@willingc willingc commented Nov 13, 2018

@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

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Nov 13, 2018

@willingc if I use that Binder, and try the following (taken from #326 (comment)) 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

This comment has been minimized.

Copy link
Author

@supersational supersational commented Nov 14, 2018

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

This comment has been minimized.

Copy link

@psychemedia psychemedia commented Nov 14, 2018

@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

This comment has been minimized.

Copy link
Contributor

@willingc willingc commented Nov 14, 2018

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

@supersational

This comment has been minimized.

Copy link
Author

@supersational supersational commented Nov 15, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.