<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Peter Sobot]]></title>
  <link href="http://petersobot.com/atom.xml" rel="self"/>
  <link href="http://petersobot.com/"/>
  <updated>2013-06-10T21:11:35-04:00</updated>
  <id>http://petersobot.com/</id>
  <author>
    <name><![CDATA[Peter Sobot]]></name>
    
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  <entry>
    <title type="html"><![CDATA[Dangerously Convenient APIs]]></title>
    <link href="http://petersobot.com/blog/dangerously-convenient-apis/"/>
    <updated>2013-04-22T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/dangerously-convenient-apis</id>
    <content type="html"><![CDATA[<p>The modern trend of providing an API for everything is wonderful. With minimal effort, any developer with an internet connection can programmatically access a wealth of data and powerful functionality. Without APIs, many hackathons wouldn’t exist, and many new developers would languish in frustration instead of participating in the best part of software development - <strong>building fun stuff</strong>.</p>

<p>However, all of this convenience comes at a cost. Often, that cost is literal, if an API provider decides to charge for access. This is the entire business model of many companies, and there are now even companies that provide API-monetization-as-a-service. This has created a kind of purely digital marketplace, by literally allowing people to buy access to data and functions. (This is a Good Thing™, as it encourages competition and variety in the API market, and reduces time-to-ship for many developers.)</p>

<p>Many of these monetized APIs are providing access to something inherently proprietary - an enormous dataset, neural network, or advanced algorithm. A problem arises when these APIs provide access to something open. Imagine an API that provides access to datasets that are completely public and free, or an API that performs simple operations on provided data. For an extreme example, imagine an API that implements strlen() - the simple, common task of finding the length of a string.</p>

<hr />

<p>Imagine a group of young, new programmers building the next cool app. They need to find the lengths of their strings, as you do. As C is hard to learn, and reading documentation would take too much time, they instead outsource their character-counting to api.strlen.com. (The venue of the hackathon has great wi-fi, so the additional network overhead is not a big deal.) They launch the app to much fanfare, pitch it, then win first prize at their Startup Weekend.</p>

<p>Months later, thousands of people are using their hot new app. They’re soaking in the TechCrunch coverage and brainstorming monetization ideas when, suddenly, their app stops working. They trace the error down to one call - to api.strlen.com - and finally see:</p>

<pre><code>&gt;HTTP/1.1 429 Too Many Requests
</code></pre>

<p>It’s 11pm on a Saturday night. The folks that run strlen.com are nowhere to be found.</p>

<hr />

<p>Monday morning rolls around, and our favourite team of programmers has barely slept. Their star app has been down all weekend. Finally, they get an email back from support@strlen.com:</p>

<blockquote><p>Congratulations on the TechCrunch coverage! Unfortunately, you’ve way exceeded our rate limit (in fact, we had to put a rate limit in place just because of your app) and we need to chat. We’re now charging $0.0001 per character counted with our string length API. Let me know if you’re interested in upgrading your free account and I can get you set up!</p>

<p>-Bjørn, CEO, strlen.com</p></blockquote>

<p>The team runs the numbers to find that, with the new rates, every additional user of the app would lose them a ridiculous amount of money every day. But, hey - they just had a great chat with an angel - and they might be getting some financing soon. They send Bjørn their details and get set up with a paid account. The app starts working again. Their users are happy, TechCrunch comes by for another interview, and the team’s reason for sleeplessness goes from “anxiety” back to “coding.”</p>

<hr />

<p>It’s been a month since our team - now incorporated as Blue Blanket, Inc. - signed up for their paid strlen.com account. Software engineers are expensive, and while they’ve considered hiring somebody to write their own version of the strlen.com API, they’re really not sure where to start. Their fancy new analytics dashboard shows increasing numbers, minute over minute, until - all at once - the graphs go dead. The app is down again, and once again, it’s due to strlen.com. The team points their web browsers angrily at api.strlen.com, only to find:</p>

<pre><code>&gt;HTTP/1.1 410 Gone
</code></pre>

<p>The homepage of strlen.com has an even bleaker message:</p>

<blockquote><p>Dear friends,</p>

<p>We at strlen.com are very proud to announce that we’ve been acquired by Standard Library Incorporated. It’s been a wild ride counting characters for you over the past six months, but we’re excited to move on and solve hard new problems with the great people at stdlib.
All API endpoints will be disabled, effective immediately.</p>

<p>-Bjørn, VP String at stdlib.com</p></blockquote>

<p>Our trusty team’s app stays down for the better part of a month while they scrounge up a handful of competent engineers to recreate the missing functionality. Once back online, their app is all but forgotten. TechCrunch runs an article a year later - “What ever happened to Blue Blanket?” - that places the blame on a power struggle between the co-founders.</p>

<hr />

<p>Obviously, implementing strlen as a paid API is an absurd example, but there are real APIs out there that are not much different. If you depend on an external service for your app’s core functionality, that’s okay. But if you can feasibly replicate the API yourself, then relying on the external service is a source of <strong>extremely risky technical debt</strong>. Your debtors (in this case, the API providers) could demand immediate repayment at any time by rate limiting or shutting down.</p>

<p>Don’t let your app be crippled by someone else’s acquisition.</p>

<hr />

<p>Thanks to <a href="http://zmanji.com">Zameer Manji</a> for proofreading this post.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Interns are Leading the Way]]></title>
    <link href="http://petersobot.com/blog/interns-are-leading-the-way/"/>
    <updated>2013-01-19T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/interns-are-leading-the-way</id>
    <content type="html"><![CDATA[<p>I attend the University of Waterloo, one of Canada&#8217;s most widely-known engineering schools. Waterloo is famous for a system they call co-op - a regimen of paid internships of 4-8 months in duration in a real-world work environment. Co-op is mandatory for all engineering students, and upon graduation, results in each student having worked at up to 6 different companies for a total of at least 24 months. Each &#8220;work term&#8221; can happen during the summer, fall, <em>or</em> winter, and can be within Canada or abroad. (We do often go abroad, primarily to Silicon Valley.) Here&#8217;s where my class went for internships this past summer:</p>

<p><img src="../../images/body/coop_map_wt4.png" alt="Where my class went for internships for WT4." /></p>

<p>Over the past year, a number of Waterloo interns have had the pleasure of interning at Khan Academy, the groundbreaking non-profit dedicated to &#8220;accelerate learning for students of all ages.&#8221; They&#8217;ve made such an impression on Sal Khan, its founder, that he&#8217;s gone on to speak extremely highly of Waterloo - even suggesting using its model as a base for furthering education - in an extremely well-written article in <em><a href="http://cacm.acm.org/magazines/2013/1/158766-what-college-could-be-like/fulltext">Communications of the ACM</a></em>:</p>

<blockquote><p>Waterloo has already proven that the division between the intellectual and the useful is artificial; I challenge anyone to argue that Waterloo co-op students are in any way less intellectual or broad thinking than the political science or history majors from other elite universities. If anything, based on my experience with Waterloo students, they tend to have a more expansive worldview and are more mature than typical new college graduates—arguably due to their broad and deep experience base.</p></blockquote>

<p>While every student gains valuable experience and ends up hugely enriched by their time in co-op, those in technical disciplines arguably have the opportunity to make a more lasting impact. In particular, the nature of the software industry allows co-ops to contribute to a company on an extremely meaningful level. Classmates of mine interning at companies like Facebook, Google, Square and Twitter have made contributions that are on par with - if not exceeding - those of senior full-time employees. It&#8217;s hugely exciting, and in my experience, it makes us interns forget that we&#8217;re only interns.</p>

<h2>The Abysmal State of Higher Education</h2>

<p>While I can&#8217;t extoll the virtues of the co-op system enough, it does set co-op students apart from the general student body in unnerving ways. Co-op students, demanding competitive wages during their internships, often do graduate with little-to-no student debt, while traditional university programs might give no workplace experience and leave a student with loans of tens-of-thousands of dollars. It&#8217;s immensely depressing to realize that outside of the little bubble of co-op, <em>that&#8217;s the norm for higher education</em>. Almost everybody that goes to college experiences student debt and difficulty finding employment.</p>

<p>It boggles the mind that for many of today&#8217;s students, it&#8217;s normal to graduate with zero industry experience before immediately searching for a job. Companies shouldn&#8217;t have to guess if a candidate, fresh out of school, can apply their theoretical skills to a workplace that is foreign to them. The question:</p>

<blockquote><p>should we even consider hiring a fresh college grad?</p></blockquote>

<p>should never have to be asked. Regrettably, with the current state of higher education, being a &#8220;fresh college grad&#8221; doesn&#8217;t give any confidence to an employer, at least for graduates of most schools. (And yet it leaves each student in considerable amounts of debt. What an amazingly flawed system.)</p>

<p>The class of Software Engineering students that I belong to will graduate in just over a year, after having spent five years at Waterloo and abroad. Most will graduate with no debt and with multiple job offers in hand, obtained from real world experience that complements their theoretical knowledge. Why must this be a unique situation? We should not be the outliers - <strong>this is how higher education should be</strong>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Text Knockout with Canvas]]></title>
    <link href="http://petersobot.com/blog/text-knockout-with-canvas/"/>
    <updated>2012-12-16T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/text-knockout-with-canvas</id>
    <content type="html"><![CDATA[<p>Recently, I&#8217;ve been working on a complete visual overhaul of my own website and
blog. Instead of the huge, bold lines of my previous site (resembling my <a href="http://petersobot.com/resume/">old
resume</a>), I decided to start fresh with a much smaller, simpler, and subtler
design. After fooling around in Photoshop for a while, I came up with the
header:</p>

<p><img src="../../images/body/newheader.png" alt="Look above." /></p>

<p>Rather than being <em>decisive</em> and choosing on a single colour to define the site,
I decided to let the colour of the background dictate the main colour of the
site at any given time. As my old design featured hundreds of random thumbnails
on its homepage, I opted to use those exact same thumbnails - just blown up,
blurred, and very colourful, as the randomly-chosen backdrop for each page.</p>

<p>To accomplish the text knockout effect in the site&#8217;s header, I simply used a
transparent PNG, as it&#8217;s lighter to bake in the font to a PNG than embed Proxima
Nova. However, I also wanted to knock out each <code>h1</code> and <code>h2</code> in the body of each
blog post. To do so, I turned to HTML5 canvas.</p>

<h3>Titles that look like this, punching out to the background!</h3>

<p>Canvas has a number of different compositing modes that allow Photoshop-style
blending tricks, although somewhat less complex.</p>

<p>To reproduce Photoshop&#8217;s <em>knockout</em> effect, I simply turned to the
<code>destination-out</code> compositing mode:</p>

<pre><code>ctx.globalCompositeOperation = 'destination-out'
</code></pre>

<p>However, it wasn&#8217;t quite that simple. To allow smooth fallback for older
browsers (or even browsers like Safari that don&#8217;t fully support compositing
modes) I wanted to dynamically swap each tag with a canvas tag after each page
load. To replicate the style of each tag, I turned to a very useful function I
hadn&#8217;t used before: <code>getComputedStyle</code>. To get every single style on any tag, I
had to simply run:</p>

<pre><code>style = document.defaultView.getComputedStyle(this.element, "")
//  style.paddingTop, style.paddingBottom, etc...
</code></pre>

<p>Thus, on <code>document.ready</code>, all I had to do was:</p>

<ul>
<li>Find the computed styles of each tag to be replaced</li>
<li>Create a new <code>&lt;canvas&gt;</code> element with the same outer width and height</li>
<li>Apply the original styles to a new <code>&lt;canvas&gt;</code> element</li>
<li>Move the padding of the element to inside its &#8216;width&#8217; (breaking the box
model)</li>
<li>Set the compositing mode to <code>destination-out</code></li>
<li>Draw the text on the canvas</li>
<li>Swap the original element for the new canvas</li>
</ul>


<h2>Et voilà, it works and looks awesome. (&#8230;in some browsers.)</h2>

<p>As always, code is on Github:</p>

<div><style type='text/css'>.gist-data{max-height: 300px}</style>
<script src='https://gist.github.com/4311375.js'></script>
<noscript><pre><code>//  Some really hacky code being used in my next blog redesign.
//  by Peter Sobot (psobot.com) on December 16, 2012

;(function ( $, window, document, undefined ) {
    var pluginName = 'punchout',
        defaults = {
        };

    function Plugin( element, options ) {
        this.element = element;
        this.options = $.extend( {}, defaults, options) ;
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }

    Plugin.prototype.init = function () {
        e = $(this.element);
        i = $('.punchout').length;

        style = document.defaultView.getComputedStyle(this.element, &quot;&quot;);

        width   = parseInt(e.width());
        height  = parseInt(e.height());

        p_top    = parseInt(style.paddingTop     || 0);
        p_bottom = parseInt(style.paddingBottom  || 0);
        p_left   = parseInt(style.paddingLeft    || 0);
        p_right  = parseInt(style.paddingRight   || 0);

        width += p_left + p_right;
        height += p_top + p_bottom;

        id = &quot;punchout_&quot; + i;
        e.after(&quot;&lt;canvas width='&quot; + width + &quot;px' height='&quot; + height +
                &quot;px' class='punchout' id='&quot; + id + &quot;'&gt;&lt;/canvas&gt;&quot;);
        canvas = document.getElementById(id);
        ctx = canvas.getContext('2d');

        canvas.style.cssText = style.cssText;
        canvas.style.backgroundColor = 'transparent';
        canvas.style.padding = 0;
        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';

        alpha = parseFloat(e.css('background').split(' ').slice(3, 4));
        ctx.globalAlpha = 1.0;

        colour = e.css('background').split(' ').slice(0, 4).join(' ');

        ctx.fillStyle = colour;
        ctx.fillRect(0, 0, width, height);
        ctx.font = e.css('font');
        ctx.fillStyle = '#000000';
        ctx.textBaseline = 'top';
        ctx.globalCompositeOperation = 'destination-out';

        text = e.html();

        function overflow(text) {
            return ctx.measureText(text, p_left, p_top).width &gt; (width - p_left - p_right);
        }

        if (overflow(text)) {
            //  TODO: This effectively re-implements text wrapping.
            //        It does not take into account the correct text metrics.
            //        It is a giant hack. Fix it.
            lines = [];
            while (text.length &gt; 0) {
                var i;
                words = text.split(' ');
                for (i = words.length; overflow(text); i--) {
                    text = words.slice(0, i).join(' ');
                }
                lines.push(text);
                text = words.slice(i + 1, words.length).join(' ');
            }
            for (l in lines) {
                ctx.fillText(lines[l], p_left, l == 0 ? p_top : ((l / lines.length) * height));
            }
        } else {
            ctx.fillText(text, p_left, p_top);
        }
        e.remove();
    };

    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName, 
                new Plugin( this, options ));
            }
        });
    }

})( jQuery, window, document );

