Monthly Archives: March 2015

Clock-smith Round 2

Several years ago, I recorded individual WAV files of my voice saying “one” through “twelve”, “fifteen”, “thirty”, “forty-five”, “AM” and “PM” to create a talking clock. A cron job fires a script to play the time every fifteen minutes. It also plays a WAV file of Big Ben at the top of the hour. The trick is to use the same voice inflection on each part to make them sound natural when slammed together to announce the hour, minutes if needed and meridian (AM/PM).

I don’t travel often, but when I do, I miss the clock. OK, it was raining, I had the house to myself and it sounded like a fun weekend project. A talking clock app is fairly easy to pull off, but it has to run in the foreground with the idle timer disabled meaning plugged in to power. Apple’s Push Notification Service (APNs for short) can start a short background task so the app should be able to announce the time in the background!

The WAV files were converted to CAF for iOS using afconvert. File format (-f) caff, data format (-d) little-endian, 16-bit, 44.1k, 1-channel (-c).

afconvert -f caff -d LEI16@44100 -c 1 <input.wav> <output.caf>

The UI is little more than a UITextField using an NSDateFormatter to update every second though that could change based on my next trip out of town.

Fabulously simple UI

Fabulously simple UI

The Scheduler

The main logic lives in a scheduler class that handles all of the details of when and what to play. TDD helped move development of the class along rather quickly. AVQueuePlayer plays the files together as AVPlayerItems.

On initialization, the scheduler determines the next quarter hour time to play.

  1. Get the current hour and minute in date components.
  2. Use an if-else tree to get the next quarter-hour minute.
  3. Add one to the hour if the minute is zero (the top of the hour).
    NSInteger hour = currentComponents.hour;
    NSInteger nextMinute = [self nextQuarterHourMinute:minute];
   
    if (nextMinute == 0) {
        hour++;
    }

Save this as an NSDate property on the scheduler.

An NSTimer runs in the viewController firing every second to update the UITextField with the current time and to check the scheduler’s isTimeToAnnounce method. When the hour and minute match the current time, start playing the audio files.

To determine which files to play, some minor adjustments are needed.

  1. If the time is after noon, set the PM flag.
  2. If the hour is greater than twelve, subtract twelve.
  3. If the hour is zero, add twelve.
    NSDateComponents *current = [[NSCalendar currentCalendar] components:NSCalendarUnitHour|NSCalendarUnitMinute fromDate:self.nextQuarterHourDate];

    BOOL isPostmeridian = NO;
    NSInteger minute = current.minute;
    NSInteger hour = current.hour;
    
    if (hour >= 12) {
        hour -=12;
        isPostmeridian = YES;
    }
    // No such thing as zero o'clock
    if (hour == 0) {
        hour = 12;
    }
    
    NSString *hourPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%li", (long)hour] ofType:@"caf"];

This completes the foreground part of the app. Some things on iOS behave quite strangely in the background! We all know that UI calls must run on the main thread, but so do NSBundle calls otherwise pathForResource returns nil. Also, while you can start an AVQueuePlayer, then background the app and have it continue to play, you can’t start one in the background. This StackOverflow thread would have saved me a few hours had I seen it sooner. So AVQueuePlayer is great for playing music or podcasts, but not so good for TClock©.

System Sound Services

System Sound Services provides a C interface for playing short sounds and it works in the background. The best solution seems to be loading all the files first, then playing the first file and using completion callbacks to play the next file in the chain. There’s only two or three small files, so loading them all into memory isn’t a problem. The tricky part was assigning the proper callback for the hour based on the minute value. If minute is zero, no minute file plays.

    if (minute > 0) {
        NSString *minutePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%li", (long)minute] ofType:@"caf"];
        self.soundIDMinute = [self loadSoundForPath:minutePath completion:minuteCallback];
        self.soundIDHour = [self loadSoundForPath:hourPath completion:hourWithMinuteCallback];
    }
    else {
        self.soundIDHour = [self loadSoundForPath:hourPath completion:minuteCallback];
    }

