Play sound effect without latency - ios

I am trying to make a little app to teach myself some swift and I'm having some problems figuring out how to get my app to function a certain way.
My app should be able to play an airhorn sound just like the way it sounds in this video...
https://www.youtube.com/watch?v=Ks5bzvT-D6I
But each time I tap the screen repeatedly there is a slight delay before the sound is played so it's not sounding like that at all.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
var hornSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("horn", ofType: "mp3")!)
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: nil)
AVAudioSession.sharedInstance().setActive(true, error: nil)
var error:NSError?
audioPlayer = AVAudioPlayer(contentsOfURL: hornSound, error: &error)
audioPlayer.prepareToPlay()
}
#IBAction func playSound(sender: UIButton) {
audioPlayer.pause()
audioPlayer.currentTime = 0
audioPlayer.play()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I have also come across this thread about using spritekit
Creating and playing a sound in swift
And in trying that I got it to play the sound without the delay, but with sprite kit I can't stop the existing sound, so they just overlap which is not the effect I want.
Is there a work around to get this working the way it sounds in the video.

Apple recommends AVAudioPlayer for playback of audio data unless you require very low I/O latency.
So you might want to try another approach.
In one of my apps I play countdown sounds by creating a system sound ID from my wav file. Try this in your class:
import UIKit
import AudioToolbox
class ViewController: UIViewController {
var sound: SystemSoundID = 0
override func viewDidLoad() {
super.viewDidLoad()
var hornSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("horn", ofType: "mp3")!)
AudioServicesCreateSystemSoundID(hornSound!, &self.sound)
}
#IBAction func playSound(sender: UIButton) {
AudioServicesPlaySystemSound(self.sound)
}
...
}

Related

xcode - stop audio files from playing over each other

So I am new to xcode and programming in general. I am creating a simple soundboard and I am running into an issue. What I want to do is stop the current audio from playing when a different audio button is selected. Then have the stopped audio reset itself and be ready to start playing from the beginning when pressed again. I had added a stop() to my code and it would stop() the audio from playing but when I would try to restart it, I discovered that the audio just paused, and continued from the where it stopped.
Can anyone provide me with the code set that I need to fix this, and if not can anyone point me in the direction of a good tutorial that can lead me down the right path. I looked at the apple Audio documentation and unfortunately I got lost.
My code is as follows.
var SoundPlayer1:AVAudioPlayer = AVAudioPlayer()
var SoundPlayer2:AVAudioPlayer = AVAudioPlayer()
var SoundPlayer3:AVAudioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
let FileLocation1 = Bundle.main.path(forResource: "Audio1", ofType: "mp3")
let FileLocation2 = Bundle.main.path(forResource: "Audio2", ofType: "mp3")
let FileLocation3 = Bundle.main.path(forResource: "Audio3", ofType: "mp3")
do {
SoundPlayer1 = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: FileLocation1!))
SoundPlayer2 = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: FileLocation2!))
SoundPlayer3 = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: FileLocation3!))
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient)
try AVAudioSession.sharedInstance().setActive(true)
}
catch {
print(error)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func catButton(_ sender: AnyObject) {
SoundPlayer1.play()
}
#IBAction func pigButton(_ sender: AnyObject) {
SoundPlayer2.play()
}
#IBAction func pigButton(_ sender: AnyObject) {
SoundPlayer3.play()
}
This is just a bit of Swift logic you can carry out to check if any other of the players are playing and then stop them!
// Make sure all audio playback times are reset
func resetAllAudioTime(){
SoundPlayer1.currentTime = 0.0;
SoundPlayer2.currentTime = 0.0;
SoundPlayer3.currentTime = 0.0;
}
IBAction func catButton(_ sender: AnyObject) {
// Check SoundPlayer2 and stop if playing
if(SoundPlayer2.playing){
SoundPlayer2.pause();
}
// Check SoundPlayer3 and stop if playing
if(SoundPlayer3.playing){
SoundPlayer3.pause();
}
// Reset all playback times to start of file
resetAllAudioTime();
// Play audio file
SoundPlayer1.play()
}
#IBAction func pigButton(_ sender: AnyObject) {
// Check SoundPlayer1 and stop if playing
if(SoundPlayer1.playing){
SoundPlayer1.pause();
}
// Check SoundPlayer3 and stop if playing
if(SoundPlayer3.playing){
SoundPlayer3.pause();
}
// Reset all playback times to start of file
resetAllAudioTime();
// Play audio file
SoundPlayer2.play()
}
#IBAction func pigButton(_ sender: AnyObject) {
// Check SoundPlayer1 and stop if playing
if(SoundPlayer1.playing){
SoundPlayer1.pause();
}
// Check SoundPlayer2 and stop if playing
if(SoundPlayer2.playing){
SoundPlayer2.pause();
}
// Reset all playback times to start of file
resetAllAudioTime();
// Play audio file
SoundPlayer3.play()
}
Note: This code is untested but should work. See the AVAudioPlayer docs for any further help.
You will see that rather than calling the AVAudioPlayer.stop() method, I have used the AVAudioPlayer.pause() and then reset the playback time to the start. If you read the documentation it states calling the AVAudioPlayer.stop() method:
func stop()
Stops playback and undoes the setup needed for playback.
Therefore, calling pause and resetting the time doesn't keep undoing the setup needed for playback (should be more efficient!).
I hope this helps! :-)
WoodyDev, thank you for the info it was clean and easy to understand. I was able to fix my issue with two functions.
func stopAll() {
SoundPlayer1.stop()
SoundPlayer2.stop()
}
func resetAudioTime() {
SoundPlayer1.currentTime = 0.0;
SoundPlayer2.currentTime = 0.0;
}
#IBAction func catButton(_ sender: AnyObject) {
stopAll();<br/>
resetAudioTime();<br/>
SoundPlayer1.play()
}

