Does AudioKit provide a high precision timer? - audiokit

Does AudioKit provide a high precision timer? I had a look and found about AKSheduledAction, which is based on Timer and seems to be the only default option:
https://github.com/AudioKit/Playgrounds/blob/master/iPad/AudioKit.playgroundbook/Contents/Modules/AudioKit.playgroundmodule/Internals/AKScheduledAction.swift
Alternatively, to the usage of Timer, I wrote a scheduler that runs in the audio thread (* it's not see bellow), correct me if wrong. I've tested and it's not very precise, as the resulting time difference varies:
func schedule(timeOut: Double, onComplete: #escaping () -> Void) -> Void {
do {
let file = try AKAudioFile.silent(samples: Int64(defaultSampleRate * timeOut))
let audioPlayer = try AKAudioPlayer(file: file)
audioPlayer >>> self.mainMixer
audioPlayer.completionHandler = {
onComplete()
}
audioPlayer.play()
} catch {
print(error.localizedDescription)
}
}
Output:
elapsedTime 4.715054468018934
elapsedTime 4.712334129028022
elapsedTime 4.71259418700356
elapsedTime 4.712263747991528
The test was run in the main thread by the following method:
func test() {
let info = ProcessInfo.processInfo
let begin = info.systemUptime
self.schedule(timeOut: 4.666667) {
let elapsedTime = info.systemUptime - begin
print("elapsedTime \(elapsedTime)")
}
}
I'll have to do a bit more research into what the .completionHandler is, but I started by assuming as AVAudioPlayerNode.scheduleBuffer:
AVAudioPlayerNode.scheduleBuffer(AVAudioPCMBuffer, at: nil, options: .interruptsAtLoop, completionHandler: nil)
Looking at the source of completionHandler, which is a AKCallback, we can see it runs in the main thread ( https://github.com/AudioKit/AudioKit/blob/master/AudioKit/Common/Nodes/Playback/Players/AKAudioPlayer.swift#L692 ):
/// Triggered when the player reaches the end of its playing range
fileprivate func internalCompletionHandler() {
DispatchQueue.main.async {
if self.isPlaying {
self.stop()
self.completionHandler?()
}
}
}
So, I'll try to rewrite it to use AVAudioPlayerNode.scheduleBuffer, which hopefully will help.
Ref:
https://developer.apple.com/documentation/foundation/timer
https://developer.apple.com/library/archive/technotes/tn2169/_index.html

I've completed my research and unfortunately didn't or couldn't find much information about a method providing a precise timer.
The best results I found is based on the following technique that holds the best result so far*, the audioFile has a length of 4.666667:
let info = ProcessInfo.processInfo
let begin = info.systemUptime
self.audioBufferPlayerNode.scheduleBuffer(self.file.pcmBuffer, at: nil, options: .interruptsAtLoop, completionHandler: {
let elapsedTime = info.systemUptime - begin
print("elapsedTime \(elapsedTime)")
})
self.audioBufferPlayerNode.play()
The output:
elapsedTime 4.691985241952352
elapsedTime 4.662765832967125
elapsedTime 4.664656096952967
elapsedTime 4.664366245968267
elapsedTime 4.666537321987562
elapsedTime 4.666480115964077
elapsedTime 4.659072204027325
elapsedTime 4.665851002908312
Have in mind that you need to compute the initialisation process for AVAudioPlayerNode and connect it to the mixer ahead of call time, otherwise if you compute all in a single call you get:
elapsedTime 4.701177543029189
elapsedTime 4.7050989009439945
elapsedTime 4.702830816968344
elapsedTime 4.695075194002129
elapsedTime 4.708125164033845
elapsedTime 4.700082497904077
elapsedTime 4.695926539017819
elapsedTime 4.711938992026262
You may want to look into the .prepare method to improve timings (https://developer.apple.com/documentation/avfoundation/avaudioplayernode)
Also, beware that this runs on the thread:
AVAudioPlayerNodeImpl.CompletionHandlerQueue
Hope this helps somebody else in the future!

Related

How to keep two timers in sync

I'm still pretty new to coding and Swift. So bear with me.
Problem Statement : I've got a stopwatch style app that has two concurrent timers start at the same time and display in a mm:ss.SS format, but one is designed to reset to 0 at specific intervals automatically while the other keeps going and tracks total time.
Similar to a "lap" function but it does it automatically. The problem I've encountered is that occasionally the timers aren't perfectly synced up when the user pauses the timers. Since the reset happens at an exact second, both timers should have identical hundredths of a second, while the seconds and minutes will obviously be different. But sometimes the hundredths will be off by .01 or more.
Now, I know Timer isn't designed to be perfectly accurate, and in practice on my app this isn't even a huge deal. My timer doesn't even need to be accurate to the hundredth of a second, and while running it's not noticeably off at all, only while paused. I could display fewer decimal places or none at all, but I prefer the style of showing the hundredths since it fits in well with the stock timer app style.
So if there's a way to make this work, I'd like to keep it.
Screenshot : screenshot
What I tried :
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .new {
//start new timer
startCurrentTimer()
startTotalTimer()
currentStartTime = Date.timeIntervalSinceReferenceDate
totalStartTime = Date.timeIntervalSinceReferenceDate
timerState = .running
//some ui updates
} else if timerState == .running {
//pause timer
totalTimer.invalidate()
currentTimer.invalidate()
timerState = .paused
pausedTime = Date()
//other ui updates
} else if timerState == .paused {
//resume paused timer
let pausedInterval = Date().timeIntervalSince(pausedTime!)
pausedIntervals.append(pausedInterval)
pausedIntervalsCurrent.append(pausedInterval)
pausedTime = nil
startCurrentTimer()
startTotalTimer()
timerState = .running
//other ui updates
}
}
func startTotalTimer() {
totalTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runTotalTimer), userInfo: nil, repeats: true)
}
func startCurrentTimer() {
currentTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runCurrentTimer), userInfo: nil, repeats: true)
}
func resetCurrentTimer() {
currentTimer.invalidate()
currentStartTime = Date.timeIntervalSinceReferenceDate
pausedIntervalsCurrent.removeAll()
startCurrentTimer()
}
#objc func runCurrentTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervalsCurrent.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let currentElapsedTime: TimeInterval = currentTime - currentStartTime - pausedSeconds
currentStepTimeLabel.text = format(time: currentElapsedTime)
if currentElapsedTime >= recipeInterval {
if recipeIndex < recipeTime.count - 1 {
recipeIndex += 1
//ui updates
//reset timer to 0
resetCurrentTimer()
} else {
//last step
currentTimer.invalidate()
}
}
}
#objc func runTotalTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervals.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let totalElapsedTime: TimeInterval = currentTime - totalStartTime - pausedSeconds
totalTimeLabel.text = format(time: totalElapsedTime)
if totalElapsedTime >= recipeTotalTime {
totalTimer.invalidate()
currentTimer.invalidate()
//ui updates
}
}
func format(time: TimeInterval) -> String {
//formats TimeInterval into mm:ss.SS
let formater = DateFormatter()
formater.dateFormat = "mm:ss.SS"
let date = Date(timeIntervalSinceReferenceDate: time)
return formater.string(from: date)
}
You should use a single timer. And when you need a reset to zero, save the current time to a variable.
When presenting the time in the UI, calculate the difference between the running total timer, and the time you saved previously.

