873 lines
27 KiB
Objective-C
873 lines
27 KiB
Objective-C
//
|
|
// MIKMIDITrack.m
|
|
// MIDI Files Testbed
|
|
//
|
|
// Created by Andrew Madsen on 5/21/14.
|
|
// Copyright (c) 2014 Mixed In Key. All rights reserved.
|
|
//
|
|
|
|
#import "MIKMIDISequence.h"
|
|
#import "MIKMIDITrack.h"
|
|
#import "MIKMIDIEvent.h"
|
|
#import "MIKMIDINoteEvent.h"
|
|
#import "MIKMIDITempoEvent.h"
|
|
#import "MIKMIDIEventIterator.h"
|
|
#import "MIKMIDIDestinationEndpoint.h"
|
|
#import "MIKMIDIErrors.h"
|
|
#import "MIKMIDISequencer+MIKMIDIPrivate.h"
|
|
|
|
|
|
#if !__has_feature(objc_arc)
|
|
#error MIKMIDITrack.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDITrack.m in the Build Phases for this target
|
|
#endif
|
|
|
|
@interface MIKMIDITrack ()
|
|
|
|
@property (weak, nonatomic, nullable) MIKMIDISequence *sequence;
|
|
@property (nonatomic, strong) NSMutableSet *internalEvents;
|
|
@property (nonatomic, strong) NSArray *sortedEventsCache;
|
|
|
|
@property (nonatomic) MusicTimeStamp restoredLength;
|
|
@property (nonatomic) MusicTrackLoopInfo restoredLoopInfo;
|
|
@property (nonatomic) BOOL hasTemporaryLengthAndLoopInfo;
|
|
|
|
@end
|
|
|
|
|
|
@implementation MIKMIDITrack
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)initWithSequence:(MIKMIDISequence *)sequence musicTrack:(MusicTrack)musicTrack
|
|
{
|
|
if (self = [super init]) {
|
|
MusicSequence musicTrackSequence;
|
|
OSStatus err = MusicTrackGetSequence(musicTrack, &musicTrackSequence);
|
|
if (err) NSLog(@"MusicTrackGetSequence() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
|
|
if (musicTrackSequence != sequence.musicSequence) {
|
|
NSLog(@"ERROR: initWithSequence:musicTrack: requires the musicTrack's associated MusicSequence to be the same as sequence's musicSequence property.");
|
|
return nil;
|
|
}
|
|
|
|
_internalEvents = [[NSMutableSet alloc] init];
|
|
_musicTrack = musicTrack;
|
|
_sequence = sequence;
|
|
[self reloadAllEventsFromMusicTrack];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
+ (instancetype)trackWithSequence:(MIKMIDISequence *)sequence musicTrack:(MusicTrack)musicTrack
|
|
{
|
|
return [[self alloc] initWithSequence:sequence musicTrack:musicTrack];
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
#ifdef DEBUG
|
|
@throw [NSException exceptionWithName:NSGenericException reason:@"Invalid initializer." userInfo:nil];
|
|
#endif
|
|
return nil;
|
|
}
|
|
|
|
#pragma mark - Sequencer Synchronization
|
|
|
|
- (void)dispatchSyncToSequencerProcessingQueueAsNeeded:(void (^)(void))block
|
|
{
|
|
if (!block) return;
|
|
|
|
MIKMIDISequencer *sequencer = self.sequence.sequencer;
|
|
if (sequencer) {
|
|
[sequencer dispatchSyncToProcessingQueueAsNeeded:block];
|
|
} else {
|
|
block();
|
|
}
|
|
}
|
|
|
|
#pragma mark - Adding and Removing Events
|
|
|
|
#pragma mark Public
|
|
|
|
- (void)addEvent:(MIKMIDIEvent *)event
|
|
{
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
if (!event) return;
|
|
if ([self.internalEvents containsObject:event]) return; // Don't allow duplicates
|
|
|
|
NSError *error = nil;
|
|
if (![self insertMIDIEventInMusicTrack:event error:&error]) {
|
|
NSLog(@"Error adding %@ to %@: %@", event, self, error);
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return;
|
|
}
|
|
|
|
[self addInternalEventsObject:event];
|
|
}];
|
|
}
|
|
|
|
- (void)addEvents:(NSArray *)events
|
|
{
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
NSMutableSet *scratch = [NSMutableSet setWithArray:events];
|
|
[scratch minusSet:self.internalEvents]; // Don't allow duplicates
|
|
if (![scratch count]) return;
|
|
|
|
NSError *error = nil;
|
|
for (MIKMIDIEvent *event in scratch) {
|
|
if (![self insertMIDIEventInMusicTrack:event error:&error]) {
|
|
NSLog(@"Error adding %@ to %@: %@", event, self, error);
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return;
|
|
}
|
|
}
|
|
|
|
[self addInternalEvents:scratch];
|
|
}];
|
|
}
|
|
|
|
- (void)removeEvent:(MIKMIDIEvent *)event
|
|
{
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
if (!event) return;
|
|
if (![self.internalEvents containsObject:event]) return;
|
|
|
|
NSError *error = nil;
|
|
if (![self removeMIDIEventsFromMusicTrack:[NSSet setWithObject:event] error:&error]) {
|
|
NSLog(@"Error removing %@ from %@: %@", event, self, error);
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return;
|
|
}
|
|
|
|
[self removeInternalEventsObject:event];
|
|
}];
|
|
}
|
|
|
|
- (void)removeEvents:(NSArray *)events
|
|
{
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
if (![events count]) return;
|
|
NSMutableSet *scratch = [NSMutableSet setWithArray:events];
|
|
[scratch intersectSet:self.internalEvents];
|
|
|
|
NSError *error = nil;
|
|
if (![self removeMIDIEventsFromMusicTrack:scratch error:&error]) {
|
|
NSLog(@"Error removing %@ from %@: %@", events, self, error);
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return;
|
|
}
|
|
|
|
[self removeInternalEvents:scratch];
|
|
}];
|
|
}
|
|
|
|
- (void)removeAllEvents
|
|
{
|
|
[self clearEventsFromStartingTimeStamp:0 toEndingTimeStamp:kMusicTimeStamp_EndOfTrack];
|
|
}
|
|
|
|
#pragma mark Private
|
|
|
|
- (BOOL)insertMIDIEventInMusicTrack:(MIKMIDIEvent *)event error:(NSError **)error
|
|
{
|
|
error = error ? error : &(NSError *__autoreleasing){ nil };
|
|
|
|
OSStatus err = noErr;
|
|
MusicTrack track = self.musicTrack;
|
|
MusicTimeStamp timeStamp = event.timeStamp;
|
|
const void *data = [event.data bytes];
|
|
|
|
switch (event.eventType) {
|
|
case MIKMIDIEventTypeNULL:
|
|
NSLog(@"Warning: %s attempted to insert NULL event.", __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeExtendedNote:
|
|
err = MusicTrackNewExtendedNoteEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewExtendedNoteEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
#if !TARGET_OS_IPHONE // Unavailable altogether on iOS.
|
|
case MIKMIDIEventTypeExtendedControl:
|
|
NSLog(@"Events of type MIKMIDIEventTypeExtendedControl are unsupported because the underlying CoreMIDI API is deprecated.");
|
|
break;
|
|
#endif
|
|
|
|
case MIKMIDIEventTypeExtendedTempo:
|
|
err = MusicTrackNewExtendedTempoEvent(track, timeStamp, ((ExtendedTempoEvent *)data)->bpm);
|
|
if (err) NSLog(@"MusicTrackNewExtendedTempoEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeUser:
|
|
err = MusicTrackNewUserEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewUserEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeMIDINoteMessage:
|
|
err = MusicTrackNewMIDINoteEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewMIDINoteEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeMIDIRawData:
|
|
err = MusicTrackNewMIDIRawDataEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewMIDIRawDataEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeParameter:
|
|
err = MusicTrackNewParameterEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewParameterEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeAUPreset:
|
|
err = MusicTrackNewAUPresetEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewAUPresetEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeMIDIChannelMessage:
|
|
case MIKMIDIEventTypeMIDIPolyphonicKeyPressureMessage:
|
|
case MIKMIDIEventTypeMIDIControlChangeMessage:
|
|
case MIKMIDIEventTypeMIDIProgramChangeMessage:
|
|
case MIKMIDIEventTypeMIDIChannelPressureMessage:
|
|
case MIKMIDIEventTypeMIDIPitchBendChangeMessage:
|
|
err = MusicTrackNewMIDIChannelEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewMIDIChannelEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
|
|
case MIKMIDIEventTypeMeta:
|
|
case MIKMIDIEventTypeMetaSequence:
|
|
case MIKMIDIEventTypeMetaText:
|
|
case MIKMIDIEventTypeMetaCopyright:
|
|
case MIKMIDIEventTypeMetaTrackSequenceName:
|
|
case MIKMIDIEventTypeMetaInstrumentName:
|
|
case MIKMIDIEventTypeMetaLyricText:
|
|
case MIKMIDIEventTypeMetaMarkerText:
|
|
case MIKMIDIEventTypeMetaCuePoint:
|
|
case MIKMIDIEventTypeMetaMIDIChannelPrefix:
|
|
case MIKMIDIEventTypeMetaEndOfTrack:
|
|
case MIKMIDIEventTypeMetaTempoSetting:
|
|
case MIKMIDIEventTypeMetaSMPTEOffset:
|
|
case MIKMIDIEventTypeMetaTimeSignature:
|
|
case MIKMIDIEventTypeMetaKeySignature:
|
|
case MIKMIDIEventTypeMetaSequenceSpecificEvent:
|
|
err = MusicTrackNewMetaEvent(track, timeStamp, data);
|
|
if (err) NSLog(@"MusicTrackNewMetaEvent() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
break;
|
|
default:
|
|
err = -1;
|
|
NSLog(@"Warning: %s attempted to insert unknown event type %@.", __PRETTY_FUNCTION__, @(event.eventType));
|
|
break;
|
|
}
|
|
|
|
if (err != noErr) {
|
|
*error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil];
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)removeMIDIEventsFromMusicTrack:(NSSet *)events error:(NSError **)error
|
|
{
|
|
error = error ? error : &(NSError *__autoreleasing){ nil };
|
|
if (![events count]) return YES;
|
|
|
|
// MusicTrackClear() doesn't reliably clear events that fall on its boundaries,
|
|
// so we iterate the track and delete that way instead
|
|
BOOL success = NO;
|
|
MIKMIDIEventIterator *iterator = [MIKMIDIEventIterator iteratorForTrack:self];
|
|
while (iterator.hasCurrentEvent) {
|
|
MIKMIDIEvent *currentEvent = iterator.currentEvent;
|
|
if ([events containsObject:currentEvent]) {
|
|
if (![iterator deleteCurrentEventWithError:error]) return NO;
|
|
success = YES;
|
|
continue; // Move to next event is done by delete.
|
|
}
|
|
|
|
[iterator moveToNextEvent];
|
|
}
|
|
|
|
*error = [NSError MIKMIDIErrorWithCode:MIKMIDITrackEventNotFoundErrorCode userInfo:nil];
|
|
return success;
|
|
}
|
|
|
|
#pragma mark - Getting Events
|
|
|
|
#pragma mark Public
|
|
|
|
// All public event getters pass through this method
|
|
- (NSArray *)eventsOfClass:(Class)eventClass fromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
NSMutableArray *result = [NSMutableArray array];
|
|
for (MIKMIDIEvent *event in self.events) {
|
|
if (event.timeStamp < startTimeStamp) { continue; }
|
|
if (event.timeStamp > endTimeStamp) { break; }
|
|
if (eventClass && ![event isKindOfClass:eventClass]) { continue; }
|
|
[result addObject:event];
|
|
}
|
|
return [result copy];
|
|
}
|
|
|
|
- (NSArray *)eventsFromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
return [self eventsOfClass:[MIKMIDIEvent class] fromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp];
|
|
}
|
|
|
|
- (NSArray *)notesFromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
return [self eventsOfClass:[MIKMIDINoteEvent class] fromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp];
|
|
}
|
|
|
|
#pragma mark Private
|
|
|
|
- (void)reloadAllEventsFromMusicTrack
|
|
{
|
|
MIKMIDIEventIterator *iterator = [MIKMIDIEventIterator iteratorForTrack:self];
|
|
NSMutableSet *allEvents = [NSMutableSet set];
|
|
while (iterator.hasCurrentEvent) {
|
|
MIKMIDIEvent *event = iterator.currentEvent;
|
|
[allEvents addObject:event];
|
|
[iterator moveToNextEvent];
|
|
}
|
|
|
|
[self willChangeValueForKey:@"internalEvents"];
|
|
[self.internalEvents intersectSet:allEvents];
|
|
[self.internalEvents unionSet:allEvents];
|
|
[self didChangeValueForKey:@"internalEvents"];
|
|
|
|
self.sortedEventsCache = nil;
|
|
}
|
|
|
|
#pragma mark - Editing Events (Public)
|
|
|
|
- (BOOL)moveEventsFromStartingTimeStamp:(MusicTimeStamp)startTimeStamp toEndingTimeStamp:(MusicTimeStamp)endTimeStamp byAmount:(MusicTimeStamp)timestampOffset
|
|
{
|
|
__block BOOL success = NO;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
success = [self private_moveEventsFromStartingTimeStamp:startTimeStamp toEndingTimeStamp:endTimeStamp byAmount:timestampOffset];
|
|
}];
|
|
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)private_moveEventsFromStartingTimeStamp:(MusicTimeStamp)startTimeStamp toEndingTimeStamp:(MusicTimeStamp)endTimeStamp byAmount:(MusicTimeStamp)timestampOffset
|
|
{
|
|
// MusicTrackMoveEvents() fails in common edge cases, so iterate the track and move that way instead
|
|
|
|
if (timestampOffset == 0) return YES; // Nothing needs to be done
|
|
MusicTimeStamp length = self.length;
|
|
if (!length || (startTimeStamp > length) || ![self.internalEvents count]) return YES;
|
|
if (endTimeStamp > length) endTimeStamp = length;
|
|
|
|
NSMutableSet *eventsToMove = [NSMutableSet setWithArray:[self eventsFromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp]];
|
|
NSSet *eventsBeforeMoving = [eventsToMove copy];
|
|
NSMutableSet *eventsAfterMoving = [NSMutableSet set];
|
|
|
|
MIKMIDIEventIterator *iterator = [MIKMIDIEventIterator iteratorForTrack:self];
|
|
while (iterator.hasCurrentEvent && [eventsToMove count] > 0) {
|
|
MIKMIDIEvent *currentEvent = iterator.currentEvent;
|
|
if (![eventsToMove containsObject:currentEvent]) {
|
|
[iterator moveToNextEvent];
|
|
continue;
|
|
}
|
|
|
|
MusicTimeStamp timestamp = currentEvent.timeStamp;
|
|
if (![iterator moveCurrentEventTo:timestamp+timestampOffset error:NULL]) {
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return NO;
|
|
}
|
|
MIKMutableMIDIEvent *movedEvent = [currentEvent mutableCopy];
|
|
movedEvent.timeStamp += timestampOffset;
|
|
[eventsAfterMoving addObject:[movedEvent copy]];
|
|
[eventsToMove removeObject:currentEvent];
|
|
[iterator seek:timestamp]; // Move back to previous position
|
|
}
|
|
|
|
[self removeInternalEvents:eventsBeforeMoving];
|
|
[self addInternalEvents:eventsAfterMoving];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)clearEventsFromStartingTimeStamp:(MusicTimeStamp)startTimeStamp toEndingTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
__block BOOL success = NO;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
success = [self private_clearEventsFromStartingTimeStamp:startTimeStamp toEndingTimeStamp:endTimeStamp];
|
|
}];
|
|
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)private_clearEventsFromStartingTimeStamp:(MusicTimeStamp)startTimeStamp toEndingTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
NSSet *events = [NSSet setWithArray:[self eventsFromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp]];
|
|
BOOL success = [self removeMIDIEventsFromMusicTrack:events error:NULL];
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)cutEventsFromStartingTimeStamp:(MusicTimeStamp)startTimeStamp toEndingTimeStamp:(MusicTimeStamp)endTimeStamp
|
|
{
|
|
__block BOOL success = NO;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
MusicTimeStamp length = self.length;
|
|
if (!length || (startTimeStamp > length) || ![self.internalEvents count]) { success = YES; return; }
|
|
|
|
MusicTimeStamp actualEndTimeStamp = endTimeStamp;
|
|
if (actualEndTimeStamp > length) actualEndTimeStamp = length;
|
|
|
|
if (![self private_clearEventsFromStartingTimeStamp:startTimeStamp toEndingTimeStamp:actualEndTimeStamp]) return;
|
|
MusicTimeStamp cutAmount = actualEndTimeStamp - startTimeStamp;
|
|
success = [self private_moveEventsFromStartingTimeStamp:actualEndTimeStamp toEndingTimeStamp:kMusicTimeStamp_EndOfTrack byAmount:-cutAmount];
|
|
}];
|
|
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)copyEventsFromMIDITrack:(MIKMIDITrack *)origTrack fromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp andInsertAtTimeStamp:(MusicTimeStamp)destTimeStamp
|
|
{
|
|
__block BOOL success = NO;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
// Move existing events to make room for new events
|
|
if (![self private_moveEventsFromStartingTimeStamp:destTimeStamp
|
|
toEndingTimeStamp:kMusicTimeStamp_EndOfTrack
|
|
byAmount:(endTimeStamp - startTimeStamp)]) return;
|
|
|
|
success = [self private_mergeEventsFromMIDITrack:origTrack fromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp atTimeStamp:destTimeStamp];
|
|
}];
|
|
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)mergeEventsFromMIDITrack:(MIKMIDITrack *)origTrack fromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp atTimeStamp:(MusicTimeStamp)destTimeStamp
|
|
{
|
|
__block BOOL success = NO;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
success = [self private_mergeEventsFromMIDITrack:origTrack fromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp atTimeStamp:destTimeStamp];
|
|
}];
|
|
|
|
return success;
|
|
}
|
|
|
|
- (BOOL)private_mergeEventsFromMIDITrack:(MIKMIDITrack *)origTrack fromTimeStamp:(MusicTimeStamp)startTimeStamp toTimeStamp:(MusicTimeStamp)endTimeStamp atTimeStamp:(MusicTimeStamp)destTimeStamp
|
|
{
|
|
NSArray *sourceEvents = [origTrack eventsFromTimeStamp:startTimeStamp toTimeStamp:endTimeStamp];
|
|
if (![sourceEvents count]) return YES;
|
|
|
|
MusicTimeStamp firstSourceTimeStamp = [[sourceEvents firstObject] timeStamp];
|
|
|
|
NSMutableSet *destinationEvents = [NSMutableSet set];
|
|
for (MIKMIDIEvent *event in sourceEvents) {
|
|
MIKMutableMIDIEvent *mutableEvent = [event mutableCopy];
|
|
mutableEvent.timeStamp = destTimeStamp + (event.timeStamp - firstSourceTimeStamp);
|
|
if (![self insertMIDIEventInMusicTrack:mutableEvent error:NULL]) {
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return NO;
|
|
}
|
|
[destinationEvents addObject:mutableEvent];
|
|
}
|
|
|
|
[self addInternalEvents:destinationEvents];
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Temporary Length and Loop Info
|
|
|
|
- (void)setTemporaryLength:(MusicTimeStamp)length andLoopInfo:(MusicTrackLoopInfo)loopInfo
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
self.restoredLength = self.length;
|
|
self.restoredLoopInfo = self.loopInfo;
|
|
self.length = length;
|
|
self.loopInfo = loopInfo;
|
|
self.hasTemporaryLengthAndLoopInfo = YES;
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)restoreLengthAndLoopInfo
|
|
{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
if (!self.hasTemporaryLengthAndLoopInfo) return;
|
|
|
|
self.hasTemporaryLengthAndLoopInfo = NO;
|
|
self.length = self.restoredLength;
|
|
self.loopInfo = self.restoredLoopInfo;
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingEvents
|
|
{
|
|
return [NSSet setWithObjects:@"sortedEventsCache", nil];
|
|
}
|
|
|
|
- (NSArray *)events
|
|
{
|
|
__block NSArray *events;
|
|
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
if (!self.sortedEventsCache) {
|
|
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:YES];
|
|
self->_sortedEventsCache = [self.internalEvents sortedArrayUsingDescriptors:@[sortDescriptor]];
|
|
}
|
|
events = self.sortedEventsCache;
|
|
}];
|
|
|
|
return events ?: @[];
|
|
}
|
|
|
|
- (void)setEvents:(NSArray *)events
|
|
{
|
|
[self clearEventsFromStartingTimeStamp:0 toEndingTimeStamp:self.length];
|
|
[self dispatchSyncToSequencerProcessingQueueAsNeeded:^{
|
|
for (MIKMIDIEvent *event in events) {
|
|
NSError *error = nil;
|
|
if (![self insertMIDIEventInMusicTrack:event error:&error]) {
|
|
NSLog(@"Error adding %@ to %@: %@", event, self, error);
|
|
[self reloadAllEventsFromMusicTrack];
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.internalEvents = [NSMutableSet setWithArray:events];
|
|
}];
|
|
}
|
|
|
|
- (void)setInternalEvents:(NSMutableSet *)internalEvents
|
|
{
|
|
if (internalEvents != _internalEvents) {
|
|
_internalEvents = internalEvents;
|
|
self.sortedEventsCache = nil;
|
|
}
|
|
}
|
|
|
|
- (void)addInternalEventsObject:(MIKMIDIEvent *)event
|
|
{
|
|
[self.internalEvents addObject:[event copy]];
|
|
self.sortedEventsCache = nil;
|
|
}
|
|
|
|
- (void)addInternalEvents:(NSSet *)events
|
|
{
|
|
for (MIKMIDIEvent *event in events) {
|
|
[self addInternalEventsObject:[event copy]];
|
|
}
|
|
}
|
|
|
|
- (void)removeInternalEventsObject:(MIKMIDIEvent *)event
|
|
{
|
|
[self.internalEvents removeObject:event];
|
|
self.sortedEventsCache = nil;
|
|
}
|
|
|
|
- (void)removeInternalEvents:(NSSet *)events
|
|
{
|
|
[self.internalEvents minusSet:events];
|
|
self.sortedEventsCache = nil;
|
|
}
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingNotes
|
|
{
|
|
return [NSSet setWithObjects:@"sortedEventsCache", nil];
|
|
}
|
|
|
|
- (NSArray *)notes
|
|
{
|
|
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *b) {
|
|
return [(MIKMIDIEvent *)obj eventType] == MIKMIDIEventTypeMIDINoteMessage;
|
|
}];
|
|
return [self.events filteredArrayUsingPredicate:predicate];
|
|
}
|
|
|
|
- (NSInteger)trackNumber
|
|
{
|
|
__strong MIKMIDISequence *sequence = self.sequence;
|
|
if (!sequence) return -1;
|
|
UInt32 trackNumber = 0;
|
|
OSStatus err = MusicSequenceGetTrackIndex(sequence.musicSequence, self.musicTrack, &trackNumber);
|
|
if (err) {
|
|
NSLog(@"MusicSequenceGetTrackIndex() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return -1;
|
|
}
|
|
return (NSInteger)trackNumber;
|
|
}
|
|
|
|
@synthesize offset = _offset;
|
|
|
|
- (MusicTimeStamp)offset
|
|
{
|
|
if (_offset != 0) return _offset;
|
|
|
|
if (self.musicTrack) {
|
|
MusicTimeStamp offset = 0;
|
|
UInt32 offsetLength = sizeof(offset);
|
|
OSStatus err = MusicTrackGetProperty(self.musicTrack, kSequenceTrackProperty_OffsetTime, &offset, &offsetLength);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return offset;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
- (void)setOffset:(MusicTimeStamp)offset
|
|
{
|
|
_offset = offset;
|
|
|
|
if (self.musicTrack) {
|
|
OSStatus err = MusicTrackSetProperty(self.musicTrack, kSequenceTrackProperty_OffsetTime, &offset, sizeof(offset));
|
|
if (err) NSLog(@"MusicTrackSetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
}
|
|
}
|
|
|
|
@synthesize muted = _muted;
|
|
|
|
- (BOOL)isMuted
|
|
{
|
|
if (_muted) return YES;
|
|
|
|
if (self.musicTrack) {
|
|
Boolean isMuted = FALSE;
|
|
UInt32 isMutedLength = sizeof(isMuted);
|
|
OSStatus err = MusicTrackGetProperty(self.musicTrack, kSequenceTrackProperty_MuteStatus, &isMuted, &isMutedLength);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return isMuted ? YES : NO;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (void)setMuted:(BOOL)muted
|
|
{
|
|
_muted = muted;
|
|
|
|
if (self.musicTrack) {
|
|
Boolean mutedBoolean = muted ? TRUE : FALSE;
|
|
OSStatus err = MusicTrackSetProperty(self.musicTrack, kSequenceTrackProperty_MuteStatus, &mutedBoolean, sizeof(mutedBoolean));
|
|
if (err) NSLog(@"MusicTrackSetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
}
|
|
}
|
|
|
|
@synthesize solo = _solo;
|
|
|
|
- (BOOL)isSolo
|
|
{
|
|
if (_solo) return YES;
|
|
|
|
if (self.musicTrack) {
|
|
Boolean isSolo = FALSE;
|
|
UInt32 isSoloLength = sizeof(isSolo);
|
|
OSStatus err = MusicTrackGetProperty(self.musicTrack, kSequenceTrackProperty_SoloStatus, &isSolo, &isSoloLength);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return isSolo ? YES : NO;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (void)setSolo:(BOOL)solo
|
|
{
|
|
_solo = solo;
|
|
|
|
if (self.musicTrack) {
|
|
Boolean soloBoolean = solo ? TRUE : FALSE;
|
|
OSStatus err = MusicTrackSetProperty(self.musicTrack, kSequenceTrackProperty_SoloStatus, &soloBoolean, sizeof(soloBoolean));
|
|
if (err) NSLog(@"MusicTrackSetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
}
|
|
}
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingLength
|
|
{
|
|
return [NSSet setWithObjects:@"sortedEventsCache", nil];
|
|
}
|
|
|
|
- (MusicTimeStamp)length
|
|
{
|
|
if (_length == -1) {
|
|
MusicTimeStamp lastStamp = 0;
|
|
|
|
for (MIKMIDIEvent *event in self.events) {
|
|
MusicTimeStamp endStamp = [event respondsToSelector:@selector(endTimeStamp)] ? [(MIKMIDINoteEvent *)event endTimeStamp] : event.timeStamp;
|
|
if (endStamp > lastStamp) lastStamp = endStamp;
|
|
}
|
|
|
|
_length = lastStamp;
|
|
}
|
|
|
|
return _length;
|
|
}
|
|
|
|
- (void)setSortedEventsCache:(NSArray *)sortedEventsCache
|
|
{
|
|
_sortedEventsCache = sortedEventsCache;
|
|
_length = -1;
|
|
}
|
|
|
|
- (SInt16)timeResolution
|
|
{
|
|
SInt16 resolution = 0;
|
|
UInt32 resolutionLength = sizeof(resolution);
|
|
OSStatus err = MusicTrackGetProperty(self.musicTrack, kSequenceTrackProperty_TimeResolution, &resolution, &resolutionLength);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return resolution;
|
|
}
|
|
|
|
#pragma mark - Deprecated
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingDoesLoop
|
|
{
|
|
return [NSSet setWithObjects:@"loopDuration", nil];
|
|
}
|
|
|
|
- (BOOL)doesLoop
|
|
{
|
|
return self.loopDuration > 0;
|
|
}
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingNumberOfLoops
|
|
{
|
|
return [NSSet setWithObjects:@"loopInfo", nil];
|
|
}
|
|
|
|
- (SInt32)numberOfLoops
|
|
{
|
|
return self.loopInfo.numberOfLoops;
|
|
}
|
|
|
|
- (void)setNumberOfLoops:(SInt32)numberOfLoops
|
|
{
|
|
MusicTrackLoopInfo loopInfo = self.loopInfo;
|
|
|
|
if (loopInfo.numberOfLoops != numberOfLoops) {
|
|
loopInfo.numberOfLoops = numberOfLoops;
|
|
self.loopInfo = loopInfo;
|
|
}
|
|
}
|
|
|
|
+ (NSSet *)keyPathsForValuesAffectingLoopDuration
|
|
{
|
|
return [NSSet setWithObjects:@"loopInfo", nil];
|
|
}
|
|
|
|
- (MusicTimeStamp)loopDuration
|
|
{
|
|
return self.loopInfo.loopDuration;
|
|
}
|
|
|
|
- (void)setLoopDuration:(MusicTimeStamp)loopDuration
|
|
{
|
|
MusicTrackLoopInfo loopInfo = self.loopInfo;
|
|
|
|
if (loopInfo.loopDuration != loopDuration) {
|
|
loopInfo.loopDuration = loopDuration;
|
|
self.loopInfo = loopInfo;
|
|
}
|
|
}
|
|
|
|
- (MusicTrackLoopInfo)loopInfo
|
|
{
|
|
MusicTrackLoopInfo info;
|
|
UInt32 infoSize = sizeof(info);
|
|
OSStatus err = MusicTrackGetProperty(self.musicTrack, kSequenceTrackProperty_LoopInfo, &info, &infoSize);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
return info;
|
|
}
|
|
|
|
- (void)setLoopInfo:(MusicTrackLoopInfo)loopInfo
|
|
{
|
|
OSStatus err = MusicTrackSetProperty(self.musicTrack, kSequenceTrackProperty_LoopInfo, &loopInfo, sizeof(loopInfo));
|
|
if (err) NSLog(@"MusicTrackSetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
}
|
|
|
|
- (BOOL)getTrackNumber:(UInt32 *)trackNumber
|
|
{
|
|
static BOOL deprectionMsgShown = NO;
|
|
if (!deprectionMsgShown) {
|
|
NSLog(@"WARNING: %s has been deprecated. Please use -trackNumber instead. This message will only be logged once", __PRETTY_FUNCTION__);
|
|
deprectionMsgShown = YES;
|
|
}
|
|
NSInteger result = self.trackNumber;
|
|
*trackNumber = (UInt32)result;
|
|
return (result >= 0);
|
|
}
|
|
|
|
@synthesize destinationEndpoint = _destinationEndpoint;
|
|
|
|
- (MIKMIDIDestinationEndpoint *)destinationEndpoint
|
|
{
|
|
NSLog(@"%s is deprecated. You should update your code to avoid calling this method. Use MIKMIDISequencer's API instead.", __PRETTY_FUNCTION__);
|
|
return _destinationEndpoint;
|
|
}
|
|
|
|
- (void)setDestinationEndpoint:(MIKMIDIDestinationEndpoint *)destinationEndpoint
|
|
{
|
|
NSLog(@"%s is deprecated. You should update your code to avoid calling this method. Use MIKMIDISequencer's API instead.", __PRETTY_FUNCTION__);
|
|
|
|
if (destinationEndpoint != _destinationEndpoint) {
|
|
OSStatus err = MusicTrackSetDestMIDIEndpoint(self.musicTrack, (MIDIEndpointRef)destinationEndpoint.objectRef);
|
|
if (err) NSLog(@"MusicTrackGetProperty() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
|
|
_destinationEndpoint = destinationEndpoint;
|
|
}
|
|
}
|
|
|
|
- (BOOL)insertMIDIEvent:(MIKMIDIEvent *)event
|
|
{
|
|
static BOOL deprectionMsgShown = NO;
|
|
if (!deprectionMsgShown) {
|
|
NSLog(@"WARNING: %s has been deprecated. Please use -addEvent: instead. This message will only be logged once.", __PRETTY_FUNCTION__);
|
|
deprectionMsgShown = YES;
|
|
}
|
|
|
|
[self addEvent:event];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)insertMIDIEvents:(NSSet *)events
|
|
{
|
|
static BOOL deprectionMsgShown = NO;
|
|
if (!deprectionMsgShown) {
|
|
NSLog(@"WARNING: %s has been deprecated. Please use -addEvent: instead. This message will only be logged once.", __PRETTY_FUNCTION__);
|
|
deprectionMsgShown = YES;
|
|
}
|
|
|
|
for (MIKMIDIEvent *event in events) {
|
|
[self addEvent:event];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)removeMIDIEvents:(NSSet *)events
|
|
{
|
|
static BOOL deprectionMsgShown = NO;
|
|
if (!deprectionMsgShown) {
|
|
NSLog(@"WARNING: %s has been deprecated. Please use -removeEvent: instead. This message will only be logged once.", __PRETTY_FUNCTION__);
|
|
deprectionMsgShown = YES;
|
|
}
|
|
|
|
for (MIKMIDIEvent *event in events) {
|
|
[self removeEvent:event];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)clearAllEvents
|
|
{
|
|
static BOOL deprectionMsgShown = NO;
|
|
if (!deprectionMsgShown) {
|
|
NSLog(@"WARNING: %s has been deprecated. Please use -removeAllEvents instead. This message will only be logged once.", __PRETTY_FUNCTION__);
|
|
deprectionMsgShown = YES;
|
|
}
|
|
|
|
[self removeAllEvents];
|
|
return YES;
|
|
}
|
|
|
|
@end
|