blog.humaneguitarist.org

museline: trying to add support for compressed MusicXML

[Sat, 05 May 2012 21:36:59 +0000]
Just a quick follow up to the last post [http://blog.humaneguitarist.org/2012/05/03/museline-charting-melodic-contours-via-web-service/] 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 [http://www.wikifonia.org] I tested. Here's a screenshot below of A-Ha [http://a-ha.com/]'s "Take On Me [http://www.wikifonia.org/node/1934#/C/0/1]", 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". IMAGE: "museline_aha_screenshot.png"[http://blog.humaneguitarist.org/uploads/museline_aha_screenshot.png] Here's the video: IFRAME: http://www.youtube.com/embed/djV11Xbc914 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/blog/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>

COMMENTS

  1. nitin [2012-06-09 04:00:19]

    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

  2. Joe Berkovitz [2012-06-09 01:08:23]

    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

  3. nitin [2012-05-08 03:16:46]

    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!

  4. Michael Cuthbert [2012-05-08 01:19:09]

    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') notes = c.parts[0].notes for n in notes: do something to n.ps best, Myke