blog.smarx.com

Steve Marx's blog about cloud development

Adaptive Streaming with Windows Azure Blobs and CDN

In this post, I’ll show you how to use Windows Azure Blobs and the Windows Azure CDN to deliver adaptive streaming video content to your users in a format compatible with Silverlight’s Smooth Streaming player. For those who just want to try it out, head over to the Adaptive Streaming with Windows Azure Blobs Uploader project on Code Gallery. The instructions there will get you going.

Understanding Smooth Streaming

Before we get into the details of how adaptive streaming works on top of Windows Azure Blobs, it’s necessary to understand what Smooth Streaming is and how it works.

Smooth Streaming is Microsoft’s HTTP-based adaptive streaming protocol. As Alex Zambelli’s wrote in his excellent “Smooth Streaming Technical Overview”:

Adaptive streaming is a hybrid delivery method that acts like streaming but is based on HTTP progressive download. It's an advanced concept that uses HTTP rather than a new protocol.

In a typical adaptive streaming implementation, the video/audio source is cut into many short segments ("chunks") and encoded to the desired delivery format… The encoded chunks are hosted on a HTTP Web server. A client requests the chunks from the Web server in a linear fashion and downloads them using plain HTTP progressive download.

The "adaptive" part of the solution comes into play when the video/audio source is encoded at multiple bit rates, generating multiple chunks of various sizes for each 2-to-4-seconds of video. The client can now choose between chunks of different sizes. Because Web servers usually deliver data as fast as network bandwidth allows them to, the client can easily estimate user bandwidth and decide to download larger or smaller chunks ahead of time. The size of the playback/download buffer is fully customizable.

In other words, HTTP-based adaptive streaming is about taking a source video, encoding it into lots of small chunks at various bitrates, and then letting the client play back the most appropriate chunks (based on available bandwidth).

If you’ve ever looked at IIS Smooth Streaming content, though, you’ll notice that there aren’t lots of tiny chunks. There are a few, fairly large video files. Alex explains this too:

IIS Smooth Streaming uses the MPEG-4 Part 14 (ISO/IEC 14496-12) file format as its disk (storage) and wire (transport) format. Specifically, the Smooth Streaming specification defines each chunk/GOP as an MPEG-4 Movie Fragment and stores it within a contiguous MP4 file for easy random access. One MP4 file is expected for each bit rate. When a client requests a specific source time segment from the IIS Web server, the server dynamically finds the appropriate Movie Fragment box within the contiguous MP4 file and sends it over the wire as a standalone file, thus ensuring full cacheability downstream.

Smooth Streaming files are quite literally all those little chunks concatenated together. I encourage you to read Alex’s entire article to understand the exact file format and wire format.

The key insight for our purpose is that to the client, Smooth Streaming content is just many small video chunks. The beauty of this model is that Smooth Streaming works great with CDNs and caches in between the client and the server. To the client, all that matters is that small chunks of video are being served from the appropriate URLs.

Using Windows Azure Blobs as an Adaptive Streaming Host

Windows Azure Blobs can serve specific content at configurable URLs, which as we’ve seen is the only requirement to provide clients with an adaptive streaming experience. To sweeten the deal, there’s built-in integration between Windows Azure Blobs and the Windows Azure CDN.

All that’s left for us to do is to figure out the set of URLs a Smooth Streaming client might request and store the appropriate video chunks at those URLs. There are two files that will help us do that:

  • The server manifest (*.ism) – This is a SMIL file that maps video and audio tracks to the file that contains them and the bitrates at which they were encoded.
  • The client manifest (*.ismc) – This is an XML file that specifies to the client which bitrates and timestamps are available. It also specifies the URL template clients should use to request chunks.

The combination of these two files tells us everything we need to know to extract the video chunks and store them in Widows Azure Blobs.

The Adaptive Streaming with Windows Azure Blobs Uploader code first reads the server manifest and keeps track of the mapping of bitrate and content type (video or audio) to tracks within files. Then it reads the client manifest and generates all the permutations of bitrate, content type, and timestamp. For each of these, it looks up the appropriate track of the appropriate file, extracts that chunk from the file, and stores it in blob storage according to the URL template in the client manifest.

