I have an observable object which provides functionality to my app to play the same chime sound whenever I need (repeatedly).
The issue is that it plays when the iPhone is on silent and I cannot stop it. I Googled as much as I could and found that people were advising to set the audio category to 'ambient', however, this still does not appear to fix the issue for me.
Here's my Chime class:
class Chime: ObservableObject {
let chimeSoundUrl = URL(fileURLWithPath: Bundle.main.path(forResource: "chime.wav", ofType: nil)!)
private var audioPlayer: AVAudioPlayer?
init() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.ambient, options: .duckOthers)
try audioSession.setActive(true)
} catch {
print("Failed to activate audio session: \(error.localizedDescription)")
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: chimeSoundUrl)
audioPlayer?.prepareToPlay()
audioPlayer?.volume = 1.0
} catch {
print("Failed to prepare audio player: \(error.localizedDescription)")
}
}
func play() {
audioPlayer?.currentTime = 0
audioPlayer?.play()
}
}
I have tried setting the audio category in the Chime class' init function, at the top-level of the app in the onAppear of the view playing the chime, and in the Chime class' 'play' method (which actually causes app performance issues) - none of these seem to work.
I'd really appreciate any help. Thanks
Not sure whether you've tried this on a real device, but here's what I found:
Using the simulator, if the silent switch is already active when the app is opened it behaves as if it isn't on silent and the sound plays. If silent switch is activated after the app is opened it works as expected.
When I tried running it on a real device it worked correctly in both cases, so maybe it's a bug in the simulator.
If you're just playing short alert-type sounds you should probably use Audio Services rather than an AVAudioPlayer. That might look like this:
class Chime {
let chimeSoundUrl = URL(fileURLWithPath: Bundle.main.path(forResource: "alert.mp3", ofType: nil)!)
let chimeSoundID: SystemSoundID
init(){
var soundID:SystemSoundID = 0
AudioServicesCreateSystemSoundID(chimeSoundUrl as CFURL, &soundID)
chimeSoundID = soundID
}
func play(){
AudioServicesPlaySystemSound(chimeSoundID)
}
}
Running the above in the simulator the silent switch has no effect and sound always plays, on a real device it works as expected.
Related
I have an app where the user can play voice messages received from other users. Playing the voice messages should interrupt device audio (music, podcast, etc playing from other apps), play the voice messages and then let the device audio continue.
Here is a use specific use case I am trying to achieve
the user starts playing music on the device via Apple Music
the user opens the app and taps a voice message
the Apple Music stops
voice message in the app plays
Apple Music continues playing
With setting AVAudioSessions category to .ambient I can play the voice message "over" the playing Apple Music, but that is not what I need exactly.
If I use the .playback category that makes the Apple Music stop, plays the voice message in the app but Apple Music does not continue playing afterwards.
In theory, Apple has provided a "protocol" for interrupting and resuming background audio, and in a downloadable example, I show you what it is and prove that it works:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/bk2ch14p653backgroundPlayerAndInterrupter
In that example, there are two projects, representing two different apps. You run both of them simultaneously. BackgroundPlayer plays sound in the background; Interrupter interrupts it, pausing it, and when it is finished interrupting, BackgroundPlayer resumes.
This, as you will see, is done by having Interrupter change its audio session category from ambient to playback while interrupting, and changing it back when finished, along with first deactivating itself entirely while sending the .notifyOthersOnDeactivation signal:
func playFile(atPath path:String) {
self.player?.delegate = nil
self.player?.stop()
let fileURL = URL(fileURLWithPath: path)
guard let p = try? AVAudioPlayer(contentsOf: fileURL) else {return} // nicer
self.player = p
// error-checking omitted
// switch to playback category while playing, interrupt background audio
try? AVAudioSession.sharedInstance().setCategory(.playback, mode:.default)
try? AVAudioSession.sharedInstance().setActive(true)
self.player.prepareToPlay()
self.player.delegate = self
let ok = self.player.play()
print("interrupter trying to play \(path): \(ok)")
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { // *
let sess = AVAudioSession.sharedInstance()
// this is the key move
try? sess.setActive(false, options: .notifyOthersOnDeactivation)
// now go back to ambient
try? sess.setCategory(.ambient, mode:.default)
try? sess.setActive(true)
delegate?.soundFinished(self)
}
The trouble, however, is that response to .notifyOthersOnDeactivation is entirely dependent on the other app being well behaved. My other app, BackgroundPlayer, is well behaved. This is what it does:
self.observer = NotificationCenter.default.addObserver(forName:
AVAudioSession.interruptionNotification, object: nil, queue: nil) {
[weak self] n in
guard let self = self else { return } // legal in Swift 4.2
let why = n.userInfo![AVAudioSessionInterruptionTypeKey] as! UInt
let type = AVAudioSession.InterruptionType(rawValue: why)!
switch type {
case .began:
print("interruption began:\n\(n.userInfo!)")
case .ended:
print("interruption ended:\n\(n.userInfo!)")
guard let opt = n.userInfo![AVAudioSessionInterruptionOptionKey] as? UInt else {return}
let opts = AVAudioSession.InterruptionOptions(rawValue: opt)
if opts.contains(.shouldResume) {
print("should resume")
self.player.prepareToPlay()
let ok = self.player.play()
print("bp tried to resume play: did I? \(ok as Any)")
} else {
print("not should resume")
}
#unknown default:
fatalError()
}
}
As you can see, we register for interruption notifications, and if we are interrupted, we look for the .shouldResume option — which is the result of the interrupter setting the notifyOthersOnDeactivation in the first place.
So far, so good. But there's a snag. Some apps are not well behaved in this regard. And the most non-well-behaved is Apple's own Music app! Thus it is actually impossible to get the Music app to do what you want it to do. You are better off using ducking, where the system just adjusts the relative levels of the two apps for you, allowing the background app (Music) to continue playing but more quietly.
You've already discovered that you can interrupt other audio apps by activating a .playback category audio session. When you finish playing your audio and want the interrupted audio to continue, deactivate your audio session and pass the notifyOthersOnDeactivation option.
e.g.
try! audioSession.setActive(false, options: .notifyOthersOnDeactivation)
I think those apps that should continue like Apple Music, Spotify, Radio apps etc implement the functionality to handle interruptions and when another app's audio is deactivated / wants to hand back responsibility of the audio.
So could you try and see if this works
// I play the audio using this AVAudioPlayer
var player: AVAudioPlayer?
// Implement playing a sound
func playSound() {
// Local url for me, but you could configure as you need
guard let url = Bundle.main.url(forResource: "se_1",
withExtension: "wav")
else { return }
do {
// Set the category to playback to interrupt the current audio
try AVAudioSession.sharedInstance().setCategory(.playback,
mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url)
// This is to know when the sound has ended
player?.delegate = self
player?.play()
} catch let error {
print(error.localizedDescription)
}
}
extension AVAudioInterruptVC: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
successfully flag: Bool) {
do {
// When the sound has ended, notify other apps
// You can do this where ever you want, I just show the
// example of doing it when the audio has ended
try AVAudioSession.sharedInstance()
.setActive(false, options: [.notifyOthersOnDeactivation])
}
catch {
print(error)
}
}
}
I am working on an iOS app, that connects to an iBeacon.
I want to be able to play sounds from the phone, if external parameters match.
I have enabled "Audio, Airplay, and picture in picture." in Background Modes.
Right now I have the issue, that it can only play sounds from the app, while:
App is in foreground.
Screen is locked, and phone is not silent.
I would like it to also be able to play sounds, while:
Locked screen and phone is silent.
Background (when app is started by iBeacon).
Is there any way to achieve this?
The function I use to play sound is:
private var avPlayer: AVAudioPlayer?
// MARK: - Play Sound
internal func playSound(forResource resource: String, ofType type: String) {
guard let path = Bundle.main.path(forResource: resource, ofType: type) else {
print("Test: Failed getting path for \(resource)")
return
}
let url = URL(fileURLWithPath: path)
// Handle playing in background
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, options: [.duckOthers])
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("Test: Failed setting category on session \(error)")
}
do {
avPlayer = try AVAudioPlayer(contentsOf: url)
} catch let error {
print("Test: Failed adding url to AVAudioPlayer \(error)")
}
avPlayer?.play()
}
And called like:
playSound(forResource: "SoundToPlay", ofType: "wav")
If you set up your app to listen for beacon region notifications then you can trigger sounds to play when you enter or leave a region, even if the phone is locked and/or your app was not running. I'm not sure if those would play if the user has the phone muted (I suspect not)
My iOS app receives notification to refresh its state from our main service. Right now, we are fetching the latest state and we play a continues sound when there is updated data. Our devices are locked in guided mode to stop them from turning off.
We are making changes such that the device can go to sleep after xx minutes of inactivity. However, we are noticing that the sound doesn't play when the app is in background and it always seems to fail on AVAudioSession.sharedInstance().setActive(true). Interestingly, if put breakpoints and run it through the debugger, it works fine but when running normally, it fails with this error:
Error Domain=NSOSStatusErrorDomain Code=561015905
We have the "Audio, AirPlay, and PnP" enabled under background modes under capabilities. Here is the code for playing the sound:
func playSound(shouldPlay: Bool) {
guard let url = Bundle.main.url(forResource: "sms_alert_circles", withExtension: "caf") else { return }
let audioSession = AVAudioSession.sharedInstance()
do {
try self.audioSession.setCategory(.playback, mode: .default, options: .mixWithOthers)
try self.audioSession.setActive(true)
/* The following line is required for the player to work on iOS 11. Change the file type accordingly*/
self.player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.caf.rawValue)
guard let player = self.player else { return }
if shouldPlay == true {
player.volume = 1.0
player.numberOfLoops = 1
player.play()
} else {
player.stop()
try self.audioSession.setActive(false)
}
} catch let error {
print("Error playing sounds")
print(error.localizedDescription)
}
}
I am hoping someone can point out the issue.
I'm working on an exercise app that needs to be able to play sounds when the app goes into the background and also when the screen is locked. I also need to allow sounds from other apps to play (such as music) while also allowing my sound effects to come through.
I had this working in iOS 9 by using AVAudioSessionCategoryAmbient, but as soon as my app becomes inactive or the screen locks, the sounds stop.
Here's a simplified version of my code in a demo app:
import UIKit
import AVFoundation
class ViewController: UIViewController {
var session: AVAudioSession = AVAudioSession.sharedInstance()
var timer = Timer()
let countdownSoundFile = "154953__keykrusher__microwave-beep"
let countdownSoundExt = "wav"
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
activateAudio()
}
func activateAudio() {
_ = try? session.setCategory(AVAudioSessionCategoryAmbient, with: [])
_ = try? session.setActive(true, with: [])
}
#IBAction func play() {
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(ViewController.playSound), userInfo: nil, repeats: true)
}
func playSound() {
if let soundURL = Bundle.main.url(forResource: countdownSoundFile, withExtension: countdownSoundExt) {
do {
print("sound playing")
try audioPlayer = AVAudioPlayer(contentsOf: soundURL)
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("no sound found")
}
}
}
}
I also checked the Audio, Airplay, and Picture in Picture capability in Background Modes, which added the Required background mode to my plist.
I'm testing this on a device, and as soon as I lock the screen or press the home button, my sounds stop, and they resume once my app becomes active again.
I found a workaround by using:
var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(soundURL as CFURL, &mySound)
AudioServicesPlaySystemSound(mySound);
but this doesn't allow the volume of the sounds to be changed, and I also need to use AVSpeechSynthesizer, and this workaround doesn't work for that.
Any ideas on what I can do to make this work?
EDIT:
I changed the category line to _ = try? session.setCategory(AVAudioSessionCategoryPlayback, with: [.mixWithOthers]) and this allows music to be played while the app is running, however the sounds still stop when the screen locks or goes into the background. I need the sounds to continue to play in these scenarios.
EDIT:
As the answer pointed out, I cannot play sounds when my app is backgrounded, but using _ = try? session.setCategory(AVAudioSessionCategoryPlayback, with: [.mixWithOthers]) does allow sounds to play when the screen turns off and mixes with sounds (music) from other apps.
You cannot play in the background with an Ambient audio session category. Your category must be Playback.
To allow sounds from other apps to play, modify your Playback category with the Mixable option (.mixWithOthers).
(Also keep in mind that you can change your category at any time. You could have it be Ambient most of the time but switch to Playback when you know you're about to go into the background so that you can keep playing.)
EDIT Also it occurs to me that there is another possible misconception that might need clearing up. You cannot (easily) start a new sound while in the background. All that background audio allows you to do is continue playing the current sound when your app goes into the background. Once that sound stops (in the background), your app suspends and that's the end of that.
check here this answer in detail. You need to enable Audio Mode as well from the app settings.
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch {
print(error)
}
} catch {
print(error)
}
i'm playing audio in the background when my app receive voip notification.
But AVAudioPlayer acting weird and sometimes play the audio and sometimes not without any error.
thats how i play:
func playAudio() {
// Set the sound file name & extension
let alertSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("testAudio", ofType: "wav")!)
// Play the sound
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: alertSound)
audioPlayer.delegate = self
if (audioPlayer.prepareToPlay()) {
audioPlayer.play()
}
else {NSLog("there is error")}
} catch {
NSLog("there is \(error)")
}
}
I initialize AVAudioSession in my AppDelegate application function:
do
{
audioPlayer = AVAudioPlayer()
audioSession = AVAudioSession()
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, withOptions: [AVAudioSessionCategoryOptions.MixWithOthers])
try audioSession.setActive(true)
try audioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.Speaker)
}
catch {
NSLog("there is \(error)")
}
and of course var audioPlayer: AVAudioPlayer! as a class verb
Its working for the first minute or two and then AVAudioPlayer stop responding without any error.
I even try to use AVAudioPlayerDelegate to receive events, but nothing happen.
can someone explain this behavior? is it a bug in ios? or i'm doing somthing wrong?
tested on Ios 9.2, iphone 6 device, xcode 7.2.
Thanks.