6

I'm trying to do something similar to Tile.app. When it shows a notification, it plays a sound. That seems simple enough-- use UILocalNotification and include the sound file name. But local notifications limit sounds to no more than 30 seconds, and Tile's sound keeps playing for a whole lot longer than that. I expect it's looping, and it continues until I can't stand the noise any more.

The sound also plays even if the phone's mute switch is on, which doesn't happen with local notifications. For these reasons, UILocalNotification appears to be out.

I thought maybe I could post a text-only local notification and have my app play the sound. But using AVAudioPlayer, the play method returns NO and the sound doesn't play.

Other questions have suggested that this is by design, that apps aren't supposed to be able to play sounds from the background-- only continue sound that's already playing. I'd accept that reasoning except that Tile does it, so it's obviously possible. One suggested workaround violates Apple's guidelines in a way that I'm pretty sure Apple checks for now.

Some details that may be relevant:

  • I have the audio background mode in Info.plist
  • I use AVAudioSessionCategoryPlayback on the audio session, and the session should be active (at least, setActive:error: claims to succeed).
  • This app uses Bluetooth but does not currently use the bluetooth-central background mode.
Community
  • 1
  • 1
Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • The Tile app uses Bluetooth, which is a different background mode than audio. Effectively keeps the app running, and although I haven't done it myself, I would guess you can play whatever sounds you want in the mode as if the app was in the foreground. – shim May 23 '16 at 04:25
  • Tile uses Bluetooth, audio, and other background modes. Do the others add extra audio capabilities like the one I need? – Tom Harrington May 23 '16 at 04:26
  • I said that it's probably the Bluetooth mode that allows them to do this. The mode on its own is not sufficient though; presumably needs to be connected to a bluetooth peripheral, which is what prompts iOS to launch the app associated with that peripheral. I haven't used Tile but it looks like you press the tile within range of your phone to make it play a sound. – shim May 23 '16 at 04:27
  • or it might be using the Background Location Updates capability to always stay awake. – Xcoder May 26 '16 at 18:29
  • I don't have any trouble waking up in the background right at the moment a sound needs to be played-- because I'm using iBeacon sensing. The problem is getting a sound to play, not getting my code to run in the background. – Tom Harrington May 26 '16 at 21:13
  • @TomHarrington cheers, check update to my answer please – sage444 May 28 '16 at 09:04

2 Answers2

3

Actually you can use one of AudioServices... functions

  • AudioServicesPlaySystemSound
  • AudioServicesPlayAlertSound
  • AudioServicesPlaySystemSoundWithCompletion

to start playing sound in background.

I'm not sure about length of sound that can be submitted to AudioServicesCreateSystemSoundID because I'm checked only with short looped sound for ringing but this API not tied to notification so possible can overpass 30 second limit.

EDIT: App still requires audio in UIBackgroundModes to play in background

Cut from my test project appDelegate, iOS 9.3.1

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // test sound in foreground.
    registerAndPlaySound()

    return true
}

func applicationDidEnterBackground(application: UIApplication) {
    var task =  UIBackgroundTaskInvalid
    task = application.beginBackgroundTaskWithName("time for music") {
        application.endBackgroundTask(task)
    }
    // dispatch not required, it's just to simulate "deeper" background  
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(5 * Double(NSEC_PER_SEC)))

    dispatch_after(delayTime, dispatch_get_main_queue()) {
        self.registerAndPlaySound()
    }
}

func registerAndPlaySound() {
    var soundId: SystemSoundID = 0

    let bundle = NSBundle.mainBundle()
    guard let soundUrl = bundle.URLForResource("InboundCallRing", withExtension: "wav") else {
        return
    }
    AudioServicesCreateSystemSoundID(soundUrl, &soundId)

    AudioServicesPlayAlertSound(soundId)
}

Update

There I used beginBackgroundTask to start sound only as simplest way to execute code in background in another project similar code to registerAndPlaySound was called from PushKit callback without background task.

sage444
  • 5,661
  • 4
  • 33
  • 60
  • Thanks. I tried that, and it works when the app is in the foreground. When in the background the sound does not play. The completion function is called immediately, and unfortunately there is no error status argument to that function. – Tom Harrington May 27 '16 at 18:44
  • I've been trying to figure this out myself. We still have some problems with playing audio in the background. Seems to be some permission errors. – Mike C. May 27 '16 at 19:32
  • Yea those function requires `audio` in background modes to work – sage444 May 27 '16 at 20:47
  • @TomHarrington please check updated answer, but you probable already guessed about required background mode – sage444 May 27 '16 at 20:52
  • @sage444 As I mentioned in the question, I already have that background mode. Does this work for you in the background? – Tom Harrington May 27 '16 at 20:53
  • @TomHarrington Yes just checked on my iPhone, and added sample code – sage444 May 27 '16 at 20:57
  • Thanks! The background task trick is apparently crucial, and I wasn't doing that. I'll accept the answer but I'd appreciate any information/theories/guesses you have about why that matters and what it does. – Tom Harrington May 27 '16 at 22:08
  • Thanks. I was trying to do whatsapp like video call notification but unable to play sound in background. Your trick worked for me.... – Leena Jan 03 '21 at 19:59
0

From official Apple documentation:

"Custom sounds must be under 30 seconds when played. If a custom sound is over that limit, the default system sound is played instead."

There is no way that you can make UILocalNotication to loop the sound. You can schedule it again with time intervals for 30s, but that one will be a schedulation of another alarm.

To setup your custom notification sound:

notif.soundName = @"sound.caf";

Make sure the sound is actually in your app’s bundle, is in the correct format (linear PCM or IMA4—pretty much anywhere that explains how to convert sounds for iOS will tell you how to do that), and is under 30 seconds.

Pau Senabre
  • 4,155
  • 2
  • 27
  • 36
  • why is no one investing in this comment! – Mohamad Bachir Sidani May 27 '16 at 14:55
  • 3
    Thanks, but-- as I mentioned in the question-- this limitation is one of the reasons that `UILocalNotification` does not help here. I have no trouble posting a local notification with sound, but the fact that it doesn't loop or play for more than 30 seconds is the exact problem that I am trying to solve. That plus the fact that notification sounds don't play when the mute switch is on. – Tom Harrington May 27 '16 at 16:47