468 lines
17 KiB
Objective-C
468 lines
17 KiB
Objective-C
//
|
|
// MIKMIDIClock.m
|
|
// MIKMIDI
|
|
//
|
|
// Created by Chris Flesner on 11/26/14.
|
|
// Copyright (c) 2014 Mixed In Key. All rights reserved.
|
|
//
|
|
|
|
#import "MIKMIDIClock.h"
|
|
#import "MIKMIDIUtilities.h"
|
|
#import <mach/mach_time.h>
|
|
|
|
#if !__has_feature(objc_arc)
|
|
#error MIKMIDIClock.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDIClock.m in the Build Phases for this target
|
|
#endif
|
|
|
|
|
|
#define kDurationToKeepHistoricalClocks 1.0
|
|
|
|
|
|
#pragma mark -
|
|
@interface MIKMIDISyncedClockProxy : NSProxy
|
|
+ (instancetype)syncedClockWithClock:(MIKMIDIClock *)masterClock;
|
|
@property (readonly, nonatomic) MIKMIDIClock *masterClock;
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
@interface MIKMIDIClock ()
|
|
{
|
|
Float64 _currentTempo;
|
|
MIDITimeStamp _timeStampZero;
|
|
MIDITimeStamp _lastSyncedMIDITimeStamp;
|
|
MusicTimeStamp _lastSyncedMusicTimeStamp;
|
|
|
|
Float64 _musicTimeStampsPerMIDITimeStamp;
|
|
Float64 _midiTimeStampsPerMusicTimeStamp;
|
|
|
|
CFMutableDictionaryRef _historicalClocks;
|
|
CFMutableSetRef _historicalClockMIDITimeStampsSet;
|
|
CFMutableArrayRef _historicalClockMIDITimeStampsArray;
|
|
|
|
dispatch_queue_t _clockQueue;
|
|
}
|
|
|
|
@property (nonatomic, getter=isReady) BOOL ready;
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
@implementation MIKMIDIClock
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
+ (instancetype)clock
|
|
{
|
|
return [[self alloc] init];
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
return [self initWithQueue:YES];
|
|
}
|
|
|
|
- (instancetype)initWithQueue:(BOOL)createQueue
|
|
{
|
|
if (self = [super init]) {
|
|
if (createQueue) {
|
|
NSString *queueLabel = [[[NSBundle mainBundle] bundleIdentifier] stringByAppendingFormat:@".%@.%p", [self class], self];
|
|
dispatch_queue_attr_t attr = DISPATCH_QUEUE_SERIAL;
|
|
|
|
#if defined (__MAC_10_10) || defined (__IPHONE_8_0)
|
|
if (@available(macOS 10.10, iOS 8, *)) {
|
|
if (&dispatch_queue_attr_make_with_qos_class != NULL) {
|
|
attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
_clockQueue = dispatch_queue_create(queueLabel.UTF8String, attr);
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
releaseHistoricalClocks(self);
|
|
}
|
|
|
|
#pragma mark - Queue
|
|
|
|
static void dispatchToClockQueue(MIKMIDIClock *self, void(^block)(void))
|
|
{
|
|
if (!block) return;
|
|
|
|
dispatch_queue_t queue = self->_clockQueue;
|
|
if (queue) {
|
|
dispatch_sync(queue, block);
|
|
} else {
|
|
block();
|
|
}
|
|
}
|
|
|
|
#pragma mark - Time Stamps
|
|
|
|
- (void)syncMusicTimeStamp:(MusicTimeStamp)musicTimeStamp withMIDITimeStamp:(MIDITimeStamp)midiTimeStamp tempo:(Float64)tempo
|
|
{
|
|
[self willChangeValueForKey:@"ready"];
|
|
dispatchToClockQueue(self, ^{
|
|
if (self->_lastSyncedMIDITimeStamp != 0) {
|
|
// Add a clock to the historical clocks
|
|
NSNumber *midiTimeStampNumber = @(midiTimeStamp);
|
|
|
|
if (!self->_historicalClocks) {
|
|
self->_historicalClocks = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
|
self->_historicalClockMIDITimeStampsSet = CFSetCreateMutable(NULL, 0, &kCFTypeSetCallBacks);
|
|
self->_historicalClockMIDITimeStampsArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
} else {
|
|
// Remove clocks old enough to not be needed anymore
|
|
MIDITimeStamp oldTimeStamp = MIKMIDIGetCurrentTimeStamp() - MIKMIDIClockMIDITimeStampsPerTimeInterval(kDurationToKeepHistoricalClocks);
|
|
|
|
CFIndex count = CFArrayGetCount(self->_historicalClockMIDITimeStampsArray);
|
|
CFMutableArrayRef timeStampsToRemove = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
CFMutableSetRef indexesToRemoveSet = CFSetCreateMutable(NULL, 0, &kCFTypeSetCallBacks);
|
|
CFMutableArrayRef indexesToRemoveArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
|
|
for (CFIndex i = 0; i < count; i++) {
|
|
NSNumber *timeStampNumber = (__bridge NSNumber *)CFArrayGetValueAtIndex(self->_historicalClockMIDITimeStampsArray, i);
|
|
MIDITimeStamp timeStamp = timeStampNumber.unsignedLongLongValue;
|
|
if (timeStamp <= oldTimeStamp) {
|
|
void *timeStampValue = (__bridge void *)timeStampNumber;
|
|
|
|
CFArrayAppendValue(timeStampsToRemove, timeStampValue);
|
|
if (!CFSetContainsValue(indexesToRemoveSet, timeStampValue)) {
|
|
CFSetAddValue(indexesToRemoveSet, timeStampValue);
|
|
CFArrayAppendValue(indexesToRemoveArray, timeStampValue);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
CFIndex timeStampsToRemoveCount = CFArrayGetCount(timeStampsToRemove);
|
|
for (CFIndex i = (timeStampsToRemoveCount - 1); i >= 0; i--) {
|
|
const void *timeStampValue = CFArrayGetValueAtIndex(timeStampsToRemove, i);
|
|
CFDictionaryRemoveValue(self->_historicalClocks, timeStampValue);
|
|
CFSetRemoveValue(self->_historicalClockMIDITimeStampsSet, timeStampValue);
|
|
CFArrayRemoveValueAtIndex(self->_historicalClockMIDITimeStampsArray, i);
|
|
}
|
|
|
|
CFRelease(timeStampsToRemove);
|
|
CFRelease(indexesToRemoveSet);
|
|
CFRelease(indexesToRemoveArray);
|
|
}
|
|
|
|
// Add clock to history
|
|
MIKMIDIClock *historicalClock = [[MIKMIDIClock alloc] initWithQueue:NO];
|
|
historicalClock->_currentTempo = self->_currentTempo;
|
|
historicalClock->_timeStampZero = self->_timeStampZero;
|
|
historicalClock->_lastSyncedMIDITimeStamp = self->_lastSyncedMIDITimeStamp;
|
|
historicalClock->_lastSyncedMusicTimeStamp = self->_lastSyncedMusicTimeStamp;
|
|
historicalClock->_musicTimeStampsPerMIDITimeStamp = self->_musicTimeStampsPerMIDITimeStamp;
|
|
historicalClock->_midiTimeStampsPerMusicTimeStamp = self->_midiTimeStampsPerMusicTimeStamp;
|
|
|
|
void *midiTimeStampValue = (__bridge void *)midiTimeStampNumber;
|
|
CFDictionaryAddValue(self->_historicalClocks, midiTimeStampValue, (__bridge void *)historicalClock);
|
|
|
|
if (!CFSetContainsValue(self->_historicalClockMIDITimeStampsSet, midiTimeStampValue)) {
|
|
CFSetAddValue(self->_historicalClockMIDITimeStampsSet, midiTimeStampValue);
|
|
CFArrayAppendValue(self->_historicalClockMIDITimeStampsArray, midiTimeStampValue);
|
|
}
|
|
}
|
|
|
|
// Update new tempo and timing information
|
|
Float64 secondsPerMIDITimeStamp = MIKMIDIClockSecondsPerMIDITimeStamp();
|
|
Float64 secondsPerMusicTimeStamp = 60.0 / tempo;
|
|
Float64 midiTimeStampsPerMusicTimeStamp = secondsPerMusicTimeStamp / secondsPerMIDITimeStamp;
|
|
|
|
self->_currentTempo = tempo;
|
|
self->_lastSyncedMIDITimeStamp = midiTimeStamp;
|
|
self->_lastSyncedMusicTimeStamp = musicTimeStamp;
|
|
self->_timeStampZero = midiTimeStamp - (musicTimeStamp * midiTimeStampsPerMusicTimeStamp);
|
|
self->_midiTimeStampsPerMusicTimeStamp = midiTimeStampsPerMusicTimeStamp;
|
|
self->_musicTimeStampsPerMIDITimeStamp = secondsPerMIDITimeStamp / secondsPerMusicTimeStamp;
|
|
self->_ready = YES;
|
|
});
|
|
[self didChangeValueForKey:@"ready"];
|
|
}
|
|
|
|
- (void)unsyncMusicTimeStampsAndTemposFromMIDITimeStamps
|
|
{
|
|
[self willChangeValueForKey:@"ready"];
|
|
dispatchToClockQueue(self, ^{
|
|
self->_ready = NO;
|
|
self->_currentTempo = 0;
|
|
self->_lastSyncedMIDITimeStamp = 0;
|
|
releaseHistoricalClocks(self);
|
|
});
|
|
[self didChangeValueForKey:@"ready"];
|
|
}
|
|
|
|
static MusicTimeStamp musicTimeStampForMIDITimeStamp(MIKMIDIClock *self, MIDITimeStamp midiTimeStamp)
|
|
{
|
|
__block MusicTimeStamp musicTimeStamp = 0;
|
|
|
|
dispatchToClockQueue(self, ^{
|
|
if (!self->_ready) return;
|
|
|
|
MIDITimeStamp lastSyncedMIDITimeStamp = self->_lastSyncedMIDITimeStamp;
|
|
if (midiTimeStamp >= lastSyncedMIDITimeStamp) {
|
|
musicTimeStamp = musicTimeStampForMIDITimeStampWithHistoricalClock(midiTimeStamp, self);
|
|
} else {
|
|
musicTimeStamp = musicTimeStampForMIDITimeStampWithHistoricalClock(midiTimeStamp, clockForMIDITimeStamp(self, midiTimeStamp));
|
|
}
|
|
});
|
|
|
|
return musicTimeStamp;
|
|
}
|
|
|
|
- (MusicTimeStamp)musicTimeStampForMIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
return musicTimeStampForMIDITimeStamp(self, midiTimeStamp);
|
|
}
|
|
|
|
static MusicTimeStamp musicTimeStampForMIDITimeStampWithHistoricalClock(MIDITimeStamp midiTimeStamp, MIKMIDIClock *clock)
|
|
{
|
|
if (midiTimeStamp == clock->_lastSyncedMIDITimeStamp) return clock->_lastSyncedMusicTimeStamp;
|
|
MIDITimeStamp timeStampZero = clock->_timeStampZero;
|
|
return (midiTimeStamp >= timeStampZero) ? ((midiTimeStamp - timeStampZero) * clock->_musicTimeStampsPerMIDITimeStamp) : -((timeStampZero - midiTimeStamp) * clock->_musicTimeStampsPerMIDITimeStamp);
|
|
}
|
|
|
|
static MIDITimeStamp midiTimeStampForMusicTimeStamp(MIKMIDIClock *self, MusicTimeStamp musicTimeStamp)
|
|
{
|
|
__block MIDITimeStamp midiTimeStamp = 0;
|
|
|
|
dispatchToClockQueue(self, ^{
|
|
if (!self->_ready) return;
|
|
if (musicTimeStamp == self->_lastSyncedMusicTimeStamp) { midiTimeStamp = self->_lastSyncedMIDITimeStamp; return; }
|
|
|
|
midiTimeStamp = round(musicTimeStamp * self->_midiTimeStampsPerMusicTimeStamp) + self->_timeStampZero;
|
|
|
|
if (midiTimeStamp < self->_lastSyncedMIDITimeStamp && self->_historicalClockMIDITimeStampsArray) {
|
|
CFIndex historicalClockMIDITimeStampsCount = CFArrayGetCount(self->_historicalClockMIDITimeStampsArray);
|
|
for (CFIndex i = (historicalClockMIDITimeStampsCount - 1); i >= 0; i--) {
|
|
const void *midiTimeStampValue = CFArrayGetValueAtIndex(self->_historicalClockMIDITimeStampsArray, i);
|
|
|
|
MIKMIDIClock *clock = (__bridge MIKMIDIClock *)CFDictionaryGetValue(self->_historicalClocks, midiTimeStampValue);
|
|
MIDITimeStamp historicalMIDITimeStamp = round(musicTimeStamp * clock->_midiTimeStampsPerMusicTimeStamp) + clock->_timeStampZero;
|
|
if (historicalMIDITimeStamp >= clock->_lastSyncedMIDITimeStamp) {
|
|
midiTimeStamp = historicalMIDITimeStamp;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return midiTimeStamp;
|
|
}
|
|
|
|
- (MIDITimeStamp)midiTimeStampForMusicTimeStamp:(MusicTimeStamp)musicTimeStamp
|
|
{
|
|
return midiTimeStampForMusicTimeStamp(self, musicTimeStamp);
|
|
}
|
|
|
|
static MIDITimeStamp midiTimeStampsPerMusicTimeStamp(MIKMIDIClock *self, MusicTimeStamp musicTimeStamp)
|
|
{
|
|
__block MIDITimeStamp midiTimeStamps = 0;
|
|
|
|
dispatchToClockQueue(self, ^{
|
|
if (self->_ready) midiTimeStamps = musicTimeStamp * self->_midiTimeStampsPerMusicTimeStamp;
|
|
});
|
|
|
|
return midiTimeStamps;
|
|
}
|
|
|
|
- (MIDITimeStamp)midiTimeStampsPerMusicTimeStamp:(MusicTimeStamp)musicTimeStamp
|
|
{
|
|
return midiTimeStampsPerMusicTimeStamp(self, musicTimeStamp);
|
|
}
|
|
|
|
#pragma mark - Tempo
|
|
|
|
static Float64 tempoAtMIDITimeStamp(MIKMIDIClock *self, MIDITimeStamp midiTimeStamp)
|
|
{
|
|
__block Float64 tempo = 0;
|
|
|
|
dispatchToClockQueue(self, ^{
|
|
if (self->_ready) {
|
|
if (midiTimeStamp >= self->_lastSyncedMIDITimeStamp) {
|
|
tempo = self->_currentTempo;
|
|
} else {
|
|
tempo = [clockForMIDITimeStamp(self, midiTimeStamp) currentTempo];
|
|
}
|
|
}
|
|
});
|
|
|
|
return tempo;
|
|
}
|
|
|
|
- (Float64)tempoAtMIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
return tempoAtMIDITimeStamp(self, midiTimeStamp);
|
|
}
|
|
|
|
Float64 tempoAtMusicTimeStamp(MIKMIDIClock *self, MusicTimeStamp musicTimeStamp)
|
|
{
|
|
return tempoAtMIDITimeStamp(self, midiTimeStampForMusicTimeStamp(self, musicTimeStamp));
|
|
}
|
|
|
|
- (Float64)tempoAtMusicTimeStamp:(MusicTimeStamp)musicTimeStamp
|
|
{
|
|
return tempoAtMusicTimeStamp(self, musicTimeStamp);
|
|
}
|
|
|
|
#pragma mark - Historical Clocks
|
|
|
|
static MIKMIDIClock *clockForMIDITimeStamp(MIKMIDIClock *self, MIDITimeStamp midiTimeStamp)
|
|
{
|
|
MIKMIDIClock *clock = self;
|
|
|
|
if (self->_historicalClockMIDITimeStampsArray) {
|
|
CFIndex count = CFArrayGetCount(self->_historicalClockMIDITimeStampsArray);
|
|
for (CFIndex i = (count - 1); i >= 0; i--) {
|
|
NSNumber *historicalClockTimeStamp = (__bridge NSNumber *)CFArrayGetValueAtIndex(self->_historicalClockMIDITimeStampsArray, i);
|
|
if ([historicalClockTimeStamp unsignedLongLongValue] > midiTimeStamp) {
|
|
clock = (__bridge MIKMIDIClock *)CFDictionaryGetValue(self->_historicalClocks, (__bridge void *)historicalClockTimeStamp);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return clock;
|
|
}
|
|
|
|
static void releaseHistoricalClocks(MIKMIDIClock *self)
|
|
{
|
|
if (self->_historicalClocks) {
|
|
CFRelease(self->_historicalClocks);
|
|
self->_historicalClocks = NULL;
|
|
}
|
|
if (self->_historicalClockMIDITimeStampsSet) {
|
|
CFRelease(self->_historicalClockMIDITimeStampsSet);
|
|
self->_historicalClockMIDITimeStampsSet = NULL;
|
|
}
|
|
if (self->_historicalClockMIDITimeStampsArray) {
|
|
CFRelease(self->_historicalClockMIDITimeStampsArray);
|
|
self->_historicalClockMIDITimeStampsArray = NULL;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Synced Clock
|
|
|
|
- (MIKMIDIClock *)syncedClock
|
|
{
|
|
return (MIKMIDIClock *)[MIKMIDISyncedClockProxy syncedClockWithClock:self];
|
|
}
|
|
|
|
#pragma mark - Functions
|
|
|
|
Float64 MIKMIDIClockSecondsPerMIDITimeStamp()
|
|
{
|
|
static Float64 secondsPerMIDITimeStamp;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
mach_timebase_info_data_t timeBaseInfoData;
|
|
mach_timebase_info(&timeBaseInfoData);
|
|
secondsPerMIDITimeStamp = ((Float64)timeBaseInfoData.numer / (Float64)timeBaseInfoData.denom) / 1.0e9;
|
|
});
|
|
return secondsPerMIDITimeStamp;
|
|
|
|
}
|
|
|
|
Float64 MIKMIDIClockMIDITimeStampsPerTimeInterval(NSTimeInterval timeInterval)
|
|
{
|
|
static Float64 midiTimeStampsPerSecond = 0;
|
|
if (!midiTimeStampsPerSecond) midiTimeStampsPerSecond = (1.0 / MIKMIDIClockSecondsPerMIDITimeStamp());
|
|
return midiTimeStampsPerSecond * timeInterval;
|
|
}
|
|
|
|
#pragma mark - Deprecated Methods
|
|
|
|
- (void)setMusicTimeStamp:(MusicTimeStamp)musicTimeStamp withTempo:(Float64)tempo atMIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
[self syncMusicTimeStamp:musicTimeStamp withMIDITimeStamp:midiTimeStamp tempo:tempo];
|
|
}
|
|
|
|
+ (Float64)secondsPerMIDITimeStamp
|
|
{
|
|
return MIKMIDIClockSecondsPerMIDITimeStamp();
|
|
}
|
|
|
|
+ (Float64)midiTimeStampsPerTimeInterval:(NSTimeInterval)timeInterval
|
|
{
|
|
return MIKMIDIClockMIDITimeStampsPerTimeInterval(timeInterval);
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
@implementation MIKMIDISyncedClockProxy
|
|
|
|
+ (instancetype)syncedClockWithClock:(MIKMIDIClock *)masterClock
|
|
{
|
|
MIKMIDISyncedClockProxy *proxy = [self alloc];
|
|
proxy->_masterClock = masterClock;
|
|
return proxy;
|
|
}
|
|
|
|
- (void)forwardInvocation:(NSInvocation *)invocation
|
|
{
|
|
SEL selector = invocation.selector;
|
|
|
|
// Optimizations
|
|
if (selector == @selector(midiTimeStampForMusicTimeStamp:)) {
|
|
MusicTimeStamp musicTimeStamp;
|
|
[invocation getArgument:&musicTimeStamp atIndex:2];
|
|
|
|
MIDITimeStamp midiTimeStamp = midiTimeStampForMusicTimeStamp(_masterClock, musicTimeStamp);
|
|
return [invocation setReturnValue:&midiTimeStamp];
|
|
} else if (selector == @selector(musicTimeStampForMIDITimeStamp:)) {
|
|
MIDITimeStamp midiTimeStamp;
|
|
[invocation getArgument:&midiTimeStamp atIndex:2];
|
|
|
|
MusicTimeStamp musicTimeStamp = musicTimeStampForMIDITimeStamp(_masterClock, midiTimeStamp);
|
|
return [invocation setReturnValue:&musicTimeStamp];
|
|
} else if (selector == @selector(tempoAtMIDITimeStamp:)) {
|
|
MIDITimeStamp midiTimeStamp;
|
|
[invocation getArgument:&midiTimeStamp atIndex:2];
|
|
|
|
Float64 tempo = tempoAtMIDITimeStamp(_masterClock, midiTimeStamp);
|
|
return [invocation setReturnValue:&tempo];
|
|
} else if (selector == @selector(tempoAtMusicTimeStamp:)) {
|
|
MusicTimeStamp musicTimeStamp;
|
|
[invocation getArgument:&musicTimeStamp atIndex:2];
|
|
|
|
Float64 tempo = tempoAtMusicTimeStamp(_masterClock, musicTimeStamp);
|
|
return [invocation setReturnValue:&tempo];
|
|
} else if (selector == @selector(midiTimeStampsPerMusicTimeStamp:)) {
|
|
MusicTimeStamp musicTimeStamp;
|
|
[invocation getArgument:&musicTimeStamp atIndex:2];
|
|
|
|
MIDITimeStamp midiTimeStamps = midiTimeStampsPerMusicTimeStamp(_masterClock, musicTimeStamp);
|
|
return [invocation setReturnValue:&midiTimeStamps];
|
|
} else if (selector == @selector(syncedClock)) {
|
|
MIKMIDISyncedClockProxy *syncedClock = self;
|
|
return [invocation setReturnValue:&syncedClock];
|
|
}
|
|
|
|
// Ignored selectors
|
|
if (selector == @selector(syncMusicTimeStamp:withMIDITimeStamp:tempo:)) return;
|
|
if (selector == @selector(unsyncMusicTimeStampsAndTemposFromMIDITimeStamps)) return;
|
|
if (selector == @selector(setMusicTimeStamp:withTempo:atMIDITimeStamp:)) return; // deprecated
|
|
|
|
// Pass through remaining selectors
|
|
[invocation invokeWithTarget:_masterClock];
|
|
}
|
|
|
|
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
|
|
{
|
|
return [_masterClock methodSignatureForSelector:sel];
|
|
}
|
|
|
|
@end
|