jQuery(window).ready(function(){
    isMobileAndWorksAndLooksGood = function() {
        return navigator.userAgent.toLowerCase().indexOf('mobile') &gt; -1 &amp;&amp;
               navigator.userAgent.toLowerCase().indexOf('safari') &gt; -1 &amp;&amp;
               window.devicePixelRatio == 1
    }
    if (navigator.userAgent.toLowerCase().indexOf('chrome') &gt; -1 ||
        isMobileAndWorksAndLooksGood()) {
        setTimeout(function(){$('h1.title, h2, h3, h4').punchout();}, 500);
    }
});
</code></pre></noscript></div>



]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Emergency Bandwidth Distribution]]></title>
    <link href="http://petersobot.com/blog/emergency-bandwidth-distribution/"/>
    <updated>2012-11-17T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/emergency-bandwidth-distribution</id>
    <content type="html"><![CDATA[<p>Late last week, I officially launched <a href="http://forever.fm">forever.fm</a>, an infinite, beatmatched radio stream powered by SoundCloud. This morning, I was happy to discover that it had been featured in <a href="http://hackaday.com/2012/11/17/forever-fm-infinite-beat-matched-music/">Hack A Day</a> - one of my favourite hack-centric blogs. However, such exposure resulted in one small issue:</p>

<p><img src="../../images/body/forever_linode_spike.png" alt="Ow, my wallet!" /></p>

<p>That&#8217;s 25% of my little 512MB Linode&#8217;s monthly bandwidth allotment being used up in 6 hours. With Linode (as of this writing) charging $0.10/GB for bandwidth (allotted or through overages), that huge server load could get very expensive, very fast. (At that rate, each listener would cost me roughly $0.25 per day of constant listening. Not viable for a free service!)</p>

<p>So, this afternoon, I was faced with a dilemma. How do I quickly and easily make it cheaper for me to host the site at peak times? A tried and true CDN would be a good solution, but even simple CDNs like <a href="http://aws.amazon.com/cloudfront/#pricing">Amazon CloudFront</a> would cost more than my existing Linode. (Such systems are generally made for scaling to multiple petabytes, while I&#8217;m looking at <em>maybe</em> 1TB tops.)</p>

<p>Instead of going for a large, expensive CDN, I decided to make my own small one. Currently, it contains exactly two nodes: the original forever.fm streaming server, and the hugely overpowered VPS I use to serve <a href="http://the.wubmachine.com">the Wub Machine</a>, my other major music hack.</p>

<p>This &#8220;CDN&#8221; is simple: I&#8217;ve added <a href="https://github.com/psobot/foreverfm/blob/master/forever/relay.py">a single Python script</a> to forever.fm that acts as a basic &#8220;relay&#8221; server. Each relay has a copy of the repo, although it runs <code>python -m forever.relay start</code> rather than <code>python -m forever.server start</code>. Each relay listens to the stream from the &#8220;root&#8221; url and re-broadcasts it to <em>n</em> users. Then, each time a user requests a new stream from the root, the logic is simple:</p>

<pre><code>if len(self.listeners) &gt; config.relay_limit:
    self.redirect(random.choice(config.relays))
</code></pre>

<p>I&#8217;ve got more features to add to the relay system - namely, each relay should be able to send back statistics about number of listeners, user agents, and more back to the central server for logging and live status monitoring. Relays could also be smart and stop listening to the &#8220;source&#8221; stream if nobody is listening to them - preventing additional bandwidth usage. However, with this very simple star pattern, the single stream can be efficiently broadcast to hundreds (if not thousands) of listeners.</p>

<p>As always, the code is available <a href="https://github.com/psobot/foreverfm/commit/ef3afe8f97a3b4e8f0196db8fd6b21916b6a832f">on github</a>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Introducing forever.fm]]></title>
    <link href="http://petersobot.com/blog/introducing-forever-fm/"/>
    <updated>2012-11-08T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/introducing-forever-fm</id>
    <content type="html"><![CDATA[<p>I&#8217;m very proud to announce the launch of my latest project - <strong>forever.fm</strong>, an automatic, infinite online DJ. Forever.fm is a beatmatched stream of the hottest tracks from SoundCloud, mixed together to sound awesome, and continuing forever. (No advertisements, DJ chatter, or breaks!) <a href="http://forever.fm">Check it out!</a></p>

<p><img src="../../images/body/foreverfm.png" alt="Live today!" /></p>

<p><strong>WARNING</strong>: Past this point, you&#8217;ll find only gory technical details of how <strong>forever.fm</strong> was made.</p>

<h2>Overview</h2>

<p>Forever is powered by a large number of technologies, some of which I stole from my previous music hack, <a href="http://the.wubmachine.com">the Wub Machine</a>:</p>

<ul>
<li>For this project, I chose to use a 512MB <a href="http://www.linode.com/?r=724cf6da8799e6c655be4694dfe9c5d460ca1aff">Linode</a> VPS, which has been running spectacularly.</li>
<li>The entire site runs on <a href="http://python.org">Python</a> and uses Facebook&#8217;s <a href="http://tornadoweb.org">Tornado</a> evented server.</li>
<li>To stream track metadata, waveforms, and other live updates, I&#8217;ve used <a href="http://socket.io/">Socket.IO</a> and <a href="https://github.com/mrjoes/tornadio2">tornadio2</a>, its Tornado wrapper.</li>
<li><a href="http://developers.soundcloud.com">the SoundCloud API</a> provides the songs, metadata and audio streams that you hear.</li>
<li><a href="http://developer.echonest.com">the Echo Nest Remix API</a> analyzes each song to find the best beats for beatmatching.</li>
<li>The Echo Nest released some <a href="http://blog.echonest.com/post/597162554/earworm-and-capsule">cool beatmatching examples</a> back in 2010, and my beatmatching code is heavily based off of their <a href="http://blog.echonest.com/post/597162554/earworm-and-capsule">capsule</a> example. (Although heavily patched to <a href="https://github.com/echonest/remix/pull/10">fix memory leaks</a>, allow infinite execution and lighter CPU usage.)</li>
<li><a href="http://lame.sourceforge.net/">LAME</a> and <a href="http://ffmpeg.org/">FFMPEG</a> for efficient encoding and decoding of the MP3 stream.</li>
<li>I found some great code for <a href="http://www.psychicorigami.com/2007/05/12/tackling-the-travelling-salesman-problem-hill-climbing/">approximating the Travelling Salesman Problem</a> by <a href="http://www.psychicorigami.com/about/">John Montgomery</a>. This is used for ordering tracks - more on that later.</li>
<li><a href="http://www.schillmania.com/">Scott Schiller</a>&#8217;s spectacular SoundManager2 JavaScript library and <a href="http://www.schillmania.com/projects/soundmanager2/demo/360-player/canvas-visualization.html">360º player UI</a> play back the audio stream in-browser and provide very neat visualizations.</li>
<li>the <a href="http://www.pythonware.com/products/pil/">Python Imaging Library</a> is used to colour, stylize, and fade the waveforms of each song.</li>
<li><a href="http://charlesleifer.com/">Charles Leifer</a>&#8217;s <a href="http://charlesleifer.com/blog/using-python-and-k-means-to-find-the-dominant-colors-in-images/">algorithm for using k-means to find the dominant colours in images</a> is used to colour each track based on its album artwork.</li>
</ul>


<h2>Streaming MP3 in Python</h2>

<p>The toughest problem to solve when creating Forever was that of <em>live streaming</em>. The core beatmatching algorithm at its heart (&#8221;<a href="http://blog.echonest.com/post/597162554/earworm-and-capsule">Capsule</a>&#8221;) has existed for a couple years now. However, making this run infinitely required some different approaches.</p>

<p>Python&#8217;s built-in generators provide a great way to implement an iterative beatmatching algorithm, as each generator can carry its own internal state. In this case, that state is the last-played song. Here&#8217;s some pseudocode:</p>

<pre><code>def forever(track_queue):
    t1 = track_queue.get()
    while not track_queue.empty():
        t2 = track_queue.get()
        yield make_transition(t1, t2)
        t1 = t2
</code></pre>

<p>This code is obviously oversimplified, but basically how the core of Forever works. Assuming that <code>forever</code> constantly yields raw audio data (i.e.: WAV, AIFF, PCM), this needs to be encoded to MP3 and streamed out to the user.</p>

<p>To tackle this MP3 problem, I created a LAME MP3 encoder interface in Python that allows real-time, buffered and synchronized MP3 encoding.</p>

<div><style type='text/css'>.gist-data{max-height: 300px}</style>
<script src='https://gist.github.com/2387349.js'></script>
<noscript><pre><code>from Queue import Queue
import subprocess
import threading
import traceback
import logging
import time

log = logging.getLogger(__name__)

&quot;&quot;&quot;
    Quick and dirty, frame-aware MP3 encoding bridge using LAME.
    About 75% of the speed of raw LAME. Pass PCM data to the Lame class,
    get back (via callback, queue or file) MP3 frames. Supports real-time
    encoding or blocking for the length of the audio stream - useful for
    an MP3 server, or something else real time, for example.
&quot;&quot;&quot;

&quot;&quot;&quot;
Some important LAME facts used below:
    Each MP3 frame is identifiable by a header.
    This header has, essentially:
        &quot;Frame Sync&quot;            11 1's (i.e.: 0xFF + 3 bits)
        &quot;Mpeg Audio Version ID&quot; should be 0b11 for MPEG V1, 0b10 for MPEG V2
        &quot;Layer Description&quot;     should be 0b11
        &quot;Protection Bit&quot;        set to 1 by Lame, not protected
        &quot;Bitrate index&quot;         0000 -&gt; free
                                0001 -&gt; 32 kbps
                                0010 -&gt; 40 kbps
                                0011 -&gt; 48 kbps
                                0100 -&gt; 56 kbps
                                0101 -&gt; 64 kbps
                                0110 -&gt; 80 kbps
                                0111 -&gt; 96 kbps
                                1000 -&gt; 112 kbps
                                1001 -&gt; 128 kbps
                                1010 -&gt; 160 kbps
                                1011 -&gt; 192 kbps
                                1100 -&gt; 224 kbps
                                1101 -&gt; 256 kbps
                                1110 -&gt; 320 kbps
                                1111 -&gt; invalid

    Following the header, there are always SAMPLES_PER_FRAME samples of audio data.
    At our constant sampling frequency of 44100, this means each frame
    contains exactly .026122449 seconds of audio.
&quot;&quot;&quot;

BITRATE_TABLE = [
    0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, None
]
SAMPLERATE_TABLE = [
    44100, 48000, 32000, None
]
HEADER_SIZE = 4
SAMPLES_PER_FRAME = 1152


def avg(l):
    return sum(l) / len(l)


def frame_length(header):
    bitrate = BITRATE_TABLE[ord(header[2]) &gt;&gt; 4]
    sample_rate = SAMPLERATE_TABLE[(ord(header[2]) &amp; 0b00001100) &gt;&gt; 2]
    padding = (ord(header[2]) &amp; 0b00000010) &gt;&gt; 1
    return int((float(SAMPLES_PER_FRAME) / sample_rate) * ((bitrate / 8) * 1000)) + padding


class Lame(threading.Thread):
    &quot;&quot;&quot;
        Live MP3 streamer. Currently only works for 16-bit, 44.1kHz stereo input.
    &quot;&quot;&quot;
    safety_buffer = 30  # seconds
    input_wordlength = 16
    samplerate = 44100
    channels = 2
    preset = &quot;-V3&quot;

    #   Time-sensitive options
    real_time = False       #   Should we encode in 1:1 real time?
    block = False           #   Regardless of real-time, should we block
                            #   for as long as the audio we've encoded lasts?

    chunk_size = samplerate * channels * (input_wordlength / 8)
    data = None

    def __init__(self, callback=None, ofile=None, oqueue=None):
        threading.Thread.__init__(self)

        self.lame = None
        self.buffered = 0
        self.oqueue = oqueue
        self.ofile = ofile
        self.callback = callback
        self.finished = False
        self.sent = False
        self.ready = threading.Semaphore()
        self.encode = threading.Semaphore()
        self.setDaemon(True)

        self.__write_queue = Queue()
        self.__write_thread = threading.Thread(target=self.__lame_write)
        self.__write_thread.setDaemon(True)
        self.__write_thread.start()

    @property
    def pcm_datarate(self):
        return self.samplerate * self.channels * (self.input_wordlength / 8)

    def add_pcm(self, data):
        &quot;&quot;&quot;
        Expects PCM data in the form of a NumPy array.

        &quot;&quot;&quot;
        if self.lame.returncode is not None:
            return False
        self.encode.acquire()
        samples = len(data)
        self.__write_queue.put(data)
        del data
        put_time = time.time()
        if self.buffered &gt;= self.safety_buffer:
            self.ready.acquire()
        done_time = time.time()
        if self.block and not self.real_time:
            delay = (samples / float(self.samplerate)) \
                    - (done_time - put_time) \
                    - self.safety_buffer
            time.sleep(delay)
        return True

    def __lame_write(self):
        while not self.finished:
            data = self.__write_queue.get()
            if data is None:
                break
            while len(data):
                chunk = data[:self.chunk_size]
                data = data[self.chunk_size:]
                self.buffered += len(chunk) / self.channels * (self.input_wordlength / 8)
                try:
                    chunk.tofile(self.lame.stdin)
                    del chunk
                except IOError:
                    self.finished = True
                    break
            self.encode.release()

    #   TODO: Extend me to work for all samplerates
    def start(self, *args, **kwargs):
        call = [&quot;lame&quot;]
        call.append('-r')
        if self.input_wordlength != 16:
            call.extend([&quot;--bitwidth&quot;, str(self.input_wordlength)])
        call.extend(self.preset.split())
        call.extend([&quot;-&quot;, &quot;-&quot;])
        self.lame = subprocess.Popen(
            call,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        )
        threading.Thread.start(self, *args, **kwargs)

    def ensure_is_alive(self):
        if self.finished:
            return False
        if self.is_alive():
            return True
        try:
            self.start()
            return True
        except Exception:
            return False

    def run(self, *args, **kwargs):
        try:
            last = None
            lag = 0
            while True:
                timing = float(SAMPLES_PER_FRAME) / self.samplerate

                header = self.lame.stdout.read(HEADER_SIZE)
                if len(header) == HEADER_SIZE:
                    frame_len = frame_length(header) - HEADER_SIZE
                    frame = self.lame.stdout.read(frame_len)
                    buf = header + frame
                    if len(frame) == frame_len:
                        self.buffered -= SAMPLES_PER_FRAME
                else:
                    buf = header

                if self.buffered &lt; (self.safety_buffer * self.samplerate):
                    self.ready.release()
                if len(buf):
                    if self.oqueue:
                        self.oqueue.put(buf)
                    if self.ofile:
                        self.ofile.write(buf)
                        self.ofile.flush()
                    if self.callback:
                        self.callback(False)
                    if self.real_time and self.sent:
                        now = time.time()
                        if last:
                            delta = (now - last - timing)
                            lag += delta
                            if lag &lt; timing:
                                time.sleep(max(0, timing - delta))
                        last = now
                    self.sent = True
                else:
                    if self.callback:
                        self.callback(True)
                    break
            self.lame.wait()
        except:
            log.error(traceback.format_exc())
            self.finish()
            raise

    def finish(self):
        &quot;&quot;&quot;
            Closes input stream to LAME and waits for the last frame(s) to
            finish encoding. Returns LAME's return value code.
        &quot;&quot;&quot;
        if self.lame:
            self.__write_queue.put(None)
            self.encode.acquire()
            self.lame.stdin.close()
            self.join()
            self.finished = True
            return self.lame.returncode
        return -1


if __name__ == &quot;__main__&quot;:
    import wave
    import numpy
    f = wave.open(&quot;test.wav&quot;)
    a = numpy.frombuffer(f.readframes(f.getnframes()), dtype=numpy.int16).reshape((-1, 2))

    s = time.time()
</code></pre></noscript></div>


<p>With this simple interface, Forever hands off chunks of large, raw audio data to LAME for MP3 encoding. Python then reads single MP3 frames from LAME as they become available and puts those frames into a queue. (If the queue is full, this blocks and prevents more raw audio from being generated, essentially throttling the entire process via negative queue pressure.)</p>

<p>Finally, someone has to read from this MP3 queue. After trying custom thread-based solutions for throttling this properly, I instead settled on a much more stable solution - asking the Tornado web server itself to hand out each MP3 frame as it becomes available. Surprisingly, this is extremely stable, and results in a perfectly real-time stream with no lead or lag:</p>

<pre><code>SECONDS_PER_FRAME = 1152.0 / 44100  # As defined by the spec
seconds_to_buffer = 60
mp3_queue = Queue(int(seconds_to_buffer / SECONDS_PER_FRAME))
. . .
def send():
    frame = mp3_queue.get_nowait()  
    if not frame:
        print "OH NOES, MP3 queue is empty!"
        return
    for listener in listeners:
        listener.send(frame)
. . .
PeriodicCallback(send, SECONDS_PER_FRAME * 1000).start()
</code></pre>

<p>This strategy has not (yet) been load tested, but is nevertheless the system I&#8217;m using in production at the moment. For all I know, this could fail completely while serving a large number of listeners. Testing locally by spinning up 200 instances of CURL, this performs admirably and causes Python to use only 5% of my aging Macbook&#8217;s CPU.</p>

<p>Interestingly, placing a limit on this final MP3 buffer propagates this queue pressure backwards all the way to the audio generator. If the output MP3 buffer is full, the LAME wrapper will be blocked until it can write to the queue. If LAME&#8217;s output is blocked, then an internal semaphore will block in the LAME wrapper&#8217;s input function, which will delay the audio generator. (Internally, the LAME wrapper writes all of the PCM to the LAME process at once, to prevent encoding delay and decrease memory usage by only storing lightweight MP3 instead of heavy PCM. This blocking behaviour is artifically implemented to save memory.)</p>

<h2>Memory Leaks and Python C Extensions</h2>

<p>In the process of implementing Forever using the Echo Nest&#8217;s <code>action</code> and <code>cAction</code> libraries, I ran across an absurdly annoying bug that took me into the depths of Python C extensions. Each time I executed a <code>Crossfade.render</code> or <code>pydirac.timeStretch</code> call, I lost memory. (A <em>lot</em> of memory - often between 25 and 100 MB.) As Forever is built around these methods, I couldn&#8217;t have used them as-is - but I couldn&#8217;t find a suitable replacement, as they were written specifically for this purpose by Tristan Jehan, a co-founder of the Echo Nest.</p>

<p>So I busted out Apple&#8217;s Instruments, which include a memory profiler. Upon initial search, I found no actual memory leaks. (i.e.: There were no calls to <code>malloc</code> without a corresponding <code>free</code>.) However, memory was increasing linearly over time on the smallest possible test case - something was definitely being leaked!</p>

<p>As Python is a garbage-collected language, I then turned to the internal heap in an attempt to find objects without references. After trying <a href="http://guppy-pe.sourceforge.net/">guppy and heapy</a> to find the size of Python&#8217;s heap, I realized it wouldn&#8217;t help. PCM audio in the Echo Nest API is stored within numpy arrays - which are garbage collected, but whose memory is allocated outside of Python. Searching for numpy arrays with heapy, guppy, or the wonderful <a href="http://mg.pov.lt/objgraph/">objgraph</a> ends up being relatively futile, as the large chunks of memory that you&#8217;re searching for won&#8217;t be in the scope of Python.</p>

<p>This left one possibility - the C extensions being used were leaving a reference to the large Numpy array somewhere. As it turns out, this was the case. (Discovered by manually reading the code, finding every <code>PyObject*</code>, and tracing it to ensure that its reference count was handled properly.) <a href="https://github.com/echonest/remix/pull/10">The solution?</a></p>

<pre><code>. .  . . 
     215    +    Py_DECREF(inSound1);
     216    +    Py_DECREF(inSound2);
215  217         return PyArray_Return(outSound);
. .  . .
</code></pre>

<p>As it turns out, a Numpy array allocation function (<code>NA_InputArray</code>) was being used incorrectly. <a href="http://structure.usc.edu/numarray/node55.html">The docs</a> state that the return value of this function should should always be <code>DECREF</code>&#8216;d, but they weren&#8217;t. It&#8217;s that simple, but with such a huge impact.</p>

<h2>The Dreaded GIL</h2>

<p>After fixing the memory leaks, optimizing the beatmaching algorithms and ensuring that the system runs indefinitely, I ran into another problem - Python&#8217;s global interpreter lock. As <a href="http://en.wikipedia.org/wiki/Global_Interpreter_Lock">Wikipedia explains succintly</a>, a GIL is:</p>

<blockquote><p>a mutual exclusion lock held by a programming language interpreter thread to avoid sharing code that is not thread-safe with other threads. In languages with a GIL, there is always one GIL for each interpreter process. CPython and CRuby use GILs.</p></blockquote>

<p>This poses a big problem for Forever. The Tornado server has to deal out MP3 frames in real time to each listener, so any execution delays will cause noticeable audio dropouts. Worse still, the important audio operations needed to beatmatch songs (including calls to the aforementioned C extensions) are very computiationally expensive, often holding the GIL for seconds at a time. (<strong>Note</strong>: there exists a <a href="http://docs.python.org/2/c-api/init.html#releasing-the-gil-from-extension-code">simple way to release the GIL from extension code</a> that I haven&#8217;t yet tried, which might mitigate the issue.)</p>

<p>To work around this and ensure total isolation between heavy, blocking audio operations and efficient, real-time MP3 streaming, Forever makes use of Python&#8217;s <code>multiprocessing</code> module to split itself into server and worker processes. By opening a queue (well, multiple queues) between the server and the worker, the server can consistently stream MP3 packets in real time, while the worker thread can block and hold the GIL for any amount of time.</p>

<p><strong><em>In theory.</em></strong></p>

<p>Unfortunately, the synchronized Queue class provided in the <code>multiprocessing</code> module buffers its data in the <em>sending</em> process, not the receiving process. This means that even if the Queue is full of audio, the GIL of the worker process could be acquired by one thread, <em>preventing the server process from reading any data from the queue</em> until it is released. This doesn&#8217;t help at all.</p>

<p>To work around this restriction yet again, I created a simple <code>BufferedReadQueue</code> class that eagerly fetches all of the data from the child process and simply buffers it in the parent, allowing the parent to read data even when the child&#8217;s GIL is blocked, and providing an isolated buffer of audio data to safeguard against dropouts.</p>

<div><style type='text/css'>.gist-data{max-height: 300px}</style>
<script src='https://gist.github.com/4047223.js'></script>
<noscript><pre><code>import Queue
import multiprocessing
import threading


class BufferedReadQueue(Queue.Queue):
    def __init__(self, lim=None):
        self.raw = multiprocessing.Queue(lim)
        self.__listener = threading.Thread(target=self.listen)
        self.__listener.setDaemon(True)
        self.__listener.start()
        Queue.Queue.__init__(self, lim)

    def listen(self):
        try:
            while True:
                self.put(self.raw.get())
        except:
            pass

    @property
    def buffered(self):
        return self.qsize()
</code></pre></noscript></div>


<h2>Cycles of Similar Songs</h2>

<p>A significant part of what makes Forever sound good is its choice of tracks. One of the hardest problems that a DJ faces is choosing which tracks to play in their set, and Forever is no different. To solve this problem programatically, I created a module called the <em>Brain</em> and turned to graph theory.</p>

<p>Forever starts by grabbing a list of the top <em>n</em> tracks from SoundCloud, ordered by &#8220;hotness.&#8221; It then culls this list by removing songs that are too short, too long, or duplicates of other songs in the list. Forever then arranges the remaining tracks in a complete graph, where each song is a vertex, and each edge describes a measure of &#8220;distance&#8221; between tracks - an inverse of similarity, if you will. For example, if two tracks have similar tempo, tags and genre, their similarity will be high, making the edge weight between them very low.</p>

<p>Here&#8217;s an example of the edge weights in a simple three-song graph:</p>

<p><img src="../../images/body/similarity.png" alt="Maths vs. Born This Way vs. Buzzard and Kestrel!" /></p>

<p>To play these songs forever, the problem reduces to finding the lowest-weight cycle in the entire undirected graph… which requires a solution to <a href="http://en.wikipedia.org/wiki/Travelling_Salesman_Problem">the Travelling Salesman Problem</a>. As TSP is one of the great NP-complete problems in computer science, I resort to using approximation algorithms. Forever uses <a href="http://www.psychicorigami.com/2007/05/12/tackling-the-travelling-salesman-problem-hill-climbing/">a great bit of Python code for a hill-climbing approximation</a>, written by <a href="http://www.psychicorigami.com/about/">John Montgomery</a> to &#8220;solve&#8221; this problem. After running the approximation for something like 10,000 iterations, I accept the solution and use that to order the resulting tracks.</p>

<p>After the track order is determined, the worker process receives these tracks and begins to remix them in order when necessary. At this step, the worker fetches the Echo Nest&#8217;s analysis (a very lengthy call) to find the metadata required to beatmatch each track. With this data comes a great &#8220;summary&#8221; that includes many factors that would be useful for determining track order - including <code>energy</code>, <code>danceability</code> and <code>loudness</code>. Hence, this summary data is cached in a SQLite database to allow the Brain to make better decisions.</p>

<h2>Live Brain Transplants (Reloading Modules)</h2>

<p>Once Forever was working without dropouts or stalls, I faced another issue. If I wanted to make a code change to any of the core algorithms, I&#8217;d usually just restart the server. However, if there are people listening to the radio stream, this would cut them off, as the underlying socket connection to the server would be broken as soon as the Python server process is killed. Hence, to ensure a truly endless stream of audio, I had to find a way to hot-swap portions of code that I might want to update frequently.</p>

<p>To test this, I started with Forever&#8217;s <em>Brain</em> module. To wrap the Brain in a container that could easily hot-swap its internal logic, I created another module - aptly-named &#8220;skull.&#8221; This module performs the infinite loop around the Brain&#8217;s logic, calling the Brain as a generator and adding its results directly to a limited queue. In short:</p>

<pre><code>class Skull(threading.Thread):
    def __init__(self, track_queue):
        self.track_queue = track_queue

        import brain
        self.brain = brain
        self.loaded = self.modtime

        threading.Thread.__init__(self)
        self.daemon = True

    @property
    def modtime(self):
        return os.path.getmtime(self.brain.__file__)

    def run(self):
        g = self.brain.add_tracks()
        while True:
            if self.modtime != self.loaded:
                log.info("Hot-swapping brain!")
                self.brain = reload(self.brain)
                self.loaded = self.modtime
                g = self.brain.add_tracks()

            track = g.next()
            log.info("Adding new track to queue.")
            self.track_queue.put(track)
</code></pre>

<p>I&#8217;ve since extended this concept to a number of other core modules - namely, the beatmatching generator, as I&#8217;m currently trying to increase its efficiency every day, and the MP3 decoding/encoding classes, as they&#8217;re still a bit too memory-hungry for my liking.</p>

<h2>Conclusions</h2>

<p>Try out <a href="http://forever.fm">Forever.fm</a>. If you like the kind of music that&#8217;s popular on SoundCloud (currently lots of EDM) then you&#8217;ll enjoy it. I have big plans for it from here on out, but it was super fun to build, and I learned a ton. As always, feel free to <a href="mailto:forever@petersobot.com">email me</a> or <a href="http://twitter.com/psobot">tweet at me</a> if you have any questions.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Rewriting in C++ for Fun, Speed and Masochism]]></title>
    <link href="http://petersobot.com/blog/rewriting-in-cpp-for-fun-speed-and-masochism/"/>
    <updated>2012-10-10T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/rewriting-in-cpp-for-fun-speed-and-masochism</id>
    <content type="html"><![CDATA[<p>A couple months ago, I posted <a href="http://petersobot.com/blog/a-use-for-smartphone-photos/">a blog post explaining my use for low-quality
smartphone photos</a>. It involved a smart image cropping algorithm written by
<a href="https://github.com/zaeleus">Michael Macias</a>, using ImageMagick and written in Ruby. I&#8217;ve actually used
the algorithm quite a bit in preparing new photos for my homepage - although
there&#8217;s one major problem - it&#8217;s amazingly slow. Take a look at the kind of processing it does:</p>

<p><img src="../../images/body/crop_01.jpg" alt="The most interesting part of Grand Central Station." /></p>

<p>On large JPEGs from my own photo library, like the one above, this Ruby script takes roughly 2 seconds to perform a smart 124px square crop on the most interesting part of the
image:</p>

<pre><code>Matched 9 images.
Originals/2012/NYC/IMG_7054.JPG =&gt; ./18.jpg in 1801.717ms
Originals/2012/NYC/IMG_7055.JPG =&gt; ./19.jpg in 1856.692ms
Originals/2012/NYC/IMG_7052.JPG =&gt; ./20.jpg in 1787.717ms
Originals/2012/NYC/IMG_7059.JPG =&gt; ./21.jpg in 1727.487ms
Originals/2012/NYC/IMG_7057.JPG =&gt; ./22.jpg in 1716.977ms
Originals/2012/NYC/IMG_7056.JPG =&gt; ./23.jpg in 1692.648ms
Originals/2012/NYC/IMG_7058.JPG =&gt; ./24.jpg in 1887.043ms
Originals/2012/NYC/IMG_7051.JPG =&gt; ./25.jpg in 1977.311ms
</code></pre>

<p>As I often run this algorithm on entire folders of images at once, I decided to
experiment and reimplement the entire program in C++. As a developer that works
primarily with Python and Ruby, I&#8217;ve always felt a small amount of guilt for
incurring the crazy performance overhead of interpreted and heavily dynamic
languages. (A friend of mine working in the hardware industry recently got
<em>angry</em> over the fact that he was working to make chips faster, while us
developers then &#8220;throw away&#8221; the speed gains by running interpreted languages!)</p>

<h2>Trying libjpeg</h2>

<p>While the original Ruby script took maybe 2 hours to write, <a href="https://github.com/psobot/smartcrop/blob/master/fastcrop.cpp">my straight port to C++</a> took more than 10! Most of this time was spent navigating the API of
<code>libjpeg</code>, fumbling with pointers and buffer arithmetic, and hunting down type
casting errors that subtly caused inaccurate results. However, check out the speed gains:</p>

<pre><code>Thumbnailing 9 images...
Processing Originals/2012/NYC/IMG_7051.JPG... 63ms.
Processing Originals/2012/NYC/IMG_7052.JPG... 41ms.
Processing Originals/2012/NYC/IMG_7053.JPG... 5ms.
Processing Originals/2012/NYC/IMG_7054.JPG... 33ms.
Processing Originals/2012/NYC/IMG_7055.JPG... 36ms.
Processing Originals/2012/NYC/IMG_7056.JPG... 31ms.
Processing Originals/2012/NYC/IMG_7057.JPG... 35ms.
Processing Originals/2012/NYC/IMG_7058.JPG... 30ms.
Processing Originals/2012/NYC/IMG_7059.JPG... 34ms.
</code></pre>

<p>The same
sample images that took ~2 seconds to process in Ruby take, on average, 35ms to process in
C++. That&#8217;s a speed up of <strong>more than 50x</strong>. This happens to line up roughly
with <a href="http://shootout.alioth.debian.org/u32q/benchmark.php?test=all&amp;lang=yarv&amp;lang2=gpp">the popular language benchmarks</a> that put Ruby at ~45x slower than
gcc-compiled C++, despite the fact that a lot of the work is done by RMagick. (I should point out that these programs are not exactly identical - the <code>libjpeg</code> version makes some small feature concessions in the name of speed. Their output is nearly identical, however.)</p>

<p><img src="../../images/body/crop_02.jpg" alt="The most interesting part of… somewhere in NYC." /></p>

<p>What&#8217;s also interesting is the cost in developer time and code quantity. The
original Ruby script was ~80 lines, give or take comments - while my C++ port is
~350 lines. In this one isolated, little-optimized, amateur test, C++ took <strong>4x</strong> the
code and <strong>5x</strong> the development time to deliver <strong>50x</strong> the performance.</p>

<h2>Trying Magick++</h2>

<p>However, this joyous speed boost was short-lived. I soon discovered that my <code>libjpeg</code>-based solution was quite buggy. Most cameras nowadays don&#8217;t rotate the raw image from the sensor before encoding to JPEG, preferring a lossless &#8220;orientation&#8221; flag in the EXIF data instead, forcing the decoding library to parse this to display the image upright. Unfortunately, <code>libjpeg</code> doesn&#8217;t contain any built-in facilities to &#8220;right&#8221; an image with such a tag, and doing so manually is extremely difficult. In addition, I hadn&#8217;t written any custom image scaling code, so I depended on a <code>libjpeg</code> flag to scale down the input image by a power of two before decoding.</p>

<p>Faced with this insurmountable rotation bug, and after spending another 8 hours trying to fix it, I decided to <a href="https://github.com/psobot/smartcrop/blob/master/smartcrop.cpp">yet again rewrite the solution</a> using <a href="http://www.imagemagick.org/Magick++/">Magick++</a>, ImageMagick&#8217;s C++ client library. Without further ado, the benchmarks:</p>

<pre><code>Thumbnailing 9 images...
Processing Originals/2012/NYC/IMG_7051.JPG... 340.717ms.
Processing Originals/2012/NYC/IMG_7052.JPG... 287.965ms.
Processing Originals/2012/NYC/IMG_7053.JPG... 94.133ms.
Processing Originals/2012/NYC/IMG_7054.JPG... 279.776ms.
Processing Originals/2012/NYC/IMG_7055.JPG... 286.434ms.
Processing Originals/2012/NYC/IMG_7056.JPG... 281.245ms.
Processing Originals/2012/NYC/IMG_7057.JPG... 289.052ms.
Processing Originals/2012/NYC/IMG_7058.JPG... 280.193ms.
Processing Originals/2012/NYC/IMG_7059.JPG... 283.7ms.
</code></pre>

<p>For numerous reasons here (Magick++ overhead, image pre-scaling, orientation correction) the Magick++-using code falls right in the middle in terms of performance. It&#8217;s <strong>~8 times slower</strong> than using <code>libjpeg</code> directly, but much more correct and still <strong>more than 5 times as fast</strong> as the equivalent Ruby code.</p>

<p><img src="../../images/body/crop_04.jpg" alt="The most interesting part of fireworks at English Bay in Vancouver." /></p>

<p>This final version of the program took about 2 hours to put together, with most of that time spent searching for auto-orientation code (eventually pulling it out of Magick&#8217;s <a href="http://www.imagemagick.org/script/mogrify.php">Mogrify</a> command-line tool) and optimizing for speed.</p>

<h2>The Value of Abstractions</h2>

<p>When given the right amount of abstraction - in this case, a fast C++ library - writing the code to be adequately fast was trivial. Using old-school C-style library integration, on the other hand, ended with me wasting hours making little to no progress. The resulting program was indeed much faster, but questionably worth the time and frustration. (My head still hurts from getting <code>improper call to jpeg library in state xyz</code> errors repeatedly, only to find zero helpful documentation on each error state.) Using a low-level library simply requires <em>more</em> knowledge and more mental state than any commonly used high-level language. (Of course, this sounds obvious.)</p>

<p>This brings up an important point on the state of popular (and slow) languages today. When acceptable speeds are measured in seconds rather than in milliseconds, it makes perfect sense to write slow and inefficient code quickly. Every time a Rubyist runs <code>gem install</code>, they&#8217;re abstracting away the low level implementation details in favour of a simple interface that helps them solve their problem faster. Amortizing the cost of running the program over its runtime, rather than its development time, is logical considering the absurdly high price of a professional software developer.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The Ubiquitous Capture Device]]></title>
    <link href="http://petersobot.com/blog/the-ubiquitous-capture-device/"/>
    <updated>2012-06-23T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/the-ubiquitous-capture-device</id>
    <content type="html"><![CDATA[<p>Every so often, I find myself in a camera store, gawking at beautiful, expensive
cameras and lenses. DSLRs have dropped in price, and mirrorless interchangeable
lens cameras (also known as <a href="http://en.wikipedia.org/wiki/Micro_Four_Thirds_system">micro four thirds</a>) now fill the gap between
cheap point-and-shoot and semi-pro. However, every single time I go to make such
a purchase, I stop myself.</p>

<p>It&#8217;s not that I don&#8217;t want a good camera, it&#8217;s that <strong>I already have a camera
good enough</strong>. Most of us have one on us at all times.</p>

<p>It&#8217;s my smartphone, and it can capture images like this:</p>

<p><img src="http://petersobot.com/images/body/meatandbread.jpg" title="A moment, as captured with my lowly smartphone." ></p>

<p>This image isn&#8217;t pristine. It&#8217;s vibrant, although it could be moreso. It&#8217;s
lacking in detail, a bit noisy, and somewhat compressed. (Compressing it for web
didn&#8217;t help with the presentation, either.) However, none of that is important.
It&#8217;s a beautiful reminder of that moment - a great lunch with a great person, on
a bright and sunny day, in a crowded restaurant. Having that moment captured is
vastly more valuable than the quality of the image.</p>

<p>It&#8217;s true that my smartphone doesn&#8217;t take beautiful 18MP stills, nor does it
have an immaculate 10x optical zoom. The lens isn&#8217;t removable, nor is the sensor
very large.  Low-light performance is horrible. The built in HDR mode, while
better than nothing, often produces horrible artifacts and delays my next shot
for seconds.  I have no control over aperture, ISO, shutter speed or white
balance.</p>

<p>Most of this doesn&#8217;t matter though, as my phone is <strong>always in my pocket</strong>. What
I lose in image quality and configurability, I gain in ubiquity. I might forget
to grab my camera before I leave the house - and I&#8217;ll definitely have to think
twice about bringing a huge DSLR along. But my phone? The only places I might
not bring it are the shower and the swimming pool. I certainly use my camera
enough, now that I&#8217;ve found <a href="http://petersobot.com/blog/a-use-for-smartphone-photos/">an interesting use for lots of low-quality
smartphone photos</a>.</p>

<p><img src="http://petersobot.com/images/body/flickr.png" title="Flickr users choose their smartphones." ></p>

<p>Apple&#8217;s iPhones 4 and 4S are now the <a href="http://www.flickr.com/cameras/">two most popular cameras on Flickr</a>.</p>

<p>Interestingly, people often embrace the lack of quality in smartphone photos.  Photo
filters are all the rage. When beautiful photo quality can&#8217;t be achieved, people
delight in reducing quality even further by adding artistic emotion with a
filter. (Ostensibly, most Instagram users don&#8217;t think <em>that</em> deeply about the
filter they choose.) Instead of creating art via careful manipulation of a fancy
camera, people do it with a one-touch filter.</p>

<p>Extending the idea to another medium, smartphones often have adequate
microphones. As a musician with a penchant for experiments in audio, I do a lot
of recording. I nearly bought an <a href="http://www.samsontech.com/zoom/products/handheld-audio-recorders/h4/">expensive Zoom field recorder</a> recently,
only to stop myself again in favour of my phone&#8217;s more-than-adequate
microphones. To capture musical ideas or field recordings, it&#8217;s perfect for the
same reason - my phone is always with me, and is <em>good enough</em>.</p>

<p>Most of my recent songs are built around samples taken with my iPhone. &#8221;<a href="http://soundcloud.com/psobot/train-in-the-sky">Train
In The Sky</a>&#8221; takes its snare sample from the closing of a <a href="http://www.translink.ca/en/About-Us/Corporate-Overview/Operating-Companies/SkyTrain.aspx">Vancouver
SkyTrain</a>&#8217;s doors. &#8221;<a href="http://soundcloud.com/psobot/mace-and-anvil">Mace and Anvil</a>&#8221; takes part of its melody from the
same train&#8217;s public alert sounds. &#8221;<a href="http://soundcloud.com/psobot/somnambulist">Somnambulist</a>&#8221; (below) starts with
samples (albeit, altered samples) of myself walking, and is filled with the
enthusiastic cries of the chefs at a Japanese restaurant. All of these samples,
while relatively low-quality, were taken with my phone.</p>

<iframe width="100%" height="166" scrolling="no" frameborder="no" src="http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F50383919&amp;"></iframe>


<p>As <a href="http://blog.chasejarvis.com/">Chase Jarvis</a> puts it, &#8220;the best camera is the one that’s with you.&#8221; The
ability to capture any moment at any time, no matter the quality level, is key.
For all I care, my pictures and sounds could come out as a grainy, hazy mess. As
long as I can extract meaning and value from them, I&#8217;ve captured a moment and
strengthened a memory.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Using Eight Cores (incorrectly) with Python]]></title>
    <link href="http://petersobot.com/blog/using-eight-cores-incorrectly-with-python/"/>
    <updated>2012-05-13T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/using-eight-cores-incorrectly-with-python</id>
    <content type="html"><![CDATA[<p>One of my web apps, <a href="http://the.wubmachine.com">The Wub Machine</a>, is very computationally expensive.
Audio decoding, processing, encoding, and streaming, all in Python. Naturally,
my first instinct was to turn to the <a href="http://docs.python.org/library/multiprocessing.html">multiprocessing</a> module to spread the
CPU-bound work across multiple processes, thus avoiding Python&#8217;s <a href="http://wiki.python.org/moin/GlobalInterpreterLock">global
interpreter lock</a>.</p>

<p><img src="http://petersobot.com/images/body/wubload.png" title="Remixing is hard work." ></p>

<p>In theory, it&#8217;s simple enough, but I did run into a few very nasty problems
when dealing with multiprocessing in Python:</p>

<ul>
<li><p>The multiprocessing module, at least on *nixes, forks the current process
and communicates with the child with a pipe. This works wonderfully if the
data you&#8217;re transferring can be easily <a href="http://docs.python.org/library/pickle.html">pickled</a>, and if the child
process doesn&#8217;t need to modify any global state in the parent.
Unfortunately, certain useful constructs in Python can&#8217;t be pickled,
including functions and lambdas (or pretty much anything callable).</p>

<p>In my app, I had a peculiar use case - I would callback pass lambdas into
the constructor of a class, then spawn another process on one of that
class&#8217;s methods like so:</p>

<pre><code>    class MyClass(object):
        def __init__(self, my_callback):
            self.my_callback = my_callback

        def start_work():
            p = multiprocessing.Process(target=self.do_work)
            p.start()
            p.join()

        def do_work():
            # Calculate fibonacci or something, iunno
            self.my_callback("hey look, some data!")
</code></pre>

<p>This lead to a baffling bug - while the callbacks were being run, their
side-effects <strong>weren&#8217;t persistent</strong>. I inserted logging in the callback
function to verify, and noted that not only was it running, but at the end
of its execution, the global state had been set properly. However, from the
perspective of the parent process, nothing had changed.</p>

<p>The reason was simple: the callback had been run in the child process and
had modified the global state of the <strong>child</strong> process, <em>not</em> the parent. A
simple fix would be to have eliminated these callbacks, but I instead used
some of Armin Ronacher&#8217;s <a href="http://www.scribd.com/doc/58306088/Bad-Ideas">bad ideas in Python</a> to create <a href="https://gist.github.com/2690045">an experimental
module</a> that allows pseudo-function-calls between processes. Use
(or even just read) at your own risk - it&#8217;s a hack.</p></li>
</ul>


<div><style type='text/css'>.gist-data{max-height: 300px}</style>
<script src='https://gist.github.com/2690045.js'></script>
<noscript><pre><code>&quot;&quot;&quot;
    multiprocesscallback.py, by Peter Sobot (psobot.com), May 13, 2012
    
    Handles callback functions in classes that have member functions that
    are executed in a different process. A crazy experiment in Python
    magic that breaks a lot of rules.
    
    Do not use in production, for any reason. (Although I do.)
    
    If your class takes in a callback, like so:
    
        class MyClass(object):
            def __init__(self, callback):
            
    you can call MultiprocessCallback.register_all(queue) to auto-create
    member functions with the names of the callback variables, which,
    when called, will be safely executed in the parent process. E.g.:
    
        class MyClass(object):
            def __init__(self, my_callback):
                self._pq = multiprocessing.Queue()
                MultiprocessCallback.register_all(self._pq)
                
            def start_other_process():
                target = MultiprocessCallback.target(self.runs_in_another_process)
                p = multiprocessing.Process(target=target)
                p.start()
                MultiprocessCallback.listen()
                p.join()
            
            def runs_in_another_process():
                self.my_callback(&quot;hey look, some data!&quot;)
            
    The data in the callback must be picklable, as it will be sent across
    the multiprocess boundary.
    
    The parent process can read from the queue itself and run .execute()
    on the MultiprocessCallback objects, or it can use the *blocking*
    MultiprocessCallback.listen(), which provides a basic listener.
    
    Known bugs or omissions:
        - Will straight-up just not work in Windows.
          (The scenario doesn't exist - you can't use multiprocessing on
           member functions on Windows.)
&quot;&quot;&quot;   

import multiprocessing
import traceback
import inspect
import sys
import time

__author__ = &quot;psobot&quot;

class EndListener(Exception): pass

def register_all(queue=None):
    _locals = inspect.getargvalues(sys._getframe(1))[3]
    if not queue:
        queue = multiprocessing.Queue()
        setattr(_locals['self'], &quot;_mpcq&quot;, queue)
    for n, v in dict([(_n, _v) for (_n, _v) in _locals.iteritems() if _n != &quot;self&quot;]).iteritems():
        if hasattr(v, &quot;__call__&quot;):
            proc = multiprocessing.current_process().name
            setattr(_locals['self'], n, lambda *args, **kwargs: _safecall(proc, queue, n, v, *args, **kwargs))
            
def _safecall(proc, queue, n, _c, *args, **kwargs):
    if multiprocessing.current_process().name != proc:
        queue.put(MultiprocessCallback(n, *args, **kwargs))
    else:
        return _c(*args, **kwargs)

def listen(queue=None):
    if not queue:
        _locals = inspect.getargvalues(sys._getframe(1))[3]
        queue = _locals['self'].__dict__[&quot;_mpcq&quot;]
    data = queue.get()
    while not isinstance(data, EndListener):
        if isinstance(data, MultiprocessCallback):
            data.execute()
        data = queue.get()

def target(_callable, queue=None):
    def _target(*args, **kwargs):
        _callable(*args, **kwargs)
        end(queue)
    return _target
        
def end(queue=None):
    if not queue:
        i = 1
        _locals = inspect.getargvalues(sys._getframe(i))[3]
        while not 'self' in _locals or not '_mpcq' in _locals['self'].__dict__:
            i += 1
            _locals = inspect.getargvalues(sys._getframe(i))[3]
        queue = _locals['self'].__dict__[&quot;_mpcq&quot;]
    queue.put(EndListener())

class MultiprocessCallback(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name
        self.stackf = traceback.format_stack(sys._getframe(3), 2)
        self.originator = multiprocessing.current_process().name
        self.args = args
        self.kwargs = kwargs
    
    def execute(self, search=None):
        if not search:
            i = 1
            search = inspect.getargvalues(sys._getframe(i))[3]
            while not self.name in search:
                i += 1
                search = inspect.getargvalues(sys._getframe(i))[3]
                if not self.name in search and 'self' in search:
                    search = search['self'].__dict__
        if self.name in search:
            if hasattr(search[self.name], '__call__'):
                try:
                    r = search[self.name](*self.args, **self.kwargs)
                    if r is not None:
                        print &quot;Warning: return value from callback ignored.&quot;
                except Exception, e:
                    e.args = (&quot; &quot;.join(list(e.args) +
                                       [&quot;\nOriginally called from %s (most recent call last):\n&quot; % self.originator] +
                                       self.stackf), )
                    raise
            else:
                raise ValueError(&quot;Function %s not callable.&quot; % self.name)
        else:
            raise KeyError(&quot;Function %s not provided.&quot; % self.name)

if __name__ == &quot;__main__&quot;:
    class Test(object):   
        def __init__(self, callback = None):
            register_all()
            
        def run_me(self):
            &quot;&quot;&quot;
                Run self.separate_process in its own process.
                When the callback is called, it will execute in the
                parent process.
            &quot;&quot;&quot;
            p = multiprocessing.Process(target=target(self.separate_process))
            p.start()
            listen()
            p.join()
        
        def separate_process(self):
            for i in xrange(0, 10):
                #    some intense computation
                self.callback(i)
                time.sleep(0.1)
    
    count = 0
    def my_callback(i):
        &quot;&quot;&quot;
            Increments a global variable by i in the main process.
        &quot;&quot;&quot;
        if multiprocessing.current_process().name != &quot;MainProcess&quot;:
            raise multiprocessing.ProcessError(&quot;The global is being incremented in the wrong process!&quot;)
        global count
        count += i
        print &quot;Counter in main process is now: %s&quot; % count
        
    Test(callback=my_callback).run_me()</code></pre></noscript></div>


<ul>
<li><p><strong><a href="http://docs.python.org/library/logging.html">Logging</a></strong>, the wonderful built-in Python module for meticulously
logging everything, is thread-safe. Sadly, it doesn&#8217;t seem to be
multiprocessing-safe. Logging makes use of its own internal I/O thread, to
ensure that all log messages are properly queued and written without
clobbering eachother. This thread is locked for every write.</p>

<p>After forking another process, the first call to the logger often hangs
while waiting for the logging thread to become unlocked.  If the logging
thread was in use (i.e.: locked) at the exact instant the process was
forked, then the locked thread will be copied to the new process.  However,
whatever log operation was in progress will then unlock the original thread,
<em>not</em> the copied thread, leaving the new process to wait forever on a lock
that will never be unlocked.</p>

<p>The solution, in my case, was to replace the logger in use with the one
provided by multiprocessing if logging from a new process:</p>

<pre><code>def initlog():
    if multiprocessing.current_process().name == "MainProcess":
        _log = logging.getLogger(config.log_name)
    else:
        _log = multiprocessing.get_logger()
    ...
</code></pre></li>
</ul>


<p>To find and fix these bugs took a lot of time, and a good debugging strategy.
The most valuable tool turned out, surprisingly, to be <strong>GDB</strong>. <a href="https://fedoraproject.org/wiki/Features/EasierPythonDebugging#Summary">GDB 7 has
support for debugging Python runtimes</a>, complete with pseudo-stack traces.
Take a look at the following backtrace of a Python process provided by GDB and
formatted for clarity:</p>

<pre><code>    [Thread debugging using libthread_db enabled]
    [New Thread 0xb0c2fb70 (LWP 12895)]
    0x006da405 in __kernel_vsyscall ()

    Thread 1 (Thread 0xaf23ab70 (LWP 12894)):
    #0  0x006da405 in __kernel_vsyscall ()
    #1  0x003a27d5 in sem_wait@@GLIBC_2.1 ()
                  from /lib/i386-linux-gnu/libpthread.so.0
    #2  0x080f2139 in PyThread_acquire_lock (...)
                  at ../Python/thread_pthread.h:309
    #3  0x080f2fd8 in lock_PyThread_acquire_lock (...)
                  at ../Modules/threadmodule.c:52
    #4  0x080da7d5 in call_function
                    (f=Frame 0x937b47c,
                      for file /usr/lib/python2.7/threading.py,
                      line 128,
                      in acquire
                      (self=&lt;_RLock(...) at remote 0x9caabec&gt;,
                        blocking=1,
                        me=-1356616848),
                      throwflag=0) at ../Python/ceval.c:4013
    ...
    (goes down 79 frames)
</code></pre>

<p>Obviously, this looks much more complicated than a normal Python stack trace,
but it&#8217;s a huge step up from zero debugability. If I proceed down a couple more
frames, I find:</p>

<pre><code>    #7  0x080dac2a in fast_function
                      (f=Frame 0x9ca278c,
                      for file /usr/lib/python2.7/logging/__init__.py,
                      line 693,
                      in acquire (self=&lt;FileHandler(stream=...
</code></pre>

<p>&#8230;which is the first piece of familiar code. Line 693 of logging/__init__.py is
surrounded by a short function, and has a comment that brings the first bit of
understanding:</p>

<pre><code>    def acquire(self):
        """
        Acquire the I/O thread lock.
        """
        if self.lock:
            self.lock.acquire()
</code></pre>

<p>Well, there you go. After fixing these race conditions and deadlocks, the Wub
Machine&#8217;s success rate immediately jumped from horrible to 95% under load.</p>

<p><img src="http://petersobot.com/images/body/wubload_fixed.png" title="Look'it dat 95\% success rate." ></p>

<p>All it took was GDB and an understanding of fork() to solve these bugs. My only
advice: <strong>be very, very, very careful when using multiprocessing</strong>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[A Site For Dinner]]></title>
    <link href="http://petersobot.com/blog/a-site-for-dinner/"/>
    <updated>2012-05-08T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/a-site-for-dinner</id>
    <content type="html"><![CDATA[<p>I like to make small, single-serving sites - frivolous sites with only one
page, and one purpose. They&#8217;re intended to be dead-simple to use, fun to play
with, and somewhat silly. I&#8217;ve made a <a href="http://petersobot.com/howhipster">couple</a> <a href="http://fbimg.petersobot.com">in</a> <a href="http://ninjaquote.com">the past</a>, both
alone and with others, often thinking of the idea over dinner and then
implementing it in the hours (or days) that follow. Last night, I decided to
make another single-serving site - and to make it <a href="http://github.com/psobot/amealforme">open-source</a>, to show
others how simple it is to do.</p>

<p>Enter <em><a href="http://amealfor.me">A Meal for Me</a></em>. Roughly 200 lines of code for a fun site that now
helps me be more adventurous in the kitchen. (Grab <a href="http://github.com/psobot/amealforme">the source on GitHub!</a>)</p>

<p><img src="http://petersobot.com/images/body/amealforme.png" title="That could make a tasty meal..." ></p>

<p>Development took a couple hours, and was simple enough:</p>

<ol>
<li>Have dinner.</li>
<li>Google for &#8220;Recipe API.&#8221;</li>
<li>Get an API key.</li>
<li>Layout a simple page in HAML.</li>
<li>Style with SASS.</li>
<li>Wire it up to the API, and use some basic jQuery to munge the data.</li>
<li>Apply a <a href="http://www.google.com/webfonts">Google Web Font</a> and <a href="http://subtlepatterns.com">subtle background pattern</a> to make
things look good.</li>
<li>Apply API caching in Nginx.</li>
<li>Sleep.</li>
</ol>


<p>To save time (and lines of code), the site is nearly 100% in-browser.  It makes
use of the wonderful <a href="http://punchfork.com">Punchfork</a> recipe API to grab data, then simply formats
the resulting recipe cleanly and simply, providing an image of the meal and a
link to instructions.</p>

<p>The site also makes use of <a href="http://haml-lang.com">HAML</a>, <a href="http://sass-lang.com">SASS</a> and <a href="http://coffeescript.org">CoffeeScript</a>, rather
than HTML, CSS and JavaScript. This saved a significant amount of development
time, and allowed me to use third-party style mixins like <a href="http://thoughtbot.com/bourbon">Bourbon</a>. Images
are sparse - the only .png files are the favicon, Punchfork reference, and the
background, which was graciously taken from <a href="http://subtlepatterns.com">SubtlePatterns</a>. <a href="http://www.google.com/webfonts">Google Web
Fonts</a> also came in handy here, providing a well-suited font after roughly
60 seconds of searching.</p>

<p>To tie it all together are two non-browser components - a <a href="https://github.com/psobot/amealforme/blob/master/Rakefile">Rakefile</a> and an
nginx config. The Rakefile allows me to easily compile the HAML, SASS and
CoffeeScript before deployment, and also fetches the required mixins (Bourbon).
It could very easily be extended to watch the files during development, making
the <a href="http://vimeo.com/36579366">feedback loop</a> much quicker.</p>

<p>The nginx config, on the other hand, serves two purposes: to cache queries to
the Punchfork API, and to hide my private API key. As their API is rate-limited,
I cache every query for 24 hours to make best use of the data I get. I also
hard-code my API key in the nginx config, to prevent others from reading it from
the client-side code and using it. All of this is quite simple to do with the
proxy_pass and proxy_cache directives:</p>

<p>Although I haven&#8217;t load-tested or browser-tested the site, I&#8217;m done. Its mission
was to provide an evening&#8217;s worth of learning and challenge. Now, it can
hopefully help some others learn how to as well - and if nothing else, it&#8217;ll
help me learn how to cook more things.</p>

<hr />

<p><em>Note</em>: I&#8217;ve definitely made some errors in the code. If you find any, or even
just have any suggestions or comments, please do <a href="mailto:contact@petersobot.com">get in touch</a>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Startups: Bands for Hackers]]></title>
    <link href="http://petersobot.com/blog/startups-bands-for-hackers/"/>
    <updated>2012-04-29T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/startups-bands-for-hackers</id>
    <content type="html"><![CDATA[<p>Growing up as a young musician in suburbia, I fantasized about being in a band:
playing music in front of thousands of people, signing a record contract,
enjoying the successes (and excesses) of stardom and celebrity. As I grew older,
I began to realize how difficult it would be to achieve that goal.</p>

<p>Years later, as I started university and was accepted into <a href="http://velocity.uwaterloo.ca">VeloCity</a>,
<a href="http://uwaterloo.ca">Waterloo</a>&#8217;s startup incubator, I noticed a lot of familiar dreams.
Although their domains are vastly different, startups are just bands for hackers.</p>

<p><img src="http://petersobot.com/images/body/drum_hack.jpg" title="Wonderful doodles courtesy of Younjin Kim." ></p>

<p>Like many bands, many startups begin in parents&#8217; garages - serious business
in a non-serious atmosphere. Founders (or band members) spend every waking
moment together, working hard to perfect their craft and their endeavour. While
bands work to perfect their lyrics, melodies and rhythms, startups work to
perfect their pitch, product and business plan. Instead of fans, startups get
users; instead of trying to impress the A&amp;R rep that may show up to a
performance, startups try to impress the investors in the audience on demo day.
For bands, this demo is a rough CD - for startups, it&#8217;s their first product.
Both are MVPs.</p>

<p>Then comes recording. Record labels often finance their artists, allowing them to
spend months in studio making a record, toiling day and night behind a recording
console. This is where the product comes from, the hard work that is initially
the band&#8217;s raison-d&#8217;être. (Their private IP, if you will.) Startups are often
financed by VCs, or by themselves, to make their initial product. For months,
they toil in their studio <em>apartments</em>, toiling day and night behind a <em>Linux</em>
console. By now, both band and startup have something nobody else does - their
work. While they both have funding, neither of them have revenue.</p>

<p>Then comes touring. It&#8217;s no secret that this is how many musical artists make
most of their revenue. While the original music may be great, one purchase of
their album makes them pitiful amounts of money. Many SaaS startups are in the
exact same position. Selling their software directly would be equivalent to
selling the copyright on a band&#8217;s music - a nice one-time sale, but the loss of
all of the private IP. Startups tour just like musicians do, and attempt to
generate revenue. They acquire customers, attempt to gain virality, live off
user (fan) counts and try to bring in whatever cash they can. They could take
the advertising approach, and play a show sponsored by a large brand, or they
could make money from their users directly by charging for tickets.</p>

<p>Then comes the pivot. A startup can quickly realize that their idea isn&#8217;t
profitable, or that nobody&#8217;s interested. Their team obviously has the skill, so
they choose to make a different product. A band can quickly realize that their
music isn&#8217;t liked, or that nobody&#8217;s interested. They obviously have the skill as
musicians, so they broaden their artistic horizons and make new music in a
different genre. Neither type of pivot is bad for the organization, although
they will both lose fans or customers. In either case, the funding
party (VC or Record Label) will definitely be involved in the decision.</p>

<p>Bands and startups are also identical in one other important area - motivation.
There exist bands that are motivated by money, just as there exist startups
whose sole goal is to generate income. These bands still make art, although
it is rarely well accepted (or noticed) by critics, and often written for hire.
(The Backstreet Boys and Justin Bieber are two examples that come to mind.)
Startups with the one goal of profitability may achieve success, but are often
similarly panned, and often stumble frequently on the way. (<a href="http://www.thestar.com/business/article/1156719--groupon-s-troubles-deepen-as-shareholder-lawsuits-pile-up">Groupon</a>.)</p>

<p>On the other hand, musicians who set out to make great music are, at the very
least, taken seriously. Music reviewers devote their attention to those who care
about their work, and it&#8217;s rare to see a &#8220;classic&#8221; or universally-adored piece
of music that was written for hire*. Startups that set out to make a great
product are, on the whole, adored for their work, and often enjoy success as a
side-effect. Google was originally a PhD research project, motivated by passion
and interest rather than profit. Dollar signs were not the focus of <a href="http://facebook.com/4">Zuck</a>&#8217;s
attention when he frantically wrote the first incarnation of Facebook, nor as
<a href="http://twitter.com/jack">Jack Dorsey</a> brainstormed how Twitter would start.</p>

<p>The best bands are made of those who care about their music, <em>not</em> their profits.
The best startups are made by those who care about their work, <em>not</em> their profits.</p>

<p><strong>*EDIT</strong>: A <a href="http://news.ycombinator.com/item?id=3908316">few good commenters on Hacker News</a> have reminded me that
European classical music (i.e.: Mozart and the like) was most certainly
for-hire. My point here was merely to highlight the <em>commodity</em> of <strong>modern</strong>,
hired pop music, and I neglected to think about any time period other than our
own.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[A Use for Smartphone Photos]]></title>
    <link href="http://petersobot.com/blog/a-use-for-smartphone-photos/"/>
    <updated>2012-04-21T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/a-use-for-smartphone-photos</id>
    <content type="html"><![CDATA[<p>As a smartphone user, I take a lot of photos. Since I bought an iPhone 4 nearly
two years ago, I&#8217;ve taken just over 6,000 photos with it. 47GB of memories. On
average, 10 photos per day, every day, often of nothing in particular.</p>

<p>These photos aren&#8217;t good enough, or meaningful enough to anyone else, to post
on Flickr. <a href="http://500px.com">500px</a> would scoff at them. The few people on Facebook that would
recognize the people, places and events in the photos wouldn&#8217;t see the point.
They&#8217;re tiny fragments of my life, and that&#8217;s about it.</p>

<p><img src="http://petersobot.com/images/body/homepage_example.jpg" title="My homepage, when this was written." ></p>

<p>Instead of forcing these thousands of photos to stay hidden in my iPhoto
library, I found an outlet for them - my homepage. Crudely modelled after the
stellar <a href="http://ted.com">TED.com</a> landing page, it&#8217;s supplied by a random set of hundreds of
images, all of which I&#8217;ve taken, and until now, hand-cropped and hand-selected.</p>

<p><a href="https://github.com/zaeleus">Michael Macias</a>, in <a href="https://gist.github.com/a54cd41137b678935c91">a submission</a> to a <a href="http://codebrawl.com/contests/content-aware-image-cropping-with-chunkypng">Codebrawl</a> last November, came up
with a brilliantly simple method of content-aware image cropping. By measuring
the greyscale entropy of a window as it slides over an image, the
highest-interest thumbnail can be determined automatically. I took this solution,
modified it (faster, uses ImageMagick, etc.), and hacked together a quick Ruby
script.</p>

<p>This script automatically chooses 50 random images from a given path (or shell
glob) and crops them to their most &#8220;interesting&#8221; thumbnails. The thumbnails are
scaled to size, and saved in incrementing order in the destination folder. It&#8217;s
highly optimized for my personal workflow, but it does seem to work quite
well. For example, take the following shot of <a href="http://zameermanji.com">Zameer Manji</a>:</p>

<p><img src="http://petersobot.com/images/body/crop_zam.jpg" title="[That's Zameer. Yup.]" ></p>

<p>The original photo was poorly exposed, had no clear subject, and was, well,
<em>weird</em>. After automatically cropping it down to a tiny thumbnail, it fits in
nicely on my homepage as an artsy shot of a bike rack in the daylight.</p>

<p>Only one thing left to do: take more photos.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Software, Art, Music and Games]]></title>
    <link href="http://petersobot.com/blog/software-art-music-and-games/"/>
    <updated>2012-02-17T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/software-art-music-and-games</id>
    <content type="html"><![CDATA[<p>I am a software engineering student. The exact definition of that varies among my classmates and professors. Some say that it implies an ability to write software. Others argue that it requires a strong grasp of algorithms and mathematical optimization. Still others say that software engineers need only be able to design large, complex pieces of software, or manage teams of coders, or communicate project specifications, etc.</p>

<p>Few people correlate software engineering with art.</p>

<p>There are those that will argue that &#8220;software itself is a form of art,&#8221; or that &#8220;this code is beautiful.&#8221; There are certainly pieces of software, written in different languages, that could be considered their own distinct forms of &#8220;poetry.&#8221; (And no, I&#8217;m not just talking about <a href="http://community.schemewiki.org/?lisp-poetry">Lisp poetry</a>.) Elegance, cleverness, and the functionality of the code all contribute to this sense of inherent artistry.</p>

<p>I prefer to write code that is <em>outwardly</em> visible as art. Code that you need to run, not read, to appreciate.</p>

<p><img src="http://petersobot.com/images/body/cfru.png" title="[Thanks, Viv and Frank!]" ></p>

<p>This is <a href="http://cfru.ca/show">the schedule view</a> of a radio station&#8217;s website. (<a href="http://cfru.ca/">CFRU 93.3fm</a> at the University of Guelph, Ontario, to be exact.) I did not design this site - that was done by the wonderful folks at <a href="http://studiofunction.com/">Studio Function</a>. However, I did have the pleasure of implementing the design and creating the website itself during my last work term at <a href="http://theworkinggroup.ca">The Working Group</a>.</p>

<p>Although there is a fair amount of complexity behind this site, the part that was most enjoyable to implement was this schedule view. It helps that it&#8217;s beautiful and eye-catching, but writing code to make this design functional was extremely satisfying. Even more satisfying was the ability to see someone use the site, enjoy it, and being able to say &#8220;Yes, I helped make that.&#8221;</p>

<p>This site, via its design and partially through its functionality, is a form of art. I&#8217;ve spent my first three work terms (one year in total) working at web development shops on client projects, implementing (and sometimes designing) beautiful software that can be appreciated by almost anybody. I had a great time doing that, and enjoyed nearly every minute.</p>

<p><img src="http://petersobot.com/images/body/wub_tr.png" title="[wub wub wub]" ></p>

<p>This is the Wub Machine, my online music remixer. If you know me, or if you read this blog, I&#8217;m sure you&#8217;ve heard enough about it so far. One thing I haven&#8217;t talked about yet is the art behind it.</p>

<p>I initially created the Wub Machine as an experiment in computer-generated music. If I were an arts student (or even a grad student in some software programs), it would have made a great thesis project to explore computer-generated art. While the&nbsp;<a href="http://the.echonest.com">technology used to power it</a> is stunningly awesome, and the site itself is somewhat complex, that&#8217;s not the purpose of it. (Although, I did learn a lot.)</p>

<p>The average user of the site is not a technophile. They could care less about the software. However, the average user can definitely appreciate the product - a piece of music (ahem, mostly) that is not only listenable, but danceable and entertaining. Many would call it art.</p>

<p>(Hopefully.)</p>

<p><img src="http://petersobot.com/images/body/deadrising2.jpeg" title="[Aww yeah, zombies.]" ></p>

<p>This is a screenshot from Dead Rising 2, an awesome action-adventure game released a couple years ago by Capcom. Yes, those are zombies, and that&#8217;s the main character (Chuck Greene) using a modified yard tool to mow them down. It&#8217;s a great game, with a great story, great gameplay, visuals, music, and the like. Save for <a href="http://blogs.suntimes.com/ebert/2010/04/video_games_can_never_be_art.html">some vocal critics</a>, most people would consider this art.</p>

<p>Visual artists modeled Chuck Greene&#8217;s character. Writers crafted the brilliant story. Software engineers put it all together, and made the entirely immersive experience possible. Their work, while technical and complex, is just as much <em>art</em> as the models, textures, sounds and words in the game. It doesn&#8217;t just allow users to interact with art; it forms the fundamental experience that is enjoyed and appreciated.</p>

<p>Using software to make immersive, beautiful, artistic experiences that can be appreciated by anybody is awesome.</p>

<p>TL;DR: Software can be art, in many ways. That&#8217;s what I like to make.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The middle ground between form and function]]></title>
    <link href="http://petersobot.com/blog/form-vs-function/"/>
    <updated>2012-01-22T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/form-vs-function</id>
    <content type="html"><![CDATA[<p>I&#8217;ve noticed a distinct trend in all of my recent work. Not all of it is useful, and not all of it is feature-complete - but it all&nbsp;places a lot of importance on form over function. Let me give an example:</p>

<p><img src="http://petersobot.com/images/body/lndrme.png" title="[launder me, get it?]" ></p>

<p>Earlier this month, I put together a quick site called <a href="http://lndr.me">lndr.me</a>, which tracks the usage of laundry machines at VeloCity, my student residence at the University of Waterloo. It&#8217;s simple and email-driven. Residents can email <a href="mailto:washer@lndr.me">washer@lndr.me</a> to say that they&#8217;re using a washer, and they&#8217;ll get an email back in ~30 minutes to remind them that their clothes are done. Other residents can also check the site and see if the machines are occupied.</p>

<p>It&#8217;s an exceedingly simple idea, with very little code required on the backend. (It&#8217;s a Rails app with ~300 lines of ruby.) I&#8217;ve even made an API to allow other residents to make apps out of it, or link in hardware sensors with Arduinos and ethernet shields.</p>

<p>However, before I even had the idea fleshed out, or the implementation decided on, I did a mockup. I opened Photoshop, drew some icons, found a simple colour scheme, searched for a viable domain name, and scribbled a UX flow into my Moleskine before ever typing <code>rails new app</code>.</p>

<p>This simple (some call it cute) design was my starting point. I added some things along the way - animation on the waves in the washing machine to show it&#8217;s running, or a slightly-shaking dryer icon to show the same - but most of the product was finished before I started writing code. I essentially&nbsp;<strong>started from the user&#8217;s perspective and then built inwards</strong>.</p>

<p>Now, some people will surely think this is obvious. &#8220;Of course you wait for designs first before starting implementation, that&#8217;s just obvious!&#8221; you yell. In the client-and-project-driven world of software contracting, that&#8217;s absolutely true. Specs must be finalized, and designs (or at least mockups) finished before the product is built.</p>

<p>A lot of other people, though, are confused by this. &#8220;It&#8217;s only a side project, who cares how it looks?&#8221; you might say. Or &#8220;I&#8217;m not a designer, I&#8217;m a coder.&#8221; I&#8217;ve heard both of those far too often to dismiss.</p>

<p><strong>Your product&#8217;s user experience is just as important as what it does.</strong>&nbsp;Most apps do things that are marginally useful - track laundry, wake you up in the morning, play music, or give you directions. Would you use a music player that required a screwdriver to change songs? What about a map that gave directions in a series of JSON-encoded latitude and longitude coordinates, to then be decoded by the user? Of course not.</p>

<p>Products are successful, useful, and a joy to use if they have great user experience. A lot of hackers and coders nowadays don&#8217;t realize how important this is.</p>

<p>Let me give another example:</p>

<p><img src="http://petersobot.com/images/body/ninjaquote.png" title="[launder me, get it?]" ></p>

<p><a href="http://ninjaquote.com">Ninjaquote</a> is a site created by <a href="http://greenlay.net">Scott Greenlay</a>, <a href="http://kimyounjin.com">Jinny Kim</a> and myself in 24 hours (21:15, to be exact) during the recent Facebook hackathon at the University of Waterloo. Its goal is simple: it takes two of your Facebook friends, and finds something one of them said in the past, and quizzes you on it. The game is exceedingly simple, and has another dead-simple user experience.</p>

<ol>
<li>Click to authorize the app to view your Facebook account.</li>
<li>Receive quote.</li>
<li>Click answer.</li>
<li>See if you were correct.</li>
<li>Goto step 2.</li>
</ol>


<p>This simple UX, coupled with a good domain name and great mascot, makes the site a pleasure to use. So simple to use, in fact, that it won the hackathon.</p>

<p>This confused me at first. Other entries were far more technically complex - <a href="http://hachiapp.com">Hachi</a>&nbsp;was an in-browser collaborative code editor built in Node.js and Socket.IO. <a href="http://friendmozaic.com/">FriendMozaic</a>&nbsp;did some image processing to make your profile picture a mosaic of friends&#8217; pictures. <a href="http://privacyveil.heroku.com/">PrivacyVeil</a> used some crazy OpenCV processing to detect faces behind you while you work, and pop up an Excel spreadsheet to cover your Reddit browsing.</p>

<p>Our winning entry was effectively ~1000 lines of Javascript, CSS3 and HTML5. Nothing fancy, nothing new - just a working, effective, and addictive user experience. Having the minimum number of features wasn&#8217;t a hinderance, as we had design to make the site appealing anyways.</p>

<p><strong>tl;dr: Find the middle ground between form and function. It&#8217;s much more valuable than either extreme.</strong></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA["The Street Preacher" - A Hyper-Local Twitter Bot]]></title>
    <link href="http://petersobot.com/blog/hyper-local-tweets/"/>
    <updated>2011-11-30T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/hyper-local-tweets</id>
    <content type="html"><![CDATA[<p>I walk through Yonge &amp; Dundas Square in Toronto every day.</p>

<p><img src="http://petersobot.com/images/body/yonge_dundas.jpeg" title="[So. Many. People.]" ></p>

<p>That intersection, which some call Toronto&#8217;s equivalent of Times Square, has a large number of street preachers. Loud, startling, obnoxious people that yell warnings of doom or urge repentance. Silly people.</p>

<p>I decided to use <a href="https://dev.twitter.com/docs/streaming-api">Twitter&#8217;s real-time streaming API</a> to make an extremely specific location-based Twitter bot. The purpose? To respond to you if you tweet near the street preachers at Yonge &amp; Dundas, with similar messages. Call it art, or a statement about society, or making fun of those preachers, whatever - I call it a fun technical and social experiment.</p>

<p>Using <a href="http://arstechnica.com/open-source/guides/2010/04/tutorial-use-twitters-new-real-time-stream-api-in-python.ars/2">an excellent ArsTechnica article</a> as a guide, I created a quick Python script that watches the Twitter stream for a given area, and replies to tweets in a very specific location. (&plusmn;10 meters or so, by my guess.) If you&#8217;re one of the lucky few to tweet within those bounds, you&#8217;ll get a reply from @yonge_dundas:</p>

<p><img src="http://petersobot.com/images/body/godalmighty.png" title="[GOD ALMIGHTY!]" ></p>

<p>A day later, I decided to clean up the script (rewrite it in Ruby, too) and open-source it. Well, here it is, in a quick Github gist:</p>

<div><style type='text/css'>.gist-data{max-height: 300px}</style>
<script src='https://gist.github.com/1413715.js'></script>
<noscript><pre><code># &quot;The Street Preacher&quot;
# hyper-local twitter bot
#
# by Peter Sobot (psobot.com)
# November 29, 2011
#
# ---------------------------
#
# Instructions:
#   Place phrases in phrases.txt (one per line)
#   Set a latitude and longitude (@from_lat, @from_lng)
#   Set a radius (currently in degrees lat/long)
#   Set your twitter account's username (to prevent feedback)
#
#   Enter your Twitter application keys and OAuth credentials
#     (get them from https://dev.twitter.com/)
#
#   Run:
#     `ruby preacher.rb start`
#
#   ???
#
#   Profit! (Not really.)
#
# ---------------------------
#
# Defaults are set to Yonge &amp; Dundas Square, Toronto.
# @yonge_dundas is a twitter clone of the notorious street preachers
# that live at that intersection.
#
# ---------------------------

require 'rubygems'
require 'tweetstream'
require 'twitter'
require 'logger'

PHRASES = Dir.pwd + '/phrases.txt'
EXCLUDED_USERS = Dir.pwd + '/excluded_users.txt'
USER_TWEET_TIME_LIMIT = 10800 #in seconds, how often is too often? 3 hours.
ERROR_TIMEOUT = 300           #in seconds, how long do we wait if Twitter barks at us?

# Set this to your account's username, so it doesn't feedback loop.
USERNAME = 'yonge_dundas'

@logger = Logger.new STDERR

# Twitter phrases:
@phrases = IO.readlines(PHRASES).collect{|p|p.chomp}.compact.reject{|n|n.empty?}
def random_phrase
  @phrases.sort_by{ rand }.first
end

def excluded_users
  IO.readlines(EXCLUDED_USERS).collect{|p|p.chomp}.compact.reject{|n|n.empty?} + [USERNAME]
end

# Let's store a list of people and times we've tweeted at them, to avoid spam
@user_cache = {}

def hit_recently user_id
  !@user_cache[user_id].nil? &amp;&amp; (Time.now - @user_cache[user_id]) &lt; USER_TWEET_TIME_LIMIT
end

# Tweet from:
@from_lat = 43.65641564830964
@from_lng = -79.38105940818787
radius = 0.001 # catch area in degrees lat/lng

consumer_key = &quot;your_twitter_consumer_key_here&quot;
consumer_secret = &quot;your_twitter_consumer_secret_here&quot;
oauth_token = &quot;your_oauth_token_here&quot;
oauth_token_secret = &quot;your_oauth_token_secret&quot;

# Confiruationses
Twitter.configure do |config|
  config.consumer_key = consumer_key
  config.consumer_secret = consumer_secret
  config.oauth_token = oauth_token
  config.oauth_token_secret = oauth_token_secret
end

TweetStream.configure do |config|
  config.consumer_key = consumer_key
  config.consumer_secret = consumer_secret
  config.oauth_token = oauth_token
  config.oauth_token_secret = oauth_token_secret
  config.auth_method = :oauth
  config.parser = :yajl
end

# Let's make us a bounding box to give Twitter's streaming API
N = @from_lat + radius
S = @from_lat - radius
E = @from_lng + radius
W = @from_lng - radius

def parse_tweet status
  return if excluded_users.include? status[:user][:screen_name] 

  if status[:coordinates] and status[:coordinates][:type] == 'Point'
    lng, lat = status[:coordinates][:coordinates]

    if    lng &lt; [E, W].max \
      and lng &gt; [E, W].min \
      and lat &lt; [N, S].max \
      and lat &gt; [N, S].min

      @logger.info &quot;Got one! Replying to @#{status[:user][:screen_name]}:&quot;
      @logger.info &quot;\t#{status[:id]}: \&quot;#{status[:text]}\&quot;&quot;

      if not status[:in_reply_to_user_id] \
        and not status[:retweeted] \
        and status[:entities][:user_mentions].empty? \
        and not hit_recently(status[:user][:id])

        tweet = Twitter.update(
          &quot;@#{status[:user][:screen_name]} #{random_phrase}&quot;,
          :in_reply_to_status_id =&gt; status[:id],
          :lat =&gt; @from_lat,
          :long =&gt; @from_lng,
          :display_coordinates =&gt; true
        )
        
        @user_cache[status[:user][:id]] = Time.now

        @logger.info &quot;\t#{tweet[:id]}: \&quot;#{tweet[:text]}\&quot;&quot;
      else
        @logger.info &quot;Didn't reply - tweet was mention, retweet, reply, or spammy.&quot;
        @logger.info &quot;In reply to: &quot; + status[:in_reply_to_user_id].inspect
        @logger.info &quot;Retweeted? &quot; + status[:retweeted].inspect
        @logger.info &quot;Mentioned: &quot; + status[:entities][:user_mentions].inspect
        @logger.info &quot;User last hit at: &quot; + @user_cache[status[:user][:id]].inspect
      end

    else
      km_away = Math.sqrt(((lat - @from_lat) * 111)**2 + ((lng - @from_lng) * 79)**2)
      @logger.info &quot;Tweet not within bounding box:\t#{km_away} km away.&quot;
    end
  end
rescue Exception =&gt; ex
  @logger.error ex.message
  @logger.error ex.backtrace.join &quot;\n&quot;
end

client = TweetStream::Daemon.new('preacher', :log_output =&gt; true)
client.on_error { |message| @logger.error message }
client.on_reconnect { |timeout, retries| @logger.error &quot;Reconnect: timeout = #{timeout}, retries = #{retries}&quot; }

# Start filtering based on location
begin
  @logger.info &quot;Starting up the Street Preacher...&quot;
  client.locations(&quot;#{W},#{S},#{E},#{N}&quot;) { |status| parse_tweet status }
rescue HTTP::Parser::Error =&gt; ex
  # Although TweetStream should recover from
  # disconnections, it fails to do so properly.
  @logger.error &quot;HTTP Parser error encountered - let's sleep for #{ERROR_TIMEOUT}s.&quot;
  @logger.error ex.message
  @logger.error ex.backtrace.join &quot;\n&quot;
  sleep ERROR_TIMEOUT
  retry
end</code></pre></noscript></div>


<p>Feel free to fork it, repurpose it, and do whatever! (Just keep my name at the top, if you please.)</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[More Lessons from The Wub Machine]]></title>
    <link href="http://petersobot.com/blog/the-wub-machine-v20/"/>
    <updated>2011-11-10T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/the-wub-machine-v20</id>
    <content type="html"><![CDATA[<p>Four months ago, I released the Wub Machine, an online Dubstep remixing web app. It hit Reddit for a couple days, got popular on 4chan, and has since remixed nearly 24,000 songs. About a month ago, at the wonderful <a href="http://montreal.musichackday.org/2011/">Music Hack Day Montr&eacute;al</a>, I wrote and released an Electro-House remixer to complement the Dubstep one. It sounds kinda awesome - here&#8217;s Stevie Wonder, remixed:</p>

<iframe width="100%" height="166" scrolling="no" frameborder="no" src="http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F24183782&amp;"></iframe>


<p>Since then, I&#8217;ve polished up a completely new framework for the Wub Machine - nearly everything about the site has been rewritten since its first release. The first version was held together with duct tape, PHP and prayers, which resulted in some catastrophic failures when the site was initially launched. I&#8217;ve sinced rebuilt it in 100% Python, load tested, and added features.</p>

<p>Instead of talking about the code (<a href="https://github.com/psobot/wub-machine#readme">which I do over on GitHub</a>), I have a better story - being featured on the immensely popular VSauce channel on YouTube. I got a seven-second mention (and the thumbnail of the video!) and on Tuesday night, when the video first went up, all hell broke loose. My little <a href="http://prgmr.com">Prgmr</a> server couldn&#8217;t keep up with the 15,000 visits in 3 hours, and the load has kept up steadily ever since.more</p>

<p>A day later, I&#8217;ve flirted with Amazon EC2 and other hosting solutions, migrated databases back and forth, jumped up and down and watched live Google Analytics for far too long. I moved the site to a new host (not Linode, although they&#8217;re awesome) and finally, it can handle the load.</p>

<p>So, what lessons have I learned from <em>this</em> surge in Wub Machine usage?</p>

<ul>
<li><p><strong>Be prepared to scale on-demand.</strong> I should have built the site on some sort of scalable, EC2 or Heroku-based architecture that I can instantly increase capacity with. As it is now, I had to scramble to find a faster web host that I could deploy onto, wait for DNS to switch over, clone the database, etc. Ideally, I could have integrated the site with EC2 to detect surges in popularity and spin up a new worker instance. Alas, that costs money and I&#8217;m cheap.</p>

<ul>
<li><strong>Know your options for scaling, too.</strong>&nbsp;I didn&#8217;t realize I could have spun up instances running the same code, and just proxied with a cookie via one master Nginx machine. Either way, there&#8217;s a number of ways I could have scaled up, and I didn&#8217;t consider some of the most popular options, only because I hadn&#8217;t heard of them.</li>
</ul>
</li>
<li><p><strong>You never know who your next audience will be.</strong>&nbsp;I had previously prepared the site for Reddit, HN, Evolver.fm, etc: sites with a more tech-savvy audience. YouTube, on the other hand, is one of the most accessible sites on the internet. My latest surge in users has come from what seems to be teenagers and your average, casual YouTube user. (the kind with a four-digit number on the end of their username.)</p>

<ul>
<li>Why is that important? Well&#8230; Reddit, Hacker News and other similarly<strong> technical audiences respond badly to advertising</strong>. I had left off ads from the site for a number of reasons, one being they wouldn&#8217;t be very effective. As soon as I saw YouTube flocking to the site&#8230; I figured they&#8217;d be slightly more interested. And so far, they have been. Very much so. I think I&#8217;ll leave the ads in place. (The site can finally pay for itself!)</li>
</ul>
</li>
<li><p><strong>Know how to jump ship.</strong>&nbsp;In the past couple hours, as I&#8217;ve been trying to make the site faster, I&#8217;ve literally set up the site on 3 different servers. It saved me a ton of time to have an installer script that (mostly) worked. All I need to do now is make that automatic, and I can do something like a Capistrano deploy to add capacity.</p>

<ul>
<li><strong>&#8230;and know how to take your data with you!</strong>&nbsp;I forgot about my DB at one point, which caused a couple conflicts, and now my statistics are missing about 100 remixes. Not a big deal, but still - have some plan in place to flip databases over instead of copying them. Or something.</li>
</ul>
</li>
<li><p><strong>People will get fed up and leave.</strong>&nbsp;I have a handy statistics page now that shows me (in real-time!) the activity on the site. I can see as people upload a track, how it progresses, if it fails, why it failed, etc. I can also see clearly when people upload a track, don&#8217;t want to wait, and close their browser. Although that can sometimes happen due to an overloaded server, more than half of the people who visited during this spike turned away after seeing that they&#8217;d have to wait. Perhaps something to try to avoid, but that ties in to my next point&#8230;</p></li>
<li><p><strong>Care about your users&#8230; just enough.</strong>&nbsp;That sounds <em>incredibly</em>&nbsp;callous, so I need to clarify myself here. Any website owner should absolutely care about their users. User experience is, in my books, the most important thing to work on. However, if your app goes viral and/or reaches a very wide, very diverse audience, you have to balance that care for UX with your own needs and sanity. I&#8217;ve recieved a handful of emails so far from ordinary people, demanding that I get the site running faster, or that I add feature X and feature Y. Not suggestions, demands!</p>

<ul>
<li>If you can provide a working app, and do your best to keep it working and user-friendly, then you&#8217;ll still never hit 100% in all of your metrics. <strong>There will always be people who have a bad experience on the site</strong>, if only because of their own personal situation. (i.e.: browser, connection, or internet literacy) I usually try to go the extra mile and make what I&#8217;m working on close to perfect, but at some point, it becomes futile.</li>
</ul>
</li>
</ul>


<p>So, what&#8217;s next for the Wub Machine? I&#8217;m not sure. I still consider the algorithm a bit of a hack, although it sounds distinctive and kinda cool now. I&#8217;m planning a YouTube remixer, but that&#8217;s technically challenging. There are some small technical problems I can try to fix as well, and I can continue to improve the code base and open source the changes. And of course, I need to keep it running. Otherwise&#8230; statistics!</p>

<p><strong>Wub Machine Statistics (as of November 10, 2011):</strong></p>

<ul>
<li>637,568&nbsp;<strong>Facebook story impressions</strong></li>
<li>208,792 <strong>total site pageviews</strong></li>
<li>64,673 <strong>total unique visitors</strong></li>
<li>29,801&nbsp;<strong>total songs uploaded</strong></li>
<li>1,956 <strong>hours of music remixed (82&nbsp;<em>days</em> worth!)</strong></li>
<li>85% <strong>of tracks remixed successfully</strong></li>
<li>1,143&nbsp;<strong>remixes shared to SoundCloud</strong></li>
<li><strong>Most commonly-remixed artists:</strong>

<ol>
<li><strong>The Beatles</strong></li>
<li><strong>Daft Punk</strong></li>
<li><strong>Adele</strong></li>
<li><strong>Deadmau5</strong></li>
<li><strong>Skrillex</strong></li>
<li><strong>Gorillaz</strong></li>
<li><strong>Blink-182</strong></li>
<li><strong>Led Zeppelin</strong></li>
<li><strong>Radiohead</strong></li>
<li><strong>Deerhunter</strong> (oddly, only the song &#8221;<a href="http://www.youtube.com/watch?v=G5RzpPrOd-4">Helicopter</a>&#8221;)</li>
</ol>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The Wub Machine, Postmortem]]></title>
    <link href="http://petersobot.com/blog/the-wub-machine-post-mortem/"/>
    <updated>2011-06-25T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/the-wub-machine-post-mortem</id>
    <content type="html"><![CDATA[<p><a href="http://the.wubmachine.com">The Wub Machine</a>, my fancy dubstep-remixing web app, unexpectedly launched last week. In the days that followed, I took a crash course in how to manage a heavily-used web service. Here&#8217;s the first of many pretty graphs:</p>

<p><img src="http://petersobot.com/images/body/wub_firstweek.png" title="[First week stats. Dem spikes.]" ></p>

<ol>
<li><strong>Uploads </strong>(whenever a song was uploaded for remixing)</li>
<li><strong>Processing</strong>&nbsp;(<strong><em>started/finished/failed</em></strong>) (the analysis &amp; rendering of the remix)</li>
<li><strong>Sharing</strong>&nbsp;(sharing of a remix to SoundCloud)</li>
<li><strong>Downloads</strong>&nbsp;(when a user explicitly downloads their remix)</li>
</ol>


<p>All of these actions are graphed separately, to provide a detailed look at what happened over the first week of running the Wub Machine.</p>

<p>So, what <em>did</em>&nbsp;I learn?</p>

<p><img src="http://petersobot.com/images/body/wub_reddit.png" title="[Dat failure spike.]" ></p>

<ol>
<li><p><strong>Don&#8217;t pretend to have capacity.</strong></p>

<ul>
<li>I didn&#8217;t expect such massive server load, and as such, I assumed there would be no harm in allowing people to upload tracks when other tracks were being remixed. This ended up creating a never-ending queue of songs, and prevented anybody from effectively hearing a remix for what would have been a 16 hour wait. Those who did upload songs for remixing had to wait exorbitant amounts of time for their remixes to finish, and the service effectively ground to a halt. Instead, I should have implemented a system that forces users to wait until capacity is available. (And I did, afterwards. Currently, you can&#8217;t upload a track if the site is currently working on one.)</li>
<li>I did make an emergency fix, though: the giant red spike you see on the graph above is the queue of 900 songs, all from eager Redditors, being cleared at once. The queue would have taken nearly 16 hours to process, and effectively caused the site to grind to a halt while still on the front page of <a href="http://www.reddit.com/r/Music">/r/Music</a>. A significant portion of the songs uploaded had been abandoned, and wouldn&#8217;t have been heard, but were still stuck in the processing queue. Needless to say, the entire system could have used more load testing.</li>
</ul>
</li>
<li><p><strong>Test, test, and load test before pushing to production.</strong></p>

<ul>
<li>I had been testing on staging for about a week, on a low-powered server, with at most 5 songs processing at once. Had I tested the site in staging with heavier load, and accounted for very unexpected amounts of traffic, I would have been much better prepared for the initial spike.</li>
<li>I had prototyped the site using SQLite and with client-side polling to deliver progress updates, as it was simplest to develop. As soon as the front page of the Wub Machine was getting 40 pageviews/sec, SQLite crumbled in production, and PHP started using a ridiculous amount of resources due to the constant polling. It wasn&#8217;t until many days later that I had time to migrate the database to MySQL and patch the progress indicator to use long polling.</li>
<li>I had planned to spend the Monday testing the site with load from Twitter, before posting it to Reddit. Someone decided to post to Reddit before I had anticipated, and I wasn&#8217;t prepared to make the required changes. A lot of songs failed processing, a lot of bandwidth was used needlessly, and a lot of exposure was wasted with a site too busy to remix songs.</li>
</ul>
</li>
<li><p><strong>Expect users to abuse features.</strong></p>

<ul>
<li>I built the site with the ability to link to individual remixes. I provided a disclaimer letting people know the links would die after an hour, figuring that anybody who wanted to share a remix would post it to SoundCloud, or at least download it.<em> Not so.</em> Hundreds of people started sending around links, essentially turning the Wub Machine into a very bandwidth-heavy temporary remix-sharing site. Had I removed this feature from the start, I would have caused a minor inconvenience to approximately 5% of users, while saving me a ton of bandwidth and hassle.</li>
<li>Later in the week, when 4chan&#8217;s <a href="http://boards.4chan.org/mu/">/mu/</a>&nbsp;discovered the site, I counted a couple hundred links to individual Wub Machine remixes in each threads. Obviously, 4chan is a more anonymous site, and very few people wanted to share via SoundCloud. I tried to lessen the impact of the links by forcing each remix to expire within 15 minutes, instead of 1 hour, which helped slightly. As soon as the traffic died down and the site dropped off of /mu/, I immediately disabled the link feature to save me bandwidth. (Had I disabled the feature while it was popular, I risked a backlash from 4chan&#8230; not something anybody wants. Ever.)</li>
</ul>
</li>
<li><p><strong>Buy more capacity than necessary.</strong></p>

<ul>
<li>I expected the Wub Machine to use a significant amount of bandwidth and tons of server load, but I could have still used more to deal with the unexpected spikes. I started by buying a Linode 512 to host the site, which worked for a while, then started to choke once it hit Reddit. That was quickly upgraded to a Linode 1024, which worked wonders for a while. 4chan drove a lot more traffic than expected, and after a week, I had used up 220GB of bandwidth.</li>
<li>After the initial traffic spike and exposure, I shut down my Linode, moved the site back to my Prgmr, and put an hourly limit on the number of songs that can be remixed. I&#8217;ll probably still need to upgrade my server or increase its bandwidth next month. Who would have guessed that creating a cool web app costs money to keep running?</li>
<li>I also realized before even launching that making any money was a very slippery slope. I could have added Adsense to the site, and probably would have been able to recoup my server costs. However, I expected that if the site was ever seen by any record companies, making even one cent off of having people upload their music could be a massive legal liability. (Even though I delete uploads as soon as they&#8217;re done remixing and don&#8217;t keep remixes for more than 15 minutes, that wouldn&#8217;t stop people from taking issue.) Besides, as long as I limit the capacity and keep the site on the VPS I already own, it costs me very little to run.</li>
</ul>
</li>
</ol>


<p><img src="http://petersobot.com/images/body/wub_4chan.png" title="[Dem channers.]" ></p>

<p>So, there you have it.&nbsp;Final first-week stats: 15,793 uniques,&nbsp;46,062 pageviews, 1,950 Facebook likes,&nbsp;273,640 story impressions on Facebook, 139 tweets and&nbsp;8,808 songs remixed.</p>

<p>The Wub Machine&#8217;s not dead, but it&#8217;s slower, and during the first week, I learned a lot more than I&#8217;d ever expected to learn about scaling web services. Having a site go viral was quite an unexpected, thrilling, and crazy experience.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The Wub Machine, Revisited]]></title>
    <link href="http://petersobot.com/blog/the-wub-machine-revisited/"/>
    <updated>2011-06-12T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/the-wub-machine-revisited</id>
    <content type="html"><![CDATA[<p><a href="http://blog.petersobot.com/the-wub-machine">The Wub Machine</a> was a great little auto-remixer project - some audio hackery in Python to make a neat script. Unfortunately, I can probably count on one hand the number of people who <em>actually</em> downloaded the script and tried it on their own songs. So, I decided to <a href="http://the.wubmachine.com">make it into a web app</a>. (tl;dr: <a href="http://the.wubmachine.com">go try out the site now</a>!)&nbsp;</p>

<p><img src="http://petersobot.com/images/body/wub_2.png" title="[Oh, Paul Gilbert...]" ></p>

<p>moreI opened up my trusty Photoshop, cranked out some multicoloured waves and set &#8220;The Wub Machine&#8221; in beautiful <a href="http://www.ms-studio.com/FontSales/proximanova.html">Proxima Nova</a>. Then I set about the immense task of actually implementing the remixer on the web.</p>

<p>I&#8217;d go into the technical impressiveness of the system, and how it&#8217;s brilliant and took me months to come up with&#8230; but it&#8217;s really not. It&#8217;s <strong>one big hack</strong>.</p>

<p>I ended up using:</p>

<ul>
<li><strong>PHP</strong> to serve the front-end, as well as serve the AJAX progress updates and interface with SoundCloud</li>
<li><strong>Python</strong>&nbsp;to power and tie together all of the processing on the back-end</li>
<li><strong><a href="http://code.google.com/p/echo-nest-remix/">the Echo Nest Remix API</a>&nbsp;</strong>to do the heavy lifting, audio analysis and beat detection</li>
<li><strong><a href="http://www.ffmpeg.org/">FFMPEG</a></strong> to decode &amp; encode the MP3s</li>
<li><strong><a href="http://code.google.com/p/mutagen/">Mutagen</a> and <a href="http://www.pythonware.com/products/pil/">PIL</a></strong>&nbsp;to rewrite the MP3&#8217;s metadata, extract artwork, overlay a graphic and put it back in to the final MP3</li>
<li><strong><a href="http://kr.github.com/beanstalkd/">Beanstalkd</a>&nbsp;</strong>to queue processing jobs and&nbsp;connect PHP to Python</li>
<li><strong>SQLite3</strong>&nbsp;for logging and some queue intelligence</li>
<li><strong>HTML5 Audio</strong>, used for a beautiful HTML5 player (taken from the <a href="http://neutroncreations.com/blog/building-a-custom-html5-audio-player-with-jquery/">extremely impressive Neutron Creations blog</a>)</li>
<li><strong>Flash</strong>&nbsp;for the fallback player on older browsers</li>
<li><strong>Javascript and jQuery</strong>&nbsp;to hold&nbsp;together the <em>very rickety</em> frontend</li>
<li><strong>CSS3</strong> animations, for the moving waves at the top of the page</li>
<li><strong>the <a href="http://soundcloud.com/api">SoundCloud API</a></strong>&nbsp;for sharing tracks (without putting me at risk of nasty legal issues or pushing storage constraints)</li>
</ul>


<p>I did have to make a couple changes to the original algorithm, though:</p>

<ul>
<li>I realized that audio volume is a nonlinear curve, so I had to account for that and create a new mixing algorithm. The volume of the original track vs. the wubwubs is now almost always about 50%.</li>
<li>I went back into my dubstep template in Logic Pro and added different types of <a href="http://blog.bronto-scorpio-music.com/?p=134">TransitionFX</a>&nbsp;samples to the intro and the wubs - booms, splashes and such. Although I&#8217;m still not happy with certain parts of the template, it&#8217;ll have to do for now. I&#8217;m not a dubstep producer - <a href="http://music.petersobot.com">I&#8217;m a rock/metal/electronica/jazz guy</a>. (for now!)</li>
<li>I made the algorithm as deterministic as possible. The remixer is essentially a function (depending on the analysis I get back from the Echo Nest) so if you put in the same song, you should get the exact same remix.</li>
<li>I improved the loudness calculation algorithms, fixed some stupidly-inefficient bugs, killed off a statistically-improbable-but-still-possible infinite loop, added logging, error handling, and progress indicators.</li>
</ul>


<p>Since my blog post about the initial hack, it&#8217;s taken me&nbsp;3 weeks to assemble this web front end. That said, there&#8217;s probably still tons of bugs - it only accepts MP3s at the moment, and it&#8217;s probably somewhat unstable. If I push it too hard, or post it to Reddit or Hacker News, my shiny new Linode will probably spontaneously combust. Be gentle!</p>

<p><a href="http://the.wubmachine.com">Go try out the Wub Machine</a>, share your tracks on SoundCloud, and enjoy!</p>

<p>&nbsp;</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The Wub Machine]]></title>
    <link href="http://petersobot.com/blog/the-wub-machine/"/>
    <updated>2011-05-23T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/the-wub-machine</id>
    <content type="html"><![CDATA[<p><strong>UPDATE:</strong>&nbsp;I turned the Wub Machine into a website. Go and&nbsp;<a href="http://the.wubmachine.com">remix your own tracks!</a></p>

<p><strong>I like dubstep.</strong></p>

<p>There, I said it!</p>

<iframe width="540" height="330" src="http://www.youtube.com/embed/LaIZ0mUJzr0 " frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>


<p>That massive bassline, two-step beat and killer rhythm has some odd allure that I can&#8217;t resist - and I&#8217;m typically a fan of rock, metal and prog!</p>

<p>I&#8217;m also a huge fan of the Echo Nest and their brilliant <a href="http://code.google.com/p/echo-nest-remix/">Remix API</a>. In their words, the Remix API is an &#8220;internet synthesizer&#8221; - quite true. I can send off an mp3, and get back extremely detailed beat, timbre and pitch information within seconds. Some people have already used this to <a href="http://musicmachinery.com/2010/05/21/the-swinger/">make any song swing</a>, <a href="http://www.donkdj.com/">put a donk on any song</a>, and much, much more.</p>

<p>For the first <a href="http://www.sehackday.com/">SE Hack Day</a>, I decided to use the Remix API to <strong>automagically add dubstep</strong> to any song.</p>

<iframe width="100%" height="450" scrolling="no" frameborder="no" src="http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F805784&amp;"></iframe>


<p>Now, as you may be able to tell from the samples above, this isn&#8217;t quite ready yet. In fact, it&#8217;s extremely rough. Even the code looks horrifically ugly. (And yes, you can download it, fork it, and edit it freely - <a href="https://github.com/psobot/wub-machine">it&#8217;s open sourced on GitHub</a>.) I have a lot of work left to make the results sound passable.</p>

<p>Technically, I&#8217;m not really doing anything too complicated:</p>

<ul>
<li>I used Logic Pro and Native Instruments&#8217; new <a href="http://www.native-instruments.com/en/products/producer/powered-by-reaktor/razor/">Reaktor</a> synth to make some dirty, dirty wub basslines at the proper dubstep tempo (140 bpm) and mangled my own kick and snare samples. I then rendered 8 bars of this pattern, in two different variants, in every key of one octave.</li>
<li>Using Python and the Echo Nest Remix API, I get an analysis of each track&#8217;s bars, beats, pitches, timbres and more. I still need to make some better use of this information, as right now, a lot of songs end up being detected and used improperly. (an 8th or 16th note off, ruining the beat)</li>
<li>Using the Echo Nest Remix API&#8217;s Dirac time-stretching abilities, I take the input song, bar-by-bar, and alter the tempo to be exactly 140bpm. Then, for each &#8220;section&#8221; of the song (as defined by, again, the API&#8217;s analysis) I do 16 bars of dubstep with a repeating 8-bar pattern of either beats, bars or tatums (notes) from the original song.</li>
<li>For the dubstep backing, I take whatever key the API tells me the song is in, and just choose the corresponding backing file from the ones I&#8217;ve prerendered.</li>
<li>For the intro, I have a pre-rendered intro with some noise sweeps. I then use a bit of brute-force audio manipulation to play a build-up pattern before the initial &#8220;drop&#8221; after 8 bars.</li>
</ul>


<p>I&#8217;m taking a ton of suggestions on how to improve the script - adding variables for time till drop, allowing overrides if the API mis-identifies the key or tempo, and allowing a different choice of beat, bar or tatum for sampling the original song. There&#8217;s obviously tons of work left to do, and I plan to improve it whenever I get the chance.</p>

<p>Let me know what you think below in the comments, leave some suggestions, and check out <a href="https://github.com/psobot/wub-machine">the code on GitHub</a> if you&#8217;re interested!</p>

<p><strong>UPDATE:</strong> I built a web interface and a very nice-looking site around the basic algorithm. Go and <a href="http://the.wubmachine.com">remix your own tracks!</a></p>

<p>&nbsp;</p>

<p>P.S: When I said I like dubstep, I meant I <em>really do</em> like dubstep. When I&#8217;m not <a href="http://music.petersobot.com">making my own music</a>, I also enjoy drumming to dubstep:</p>

<iframe width="540" height="330" src="http://www.youtube.com/embed/vts0-CXmiK8 " frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>



]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[A Better Music Workflow?]]></title>
    <link href="http://petersobot.com/blog/music-workflow/"/>
    <updated>2011-03-17T00:00:00-04:00</updated>
    <id>http://petersobot.com/blog/music-workflow</id>
    <content type="html"><![CDATA[<p>I produce <em>a lot</em> of music.</p>

<p><img src="http://petersobot.com/images/body/logic.png" title="[One of many, many songs.]" ></p>

<p>I don&#8217;t necessarily release a lot of it (or finish all of it) but I have at least 100 songs I consider developed enough to listen to, and about 300 other song files that are just riffs, beats and vocal ideas floating alone.</p>

<p>With so many files, projects, songs, sounds, and work in one place, I&#8217;ve developed musical workflow that borrows a lot from software development patterns. My music tends to be produced in stages and cycles.</p>

<p>I&#8217;ll usually start with an idea from noodling around on some instruments, then do two things:</p>

<ol>
<li>Record (<em>track</em>) instruments with the main riffs of the song or basic chord structure.</li>
<li>Record a basic drum loop and bass loop, then arrange to fit song structure.</li>
</ol>


<p>Then, I don&#8217;t do anything.</p>

<p>For any length of time between a day and a month, I usually don&#8217;t touch the song-in-progress. The song needs a break, while I can forget about whatever ideas I had while writing the initial sections of the song.</p>

<p>Then, I&#8217;ll come back to it, add some more riffs, ideas, melody, direction, possibly a scratch vocal track. Then the real fun begins.</p>

<p><img src="http://petersobot.com/images/body/guitarrig.png" title="[Man, I love Guitar Rig.]" ></p>

<ol>
<li>Re-record any parts with mistakes or even rhythm that&#8217;s slightly off.</li>
<li>Add whatever new sections, parts, instruments, riffs, or notes that need to be added.</li>
<li>Make updates, edits and changes written down from previous listens.</li>
<li>Bounce to disk.</li>
<li>Place .flac in Dropbox for remote listening and burn to CD for in-car auditioning.</li>
<li>Make comments on SoundCloud/Evernote on each track.</li>
<li>See step 1.</li>
</ol>


<p>This process continues until I find it extremely hard to find changes to make to a track, or until I&#8217;ve reached the self-imposed deadline and am happy with the track.</p>

<p>In an ideal world, I wouldn&#8217;t need to burn CDs for the car - I could plug in my phone and have it stream the latest version of the song from SoundCloud with no user intervention. (very similar to &#8220;nightlies&#8221; in the open-source programming world.) I could listen at work, make comments (that would get synced back into Logic), listen on my phone, make comments, send the links to others, get their feedback, and have this all happen with one utility.</p>

<p>(just to round out the post with some audio, here&#8217;s one of my SoundCloud tracks.)</p>

<iframe width="100%" height="166" scrolling="no" frameborder="no" src="http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F8899825&amp;"></iframe>


<p>Unfortunately, if I continue writing this script at the moment, I won&#8217;t have any time to finish the album the tool would be created for. Once I&#8217;ve released my next album, (ETA: May 2011) I&#8217;ll be open-sourcing some cool tools to make such a workflow easy.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Pushed to my Pocket]]></title>
    <link href="http://petersobot.com/blog/pushed-to-my-pocket/"/>
    <updated>2011-01-23T00:00:00-05:00</updated>
    <id>http://petersobot.com/blog/pushed-to-my-pocket</id>
    <content type="html"><![CDATA[<p>As soon as I got my shiny new iPhone about six months ago, I set up instant Push email. This remarkably useful feature has really changed the way I use my email accounts and respond to email. It&#8217;s also made me reflect on exactly how dynamic and instant the web has become.</p>

<p>A lot of people wouldn&#8217;t expect that posting a YouTube comment like this:</p>

<p><img src="http://petersobot.com/images/body/response.png" title="[Thank you, YouTube!]" ></p>

<p>&#8230;would cause the device in my pocket (or on my nightstand) to vibrate and alert me instantly. This wasn&#8217;t even something I had to go out of my way to set up - YouTube&#8217;s default email notification settings accomplished this.</p>

<p>Responding to a tweet, commenting on Facebook, or any other number of nearly-instinctive online actions people do nowadays all cause unexpected side-effects: vibrating phones. It&#8217;s not a bad thing, nor is it even that annoying. (yet) Just somewhat mind-blowing that the click of a mouse on one side of the world will (near-)instantly cause a device in my pocket to vibrate and alert me.</p>

<p>And I still think the world could and should be more connected.</p>
]]></content>
  </entry>
  
</feed>