UISlider jumps when updating for AVPlayer

I try to implement simple player with UISlider to indicate at what time is current audio file.
In code I have added two observers:
slider.rx.value.subscribe(onNext: { value in
let totalTime = Float(CMTimeGetSeconds(self.player.currentItem!.duration))
let seconds = value * totalTime
let time = CMTime(seconds: Double(seconds), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.player.seek(to: time)
}).disposed(by: bag)
let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
self?.updateSlider(with: time)
}
with one private function:
private func updateSlider(with time: CMTime) {
let currentTime = CMTimeGetSeconds(time)
var totalTime = CMTimeGetSeconds(player.currentItem!.duration)
if totalTime.isNaN {
totalTime = 0
}
startLabel.text = Int(currentTime).descriptiveDuration
endLabel.text = Int(totalTime).descriptiveDuration
slider.value = Float(currentTime / totalTime)
}
When audio plays, everything is fine and slider is pretty much updated. The problem occurs when I try to move slider manually while audio is playing, then it jumps. Why?
UPDATE:
I know why actually. Because I update it twice: manually and from player observer, but how to prevent from this behaviour? I have no idea;) please, help.
One simple way to go about this would be to prevent addPeriodicTimeObserver from calling self?.updateSlider(with: time) when the slider is being touched.
This can be determined via the UISliders isTracking property:
isTracking
A Boolean value indicating whether the control is currently tracking
touch events.
While tracking of a touch event is in progress, the control sets the
value of this property to true. When tracking ends or is cancelled for
any reason, it sets this property to false.
Ref: https://developer.apple.com/documentation/uikit/uicontrol/1618210-istracking
This is present in all UIControl elements which you can use in this way:
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
//check if slider is being touched/tracked
guard self?.slider.isTracking == false else { return }
//if slider is not being touched, then update the slider from here
self?.updateSlider(with: time)
}
Generic Example:
#IBOutlet var slider: UISlider!
//...
func startSlider() {
slider.value = 0
slider.maximumValue = 10
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] (timer) in
print("Slider at: \(self?.slider.value)")
guard self?.slider.isTracking == false else { return }
self?.updateSlider(to: self!.slider.value + 0.1)
}
}
private func updateSlider(to value: Float) {
slider.value = value
}
I'm sure there are other (better) ways out there but I haven't done much in RxSwift (yet).
I hope this is good enough for now.

