I have a volume toggle button. When I press the button, then pause or play is called with some fading. The problem is that if I quickly press the button twice, for example, pause and then playback, I will hear a jump in volume. Moreover, if I quickly call play, and then press sharply and call pause, then in playback mode the audio will stop due to calling DispatchQueue
with a delay. How to fix this?
ViewController:
class ViewController: UIViewController {
@objc func soundState() {
if defaults.string(forKey: "sound") == "false" {
audio.pause(volume: 0.0, duration: 3.0)
} else if defaults.string(forKey: "sound") == "true" {
audio.play(volume: 1.0, duration: 3.0)
}
}
}
AudioController:
class Audio: NSObject, AVAudioPlayerDelegate {
var audioPlayer: AVAudioPlayer!
func loopedAudio(fileName: String, fileExtension: String) {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
try? AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension)
do {
audioPlayer = try AVAudioPlayer(contentsOf: url!)
audioPlayer.prepareToPlay()
audioPlayer.delegate = self
audioPlayer.numberOfLoops = -1
} catch {
print("error")
}
}
func play(volume: Float, duration: Double) {
audioPlayer.volume = 0
audioPlayer.play()
audioPlayer.setVolume(volume, fadeDuration: duration)
}
func pause(volume: Float, duration: Double) {
audioPlayer.volume = 1
audioPlayer.setVolume(volume, fadeDuration: duration)
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
self.audioPlayer.pause()
}
}
}
Your issue arises because one of your actions,pause
is delayed, while your other action play
is instant. This means that if you change state rapidly, they delayed action may no longer be required.
The solution is to explicitly track the desired state in order to avoid unnecessary delayed actions:
enum PlaybackState {
case playing
case paused
}
class Audio: NSObject, AVAudioPlayerDelegate {
var playbackState: PlaybackState = .paused
var audioPlayer: AVAudioPlayer!
func loopedAudio(fileName: String, fileExtension: String) {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
try? AVAudioSession.sharedInstance().setActive(true)
let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension)
do {
audioPlayer = try AVAudioPlayer(contentsOf: url!)
audioPlayer.prepareToPlay()
audioPlayer.delegate = self
audioPlayer.numberOfLoops = -1
} catch {
print("error")
}
}
func play(volume: Float, duration: Double) {
audioPlayer.volume = 0
audioPlayer.play()
audioPlayer.setVolume(volume, fadeDuration: duration)
self.playbackState = .playing
}
func pause(volume: Float, duration: Double) {
audioPlayer.volume = 1
audioPlayer.setVolume(volume, fadeDuration: duration)
self.playbackState = paused
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
guard self.playbackState == .paused else {
return
}
self.audioPlayer.pause()
}
}
}
Now, when your dispatchAfter
executes, it will first check to see if the pause
state is still required. If not, it will simply return, leaving the audio playing.