Sound not playing AudioToolbox Swift - ios

I'm trying to play an alarm sound, but when I've implemented the code, it runs, but no sound is played. What is the problem?
Here is my code (the important bits):
import UIKit
//Timer Sound
let timerSoundURL = NSBundle.mainBundle().URLForResource("alarm", withExtension: "caf")
var timerSoundID: SystemSoundID = 0
override func viewDidLoad() {
//Setup sound for timer
AudioServicesCreateSystemSoundID(timerSoundURL, &timerSoundID)
}
//Plays sound
override func viewDidAppear(animated: Bool) {
AudioServicesPlaySystemSound(timerSoundID);
NSLog("Play sound")
}

Your code looks fine, but you are probably testing it in the Simulator. If so, that's your mistake. AudioServicesPlaySystemSound doesn't work in the Simulator.
Run your code on an actual device and all will be well (except that you've got a memory leak, but that's another matter entirely - the point is, you'll hear the sound, if the resource is valid, when you run on the device).

Related

AVAudioPlayerDidFinishPlaying never gets called, so "other sounds" remain "ducked" - What am I doing wrong?

Many questions on this topic, but most are so old the answers no longer apply due to both Swift and iOS having evolved so much since they were posted. The few that look helpful at first glance don't actually tell me anything I haven't already figured out, and haven't done anything to help me fix my incarnation of the problem, despite appearing to be nearly identical to the problem I'm having.
Aside from AVAudioPlayerDidFinishPlaying never being called (And therefore leaving "other sounds" being made by the device "ducked" until the app is totally shut down) the code below functions exactly as expected, in both Simulator and on a physical iPhone 7 Plus running iOS 13.3. If it makes a difference, I'm coding to Swift 5.1, targeting iOS 13.3, in Xcode 11.3, on a rig that runs Catalina (10.15.2).
Here's the "broken" code:
import Foundation
import AVFoundation
class NoiseMaker: NSObject, AVAudioPlayerDelegate {
var ourSession = AVAudioSession()
var ourPlayer = AVAudioPlayer()
func playSound(sound: String) {
print("Entered NoiseMaker.playSound(sound: \(sound))")
let theURL: URL = Bundle.main.url(forResource: sound, withExtension: "mp3")!
try!ourSession.setCategory(AVAudioSession.Category.ambient, options: AVAudioSession.CategoryOptions.duckOthers)
try!ourSession.setActive(true)
ourPlayer.delegate = self
ourPlayer.prepareToPlay()
do {
ourPlayer = try AVAudioPlayer(contentsOf: theURL)
ourPlayer.play()
} catch let theError as NSError {
print("Couldn't load \(sound).mp3 - Error \(theError)")
}
}
// MARK: - AVAudioPlayer delegate methods
// Never gets called - Other sound sources remain "ducked" until the app is completely shut down.
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
print("Arrived in audioPlayerDidFinishPlaying")
try!ourSession.setActive(false)
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
print("Decoding error from AVAudioPlayer: \(error!.localizedDescription)")
}
}
As noted, everything works as expected - EXCEPT for my audioPlayerDidFinishPlaying method never being called, meaning I never get a chance to "unduck" other sounds that might be playing. (although everything "unducks" as soon as the app is completely shut down)
Why? How am I screwing up what seems to be a nearly trivial task?
I see the problem, you set self as the delegate but then you change the reference to a new instance of AVAudioPlayer
You should set the delegate after creating new instance in here
ourPlayer = try AVAudioPlayer(contentsOf: theURL)
ourPlayer.delegate = self
ourPlayer.play()

Swift 5 loading multiple AVAudioPlayers causes too many open files error

