I’m building a string instrument that makes music with the strings’ harmonic tones as well as their fundamentals.  And, for hard-to-explain reasons, it’s going to have more than 12 fundamental tones per octave.  So I needed to figure out which tuning system has the best overlap between its equally-tempered fundamentals and their respective overtone sequences.  This is exactly the kind of massive combinatorics problem I don’t know how to solve in my head.  So I wrote some software to help me visualize it.

Temperaments v.s Overtones

It’s written in SVG and Javascript.  Firefox: definitely.  IE: definitely not.  I haven’t yet checked if Safari and Chrome support these features.

http://web.media.mit.edu/~hellyeah/crunch.svg

The + and – buttons* change the number of notes in the temperament. Each circle in the top row is a note in the selected equal temperament.  Each row is an overtone series.

Hover over a note to see some information about it.  Click on any note to see the distribution of notes that closely match simple musical intervals. Lighter green indicates a better match.

Enjoy!

* If clicking the buttons doesn’t seem to be working, try clicking their edges.  And be aware that it takes a second or 2 to re-render the whole grid.

iTunesCaster

June 2, 2009

Okay. This may be illegal. Or maybe not. Just don’t use it for illegal filesharing and I’m probably fine.

The iTunesCaster 0.1 turns your iTunes playlists into podcasts that can be easily shared with friends or the general public. Think of it as a nice way to make podcasts, not a way to share MP3s with your friends.

Its usage is still a little geeky at this point. You’ll need iTunes (a-doy), Apache HTTP server, and a newish Python interpreter. Macs will have all of that. Windows users will need to install all 3. For Linux users, are there any open-source music players that use the same XML library format as iTunes? If you know of one, drop me a line.

You don’t need to have those 3 programs on the same computer, so long as they can see each other through the filesystem.

Here is the code:

"""
++++++++++ iTunesCaster 0.1 [super alpha] ++++++++++ 

This script turns your iTunes playlists into podcasts.  

Usage:
  1. Create a playlist for a friend in iTunes
  2. Run this script
  3. Give friend the new podcast URL (see Setup below)
    3a. Bask in gushing praise as your friend easily downloads selected files from your computer.
    3b. Take a moment to reflect that this is totally illegal if done wrong.

Requirements:
  1. Python 2.4.x or newer
  2. mod_python 3.2 or newer
  3. Apache HTTPD server 2.0.x or newer
  4. iTunes 6 or newer

Setup:
  0. first off, iTunes, Apache, and your music library must be visible to one another.  They can be on the same machine or on separate network shares.
  1. Update the values in the conf_d dictionary below.
    set iTunesLib_filePath_str to the file path for your "iTunes Music Library.xml" file
    set iTunesDir_filePath_str to the file path for your iTunes Music Folder
    set TracksDir_urlPath_str to the URL path mapped to your iTunes Music Folder in Apache's httpd.conf file (see my httpd.conf example below)
    set podcastDir_filePath_str to the file path where this script will write the podcast files.  This path should be available via the Web. (see my httpd.conf example below)
  2. run script, see new podcasts (XML files) appear in the folder named by podcastDir_filePath_str
  3. Test it.  Copy a podcast URL and subscribe to it in iTunes.  If you can't download the files, check your paths in conf_d, httpd.conf, and in the podcast.

  +++++++++ PERTINENT PART OF MY HTTPD.CONF +++++++++
  # this is what makes this file work on my site.  Note the correlation between the directories here and those in the script's configuration dictionary ("conf_d") 

    DocumentRoot C:server/websites/podcast.nervebox.com
    ServerName podcast.nervebox.com
    CustomLog logs/podcast_nervebox_com.log combined
    DefaultType text/html

        Options Indexes MultiViews
        IndexIgnore . ..
        AllowOverride None
        Order allow,deny
        Allow from all
        AddHandler mod_python .py
        PythonHandler server
       	PythonPath "[  (Put your own Python Path vals in here)  ]"
        PythonDebug On

    Alias /mp3s "C:server/data/music"

        Options Indexes MultiViews
        IndexIgnore . ..
        Order allow,deny
        Allow from all

"""

import sys, os
from mod_python import apache
from xml.dom import minidom

def handler(req):
  global tracks_d
  global playlists_l
  global req_global
  global debugOut_str
  global conf_d
  req_global = req
  debugOut_str = ""
  conf_d = {
    "iTunesLib_filePath_str":"C:Documents and SettingsAdministratorMy DocumentsMy MusiciTunesiTunes Music Library.xml",
    "iTunesDir_filePath_str":"file://localhost/C:/server/data/music/",
    "TracksDir_urlPath_str":"http://podcast.nervebox.com/mp3s/",
    "podcastDir_filePath_str":"C:server/websites/podcast.nervebox.com/podcasts"
  }
  req.content_type="text/plain"
  iTunesLib().parseMusicLibrary()
  podcast().scanPlaylists()
  req.write(debugOut_str)
  return apache.OK

