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

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 ?

Related

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

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 playback lock screen controls not displaying on iPhone

I am testing this using iOS 10.2 on my actual iPhone 6s device.
I am playing streamed audio and am able to play/pause audio, skip tracks, etc. I also have enabled background modes and the audio plays in the background and continues through a playlist properly. The only issue I am having is getting the lock screen controls to show up. Nothing displays at all...
In viewDidLoad() of my MainViewController, right when my app launches, I call this...
func setupAudioSession(){
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
self.becomeFirstResponder()
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
and then in my AudioPlayer class after I begin playing audio I call ...
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action:#selector(skipTrack))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: "TESTING"]
}
When I lock my iPhone and then tap the power button again to go to the lock screen, the audio controls are not displayed at all. It is as if no audio is playing, I just see my normal background photo. Also no controls are displayed in the control panel (swiping up on home screen and then swiping left to where the music controls should be).
Is the issue because I am not using AVAudioPlayer or AVPlayer? But then how does, for example, Spotify get the lock screen controls to display using their own custom audio player? Thanks for any advice / help
The issue turned out to be this line...
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.duckOthers)
Once I changed it to
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [])
everything worked fine. So it seems that passing in any argument for AVAudioSessionCategoryPlaybackOptions causes the lock screen controls to not display. I also tried passing in .mixWithOthers an that too caused the lock screen controls to not be displayed
In Swift 4. This example is only to show the player on the lock screen and works with iOS 11. To know how to play auidio on the device you can follow this thread https://stackoverflow.com/a/47710809/1283517
import MediaPlayer
import AVFoundation
Declare player
var player : AVPlayer?
Now create a function
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(controlPause))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: currentStation]
}
now create a function for control play and pause event. I have created a BOOL "isPlaying" to determine the status of the player.
#objc func controlPause() {
if isPlaying == true {
player?.pause()
isPlaying = false
} else {
player?.play()
isPlaying = true
}
}
And ready. Now the player will be displayed on the lock screen
Yes, for the lock screen to work you need to use iOS APIs to play audio. Not sure how Spotify does it but they may be using a second audio session in parallel for this purpose and use the controls to control both. Your background handler (the singleton in my case) could start playing the second audio with 0 volume when it goes into background and stop it when in foreground. I haven't tested it myself but an option to try.

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.

Swift AVAudioPlayer Restart from Beginning

I have an AVAudioPlayer stored in a property called "player". When the user clicks a button, it triggers this code:
#IBAction func restartPressed(sender: AnyObject) {
player.play()
}
The problem is happening when the user clicks the button twice in a row very quickly.
If the sound from the first click is still playing, it seems like the second call is ignored.
Instead, I'd like to either:
a) restart the player from the beginning when the button is clicked a second time; or
b) have two "instances" of the sound playing at the same time.
How would I go about achieving either or both of these options?
Answering to "either" part (rather than "both") of these options: (a) how to stop and play the sound from beginning:
#IBAction func restartPressed(sender: AnyObject) {
if player.playing {
player.pause()
}
player.currentTime = 0
player.play()
}
Regarding (b): from the Language Ref for AVAudioPlayer:
Using an audio player you can:
...
Play multiple sounds simultaneously, one sound per audio player, with precise synchronization.
So you'd need two separate players to simultaneously (off-sync) play two separate sounds, even if both players use the same sound. I'd stick with (a).
dfri's answer updated for Swift 4
#IBAction func restartPressed(sender: UIButton) {
if negativePlayer.isPlaying
{
negativePlayer.pause()
}
negativePlayer.currentTime = 0
negativePlayer.play()
}
player.seek(to: .zero)
player.play()

Resources