blog.humaneguitarist.org

discoveries in digital audio, music notation, and information encoding

Archive for the ‘museline’ tag

museline: trying to add support for compressed MusicXML

4 comments

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:

Written by nitin

May 5th, 2012 at 5:36 pm

museline: charting melodic contours via web service

leave a comment

In the last post, I mentioned I was playing with Google App Engine and Google Chart Tools.

Last night, with some silly movie streaming in the background, I was in bed tinkering with a little idea that I'm sure has been done a-thousand times already and that may be built into high end music notation applications. But it hasn't been done by anyone as stoopid as me!

:P

What I did was whip up a little App Engine/Python app where one can pass it a partwise MusicXML file and it will use Google Chart Tools to create a little line chart of the melodic contour of the first <part> element.

Here's a screenshot below of the results using the MusicXML sample file available on the MakeMusic site of Schumann's "Im wunderschönen Monat Mai" from the Dichterliebe. The app has an "mxml" parameter that tells it which MusicXML file to use a la "http://localhost:8083/?mxml=http://downloads2.makemusic.com/musicxml/Dichterliebe01.xml".

 

I've embedded a really nice performance on YouTube if anyone wants to follow along. The contour graph represents the vocal part only.

 

Now, this is just a start. There's a lot of work to do if I pursue this. For starters, I'd like to make the chart synced with an audio/video recording. I don't know if I can do that with Chart Tools, but probably with the <canvas> element if nothing else. Also, I haven't tried this yet with any non-homophonic parts. Anyway, it's a start and it's kinda fun.

I tried to add another line for the actual pitch class contour but it wasn't as interesting to look at as the melodic contour so I disabled that "feature". By pitch class, I mean I was using octave equivalency so that all "C" notes, for example, were plotted at the exact same vertical position as opposed to the screenshot above where two "C" notes an octave apart would have different vertical points on the graph to depict the intervallic difference.

As far as plotting the notes, I ignored rests and durations. I just plotted the pitches as below, starting with "C" with a value of "1" and with the "B" a seventh up from that "C" receiving a "12".

  • C : 1
  • D : 3
  • E : 5
  • F : 6
  • G : 8
  • A : 10
  • B : 12

This way a "C-sharp" and "D-flat" receive a score of "2", for example, because they lie between "C as 1" and "D as 3".

In MusicXML, the <step> element has the note name and the optional <alter> element, which is a number, tells you if it's sharp or flat, etc. The numerical <octave> element tells you what octave range the pitch is in.

So what I'm doing is pulling out the <step> value and converting it to a number as above, adding the <alter> value (a flat is a negative number), and then multiplying adding that sum to 12 times the <octave> value. Then, I multiple the value by ".01" just to reduce the number because I want the graph's vertical limit to be a small number even though this shouldn't change the contour itself.

Last, I'm trying to pull some basic descriptive metadata if they are present in the MusicXML file and show it below the graph.

Maybe I'll do more with this later. Just goofin' for now.

--------------

Related Content:

Written by nitin

May 3rd, 2012 at 3:55 pm

Switch to our mobile site