Category: Programming

  • Bluetooth, LineIn, Soundflower: talking over Skype and playing music

    Someone who wants to teach dance classes online asked me if there was a reasonable way (i.e.., without spending a lot of money) to set up a Skype link that can be used for both music and a wireless microphone setup.

    The plan is to put something together that allows her to

    • Get far enough away from the camera that she can be seen head to toe (being able to see the footwork is important) and with a wide enough angle that she doesn’t have to dance unnaturally in one spot.
    • Send iTunes output and her voice over the line at the same time to one or more people,  in sync to the music.
    • Have some kind of a wireless mic to be able to communicate to her students without shouting.
    • Be able to hear her students talk back without their hearing their own voices delayed, or her hearing her own voice, delayed.

    This turns out to be more complicated than it might seem. The iSight camera doesn’t work very well for this; its field of view is quite narrow, and it’s very difficult to adjust it so that it pointed properly on top of that. This was relatively easy to solve: a Logitech HD Pro 920 works fine for both the wide-angle and head-to-toe issues; it can be mounted on a tripod (it has the necessary threading to mount on a standard photo tripod), and after an upgrade to a more powerful laptop – her 2008 MacBook Air was just not cutting it! – the video issue was solved.

    The audio issue was thornier. Originally, I hit up Sweetwater Sound for a real wireless mic setup; after realizing this was going to be well north of $300 once I got the mic, the base station, and the computer interface to actually hook it up with, and that this was going to be a lot of different hardware issues to deal with as well, I decided I’d better scout around for a better option.

    I was stuck until the instructor suggested a Bluetooth headset instead. It’s a reasonable, good-enough audio input channel at 8KHz – she wants to talk across it, not record studio-quality audio, so a little bit tinny is OK – and it’s definitely wireless. After a bit of investigation, I settled on the Jawbone ERA as the most-likely-workable option. The ERA is light, small, fits tightly (important for a dancer) and is the current best headset suggestion from Wirecutter, who I have learned to trust on stuff like this. It’s easy to connect a Bluetooth headset to OS X (getting it to talk properly to the software’s a different issue, see below). This takes a lot of hardware complication out of the way. Skype supports Bluetooth, so I thought I’d solved the problem.

    Unfortunately, an audio test with the music and voice both going through the Bluetooth mic showed me I’d have to get more creative; the music was either inaudible or distorted (that 8KHz bandwidth made it sound hideous, when you could hear it at all). It needed to be audible and undistorted if it was going to be possible for a student on the far end to use it to dance along with.

    A lot of Googling finally led me to thisevilempire’s blog entry on how to play system audio in Skype calls on OS X. This got me part of the way: I had, according to tests with the Skype Audio Tester “number”, gotten the audio to play nicely across the link, but I was getting a half-second delay of my voice back on the same channel, which made it hard to talk continuously. Not good enough for an instructor.

    More searching found a post on Lockergnome spelling out how to transmit clean audio, overlay voice,  and hear the returned call without an echo. Here’s how:

    1. Install Soundflower and LineIn, both free.
    2. Make sure the Bluetooth headset is on.
    3. Open the Sound preference pane in System Preferences.
    4. Set the
      1. Jawbone ERA as the input device
      2. Soundflower (64ch) as the output.
    5. Duplicate LineIn in the Applications folder, and rename both copies: one to “LineIn Bluetooth” and the other to “Bluetooth System”. The names aren’t important; this just so you can tell them apart.
    6. Launch both copies of LineIn. You’ll need to drag one window aside to reveal them both; they initially launch in exactly the same spot.
    7. Choose the “LineIn Bluetooth” instance in the Dock, and set
      1. Input to “ERA by Jawbone”
      2. Output to Soundflower (2ch).
      3. Click the “Pass thru” button.
    8. Select the other instance, “LineIn System”, and set
      1. Input to Soundflower (16ch)
      2. Output to Soundflower (2ch).
      3. Click the “Pass thru” button.
    9. Run Soundflowerbed (installed in the Applications folder by the Soundflower install). In the menu bar, click on the little flower icon, and
      1. Select “None” under Soundflower (2ch)
      2. Select “Built-in Output” under Soundflower (16ch).
    10. Run Skype, and open its preferences.
      1. Select “Soundflower 2ch” in its Microphone pulldown, and leave everything else alone.
      2. If you have an alternate camera attached, switch the Camera pulldown to the appropriate camera.

    You should now be able to make a Skype call, and play music from iTunes, DVD Player, or Youtube over the wire at full fidelity, and talk at the same time. You should hear the far end’s voice on your  speakers, along with the music you’re sending across (undelayed).

    Try to keep the headset away from the speakers to minimize the chances of feedback.

    It’s not all that  difficult; it’s just the tricky bits of being able to reroute the audio internally via the two LineIn instances and Soundflower. Getting those tricky bits right is the difficult part.

    I’ve tested this with the Skype test call and it seems to have worked; the big test will be the full-up video camera plus the streaming audio. We’ll give that a shot soon and I’ll follow up on whether the Bluetooth mic is good enough, or if a better mic is needed.

    Update: Undoing the process!

    It’s necessary to restore the normal audio routing after the call; you can do this with System Preferences.

    1. Open System Preferences and select Sound.
    2. Set Input to Internal Microphone. If you’re wearing the ERA, it will make a little descending bleep to let you know it’s been disconnected.
    3. Set Output to Internal Speakers.
    4. Quit both copies of LineIn.
    5. Check the Soundflowerbed menu; it should have both Soundflower 2ch and SoundFlower 64ch pointing to None. Quit Soundflowerbed.
    6. Turn off the Bluetooth headset; put it on its charger for a while.
    7. Quit Skype.

    You should be all set.

  • Mojolicious Revolutions

    3rd in my series of talks at SVPerl about Mojolicious; this one reviews using the server-side features for building bespoke mock servers, and adds a quick overview of the Mojo client features, which I had missed until last week. Color me corrected.

    Mojolicious Revolutions

     

  • youtube-dl: it just works

    I was having trouble watching the Théâtre du Châtelet performance of Einstein on the beach at home; my connection was stuttering and buffering, which makes listening to highly-pulsed minimalist music extremely unrewarding. Nothing like a hitch in the middle of the stream to throw you out of the zone that Glass is trying to establish. (This is a brilliant staging of this opera and you should go watch it Right Now.)

    So I started casting around for a way to download the video and watch it at my convenience. (Public note: I would never redistribute the recording; this is solely to allow me to timeshift the recording such that I can watch it continuously.) I looked at the page and thought, “yeah, I could work this out, but isn’t there a better way?” I searched for a downloader for the site in question, and found it mentioned in a comment in the GitHub pages for youtube-dl.

    I wasn’t 100% certain that this would work, but a quick perusal seemed to indicate that it was a nicely sophisticated Python script that ought to be able to do the job. I checked it out and tried a run; it needed a few things installed, most importantly ffmpeg. At this point I started getting a little excited, as I knew ffmpeg should technically be quite nicely able to do any re-enoding etc. that the stream might need.

    A quick brew install later, I had ffmpeg, and I asked for the download (this is where we’d gotten to while I’ve been writing this post):

    $ youtube_dl/__main__.py http://culturebox.francetvinfo.fr/einstein-on-the-beach-au-theatre-du-chatelet-146813
     [culturebox.francetvinfo.fr] einstein-on-the-beach-au-theatre-du-chatelet-146813: Downloading webpage
     [culturebox.francetvinfo.fr] EV_6785: Downloading XML config
     [download] Destination: Einstein on the beach au Théâtre du Châtelet-EV_6785.mp4
     ffmpeg version 1.2.1 Copyright (c) 2000-2013 the FFmpeg developers
     built on Jan 12 2014 20:50:55 with Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
     configuration: --prefix=/usr/local/Cellar/ffmpeg/1.2.1 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-nonfree --enable-hardcoded-tables --enable-avresample --enable-vda --cc=cc --host-cflags= --host-ldflags= --enable-libx264 --enable-libfaac --enable-libmp3lame --enable-libxvid
     libavutil 52. 18.100 / 52. 18.100
     libavcodec 54. 92.100 / 54. 92.100
     libavformat 54. 63.104 / 54. 63.104
     libavdevice 54. 3.103 / 54. 3.103
     libavfilter 3. 42.103 / 3. 42.103
     libswscale 2. 2.100 / 2. 2.100
     libswresample 0. 17.102 / 0. 17.102
     libpostproc 52. 2.100 / 52. 2.100
     [h264 @ 0x7ffb5181ac00] non-existing SPS 0 referenced in buffering period
     [h264 @ 0x7ffb5181ac00] non-existing SPS 15 referenced in buffering period
     [h264 @ 0x7ffb5181ac00] non-existing SPS 0 referenced in buffering period
     [h264 @ 0x7ffb5181ac00] non-existing SPS 15 referenced in buffering period
     [mpegts @ 0x7ffb52deb000] max_analyze_duration 5000000 reached at 5013333 microseconds
     [mpegts @ 0x7ffb52deb000] Could not find codec parameters for stream 2 (Unknown: none ([21][0][0][0] / 0x0015)): unknown codec
     Consider increasing the value for the 'analyzeduration' and 'probesize' options
     [mpegts @ 0x7ffb52deb000] Estimating duration from bitrate, this may be inaccurate
     [h264 @ 0x7ffb51f9aa00] non-existing SPS 0 referenced in buffering period
     [h264 @ 0x7ffb51f9aa00] non-existing SPS 15 referenced in buffering period
     [hls,applehttp @ 0x7ffb51815c00] max_analyze_duration 5000000 reached at 5013333 microseconds
     [hls,applehttp @ 0x7ffb51815c00] Could not find codec parameters for stream 2 (Unknown: none ([21][0][0][0] / 0x0015)): unknown codec
     Consider increasing the value for the 'analyzeduration' and 'probesize' options
     Input #0, hls,applehttp, from 'http://ftvodhdsecz-f.akamaihd.net/i/streaming-adaptatif/evt/pf-culture/2014/01/6785-1389114600-1-,320x176-304,512x288-576,704x400-832,1280x720-2176,k.mp4.csmil/index_2_av.m3u8':
     Duration: 04:36:34.00, start: 0.100667, bitrate: 0 kb/s
     Program 0
     Metadata:
     variant_bitrate : 0
     Stream #0:0: Video: h264 (Main) ([27][0][0][0] / 0x001B), yuv420p, 704x396, 12.50 fps, 25 tbr, 90k tbn, 50 tbc
     Stream #0:1: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 102 kb/s
     Stream #0:2: Unknown: none ([21][0][0][0] / 0x0015)
     Output #0, mp4, to 'Einstein on the beach au Théâtre du Châtelet-EV_6785.mp4.part':
     Metadata:
     encoder : Lavf54.63.104
     Stream #0:0: Video: h264 ([33][0][0][0] / 0x0021), yuv420p, 704x396, q=2-31, 12.50 fps, 90k tbn, 90k tbc
     Stream #0:1: Audio: aac ([64][0][0][0] / 0x0040), 48000 Hz, stereo, 102 kb/s
     Stream mapping:
     Stream #0:0 -> #0:0 (copy)
     Stream #0:1 -> #0:1 (copy)
     Press [q] to stop, [?] for help
     frame=254997 fps=352 q=-1.0 size= 1072839kB time=02:49:59.87 bitrate= 861.6kbits/s

    Son of a gun. It works.

    I’m waiting for the download to complete to be sure I got the whole video, but I am pretty certain this is going to work. Way better than playing screen-capture games. We’ll see how it looks when we’re all done, but I’m quite pleased to have it at all. The download appears to be happening at about 10x realtime, so I should have it all in about 24 minutes, give or take (it’s a four-hour, or 240 minute, presentation).

    Update: Sadly, does not work for PBS videos, but you can actually buy those; I can live with that.

  • Test::Routine slides

    This is my Test::Routine slide deck for the presentation I ended up doing from memory at the last SVPerl.org meeting. I remembered almost all of it except for the Moose trigger and modifier demos – but since I didn’t have any written yet, we didn’t miss those either!

    Update: My WordPress installation seems to have misplaced this file. I’ll look around for it and try to put it back soon.

  • Intro to Perl Testing at SVPerl

    A nice evening at SVPerl – we talked about the basic concepts of testing, and walked through some examples of using Test::Simple, Test::More, and Test::Exception to write tests. We did a fair amount of demo that’s not included in the slides – we’ll have to start recording these sometime – but you should be able to get the gist of the talk from the slides.
    [wpdm_file id=2]

  • CrashPlan folder date recovery

    The situation: a friend had a MacBook Air whose motherboard went poof. Fortunately she had backups (almost up-to-date) in CrashPlan, so she did a restore of her home directory, which worked fine in that she had her files, but not so fine in that all the folder last-changed dates now ran from the current date to a couple days previous (it takes a couple days to recover ~60GB of data).

    This was a problem for her, because she partly uses the last-changed date on her folders to help her keep organized. “When was the last time I did anything on project X?” (I should note: she uses Microsoft Word and a couple different photo library managers, so git or the equivalent doesn’t work well for her workflow. She is considering git or the like now for her future text-based work…)

    A check with CrashPlan let us know that they did not track folder update dates and couldn’t restore them. We therefore needed to come up with a way to re-establish as best we could what the dates were before the crash.

    Our original thought was simply to start at the bottom and recursively restore the folder last-used dates using touch -t, taking the most-recently-updated file in the folder as the folder’s last-updated date. Some research and thought turned up the following:

    • Updating a file updated the folder’s last-updated date.
    • Updating a folder did not update the containing folder’s last-updated date.

    This meant that we couldn’t precisely guarantee that the folder’s last-updated date would accurately reflect the last update of its contents. We decided in the end that the best strategy for her was to “bubble up” the last-updated dates by checking both files and folders contained in a subject folder. This way, if a file deep in the hierarchy is updated, but the files and folders above it have not been, the file’s last-updated date is applied to its containing folder, and subsequently is applied also to each containing folder (since we’re checking both files and folders, and there’s always a folder that has the last-updated date that corresponds to the one on the deeply-nested file). This seemed like the better choice for her as she had no other records of what had been worked on when, and runs a very nested set of folders.

    If you were running a flatter hierarchy, only updating the folders to the last-updated date of the files might be a better choice.  Since I was writing a script to do this anyway, it seemed reasonable to go ahead and implement it so that you could choose to bubble up or not as you liked, and to also allow you to selectively bubble-up or not in a single directory.

    This was the genesis of date-fixer.pl. Here’s the script. A more detailed example of why neither approach to restoring the folder dates is perfect is contained in the POD.
    [wpdm_file id=1]

    use strict;
    use warnings;
    use 5.010;
    
    =head1 NAME
    
    date-fixer.pl - update folder dates to match newest contained file
    
    =head1 SYNOPSIS
    
    date-fixer.pl --directory top_dir_to_fix
                 [--commit]
                 [--verbose]
                 [--includefolders]
                 [--single]
    
    =head1 DESCRIPTION
    
    date-fixer.pl is meant to be used after you've used something like CrashPlan
    to restore your files. The restore process will put the files back with their
    proper dates, but the folders containing those files will be updated to the
    current date (the last time any operation was done in this folder -
    specifically, putting the files back).
    
    date-fixer.pl's default operation is to tell you what it would do; if you want
    it to actually do anything, you need to add the --commit argument to force it
    to actually execute the commands that change the folder dates.
    
    If you supply the --verbose argument, date-fixer.pl will print all the commands
    it is about to execute (and if you didn't specify --includefolders, warn you
    about younger contained folders - see below). You can capture these from STDOUT
    and further process them if you like.
    
    =head2 Younger contained folders and --includefolders
    
    Consider the following:
    
        folder1           (created January 2010 - date is April 2011)
            veryoldfile 1 (updated March 2011)
            oldfile2      (updated April 2011)
            folder2       (created June 2012 - date is July 2012)
                newfile   (updated July 2012)
    
    If we update folder1 to only match the files within it, we won't catch that
    folder2's date could actually be much more recent that that of either of the
    files directly contained by folder1. However, if we use contained folder dates
    as well as contained file dates to calculate the "last updated" date of the
    current folder, we may make the date of the current folder considerably more
    recent than it may actually have been.
    
    Example: veryoldfile1 and oldfile2 were updated in March and April 2011.
    Folder2 was updated in June 2012, and newfile was added to in in July 2012.
    The creation of folder2 updates the last-updated date of folder1 to June 2012;
    the addition of newfile updates folder2's last-updated date to that date --
    but the last-updated date of folder1 does not change - it remains June 2012.
    
    If we restore all the files and try to determine the "right" dates to set the
    folder update dates to, we discover that there is no unambiguous way to decide
    what the "right" dates are. If we use the file dates, alone, we'll miss that
    folder2 was created in June (causing folder1 to update to June); if we use
    both file and folder dates, we update folder1 to July 2012, which is not
    accurate either.
    
    date-fixer.pl takes a cautious middle road, defaulting to only using the files
    within a folder to update that folder's last-modified date. If you prefer to
    ensure that the newest date buried in a folder hierarchy always "bubbles up"
    to the top, add the --includefolders option to the command.
    
    date-fixer will, in verbose mode, print a warning for every folder that
    contains a folder younger than itself; you may choose to go back and adjust
    the dates on those folders with
    
    date-fixer.pl --directory fixthisone --includefolders --single
    
    This will, for this one folder, adjust the folder's last-updated date to the
    most recent date of any of the items contained in it.
    
    =head1 USAGE
    
    To fix all the dates in a directory and all directories below it, "bubbling
    up" dates from later files:
    
        date-fixer.pl --directory dir --commit --includefolders
    
    To fix the dates in just one directory based on only the files in it and
    ignoring the dates on any directories it contains:
    
        date-fixer.pl --directory dir --commit --single
    
    To see in detail what date-fixer is doing while recursively fixing dates,
    "bubbling up" folder dates:
    
        date-fixer.pl --directory dir --commit --verbose --includefolders
    
    =head1 NOTES
    
    "Why didn't you use File::Find?"
    
    I conceived the code as a simple recursion; it seemed much easier to go ahead and read the directories
    myself than to go through the mental exercise of transforming the treewalk into an iteration such as I
    would need to use File::Find instead.
    
    =head1 AUTHOR
    
    Joe McMahon, mcmahon@cpan.org
    
    =head1 LICENSE
    
    This code is licensed under the same terms as Perl itself.
    
    =cut
    
    use Getopt::Long;
    use Date::Format;
    
    my($commit, $start_dir, $verbose, $includefolders, $single);
    GetOptions(
        'commit' => \$commit,
        'directory=s' => \$start_dir,
        'verbose|v' => \$verbose,
        'includefolders' => \$includefolders,
        'single' => \$single,
    );
    
    $start_dir or die "Must specify --directory\n";
    
    set_date_from_contained_files($start_dir);
    
    sub set_date_from_contained_files {
        my($directory) = @_;
        return unless defined $directory;
    
        opendir my $dirhandle, $directory
            or die "Can't read $directory: $!\n";
        my @contents;
        push @contents, $_ while readdir($dirhandle);
        closedir $dirhandle;
    
        @contents = grep { !/\.$|\.\.$/ } @contents;
        my @dirs = grep { -d "$directory/$_" } @contents;
    
        my %dirmap;
        @dirmap{@{[@dirs]}} = ();
    
        my @files = grep { !exists $dirmap{$_}} @contents;
    
        # Recursively apply the same update criteria unless --single is on.
        unless ($single) {
            foreach my $dir (@dirs) {
                set_date_from_contained_files("$directory/$dir");
            }
        }
    
        my $most_recent_date;
        if (! $includefolders) {
             $most_recent_date = most_recent_date($directory, @files);
             my $most_recent_folder = most_recent_date($directory, @dirs);
             warn "Folders in $directory are more recent ($most_recent_folder) than the most-recent file ($most_recent_date)\n";
        }
        else {
             $most_recent_date = most_recent_date($directory, @files, @dirs);
        }
    
        if (defined $most_recent_date) {
            (my $requoted = $directory) =~ s/'/\\'/g;
            my @command = (qw(touch -t), $most_recent_date, $directory);
            print "@command\n" if $verbose;
            system @command if $commit;
        }
        else {
            warn "$directory unchanged because it is empty\n" if $verbose;
        }
    }
    
    sub most_recent_date {
        my ($directory, @items) = @_;
        my @dates =     map  { (stat "$directory/$_")[9] } @items;
        my @formatted = map  { time2str("%Y%m%d%H%M.%S", $_) } @dates;
        my @ordered =   sort { $a lt $b } @formatted;
        return $ordered[0];
    }
    
  • Turning off parental restrictions on the iPhone when you’ve forgotten the PIN

    FOLLOWUP:

    I followed these instructions, but alas, they do not work under iOS 6.1.3. I got an OK restore, but still couldn’t unlock Parental Restrictions. I used iExplorer to backup all my text messages and voicemails, and did a “set up as new device” to get a known passcode on Parental Restrictions again.

    This is, not to put too fine a point on it, a real pain. Fortunately the nice folks at iphonebackupextractor.com had the steps lined out; I just needed to do a little command-line tinkering to make it work. So here’s the rundown:

    1. Turn off the password protection of your backups. (If you’ve forgotten this one too you may be stuck.)
    2. Take a backup via iTunes.
    3. Open Terminal.
      1. cd ~/Library/Application\ Support/MobileSync/Backups
      2. ls -lart
      3. cd (the latest one, which will be the last one listed)
      4. cp 662bc19b13aecef58a7e855d0316e4cf61e2642b ~
      5. openssl sha1 662bc19b13aecef58a7e855d0316e4cf61e2642b
      6. Copy the resulting SHA1 for later.
      7. plutil -convert xml1 662bc19b13aecef58a7e855d0316e4cf61e2642b
      8. Using the editor of your choice, look for “SBParentalControlsEnabled” and make sure the value below it is “true”. If it isn’t, you won’t be able to do the rest of this.
      9. Add these two lines right below “true”:

        <key> SBParentalControlsPIN</key>
        <string>1234</string>

      10. Save.
      11. openssl sha1 662bc19b13aecef58a7e855d0316e4cf61e2642b
      12. Record this new SHA1.
      13. cp Manifest.mbdb ~
      14. Open Manifest.mbdb with a hex editor. I used Hex Fiend.
      15. Find the first SHA1 you saved.
      16. Replace it with the second one. This is quite easy with Hex Fiend, as the “Find…” operation lets you cut and paste the hex strings directly into the find-and-replace boxes.
      17. Save.
    4. Go back to iTunes and restore this backup. Make sure you get the backup you just edited.

    You should now be able to access parental controls again if you got all the steps right. If not, copy the two files you backed up to ~ back into the backup directory and restore the backup again. You should be in no worse shape than you were before.

  • A few days with iOS 7

    tl;dr – I like it; looking forward to the GM.

    So, like every other iPhone user, I was *very* curious about iOS 7. As a developer, even more so. (Particularly, was I going to have to scramble to get my app working again under iOS 7?)

    So I took my backup and installed it. First impression is that it feels ever so much lighter, psychologically, than iOS 6. The “flattening” of the interface greatly enhances the experience; Microsoft was right on the money with that one. My experiences with Windows 8 only make me wish they could have committed even harder to it and gotten rid of the desktop altogether – but I digress.

    Some bugs, as expected, and I’ll be filing radars about them. In general, working pretty well, but there are a few showstoppers for me in this beta related to my day job. If it were not for those, I’d stick with it. Even with the crashes and hiccups, it’s that much of an improvement.

    My app does continue to work, and I’ve now, I think, spotted the problem that’s causing it to drop and resume streaming, so that was a benefit.

    Today I DFU my phone and return it to iOS 6 so I have a dependable device again, but it’s definitely a wrench. I’d much rather stay in the brighter, smoother, lighter world of iOS 7.

  • pemungkah.com migrated to WordPress

    I’ve decided to consolidate both my website and blog into a single WordPress site. This greatly decreases the friction in adding new stuff to the site, and makes it much easier to update and maintain. I’m currently working on a series of posts about Scape and its internals – first one will be a tour of the basics and how to back up your scapes and name them.

    I’ve grabbed the few posts I had on Blogger under the blog of the same name and have republished them here (with edits).

  • What’s on the fire: update

    The sound design for the Rachel Rising project, a multimedia presentation of a song cycle as poetry, video, and music. This project had been stalled for some time, as I was trying to figure out how to get the effect I wanted for an installation; I think I’ve finally got a couple of good ideas that revolve around having the internals of the Scape app to build a custom set of soundscapes from my own samples and visuals. More on that in the Scape posts to come.Jamming as part of Shojo Blue – Shojo Blue kind of fell apart, as we were only three people, our guitarist left, and then I was overwhelmed by work. Greg Hurley has moved into his house as of last weekend, so perhaps we’ll start working on a Western Skies sometime soon.

    Considering whether the StillStream iOS app needs an iPad-specific version, and looking over the new interfaces in iOS 6. The Facebook API looks interesting, and it’s probably time to convert to storyboards and ARC. Given more recent reports of hiccups and hangs, I think I’m going to have to move forward at least to iOS 5, and I should probably go to 6 (perhaps as StillStream Radio Plus, re-releasing the old app as StillStream Radio Classic).