I have an app with around 10 ViewControllers all connected to a single tab bar. In most of the ViewControllers, there are multiple buttons which cause a different local sound file to be played. Everything in all of the ViewControllers work fine independently. Sound files play fine. There are around 20 to 70 sound files initalised and loaded for each of the ViewControllers, so if the user cycles through all 10 controllers, the app could potentially load around 500 sound files and never unload them. I am thinking of added more tabs, so this is proving to be a problem.
My code (simplified) is as follows, for each ViewControllers:
class TrickyWordsViewController: UIViewController {
var musicEffect_hello: AVAudioPlayer = AVAudioPlayer()
// another 50 lines ...
var musicEffect_bye: AVAudioPlayer = AVAudioPlayer()
override func viewDidLoad() {
let musicFile_she = Bundle.main.path(forResource: "hello", ofType: ".m4a")
do {
try musicEffect_hello = AVAudioPlayer(contentsOf: URL (fileURLWithPath: musicFile_hello!))
}
catch { print(error) }
}
#IBAction func playSound(_ sender: Any) {
musicEffect_hello.play()
}
}
However, as the user clicks through many ViewControllers, the app usually crashes on the
try musicEffect_sound = AVAudioPlayer(...
line, with the following messages:
Error Domain=NSOSStatusErrorDomain Code=-42 "(null)"
Failed to open audio settings path fd. Error: (24) Too many open files
It looks like the app has loaded too many files.
a) Should I move the initialisation of all the sound files from viewDidLoad() into the playSound()? Sound files would only get initialised and loaded when the button is pressed. The theory is, every sound file is created on demand, within the IBAction function, played, and then destroyed when the IBAction function goes out of scope. I have between 20 and 70 sound files in each ViewControllers. Each sound file is only a few seconds long, and between 10KB and 150KB in size. Would this impact performance, and lag on playing each sound?
b) Should I count the number of sound files I load in the app, as the user clicks through the tab bar into each ViewControllers, and if the number of sound files exceeds a specific number, which seems to be 300-ish sound files, I would start to unload previous sound files? If so, how do I un-load sound files loaded in another ViewControllers?
c) Should I un-load sound files after the user navigates to another ViewControllers? If so, is there a function I can override when a user presses on another tab bar icon? viewDidDisappear() and viewWillDisappear() doesn't seem to be called when using tab bar.
d) Should I handle the memory warning called? I did try:
override func didReceiveMemoryWarning() {
print("didReceiveMemoryWarning...")
super.didReceiveMemoryWarning()
}
but that didn't seem to be called, or do anything.
Any ideas?
Maybe you don't setting the bundle properly.
If you set Bundle, you shouldn't set url path. Because its in your bundle. Simple
May you should try something like this:
func playSound() {
guard let url = Bundle.main.url(forResource: "hello", withExtension: "m4a") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
/* iOS 11. */
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
/* iOS 10 :
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3) */
guard let player = player else { return }
player.play()
} catch let error {
print(error.localizedDescription)
}
}
I really hope I helped you =D

Audio won't play after app interrupted by phone call iOS

