I have built a random number generator, I want it to play a sound when the button is pressed. Xcode doesn't give me any errors and when I simulate my app it generates numbers no problem but there is no sound. I have my sound in a sound folder.
Here is my code.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player: AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBOutlet weak var operations: UIImageView!
#IBOutlet weak var bottomRightNumber: UIImageView!
#IBOutlet weak var bottomMiddleNumber: UIImageView!
#IBOutlet weak var bottomLeftNumber: UIImageView!
#IBOutlet weak var topRightNumber: UIImageView!
#IBOutlet weak var topMiddleNumber: UIImageView!
#IBOutlet weak var topLeftNumber: UIImageView!
#IBAction func buttonPressed(_ sender: UIButton) {
let numberArray = [#imageLiteral(resourceName: "RN1"), #imageLiteral(resourceName: "RN2"), #imageLiteral(resourceName: "RN3"), #imageLiteral(resourceName: "RN4"),#imageLiteral(resourceName: "RN5"), #imageLiteral(resourceName: "RN6"), #imageLiteral(resourceName: "RN7"), #imageLiteral(resourceName: "RN8"), #imageLiteral(resourceName: "RN9"), #imageLiteral(resourceName: "RN0")]
bottomRightNumber.image = numberArray.randomElement()
bottomLeftNumber.image = numberArray.randomElement()
bottomMiddleNumber.image = numberArray.randomElement()
topRightNumber.image = numberArray.randomElement()
topMiddleNumber.image = numberArray.randomElement()
topLeftNumber.image = numberArray.randomElement()
let operationArray = [#imageLiteral(resourceName: "-"), #imageLiteral(resourceName: "+")]
operations.image = operationArray.randomElement()
func playSound() {
guard let url = Bundle.main.url(forResource: "A", withExtension: "wav") else { return }
do { /* The following line is required for the player to work on iOS 11. Change the file type accordingly*/
player = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
player.play()
}
}
}
}
Looks like you just copied and pasted someone else’s code. But you pasted it into some other code, so the problem now is that your code that plays sound has ended up wrapped in a nested function declaration, inside your func buttonPressed code:
func playSound() {
guard let url = Bundle.main.url(forResource: "A", withExtension: "wav") else { return }
do {
player = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
player.play() // this is where sound _would_ play
}
}
Nothing ever calls that function, so it never runs.
Just delete those two lines that wrap the code, and now the code can run as part of what buttonPressed does. I would also suggest removing the pointless do construct as well, so you will end up with this:
guard let url = Bundle.main.url(forResource: "A", withExtension: "wav") else { return }
player = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
player.play() // this is where sound _would_ play
Your sound still might not play, or you might crash, but at least the code will now actually run!
Having said all that, let me change the code a little more to demonstrate best practices:
guard let url = Bundle.main.url(forResource: "A", withExtension: "wav") else {
print("the URL was not valid") // tell yourself what went wrong
return
}
do {
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
player.play()
} catch { print(error) } // tell yourself what went wrong
In that version, I've put the do construct back, but this time it actually does something useful.
you can do 2 things:
1. Take the function playSound() out of the buttonPressed action and call the playSound() function - because I see that you have not called the function to play the sound.
2. you can just take the audio code out of the function playSound() if you are eventually going to use it there itself.
Here is the sample class to play sounds,
import Foundation
import AudioToolbox
import AVKit
class SoundManager {
static var objPlayer: AVAudioPlayer?
class func playAppLaunchSound() {
registerAndPlaySoundForFile(fileName: "AppLaunchSound", fileExtension: "mp3")
}
class func playEnterButtonSound() {
registerAndPlaySoundForFile(fileName: "EnterButtonSound", fileExtension: "mp3")
}
private class func registerAndPlaySoundForFile(fileName: String, fileExtension: String, withVibration: Bool = true) {
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: fileExtension) else {
return
}
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
try AVAudioSession.sharedInstance().setActive(true)
objPlayer = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
guard let aPlayer = objPlayer else { return }
aPlayer.play()
} catch let error {
print(error.localizedDescription)
}
}
}
registerAndPlaySoundForFile is the main method responsible to play sound!
You can use it like SoundManager.playEnterButtonSound(). You can create multiple methods in this class like this.
If you want to play sounds in your iOS application, stop what you are doing. Throw away the manual on swifts built in audio services and use a framework.
AudioKit is an incredible powerful framework built by some very competent audio programmers. This library has a nice API, is well documented and is very powerful. They have plenty of example apps, including sample code to help get you started. If you just want to play a sound, you will be covered. If you wanted to build a granular synth, you will also be covered.
Especially if you are new to programming, usually someone has done the hard work for you. Don't reinvent the wheel!
Related
I was following this tutorial on YouTub: https://www.youtube.com/watch?v=qC6DzF_ACpQ&t=714s , and at 12.18 when he clicks the button, it plays audio. I followed all the exact same steps as he did in the video and it is not working for me.
I am pretty new to coding in swift so I did not try anything else other than restarting the whole project again, but it did not work.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var audioPlayer: AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func aTapped(_ sender: Any) {
let url = Bundle.main.url(forResource: "a", withExtension: "mp3")
// Make sure that weve got the url, otherwise abord
guard url != nil else{
return
}
do{
audioPlayer = try AVAudioPlayer(contentsOf: url!)
audioPlayer?.play()
}
catch{
print("error")
}
}
I just want the audio to play when I click the button assigned to the code, but when I click the button, no audio plays at all.
Please Help.
I'm new in Swift. I am trying to Run this code but got an error:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x48) a.
Error appears here:
self.player.play()
Can anyone help in this issue?
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
do {
if let audioPath = Bundle.main.path(forResource: "music", ofType: "mp3") {
try player = AVAudioPlayer(contentsOf: URL(fileURLWithPath: audioPath))
}
} catch {
print("ERROR")
}
self.player.play()
}
What you doing with your code is:
Creating an instance of AVAudioPlayer without any data
Trying to create a new instance with some data from music.mp3 file
Playing it
If the audioPath is not correct, then the player is not correctly created: application will use the one without valid data, leading to a crash during playback.
Your code can be written making the player optional, which helps preventing the crash:
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player: AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
player = initializePlayer()
player?.play()
}
private func initializePlayer() -> AVAudioPlayer? {
guard let path = Bundle.main.path(forResource: "music", ofType: "mp3") else {
return nil
}
return try? AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
}
}
Some changes performed to the code:
player is now optional: there is no need to initialize it twice. Plus if the initialization goes well you can use it without problems, otherwise your app won't crash.
The play method is now in an optional chain
I've moved the initialization of the player in a separate private methods, to maintain the code readable. Feel free to change it, it is just a suggestion.
I've got it. The issue is that the file isn't being coping to my app bundle. To fix it:
Click your project
Click your target
Select Build Phases
Expand Copy Bundle Resources
Click '+' and select your file.
Thanks Denis Hennessy
I'm working on an app (piano) that has a series of buttons that each has a different mp3. The screen shows 12 buttons (piano keys) and I want the user to be able to play an individual sound or swipe across a couple to hear multiple. Just like a real piano. I've seen many apps do this but mine seems to have a problem when the user slides across multiple buttons quickly. At the same speed, other apps will play all the notes, but mine will skip a few. Thank you for any help! This will make all the difference in my app!
A couple quick notes about this code:
-I just have the bare bones here to save space
-I just showed 6 audio players, but you get the idea
-the locationInNote1...Note2...Note3 is just showing 6 here to save place, but you get the idea
-"note1" in the button action is a string that can be changed when the user selects different octaves to play from, but its just a #, so the audio files ultimately are 1.mp3, 2.mp3, etc.
-the button action playNote1 is the same as the other button actions so i didn't repeat them all there.
var audioPlayer = AVAudioPlayer()
var audioPlayer2 = AVAudioPlayer()
var audioPlayer3 = AVAudioPlayer()
var audioPlayer4 = AVAudioPlayer()
var audioPlayer5 = AVAudioPlayer()
var audioPlayer6 = AVAudioPlayer()
func playNote(for locationInView: CGPoint) {
let locationInNote1 = note1Button.convert(locationInView, from: view)
let locationInNote2 = note2Button.convert(locationInView, from: view)
let locationInNote3 = note3Button.convert(locationInView, from: view)
let locationInNote4 = note4Button.convert(locationInView, from: view)
let locationInNote5 = note5Button.convert(locationInView, from: view)
let locationInNote6 = note6Button.convert(locationInView, from: view)
if note1Button.point(inside: locationInButton1, with: nil) {
playNote1(self)
}
if note2Button.point(inside: locationInButton2, with: nil) {
playNote2(self)
}
if note3Button.point(inside: locationInButton3, with: nil) {
playNote3(self)
}
if note4Button.point(inside: locationInButton4, with: nil) {
playNote4(self)
}
if note5Button.point(inside: locationInButton5, with: nil) {
playNote5(self)
}
if note6Button.point(inside: locationInButton6, with: nil) {
playNote6(self)
}
}
#IBAction func playNote1(_ sender: Any) {
let note1mp3 = note1
if let path = Bundle.main.path(forResource: note1mp3, ofType: "mp3") {
let url = URL(fileURLWithPath: path)
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.prepareToPlay()
audioPlayer.play()
}
catch {
print(error)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(panGesture)
Here is a way to hopefully:
Improve your code
Make your code simpler
Fix the problem you're having
In this example, I'll just put 3 audio players.
I'll try to make it as straight forward as possible...
import UIKit
class MyViewController: UIViewController {
var audioPlayer = AVAudioPlayer()
var audioPlayer2 = AVAudioPlayer()
var audioPlayer3 = AVAudioPlayer()
#IBOutlet var notes: [UIButton]!
let references = [note1, note2, note3]
#IBAction func notePressed(_ sender: UIButton) {
play(note: notes.index(of: sender)! + 1)
}
func play(note: Int) {
let reference = references[note - 1]
if let path = Bundle.main.path(forResource: reference, ofType: "mp3") {
let url = URL(fileURLWithPath: path)
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.prepareToPlay()
audioPlayer.play()
}
catch {
print(error)
}
}
}
}
Explanation
#IBOutlet var notes: [UIButton]!
This is an Outlet Collection of buttons. To make this, connect a button but select Outlet Collection instead of just Outlet. Make sure you connect each button in order (1, 2, 3), otherwise it will break!
let references = [note1, note2, note3]
This is an array of references to each of the note files. This is so that we only need one function to play a note.
#IBAction func notePressed(_ :sender:)
This function gets called by the buttons. For each button, connect the Touch Drag Enter (for sliding along notes) and Touch Down actions. You can add other ones if you want. The functions compares the sender and notes Outlet Collection to find out which note was pressed, then passes it to the func play(:note:) function.
func play(:note:)
This function takes the note number, and plays the corresponding file. It's almost identical to your original one, but instead of having a fixed note, it has one that is passed by the #IBAction func notePressed(_ :sender:) method.
I hope this is helpful, good luck!
I'm trying to make a simple app for some kids. I want to show a photo and to link that photo to a sound.
When the kid click on "Play Sound" it should play an audio that represents the image.
so far I've done only showing the images, that I've put in Assets.xcassets, but my question is..
How can I link those photos to some audio files and play them by clicking on "Play Sound"
Here is my code in ViewController.swift :
#IBOutlet weak var viewImage: UIImageView!
var images: [UIImage] = [
UIImage(named: "photo1.png")!,
UIImage(named: "photo2.png")!,
UIImage(named: "photo3.png")!,
UIImage(named: "photo4.png")!,
UIImage(named: "photo5.png")!,
UIImage(named: "photo6.png")!,
UIImage(named: "photo7.png")!,
UIImage(named: "photo8.png")!
]
var currentImagesIndex = 0
#IBAction func nextImage(_ sender: Any) {
currentImagesIndex += 1
let numberOfImages = images.count
let nextImagesIndex = currentImagesIndex % numberOfImages
viewImage.image = images[nextImagesIndex]
}
So far, when I click on Next Photo it shows me a new photo, and it's working ok. All I want to do for now, it's to link the "photo1-8" to an audio and play it.
I'll appreciate any kind of help,
Radu
You should first add all your sound files to your project. I assume you know how to do this. After adding the files, your project navigator should look a bit like this:
For convenience, I will call your 8 sound files sound1.mp3, sound2.mp3, sound3.mp3 ... sound8.mp3.
Create a property in the class called soundFiles:
let soundFiles: [String] = [
"sound1",
"sound2",
"sound3",
"sound4",
"sound5",
"sound6",
"sound7",
"sound8"
]
Also create a property called player to play the sounds:
var player: AVAudioPlayer!
In the #IBAction of the "Play Sound" button, get the file path of the sound file and initialise the player:
let numberOfImages = images.count
let nextImagesIndex = currentImagesIndex % numberOfImages
let soundFilePath = Bundle.main.url(forResource: soundFiles[nextImagesIndex], withExtension: ".mp3")!
player = try! AVAudioPlayer(contentsOf: soundFilePath)
Then you can play the sound by calling prepareToPlay() and play() consecutively:
player.prepareToPlay()
player.play()
Import AV Foundation by adding this line in top: import AVFoundation
Drag MP3, WAV file in main project directory
Check "Copy items if needed" & App Name in "Add to Targets"
Use this code to play sound
func playSound() {
var btnSound: AVAudioPlayer!
if let audioFileURL = Bundle.main.url(forResource: "fileName", withExtension: ".wav") {
do{
try btnSound = AVAudioPlayer(contentsOf: audioFileURL)
btnSound.prepareToPlay()
btnSound.play()
} catch let err as NSError {
print(err.debugDescription)
}
}
}
//call playSound function inside nextImage method
Tested with Swift 3.1 in xCode 8.3.2
I'm trying to get multiple sounds files to play on an AVAudioPlayer instance, however when one sound plays, the other stops. I can't get more than one sound to play at a time. Here is my code:
import AVFoundation
class GSAudio{
static var instance: GSAudio!
var soundFileNameURL: NSURL = NSURL()
var soundFileName = ""
var soundPlay = AVAudioPlayer()
func playSound (soundFile: String){
GSAudio.instance = self
soundFileName = soundFile
soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)
do{
try soundPlay = AVAudioPlayer(contentsOfURL: soundFileNameURL)
} catch {
print("Could not play sound file!")
}
soundPlay.prepareToPlay()
soundPlay.play ()
}
}
Can anyone please help me by telling me how to get more than one sound file to play at a time? Any help is much appreciated.
Many thanks,
Kai
The reason the audio stops is because you only have one AVAudioPlayer set up, so when you ask the class to play another sound you are currently replacing the old instance with a new instance of AVAudioPlayer. You are overwriting it basically.
You can either create two instances of the GSAudio class, and then call playSound on each of them, or make the class a generic audio manager that uses a dictionary of audioPlayers.
I much prefer the latter option, as it allows for cleaner code and is also more efficient. You can check to see if you have already made a player for the sound before, rather than making a new player for example.
Anyways, I re-made your class for you so that it will play multiple sounds at once. It can also play the same sound over itself (it doesn't replace the previous instance of the sound) Hope it helps!
The class is a singleton, so to access the class use:
GSAudio.sharedInstance
for example, to play a sound you would call:
GSAudio.sharedInstance.playSound("AudioFileName")
and to play a number of sounds at once:
GSAudio.sharedInstance.playSounds("AudioFileName1", "AudioFileName2")
or you could load up the sounds in an array somewhere and call the playSounds function that accepts an array:
let sounds = ["AudioFileName1", "AudioFileName2"]
GSAudio.sharedInstance.playSounds(sounds)
I also added a playSounds function that allows you to delay each sound being played in a cascade kind of format. So:
let soundFileNames = ["SoundFileName1", "SoundFileName2", "SoundFileName3"]
GSAudio.sharedInstance.playSounds(soundFileNames, withDelay: 1.0)
would play sound2 a second after sound1, then sound3 would play a second after sound2 etc.
Here is the class:
class GSAudio: NSObject, AVAudioPlayerDelegate {
static let sharedInstance = GSAudio()
private override init() {}
var players = [NSURL:AVAudioPlayer]()
var duplicatePlayers = [AVAudioPlayer]()
func playSound (soundFileName: String){
let soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)
if let player = players[soundFileNameURL] { //player for sound has been found
if player.playing == false { //player is not in use, so use that one
player.prepareToPlay()
player.play()
} else { // player is in use, create a new, duplicate, player and use that instead
let duplicatePlayer = try! AVAudioPlayer(contentsOfURL: soundFileNameURL)
//use 'try!' because we know the URL worked before.
duplicatePlayer.delegate = self
//assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing
duplicatePlayers.append(duplicatePlayer)
//add duplicate to array so it doesn't get removed from memory before finishing
duplicatePlayer.prepareToPlay()
duplicatePlayer.play()
}
} else { //player has not been found, create a new player with the URL if possible
do{
let player = try AVAudioPlayer(contentsOfURL: soundFileNameURL)
players[soundFileNameURL] = player
player.prepareToPlay()
player.play()
} catch {
print("Could not play sound file!")
}
}
}
func playSounds(soundFileNames: [String]){
for soundFileName in soundFileNames {
playSound(soundFileName)
}
}
func playSounds(soundFileNames: String...){
for soundFileName in soundFileNames {
playSound(soundFileName)
}
}
func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
for (index, soundFileName) in soundFileNames.enumerate() {
let delay = withDelay*Double(index)
let _ = NSTimer.scheduledTimerWithTimeInterval(delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName":soundFileName], repeats: false)
}
}
func playSoundNotification(notification: NSNotification) {
if let soundFileName = notification.userInfo?["fileName"] as? String {
playSound(soundFileName)
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
duplicatePlayers.removeAtIndex(duplicatePlayers.indexOf(player)!)
//Remove the duplicate player once it is done
}
}
Here's a Swift 4 version of #Oliver Wilkinson code with some safechecks and improved code formatting:
import Foundation
import AVFoundation
class GSAudio: NSObject, AVAudioPlayerDelegate {
static let sharedInstance = GSAudio()
private override init() { }
var players: [URL: AVAudioPlayer] = [:]
var duplicatePlayers: [AVAudioPlayer] = []
func playSound(soundFileName: String) {
guard let bundle = Bundle.main.path(forResource: soundFileName, ofType: "aac") else { return }
let soundFileNameURL = URL(fileURLWithPath: bundle)
if let player = players[soundFileNameURL] { //player for sound has been found
if !player.isPlaying { //player is not in use, so use that one
player.prepareToPlay()
player.play()
} else { // player is in use, create a new, duplicate, player and use that instead
do {
let duplicatePlayer = try AVAudioPlayer(contentsOf: soundFileNameURL)
duplicatePlayer.delegate = self
//assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing
duplicatePlayers.append(duplicatePlayer)
//add duplicate to array so it doesn't get removed from memory before finishing
duplicatePlayer.prepareToPlay()
duplicatePlayer.play()
} catch let error {
print(error.localizedDescription)
}
}
} else { //player has not been found, create a new player with the URL if possible
do {
let player = try AVAudioPlayer(contentsOf: soundFileNameURL)
players[soundFileNameURL] = player
player.prepareToPlay()
player.play()
} catch let error {
print(error.localizedDescription)
}
}
}
func playSounds(soundFileNames: [String]) {
for soundFileName in soundFileNames {
playSound(soundFileName: soundFileName)
}
}
func playSounds(soundFileNames: String...) {
for soundFileName in soundFileNames {
playSound(soundFileName: soundFileName)
}
}
func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
for (index, soundFileName) in soundFileNames.enumerated() {
let delay = withDelay * Double(index)
let _ = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName": soundFileName], repeats: false)
}
}
#objc func playSoundNotification(_ notification: NSNotification) {
if let soundFileName = notification.userInfo?["fileName"] as? String {
playSound(soundFileName: soundFileName)
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if let index = duplicatePlayers.index(of: player) {
duplicatePlayers.remove(at: index)
}
}
}
I have created a helper library that simplifies playing sounds in Swift. It creates multiple instances of AVAudioPlayer to allow playing the same sound multiple times concurrently. You can download it from Github or import with Cocoapods.
Here is the link: SwiftySound
The usage is as simple as it can be:
Sound.play(file: "sound.mp3")
All answers are posting pages of code; it doesn't need to be that complicated.
// Create a new player for the sound; it doesn't matter which sound file this is
let soundPlayer = try AVAudioPlayer( contentsOf: url )
soundPlayer.numberOfLoops = 0
soundPlayer.volume = 1
soundPlayer.play()
soundPlayers.append( soundPlayer )
// In an timer based loop or other callback such as display link, prune out players that are done, thus deallocating them
checkSfx: for player in soundPlayers {
if player.isPlaying { continue } else {
if let index = soundPlayers.index(of: player) {
soundPlayers.remove(at: index)
break checkSfx
}
}
}
Swift 5+
Compiling some of the previous answers, improving code style and reusability
I usually avoid loose strings throughout my projects and use, instead, custom protocols for objects that will hold those string properties.
I prefer this to the enum approach simply because enumerations tend to couple your project together quite quickly. Everytime you add a new case you must edit the same file with the enumeration, breaking somewhat the Open-Closed principle from SOLID and increasing chances for error.
In this particular case, you could have a protocol that defines sounds:
protocol Sound {
func getFileName() -> String
func getFileExtension() -> String
func getVolume() -> Float
func isLoop() -> Bool
}
extension Sound {
func getVolume() -> Float { 1 }
func isLoop() -> Bool { false }
}
And when you need a new sound you can simply create a new structure or class that implements this protocol (It will even be suggested on autocomplete if your IDE, just like Xcode, supports it, giving you similar benefits to those of the enumeration... and it works way better in medium to large multi framework projects).
(Usually I leave volume and other configurations with default implementations as they are less frequently customized).
For instance, you could have a coin drop sound:
struct CoinDropSound: Sound {
func getFileName() -> String { "coin_drop" }
func getFileExtension() -> String { "wav" }
}
Then, you could use a singleton SoundManager that would take care of managing playing audio files
import AVFAudio
final class SoundManager: NSObject, AVAudioPlayerDelegate {
static let shared = SoundManager()
private var audioPlayers: [URL: AVAudioPlayer] = [:]
private var duplicateAudioPlayers: [AVAudioPlayer] = []
private override init() {}
func play(sound: Sound) {
let fileName = sound.getFileName()
let fileExtension = sound.getFileExtension()
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension),
let player = getAudioPlayer(for: url) else { return }
player.volume = sound.getVolume()
player.numberOfLoops = numberOfLoops
player.prepareToPlay()
player.play()
}
private func getAudioPlayer(for url: URL) -> AVAudioPlayer? {
guard let player = audioPlayers[url] else {
let player = try? AVAudioPlayer(contentsOf: url)
audioPlayers[url] = player
return player
}
guard player.isPlaying else { return player }
guard let duplicatePlayer = try? AVAudioPlayer(contentsOf: url) else { return nil }
duplicatePlayer.delegate = self
duplicateAudioPlayers.append(duplicatePlayer)
return duplicatePlayer
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
duplicateAudioPlayers.removeAll { $0 == player }
}
}
Here I created a helper getAudioPlayer to be able to return early from code execution and make use of the guard let.
Using guard let more often and preferring less nested code can, most of the time, highly improve readability.
To use this SoundManager from anywhere in your project, simply access its shared instance and pass an object that conforms to Sound.
For example, given the previous CoinDropSound:
SoundManager.shared.play(sound: CoinDropSound())
You could maybe omit the sound parameter as it may improve readability
class SoundManager {
// ...
func play(_ sound: Sound) {
// ...
}
// ...
}
And then:
SoundManager.shared.play(CoinDropSound())