How to Update player slider handle current value by AVAudioPlayer current time

Now I'm using the MSCircularSlider library as a Cocoapod. Its slider.value is slider.currentValue and when I try to do simple UISlider (try to equal slider.value to player.currentTime), it's working well, but when I try to do it in CircularSlider, the handle is not moving to the AVAudioPlayer.currentTime.
How to solve it?
Please, help me!
let player = AVAudioPlayer()
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! CustomCollectionCell
print("tapped")
guard let url = Bundle.main.url(forResource: songsData[indexPath.row].name, withExtension: "mp3") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.mp3.rawValue)
} catch let error {
print(error.localizedDescription)
}
player.prepareToPlay()
timerLabel.text = String(player.currentTime)
slider.alpha = 1
slider.maximumValue = Float(player.duration)
slider.currentValue = 0.0
Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
player.play()
}
#objc func updateTime(_ timer: Timer) {
let currentTime = player.currentTime
var elapsedTime: TimeInterval = currentTime
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
timerLabel.text = "\(strMinutes):\(strSeconds)"
timer.invalidate()
self.slider.currentValue = Float(self.player.currentTime)
timer.invalidate()
}
Your updateTime(_:) code invalidates your timer. (Twice!) I would expect it to fire once after .01 seconds, then never again, and as a result you probably won't see a visible change in the slider value.
Get rid of the calls to timer.invalidate() except when the sound is finished playing.
I haven't used that particular framework before so I don't know for sure if it works as you're using it but a quick glance at the README file suggests that you are.
On another note, Timer objects are a little crude, and have a resolution of about 0.02 seconds at best, so a timer with an interval of 0.01 seconds isn't likely to fire that often. Also, the refresh rate on iOS device screens is 1/60th of a second, so there's no point in trying to update the screen more often than that.
If you really need smooth drawing that updates on every screen update you should look at using a CADisplayLink timer, but for something as simple as this a timer interval of 1/30 of a second or so should be fine.

How to call method when AVAudioPlayer reaches a certain playback time?

