932 lines
38 KiB
Objective-C
932 lines
38 KiB
Objective-C
//
|
|
// MIKMIDISequencer.m
|
|
// MIKMIDI
|
|
//
|
|
// Created by Chris Flesner on 11/26/14.
|
|
// Copyright (c) 2014 Mixed In Key. All rights reserved.
|
|
//
|
|
|
|
#import "MIKMIDISequencer.h"
|
|
#import <mach/mach_time.h>
|
|
#import "MIKMIDISequence.h"
|
|
#import "MIKMIDITrack.h"
|
|
#import "MIKMIDIClock.h"
|
|
#import "MIKMIDITempoEvent.h"
|
|
#import "MIKMIDINoteEvent.h"
|
|
#import "MIKMIDIChannelEvent.h"
|
|
#import "MIKMIDINoteOnCommand.h"
|
|
#import "MIKMIDINoteOffCommand.h"
|
|
#import "MIKMIDIDeviceManager.h"
|
|
#import "MIKMIDIMetronome.h"
|
|
#import "MIKMIDIMetaTimeSignatureEvent.h"
|
|
#import "MIKMIDIUtilities.h"
|
|
#import "MIKMIDISynthesizer.h"
|
|
#import "MIKMIDISequencer+MIKMIDIPrivate.h"
|
|
#import "MIKMIDISequence+MIKMIDIPrivate.h"
|
|
#import "MIKMIDICommandScheduler.h"
|
|
#import "MIKMIDIDestinationEndpoint.h"
|
|
#import "MIKMIDIControlChangeCommand.h"
|
|
#import "MIKMIDIControlChangeEvent.h"
|
|
|
|
|
|
#if !__has_feature(objc_arc)
|
|
#error MIKMIDISequencer.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDISequencer.m in the Build Phases for this target
|
|
#endif
|
|
|
|
#define kDefaultTempo 120
|
|
|
|
NSString * const MIKMIDISequencerWillLoopNotification = @"MIKMIDISequencerWillLoopNotification";
|
|
const MusicTimeStamp MIKMIDISequencerEndOfSequenceLoopEndTimeStamp = -1;
|
|
|
|
|
|
#pragma mark -
|
|
|
|
@interface MIKMIDIEventWithDestination : NSObject
|
|
@property (nonatomic, strong) MIKMIDIEvent *event;
|
|
@property (nonatomic, strong) id<MIKMIDICommandScheduler> destination;
|
|
@property (nonatomic, readonly) BOOL representsNoteOff;
|
|
+ (instancetype)eventWithDestination:(id<MIKMIDICommandScheduler>)destination event:(MIKMIDIEvent *)event;
|
|
+ (instancetype)eventWithDestination:(id<MIKMIDICommandScheduler>)destination event:(MIKMIDIEvent *)event representsNoteOff:(BOOL)representsNoteOff;
|
|
@end
|
|
|
|
|
|
@interface MIKMIDICommandWithDestination : NSObject
|
|
@property (nonatomic, strong) MIKMIDICommand *command;
|
|
@property (nonatomic, strong) id<MIKMIDICommandScheduler> destination;
|
|
+ (instancetype)commandWithDestination:(id<MIKMIDICommandScheduler>)destination command:(MIKMIDICommand *)command;
|
|
@end
|
|
|
|
|
|
@interface MIKMIDIPendingNoteOffsForTimeStamp : NSObject
|
|
@property (nonatomic, strong) NSMutableArray *noteEventsWithEndTimeStamp;
|
|
@property (nonatomic) MusicTimeStamp endTimeStamp;
|
|
+ (instancetype)pendingNoteOffWithEndTimeStamp:(MusicTimeStamp)endTimeStamp;
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
|
|
@interface MIKMIDISequencer ()
|
|
{
|
|
void *_processingQueueKey;
|
|
void *_processingQueueContext;
|
|
}
|
|
|
|
@property (readonly, nonatomic) MIKMIDIClock *clock;
|
|
|
|
@property (nonatomic, getter=isPlaying) BOOL playing;
|
|
@property (nonatomic, getter=isRecording) BOOL recording;
|
|
@property (nonatomic, getter=isLooping) BOOL looping;
|
|
|
|
@property (nonatomic) MIDITimeStamp latestScheduledMIDITimeStamp;
|
|
|
|
@property (nonatomic, strong) NSMutableDictionary *pendingNoteOffs;
|
|
|
|
@property (nonatomic, strong) NSMutableDictionary *pendingRecordedNoteEvents;
|
|
|
|
@property (nonatomic) MusicTimeStamp startingTimeStamp;
|
|
@property (nonatomic) MusicTimeStamp initialStartingTimeStamp;
|
|
|
|
@property (nonatomic, strong) NSMapTable *tracksToDestinationsMap;
|
|
@property (nonatomic, strong) NSMapTable *tracksToDefaultSynthsMap;
|
|
|
|
@property (nonatomic) BOOL needsCurrentTempoUpdate;
|
|
|
|
@property (readonly, nonatomic) MusicTimeStamp sequenceLength;
|
|
|
|
@property (nonatomic) dispatch_queue_t processingQueue;
|
|
@property (nonatomic) dispatch_source_t processingTimer;
|
|
|
|
@end
|
|
|
|
|
|
@implementation MIKMIDISequencer
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)initWithSequence:(MIKMIDISequence *)sequence
|
|
{
|
|
if (self = [super init]) {
|
|
self.sequence = sequence;
|
|
_clock = [MIKMIDIClock clock];
|
|
_syncedClock = [_clock syncedClock];
|
|
_loopEndTimeStamp = MIKMIDISequencerEndOfSequenceLoopEndTimeStamp;
|
|
_preRoll = 4;
|
|
_clickTrackStatus = MIKMIDISequencerClickTrackStatusEnabledInRecord;
|
|
_tracksToDestinationsMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory];
|
|
_tracksToDefaultSynthsMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory];
|
|
_createSynthsIfNeeded = YES;
|
|
_processingQueueKey = &_processingQueueKey;
|
|
_processingQueueContext = &_processingQueueContext;
|
|
_maximumLookAheadInterval = 0.1;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
+ (instancetype)sequencerWithSequence:(MIKMIDISequence *)sequence
|
|
{
|
|
return [[self alloc] initWithSequence:sequence];
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
return [self initWithSequence:[MIKMIDISequence sequence]];
|
|
}
|
|
|
|
+ (instancetype)sequencer
|
|
{
|
|
return [[self alloc] init];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_sequence removeObserver:self forKeyPath:@"tracks"];
|
|
self.processingTimer = NULL;
|
|
}
|
|
|
|
#pragma mark - Playback
|
|
|
|
- (void)startPlayback
|
|
{
|
|
[self startPlaybackAtTimeStamp:0];
|
|
}
|
|
|
|
- (void)startPlaybackAtTimeStamp:(MusicTimeStamp)timeStamp
|
|
{
|
|
[self startPlaybackAtTimeStamp:timeStamp adjustForPreRollWhenRecording:YES];
|
|
}
|
|
|
|
- (void)startPlaybackAtTimeStamp:(MusicTimeStamp)timeStamp adjustForPreRollWhenRecording:(BOOL)adjustForPreRoll
|
|
{
|
|
MIDITimeStamp midiTimeStamp = MIKMIDIGetCurrentTimeStamp() + MIKMIDIClockMIDITimeStampsPerTimeInterval(0.001);
|
|
[self startPlaybackAtTimeStamp:timeStamp MIDITimeStamp:midiTimeStamp];
|
|
}
|
|
|
|
- (void)startPlaybackAtTimeStamp:(MusicTimeStamp)timeStamp MIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
[self startPlaybackAtTimeStamp:timeStamp MIDITimeStamp:midiTimeStamp adjustForPreRollWhenRecording:YES];
|
|
}
|
|
|
|
- (void)startPlaybackAtTimeStamp:(MusicTimeStamp)timeStamp MIDITimeStamp:(MIDITimeStamp)midiTimeStamp adjustForPreRollWhenRecording:(BOOL)adjustForPreRoll
|
|
{
|
|
if (self.isPlaying) [self stop];
|
|
if (adjustForPreRoll && self.isRecording) timeStamp -= self.preRoll;
|
|
|
|
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_INITIATED, 0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
dispatch_queue_t queue = dispatch_queue_create(queueLabel.UTF8String, attr);
|
|
dispatch_queue_set_specific(queue, &_processingQueueKey, &_processingQueueContext, NULL);
|
|
self.processingQueue = queue;
|
|
|
|
dispatch_sync(queue, ^{
|
|
self.startingTimeStamp = timeStamp;
|
|
self.initialStartingTimeStamp = timeStamp;
|
|
|
|
Float64 startingTempo = [self.sequence tempoAtTimeStamp:timeStamp];
|
|
if (!startingTempo) startingTempo = kDefaultTempo;
|
|
[self updateClockWithMusicTimeStamp:timeStamp tempo:startingTempo atMIDITimeStamp:midiTimeStamp];
|
|
});
|
|
|
|
self.playing = YES;
|
|
|
|
dispatch_sync(queue, ^{
|
|
self.pendingNoteOffs = [NSMutableDictionary dictionary];
|
|
self.latestScheduledMIDITimeStamp = midiTimeStamp;
|
|
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.processingQueue);
|
|
if (!timer) return NSLog(@"Unable to create processing timer for %@.", [self class]);
|
|
self.processingTimer = timer;
|
|
|
|
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC, 0.05 * NSEC_PER_SEC);
|
|
dispatch_source_set_event_handler(timer, ^{
|
|
[self processSequenceStartingFromMIDITimeStamp:self.latestScheduledMIDITimeStamp];
|
|
});
|
|
|
|
dispatch_resume(timer);
|
|
});
|
|
}
|
|
|
|
- (void)resumePlayback
|
|
{
|
|
[self startPlaybackAtTimeStamp:self.currentTimeStamp];
|
|
}
|
|
|
|
- (void)stop
|
|
{
|
|
[self stopWithDispatchToProcessingQueue:YES];
|
|
}
|
|
|
|
- (void)stopAllPlayingNotesForCommandScheduler:(id<MIKMIDICommandScheduler>)scheduler
|
|
{
|
|
[self dispatchSyncToProcessingQueueAsNeeded:^{
|
|
NSMutableArray *commandsToSendNow = [NSMutableArray array];
|
|
MIDITimeStamp offTimeStamp = MIKMIDIGetCurrentTimeStamp() + MIKMIDIClockMIDITimeStampsPerTimeInterval(self.maximumLookAheadInterval);
|
|
|
|
for (MIKMIDIPendingNoteOffsForTimeStamp *pendingNoteOffsForTimeStamp in self.pendingNoteOffs.allValues) {
|
|
NSMutableArray *noteEvents = pendingNoteOffsForTimeStamp.noteEventsWithEndTimeStamp;
|
|
NSUInteger count = noteEvents.count;
|
|
NSMutableIndexSet *indexesToRemove = [NSMutableIndexSet indexSet];
|
|
for (NSUInteger i = 0; i < count; i++) {
|
|
MIKMIDIEventWithDestination *event = noteEvents[i];
|
|
if (event.destination == scheduler) {
|
|
[indexesToRemove addIndex:i];
|
|
|
|
MIKMIDINoteEvent *noteEvent = (MIKMIDINoteEvent *)event.event;
|
|
MIKMIDINoteOffCommand *command = [MIKMIDINoteOffCommand noteOffCommandWithNote:noteEvent.note velocity:0 channel:noteEvent.channel midiTimeStamp:offTimeStamp];
|
|
[commandsToSendNow addObject:command];
|
|
}
|
|
}
|
|
|
|
[noteEvents removeObjectsAtIndexes:indexesToRemove];
|
|
}
|
|
|
|
if (commandsToSendNow.count) [self scheduleCommands:commandsToSendNow withCommandScheduler:scheduler];
|
|
}];
|
|
}
|
|
|
|
- (void)stopWithDispatchToProcessingQueue:(BOOL)dispatchToProcessingQueue
|
|
{
|
|
MIDITimeStamp stopTimeStamp = MIKMIDIGetCurrentTimeStamp();
|
|
if (!self.isPlaying) return;
|
|
|
|
void (^stopPlayback)(void) = ^{
|
|
self.processingTimer = NULL;
|
|
|
|
MIKMIDIClock *clock = self.clock;
|
|
[self recordAllPendingNoteEventsWithOffTimeStamp:[clock musicTimeStampForMIDITimeStamp:stopTimeStamp]];
|
|
MusicTimeStamp allPendingNotesOffTimeStamp = MAX(self.latestScheduledMIDITimeStamp + 1, MIKMIDIGetCurrentTimeStamp() + MIKMIDIClockMIDITimeStampsPerTimeInterval(0.001));
|
|
[self sendAllPendingNoteOffsWithMIDITimeStamp:allPendingNotesOffTimeStamp];
|
|
self.pendingRecordedNoteEvents = nil;
|
|
self.looping = NO;
|
|
|
|
MusicTimeStamp stopMusicTimeStamp = [clock musicTimeStampForMIDITimeStamp:stopTimeStamp];
|
|
self->_currentTimeStamp = (stopMusicTimeStamp <= self.sequenceLength) ? stopMusicTimeStamp : self.sequenceLength;
|
|
|
|
[clock unsyncMusicTimeStampsAndTemposFromMIDITimeStamps];
|
|
};
|
|
|
|
dispatchToProcessingQueue ? dispatch_sync(self.processingQueue, stopPlayback) : stopPlayback();
|
|
|
|
self.processingQueue = NULL;
|
|
self.playing = NO;
|
|
self.recording = NO;
|
|
}
|
|
|
|
- (void)processSequenceStartingFromMIDITimeStamp:(MIDITimeStamp)fromMIDITimeStamp
|
|
{
|
|
MIDITimeStamp toMIDITimeStamp = MIKMIDIGetCurrentTimeStamp() + MIKMIDIClockMIDITimeStampsPerTimeInterval(self.maximumLookAheadInterval);
|
|
if (toMIDITimeStamp < fromMIDITimeStamp) return;
|
|
MIKMIDIClock *clock = self.clock;
|
|
|
|
MIKMIDISequence *sequence = self.sequence;
|
|
MusicTimeStamp loopStartTimeStamp = self.loopStartTimeStamp;
|
|
MusicTimeStamp loopEndTimeStamp = self.effectiveLoopEndTimeStamp;
|
|
MusicTimeStamp fromMusicTimeStamp = [clock musicTimeStampForMIDITimeStamp:fromMIDITimeStamp];
|
|
MusicTimeStamp calculatedToMusicTimeStamp = [clock musicTimeStampForMIDITimeStamp:toMIDITimeStamp];
|
|
BOOL isLooping = (self.shouldLoop && calculatedToMusicTimeStamp > loopStartTimeStamp && loopEndTimeStamp > loopStartTimeStamp);
|
|
if (isLooping != self.isLooping) self.looping = isLooping;
|
|
MusicTimeStamp maxToMusicTimeStamp = self.isRecording ? DBL_MAX : self.sequenceLength; // If recording, don't limit max timestamp (Issue #45)
|
|
maxToMusicTimeStamp = isLooping ? loopEndTimeStamp : maxToMusicTimeStamp;
|
|
MusicTimeStamp toMusicTimeStamp = MIN(calculatedToMusicTimeStamp, maxToMusicTimeStamp);
|
|
MIDITimeStamp actualToMIDITimeStamp = [clock midiTimeStampForMusicTimeStamp:toMusicTimeStamp];
|
|
|
|
// Get relevant tempo events
|
|
NSMutableDictionary *allEventsByTimeStamp = [NSMutableDictionary dictionary];
|
|
NSMutableDictionary *tempoEventsByTimeStamp = [NSMutableDictionary dictionary];
|
|
Float64 overrideTempo = self.tempo;
|
|
|
|
if (!overrideTempo) {
|
|
NSArray *sequenceTempoEvents = [sequence.tempoTrack eventsOfClass:[MIKMIDITempoEvent class] fromTimeStamp:MAX(fromMusicTimeStamp, 0) toTimeStamp:toMusicTimeStamp];
|
|
for (MIKMIDITempoEvent *tempoEvent in sequenceTempoEvents) {
|
|
NSNumber *timeStampKey = @(tempoEvent.timeStamp);
|
|
allEventsByTimeStamp[timeStampKey] = [NSMutableArray arrayWithObject:tempoEvent];
|
|
tempoEventsByTimeStamp[timeStampKey] = tempoEvent;
|
|
}
|
|
}
|
|
|
|
if (self.needsCurrentTempoUpdate) {
|
|
if (!tempoEventsByTimeStamp.count) {
|
|
if (!overrideTempo) overrideTempo = [sequence tempoAtTimeStamp:fromMusicTimeStamp];
|
|
if (!overrideTempo) overrideTempo = kDefaultTempo;
|
|
|
|
MIKMIDITempoEvent *tempoEvent = [MIKMIDITempoEvent tempoEventWithTimeStamp:fromMusicTimeStamp tempo:overrideTempo];
|
|
NSNumber *timeStampKey = @(fromMusicTimeStamp);
|
|
allEventsByTimeStamp[timeStampKey] = [NSMutableArray arrayWithObject:tempoEvent];
|
|
tempoEventsByTimeStamp[timeStampKey] = tempoEvent;
|
|
}
|
|
self.needsCurrentTempoUpdate = NO;
|
|
}
|
|
|
|
// Get pending note off events
|
|
NSMutableDictionary *pendingNoteOffs = self.pendingNoteOffs;
|
|
for (NSNumber *timeStampKey in [pendingNoteOffs copy]) {
|
|
MusicTimeStamp pendingNoteOffsMusicTimeStamp = timeStampKey.doubleValue;
|
|
if (pendingNoteOffsMusicTimeStamp < fromMusicTimeStamp) continue;
|
|
if (pendingNoteOffsMusicTimeStamp > toMusicTimeStamp) continue;
|
|
if (isLooping && (pendingNoteOffsMusicTimeStamp == loopEndTimeStamp)) continue; // These pending note offs will be handled right before we loop
|
|
|
|
NSMutableArray *eventsAtTimeStamp = allEventsByTimeStamp[timeStampKey] ? allEventsByTimeStamp[timeStampKey] : [NSMutableArray array];
|
|
[eventsAtTimeStamp addObject:pendingNoteOffs[timeStampKey]];
|
|
allEventsByTimeStamp[timeStampKey] = eventsAtTimeStamp;
|
|
[pendingNoteOffs removeObjectForKey:timeStampKey];
|
|
}
|
|
|
|
// Get other events
|
|
NSMutableArray *nonMutedTracks = [[NSMutableArray alloc] init];
|
|
NSMutableArray *soloTracks = [[NSMutableArray alloc] init];
|
|
for (MIKMIDITrack *track in sequence.tracks) {
|
|
if (track.isMuted) continue;
|
|
|
|
[nonMutedTracks addObject:track];
|
|
if (track.solo) { [soloTracks addObject:track]; }
|
|
}
|
|
|
|
// Never play muted tracks. If any non-muted tracks are soloed, only play those. Matches MusicPlayer behavior
|
|
NSArray *tracksToPlay = soloTracks.count != 0 ? soloTracks : nonMutedTracks;
|
|
|
|
for (MIKMIDITrack *track in tracksToPlay) {
|
|
MusicTimeStamp startTimeStamp = MAX(fromMusicTimeStamp - track.offset, 0);
|
|
MusicTimeStamp endTimeStamp = toMusicTimeStamp - track.offset;
|
|
NSArray *events = [track eventsFromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp];
|
|
if (track.offset != 0) {
|
|
// Shift events by offset
|
|
NSMutableArray *shiftedEvents = [NSMutableArray array];
|
|
for (MIKMIDIEvent *event in events) {
|
|
MIKMutableMIDIEvent *shiftedEvent = [event mutableCopy];
|
|
shiftedEvent.timeStamp += track.offset;
|
|
[shiftedEvents addObject:shiftedEvent];
|
|
}
|
|
events = shiftedEvents;
|
|
}
|
|
|
|
id<MIKMIDICommandScheduler> destination = events.count ? [self commandSchedulerForTrack:track] : nil; // only get the destination if there's events so we don't create a destination endpoint if not needed
|
|
for (MIKMIDIEvent *event in events) {
|
|
if ([event isKindOfClass:[MIKMIDINoteEvent class]] && [(MIKMIDINoteEvent *)event duration] <= 0) continue;
|
|
NSNumber *timeStampKey = @(event.timeStamp);
|
|
NSMutableArray *eventsAtTimeStamp = allEventsByTimeStamp[timeStampKey] ? allEventsByTimeStamp[timeStampKey] : [NSMutableArray array];
|
|
[eventsAtTimeStamp addObject:[MIKMIDIEventWithDestination eventWithDestination:destination event:event]];
|
|
allEventsByTimeStamp[timeStampKey] = eventsAtTimeStamp;
|
|
}
|
|
}
|
|
|
|
// Get click track events
|
|
for (MIKMIDIEventWithDestination *destinationEvent in [self clickTrackEventsFromTimeStamp:fromMusicTimeStamp toTimeStamp:toMusicTimeStamp]) {
|
|
NSNumber *timeStampKey = @(destinationEvent.event.timeStamp);
|
|
NSMutableArray *eventsAtTimesStamp = allEventsByTimeStamp[timeStampKey] ? allEventsByTimeStamp[timeStampKey] : [NSMutableArray array];
|
|
[eventsAtTimesStamp addObject:destinationEvent];
|
|
allEventsByTimeStamp[timeStampKey] = eventsAtTimesStamp;
|
|
}
|
|
|
|
// Schedule events
|
|
for (NSNumber *timeStampKey in [allEventsByTimeStamp.allKeys sortedArrayUsingSelector:@selector(compare:)]) {
|
|
MusicTimeStamp musicTimeStamp = timeStampKey.doubleValue;
|
|
if (isLooping && (musicTimeStamp < loopStartTimeStamp || musicTimeStamp >= loopEndTimeStamp)) continue;
|
|
MIDITimeStamp midiTimeStamp = [clock midiTimeStampForMusicTimeStamp:musicTimeStamp];
|
|
if (midiTimeStamp < MIKMIDIGetCurrentTimeStamp() && midiTimeStamp > fromMIDITimeStamp) continue; // prevents events that were just recorded from being scheduled
|
|
|
|
MIKMIDITempoEvent *tempoEventAtTimeStamp = tempoEventsByTimeStamp[timeStampKey];
|
|
if (tempoEventAtTimeStamp) [self updateClockWithMusicTimeStamp:musicTimeStamp tempo:tempoEventAtTimeStamp.bpm atMIDITimeStamp:midiTimeStamp];
|
|
|
|
NSArray *events = allEventsByTimeStamp[timeStampKey];
|
|
for (id eventObject in events) {
|
|
if ([eventObject isKindOfClass:[MIKMIDIEventWithDestination class]]) {
|
|
[self scheduleEventWithDestination:eventObject];
|
|
} else if ([eventObject isKindOfClass:[MIKMIDIPendingNoteOffsForTimeStamp class]]) {
|
|
for (MIKMIDIEventWithDestination *noteOffEvent in [eventObject noteEventsWithEndTimeStamp]) {
|
|
[self scheduleEventWithDestination:noteOffEvent];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.latestScheduledMIDITimeStamp = actualToMIDITimeStamp;
|
|
|
|
// Handle looping or stopping at the end of the sequence
|
|
if (isLooping) {
|
|
if (calculatedToMusicTimeStamp > toMusicTimeStamp) {
|
|
[self recordAllPendingNoteEventsWithOffTimeStamp:loopEndTimeStamp];
|
|
Float64 tempo = [sequence tempoAtTimeStamp:loopStartTimeStamp];
|
|
if (!tempo) tempo = kDefaultTempo;
|
|
MusicTimeStamp loopLength = loopEndTimeStamp - loopStartTimeStamp;
|
|
|
|
MIDITimeStamp loopStartMIDITimeStamp = [clock midiTimeStampForMusicTimeStamp:loopStartTimeStamp + loopLength];
|
|
[self sendAllPendingNoteOffsWithMIDITimeStamp:loopStartMIDITimeStamp];
|
|
[self updateClockWithMusicTimeStamp:loopStartTimeStamp tempo:tempo atMIDITimeStamp:loopStartMIDITimeStamp];
|
|
|
|
self.startingTimeStamp = loopStartTimeStamp;
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:MIKMIDISequencerWillLoopNotification object:self userInfo:nil];
|
|
[self processSequenceStartingFromMIDITimeStamp:loopStartMIDITimeStamp];
|
|
}
|
|
} else if (!self.isRecording) { // Don't stop automatically during recording
|
|
MIDITimeStamp systemTimeStamp = MIKMIDIGetCurrentTimeStamp();
|
|
if ((systemTimeStamp > actualToMIDITimeStamp) && ([clock musicTimeStampForMIDITimeStamp:systemTimeStamp] >= self.sequenceLength)) {
|
|
[self stopWithDispatchToProcessingQueue:NO];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)scheduleEventWithDestination:(MIKMIDIEventWithDestination *)destinationEvent
|
|
{
|
|
MIKMIDIEvent *event = destinationEvent.event;
|
|
id<MIKMIDICommandScheduler> destination = destinationEvent.destination;
|
|
MIKMIDIClock *clock = self.clock;
|
|
MIKMIDICommand *command;
|
|
|
|
if (event.eventType == MIKMIDIEventTypeMIDINoteMessage) {
|
|
if (destinationEvent.representsNoteOff) {
|
|
command = [MIKMIDICommand noteOffCommandFromNoteEvent:(MIKMIDINoteEvent *)event clock:clock];
|
|
} else {
|
|
MIKMIDINoteEvent *noteEvent = (MIKMIDINoteEvent *)event;
|
|
command = [MIKMIDICommand noteOnCommandFromNoteEvent:noteEvent clock:clock];
|
|
|
|
// Add note off to pending note offs
|
|
MusicTimeStamp endTimeStamp = noteEvent.endTimeStamp;
|
|
NSMutableDictionary *pendingNoteOffs = self.pendingNoteOffs;
|
|
MIKMIDIPendingNoteOffsForTimeStamp *pendingNoteOffsForEndTimeStamp = pendingNoteOffs[@(endTimeStamp)];
|
|
if (!pendingNoteOffsForEndTimeStamp) {
|
|
pendingNoteOffsForEndTimeStamp = [MIKMIDIPendingNoteOffsForTimeStamp pendingNoteOffWithEndTimeStamp:endTimeStamp];
|
|
pendingNoteOffs[@(endTimeStamp)] = pendingNoteOffsForEndTimeStamp;
|
|
}
|
|
[pendingNoteOffsForEndTimeStamp.noteEventsWithEndTimeStamp addObject:[MIKMIDIEventWithDestination eventWithDestination:destination event:event representsNoteOff:YES]];
|
|
}
|
|
} else if ([event isKindOfClass:[MIKMIDIChannelEvent class]]) {
|
|
command = [MIKMIDICommand commandFromChannelEvent:(MIKMIDIChannelEvent *)event clock:clock];
|
|
}
|
|
|
|
if (command) [self scheduleCommands:@[command] withCommandScheduler:destination];
|
|
}
|
|
|
|
- (void)sendAllPendingNoteOffsWithMIDITimeStamp:(MIDITimeStamp)offTimeStamp
|
|
{
|
|
NSMutableDictionary *noteOffs = self.pendingNoteOffs;
|
|
if (!noteOffs.count) return;
|
|
|
|
NSMapTable *noteOffDestinationsToCommands = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory];
|
|
MIKMIDIClock *clock = self.clock;
|
|
|
|
for (NSNumber *musicTimeStampNumber in noteOffs) {
|
|
MIKMIDIPendingNoteOffsForTimeStamp *pendingNoteOffs = noteOffs[musicTimeStampNumber];
|
|
for (MIKMIDIEventWithDestination *noteOffEventWithDestination in pendingNoteOffs.noteEventsWithEndTimeStamp) {
|
|
MIKMIDINoteEvent *event = (MIKMIDINoteEvent *)noteOffEventWithDestination.event;
|
|
id<MIKMIDICommandScheduler> destination = noteOffEventWithDestination.destination;
|
|
NSMutableArray *noteOffCommandsForDestination = [noteOffDestinationsToCommands objectForKey:destination] ? [noteOffDestinationsToCommands objectForKey:destination] : [NSMutableArray array];
|
|
|
|
MIKMutableMIDICommand *noteOffCommand = [[MIKMIDICommand noteOffCommandFromNoteEvent:event clock:clock] mutableCopy];
|
|
noteOffCommand.midiTimestamp = offTimeStamp;
|
|
[noteOffCommandsForDestination addObject:noteOffCommand];
|
|
[noteOffDestinationsToCommands setObject:noteOffCommandsForDestination forKey:destination];
|
|
}
|
|
}
|
|
|
|
for (id<MIKMIDICommandScheduler> scheduler in [[noteOffDestinationsToCommands keyEnumerator] allObjects]) {
|
|
[self scheduleCommands:[noteOffDestinationsToCommands objectForKey:scheduler] withCommandScheduler:scheduler];
|
|
}
|
|
|
|
[noteOffs removeAllObjects];
|
|
}
|
|
|
|
- (void)updateClockWithMusicTimeStamp:(MusicTimeStamp)musicTimeStamp tempo:(Float64)tempo atMIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
// Override tempo if neccessary
|
|
Float64 tempoOverride = self.tempo;
|
|
if (tempoOverride) tempo = tempoOverride;
|
|
[self.clock syncMusicTimeStamp:musicTimeStamp withMIDITimeStamp:midiTimeStamp tempo:tempo];
|
|
}
|
|
|
|
- (void)scheduleCommands:(NSArray *)commands withCommandScheduler:(id<MIKMIDICommandScheduler>)scheduler
|
|
{
|
|
[scheduler scheduleMIDICommands:[self modifiedMIDICommandsFromCommandsToBeScheduled:commands forCommandScheduler:scheduler]];
|
|
}
|
|
|
|
- (NSArray *)modifiedMIDICommandsFromCommandsToBeScheduled:(NSArray *)commandsToBeScheduled forCommandScheduler:(id<MIKMIDICommandScheduler>)scheduler { return commandsToBeScheduled; }
|
|
|
|
#pragma mark - Recording
|
|
|
|
- (void)startRecording
|
|
{
|
|
[self prepareForRecordingWithPreRoll:YES];
|
|
[self startPlayback];
|
|
}
|
|
|
|
- (void)startRecordingAtTimeStamp:(MusicTimeStamp)timeStamp
|
|
{
|
|
[self prepareForRecordingWithPreRoll:YES];
|
|
[self startPlaybackAtTimeStamp:timeStamp];
|
|
}
|
|
|
|
- (void)startRecordingAtTimeStamp:(MusicTimeStamp)timeStamp MIDITimeStamp:(MIDITimeStamp)midiTimeStamp
|
|
{
|
|
[self prepareForRecordingWithPreRoll:YES];
|
|
[self startPlaybackAtTimeStamp:timeStamp MIDITimeStamp:midiTimeStamp];
|
|
}
|
|
|
|
- (void)resumeRecording
|
|
{
|
|
[self prepareForRecordingWithPreRoll:YES];
|
|
[self resumePlayback];
|
|
}
|
|
|
|
- (void)prepareForRecordingWithPreRoll:(BOOL)includePreRoll
|
|
{
|
|
self.pendingRecordedNoteEvents = [NSMutableDictionary dictionary];
|
|
self.recording = YES;
|
|
}
|
|
|
|
- (void)recordMIDICommand:(MIKMIDICommand *)command
|
|
{
|
|
if (!self.isRecording) return;
|
|
|
|
MIDITimeStamp midiTimeStamp = command.midiTimestamp;
|
|
MusicTimeStamp musicTimeStamp = [self.clock musicTimeStampForMIDITimeStamp:midiTimeStamp];
|
|
|
|
if (musicTimeStamp < 0) { return; } // Command is in pre-roll
|
|
|
|
MIKMIDIEvent *event;
|
|
if ([command isKindOfClass:[MIKMIDINoteOnCommand class]]) { // note On
|
|
MIKMIDINoteOnCommand *noteOnCommand = (MIKMIDINoteOnCommand *)command;
|
|
if (noteOnCommand.velocity) {
|
|
MIDINoteMessage message = { .channel = noteOnCommand.channel, .note = noteOnCommand.note, .velocity = noteOnCommand.velocity, 0, 0 };
|
|
MIKMutableMIDINoteEvent *noteEvent = [MIKMutableMIDINoteEvent noteEventWithTimeStamp:musicTimeStamp message:message];
|
|
NSNumber *noteNumber = @(noteOnCommand.note);
|
|
NSMutableSet *noteEventsAtNote = self.pendingRecordedNoteEvents[noteNumber];
|
|
if (!noteEventsAtNote) {
|
|
noteEventsAtNote = [NSMutableSet setWithCapacity:1];
|
|
self.pendingRecordedNoteEvents[noteNumber] = noteEventsAtNote;
|
|
}
|
|
[noteEventsAtNote addObject:noteEvent];
|
|
} else { // Velocity is 0, treat as a note Off per MIDI spec
|
|
event = [self pendingNoteEventWithNoteNumber:@(noteOnCommand.note) channel:noteOnCommand.channel releaseVelocity:0 offTimeStamp:musicTimeStamp];
|
|
}
|
|
} else if ([command isKindOfClass:[MIKMIDINoteOffCommand class]]) { // note Off
|
|
MIKMIDINoteOffCommand *noteOffCommand = (MIKMIDINoteOffCommand *)command;
|
|
event = [self pendingNoteEventWithNoteNumber:@(noteOffCommand.note) channel:noteOffCommand.channel releaseVelocity:noteOffCommand.velocity offTimeStamp:musicTimeStamp];
|
|
} else if ([command isKindOfClass:[MIKMIDIControlChangeCommand class]]) { // cc command
|
|
MIKMIDIControlChangeCommand* ccCmd = (MIKMIDIControlChangeCommand*)command;
|
|
MIKMutableMIDIControlChangeEvent* ccEvent = [[MIKMutableMIDIControlChangeEvent alloc] init];
|
|
ccEvent.controllerNumber = ccCmd.controllerNumber;
|
|
ccEvent.controllerValue = ccCmd.controllerValue;
|
|
ccEvent.channel = ccCmd.channel;
|
|
ccEvent.timeStamp = musicTimeStamp;
|
|
event = ccEvent;
|
|
}
|
|
|
|
if (event) [self.recordEnabledTracks makeObjectsPerformSelector:@selector(addEvent:) withObject:event];
|
|
}
|
|
|
|
- (void)recordAllPendingNoteEventsWithOffTimeStamp:(MusicTimeStamp)offTimeStamp
|
|
{
|
|
NSMutableSet *events = [NSMutableSet set];
|
|
|
|
NSMutableDictionary *pendingRecordedNoteEvents = self.pendingRecordedNoteEvents;
|
|
for (NSNumber *noteNumber in pendingRecordedNoteEvents) {
|
|
for (MIKMutableMIDINoteEvent *event in pendingRecordedNoteEvents[noteNumber]) {
|
|
event.releaseVelocity = 0;
|
|
event.duration = offTimeStamp - event.timeStamp;
|
|
[events addObject:event];
|
|
}
|
|
}
|
|
self.pendingRecordedNoteEvents = [NSMutableDictionary dictionary];
|
|
|
|
if ([events count]) {
|
|
for (MIKMIDITrack *track in self.recordEnabledTracks) {
|
|
[track addEvents:[events allObjects]];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (MIKMIDINoteEvent *)pendingNoteEventWithNoteNumber:(NSNumber *)noteNumber channel:(UInt8)channel releaseVelocity:(UInt8)releaseVelocity offTimeStamp:(MusicTimeStamp)offTimeStamp
|
|
{
|
|
NSMutableSet *pendingRecordedNoteEventsAtNote = self.pendingRecordedNoteEvents[noteNumber];
|
|
for (MIKMutableMIDINoteEvent *noteEvent in [pendingRecordedNoteEventsAtNote copy]) {
|
|
if (channel == noteEvent.channel) {
|
|
noteEvent.releaseVelocity = releaseVelocity;
|
|
noteEvent.duration = offTimeStamp - noteEvent.timeStamp;
|
|
|
|
if (pendingRecordedNoteEventsAtNote.count > 1) {
|
|
[pendingRecordedNoteEventsAtNote removeObject:noteEvent];
|
|
} else {
|
|
[self.pendingRecordedNoteEvents removeObjectForKey:noteNumber];
|
|
}
|
|
|
|
return noteEvent;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
#pragma mark - Configuration
|
|
|
|
- (void)setCommandScheduler:(id<MIKMIDICommandScheduler>)commandScheduler forTrack:(MIKMIDITrack *)track
|
|
{
|
|
if (!commandScheduler) {
|
|
[self.tracksToDestinationsMap removeObjectForKey:track];
|
|
return;
|
|
}
|
|
[self.tracksToDestinationsMap setObject:commandScheduler forKey:track];
|
|
[self.tracksToDefaultSynthsMap removeObjectForKey:track];
|
|
}
|
|
|
|
- (id<MIKMIDICommandScheduler>)commandSchedulerForTrack:(MIKMIDITrack *)track
|
|
{
|
|
id<MIKMIDICommandScheduler> result = [self.tracksToDestinationsMap objectForKey:track];
|
|
if (!result && self.shouldCreateSynthsIfNeeded) {
|
|
// Create a default synthesizer
|
|
NSError *error = nil;
|
|
result = [[MIKMIDISynthesizer alloc] initWithError:&error];
|
|
if (!result) {
|
|
NSLog(@"Error creating default synthesizer for %@: %@", track, error);
|
|
return nil;
|
|
}
|
|
[self setCommandScheduler:result forTrack:track];
|
|
[self.tracksToDefaultSynthsMap setObject:result forKey:track];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
- (MIKMIDISynthesizer *)builtinSynthesizerForTrack:(MIKMIDITrack *)track
|
|
{
|
|
[[self commandSchedulerForTrack:track] self]; // Will force creation of a synth if one doesn't exist, but should
|
|
return [self.tracksToDefaultSynthsMap objectForKey:track];
|
|
}
|
|
|
|
#pragma mark - Click Track
|
|
|
|
- (NSMutableArray *)clickTrackEventsFromTimeStamp:(MusicTimeStamp)fromTimeStamp toTimeStamp:(MusicTimeStamp)toTimeStamp
|
|
{
|
|
if (!self.metronome) return [NSMutableArray array];
|
|
|
|
MIKMIDISequencerClickTrackStatus clickTrackStatus = self.clickTrackStatus;
|
|
if (clickTrackStatus == MIKMIDISequencerClickTrackStatusDisabled) return nil;
|
|
if (!self.isRecording && clickTrackStatus != MIKMIDISequencerClickTrackStatusAlwaysEnabled) return nil;
|
|
|
|
NSMutableArray *clickEvents = [NSMutableArray array];
|
|
MIKMIDIMetronome *metronome = self.metronome;
|
|
MIDINoteMessage tickMessage = metronome.tickMessage;
|
|
MIDINoteMessage tockMessage = metronome.tockMessage;
|
|
|
|
MIKMIDISequence *sequence = self.sequence;
|
|
MIKMIDITimeSignature timeSignature = [sequence timeSignatureAtTimeStamp:MAX(fromTimeStamp, 0)];
|
|
NSMutableArray *timeSignatureEvents = [[sequence.tempoTrack eventsOfClass:[MIKMIDIMetaTimeSignatureEvent class]
|
|
fromTimeStamp:MAX(fromTimeStamp, 0)
|
|
toTimeStamp:MAX(toTimeStamp, 0)] mutableCopy];
|
|
|
|
MusicTimeStamp clickTimeStamp = floor(fromTimeStamp);
|
|
while (clickTimeStamp <= toTimeStamp) {
|
|
if (clickTrackStatus == MIKMIDISequencerClickTrackStatusEnabledOnlyInPreRoll && clickTimeStamp >= self.initialStartingTimeStamp + self.preRoll) break;
|
|
|
|
MIKMIDIMetaTimeSignatureEvent *event = [timeSignatureEvents firstObject];
|
|
if (event && event.timeStamp <= clickTimeStamp) {
|
|
timeSignature = (MIKMIDITimeSignature) { .numerator = event.numerator, .denominator = event.denominator };
|
|
[timeSignatureEvents removeObjectAtIndex:0];
|
|
}
|
|
|
|
if (clickTimeStamp >= fromTimeStamp) { // ignore if clickTimeStamp is still less than fromTimeStamp (from being floored)
|
|
NSInteger adjustedTimeStamp = clickTimeStamp * timeSignature.denominator / 4.0;
|
|
BOOL isTick = !((adjustedTimeStamp + timeSignature.numerator) % (timeSignature.numerator));
|
|
MIDINoteMessage clickMessage = isTick ? tickMessage : tockMessage;
|
|
MIKMIDINoteEvent *noteEvent = [MIKMIDINoteEvent noteEventWithTimeStamp:clickTimeStamp message:clickMessage];
|
|
[clickEvents addObject:[MIKMIDIEventWithDestination eventWithDestination:metronome event:noteEvent]];
|
|
}
|
|
|
|
clickTimeStamp += 4.0 / timeSignature.denominator;
|
|
}
|
|
|
|
return clickEvents;
|
|
}
|
|
|
|
#pragma mark - Loop Points
|
|
|
|
- (void)setLoopStartTimeStamp:(MusicTimeStamp)loopStartTimeStamp endTimeStamp:(MusicTimeStamp)loopEndTimeStamp
|
|
{
|
|
if (loopEndTimeStamp != MIKMIDISequencerEndOfSequenceLoopEndTimeStamp && (loopStartTimeStamp >= loopEndTimeStamp)) return;
|
|
|
|
[self dispatchSyncToProcessingQueueAsNeeded:^{
|
|
[self willChangeValueForKey:@"loopStartTimeStamp"];
|
|
[self willChangeValueForKey:@"loopEndTimeStamp"];
|
|
|
|
self->_loopStartTimeStamp = loopStartTimeStamp;
|
|
self->_loopEndTimeStamp = loopEndTimeStamp;
|
|
|
|
[self didChangeValueForKey:@"loopStartTimeStamp"];
|
|
[self didChangeValueForKey:@"loopEndTimeStamp"];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Timer
|
|
|
|
- (void)processingTimerFired:(NSTimer *)timer
|
|
{
|
|
[self processSequenceStartingFromMIDITimeStamp:self.latestScheduledMIDITimeStamp + 1];
|
|
}
|
|
|
|
#pragma mark - KVO
|
|
|
|
+ (BOOL)automaticallyNotifiesObserversOfSequence { return NO; }
|
|
+ (NSSet *)keyPathsForValuesAffectingEffectiveLoopEndTimeStamp { return [NSSet setWithObjects:@"loopEndTimeStamp", @"sequence.length", nil]; }
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
NSSet *currentTracks = [NSSet setWithArray:self.sequence.tracks];
|
|
|
|
NSMapTable *tracksToDestinationMap = self.tracksToDestinationsMap;
|
|
NSMutableSet *tracksToRemoveFromDestinationMap = [NSMutableSet setWithArray:[[tracksToDestinationMap keyEnumerator] allObjects]];
|
|
[tracksToRemoveFromDestinationMap minusSet:currentTracks];
|
|
|
|
for (MIKMIDITrack *track in tracksToRemoveFromDestinationMap) {
|
|
[tracksToDestinationMap removeObjectForKey:track];
|
|
}
|
|
|
|
NSMapTable *tracksToSynthsMap = self.tracksToDefaultSynthsMap;
|
|
NSMutableSet *tracksToRemoveFromSynthsMap = [NSMutableSet setWithArray:[[tracksToSynthsMap keyEnumerator] allObjects]];
|
|
[tracksToRemoveFromSynthsMap minusSet:currentTracks];
|
|
|
|
for (MIKMIDITrack *track in tracksToRemoveFromSynthsMap) {
|
|
[tracksToSynthsMap removeObjectForKey:track];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
@synthesize currentTimeStamp = _currentTimeStamp;
|
|
- (MusicTimeStamp)currentTimeStamp
|
|
{
|
|
MIKMIDIClock *clock = self.clock;
|
|
if (clock.isReady) {
|
|
MusicTimeStamp timeStamp = [clock musicTimeStampForMIDITimeStamp:MIKMIDIGetCurrentTimeStamp()];
|
|
_currentTimeStamp = MAX(((timeStamp <= self.sequenceLength) ? timeStamp : self.sequenceLength), self.startingTimeStamp);
|
|
}
|
|
return _currentTimeStamp;
|
|
}
|
|
|
|
- (void)setCurrentTimeStamp:(MusicTimeStamp)currentTimeStamp
|
|
{
|
|
if (self.isPlaying) {
|
|
BOOL isRecording = self.isRecording;
|
|
[self stop];
|
|
if (isRecording) [self prepareForRecordingWithPreRoll:NO];
|
|
[self startPlaybackAtTimeStamp:currentTimeStamp adjustForPreRollWhenRecording:NO];
|
|
} else {
|
|
_currentTimeStamp = currentTimeStamp;
|
|
}
|
|
}
|
|
|
|
- (MusicTimeStamp)effectiveLoopEndTimeStamp
|
|
{
|
|
return (_loopEndTimeStamp < 0) ? self.sequenceLength : _loopEndTimeStamp;
|
|
}
|
|
|
|
- (void)setPreRoll:(MusicTimeStamp)preRoll
|
|
{
|
|
_preRoll = (preRoll >= 0) ? preRoll : 0;
|
|
}
|
|
|
|
- (void)setProcessingTimer:(dispatch_source_t)processingTimer
|
|
{
|
|
if (_processingTimer != processingTimer) {
|
|
if (_processingTimer) {
|
|
dispatch_source_cancel(_processingTimer);
|
|
}
|
|
_processingTimer = processingTimer;
|
|
}
|
|
}
|
|
|
|
@synthesize metronome = _metronome;
|
|
- (MIKMIDIMetronome *)metronome
|
|
{
|
|
#if (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0) || !TARGET_OS_IPHONE
|
|
if (!_metronome) _metronome = [[MIKMIDIMetronome alloc] initWithError:NULL];
|
|
return _metronome;
|
|
#else
|
|
return nil;
|
|
#endif
|
|
}
|
|
|
|
- (void)setTempo:(Float64)tempo
|
|
{
|
|
if (tempo < 0) tempo = 0;
|
|
if (_tempo != tempo) {
|
|
_tempo = tempo;
|
|
if (self.isPlaying) self.needsCurrentTempoUpdate = YES;
|
|
}
|
|
}
|
|
|
|
- (MusicTimeStamp)sequenceLength
|
|
{
|
|
MusicTimeStamp length = self.overriddenSequenceLength;
|
|
return length ? length : self.sequence.length;
|
|
}
|
|
|
|
- (void)setSequence:(MIKMIDISequence *)sequence
|
|
{
|
|
if (_sequence != sequence) {
|
|
[self dispatchSyncToProcessingQueueAsNeeded:^{
|
|
[self willChangeValueForKey:@"sequence"];
|
|
[self->_sequence removeObserver:self forKeyPath:@"tracks"];
|
|
|
|
if (self->_sequence.sequencer == self) self->_sequence.sequencer = nil;
|
|
self->_sequence = sequence;
|
|
self->_sequence.sequencer = self;
|
|
|
|
[self->_sequence addObserver:self forKeyPath:@"tracks" options:NSKeyValueObservingOptionInitial context:NULL];
|
|
[self didChangeValueForKey:@"sequence"];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)setMaximumLookAheadInterval:(NSTimeInterval)maximumLookAheadInterval
|
|
{
|
|
_maximumLookAheadInterval = MIN(MAX(maximumLookAheadInterval, 0.05), 1.0);
|
|
}
|
|
|
|
#pragma mark - Deprecated
|
|
|
|
- (void)setDestinationEndpoint:(MIKMIDIDestinationEndpoint *)endpoint forTrack:(MIKMIDITrack *)track
|
|
{
|
|
[self setCommandScheduler:endpoint forTrack:track];
|
|
}
|
|
|
|
- (MIKMIDIDestinationEndpoint *)destinationEndpointForTrack:(MIKMIDITrack *)track
|
|
{
|
|
id<MIKMIDICommandScheduler> commandScheduler = [self commandSchedulerForTrack:track];
|
|
return [commandScheduler isKindOfClass:[MIKMIDIDestinationEndpoint class]] ? commandScheduler : nil;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
@implementation MIKMIDISequencer (MIKMIDIPrivate)
|
|
|
|
- (void)dispatchSyncToProcessingQueueAsNeeded:(void (^)(void))block
|
|
{
|
|
if (!block) return;
|
|
|
|
dispatch_queue_t processingQueue = self.processingQueue;
|
|
if (processingQueue && dispatch_get_specific(_processingQueueKey) != _processingQueueContext) {
|
|
dispatch_sync(processingQueue, block);
|
|
} else {
|
|
block();
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
#pragma mark -
|
|
@implementation MIKMIDIEventWithDestination
|
|
|
|
+ (instancetype)eventWithDestination:(id<MIKMIDICommandScheduler>)destination event:(MIKMIDIEvent *)event
|
|
{
|
|
return [self eventWithDestination:destination event:event representsNoteOff:NO];
|
|
}
|
|
|
|
+ (instancetype)eventWithDestination:(id<MIKMIDICommandScheduler>)destination event:(MIKMIDIEvent *)event representsNoteOff:(BOOL)representsNoteOff
|
|
{
|
|
MIKMIDIEventWithDestination *destinationEvent = [[self alloc] init];
|
|
destinationEvent->_event = event;
|
|
destinationEvent->_destination = destination;
|
|
destinationEvent->_representsNoteOff = representsNoteOff;
|
|
return destinationEvent;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation MIKMIDICommandWithDestination
|
|
|
|
+ (instancetype)commandWithDestination:(id<MIKMIDICommandScheduler>)destination command:(MIKMIDICommand *)command
|
|
{
|
|
MIKMIDICommandWithDestination *destinationCommand = [[self alloc] init];
|
|
destinationCommand->_destination = destination;
|
|
destinationCommand->_command = command;
|
|
return destinationCommand;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation MIKMIDIPendingNoteOffsForTimeStamp
|
|
|
|
+ (instancetype)pendingNoteOffWithEndTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
MIKMIDIPendingNoteOffsForTimeStamp *noteOff = [[self alloc] init];
|
|
noteOff->_endTimeStamp = endTimeStamp;
|
|
return noteOff;
|
|
}
|
|
|
|
- (NSMutableArray *)noteEventsWithEndTimeStamp
|
|
{
|
|
if (!_noteEventsWithEndTimeStamp) _noteEventsWithEndTimeStamp = [NSMutableArray array];
|
|
return _noteEventsWithEndTimeStamp;
|
|
}
|
|
|
|
@end
|