Thu, 27 May 2010

Making Songs Swing with Windows Azure, Python, and the Echo Nest API

image

I’ve put together a sample application at http://swingify.cloudapp.net that lets you upload a song as an MP3 and produces a “swing time” version of it. It’s easier to explain by example, so here’s the Tetris theme song as converted by Swingify.

Background

The app makes use of the Echo Nest API and a sample developed by Tristan Jehan that converts any straight-time song to swing time by extended the first half of each beat and compressing the second half. I first saw the story over on the Music Machinery blog and then later in the week on Engadget.

I immediately wanted to try this with some songs of my own, and I thought others would want to do the same, so I thought I’d create a Windows Azure application to do this in the cloud.

How it Works

We covered this application on the latest episode of the Cloud Cover show on Channel 9 (to go live tomorrow morning – watch the teaser now). In short, the application consists of an ASP.NET MVC web role and a worker role that is mostly a thin wrapper around a Python script.

The ASP.NET MVC web role accepts an MP3 upload, stores the file in blob storage, and enqueues the name of the blob:


[HttpPost]
public ActionResult Create()
{
    var guid = Guid.NewGuid().ToString();
    var file = Request.Files[0];
    var account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
    var blob = account.CreateCloudBlobClient().GetContainerReference("incoming").GetBlobReference(guid);
    blob.UploadFromStream(file.InputStream);
    account.CreateCloudQueueClient().GetQueueReference("incoming").AddMessage(new CloudQueueMessage(guid));
    return RedirectToAction("Result", new { id = guid });
}

The worker role mounts a Windows Azure drive in OnStart(). Here I used the same tools and initialization code as I developed for my blog post “Serving Your Website From a Windows Azure Drive.” In OnStart():

var cache = RoleEnvironment.GetLocalResource("DriveCache");
CloudDrive.InitializeCache(cache.RootPath.TrimEnd('\\'), cache.MaximumSizeInMegabytes);

drive = CloudStorageAccount.FromConfigurationSetting("DataConnectionString")
    .CreateCloudDrive(RoleEnvironment.GetConfigurationSettingValue("DriveSnapshotUrl"));
drive.Mount(cache.MaximumSizeInMegabytes, DriveMountOptions.None);

Then there’s a simple loop in Run():

while (true)
{
    var msg = q.GetMessage(TimeSpan.FromMinutes(5));
    if (msg != null)
    {
        SwingifyBlob(msg.AsString);
        q.DeleteMessage(msg);
    }
    else
    {
        Thread.Sleep(TimeSpan.FromSeconds(5));
    }
}

The meat of the application is in the implementation of SwingifyBlob(), which calls out to python.exe on the mounted Windows Azure drive:

public void SwingifyBlob(string guid)
{
    var account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
    var blobs = account.CreateCloudBlobClient();
    var blob = blobs.GetContainerReference("incoming").GetBlobReference(guid);

    var tempdir = RoleEnvironment.GetLocalResource("temp").RootPath;
    string filepath = Path.Combine(tempdir, guid + ".mp3");
    blob.DownloadToFile(filepath);

    var localPath = drive.LocalPath + @"\";

    var process = new Process()
    {
        StartInfo = new ProcessStartInfo(
            Path.Combine(localPath, @"python\python.exe"),
            string.Format(@"swinger.py ""{0}""", filepath))
        {
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            WorkingDirectory = localPath
        }
    };

    process.StartInfo.EnvironmentVariables["PATH"] = Path.Combine(localPath, @"python");
    process.StartInfo.EnvironmentVariables["TEMP"] = tempdir;
    process.StartInfo.EnvironmentVariables["ECHO_NEST_API_KEY"] =
        RoleEnvironment.GetConfigurationSettingValue("EchoNestApiKey");

    DataReceivedEventHandler logit = (s, e) =>
        {
            if (e.Data != null)
            {
                Debug.WriteLine(e.Data);
            }
        };

    process.OutputDataReceived += logit;
    process.ErrorDataReceived += logit;

    process.Start();
    process.BeginOutputReadLine();
    process.BeginErrorReadLine();
    process.WaitForExit();

    var outfilepath = Path.Combine(tempdir, guid + "_swing+33.mp3");
    var outblob = blobs.GetContainerReference("output").GetBlobReference(guid);
    if (File.Exists(outfilepath))
    {
        outblob.Properties.ContentType = "audio/mp3";
        using (var reader = File.OpenText(Path.Combine(tempdir, guid + "_metadata.txt")))
        {
            var title = reader.ReadLine().Trim();
            if (!string.IsNullOrEmpty(title))
            {
                outblob.Metadata["title"] = title;
            }
            var artist = reader.ReadLine().Trim();
            if (!string.IsNullOrEmpty(artist))
            {
                outblob.Metadata["artist"] = artist;
            }
        }
        outblob.UploadFile(outfilepath);
    }
    else
    {
        outblob.Properties.ContentType = "text/plain";
        outblob.UploadText("Sorry... we failed to swingify this song.  Try a different one?");
    }

    foreach (var filename in Directory.GetFiles(tempdir))
    {
        File.Delete(filename);
    }
}

The first part of this code sets up the call to the python.exe process by downloading the blob to local storage, making sure the path environment variable is correct, the Echo Nest API key is set, and that the working directory is right.

The second part of the method uploads the processed MP3 and parses out the title and artist written to disk by the Python script.

Running Python in Windows Azure

For the most part, all I need to do to run Python in Windows Azure was copy my local Python directory (c:\python26) to a Windows Azure drive and mount that in my worker role. There was one gotcha, however, which stumped me for almost a day. Python is actually not copy-deployable. I needed to copy python26.dll onto the drive as well (in the Python folder, which you’ll note I added to the path). On my local machine, this DLL was in %windir%\system32.

You might also check out the Portable Python project, which seems like an easier (and better supported) way to make sure your Python distribution can actually run in Windows Azure.

Other Swingified Songs

If you wanted more examples of what you can make with http://swingify.cloudapp.net, here are some that I’ve made:

Also be sure to check out the original post on the Music Machinery blog for a few more examples.

Make Some Music

Go to http://swingify.cloudapp.net and make the songs in your own MP3 collection swing, and please send me a tweet (@smarx) with the good ones you come up with!