iosavplayeravaudioplayeravplayerlooper

Loop audio for certain duration on iOS


I am wondering what the best solution to looping audio for a defined duration on iOS is. I am currently playing around with

So what I need is to define a duration for which a certain sound-file is repeated. F.e. I have a 8 second mp3 and want to play it for f.e one minute.

What would also be suuuuper great, is if I could cross-fade when it starts over again.


Solution

  • You were on the right track with AVPlayerLooper.

    This is how you setup AVPlayerLooper

    var playerLooper: AVPlayerLooper!
    var player: AVQueuePlayer!
    
    func play(_ url: URL) {
        let asset = AVAsset(url: url)
        let playerItem = AVPlayerItem(asset: asset)
    
        player = AVQueuePlayer(playerItem: playerItem)
        playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
    
        player.play()
    }
    

    To stop the loop after a set amount of time you can use addBoundaryTimeObserver(forTimes:queue:using:) For example:

    let assetDuration = CMTimeGetSeconds(asset.duration)
    let maxDuration = 60.0 // Define max duration
    let maxLoops = floor(maxDuration / assetDuration)
    let lastLoopDuration = maxDuration - (assetDuration * maxLoops)
    let boundaryTime = CMTimeMakeWithSeconds(lastLoopDuration, preferredTimescale: 1)
    let boundaryTimeValue = NSValue(time: boundaryTime)
    
    player.addBoundaryTimeObserver(forTimes: [boundaryTimeValue], queue: DispatchQueue.main) { [weak self] in
        if self?.playerLooper.loopCount == Int(maxLoops) {
            self?.player.pause()
        }
    }
    

    For fading in/out you have to set the audioMix property to your AVPlayerItem instance before using it.

    let introRange = CMTimeRangeMake(start: CMTimeMakeWithSeconds(0, preferredTimescale: 1), duration: CMTimeMakeWithSeconds(1, preferredTimescale: 1))
    let endingSecond = CMTimeRangeMake(start: CMTimeMakeWithSeconds(assetDuration - 1, preferredTimescale: 1), duration: CMTimeMakeWithSeconds(1, preferredTimescale: 1))
    
    let inputParams = AVMutableAudioMixInputParameters(track: asset.tracks.first! as AVAssetTrack)
    inputParams.setVolumeRamp(fromStartVolume: 0, toEndVolume: 1, timeRange: introRange)
    inputParams.setVolumeRamp(fromStartVolume: 1, toEndVolume: 0, timeRange: endingSecond)
    
    let audioMix = AVMutableAudioMix()
    audioMix.inputParameters = [inputParams]
    playerItem.audioMix = audioMix
    

    Complete function:

    func play(_ url: URL) {
        let asset = AVAsset(url: url)
        let playerItem = AVPlayerItem(asset: asset)
    
        let assetDuration = CMTimeGetSeconds(asset.duration)
    
        let introRange = CMTimeRangeMake(start: CMTimeMakeWithSeconds(0, preferredTimescale: 1), duration: CMTimeMakeWithSeconds(1, preferredTimescale: 1))
        let endingSecond = CMTimeRangeMake(start: CMTimeMakeWithSeconds(assetDuration - 1, preferredTimescale: 1), duration: CMTimeMakeWithSeconds(1, preferredTimescale: 1))
    
        let inputParams = AVMutableAudioMixInputParameters(track: asset.tracks.first! as AVAssetTrack)
        inputParams.setVolumeRamp(fromStartVolume: 0, toEndVolume: 1, timeRange: introRange)
        inputParams.setVolumeRamp(fromStartVolume: 1, toEndVolume: 0, timeRange: endingSecond)
    
        let audioMix = AVMutableAudioMix()
        audioMix.inputParameters = [inputParams]
        playerItem.audioMix = audioMix
    
        player = AVQueuePlayer(playerItem: playerItem)
        playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
        player.play()
    
        let maxDuration = 60.0 // Define max duration
        let maxLoops = floor(maxDuration / assetDuration)
        let lastLoopDuration = maxDuration - (assetDuration * maxLoops)
        let boundaryTime = CMTimeMakeWithSeconds(lastLoopDuration, preferredTimescale: 1)
        let boundaryTimeValue = NSValue(time: boundaryTime)
    
        player.addBoundaryTimeObserver(forTimes: [boundaryTimeValue], queue: DispatchQueue.main) { [weak self] in
            if self?.playerLooper.loopCount == Int(maxLoops) {
                self?.player.pause()
            }
        }
    }