2

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

  • AVAudioPlayer (where I can define a repeat count but can't define an end-time)
  • AVPlayer (where I can define a forwardPlaybackEndTime bot not a loop count)
  • AVPlayerLooper (that I don't yet fully understand)

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.

Jayesh Thanki
  • 2,037
  • 2
  • 23
  • 32
Georg
  • 3,664
  • 3
  • 34
  • 75
  • Hello! Have you tried with a `Timer`? – IMACODE Oct 19 '18 at 09:36
  • As my app should also be working when backgrounded I guess Timer would not be an ideal solution?!? I am wondering if there is something more elegant... – Georg Oct 19 '18 at 09:37
  • Yes, maybe not the good idea for that case. In the `AVAudioPlayer` you have an `numberOfLoops` attributes. So if your mp3 has a duration of 8 seconds, you should run 60 / 8 = 7.5 times the sound to play it 1 minute. – IMACODE Oct 19 '18 at 09:42
  • 1
    Have you look into the CocoaPods? I think this Pod can be helpful https://cocoapods.org/pods/AudioPlayerSwift, you have an `numberOfLoops` attribute and an `currentTime` attribute and also two methods (`fadeIn` and `fadeOut`). It seems to be the perfect Pod for your case. – IMACODE Oct 19 '18 at 09:46

1 Answers1

4

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()
        }
    }
}
Alejandro Cotilla
  • 2,501
  • 1
  • 20
  • 35