633 lines
17 KiB
Objective-C
633 lines
17 KiB
Objective-C
|
|
// AudioController.m
|
|
// Cog
|
|
//
|
|
// Created by Vincent Spader on 8/7/05.
|
|
// Copyright 2005 Vincent Spader. All rights reserved.
|
|
//
|
|
|
|
#import "AudioPlayer.h"
|
|
#import "BufferChain.h"
|
|
#import "Helper.h"
|
|
#import "OutputNode.h"
|
|
#import "PluginController.h"
|
|
#import "Status.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
@implementation AudioPlayer
|
|
|
|
- (id)init {
|
|
self = [super init];
|
|
if(self) {
|
|
output = NULL;
|
|
bufferChain = nil;
|
|
outputLaunched = NO;
|
|
endOfInputReached = NO;
|
|
|
|
chainQueue = [[NSMutableArray alloc] init];
|
|
|
|
semaphore = [[Semaphore alloc] init];
|
|
|
|
atomic_init(&resettingNow, false);
|
|
atomic_init(&refCount, 0);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)setDelegate:(id)d {
|
|
delegate = d;
|
|
}
|
|
|
|
- (id)delegate {
|
|
return delegate;
|
|
}
|
|
|
|
- (void)play:(NSURL *)url {
|
|
[self play:url withUserInfo:nil withRGInfo:nil startPaused:NO andSeekTo:0.0];
|
|
}
|
|
|
|
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
|
|
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:NO andSeekTo:0.0];
|
|
}
|
|
|
|
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused {
|
|
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:paused andSeekTo:0.0];
|
|
}
|
|
|
|
- (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi startPaused:(BOOL)paused andSeekTo:(double)time {
|
|
ALog(@"Opening file for playback: %@ at seek offset %f%@", url, time, (paused) ? @", starting paused" : @"");
|
|
|
|
[self waitUntilCallbacksExit];
|
|
if(output) {
|
|
[output setShouldContinue:NO];
|
|
[output close];
|
|
}
|
|
if(!output) {
|
|
output = [[OutputNode alloc] initWithController:self previous:nil];
|
|
}
|
|
[output setup];
|
|
[output setVolume:volume];
|
|
@synchronized(chainQueue) {
|
|
for(id anObject in chainQueue) {
|
|
[anObject setShouldContinue:NO];
|
|
}
|
|
[chainQueue removeAllObjects];
|
|
endOfInputReached = NO;
|
|
if(bufferChain) {
|
|
[bufferChain setShouldContinue:NO];
|
|
|
|
bufferChain = nil;
|
|
}
|
|
}
|
|
|
|
bufferChain = [[BufferChain alloc] initWithController:self];
|
|
[self notifyStreamChanged:userInfo];
|
|
|
|
while(![bufferChain open:url withUserInfo:userInfo withRGInfo:rgi]) {
|
|
bufferChain = nil;
|
|
|
|
[self requestNextStream:userInfo];
|
|
|
|
url = nextStream;
|
|
if(url == nil) {
|
|
return;
|
|
}
|
|
|
|
userInfo = nextStreamUserInfo;
|
|
rgi = nextStreamRGInfo;
|
|
|
|
[self notifyStreamChanged:userInfo];
|
|
|
|
bufferChain = [[BufferChain alloc] initWithController:self];
|
|
}
|
|
|
|
if(time > 0.0) {
|
|
[output seek:time];
|
|
[bufferChain seek:time];
|
|
}
|
|
|
|
[self setShouldContinue:YES];
|
|
|
|
outputLaunched = NO;
|
|
startedPaused = paused;
|
|
initialBufferFilled = NO;
|
|
previousUserInfo = userInfo;
|
|
|
|
[bufferChain launchThreads];
|
|
|
|
if(paused)
|
|
[self setPlaybackStatus:CogStatusPaused waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)stop {
|
|
// Set shouldoContinue to NO on all things
|
|
[self setShouldContinue:NO];
|
|
[self setPlaybackStatus:CogStatusStopped waitUntilDone:YES];
|
|
|
|
@synchronized(chainQueue) {
|
|
for(id anObject in chainQueue) {
|
|
[anObject setShouldContinue:NO];
|
|
}
|
|
[chainQueue removeAllObjects];
|
|
endOfInputReached = NO;
|
|
if(bufferChain) {
|
|
bufferChain = nil;
|
|
}
|
|
}
|
|
if(output) {
|
|
[output setShouldContinue:NO];
|
|
[output close];
|
|
}
|
|
output = nil;
|
|
}
|
|
|
|
- (void)pause {
|
|
[output pause];
|
|
|
|
[self setPlaybackStatus:CogStatusPaused waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)resume {
|
|
if(startedPaused) {
|
|
startedPaused = NO;
|
|
if(initialBufferFilled)
|
|
[self launchOutputThread];
|
|
}
|
|
|
|
[output resume];
|
|
|
|
[self setPlaybackStatus:CogStatusPlaying waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)seekToTime:(double)time {
|
|
if(endOfInputReached) {
|
|
// This is a dirty hack in case the playback has finished with the track
|
|
// that the user thinks they're seeking into
|
|
CogStatus status = (CogStatus)currentPlaybackStatus;
|
|
NSURL *url;
|
|
id userInfo;
|
|
NSDictionary *rgi;
|
|
|
|
@synchronized(chainQueue) {
|
|
url = [bufferChain streamURL];
|
|
userInfo = [bufferChain userInfo];
|
|
rgi = [bufferChain rgInfo];
|
|
}
|
|
|
|
[self stop];
|
|
|
|
[self play:url withUserInfo:userInfo withRGInfo:rgi startPaused:(status == CogStatusPaused) andSeekTo:time];
|
|
} else {
|
|
// Still decoding the current file, safe to seek within it
|
|
[output seek:time];
|
|
[bufferChain seek:time];
|
|
}
|
|
}
|
|
|
|
- (void)setVolume:(double)v {
|
|
volume = v;
|
|
|
|
[output setVolume:v];
|
|
}
|
|
|
|
- (double)volume {
|
|
return volume;
|
|
}
|
|
|
|
// This is called by the delegate DURING a requestNextStream request.
|
|
- (void)setNextStream:(NSURL *)url {
|
|
[self setNextStream:url withUserInfo:nil withRGInfo:nil];
|
|
}
|
|
|
|
- (void)setNextStream:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi {
|
|
nextStream = url;
|
|
|
|
nextStreamUserInfo = userInfo;
|
|
|
|
nextStreamRGInfo = rgi;
|
|
}
|
|
|
|
// Called when the playlist changed before we actually started playing a requested stream. We will re-request.
|
|
- (void)resetNextStreams {
|
|
[self waitUntilCallbacksExit];
|
|
|
|
@synchronized(chainQueue) {
|
|
for(id anObject in chainQueue) {
|
|
[anObject setShouldContinue:NO];
|
|
}
|
|
[chainQueue removeAllObjects];
|
|
|
|
if(endOfInputReached) {
|
|
[self endOfInputReached:bufferChain];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)restartPlaybackAtCurrentPosition {
|
|
[self sendDelegateMethod:@selector(audioPlayer:restartPlaybackAtCurrentPosition:) withObject:previousUserInfo waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)pushInfo:(NSDictionary *)info toTrack:(id)userInfo {
|
|
[self sendDelegateMethod:@selector(audioPlayer:pushInfo:toTrack:) withObject:info withObject:userInfo waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)reportPlayCountForTrack:(id)userInfo {
|
|
[self sendDelegateMethod:@selector(audioPlayer:reportPlayCountForTrack:) withObject:userInfo waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)setShouldContinue:(BOOL)s {
|
|
shouldContinue = s;
|
|
|
|
if(bufferChain)
|
|
[bufferChain setShouldContinue:s];
|
|
|
|
if(output)
|
|
[output setShouldContinue:s];
|
|
}
|
|
|
|
- (double)amountPlayed {
|
|
return [output amountPlayed];
|
|
}
|
|
|
|
- (double)amountPlayedInterval {
|
|
return [output amountPlayedInterval];
|
|
}
|
|
|
|
- (void)launchOutputThread {
|
|
initialBufferFilled = YES;
|
|
if(outputLaunched == NO && startedPaused == NO) {
|
|
[self setPlaybackStatus:CogStatusPlaying];
|
|
[output launchThread];
|
|
outputLaunched = YES;
|
|
}
|
|
}
|
|
|
|
- (void)requestNextStream:(id)userInfo {
|
|
[self sendDelegateMethod:@selector(audioPlayer:willEndStream:) withObject:userInfo waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)notifyStreamChanged:(id)userInfo {
|
|
[self sendDelegateMethod:@selector(audioPlayer:didBeginStream:) withObject:userInfo waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)notifyPlaybackStopped:(id)userInfo {
|
|
[self sendDelegateMethod:@selector(audioPlayer:didStopNaturally:) withObject:userInfo waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)beginEqualizer:(AudioUnit)eq {
|
|
[self sendDelegateMethod:@selector(audioPlayer:displayEqualizer:) withVoid:eq waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)refreshEqualizer:(AudioUnit)eq {
|
|
[self sendDelegateMethod:@selector(audioPlayer:refreshEqualizer:) withVoid:eq waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)endEqualizer:(AudioUnit)eq {
|
|
[self sendDelegateMethod:@selector(audioPlayer:removeEqualizer:) withVoid:eq waitUntilDone:YES];
|
|
}
|
|
|
|
- (void)addChainToQueue:(BufferChain *)newChain {
|
|
[newChain setShouldContinue:YES];
|
|
[newChain launchThreads];
|
|
|
|
[chainQueue insertObject:newChain atIndex:[chainQueue count]];
|
|
}
|
|
|
|
- (BOOL)endOfInputReached:(BufferChain *)sender // Sender is a BufferChain
|
|
{
|
|
previousUserInfo = [sender userInfo];
|
|
|
|
BufferChain *newChain = nil;
|
|
|
|
if(atomic_load_explicit(&resettingNow, memory_order_relaxed))
|
|
return YES;
|
|
|
|
atomic_fetch_add(&refCount, 1);
|
|
|
|
@synchronized(chainQueue) {
|
|
// No point in constructing new chain for the next playlist entry
|
|
// if there's already one at the head of chainQueue... r-r-right?
|
|
for(BufferChain *chain in chainQueue) {
|
|
if([chain isRunning]) {
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
// We don't want to do this, it may happen with a lot of short files
|
|
// if ([chainQueue count] >= 5)
|
|
//{
|
|
// return YES;
|
|
//}
|
|
}
|
|
|
|
double duration = 0.0;
|
|
|
|
@synchronized(chainQueue) {
|
|
for(BufferChain *chain in chainQueue) {
|
|
duration += [chain secondsBuffered];
|
|
}
|
|
}
|
|
|
|
while(duration >= 30.0 && shouldContinue) {
|
|
[semaphore wait];
|
|
if(atomic_load_explicit(&resettingNow, memory_order_relaxed)) {
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return YES;
|
|
}
|
|
@synchronized(chainQueue) {
|
|
duration = 0.0;
|
|
for(BufferChain *chain in chainQueue) {
|
|
duration += [chain secondsBuffered];
|
|
}
|
|
}
|
|
}
|
|
|
|
nextStreamUserInfo = [sender userInfo];
|
|
|
|
nextStreamRGInfo = [sender rgInfo];
|
|
|
|
// This call can sometimes lead to invoking a chainQueue block on another thread
|
|
[self requestNextStream:nextStreamUserInfo];
|
|
|
|
if(!nextStream) {
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return YES;
|
|
}
|
|
|
|
BufferChain *lastChain;
|
|
|
|
@synchronized(chainQueue) {
|
|
newChain = [[BufferChain alloc] initWithController:self];
|
|
|
|
endOfInputReached = YES;
|
|
|
|
lastChain = [chainQueue lastObject];
|
|
if(lastChain == nil) {
|
|
lastChain = bufferChain;
|
|
}
|
|
}
|
|
|
|
BOOL pathsEqual = NO;
|
|
|
|
if([nextStream isFileURL] && [[lastChain streamURL] isFileURL]) {
|
|
NSString *unixPathNext = [nextStream path];
|
|
NSString *unixPathPrev = [[lastChain streamURL] path];
|
|
|
|
if([unixPathNext isEqualToString:unixPathPrev])
|
|
pathsEqual = YES;
|
|
}
|
|
|
|
if(pathsEqual || ([[nextStream scheme] isEqualToString:[[lastChain streamURL] scheme]] && (([nextStream host] == nil && [[lastChain streamURL] host] == nil) || [[nextStream host] isEqualToString:[[lastChain streamURL] host]]) && [[nextStream path] isEqualToString:[[lastChain streamURL] path]])) {
|
|
if([lastChain setTrack:nextStream] && [newChain openWithInput:[lastChain inputNode] withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
|
|
[newChain setStreamURL:nextStream];
|
|
|
|
@synchronized(chainQueue) {
|
|
[self addChainToQueue:newChain];
|
|
}
|
|
DLog(@"TRACK SET!!! %@", newChain);
|
|
// Keep on-playin
|
|
newChain = nil;
|
|
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
lastChain = nil;
|
|
|
|
while(shouldContinue && ![newChain open:nextStream withUserInfo:nextStreamUserInfo withRGInfo:nextStreamRGInfo]) {
|
|
if(nextStream == nil) {
|
|
newChain = nil;
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return YES;
|
|
}
|
|
|
|
newChain = nil;
|
|
[self requestNextStream:nextStreamUserInfo];
|
|
|
|
newChain = [[BufferChain alloc] initWithController:self];
|
|
}
|
|
|
|
@synchronized(chainQueue) {
|
|
[self addChainToQueue:newChain];
|
|
}
|
|
|
|
newChain = nil;
|
|
|
|
// I'm stupid and can't hold too much stuff in my head all at once, so writing it here.
|
|
//
|
|
// Once we get here:
|
|
// - buffer chain for previous stream finished reading
|
|
// - there are (probably) some bytes of the previous stream in the output buffer which haven't been played
|
|
// (by output node) yet
|
|
// - self.bufferChain == previous playlist entry's buffer chain
|
|
// - self.nextStream == next playlist entry's URL
|
|
// - self.nextStreamUserInfo == next playlist entry
|
|
// - head of chainQueue is the buffer chain for the next entry (which has launched its threads already)
|
|
|
|
atomic_fetch_sub(&refCount, 1);
|
|
return YES;
|
|
}
|
|
|
|
- (void)reportPlayCount {
|
|
[self reportPlayCountForTrack:previousUserInfo];
|
|
}
|
|
|
|
- (BOOL)selectNextBuffer {
|
|
BOOL signalStopped = NO;
|
|
do {
|
|
@synchronized(chainQueue) {
|
|
endOfInputReached = NO;
|
|
|
|
if([chainQueue count] <= 0) {
|
|
// End of playlist
|
|
signalStopped = YES;
|
|
break;
|
|
}
|
|
|
|
bufferChain = nil;
|
|
bufferChain = [chainQueue objectAtIndex:0];
|
|
|
|
[chainQueue removeObjectAtIndex:0];
|
|
DLog(@"New!!! %@ %@", bufferChain, [[bufferChain inputNode] decoder]);
|
|
|
|
[semaphore signal];
|
|
}
|
|
} while(0);
|
|
|
|
if(signalStopped) {
|
|
double latency = 0;
|
|
if(output) latency = [output latency];
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, latency * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
|
[self stop];
|
|
|
|
self->bufferChain = nil;
|
|
|
|
[self notifyPlaybackStopped:nil];
|
|
});
|
|
|
|
return YES;
|
|
}
|
|
|
|
[output setEndOfStream:NO];
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)endOfInputPlayed {
|
|
// Once we get here:
|
|
// - the buffer chain for the next playlist entry (started in endOfInputReached) have been working for some time
|
|
// already, so that there is some decoded and converted data to play
|
|
// - the buffer chain for the next entry is the first item in chainQueue
|
|
previousUserInfo = [bufferChain userInfo];
|
|
[self notifyStreamChanged:previousUserInfo];
|
|
}
|
|
|
|
- (BOOL)chainQueueHasTracks {
|
|
@synchronized(chainQueue) {
|
|
return [chainQueue count] > 0;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)sendDelegateMethod:(SEL)selector withVoid:(void *)obj waitUntilDone:(BOOL)wait {
|
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:selector]];
|
|
[invocation setTarget:delegate];
|
|
[invocation setSelector:selector];
|
|
[invocation setArgument:(void *)&self atIndex:2];
|
|
[invocation setArgument:&obj atIndex:3];
|
|
[invocation retainArguments];
|
|
|
|
[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:wait];
|
|
}
|
|
|
|
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj waitUntilDone:(BOOL)wait {
|
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:selector]];
|
|
[invocation setTarget:delegate];
|
|
[invocation setSelector:selector];
|
|
[invocation setArgument:(void *)&self atIndex:2];
|
|
[invocation setArgument:&obj atIndex:3];
|
|
[invocation retainArguments];
|
|
|
|
[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:wait];
|
|
}
|
|
|
|
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj withObject:(id)obj2 waitUntilDone:(BOOL)wait {
|
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:selector]];
|
|
[invocation setTarget:delegate];
|
|
[invocation setSelector:selector];
|
|
[invocation setArgument:(void *)&self atIndex:2];
|
|
[invocation setArgument:&obj atIndex:3];
|
|
[invocation setArgument:&obj2 atIndex:4];
|
|
[invocation retainArguments];
|
|
|
|
[invocation performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:wait];
|
|
}
|
|
|
|
- (void)setPlaybackStatus:(int)status waitUntilDone:(BOOL)wait {
|
|
currentPlaybackStatus = status;
|
|
|
|
[self sendDelegateMethod:@selector(audioPlayer:didChangeStatus:userInfo:) withObject:@(status) withObject:[bufferChain userInfo] waitUntilDone:wait];
|
|
}
|
|
|
|
- (void)sustainHDCD {
|
|
[self sendDelegateMethod:@selector(audioPlayer:sustainHDCD:) withObject:[bufferChain userInfo] waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)setError:(BOOL)status {
|
|
[self sendDelegateMethod:@selector(audioPlayer:setError:toTrack:) withObject:@(status) withObject:[bufferChain userInfo] waitUntilDone:NO];
|
|
}
|
|
|
|
- (void)setPlaybackStatus:(int)status {
|
|
[self setPlaybackStatus:status waitUntilDone:NO];
|
|
}
|
|
|
|
- (BufferChain *)bufferChain {
|
|
return bufferChain;
|
|
}
|
|
|
|
- (OutputNode *)output {
|
|
return output;
|
|
}
|
|
|
|
+ (NSArray *)containerTypes {
|
|
return [[[PluginController sharedPluginController] containers] allKeys];
|
|
}
|
|
|
|
+ (NSArray *)fileTypes {
|
|
PluginController *pluginController = [PluginController sharedPluginController];
|
|
|
|
NSArray *containerTypes = [[pluginController containers] allKeys];
|
|
NSArray *decoderTypes = [[pluginController decodersByExtension] allKeys];
|
|
NSArray *metdataReaderTypes = [[pluginController metadataReaders] allKeys];
|
|
NSArray *propertiesReaderTypes = [[pluginController propertiesReadersByExtension] allKeys];
|
|
|
|
NSMutableSet *types = [NSMutableSet set];
|
|
|
|
[types addObjectsFromArray:containerTypes];
|
|
[types addObjectsFromArray:decoderTypes];
|
|
[types addObjectsFromArray:metdataReaderTypes];
|
|
[types addObjectsFromArray:propertiesReaderTypes];
|
|
|
|
return [types allObjects];
|
|
}
|
|
|
|
+ (NSArray *)schemes {
|
|
PluginController *pluginController = [PluginController sharedPluginController];
|
|
|
|
return [[pluginController sources] allKeys];
|
|
}
|
|
|
|
- (double)volumeUp:(double)amount {
|
|
BOOL volumeLimit = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"volumeLimit"];
|
|
const double MAX_VOLUME = (volumeLimit) ? 100.0 : 800.0;
|
|
|
|
double newVolume = linearToLogarithmic(logarithmicToLinear(volume + amount, MAX_VOLUME), MAX_VOLUME);
|
|
if(newVolume > MAX_VOLUME)
|
|
newVolume = MAX_VOLUME;
|
|
|
|
[self setVolume:newVolume];
|
|
|
|
// the playbackController needs to know the new volume, so it can update the
|
|
// volumeSlider accordingly.
|
|
return newVolume;
|
|
}
|
|
|
|
- (double)volumeDown:(double)amount {
|
|
BOOL volumeLimit = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] boolForKey:@"volumeLimit"];
|
|
const double MAX_VOLUME = (volumeLimit) ? 100.0 : 800.0;
|
|
|
|
double newVolume;
|
|
if(amount > volume)
|
|
newVolume = 0.0;
|
|
else
|
|
newVolume = linearToLogarithmic(logarithmicToLinear(volume - amount, MAX_VOLUME), MAX_VOLUME);
|
|
|
|
[self setVolume:newVolume];
|
|
return newVolume;
|
|
}
|
|
|
|
- (void)waitUntilCallbacksExit {
|
|
// This sucks! And since the thread that's inside the function can be calling
|
|
// event dispatches, we have to pump the message queue if we're on the main
|
|
// thread. Damn.
|
|
if(atomic_load_explicit(&refCount, memory_order_relaxed) != 0) {
|
|
BOOL mainThread = (dispatch_queue_get_label(dispatch_get_main_queue()) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
|
|
atomic_store(&resettingNow, true);
|
|
while(atomic_load_explicit(&refCount, memory_order_relaxed) != 0) {
|
|
[semaphore signal]; // Gotta poke this periodically
|
|
if(mainThread)
|
|
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]];
|
|
else
|
|
usleep(500);
|
|
}
|
|
atomic_store(&resettingNow, false);
|
|
}
|
|
}
|
|
|
|
@end
|