I'm creating a game where the user controls a character with a jetpack. When the jetpack intersects a diamond, I add the diamond to their total and then play a sound. However, the sound makes the game pause for a tenth of a second or so and disrupts the flow. This is the code I'm using:
var diamondSound = NSBundle.mainBundle().URLForResource("diamondCollect", withExtension: "wav")!
var diamondPlayer = AVAudioPlayer?()
class GameScene: SKScene{
override func didMoveToView(view: SKView) {
do {
diamondPlayer = try AVAudioPlayer(contentsOfURL: diamondSound)
guard let player = diamondPlayer else { return }
player.prepareToPlay()
} catch let error as NSError {
print(error.description)
}
}
And then later:
override func update(currentTime: CFTimeInterval) {
if character.intersectsNode(diamond){
diamondPlayer?.play()
addDiamond()
diamond.removeFromParent()
}
}
Also I am using Sprite Kit if that matters. Any help is greatly appreciated!
Usually, I prefeer to use SKAction.playSoundWithFile in my games but this one have a limitation, there is no volume setting.
So, whit this extension you can solve this lack:
public extension SKAction {
public class func playSoundFileNamed(fileName: String, atVolume: Float, waitForCompletion: Bool) -> SKAction {
let nameOnly = (fileName as NSString).stringByDeletingPathExtension
let fileExt = (fileName as NSString).pathExtension
let soundPath = NSBundle.mainBundle().URLForResource(nameOnly, withExtension: fileExt)
var player: AVAudioPlayer! = AVAudioPlayer()
do { player = try AVAudioPlayer(contentsOfURL: soundPath!, fileTypeHint: nil) }
catch let error as NSError { print(error.description) }
player.volume = atVolume
let playAction: SKAction = SKAction.runBlock { () -> Void in
player.prepareToPlay()
player.play()
}
if(waitForCompletion){
let waitAction = SKAction.waitForDuration(player.duration)
let groupAction: SKAction = SKAction.group([playAction, waitAction])
return groupAction
}
return playAction
}
}
Usage:
self.runAction(SKAction.playSoundFileNamed("diamondCollect.wav", atVolume:0.5, waitForCompletion: true))
Related
The goal is to play several sounds one after another (getReady -> nextExercise -> burpees).
The problem is that only the first one is being played
How it should work:
I call playGetReady() from WorkoutTabataViewController
I plays the first sound
After the first sound is finished, automatically "audioPlayerDidFinishPlaying()" is being called
It triggers "playNextSound()" func, which playing next sound
But audioPlayerDidFinishPlaying() is not being called. Or am I missing something and it should work differently?
class AudioPlayerManager: AVAudioPlayerDelegate {
var description: String
static let shared = AudioPlayerManager()
var audioPlayer: AVAudioPlayer?
var workoutVC: WorkoutTabataViewController?
var mainVC: MainTabataViewController?
var currentSound = 0
let urls: [URL]
init() {
self.description = ""
//First sound
let getReady = Bundle.main.path(forResource: "Get ready", ofType: "mp3")!
let urlGetReady = URL(fileURLWithPath: getReady)
//Second sound
let nextExercise = Bundle.main.path(forResource: "Next Exercise", ofType: "mp3")!
let urlNextExercise = URL(fileURLWithPath: nextExercise)
//Third sound
let burpees = Bundle.main.path(forResource: "Burpees", ofType: "mp3")!
let urlBurpees = URL(fileURLWithPath: burpees)
urls = [urlGetReady, urlNextExercise, urlBurpees]
}
func playGetReady() {
do {
audioPlayer = try AVAudioPlayer(contentsOf: urls[currentSound])
audioPlayer?.delegate = self
audioPlayer?.play()
} catch {
print(error)
}
}
func playNextSound() {
currentSound += 1
if currentSound < urls.count {
do {
audioPlayer = try AVAudioPlayer(contentsOf: urls[currentSound])
audioPlayer?.delegate = self
audioPlayer?.play()
} catch {
print(error)
}
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if flag {
playNextSound()
}
}
}
Your audio manager class is not introspectable. Say #objc func audioPlayerDidFinishPlaying or, better, make it an NSObject.
I am creating a basic soundboard app. I have two switches. One that when activated will make the audio slower and lower pitch and one that makes it faster and higher. I have an if else if if statement that looks at those switches and then plays the audio accordingly, however when I try to push it a second time, either for the same sound or a different sound, it crashes.
I'm about 99% sure that this is due to the AVAudioEngine needing to be reset or having the nodes themselves reset, but I am way past being out of my league here. I've searched high and low, but the answer I seem to be finding relate to resetting the player when using different buttons to make the high or low sounds. Any thoughts?
class ViewController: UIViewController {
#IBOutlet weak var sassSwitch: UISwitch!
#IBOutlet weak var chipSwitch: UISwitch!
#IBAction func sassAction(_ sender: UISwitch) {
chipSwitch.setOn(false, animated: true)
}
#IBAction func chipSwitch(_ sender: UISwitch) {
sassSwitch.setOn(false, animated: true)
}
///Playback Engine
private let audioEngine = AVAudioEngine()
///Player's Nodes
private let pitchPlayer = AVAudioPlayerNode()
private let timePitch = AVAudioUnitTimePitch()
///Audio Files to be played
private var audioFile1 = AVAudioFile()
private var audioFile2 = AVAudioFile()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if let filePath = Bundle.main.path(forResource: "PeteNope", ofType:
"mp3") {
let filePathURL = URL(fileURLWithPath: filePath)
setPlayerFile(filePathURL)
}
if let filePath2 = Bundle.main.path(forResource: "Law_WOW", ofType:
"mp3") {
let filePath2URL = URL(fileURLWithPath: filePath2)
setPlayerFile2(filePath2URL)
}
}
private func setPlayerFile(_ fileURL: URL) {
do {
let file = try AVAudioFile(forReading: fileURL)
self.audioFile1 = file
} catch {
fatalError("Could not create AVAudioFile instance. error: \(error).")
}
}
private func setPlayerFile2(_ fileURL: URL) {
do {
let file = try AVAudioFile(forReading: fileURL)
self.audioFile2 = file
} catch {
fatalError("Could not create AVAudioFile instance. error: \(error).")
}
}
#IBAction func sound1Play(_ sender: UIButton) {
if sassSwitch.isOn {
timePitch.pitch = -300
timePitch.rate = 0.5
audioEngine.attach(pitchPlayer)
audioEngine.attach(timePitch)
audioEngine.connect(pitchPlayer, to: timePitch, format: audioFile1.processingFormat)
audioEngine.connect(timePitch, to: audioEngine.outputNode, format: audioFile1.processingFormat)
pitchPlayer.scheduleFile(audioFile1, at: nil, completionHandler: nil)
// Start the engine.
do {
try audioEngine.start()
} catch {
fatalError("Could not start engine. error: \(error).")
}
pitchPlayer.play()
} else if chipSwitch.isOn {
timePitch.pitch = +500
timePitch.rate = 2.0
audioEngine.attach(pitchPlayer)
audioEngine.attach(timePitch)
audioEngine.connect(pitchPlayer, to: timePitch, format: audioFile1.processingFormat)
audioEngine.connect(timePitch, to: audioEngine.outputNode, format: audioFile1.processingFormat)
pitchPlayer.scheduleFile(audioFile1, at: nil, completionHandler: nil)
// Start the engine.
do {
try audioEngine.start()
} catch {
fatalError("Could not start engine. error: \(error).")
}
pitchPlayer.play()
} else {
timePitch.pitch = +0
timePitch.rate = 1.0
audioEngine.attach(pitchPlayer)
audioEngine.attach(timePitch)
audioEngine.connect(pitchPlayer, to: timePitch, format: audioFile1.processingFormat)
audioEngine.connect(timePitch, to: audioEngine.outputNode, format: audioFile1.processingFormat)
pitchPlayer.scheduleFile(audioFile1, at: nil, completionHandler: nil)
// Start the engine.
do {
try audioEngine.start()
} catch {
fatalError("Could not start engine. error: \(error).")
}
pitchPlayer.play()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
It looks like you might just need to move the attach, connect, and engine.start functions out of the sound1Play function, and put them into viewDidLoad. Currently you're connecting the nodes every action, and restarting the engine multiple times as well.
I have a struct with my audio player in it:
struct MT_Audio {
func playAudio(_ fileName:String, _ fileExtension:String, _ atVolume:Float) {
var audioPlayer = AVAudioPlayer()
if let audioPath = Bundle.main.path(forResource: fileName, ofType: fileExtension) {
let audioURL = URL(string:audioPath)
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioURL!)
audioPlayer.volume = atVolume
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
}
}
}
//I'm calling it in viewDidLoad like this:
guard let fileURL = Bundle.main.url(forResource:"heartbeat-01a", withExtension: "mp3")
else {
print("can't find file")
return
}
let myAudioPlayer = MT_Audio() //<--RESOLVED THE ISSUE BY MAKING THIS A PROPERTY OF THE VIEWCONTROLLER
myAudioPlayer.playAudio("heartbeat-01a", "mp3", 1.0)
Since it doesn't crash and burn on the guard I know the file is there. I've also put a break point in after the try and I am getting to the audio player. When I go to the actual file and click on it in Xcode it plays. This fails on both the sim and device. Any help would be appreciated.
Looks like your audioPlayer is only stored within your playAudio function.
Try to keep the audioPlayer as an variable inside your class like this:
struct MT_Audio {
var audioPlayer: AVAudioPlayer?
mutating func playAudio(_ fileName:String, _ fileExtension:String, _ atVolume:Float) {
// is now member of your struct -> var audioPlayer = AVAudioPlayer()
if let audioPath = Bundle.main.path(forResource: fileName, ofType: fileExtension) {
let audioURL = URL(string:audioPath)
do {
let audioPlayer = try AVAudioPlayer(contentsOf: audioURL!)
audioPlayer.volume = atVolume
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print(error)
}
}
}
}
I used many codes that was for record an play the voice, but most of them are not in swift3 and they don't work in my app.
This code works, but I want to create a separate class from the viewcontroller that do recording an playing voices. Also the mentioned github code is complex an I'm searching for a simplified code.
Update:
After recording, when I check existence of the recorded file, the file doesn't exist, and it raises EXC_BAD_ACCESS error on appDelegate.
What's wrong?
Any suggestions would be appreciated.
Try to record audio by wirting line
let isRec = AudioManager.shared.record(fileName: "rec")
if isRec returned true then recording is happening else not.
To finish recording use : let recordedURL = AudioManager.shared.finishRecording()
To play recorded file send above url to setupUpPlayer() function in manager class
Not to forget to use extension code snippets give below the code snippet which are delegate functions of AVAudioRecorder and AVAudioPlayer
import Foundation
import AVFoundation
class AudioManager: NSObject {
static let shared = AudioManager()
var recordingSession: AVAudioSession?
var recorder: AVAudioRecorder?
var meterTimer: Timer?
var recorderApc0: Float = 0
var recorderPeak0: Float = 0
//PLayer
var player: AVAudioPlayer?
var savedFileURL: URL?
func setup() {
recordingSession = AVAudioSession.sharedInstance()
do {
try recordingSession?.setCategory(AVAudioSessionCategoryPlayAndRecord, with: .defaultToSpeaker)
try recordingSession?.setActive(true)
recordingSession?.requestRecordPermission({ (allowed) in
if allowed {
print("Mic Authorised")
} else {
print("Mic not Authorised")
}
})
} catch {
print("Failed to set Category", error.localizedDescription)
}
}
func record(fileName: String) -> Bool {
setup()
let url = getUserPath().appendingPathComponent(fileName + ".m4a")
let audioURL = URL.init(fileURLWithPath: url.path)
let recordSettings: [String: Any] = [AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
AVNumberOfChannelsKey: 2,
AVSampleRateKey: 44100.0]
do {
recorder = try AVAudioRecorder.init(url: audioURL, settings: recordSettings)
recorder?.delegate = self
recorder?.isMeteringEnabled = true
recorder?.prepareToRecord()
recorder?.record()
self.meterTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer: Timer) in
//Update Recording Meter Values so we can track voice loudness
if let recorder = self.recorder {
recorder.updateMeters()
self.recorderApc0 = recorder.averagePower(forChannel: 0)
self.recorderPeak0 = recorder.peakPower(forChannel: 0)
}
})
savedFileURL = url
print("Recording")
return true
} catch {
print("Error Handling", error.localizedDescription)
return false
}
}
func getUserPath() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
func finishRecording() -> String {
recorder?.stop()
self.meterTimer?.invalidate()
var fileURL: String?
if let url: URL = recorder?.url {
fileURL = String(describing: url)
}
return /fileURL
}
//Player
func setupPlayer(_ url: URL) {
do {
try player = AVAudioPlayer.init(contentsOf: url)
} catch {
print("Error1", error.localizedDescription)
}
player?.prepareToPlay()
player?.play()
player?.volume = 1.0
player?.delegate = self
}
}
//MARK:- Audio Recorder Delegate
extension AudioManager: AVAudioRecorderDelegate {
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
print("AudioManager Finish Recording")
}
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
print("Encoding Error", /error?.localizedDescription)
}
}
//MARK:- Audio Player Delegates
extension AudioManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
successfully flag: Bool) {
player.stop()
print("Finish Playing")
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer,
error: Error?) {
print(/error?.localizedDescription)
}
}
I want a user to press a button, it changes background color (to yellow), a WAV is played and on completion of the WAV the button reverts to its original color (to red). So have a completion handler around the sound. Have tried various combinations of the code below but the WAV plays and the button doesn't appear to change color.
Is this the wrong approach or am I doing something wrong? Don't want to have to put completion handlers around the color changes as that, I presume, is overkill.
Many thanks.
typealias CompletionHandler = (success:Bool) -> Void
#IBAction func fireButton(sender: AnyObject) {
playLaser( { (success)-> Void in
if success {
self.shots -= 1
self.labelShotsLeft.text = String(self.shots)
} else {
}
})
}
func playLaser(completionHandler: CompletionHandler) {
fireButton.layer.backgroundColor = UIColor.yellowColor().CGColor
let url = NSBundle.mainBundle().URLForResource("laser", withExtension: "wav")!
do {
player = try AVAudioPlayer(contentsOfURL: url)
guard let player = player else { return }
player.prepareToPlay()
player.play()
} catch let error as NSError {
print(error.description)
}
self.fireButton.layer.backgroundColor = UIColor.redColor().CGColor
completionHandler(success: true)
}
To detect AVAudioPlayer finish playing, you need to use AVAudioPlayerDelegate.
You may need to write something like this:
func playLaser(completionHandler: CompletionHandler) {
fireButton.layer.backgroundColor = UIColor.yellowColor().CGColor
let url = NSBundle.mainBundle().URLForResource("laser", withExtension: "wav")!
do {
player = try AVAudioPlayer(contentsOfURL: url)
guard let player = player else { return }
player.delegate = self //<- Sorry, this was missing in my first post
player.play()
} catch let error as NSError {
print(error.description)
}
audioPlayerCompletionHandler = completionHandler
}
var audioPlayerCompletionHandler: CompletionHandler?
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
self.fireButton.layer.backgroundColor = UIColor.redColor().CGColor
audioPlayerCompletionHandler?(success: true)
}
(You need to add conformance to AVAudioPlayerDelegate to your ViewController's declaration header.)
Code does not magically pause and wait just because you say play.play() — that would be horrible! Thus, your so-called completion handler is not a completion handler at all. It runs immediately — that is, as soon you start playing. Your code does nothing about obtaining information as to when the audio player has finished playing.
For that, you need to configure a delegate and receive the delegate message that audio player emits when it finishes playing.
This is one of those questions which is a little more subtle than meets the eye. I tried putting three completion handlers around each task: change colour to yellow, play sound, change colour back to red. The code was being executed in the correct sequence as I NSLogged it but the button never changed colour due to screen updating controls. Here is the code that works that I hope other readers might find useful:
Swift 2.0
#IBAction func fireButton(sender: AnyObject) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
dispatch_sync(dispatch_get_main_queue()) {
self.fireButton.layer.backgroundColor = UIColor.yellowColor().CGColor
}
self.playLaser( { (success)-> Void in
if success {
self.shots -= 1
} else {
}
})
dispatch_sync(dispatch_get_main_queue()) {
self.labelShotsLeft.text = String(self.shots)
self.fireButton.layer.backgroundColor = UIColor.redColor().CGColor
}
}
}
func playLaser(completion: (success: Bool) -> ()) {
let url = NSBundle.mainBundle().URLForResource("laser", withExtension: "wav")!
do {
player = try AVAudioPlayer(contentsOfURL: url)
guard let player = player else { return }
player.play()
completion(success: true)
} catch let error as NSError {
completion(success: false)
}
}
Swift 3.0
#IBAction func fireButton(_ sender: AnyObject) {
let fireQueue = DispatchQueue(label: "queueFirebutton")
fireQueue.async {
DispatchQueue.main.sync {
self.fireButtonDisabled()
}
DispatchQueue.main.sync {
self.playLaser()
self.shots -= 1
if self.shots <= 0 {
self.shots = 0
}
}
DispatchQueue.main.sync {
if self.shots < 0 { self.shots = 0}
self.labelShotsLeft.text = String(self.shots)
sleep(1)
self.fireButtonEnabled()
}
}
}
func playLaser() {
let url = Bundle.main.url(forResource: "laser", withExtension: "wav")!
do {
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else { return }
player.play()
} catch {
}
}