The code’s not too complicated, and you can find it in the Code Gallery project in SmoothStreamingAzure.cs.

Prior Work

After I patted myself on the back for coming up with this brilliant scheme, it was pointed out to me that Alden Torres blogged about this back in December 2009. He used a tool on Codeplex called MP4 Explorer, which has a feature that allows uploading to blob storage. That tool reads the source MP4 files themselves and derives the chunks from there (as opposed to my approach, which reads the client manifest).

The two big reasons I decided to write my own code for this were that I wanted a command-line tool and that I wanted to upload the blobs in parallel. I was able to cut down the upload time for the Big Buck Bunny video from around three hours (as Alden mentions in his post) to around thirty minutes simply by doing the uploads in parallel.

Shortcomings of This Approach

To the client doing simple playback, there’s no difference between IIS Smooth Streaming (hosted by IIS Media Services) or Adaptive Streaming with Windows Azure Blobs. However, to the content owner and to the server, there are significant differences:

  • With IIS on the server, scenarios that require server intelligence are possible (like real-time transcoding or encryption).
  • There are fewer files to manage with IIS (since it keeps all the content in a small number of files). This makes copying files around and renaming them much simpler.
  • As future features (like fast-forward and new targets like the iPad) come out, all you need to do is update IIS Media Services to get the new functionality. With a solution like the one described in this post, you’ll need to reprocess existing content.
  • Because the manifest formats for IIS Smooth Streaming are actively evolving, there’s no guarantee that my code will work correctly with future Smooth Streaming clients and content.

Specifically, there are a few features of IIS Smooth Streaming that my code doesn’t handle today:

  1. Trick play (fast-forward and rewind). This is supported under IIS by extracting keyframes from the video. My code doesn’t support extracting these keyframes.
  2. Live Smooth Streaming. Handling a live event (where the manifest is changing and the chunks include extra hints about future chunks) isn’t supported in my code.

The Windows Azure team is still committed to running full IIS Media Services within Windows Azure web roles in the future.

Get the Tool

If you’d like to host Smooth Streaming content in Windows Azure Blobs, please check out the Adaptive Streaming with Windows Azure Blobs Uploader project on Code Gallery, where you can download the command-line tool as well as the full source code.

Sample

Here's Big Buck Bunny served from cdn.blog.smarx.com:

 

[UPDATE 11:29pm] The name of the tool has been changed to “Adaptive Streaming with Windows Azure Blobs Uploader”.


Deleting Windows Azure Queue Messages: Handling Exceptions

This week’s episode of Cloud Cover (scheduled to go live tomorrow morning) is all about Windows Azure queues. It’s a bit of a long episode, but there’s a lot of interesting technical content in there. During the show, a detail came up as Ryan and I were discussing queues and concurrency. At the time, I wasn’t sure exactly what guidance to give, so I committed to following up before the show went live.

To understand the situation, remember that Windows Azure employs reliable queueing, meaning that it guarantees no message is lost without being handled by a consumer. That means that consuming queue messages is a two step process. First, the consumer dequeues the message, specifying a visibility timeout. At this point, the message is invisible and can’t be retrieved by other consumers. When the consumer is finished with the message, it deletes it. If, however, the consumer is unable to finish processing the message, the visibility timeout will expire, and the message will reappear on the queue. This is what guarantees the message will eventually be handled.

This leads us to the situation Ryan and I were discussing on the show. The scenario is as follows:

  1. Instance #1 dequeues a message and starts working on it.
  2. The message’s visibility timeout expires, making it visible again on the queue.
  3. Instance #2 dequeues the message and starts working on it.
  4. Instance #1 finishes working on the message and tries to delete it.

At that last step, an error will be returned from the queue service. This is because the first instance no longer “owns” the message; it’s already been delivered to another instance. (The underlying mechanism is a pop receipt which is invalidated by the second dequeueing of the message.)

