museline: trying to add support for compressed MusicXML

Just a quick follow up to the last post about using Google Chart Tools to outline melodic contours from MusicXML files …

I wanted to add support for compressed MusicXML files in addition to the non-compressed ones. So far, the code I've got seems to be working with the two or three compressed MusicXML files from Wikifonia I tested.

Here's a screenshot below of A-Ha's "Take On Me", one of the best songs from the 80's with one of the absolute best videos, too! To make the graph I passed it to the app a la "http://localhost:8083/?mxml=http://static.wikifonia.org/1934/musicxml.mxl".

museline_aha_screenshot.png

Here's the video:

Keep in mind the contour script doesn't take repeats into account and that the entire melody repeats three times in the song.

Also, I don't like to make code downloadable if I'm still working on it because I don't want to junk up my web directory, but I'll paste everything essential below: the Google App Engine YAML file, the Python code, and the Jinja/HTML template.

YAML:

application: museline
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /stylesheets
  static_dir: stylesheets
- url: /.*
  script: museline.app
 
libraries:
- name: jinja2
  version: latest
- name: lxml
  version: latest

Python:

### museline.py
### 2012, Nitin Arora

### import modules
import urllib
from lxml import etree
import math
import re
import webapp2
import jinja2
import os
     
jinja_environment = jinja2.Environment(
  loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))
  
#####
class museline(webapp2.RequestHandler):
  def get(self):
    
    ### read MusicXML file
    try:
      url = self.request.get('mxml')
##      url = 'http://blog.humaneguitarist.org/uploads/i_heart_thee.xml' #test line
      if url[-4:] == '.xml': # uncompressed MusicXML
        readUrl = urllib.urlopen(url).read()
        
      else: # compressed MusicXML
      ### References:
        # http://stackoverflow.com/a/8858735
        # http://stackoverflow.com/questions/1313845/if-i-have-the-contents-of-a-zipfile-in-a-python-string-can-i-decompress-it-with
        from cStringIO import StringIO
        compressed = urllib.urlopen(url)
        compressedString = StringIO(compressed.read())
        import zipfile
        zipped = zipfile.ZipFile(compressedString, "r")

        archiveFiles = zipped.namelist()
##        self.response.out.write(archiveFiles) # test line
        for archiveFile in archiveFiles:
          if archiveFile[-4:] == ".xml" and "/" not in archiveFile:
            realXML = archiveFile
        extracted = zipped.open(realXML,'r')
        readUrl = extracted.read()

##      self.response.out.write(readUrl) # test line
                
    except:
      errorMessage = '''<pre>
You must pass an "mxml" parameter.
If you have but still see this message, then there is a problem accessing/reading the MusicXML file.
</pre>'''
      self.response.out.write(errorMessage)
      return

    ### setup pitch values
    notes = ['C','D','E','F','G','A','B']
    i = 0
    noteVals = {}
    for note in notes:
      if note == 'C' or note == 'F':
        noteVals[note] = i + 1
        i = i + 1
      else:
        noteVals[note] = i + 2
        i = i + 2

    ### parse MusicXML file
    parsed = etree.XML(readUrl)

    ### get basic descriptive metadata
    metadata = []
    elementList = ['work-title',
                   'work-number',
                   'movement-number',
                   'movement-title',
                   'creator[@type="composer"]',
                   'creator[@type="lyricist"]']
    for element in elementList:
      xpath = str(".//%s") %element
      if parsed.find(xpath) !=None:
        found = parsed.find(xpath).text
        att = re.match(r'(.*)type="(.*)\"', element)
        if att:
          element = att.group(2)
        if found:
          metadata.append((element,found))
##    self.response.out.write(metadata) # test line

    ### access part one tree                       
    part = parsed.find('.//part[@id="P1"]')
    pitches = part.findall('.//pitch')
