Two simultaneous AVAudioSessions - swift

I have an app that uses WebRTC for voice chat.
After a lot of troubleshooting I came to the conclusion that whenever I receive a stream, the WebRTC will set the AVAudioSession shared instance to the following:
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
I want to play an audio file (about a second long) and if I simply load it and do player.play(), it won't play. If I try to set the category to .playback or anything that would make sense for an audio file, the stream's audio will stop playing.
I also tried to create a new AVAudioSession instance but once the audio file plays, the stream's audio will stop.
Is there a way for me to get the audio file to play without affecting the stream's audio and its configuration?
Here's the full code
private func playTapSound() {
guard let url = Bundle.main.url(forResource: App.AudioFile.emojiTapSound, withExtension: "mp3") else { return }
do {
let audioSesson = AVAudioSession()
try audioSesson.setCategory(.playback, mode: .default)
try audioSesson.setActive(true)
audioPlayer = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
guard let player = audioPlayer else { return }
player.play()
} catch let error {
LogManager.shared.print("Error - playing emoji tap audio -", error.localizedDescription, type: [.Error, .Audio])
}
// var chime1URL: NSURL?
// var chime1ID: SystemSoundID = 0
// let filePath = Bundle.main.path(forResource: "emojiTapSound", ofType: "wav")
// chime1URL = NSURL(fileURLWithPath: filePath!)
// AudioServicesCreateSystemSoundID(chime1URL!, &chime1ID)
// AudioServicesPlaySystemSound(0)
}
I also tried using the AudioToolBox but for some reason it only played on the simulator. I verified that it's not my phone's settings by trying one of Apple's preloaded sounds and it worked.

Related

How do you fix the "found nil while unwrapping optional value" error when trying to play sound?

I'm making a function to play sound
func playSound(soundName: String) {
let url = Bundle.main.url(forResource: soundName, withExtension: "wav")
player = try! AVAudioPlayer(contentsOf: url!)
player.play()
}
Then call this function in an IBAction that contains all my buttons
#IBAction func buttonPiano(_ sender: UIButton) {
playSound(soundName: String(sender.currentTitle!))
sender.backgroundColor = UIColor.white
sender.alpha = 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
sender.backgroundColor = UIColor.systemBackground
sender.alpha = 1
})
}
Running the app I can do. But whenever you press a button, it crashes and gives me this error:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/administrator/Desktop/Xcode Projects/pianoButtons/pianoButtons/ViewController.swift, line 37
The optional value seems to be url! from my sound function.
I've tried all I could, but no luck. How do I avoid this error and play the sound without crashes?
Make sure that your soundName.wave file is shown inside the copy bundle resources. You can find that by clicking on your project > selecting your target > Build Phases > Copy Bundle Resources. If you do not see it there, click the plus button to add it.
var soundPlayer: AVAudioPlayer?
func playSentSound() {
DispatchQueue.main.async{
let path = Bundle.main.path(forResource: "soundName.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
self.soundPlayer = try AVAudioPlayer(contentsOf: url)
print("Playing")
self.soundPlayer?.play()
} catch {
// couldn't load file :(
print("Cant Load File")
}
}
}
Your code is essentially calling this
try! AVAudioPlayer(contentsOf: Bundle.main.url(forResource: String(button.currentTitle!), withExtension: "wav")!)
Each ! is a potential crash. This is when they would occur
The button may not have a current title at the time the action is triggered.
The main bundle may not have a resource with that name/extension
The audio player may not be able to play the contents of that file
In your specific case it seems like it is failing at 2. The nicer way to handle this is like so
if let url = Bundle.main.url(forResource: soundName, withExtension: "wav") {
player = try! AVAudioPlayer(contentsOf: url)
} else {
print("No resouce named \(soundName).wav")
}
First of all your app will just not play a sound instead of crashing, second you will get a helpful log message which might show you why the resource isn't being found.
Ideally all of your ! should be replaced with similar constructs, to log errors or perform some fallback action instead of crashing.

AVAudioPlayer does not think it is playing when it is

I call the superPlay function to start audio playback a little while later I call the superPlay function again to stop the playback. This does not work though because player.isPlaying is false even knowing the sound is clearly playing in an infinite loop. I have no idea why please help!
func superPlay(timeInterval: TimeInterval, soundName: String) {
do {
alarmSound = soundName
//set up audio session
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: alarmSound, withExtension: "mp3")
player = try! AVAudioPlayer(contentsOf: url!)
player.numberOfLoops = -1
//Start AVAudioPlayer
print(timeInterval)
print(time)
let playbackDelay = timeInterval // must be ≥ 0
if player.isPlaying {
player.stop()
} else {
player.play(atTime: player.deviceCurrentTime + playbackDelay) //time is a TimeInterval after which the audio will start
}
}
catch {
print(error)
}
}
I have spent a couple more days debugging this now. What is happening is the original play is being assigned to a specific AudioPlayer ID for example the print is: "Optional(<AVAudioPlayer: 0x600002802280>)"
When I call the function again to stop the play the AVAudioPlayer is assigned a different ID therefore it does not find that the old player is still playing and moves forward with playing a new sound on top of the old sound.
I am not sure how to store the AVAudioPlayer ID and then call the function so that it checks the store Player for if it is ".isPlaying"??
This happen because you initialize it before stopping current audio. Try this way:-
func superPlay(timeInterval: TimeInterval, soundName: String) {
// If audio is playing already then stop it.
if let audioPlayer = player {
if player.isplaying {
player.stop()
}
}
// Initilize audio player object with new sound
do {
alarmSound = soundName
//set up audio session
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: alarmSound, withExtension: "mp3")
player = try! AVAudioPlayer(contentsOf: url!)
player.numberOfLoops = -1
//Start AVAudioPlayer
print(timeInterval)
print(time)
let playbackDelay = timeInterval // must be ≥ 0
player.play(atTime: player.deviceCurrentTime + playbackDelay) //time is a TimeInterval after which the audio will start
}
catch {
print(error)
}
}

