I am creating a simple drum machine. This function controls the time between each sample that is played (thus controlling the tempo of the drum machine). I need to control the tempo with a slider, so I'm hoping to be able to control the 'time duration until next step' value with this if possible. However, when I have tried to do this, it tells me "time is part of NSDate"
-(void)run
{
#autoreleasepool
{
// get current time
NSDate* time = [NSDate date];
// keeping going around the while loop if the sequencer is running
while (self.running)
{
// sleep until the next step is due
[NSThread sleepUntilDate:time];
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// time duration until next step
time = [time dateByAddingTimeInterval:0.5];
}
// exit thread
[NSThread exit];
}
}
This tells me NSTimeInterval is an incompatable type
// time duration until next step
time = [time dateByAddingTimeInterval: self.tempoControls];
Here is where the slider is declared
.m
- (IBAction)sliderMoved:(UISlider *)sender
{
AppDelegate* app = [[UIApplication sharedApplication] delegate];
if (sender == self.tempoSlider)
{
PAEControl* tempoControl = app.tempoControls[app.editIndex];
tempoControl.value = self.tempoSlider.value;
}
}
.h
#interface DetailController : UIViewController
#property (weak, nonatomic) IBOutlet UISlider *tempoSlider;
- (IBAction)sliderMoved:(UISlider *)sender;
Any help would me much appriciated, thanks in advance.
It looks like self.tempoControls is an array of PAEControl objects. The method named dateByAddingTimeInterval: needs an argument of type NSTimeInterval (aka double). It looks like you're trying to pass in this array instead.
Try changing this line -
time = [time dateByAddingTimeInterval: self.tempoControls];
To maybe this -
PAEControl* tempoControl = self.tempoControls[self.editIndex];
time = [time dateByAddingTimeInterval: (NSTimeInterval)tempoControl.value];
On another note, if this is all running on the main thread, be aware that you are blocking it and the UI will become very unresponsive.
Related
Question:
How can I make sure that the code executed due to a runloop event (timer, user interaction, performSelector, etc) have the same concept of "now"?
Background:
Say that event handler takes 100ms to execute, that means that [NSDate date] will return a slightly different "now" depending on when in the execution you make the call. If you are very unlucky with the timing you might even end up with different dates between the calls.
This creates problems for things that rely on the current time for doing various calculations since those calculations can differ during the execution.
Of course, for a specific event handler you could just store the date in the AppDelegate or similar or pass it on in each call starting from the entry point.
However, I want something safer and automatic. Ideally I want to know at what time the current run loop started processing the event. Something I can simply replace [NSDate date] with and always get the same result until the next event is fired.
I looked into the documentation of NSRunLoop without much luck. I also looked into CADisplayLink for potential workarounds. Neither provided a clear cut answer.
It feels like this should be a common thing to need, not something that needs "workarounds". My guess is that I am looking in the wrong places or using the wrong search terms.
Code Example:
UIView *_foo, _fie;
NSDate *_hideDate;
- (void)handleTimer
{
[self checkVisible:_foo];
[self checkVisible:_fie];
}
- (void)checkVisible:(UIView *)view
{
view.hidden = [_hideDate timeIntervalSinceNow] < 0];
}
In this case we could end up with _fie being hidden when _foo is still visible since "now" has changed by a very small amount between calls.
This is a very simplified example in which a fix is trivial by simply calling [NSDate date] and sending that instance to all callers. It is the general case that I am interested in though where call chains might be very deep, cyclic, re-entrant, etc.
NSRunLoop is a wrapper for CFRunLoop. CFRunLoop has features that NSRunLoop doesn't expose, so sometimes you have to drop down to the CF level.
One such feature is observers, which are callbacks you can register to be called when the run loop enters different phases. The phase you want in this case is an after-waiting observer, which is called after the run loop receives an event (from a source, or due to a timer firing, or due to a block being added to the main queue).
Let's add a wakeDate property to NSRunLoop:
// NSRunLoop+wakeDate.h
#import <Foundation/Foundation.h>
#interface NSRunLoop (wakeDate)
#property (nonatomic, strong, readonly) NSDate *wakeDate;
#end
With this category, we can ask an NSRunLoop for its wakeDate property any time we want, for example like this:
#import "AppDelegate.h"
#import "NSRunLoop+wakeDate.h"
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer){
NSLog(#"timer: %.6f", NSRunLoop.currentRunLoop.wakeDate.timeIntervalSinceReferenceDate);
}];
[NSRunLoop.currentRunLoop addTimer:timer forMode:NSRunLoopCommonModes];
return YES;
}
#end
To implement this property, we'll create a WakeDateRecord class that we can attach to the run loop as an associated object:
// NSRunLoop+wakeDate.m
#import "NSRunLoop+wakeDate.h"
#import <objc/runtime.h>
#interface WakeDateRecord: NSObject
#property (nonatomic, strong) NSDate *date;
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop;
#end
static const void *wakeDateRecordKey = &wakeDateRecordKey;
#implementation NSRunLoop (wakeDate)
- (NSDate *)wakeDate {
WakeDateRecord *record = objc_getAssociatedObject(self, wakeDateRecordKey);
if (record == nil) {
record = [[WakeDateRecord alloc] initWithRunLoop:self];
objc_setAssociatedObject(self, wakeDateRecordKey, record, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return record.date;
}
#end
The run loop can run in different modes, and although there are a small number of common modes, new modes can in theory be created on the fly. If you want an observer to be called in a particular mode, you have to register it for that mode. So, to ensure that the reported date is always correct, we'll remember not just the date but also the mode in which we recorded the date:
#implementation WakeDateRecord {
NSRunLoop *_runLoop;
NSRunLoopMode _dateMode;
NSDate *_date;
CFRunLoopObserverRef _observer;
}
To initialize, we just store the run loop and create the observer:
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop {
if (self = [super init]) {
_runLoop = runLoop;
_observer = CFRunLoopObserverCreateWithHandler(nil, kCFRunLoopEntry | kCFRunLoopAfterWaiting, true, -2000000, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self setDate];
});
}
return self;
}
When asked for the date, we first check whether the current mode is different from the date in which we recorded the mode. If so, then the date wasn't updated when the run loop awoke in the current mode. That means the observer wasn't registered for the current mode, so we should register it now and update the date now:
- (NSDate *)date {
NSRunLoopMode mode = _runLoop.currentMode;
if (![_dateMode isEqualToString:mode]) {
// My observer didn't run when the run loop awoke in this mode, so it must not be registered in this mode yet.
NSLog(#"debug: WakeDateRecord registering in mode %#", mode);
CFRunLoopAddObserver(_runLoop.getCFRunLoop, _observer, (__bridge CFRunLoopMode)mode);
[self setDate];
}
return _date;
}
When we update the date, we also need to update the stored mode:
- (void)setDate {
_date = [NSDate date];
_dateMode = _runLoop.currentMode;
}
#end
An important warning about this solution: the observer fires once per pass through the run loop. The run loop can service multiple timers and multiple blocks added to the main queue during a single pass. All of the serviced timers or blocks will see the same wakeDate.
I was wondering if there was a clean way to do a countdown timer with Grand Central Dispatch (then display for example in a UILabel) that's synchronized to the system clock... based on a reference date? — So if my reference date is an NSDate that's 20 minutes from now, I'd have a countdown displayed in seconds (don't worry about formatting) that's synced to the system clock.
Just doing a quick version of this skips seconds every once in a while in case the update method call doesn't arrive on schedule.
To me this seems like a pretty basic question, so I'm looking ideally for a basic/clean solution besides increasing the update interval to be 10x or something.
Also, the solution shouldn't use NSTimer.
I would use a recursive method like this:
- (void) updateTimer{
NSDate *now = [NSDate date];
NSTimeInterval secondsToDate = [now timeIntervalSinceDate:self.referenceDate];
_timerLabel.text = [NSString stringWithFormat:#"%.0f", secondsToDate];
if( secondsToDate < 0 ){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self updateTimer];
});
}else{
NSLog(#"Timer triggered!");
}
}
You just have to call it the first time, then it will update the countdown every 0.1 seconds.
I have created a drum sequencer using Objective C. I want the slider to control the tempo. At the moment, everything works, and the interval between each step is being controlled by:
while (self.running)
{
// sleep until the next step is due
[NSThread sleepUntilDate:time];
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// time duration until next step
time = [time dateByAddingTimeInterval:0.2];
}
So the time between each step is 0.2 seconds. I have tried to implement a tempo slider like so in the view controller .m (the slider has a range of 0.3 to 1.0 so will output similar value to what is currently in time):
- (IBAction)sliderMoved:(UISlider *)sender
{
AppDelegate* app = [[UIApplication sharedApplication] delegate];
app.tempo = sender.value;
}
and by changing the line in the while(self.running) thread to:
time = [time dateByAddingTimeInterval: (NSTimeInterval) _tempo];
However, this causes the time between steps to be far too short (tempo is crazy fast) and when any control in the app is touched, it crashes.
I wonder if I need to set up a function like this, but I'm not sure what would go inside to enable the tempo slider to work:
- (void)setTempo:(float)tempo
{
}
I have tried to be as clear as I can, if anyone can help me I'd be very grateful, thanks in advance
-(void) startDrumTick{
[self.myDrumTimer invalidate]; // stop any current existing timer
// perform the call to the method 'drumMethodOperation:'
// every 0.2 sec. NB: drumMethodOperation will run on main thread.
// this means that if you expect to do long-blocking operation,
// you will need to move that op to an async thread, in order to avoid
// the UI blocking
self.myDrumTimer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:#selector(drumMethodOperation:)
userInfo:nil
repeats:YES];
}
-(void)drumMethodOperation:(id)sender
{
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// any other needed operation to run every 0.2 secs
}
Below an example for an async thread management using GCD
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// Long blocking operation ( DO NOT PERFORM ANY UI OPERATION, like changing a text label, setting an image to an UIImageView, etc. )
[self myLongDbQuery];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Perform you UI Updates here
self.myLabel.text = #"Query done!!!";
});
});
Hope it helps
Luca is right about using GCD. If talk about your initial solution.
Did you set initial value for _tempo? Looks like your bug can be caused by _tempo = 0 initially. As you understand sliderMoved will be called only after some user action so you need to set initial value.
I would like to animate UI elements that are not supported by animateWithDuration:animations: and can't be animated by a transition.
First thing that comes to mind is doing something like this:
_animationStartDate = [NSDate date];
[self performSelector:#selector(manualAnimation) withObject:nil afterDelay:1.0/MYManualAnimationFrameRate];
- (void)manualAnimation
{
NSDate *now = [NSDate date];
NSTimeInterval interval = [now timeIntervalSinceDate:_animationStartDate];
CGFloat progress = interval / MYManualAnimationDuration;
if (progress < 1)
{
[self setValuesOfManualAnimationAtProgress:progress];
[self performSelector:#selector(manualAnimation:) withObject:number afterDelay:1.0/MYManualAnimationFrameRate];
} else {
[self setValuesOfManualAnimationAtProgress:1];
}
}
Before reinventing the wheel, is there any API for these kinds of animations?
If not, anything else I should take into account (e.g., calling cancelPreviousPerformRequestsWithTarget:selector:object: in dealloc, dynamic frame rate)? Would I gain something by using GCD?
Before reinventing the wheel, is there any API for these kinds of animations?
That would be CADisplayLink which "is a timer object that allows your application to synchronize its drawing to the refresh rate of the display"
You use it to call a selector with this signature whenever it's time to redraw
- (void) selector:(CADisplayLink *)sender;
In your drawing code you can use the timestamp of the display link to know how far into the animation to draw.
After setting mPlayer.usesExternalPlaybackWhileExternalScreenIsActive to YES in AVPlayerDemoPlaybackViewController of Apple's AVPlayerDemo sample project, how do you throttle the scrubbing so it doesn't lag behind on the AppleTV?
What I mean is that when you move the slider really fast back and forth the AppleTV performs each and every seekToTime operation, but takes longer to do it then the user takes to slide.
One of the problems with the demo is it uses both the "Touch Drag Inside" and "Value Changed" events which causes it to send the same value twice. If you remove "Value Changed" it improves a bit, but still lags.
I've tried rounding to whole seconds and then only send seekToTime when the second changes, but that doesn't seem to help as much. What I really need to do is send fewer commands the faster the user moves the slider, but more when the user moves slower.
Any ideas on how to accomplish this?
The UISlider already somewhat throttles itself. The faster you move it the fewer values you get from point A to point B. This isn't enough to stop the seek operations from stacking up over AirPlay.
You can, however, use the seekToTime:completionHandler: to prevent the stack up like this:
if(seeking) {
return;
}
seeking = YES;
[player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
seeking = NO;
}];
This drops any new seeks until the one in progress finishes. This seems to work well. You just need to make sure to send one last seek operation after the user stops scrubbing.
While an NSTimer can do the same thing, it's less accurate, and results will vary depending on the latency of the connection. The completionHandler used in this manner ensures that seeks do not stack up, regardless of latency times.
I also found that the "Value Changed" action of the UISlider can happen before any touch start actions. So it's better to use the touch drag inside/outside actions instead, which are guaranteed to happen after a touch start.
Improved Luke answer with some additional code:
static NSTimeInterval ToleranceForAsset(AVAsset *asset) {
NSTimeInterval tolerance = 0.0;
for (AVAssetTrack *track in asset.tracks) {
NSTimeInterval trackTolerance = CMTimeGetSeconds(track.minFrameDuration);
tolerance = MAX(tolerance, trackTolerance);
}
return tolerance;
}
#interface MyPlayerWrapper ()
#property (strong, nonatomic) AVPlayer *player;
#property (assign, nonatomic) NSTimeInterval playerTime;
#property (assign, nonatomic, getter=isSeeking) BOOL seeking;
#property (assign, nonatomic) CGFloat latestSetTime;
#end
#implementation MyPlayerWrapper
- (NSTimeInterval)playerTime {
return CMTimeGetSeconds(self.player.currentItem.currentTime);
}
- (void)setPlayerTime:(NSTimeInterval)playerTime {
NSTimeInterval tolerance = ToleranceForAsset(self.player.currentItem.asset);
if (tolerance) {
// round to nearest seek tolerance (for example 1/30 sec)
playerTime = floor(playerTime / tolerance) * tolerance;
}
self.latestSetTime = playerTime;
if (self.isSeeking) {
return;
}
self.seeking = YES;
[self.player seekToTime:CMTimeMakeWithSeconds(playerTime, self.player.currentItem.duration.timescale) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
self.seeking = NO;
if (ABS(self.player.currentItem.currentTime - latestSetTime) > MAX(tolerance, DBL_EPSILON)) {
self.playerTime = latestSetTime;
}
}];
}
#end