I have a problem in my SpriteKit game where audio using playSoundFileNamed(_ soundFile:, waitForCompletion:) will not play after the app is interrupted by a phone call. (I also use SKAudioNodes in my app which aren't affected but I really really really want to be able to use the SKAction playSoundFileNamed as well.)
Here's the gameScene.swift file from a stripped down SpriteKit game template which reproduces the problem. You just need to add an audio file to the project and call it "note"
I've attached the code that should reside in appDelegate to a toggle on/off button to simulate the phone call interruption. That code 1) Stops AudioEngine then deactivates AVAudioSession - (normally in applicationWillResignActive) ... and 2) Activates AVAudioSession then Starts AudioEngine - (normally in applicationDidBecomeActive)
The error:
AVAudioSession.mm:1079:-[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.
This occurs when attempting to deactivate the audio session but only after a sound has been played at least once.
to reproduce:
1) Run the app
2) toggle the engine off and on a few times. No error will occur.
3) Tap the playSoundFileNamed button 1 or more times to play the sound.
4) Wait for sound to stop
5) Wait some more to be sure
6) Tap Toggle Audio Engine button to stop the audioEngine and deactivate session -
the error occurs.
7) Toggle the engine on and of a few times to see session activated, session deactivated, session activated printed in debug area - i.e. no errors reported.
8) Now with session active and engine running, playSoundFileNamed button will not play the sound anymore.
What am I doing wrong?
import SpriteKit
import AVFoundation
class GameScene: SKScene {
var toggleAudioButton: SKLabelNode?
var playSoundFileButton: SKLabelNode?
var engineIsRunning = true
override func didMove(to view: SKView) {
toggleAudioButton = SKLabelNode(text: "toggle Audio Engine")
toggleAudioButton?.position = CGPoint(x:20, y:100)
toggleAudioButton?.name = "toggleAudioEngine"
toggleAudioButton?.fontSize = 80
addChild(toggleAudioButton!)
playSoundFileButton = SKLabelNode(text: "playSoundFileNamed")
playSoundFileButton?.position = CGPoint(x: (toggleAudioButton?.frame.midX)!, y: (toggleAudioButton?.frame.midY)!-240)
playSoundFileButton?.name = "playSoundFileNamed"
playSoundFileButton?.fontSize = 80
addChild(playSoundFileButton!)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let nodes = self.nodes(at: location)
for spriteNode in nodes {
if spriteNode.name == "toggleAudioEngine" {
if engineIsRunning { // 1 stop engine, 2 deactivate session
scene?.audioEngine.stop() // 1
toggleAudioButton!.text = "engine is paused"
engineIsRunning = !engineIsRunning
do{
// this is the line that fails when hit anytime after the playSoundFileButton has played a sound
try AVAudioSession.sharedInstance().setActive(false) // 2
print("session deactivated")
}
catch{
print("DEACTIVATE SESSION FAILED")
}
}
else { // 1 activate session/ 2 start engine
do{
try AVAudioSession.sharedInstance().setActive(true) // 1
print("session activated")
}
catch{
print("couldn't setActive = true")
}
do {
try scene?.audioEngine.start() // 2
toggleAudioButton!.text = "engine is running"
engineIsRunning = !engineIsRunning
}
catch {
//
}
}
}
if spriteNode.name == "playSoundFileNamed" {
self.run(SKAction.playSoundFileNamed("note", waitForCompletion: false))
}
}
}
}
}
Let me save you some time here: playSoundFileNamed sounds wonderful in theory, so wonderful that you might say use it in an app you spent 4 years developing until one day you realize it’s not just totally broken on interruptions but will even crash your app in the most critical of interruptions, your IAP. Don’t do it. I’m still not entirely sure whether SKAudioNode or AVPlayer is the answer, but it may depend on your use case. Just don’t do it.
If you need scientific evidence, create an app and create a for loop that playSoundFileNamed whatever you want in touchesBegan, and see what happens to your memory usage. The method is a leaky piece of garbage.
EDITED FOR OUR FINAL SOLUTION:
We found having a proper number of preloaded instances of AVAudioPlayer in memory with prepareToPlay() was the best method. The SwiftySound audio class uses an on-the-fly generator, but making AVAudioPlayers on the fly created slowdown in animation. We found having a max number of AVAudioPlayers and checking an array for those where isPlaying == false was simplest and best; if one isn't available you don't get sound, similar to what you likely saw with PSFN if you had it playing lots of sounds on top of each other. Overall, we have not found an ideal solution, but this was close for us.
In response to Mike Pandolfini’s advice not to use playSoundFileNamed I’ve converted my code to only use SKAudioNodes.
(and sent the bug report to apple).
I then found that some of these SKAudioNodes don’t play after app interruption either … and I’ve stumbled across a fix.
You need to tell each SKAudioNode to stop() as the app resigns to, or returns from the background - even if they’re not playing.
(I'm now not using any of the code in my first post which stops the audio engine and deactivates the session)
The problem then became how to play the same sound rapidly where it possibly plays over itself. That was what was so good about playSoundFileNamed.
1) The SKAudioNode fix:
Preload your SKAudioNodes i.e.
let sound = SKAudioNode(fileNamed: "super-20")
In didMoveToView add them
sound.autoplayLooped = false
addChild(sound)
Add a willResignActive notification
notificationCenter.addObserver(self, selector:#selector(willResignActive), name:UIApplication.willResignActiveNotification, object: nil)
Then create the selector’s function which stops all audioNodes playing:
#objc func willResignActive() {
for node in self.children {
if NSStringFromClass(type(of: node)) == “SKAudioNode" {
node.run(SKAction.stop())
}
}
}
All SKAudioNodes now play reliably after app interrupt.
2) To replicate playSoundFileNamed’s ability to play the short rapid repeating sounds or longer sounds that may need to play more than once and therefore could overlap, create/preload more than 1 property for each sound and use them like this:
let sound1 = SKAudioNode(fileNamed: "super-20")
let sound2 = SKAudioNode(fileNamed: "super-20")
let sound3 = SKAudioNode(fileNamed: "super-20")
let sound4 = SKAudioNode(fileNamed: "super-20")
var soundArray: [SKAudioNode] = []
var soundCounter: Int = 0
in didMoveToView
soundArray = [sound1, sound2, sound3, sound4]
for sound in soundArray {
sound.autoplayLooped = false
addChild(sound)
}
Create a play function
func playFastSound(from array:[SKAudioNode], with counter:inout Int) {
counter += 1
if counter > array.count-1 {
counter = 0
}
array[counter].run(SKAction.play())
}
To play a sound pass that particular sound's array and its counter to the play function.
playFastSound(from: soundArray, with: &soundCounter)

