Search code examples
iosswiftuibuttonavaudioplayerdispatch-async

Error when pressing the sound button quickly


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()
        }
    }

}

Solution

  • 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.