I'm trying to perform an action when the playback reaches a certain time. I can't find any delegate methods or examples of how to do this. How can I call a method when the playback reaches a certain point?
That should help you addBoundaryTimeObserverForTimes:queue:usingBlock:
Requests the invocation of a block when specified times are traversed
during normal playback.
Objective C:
- (id)addBoundaryTimeObserverForTimes:(NSArray<NSValue *> *)times
queue:(dispatch_queue_t)queue
usingBlock:(void (^)(void))block;
Swift:
func addBoundaryTimeObserver(forTimes times: [NSValue],
queue: DispatchQueue?,
using block: #escaping () -> Void) -> Any
Usage:
_ = self.player.addBoundaryTimeObserver(forTimes: times, queue: DispatchQueue.main, using: {
[weak self] time in
// Your code goes here
})
Adding on some more details.
You can addBoundaryTimeObserver(forTimes:queue:using:) for a certain time or use addPeriodicTimeObserver(forInterval:queue:using:) for periodic intervals.
// for specific time
func addTimeObserver() {
var times = [NSValue]()
var currentTime = kCMTimeZero // make your time here
times.append(NSValue(time:currentTime))
// Queue on which to invoke the callback
let mainQueue = DispatchQueue.main
// Add time observer
timeObserverToken =
player.addBoundaryTimeObserver(forTimes: times, queue: mainQueue) {
[weak self] time in
// Update UI
}
}
To fire every half second during normal playback
func addPeriodicTimeObserver() {
// Invoke callback every half second
let interval = CMTime(seconds: 0.5,
preferredTimescale: CMTimeScale(NSEC_PER_SEC))
let mainQueue = DispatchQueue.main
timeObserverToken =
player.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) {
[weak self] time in
// update UI
}}

How to program a delay in Swift 3

In earlier versions of Swift, one could create a delay with the following code:
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 4 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
//put your code which should be executed with a delay here
}
But now, in Swift 3, Xcode automatically changes 6 different things but then the following error appears: "Cannot convert DispatchTime.now to expected value dispatch_time_t aka UInt64."
How can one create a delay before running a sequence of code in Swift 3?
After a lot of research, I finally figured this one out.
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { // Change `2.0` to the desired number of seconds.
// Code you want to be delayed
}
This creates the desired "wait" effect in Swift 3 and Swift 4.
Inspired by a part of this answer.
I like one-line notation for GCD, it's more elegant:
DispatchQueue.main.asyncAfter(deadline: .now() + 42.0) {
// do stuff 42 seconds later
}
Also, in iOS 10 we have new Timer methods, e.g. block initializer:
(so delayed action may be canceled)
let timer = Timer.scheduledTimer(withTimeInterval: 42.0, repeats: false) { (timer) in
// do stuff 42 seconds later
}
Btw, keep in mind: by default, timer is added to the default run loop mode. It means timer may be frozen when the user is interacting with the UI of your app (for example, when scrolling a UIScrollView)
You can solve this issue by adding the timer to the specific run loop mode:
RunLoop.current.add(timer, forMode: .common)
At this blog post you can find more details.
Try the following function implemented in Swift 3.0 and above
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
Usage
delayWithSeconds(1) {
//Do something
}
Try the below code for delay
//MARK: First Way
func delayForWork() {
delay(3.0) {
print("delay for 3.0 second")
}
}
delayForWork()
// MARK: Second Way
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// your code here delayed by 0.5 seconds
}
One way is to use DispatchQueue.main.asyncAfter as a lot of people have answered.
Another way is to use perform(_:with:afterDelay:). More details here
perform(#selector(delayedFunc), with: nil, afterDelay: 3)
#IBAction func delayedFunc() {
// implement code
}
Most common things to use are asyncAfter() and Timer. But if blocking thread is OK, then there is an option:
sleep(3) // in seconds
usleep // in 1/million of second
For asynchronous programming (Swift 5.5) pausing in func looks like this:
func someAsyncFunc() async {
await Task.sleep(2_000_000_000) // Two seconds
// Code to be executed with a delay here
}
//Runs function after x seconds
public static func runThisAfterDelay(seconds: Double, after: #escaping () -> Void) {
runThisAfterDelay(seconds: seconds, queue: DispatchQueue.main, after: after)
}
public static func runThisAfterDelay(seconds: Double, queue: DispatchQueue, after: #escaping () -> Void) {
let time = DispatchTime.now() + Double(Int64(seconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
queue.asyncAfter(deadline: time, execute: after)
}
//Use:-
runThisAfterDelay(seconds: x){
//write your code here
}

Resources