class iTunesLib:
  def __init__(self):
    global conf_d
    self.XMLPath_str = conf_d["iTunesLib_filePath_str"]
    self._tracks_d = {}
    self._playlists_l = []
  def parseMusicLibrary(self):
    global tracks_d
    global playlists_l
    fields_l = ["Track ID","Name","Artist","Total Time","Date Modified","Location"]
    itml_doc = minidom.parse(self.XMLPath_str)
    plist_node = itml_doc.childNodes.item(1).childNodes.item(1)
    for ni in range(plist_node.childNodes.length):
      if plist_node.childNodes.item(ni).nodeName == "dict": # filter for the 1 dict (tracks) in plist
        tracks_node = plist_node.childNodes.item(ni)
      if plist_node.childNodes.item(ni).nodeName == "array": # filter for the 1 array (playlists) in plist
        playlists_node = plist_node.childNodes.item(ni)
    # get tracks
    trackNodes_coll = tracks_node.getElementsByTagName("dict") # collect references to all track defs in an HTMLCollection
    for ti in range(trackNodes_coll.length): # loop through all tracks
      thisTrackData_coll = trackNodes_coll[ti].childNodes # reference to this track node
      for ttdi in range(thisTrackData_coll.length): # loop through properties of each track
        if thisTrackData_coll[ttdi].nodeName == "key":
          if fields_l.count(thisTrackData_coll[ttdi].firstChild.data)>0:
            if thisTrackData_coll[ttdi].firstChild.data== "Track ID": # should will first property of the track data.  so we can set this here
              trackID_str = str(thisTrackData_coll[ttdi+1].firstChild.data)
              self._tracks_d[trackID_str] = {"Track ID":"", "Name":"", "Artist":"", "Total Time":"", "Date Modified":"", "Location":""}
            try:
              self._tracks_d[trackID_str][str(thisTrackData_coll[ttdi].firstChild.data)] = str(thisTrackData_coll[ttdi+1].firstChild.data)
            except:
              self._tracks_d[trackID_str][str(thisTrackData_coll[ttdi].firstChild.data)] = "bugginess: characters out of ascii range"
    tracks_d = self._tracks_d
    # get playlists
    playlistNodes_coll = playlists_node.childNodes
    for pni in range(playlistNodes_coll.length):
      if playlistNodes_coll[pni].nodeName == "dict": # filter out textNodes
        tempPlaylist_d = {"name":"","trackIDs":[]}
        thisPlaylist_coll = playlistNodes_coll[pni].childNodes
        for ppi in range(thisPlaylist_coll.length):
          if thisPlaylist_coll[ppi].nodeName == "key" and thisPlaylist_coll[ppi].firstChild.data == "Name":
            tempPlaylist_d["name"] = str(thisPlaylist_coll[ppi+1].firstChild.data)
          if thisPlaylist_coll[ppi].nodeName == "array":
            dicts_l = thisPlaylist_coll[ppi].getElementsByTagName("dict")
            for di in range(dicts_l.length): # loop through the dicts in the playlist array
              integer_node = dicts_l[di].getElementsByTagName("integer")[0]
              tempPlaylist_d["trackIDs"].append(str(integer_node.firstChild.data))
        self._playlists_l.append(tempPlaylist_d)
    playlists_l = self._playlists_l

