avfoundationschedulingsamplesequencing

Scheduling Audio Sample Playback on iOS


In an iOS app (Swift), how would I schedule audio samples to playback at exact times?

I'd like to make music by scheduling audio samples to play at exact times - a series of "tracks". I have some idea that I should be using AVFoundation, but I find the documentation lacking in serving this particular use-case.

I undersand that AudioKit exists, but I am looking to eventually move my app to the Apple Watch, which is unsupported by AudioKit at this time.


Solution

  • I worked out what I needed to do.

    Make an AVMutableComposition, add a mutable track, and on that track add segments (silence and a view on an sound asset)

    import AVFoundation
    
    let drumUrl = Bundle.main.url(forResource: "bd_909dwsd", withExtension: "wav")!
    
    func makePlayer(bpm: Float, beatCount: Int) -> AVPlayer? {
        let beatDuration = CMTime(seconds: Double(60 / bpm), preferredTimescale: .max)
        
        let composition = AVMutableComposition()
        guard let track = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
            return nil
        }
        
        let drumAsset = AVAsset(url: drumUrl)
        let drumDuration = drumAsset.duration
        let drumTimeRange = CMTimeRange(start: CMTime.zero, duration: drumDuration)
        
        let silenceDuration = beatDuration - drumDuration
        
        var prevBeatEnd = CMTime.zero
        
        for deatIndex in 0 ..< beatCount {
            let drumTargetRange = CMTimeRange(start: prevBeatEnd, duration: drumDuration)
            let drumSegment = AVCompositionTrackSegment(url: drumUrl, trackID: track.trackID, sourceTimeRange: drumTimeRange, targetTimeRange: drumTargetRange)
            track.segments.append(drumSegment)
            
            if deatIndex == 0 {
                prevBeatEnd = prevBeatEnd + drumDuration
            } else {
                let silenceTargetRange = CMTimeRange(start: prevBeatEnd, duration: silenceDuration)
                track.insertEmptyTimeRange(silenceTargetRange)
                prevBeatEnd = prevBeatEnd + silenceDuration + drumDuration
            }
        }
        
        try! track.validateSegments(track.segments)
        
        let playerItem = AVPlayerItem(asset: composition)
        
        return AVPlayer(playerItem: playerItem)
    }