##    self.response.out.write(str(len(pitches)) + " pitches.\n") # test line, number of notes (non-rests)
##    self.response.out.write(str(len(pitches)*.618) + " Golden Ratio.\n") # test line, maybe something for the future.

    ### put pitch values in a list
    pitchList = []
    i = 1
    for pitch in pitches:
      if pitch.find('.//alter') != None:
        alter = int(pitch.find('.//alter').text)
      else:
        alter = 0
      step = pitch.find('.//step')
      octave = int(pitch.find('.//octave').text)
      pitchPos = str('pitch: ' + str(i))
      pitchClassVal = ((int(noteVals[step.text]) + alter)) * .01
      pitchVal = ((int(noteVals[step.text]) + alter) + (octave * 12)) * .01
      label = (pitchPos, pitchVal, pitchClassVal)
      pitchList.append(label)
      i = i + 1

##    for pitch in pitchList: # test block
##      self.response.out.write(str(pitch)+'<br>')
      
    #data for the Jinja template  
    template_values = {
      'pitchList': pitchList,
      'url': url,
      'metadata': metadata}

    template = jinja_environment.get_template('museline.html')
    self.response.out.write(template.render(template_values)) #write data to the html template
  
app = webapp2.WSGIApplication([('/', museline)],
                              debug=True)

Template:

<!DOCTYPE HTML>
<!-- museline.html -->
<html>
  <head>
    <title>
      museline
    </title>
    <link type="text/css" rel="stylesheet" href="/stylesheets/style.css" />
    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
    <script type="text/javascript">
      google.load('visualization', '1', {packages: ['corechart']});
    </script>
    <script type="text/javascript">
      function drawVisualization() {
        // Create and populate the data table.
        var data = google.visualization.arrayToDataTable([
        ['pitch position', 'melodic contour'],
        {% for pitch in pitchList %}
          ['{{ pitch[0] }}', {{ pitch[1] }}],
        {% endfor %}
        ]);
       
        // Create and draw the visualization.
        new google.visualization.LineChart(document.getElementById('visualization')).
        draw(data, {curveType: "function",
          width: 800, height: 400,
        vAxis: {maxValue: 1}}
        );
      }
      google.setOnLoadCallback(drawVisualization);
    </script>
  </head>
  <body>
    <div id="visualization"></div>
    <p>Metadata:</p>
    <ul>
    {% for metadatum in metadata %}
      <li>{{ metadatum[0] }} : {{ metadatum[1] }}</li>
    {% endfor %}
      <li>URL: <a href="{{ url }}">{{ url }}</a></li>
    </ul>
  </body>
</html>
--------------

Related Content:

4 Comments

  1. Michael Cuthbert

    Hi Nitin — great work!  I don't want to plug too much my own work, but since you're a python guru, you might be interested in my python toolkit music21 (http://web.mit.edu/music21/) which among other things can already fully parse a compressed musicxml file (and can take in a URL as input to converter.parse) — you're likely to get the contour data you're interested with:
    from music21 import *
    c = converter.parse('http://static.wikifonia.org/1934/musicxml.mxl&#039😉
    notes = c.parts[0].notes

    for n in notes:
       do something to n.ps
    best,
    Myke

    Reply
    1. nitin (Post author)

      Hey Myke,

      Plug away man! This looks really cool.

      It was a good lesson for me to learn how to parse the compressed files as I did, but I will definitely be looking into your toolkit.

      Thanks!

      Reply
  2. Joe Berkovitz

    Hi Nitin —
    Just found your blog, and I love the stuff you're working on. I read your Beyond Images paper a while back and thought a lot of your observations were spot on. In fact, I've always been very interested in MIR/SMR and I feel that these capabilities would be right at home in our platform. At 2011 Boston Music Hackday we demonstrated melodic search and retrieval and it was pretty exciting stuff.
    I'd love to enable the Noteflight MusicXML APIs for you so that you can use truly web-based notation input and output in your research. LilyPond and MuseScore are both good programs, but the ease of having notation entry and display UIs embedded directly into a web page, sending and receiving MusicXML, is hard to beat once you've experienced it.
    Anyway, hope to connect with you sooner or later, and keep up all the great stuff.
    …Joe Berkovitz
    CEO/President
    Noteflight LLC

    Reply
    1. nitin (Post author)

      Hey Joe!

      Thanks for posting. I know of you through Noteflight and the MusicXML list.
      I'd love to try the Noteflight API!
      I'll message you off list and maybe we can discuss things further.

      thanks again,
      Nitin

      Reply

Leave a Comment

Your email address will not be published. Required fields are marked *

*