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