How to fix 'appendingPathComponent' is unavailable: Use appendingPathComponent on URL error

I'm working on a old Swift tutorial (Swift 2.0) that's posted on Ray Wenderlich's web site (https://www.raywenderlich.com/2185-how-to-make-a-letter-word-game-with-uikit-and-swift-part-3-3) and I'm running into an error when I tried to re-setup a function called "preloadAudioEffects" in Swift 4.2 . The error? appendingPathComponent' is unavailable: Use appendingPathComponent on URL instead.
I've tried to rename the old Swift code [Ex: NSBundle to Bundle , stringByAppendingPathComponent to appendingPathComponent()], but I'm still running into some syntax issues due to my inexperience with Swift.
This is the original code:
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
//1 get the file path URL
let soundPath = NSBundle.mainBundle().resourcePath!.stringByAppendingPathComponent(effect)
let soundURL = NSURL.fileURLWithPath(soundPath)
//2 load the file contents
var loadError:NSError?
let player = AVAudioPlayer(contentsOfURL: soundURL, error: &loadError)
assert(loadError == nil, "Load sound failed")
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
}
}
And this is what I've tried to do via following the suggestions in Xcode:
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
//1 get the file path URL
let soundPath = Bundle.main.resourcePath!.appendingPathComponent(effect)
let soundURL = NSURL.fileURL(withPath: soundPath)
//2 load the file contents
var loadError:NSError?
let player = AVAudioPlayer(contentsOfURL: soundURL, error: &loadError)
assert(loadError == nil, "Load sound failed")
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
}
}
Get the full path to the sound file and convert it to a URL by using NSURL.fileURLWithPath().
Call AVAudioPlayer(contentsOfURL:error:) to load a sound file in an audio player.
Set the numberOfLoops to zero so that the sound won’t loop at all. Call prepareToPlay() to preload the audio buffer for that sound.
Finally, save the player object in the audio dictionary, using the name of the file as the dictionary key.
Just replace resourcePath with resourceURL
let soundURL = Bundle.main.resourceURL!.appendingPathComponent(effect)
and you have to wrap the AVAudioPlayer initializer in a try block
func preloadAudioEffects(effectFileNames:[String]) {
for effect in AudioEffectFiles {
let soundURL = Bundle.main.resourceURL!.appendingPathComponent(effect)
//2 load the file contents
do {
let player = try AVAudioPlayer(contentsOf: soundURL)
//3 prepare the play
player.numberOfLoops = 0
player.prepareToPlay()
//4 add to the audio dictionary
audio[effect] = player
} catch { print(error) }
}
}

Can not play sound file Swift4

I'm trying play simple .wav file in my application.
Here is the code:
func alarmSound() {
var player: AVAudioPlayer?
guard let path = Bundle.main.path(forResource: "emergency021", ofType: "wav") else {
print("Can't find audio file")
return
}
let url = URL(fileURLWithPath: path)
do {
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
if let prepare = player?.prepareToPlay() {
print("Player prepared: \(prepare)")
}
player?.volume = 1.0
player?.play()
} catch {
print("Playing sound error: \(error)")
}
}
Everything ok, file in the bundle, url is ok, player prepared, but.. there is no any sound. I have tried .mp3 file - same result.
Any ideas, why?
Issue was declaring player: AVAudioPlayer locally in function body, when I moved it up to ViewController all works.

Sound recorded but not playing in iOS

In an iOS app, I record a sound (.caf): I can see the file is in the Documents directory and I can drag it from iTunes to my desktop and play it (it contains what I record).
I test this thing on a regular device and when I play the sound I can't hear any sound: the device volume is at its maximum.
The code I use to play the sound is the following and returns no error:
let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let fullPath = (documentsDirectory as NSString).stringByAppendingPathComponent("docu.caf")
let url = NSURL.fileURLWithPath(fullPath)
do {
let sound = try AVAudioPlayer(contentsOfURL: url)
// sound is printed as <AVAudioPlayer: 0x14ed5bd20>
print(sound)
sound.prepareToPlay()
sound.play()
} catch {
// never gets called
print("couldn't load any file")
}
Any idea is appreciated...