audioloopcode

Looping through audio files in Swift 2.2 on iOS

While adding background music to Roller Derby Dash, I wanted a fade out in between each audio loop. So instead of playing one song in the background over and over, I had a handful of files that I wanted to play in a queue and fade out in between each one.

Never having worked with audio files before, I searched for examples and found snippets that would do what I wanted, but in Objective-C and nothing in Swift. After a bit of muddling about, I got it to work how I wanted! Hopefully, this sample code would help out anyone else in my predicament.

To start, I added 4 audio files imaginatively named loop1.m4a, loop2.m4a, loop3.m4a and loop4.m4a to my XCode project by right-clicking on the project name on the left and selecting the Add New Files... option.

I had read that I could use an AVQueuePlayer object in the AVFoundation framework to play media sequentially, so that's where I started with.


import AVFoundation //1
class AudioPlayer{
//2
var audioQueue = AVQueuePlayer()
let numberOfMusicAssets = 4
let fadeTime: NSTimeInterval = 5.0
init() {
//3
var avPlayerItems = [AVPlayerItem]()
var i = 0
while i < 3 {
let numberToPlay = arc4random_uniform(UInt32(numberOfMusicAssets)) + 1
let musicFileName = "loop(numberToPlay)"
avPlayerItems.append(self.AVPlayerItemFromName(musicFileName))
i += 1
}
self.audioQueue = AVQueuePlayer(items: avPlayerItems) //4
self.audioQueue.actionAtItemEnd = AVPlayerActionAtItemEnd.Advance //5
}//init()
}//class AudioPlayer

  1. Import the AVFoundation framework into the class
  2. I created an AVQueuePlayer object as a class variable. To keep things simple, I'm going to create a string with the value of loopX where X is the number of one of the music files I'd added at the beginning. If I add another music file, I'll just increase the numberOfMusicAssets value. I also have a fadeTime constant which I'll use to set how long I want the fade out to be for each song.
  3. Within the initializer, I want to first create a queue of 3 AVPlayerItems. For each initial item, I'll randomly get a value of 1 to 4, append it to the "loop" string, and pass it to a helper function which I'll get to further on.
  4. Once all three items have been added to the AVPlayerItems array, I'll add the entire array to the AVQueuePlayer.
  5. With most of the Objective-C code I've read, they specify None for the AVPlayerActionAtItemEnd and then perform the advance to next item in another function. I've noticed a small, but noticeable framerate hit when doing so. This is why I specified the action at the end of each AVPlayerItem to simply play the next song.


//1
func AVPlayerItemFromName(name: String) -> AVPlayerItem {
//2
let path = NSBundle.mainBundle().pathForResource(name, ofType: "m4a")
let avURL = NSURL(fileURLWithPath: path!)
let avAsset = AVAsset(URL: avURL)
//3
let avAssetTrack = avAsset.tracksWithMediaType(AVMediaTypeAudio)[0]
//4
let avParams = AVMutableAudioMixInputParameters(track: avAssetTrack)
avParams.setVolumeRampFromStartVolume(1.0, toEndVolume: 0.0, timeRange: CMTimeRangeMake(avAsset.duration - CMTimeMakeWithSeconds(fadeTime, 1), CMTimeMakeWithSeconds(fadeTime, 1)))
avParams.trackID = avAssetTrack.trackID //5
//6
let fadeOutMix = AVMutableAudioMix()
fadeOutMix.inputParameters = [avParams]
//7
let avItem = AVPlayerItem(asset: avAsset)
//8
avItem.audioMix = fadeOutMix
//9
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerItemDidEnd), name: AVPlayerItemDidPlayToEndTimeNotification, object: avItem)
return avItem
}//AVPlayerItemFromName(String)

  1. This is the heart of the matter. I'll create an AVItem which will have a fade out at the end of it and return it to the calling function. This function goes after the end of the init() function and before the end of the class.
  2. This bit here simply grabs the file named in the passed in parameter from the apps mainBundle root directory. Since I know each file will have an .m4a extension, I hard-coded the value. It could, of course be any supported audio extension.
  3. I extracted the audio track from the asset I'd grabbed in (2). Since each file only has one audio track, I take the first track that's there.
  4. To create the fade out effect, I first need to create an AVMutableAudioMixInputParameters object. This input parameter will basically start with full volume from the end of the audio file (minus the fade out time)  and end with silence at the end of the fade time duration.
  5. Without assigning the trackID of this input parameter, it would not apply to the audio track.
  6. I'll use the AVMutableAudioMixInputParameters from (4) as the single input parameter for an AVMutableAudioMix object.
  7. This will be the AVPlayerItem that is returned to the calling function.
  8. I set the audioMix for the AVPlayerItem to the fade out effect created in (6)
  9. Since I want to keep on adding to the queue whenever a song has finished playing, I added a post notification that will call a third function whenever the end of a song has been reached.


//1
@objc func playerItemDidEnd() {
//2
let numberToPlay = arc4random_uniform(UInt32(self.numberOfMusicAssets)) + 1
let musicFileName = "loop(numberToPlay)"
//3
self.audioQueue.insertItem(self.AVPlayerItemFromName(musicFileName), afterItem: self.audioQueue.items().last)
}//end playerItemDidEnd()

  1. This function acts on the notification that the end of a song has been reached. This goes after the end of the AVPLayerItemFromName  function but before the end of the class.
  2. Similar to the initializer, I generate a random name and retrieve an AVPlayerItem based on it.
  3. The AVPlayerItem is then inserted after the last song in the queue.

And that's it! I also have a couple functions that will play and pause the queue.


func playMusic() {
audioQueue.play()
}
func pauseMusic() {
audioQueue.pause()
}

Leave a Reply