System Sound Services isn’t hard to work with though it is a little different than you may be use to. These are the main parts of the code for loading and playing the audio. The AudioToolbox header and framework need to be included.

- (SystemSoundID)loadSoundForPath:(NSString *)path completion:(AudioServicesSystemSoundCompletionProc)completion
{
    SystemSoundID soundId;
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)[NSURL fileURLWithPath:path], &soundId);
    AudioServicesAddSystemSoundCompletion(soundId, NULL, NULL, completion, (__bridge void*)self);
    return soundId;
}

- (void)releaseSystemSound:(SystemSoundID)soundId
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        AudioServicesRemoveSystemSoundCompletion(soundId);
        AudioServicesDisposeSystemSoundID(soundId);
    });
}

static void hourWithMinuteCallback(SystemSoundID soundId, void* myself)
{
    TCLScheduler *scheduler = (__bridge TCLScheduler *)myself;
    AudioServicesPlaySystemSound(scheduler.soundIDMinute);
    [scheduler releaseSystemSound:soundId];
}

static void minuteCallback(SystemSoundID soundId, void* myself)
{
    TCLScheduler *scheduler = (__bridge TCLScheduler *)myself;
    AudioServicesPlaySystemSound(scheduler.soundIDMeridian);
    [scheduler releaseSystemSound:soundId];
}

static void meridianCallback(SystemSoundID soundId, void* myself)
{
    TCLScheduler *scheduler = (__bridge TCLScheduler *)myself;
    [scheduler releaseSystemSound:soundId];
    scheduler.soundIDHour = 0;
    scheduler.soundIDMinute = 0;
    scheduler.soundIDMeridian = 0;
}

Each callback starts the next file in the chain and disposes of the finished sound. The last meridianCallback resets the object’s SystemSoundID (UInt32) properties.

Push Notifications

I found a great article on Ray Wenderlich’s site that walks you through setting up your own APNs server and creating an app to receive push notifications. Starting from a point of having full access to an internet-connected Linux machine speeds this part up tremendously, otherwise, this would have been the most time-consuming piece.

The article provides a series of PHP scripts using the LAMP stack and excellent instructions on dealing with generating and manipulating the certificates.

  1. Generate an Apple certificate and provisioning profile for your app.
  2. Download the certificate and extract the private key.
  3. Convert the certificate and private key into PEM format for the server using terminal commands.

An iOS background task will run for about thirty seconds from a remote push so the server initiates pushes at around fifteen seconds before it’s time to announce. This usually gives ample time for the push to go to Apple then down to the device. The happy-path delay is a second or two, but timing adjustments may be required.

To get the notification to run a background task, the key “content-available” = 1 needs to be part of the payload. Use the fetchCompletionHandler version of didReceiveRemoteNotification. Simply call completionHandler(UIBackgroundFetchResultNewData) after kicking off your background task. Read the article as it explains all of this in much greater detail.

The background task does the same thing the viewController does in the foreground and uses the device’s clock to determine when to speak the time. There are a few trips through [NSThread sleepForTimeInterval:1], but usually less than ten.

  1. Cron runs a PERL script.
  2. The PERL script loops for forty-five seconds, then INSERTs a row in the push table.
  3. push.php polls the table and when it encounters a new row, sends a payload to Apple’s servers.
  4. Apple’s servers send the notification to the device.
  5. The device receives the push and starts the background task.
  6. The background tasks loops and plays the time.

Parting Thoughts

While I admit, this is somewhat of a Rube Goldberg machine, I’m sending my own push notifications! #thumbs_up_emoji

The back-end of the push process has been a black box to me for a while so it was fun to learn  all of it end-to-end.

The thirty second limit is there to keep apps from running long, battery-intensive processes in the background. Keep an eye on [[UIApplication sharedApplication] backgroundTimeRemaining] from within your background task and jump out if it gets down below five seconds otherwise, the OS will kill it.

The ringer switch will silence the announcements. I suppose a UIButton would accomplish the same thing, but why mess with a flawless interface?