我正在尝试为音频文件建立手动节拍计数器。
它如何工作?
AVPlayer
播放音频文件时,用户在UI中点击一个按钮,该按钮会记录歌曲的拍数。节拍计数以1到8的顺序记录,并构成了一系列节拍[[String:String]] //timeInSeconds:beatCount
在记录了每个节拍的节拍计数和timeInSeconds之后,我将节拍数组作为字符串表示形式保存在数据库中,稍后进行下载,当我再次播放歌曲时,我想在标签中显示每次timeInSeconds
越过func addBoundaryTimeObserver(url: URL)
时,我先前记录的节拍计数。
出了什么问题?
在func addBoundaryTimeObserver(url: URL)
中,当我将arrayOfBeats
的键与strongSelf.timeString
进行比较时,没有匹配项。
如果您查看下面的打印语句,您会发现player?.addBoundaryTimeObserver(forTimes: timesToTransverse, queue: mainQueue)
稍后才被调用,因此即使我们以相同的速率播放相同的音频文件,timesToTransverse
也不匹配。< / p>
// print statement in func createStringOfBeatsToSaveInDB()
finalString is 4.43:1,5.63:2,6.28:3 //seconds:beatCount
//print statement in func addBoundaryTimeObserver(url: URL)
CMTime times recorded are
[CMTime: {4430000000/1000000000 = 4.430},
CMTime: {5630000000/1000000000 = 5.630},
CMTime: {6280000000/1000000000 = 6.280}]
//print statement in func addBoundaryTimeObserver(url: URL)
//This is NOT the expected result
timeinSeconds double is 6.67019307613373
timeString format: %.2f is 6.67
timeinSeconds double is 7.86572802066803
timeString format: %.2f is 7.87
timeinSeconds double is 8.51492607593536
timeString format: %.2f is 8.51
var beatCount = 0
var arrayOfBeats = [String: Int]() //time: beatCount
var timeInSeconds: Double = 0
var startTime: Double = 0
var timeString: String = ""
var player: AVPlayer?
var timeObserverToken: Any?
var temporaryArrOfbeats = [[String:String]]()
var finalStringOfBeats = ""
@IBAction func playStopSong(_ sender: Any) {
//if player was stopped
if isFinished == true {
player?.replaceCurrentItem(with: nil)
removePeriodicTimeObserver()
createStringOfBeatsToSaveInDB()
}else {
let url = URL(string: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")!
self.addPeriodicTimeObserver(url: url)
}
//toggle the isFinished boolean value to decide if player stopped
isFinished = !isFinished
}//end playStopSong
func addPeriodicTimeObserver(url: URL) {
let playerItem = AVPlayerItem(url: url)
player = AVPlayer(playerItem: playerItem)
player?.play()
startTime = Date().timeIntervalSinceReferenceDate
let durationInSeconds = CMTimeGetSeconds(playerItem.asset.duration)
let durationInMinutes = durationInSeconds / 60
songTotalTime.text = String(format: "%.2f", durationInMinutes)
// Invoke callback every 0.05 seconds
let interval = CMTime(seconds: 0.05,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
timeObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { [weak self] timeNow in
guard let strongSelf = self else {return}
let duration = CMTimeGetSeconds(playerItem.duration)
strongSelf.progressView.progress = Float((CMTimeGetSeconds(timeNow) / duration))
//Total time since timer started, in seconds
strongSelf.timeInSeconds = Date().timeIntervalSinceReferenceDate - strongSelf.startTime
//Convert the time to a string with 2 decimal places
let timeString = String(format: "%.2f", strongSelf.timeInSeconds)
strongSelf.timeString = timeString
//Display the time string to a label in our view controller
strongSelf.songTimeLeft.text = timeString
// expression evaluates to true if player stopped. Now we can compare timeString with keys in arrayOfBeats (var arrayOfBeats:[String: Int]() //timeInSeconds: beatCount)
if strongSelf.isPlaySongWithRecordedBeatEnabled == true {
strongSelf.comparePeriodicTimeObserver(timeString: timeString)
}
}
}//end addPeriodicTimeObserver
func createStringOfBeatsToSaveInDB() {
let newArrMapped: [(key: String, value: String)] = temporaryArrOfbeats.flatMap {$0}
let totalCount = newArrMapped.count
var count = 0
newArrMapped.forEach { (timeString, beatCount) in
count += 1
let newString = "\(timeString):\(beatCount)"
if count == totalCount {
finalStringOfBeats.append(newString)
}else {
finalStringOfBeats.append(newString)
finalStringOfBeats.append(",")
}
}
print("finalString is \(finalStringOfBeats)")
}//end createStringOfBeatsToSaveInDB
@IBAction func tapBeat(_ sender: Any) {
addBeatToArray()
}
func addBeatToArray() {
if beatCount == 8 {beatCount = 0}
if beatCount == 0 || beatCount <= 7 {beatCount += 1}
showBeatCount.text = "\(beatCount)"
arrayOfBeats[timeString] = beatCount
let newPair = [timeString:String(beatCount)]
temporaryArrOfbeats.append(newPair)
// print("time:beatCount \(timeString): \(beatCount) ")
}
//after user stops the song we can play the same song again, but this time we will compare the keys of `arrayOfBeats` against the current playing time
@IBAction func playSongWithRecordedBeat(_ sender: Any) {
if arrayOfBeats.count != 0 {
beatCount = 0
timeInSeconds = 0
timeString = ""
isPlaySongWithRecordedBeatEnabled = true
let testURL = URL(string: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")!
addBoundaryTimeObserver(url: testURL)
}
}
func addBoundaryTimeObserver(url: URL) {
let playerItem = AVPlayerItem(url: url)
player = AVPlayer(playerItem: playerItem)
var timesToTransverse = [NSValue]()
// Build boundary times from arrayOfBeats keys
let keys = arrayOfBeats.keys.compactMap {$0}
//get the times for every single time Tap Beat button was touched and assign them to timesToTransverse
keys.forEach { (key) in
let second: Double = Double("\(key)")!
let cmtime = CMTime(seconds: second, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
let cmtimeValue = NSValue(time: cmtime)
timesToTransverse.append(cmtimeValue)
}
print("CMTime times recorded are \(times)")
startTime = Date().timeIntervalSinceReferenceDate
player?.play()
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
timeObserverToken =
player?.addBoundaryTimeObserver(forTimes: timesToTransverse, queue: mainQueue) {
[weak self] in
guard let strongSelf = self else {return}
//time that passed since song started to play
strongSelf.timeInSeconds = Date().timeIntervalSinceReferenceDate - strongSelf.startTime
let timeString = String(format: "%.2f", strongSelf.timeInSeconds)
strongSelf.timeString = timeString
print("timeinSeconds double is \(strongSelf.timeInSeconds)")
print("timeString format: %.2f is \(timeString)")
strongSelf.comparePeriodicTimeObserver(timeString: timeString)
}
}//end addBoundaryTimeObserver
//after admin has finished tapping the beat count in UI, he can tap on playSongWithRecordedBeat button and this method will display the beatCount recorded
func comparePeriodicTimeObserver(timeString: String) {
//var arrayOfBeats:[String: Int]() //timeInSeconds: beatCount
let keys = arrayOfBeats.keys.compactMap {$0}
if keys.contains(timeString) {
print("timeString is \(timeString)")
let _ = arrayOfBeats.filter({ (key, beatCount) -> Bool in
if key == timeString {
showBeatCount.text = "\(beatCount)"
print("key, val found \(key): \(beatCount)")
return true
}
return false
})
}
}