How do I perform segue AFTER the sound has stopped playing

I have made a custom LaunchScreen in the Main.Storyboard to make it seem that a sound is actually coming from the LaunchScreen. The sound works fine and the segue to the next view controller as well. The only problem is that the segue happens before the sound has stopped playing. I would like the sound to complete before making the segue. Logically, it should work since the performSegue is directly after the .play(). But it seems that the two happens simultaneously. Here's my code:
super.viewDidLoad()
//PLAY SOUND CLIP//
let musicFile = Bundle.main.path(forResource: "fanfare", ofType: ".mp3")
do {
try musicSound = AVAudioPlayer(contentsOf: URL (fileURLWithPath: musicFile!))
} catch { print("ERROR PLAYING MUSIC")}
musicSound.play() //WORKS
//
DispatchQueue.main.async() {
self.performSegue(withIdentifier: "myLaunchSegue", sender: self) //WORKS
}
I have tried to add:
perform(Selector(("showNavController")), with: self, afterDelay: 3)
where "showNavController" simply is the segue:
func showNavController() {
performSegue(withIdentifier: "myLaunchSegue", sender: nil)
}
but the program crashes with the error "uncaught exception.... ....unrecognized selector sent to instance"
I have also tried to add a boolean to keep the program from progressing until the sound has played, but didn't get it to work. Any ideas?
//////////////////////////
Update:
Trying Russels answer, but have a few questions. Setting AVAudioPlayer as delegate, does that mean setting it next to the class like this:
class MyLaunchViewController: UIViewController, AVAudioPlayer { ...
Also, how do I call the function audioPlayerDidFinishPlaying? Like so:
audioPlayerDidFinishPlaying(musicSound, successfully: true)
I'll post the whole code block. Makes it easier to understand...
import UIKit
import AVFoundation //FOR SOUND
class MyLaunchViewController: UIViewController, AVAudioPlayer {
var musicSound: AVAudioPlayer = AVAudioPlayer() //FOR SOUND
override func viewDidLoad() {
super.viewDidLoad()
//PLAY SOUND CLIP//
let musicFile = Bundle.main.path(forResource: "fanfare", ofType: ".mp3")
do {
try musicSound = AVAudioPlayer(contentsOf: URL (fileURLWithPath: musicFile!))
} catch { print("ERROR PLAYING MUSIC")}
musicSound.play() //WORKS
//
audioPlayerDidFinishPlaying(musicSound, successfully: true)
}
optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
DispatchQueue.main.async() {
self.performSegue(withIdentifier: "myLaunchSegue", sender: self)
}
}
I get an error when writing AVAudioPlayer in the class (perhaps I misunderstood what I was supposed to do?). It says I have multiple inheritances. Also, it doesn't want me to set the new function as optional, as its only for protocol members. Finally, If I correct the errors and run the program, the next segue runs before the sound has finished playing... :( sad panda.
You need to make your LaunchScreen an AVAudioPlayerDelegate, and then use the audioPlayerDidFinishPlaying callback. Here's all you need in the first controller
import UIKit
import AVFoundation
class ViewController: UIViewController, AVAudioPlayerDelegate
{
var musicSound: AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
//PLAY SOUND CLIP//
let musicFile = Bundle.main.path(forResource: "sound", ofType: ".wav")
do {
try musicSound = AVAudioPlayer(contentsOf: URL (fileURLWithPath: musicFile!))
} catch { print("ERROR PLAYING MUSIC")}
musicSound?.delegate = self
musicSound!.play() //WORKS
print("Next line after play")
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
DispatchQueue.main.async() {
print("audioPlayerDidFinishPlaying")
self.performSegue(withIdentifier: "myLaunchSegue", sender: self)
}
}
}
You can get more details here https://developer.apple.com/documentation/avfoundation/avaudioplayerdelegate/1389160-audioplayerdidfinishplaying

How To Make UISlider match Audio progress

