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
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
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
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!
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