Swift 5 loading multiple AVAudioPlayers causes too many open files error - ios

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

Related

How to fade out one audio file while playing the next in AudioKit

I'm creating a traditional music player with AudioKit. Initially it plays one file, then you can tap the next button to skip to the next audio file. The songs aren't all known up-front, the playlist can change while a song is currently playing, so it's not known what the next audio file will be until we go to play it.
My current implementation for that works well. I create a player for the first audio file and set that to AudioKit.output and call AudioKit.start() then player.play(), then when next is tapped I call AudioKit.stop() and then create the new player, set it as the output, start AudioKit, and play the new player. If you don't stop AudioKit before modifying the output, you'll encounter an exception as I saw previously.
Now you should also be able to tap a fade button which will crossfade between the current song and the next song - fade out the current song for 3 seconds and immediately play the next song. This is proving to be difficult. I'm not sure how to properly implement it.
The AudioKit playgrounds have a Mixing Nodes example where multiple AKPlayers are created, AKMixer is used to combine them, and the mixer is assigned to the output. But it appears you cannot change the players in the mixer. So the solution I have currently is to stop AudioKit when the fade button is tapped, recreate the AKMixer adding a new player for the next song, start AudioKit, then resume playback of the first player and play the new player. This experience isn't smooth; you can certainly hear the audio stop and resume.
How can I properly fade out one song while playing the next song?
Please see my sample project on GitHub. I've included its code below:
final class Maestro: NSObject {
static let shared = Maestro()
private var trackPlayers = [AKPlayer]() {
didSet {
do {
try AudioKit.stop()
} catch {
print("Maestro AudioKit.stop error: \(error)")
}
mixer = AKMixer(trackPlayers)
AudioKit.output = mixer
do {
try AudioKit.start()
} catch {
print("Maestro AudioKit.start error: \(error)")
}
trackPlayers.forEach {
if $0.isPlaying {
let pos = $0.currentTime
$0.stop()
$0.play(from: pos)
}
}
}
}
private var mixer: AKMixer?
private let trackURLs = [
Bundle.main.url(forResource: "SampleAudio_0.4mb", withExtension: "mp3")!,
Bundle.main.url(forResource: "SampleAudio_0.7mb", withExtension: "mp3")!
]
func playFirstTrack() {
playNewPlayer(fileURL: trackURLs[0])
}
func next() {
trackPlayers.forEach { $0.stop() }
trackPlayers.removeAll()
playNewPlayer(fileURL: trackURLs[1])
}
func fadeAndStartNext() {
playNewPlayer(fileURL: trackURLs[1])
//here we would adjust the volume of the players and remove the first player after 3 seconds
}
private func playNewPlayer(fileURL: URL) {
let newPlayer = AKPlayer(url: fileURL)!
trackPlayers.append(newPlayer) //triggers didSet to update AudioKit.output
newPlayer.play()
}
}

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)

AVAudioPlayer audio not playing when its set and played from different methods

I have an app with different button press sounds. I'm trying to use two functions to set the next button press sound and then play it. The idea is to load the audio file for when the next button is pressed, and then finally play it through a different function when a button is pressed. Using prepareToPlay(), this should make playback faster.
The problem is that when I press a button, I hear 'silence'. I say 'silence' because my speakers actually receive some signal, but its too week to be heard.
Here's my code:
func loadNextButtonPressSound() {
let randomSoundIndex = Int(arc4random_uniform(UInt32(pressSounds.count)))
pressSoundPlayer = try! AVAudioPlayer(contentsOfURL: pressSounds[randomSoundIndex])
pressSoundPlayer.prepareToPlay()
pressSoundPlayer.delegate = self
}
func playButtonPressSound() { // called when a button is pressed
if(StoredSettings.instance.getEnableSoundsSetting()) {
pressSoundPlayer.play()
loadNextButtonPressSound()
}
}
If I instead merge the codes to form this:
internal func playButterPressSound() {
if(StoredSettings.instance.getEnableSoundsSetting()) {
let randomSoundIndex = Int(arc4random_uniform(UInt32(pressSounds.count)))
pressSoundPlayer = try! AVAudioPlayer(contentsOfURL: pressSounds[randomSoundIndex])
pressSoundPlayer.play()
}
}
It starts working (i.e. I hear button presses), but it defeats the purpose of using prepareToPlay() to preload the next sound.
Any suggestions on how to make the first code above work ?

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.

Sound file doesn't play when followed by exit(0) in Swift 2.0

OK, I want to have code that plays a certain sound file if the user quits, or if the user is eliminated from the game. After that, I want to exit the app.
So far, I have this:
#IBAction func justQuit(sender: UIButton) // user just wants to quit game
{
let path = NSBundle.mainBundle().pathForResource
("PowerDown3", ofType:"wav")
let url = NSURL(fileURLWithPath: path!)
do
{
let sdfx2 = try AVAudioPlayer(contentsOfURL: url)
PowerDown3 = sdfx2
sdfx2.play()
} catch { /* couldn't load file */}
exit(0)
}
I have the closing bracket to the above function, and have initialized the variables needed. I tried the code without the exit(0) command, and it plays the sound file, but when I include exit(0) to exit the app, the sound file does not play at all. The point is to exit the app, but before exiting, to play the
sound file.
I even included a timer code to try to delay the exit(0) command, but still the sound file doesn't play. The timer didn't seem to make a difference to delay the exiting action.

Resources