The question I couldn’t answer on the fly during the show was how to accurately detect this error and handle it in code. After some discussion with the storage team, this is the .NET code I’m recommending people use to identify this error:

try
{
    q.DeleteMessage(msg);
}
catch (StorageClientException ex)
{
    if (ex.ExtendedErrorInformation.ErrorCode == "MessageNotFound")
    {
        // pop receipt must be invalid
        // ignore or log (so we can tune the visibility timeout)
    }
    else
    {
        // not the error we were expecting
        throw;
    }
}

It would be nice if the storage client library included a constant for “MessageNotFound,” as it does for a number of other common error codes, but we can be confident that’s the right string by consulting the documentation on Queue Service Error Codes.

Note that I’m not just checking for the HTTP 404 status code, because that could mean some other things (like an incorrect queue name). Looking for the “MessageNotFound” error code is more specific and thus better to use.

Now go watch the latest Cloud Cover episode!


Pivot, OData, and Windows Azure: Visual Netflix Browsing

netflixpivot.cloudapp.net screenshot The PivotViewer Silverlight control shipped this morning, which means you can now embed a Pivot collection (with great UI) directly in a web page. Pivot is fantastic for sorting, filtering, and browsing large numbers of items.

I’ve put together my own example of using the new PivotViewer control at http://netflixpivot.cloudapp.net. It lets you browse the top ~3,000 movies that Netflix has available to stream online. I really encourage you to click through to the demo… it’s a fantastic way to find a movie to watch.

Technical Overview

The demo is built on Windows Azure and consists of a web role (which serves the web page itself), a worker role (which creates the Pivot collection once every hour or so), and blob storage, which hosts the collection and the Silverlight control (all behind the Windows Azure CDN). The data comes from Netflix’s OData feed.

I only had to write about 500 lines of code to make this all happen, and I suspect that number would go down if I used the Pauthor library (which I didn’t have access to when I wrote this demo).

Creating the Pivot Collection

The Pivot collection is created by a worker role that only has a single instance. It takes more than an hour to process the latest Netflix feed into the form needed for Pivot. I could have parallelized some of this and spread the load across multiple instances, but the feed changes infrequently, so I’m not in any particular rush to get the work done. Using a single instance makes the code very simple, because everything happens locally on a single disk, but I have also built Pivot collections in the past using a large number of instances.

The collection is created on the local disk in NetflixPivotCreator.cs. The first step is loading all the available titles from the OData feed. To generate the NetflixCatalog class, I just right-clicked on the worker role’s references and added a service reference to http://odata.netflix.com/Catalog.

var context = new NetflixCatalog(new Uri("http://odata.netflix.com/Catalog"));
DataServiceQueryContinuation<Title> token = null;
var response = ((from title in context.Titles 
                 where title.Instant.Available && title.Type == "Movie"
                 orderby title.AverageRating descending select title)
                as DataServiceQuery<Title>)
               .Expand("Genres,Cast,Directors")
               .Execute() as QueryOperationResponse<Title>;
int count = 0;
var ids = new HashSet<string>();
do
{
    if (token != null)
    {
        response = context.Execute<Title>(token);
    }
    foreach (var title in response)
    {
        if (ids.Add(title.Id))
        {
            if (count < howMany)
            {
                yield return title;
            }
            count++;
        }
    }
    token = response.GetContinuation();
}
while (token != null && count < howMany);

The next step is to download each title’s box art and create a Deep Zoom images out of it. This is a simplified version of that code:

Parallel.ForEach(GetTopInstantWatchTitles(3000),
    new ParallelOptions { MaxDegreeOfParallelism = 16 },
    (title) =>
{
    var boxArtUrl = title.BoxArt.HighDefinitionUrl ?? title.BoxArt.LargeUrl;
    var imagePath = string.Format(@"{0}\images\{1}.jpg", outputDirectory, title.Id.ToHex());
    new WebClient().DownloadFile(boxArtUrl, imagePath);
    new ImageCreator().Create(imagePath, string.Format(@"{0}\output\{1}.xml", outputDirectory, title.Id));
});