I am creating a simple music app, and I was wondering how I can make a UiSlider to follow the progress of a audio file. Here's my project so far:
Code:
import UIKit
import AVFoundation
class SongDetailViewController: UITableViewController {
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "Song Name", ofType: "mp3")!))
audioPlayer.prepareToPlay()
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
}
}
catch {
print(error)
}
}
// Buttons
// Dismiss
#IBAction func dismiss(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
// Play
#IBAction func play(_ sender: Any) {
audioPlayer.stop()
audioPlayer.play()
}
// Pause
#IBAction func pause(_ sender: Any) {
audioPlayer.pause()
}
// Restart
#IBAction func restart(_ sender: Any) {
audioPlayer.currentTime = 0
}
}
I'm wanting to create the uislider similar to the Apple Music app where it follows the audio file's progress and whenever the user slides the ball thing (lol) it goes to that time of the song. If you could give me some code to complete this, that would be amazing!
Please keep in mind that I am fairly new to coding and am still learning swift, so keep it simple :-) Thanks again!
If you switch to using an AVPlayer, you can add a periodicTimeObserver to your AVPlayer. In the example below you'll get a callback every 1/30 second…
let player = AVPlayer(url: Bundle.main.url(forResource: "Song Name", withExtension: "mp3")!)
player.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 30), queue: .main) { time in
let fraction = CMTimeGetSeconds(time) / CMTimeGetSeconds(player.currentItem!.duration)
self.slider.value = fraction
}
Where you create an audioPlayer in your code, replace with the code above.
Using AVAudioPlayer you could create a periodic timer that fires several times a second (up to 60 times/second - any more would be a waste) and updates your slider based on your audio player's currentTime property. To sync the update with screen refresh you could use a CADisplayLink timer.
Edit:
This part of my answer doesn't work:
It should also be possible to set up a Key Value Observer on your
AVAudioPlayers currentTime property so that each time the value
changes your observer fires. (I haven't tried this, but it should
work.)

ReEdited : Audio not playing in real ios Device but playing in simulated devices in Swift2

This is my code in swift2 , absolutely no error ,Im running on a real device but no sound is coming ? no exceptions nothing am I missing something?
import UIKit
import AVFoundation
class ViewController: UIViewController {
#IBAction func play() {
audioPlayer.play()
}
var audioPlayer : AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let alertSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("drum", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: alertSound)
audioPlayer.prepareToPlay()
audioPlayer.volume = 1.0
audioPlayer.play()
print("yes")
}catch _ {
audioPlayer = nil
}
}
}
I found out what was the problem from one of the videos of Apple's WWDC Conference
http://adcdownload.apple.com/videos/wwdc_2010__hd/session_412__audio_development_for_iphone_os_part_1.mov (only viewable on safari)
The problem was audio routing , when i plugged in the earphone I could listen to the sound from the real device .
When the route is changed we need to set the proper audio routing using AVAudioSession . More info in that video above.
Import AVFoundation and add this to your viewDidLoad:
var err = NSErrorPointer()
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: err)

Have a sound stop and start immediately on button press

I am making a little app for fun to test my learning and I've gotten it for the most part but there is a little caveat to the solution I've come up with.
The app should play a horn sound everytime you click the screen. But this should allow you to continually press it in quick succession and the horn sound plays each time. It should also stop the previous horn sound as well so there are no overlapping sounds.
Here is what I've managed so far
import UIKit
import AVFoundation
class ViewController: UIViewController {
var horn:AVAudioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
prepareAudios()
}
#IBAction func buttonPressed(sender: AnyObject) {
horn.stop()
horn.currentTime = 0
horn.play()
}
func prepareAudios() {
var path = NSBundle.mainBundle().pathForResource("horn", ofType: "mp3")
horn = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path!), error: nil)
horn.prepareToPlay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
And it works, sort of. It stops the sound and sets it back to the beginning, but if you click really quickly it skips and doesn't play sometimes. I want to try ti get around this but I'm not finding much on google. Is there a better solution that will allow me to play this sound like this really quickly?
UPDATE
As per the answer from Duncan, I tried to create two instances of the sound to play and switch between them both but I can't seem to get them to start the sound right when the screen is touched each time it's touched in quick succession. Here is the code I have tried
class ViewController: UIViewController {
var horn1:AVAudioPlayer = AVAudioPlayer()
var horn2:AVAudioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
prepareAudios()
}
var bool = true
#IBAction func butonPressed(sender: AnyObject) {
if(bool){
horn1.play()
horn2.stop()
horn2.currentTime = 0
bool = false
}else{
horn2.play()
horn1.stop()
horn1.currentTime = 0
bool = true
}
}
func prepareAudios() {
var path = NSBundle.mainBundle().pathForResource("horn", ofType: "mp3")
horn1 = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path!), error: nil)
horn2 = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path!), error: nil)
horn1.prepareToPlay()
horn2.prepareToPlay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
On each press of the button there is a slight delay before the sound plays. And I want the sound to start over immediately. I want the horn to be able to sound like in this video
https://www.youtube.com/watch?v=Ks5bzvT-D6I
it's so much simpler than that! if you want the audio to start exactly after the button is pressed, you should change the event to "Touch Down" when you're connecting the button to your code. That's it! and for the button this is enough :
#IBAction func buttonPressed(sender: AnyObject) {
horn.currentTime = 0
horn.play()
}
Try creating 2 instances of your horn player, and calling prepareToPlay each time. Then when you click your button, stop the one that's playing, start the new one, and call prepareToPlay on the one that you stopped in order to get it ready. You'll need to build some program logic to implement that.

Resources