class podcast:
  def __init__(self):
    global conf_d
    self.TracksDir_urlPath_str = conf_d["TracksDir_urlPath_str"]
    self.iTunesDir_filePath_str = conf_d["iTunesDir_filePath_str"]
    self.podcastDir_filePath_str = conf_d["podcastDir_filePath_str"]

  def scanPlaylists(self):
    global playlists_l
    for pli in range(len(playlists_l)): # loop through playlists, making a podcast for each
      self.makeFile(playlists_l[pli])

  def makeFile(self, playlist_ref):
    global debugOut_str
    global tracks_d
    impl = minidom.getDOMImplementation()# make podcast doc object
    podcast_doc = impl.createDocument(None, "rss", None)
    rss_node = podcast_doc.getElementsByTagName("rss").item(0)
    self.setAttribute(rss_node, "xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
    self.setAttribute(rss_node, "version", "2.0")
    # populate podcast doc object
    channel_node = podcast_doc.createElement("channel")
    rss_node.appendChild(channel_node)
    self.addSimpleTag(podcast_doc,channel_node,"title",playlist_ref["name"])
    self.addSimpleTag(podcast_doc,channel_node,"link","http://www.nervebox.com")
    self.addSimpleTag(podcast_doc,channel_node,"description","Nervebox Experimental iTunes-podcast Bridge")
    self.addSimpleTag(podcast_doc,channel_node,"itunes:summary","Nervebox Experimental iTunes-podcast Bridge")
    self.addSimpleTag(podcast_doc,channel_node,"itunes:subtitle","Nervebox Experimental iTunes-podcast Bridge")
    self.addSimpleTag(podcast_doc,channel_node,"language","en-us")
    self.addSimpleTag(podcast_doc,channel_node,"lastBuildDate","Fri, 9 Dec 2005 09:00:00 EST") # date should be dynamic: date of script execution
    self.addSimpleTag(podcast_doc,channel_node,"itunes:author","Nervebox Studio")
    # add podcast items
    for ii in range(len(playlist_ref["trackIDs"])):
      if tracks_d.has_key(playlist_ref["trackIDs"][ii]): # omit playlist items that are not found among tracks
        thisItem_node = podcast_doc.createElement("item")
        thisTrack_l = tracks_d[playlist_ref["trackIDs"][ii]]
        location_str = thisTrack_l["Location"].replace(self.iTunesDir_filePath_str,self.TracksDir_urlPath_str)
        self.addSimpleTag(podcast_doc,thisItem_node,"title",thisTrack_l["Name"] + "-" + thisTrack_l["Artist"])
        self.addSimpleTag(podcast_doc,thisItem_node,"pubDate",thisTrack_l["Date Modified"])
        self.addSimpleTag(podcast_doc,thisItem_node,"itunes:author",thisTrack_l["Artist"])
        self.addSimpleTag(podcast_doc,thisItem_node,"guid",location_str)
        enclosure_node = self.addSimpleTag(podcast_doc,thisItem_node,"enclosure")
        self.setAttribute(enclosure_node, "url", location_str)
        self.setAttribute(enclosure_node, "length", "100000") # of course this is not the correct length.  works fine anyway.
        self.setAttribute(enclosure_node, "type", "audio/x-mp3")
        channel_node.appendChild(thisItem_node)
    # serialize podcast doc object to XML(!)
    debugOut_str = debugOut_str + podcast_doc.toxml("utf-8") + "

"
    # write XML to filesystem
    podcastFilePath_str = self.podcastDir_filePath_str + playlist_ref["name"] + ".xml"
    podcast_file = file(podcastFilePath_str, 'w')
    podcast_file.write(podcast_doc.toxml("utf-8"))
    podcast_file.close()
    podcast_doc.unlink()

  def setAttribute(self,target_node,name_str,val_str):
    document = minidom.Document()# make podcast doc object
    _att= document.createAttribute(name_str);
    _att.value = val_str
    target_node.attributes.setNamedItem(_att)

  def addSimpleTag(self,doc_ref,parent_node,tagName_str,content_str=False):
    newTag_node = doc_ref.createElement(tagName_str)
    if content_str:
      t_node = doc_ref.createTextNode(content_str)
      newTag_node.appendChild(t_node)
    parent_node.appendChild(newTag_node)
    return newTag_node

Future development: (feel free to jump in here)

  • Faster XML parser! It currently takes over a minute to parse my 4000-track iTunes library.
  • Easy installers for Windows and MacOS
  • On Windows, uses Python’s HTTP server if none is found.
  • A fancy new desktop interface:
    • shows user’s current IP address and URLs for the podcasts.
    • allows user to select which playlists to convert to podcasts.
    • runs as desktop program (with Tkinter) instead of requiring Apache and mod_python

Some of the dorkier among you might believe that this is a perfect place to use XSLT. Au Contraire! It’s true that this program simply transforms one flavor of XML file into a set of other-flavored XML files. But the logic required seems beyond any XSLT-fu that I possess. If you’d like to give it a hack, go right ahead. Tell me how it went. :)

NerveMail

June 1, 2009

I just want to claim dibs: I may have written the very first rich and useful AJAX application.

NerveMail main window

Back in 2001, years before GMail, or Firefox, or even the coining of the term AJAX, I built a webmail system that looked and *worked* like a desktop client.  There were no pages, just a client program (in JavaScript) that managed a rich GUI and dynamically loaded data and libraries.  It had some interesting features like server push and an error reporting system stored JavaScript exception data in cookies so errors could be reported in the event of a crash.   It was also a pretty mature mail client.

options window

It worked more of less the same way Google Docs or other application-in-a-browser systems work.  Except this was 2001 and the only browser capable of handling it was Mozilla 0.9x.

After endless tweaking I finally shared this in 2002:

http://www.mozillazine.org/talkback.html?article=2716

What seems obvious now – that rich clients are as good an idea on the Web as they are on the desktop – took more than a paragraph to explain in 2002.  And in 2002, at the bottom of the crash, that last thing anyone wanted to hear about was “a new technology that was going to revolutionize the Web”.

NerveMail system diagram

So, while AOL offered me a job developing it for them in Mountain View, I never directly turned it into money.  There certainly weren’t any companies looking out for this technology they didn’t yet know existed.  The release of  Google’s GMail had the simultaneous effect of making everybody get it and also making NerveMail far less relevant.

I’ve extended that codebase and still use it to build great things that can’t be built with Dojo or EXT.  And I believe I was there before anyone.  But I never got any props or cash.  So I used to be a little touchy about it.

Just staking my claim.

Follow

Get every new post delivered to your Inbox.