Note the use of the Task Parallel Library, which is an awesome way to make multi-threaded programming easy.

From there, there are just one more line to create the full Deep Zoom collection:

new CollectionCreator().Create(
    titles.Select(t => string.Format(@"{0}\output\{1}.xml", outputDirectory, t.Id.ToHex())).ToList(),
    string.Format(@"{0}\output\collection-{1}.dzc", outputDirectory, suffix));

At this point, I’m ready to create the actual Pivot collection (a .cxml file that contains all the details about the movies). Check out the source code in the method CreateCxml to see how this is done. (It’s just XML generation, probably made much simpler if I use the Pauthor library.

Storing the Collection in Blob Storage

Once the collection has been created, the worker role uploads it to blob storage, using some rather mundane code. I’m including it here because it demonstrates a few important details: parallelizing uploads for performance, setting the correct content type on blobs, and setting the cache control header when using the CDN. Note also that the main .cxml file is uploaded last, to ensure that it’s not served to users before all the supporting files have been uploaded.

private void UploadDirectoryRecursive(string path, CloudBlobContainer container)
{
    string cxmlPath = null;

    // use 16 threads to upload
    Parallel.ForEach(EnumerateDirectoryRecursive(path),
        new ParallelOptions { MaxDegreeOfParallelism = 16 },
        (file) =>
    {
        // save collection-#####.cxml for last
        if (Path.GetFileName(file).StartsWith("collection-") && Path.GetExtension(file) == ".cxml")
        {
            cxmlPath = file;
        }
        else
        {
            // upload each file, using the relative path as a blob name
            UploadFile(file, container.GetBlobReference(Path.GetFullPath(file).Substring(path.Length)));
        }
    });

    // finish up with the cxml itself
    if (cxmlPath != null)
    {
        UploadFile(cxmlPath, container.GetBlobReference(Path.GetFullPath(cxmlPath).Substring(path.Length)));
    }
}

private IEnumerable<string> EnumerateDirectoryRecursive(string root)
{
    foreach (var file in Directory.GetFiles(root))
        yield return file;
    foreach (var subdir in Directory.GetDirectories(root))
        foreach (var file in EnumerateDirectoryRecursive(subdir))
            yield return file;
}

private void UploadFile(string filename, CloudBlob blob)
{
    var extension = Path.GetExtension(filename).ToLower();
    if (extension == ".cxml")
    {
        // cache CXML for 30 minutes
        blob.Properties.CacheControl = "max-age=1800";
    }
    else
    {
        // cache everything else (images) for 2 hours
        blob.Properties.CacheControl = "max-age=7200";
    }
    switch (extension)
        {
            case ".xml":
            case ".cxml":
            case ".dzc":
                blob.Properties.ContentType = "application/xml";
                break;
            case ".jpg":
                blob.Properties.ContentType = "image/jpeg";
                break;
        }
    blob.UploadFile(filename);
}

Serving the Collection

Once the collection is done, there’s very little left to do. I subclassed PivotViewer to handle users clicking through to the movie listing on Netflix (either by clicking “View on Netflix” or by double-clicking an item).

public class NetflixPivotControl : PivotViewer
{
    public NetflixPivotControl()
    {
        ItemActionExecuted += new EventHandler<ItemActionEventArgs>(NetflixPivotViewer_ItemActionExecuted);
        ItemDoubleClicked += new EventHandler<ItemEventArgs>(NetflixPivotViewer_ItemDoubleClicked);
    }

    private void BrowseTo(string itemId)
    {
        HtmlPage.Window.Navigate(new Uri(GetItem(itemId).Href));
    }

    private void NetflixPivotViewer_ItemDoubleClicked(object sender, ItemEventArgs e)
    {
        BrowseTo(e.ItemId);
    }

    private void NetflixPivotViewer_ItemActionExecuted(object sender, ItemActionEventArgs e)
    {
        BrowseTo(e.ItemId);
    }

    protected override List<CustomAction> GetCustomActionsForItem(string itemId)
    {
        var list = new List<CustomAction>();
        list.Add(new CustomAction("View on Netflix", null, "View this movie at Netflix", "view"));
        return list;
    }
}

Finally, I wrote an ASP.NET MVC web role that serves up a web page with the Silverlight control embedded. The actual Silverlight application (.xap file) is stored in blob storage just like the rest of the content, so the interesting part of the controller is constructing the proper URL to the CDN version of the blob.

We have to be careful not to get a mismatched collection due to CDN caching (for example, an updated collection with new movies mismatched with cached Deep Zoom images). To avoid this situation, every time a new collection is created, all files involved are suffixed with a reversed timestamp. The ASP.NET MVC controller below references the latest .cxml file (which in turn references the matching Deep Zoom images).

private Uri GetBlobOrCdnUri(CloudBlob blob, string cdnHost)
{
    // always use HTTP to avoid Silverlight cross-protocol issues
    var ub = new UriBuilder(blob.Uri)
    {
        Scheme = "http",
        Port = 80
    };
    if (!string.IsNullOrEmpty(cdnHost))
    {
        ub.Host = cdnHost;
    }
    return ub.Uri;
}

public ActionResult Index()
{
    var blobs = CloudStorageAccount.Parse(RoleEnvironment.GetConfigurationSettingValue("DataConnectionString"))
        .CreateCloudBlobClient();
    var cdnHost = RoleEnvironment.GetConfigurationSettingValue("CdnHost");

    var controlBlob = blobs.GetBlobReference("control/NetflixPivotViewer.xap");
    var collectionBlob = blobs.ListBlobsWithPrefix("collection/collection-").OfType<CloudBlob>()
        .Where(b => b.Uri.AbsolutePath.EndsWith(".cxml")).First();

    ViewData["xapUrl"] = GetBlobOrCdnUri(controlBlob, cdnHost).AbsoluteUri;
    ViewData["collectionUrl"] = GetBlobOrCdnUri(collectionBlob, cdnHost).AbsoluteUri;
    return View();
}

Download the Code

You’ve now seen nearly all of the code involved, but you can download the full Visual Studio 2010 solution at http://cdn.blog.smarx.com/files/NetflixPivot_source_updated2.zip.

If you want to run it, you’ll also need:

And note that the collection takes quite some time to create, so expect to run this for at least an hour before you can see anything.

[UPDATE 2:58pm] The first revision of this code had a couple bugs, notably around the use of the CDN. (I originally didn’t create new blob names for each update to the collection, so mismatches due to caching were possible.) I’ve updated a couple code snippets in the text of this post, and I’ve posted a new version of the source code.

[UPDATE 6/30/2010 4:28pm] I made another small revision to the code to make sure the .cxml file is uploaded last. I updated one code snippet above, and I’ve posted a new version of the source code.


Computing the Total Size of Your Blobs

A question came up on the Windows Azure MSDN forum recently about how to find the total number of bytes used by a blobs in a particular container. There’s no API that retrieves that information at the container level, but you can compute it by enumerating the blobs, as in the following one-liner:

var totalBytes = (from CloudBlob blob in
                  container.ListBlobs(new BlobRequestOptions() { UseFlatBlobListing = true })
                  select blob.Properties.Length
                 ).Sum();

To go one step further, we can enumerate all the containers too. Here’s how to sum the sizes of all the blobs in a particular account (still a one-liner):

var totalBytes = (from container in blobClient.ListContainers()
                  select
                  (from CloudBlob blob in
                   container.ListBlobs(new BlobRequestOptions() { UseFlatBlobListing = true })
                   select blob.Properties.Length
                  ).Sum()
                 ).Sum();

Note that this does not reflect the number of bytes you’re billed for. Things like empty pages in page blobs, uncommitted blocks in block blobs, snapshots, metadata, etc. all affect the total storage used in your account. The code snippets above simply sums the “sizes” (if you were to download them, for example) of all the blobs.


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!