Keep AVAudioPlayer sound in the memory

I use AVAudioPlayer to play a click sound if the user taps on a button.
Because there is a delay between the tap and the sound, I play the sound once in viewDidAppear with volume = 0
I found that if the user taps on the button within a time period the sound plays immediately, but after a certain time there is a delay between the tap and the sound in this case also.
It seems like in the first case the sound comes from cache of the initial play, and in the second case the app has to load the sound again.
Therefore now I play the sound every 2 seconds with volume = 0 and when the user actually taps on the button the sound comes right away.
My question is there a better approach for this?
My goal would be to keep the sound in cache within the whole lifetime of the app.
Thank you,
To avoid audio lag, use the .prepareToPlay() method of AVAudioPlayer.
Apple's Documentation on Prepare To Play
Calling this method preloads buffers and acquires the audio hardware
needed for playback, which minimizes the lag between calling the
play() method and the start of sound output.
If player is declared as an AVAudioPlayer then player.prepareToPlay() can be called to avoid the audio lag. Example code:
struct AudioPlayerManager {
var player: AVAudioPlayer? = AVAudioPlayer()
mutating func setupPlayer(soundName: String, soundType: SoundType) {
if let soundURL = Bundle.main.url(forResource: soundName, withExtension: soundType.rawValue) {
do {
player = try AVAudioPlayer(contentsOf: soundURL)
player?.prepareToPlay()
}
catch {
print(error.localizedDescription)
}
} else {
print("Sound file was missing, name is misspelled or wrong case.")
}
}
Then play() can be called with minimal lag:
player?.play()
If you save the pointer to AVAudioPlayer then your sound remains in memory and no other lag will occur.
First delay is caused by sound loading, so your 1st playback in viewDidAppear is right.

How to stop a sound action in swift spriteKit

i am a swift beginner trying to build a game with a sound in it. i use swift SpriteKit and also AVFoundation as my sound starter. Below is the action i call
whenever i want a sound action in the game. The sound (background music) plays
alright but just wouldn't stop when the game is over.
self.runAction(SKAction.playSoundFileNamed("Sound/Gameplay.wav", waitForCompletion: false))
Can somebody please help me get this issue behind me? thanks.
You could pause and resume the music?
var gameMusic = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Gameplay", ofType: "wav")!), error: nil)
//call playBackgroundMusic func
playBackgroundMusic()
func playBackgroundMusic(){
gameMusic.play()
}
func pauseBackgroundMusic(){
gameMusic.pause()
}
if(gameOver){
pauseBackgroundMusic()
}

Resources