diff --git a/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m b/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m index d0226106..ee206907 100644 --- a/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m +++ b/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m @@ -12,6 +12,9 @@ @interface MIKMIDISequenceTests : XCTestCase +@property (nonatomic, strong) MIKMIDISequence *sequence; +@property (nonatomic, strong) NSMutableSet *receivedNotificationKeyPaths; + @end @implementation MIKMIDISequenceTests @@ -19,12 +22,16 @@ @implementation MIKMIDISequenceTests - (void)setUp { [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. + + self.receivedNotificationKeyPaths = [NSMutableSet set]; + self.sequence = [MIKMIDISequence sequence]; + [self.sequence addObserver:self forKeyPath:@"tracks" options:0 context:NULL]; } - (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. + [self.sequence removeObserver:self forKeyPath:@"tracks"]; + [super tearDown]; } @@ -54,4 +61,35 @@ - (void)testMIDIFileReadPerformance }]; } +- (void)testKVOForAddingATrack +{ + XCTAssertNotNil(self.sequence); + + MIKMIDITrack *firstTrack = [self.sequence addTrack]; + XCTAssertNotNil(firstTrack, @"Creating an MIKMIDITrack failed."); + XCTAssertTrue([self.receivedNotificationKeyPaths containsObject:@"tracks"], @"KVO notification when adding a track not received."); +} + +- (void)testKVOForRemovingATrack +{ + MIKMIDITrack *firstTrack = [self.sequence addTrack]; + XCTAssertNotNil(firstTrack, @"Creating an MIKMIDITrack failed."); + MIKMIDITrack *secondTrack = [self.sequence addTrack]; + XCTAssertNotNil(secondTrack, @"Creating an MIKMIDITrack failed."); + + [self.receivedNotificationKeyPaths removeAllObjects]; + [self.sequence removeTrack:firstTrack]; + XCTAssertTrue([self.receivedNotificationKeyPaths containsObject:@"tracks"], @"KVO notification when removing a track not received."); + XCTAssertEqualObjects(self.sequence.tracks, @[secondTrack], @"Removing a track failed."); +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (object == self.sequence) { + [self.receivedNotificationKeyPaths addObject:keyPath]; + } +} + @end diff --git a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist index 0055c744..7efa07a8 100644 --- a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist +++ b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C1C9286E-763A-4210-B3E7-DF2205A6AA20.plist @@ -11,7 +11,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 1.38 + 0.14 baselineIntegrationDisplayName Local Baseline maxPercentRelativeStandardDeviation diff --git a/Source/MIKMIDISequence.h b/Source/MIKMIDISequence.h index 53b66ceb..a9494c03 100644 --- a/Source/MIKMIDISequence.h +++ b/Source/MIKMIDISequence.h @@ -153,6 +153,8 @@ NS_INLINE MIKMIDITimeSignature MIKMIDITimeSignatureMake(UInt8 numerator, UInt8 d */ - (BOOL)writeToURL:(NSURL *)fileURL error:(NSError **)error; +#pragma mark - Track Management + /** * Creates and adds a new MIDI track to the sequence. */ @@ -167,18 +169,7 @@ NS_INLINE MIKMIDITimeSignature MIKMIDITimeSignatureMake(UInt8 numerator, UInt8 d */ - (BOOL)removeTrack:(MIKMIDITrack *)track; -/** - * A MusicTimeStamp that is less than the sequence's length, but is at an equivalent position in the looped sequence as loopedTimeStamp - * - * When the music sequence is being looped by an MIKMIDIPlayer, the time stamp of the player continuosly increases. This method can be - * used to find where in the MIDI sequence the looped playback is at. For example, in a sequence with a length of 16, - * calling this method with a loopedTimeStamp of 17 would return 1. - * - * @param loopedTimeStamp The time stamp that you would like an equivalent time stamp for. - * - * @return The MusicTimeStamp of the sequence that is in an equivalent position in the sequence as loopedTimeStamp. - */ -- (MusicTimeStamp)equivalentTimeStampForLoopedTimeStamp:(MusicTimeStamp)loopedTimeStamp; +#pragma mark - Tempo & Time Signature /** * Returns an array of MIKMIDIEvent from the tempo track. @@ -274,7 +265,22 @@ NS_INLINE MIKMIDITimeSignature MIKMIDITimeSignatureMake(UInt8 numerator, UInt8 d */ - (MIKMIDITimeSignature)timeSignatureAtTimeStamp:(MusicTimeStamp)timeStamp; -// Properties +#pragma mark - Timing + +/** + * A MusicTimeStamp that is less than the sequence's length, but is at an equivalent position in the looped sequence as loopedTimeStamp + * + * When the music sequence is being looped by an MIKMIDIPlayer, the time stamp of the player continuosly increases. This method can be + * used to find where in the MIDI sequence the looped playback is at. For example, in a sequence with a length of 16, + * calling this method with a loopedTimeStamp of 17 would return 1. + * + * @param loopedTimeStamp The time stamp that you would like an equivalent time stamp for. + * + * @return The MusicTimeStamp of the sequence that is in an equivalent position in the sequence as loopedTimeStamp. + */ +- (MusicTimeStamp)equivalentTimeStampForLoopedTimeStamp:(MusicTimeStamp)loopedTimeStamp; + +#pragma mark - Properties /** * The tempo track for the sequence. Even in a new, empty sequence, diff --git a/Source/MIKMIDISequence.m b/Source/MIKMIDISequence.m index 14c72c60..d7c2a66e 100644 --- a/Source/MIKMIDISequence.m +++ b/Source/MIKMIDISequence.m @@ -23,8 +23,8 @@ @interface MIKMIDISequence () @property (nonatomic) MusicSequence musicSequence; -@property (strong, nonatomic) MIKMIDITrack *tempoTrack; -@property (strong, nonatomic) NSArray *tracks; +@property (nonatomic, strong) MIKMIDITrack *tempoTrack; +@property (nonatomic, strong) NSMutableArray *internalTracks; @end @@ -155,7 +155,7 @@ - (instancetype)initWithMusicSequence:(MusicSequence)musicSequence error:(NSErro } [tracks addObject:[MIKMIDITrack trackWithSequence:self musicTrack:musicTrack]]; } - self.tracks = tracks; + self.internalTracks = tracks; self.length = MIKMIDISequenceLongestTrackLength; } @@ -181,27 +181,22 @@ - (MIKMIDITrack *)addTrack } MIKMIDITrack *track = [MIKMIDITrack trackWithSequence:self musicTrack:musicTrack]; - - if (track) { - NSMutableArray *tracks = [self.tracks mutableCopy]; - [tracks addObject:track]; - self.tracks = tracks; - } + [self addTracksObject:track]; return track; } - (BOOL)removeTrack:(MIKMIDITrack *)track { + if (!track) return NO; + OSStatus err = MusicSequenceDisposeTrack(self.musicSequence, track.musicTrack); if (err) { NSLog(@"MusicSequenceDisposeTrack() failed with error %d in %s.", err, __PRETTY_FUNCTION__); return NO; } - NSMutableArray *tracks = [self.tracks mutableCopy]; - [tracks removeObject:track]; - self.tracks = tracks; + [self removeTracksObject:track]; return YES; } @@ -324,6 +319,42 @@ - (NSString *)description #pragma mark - Properties ++ (NSSet *)keyPathsForValuesAffectingTracks +{ + return [NSSet setWithObjects:@"internalTracks", nil]; +} + +- (NSArray *)tracks +{ + return [self.internalTracks copy]; +} + +- (void)addTracksObject:(MIKMIDITrack *)track +{ + if (!track) return; + [self insertObject:track inTracksAtIndex:[self.internalTracks count]]; +} + +- (void)insertObject:(MIKMIDITrack *)track inTracksAtIndex:(NSUInteger)index +{ + if (!track) return; + [self.internalTracks insertObject:track atIndex:index]; +} + +- (void)removeTracksObject:(MIKMIDITrack *)track +{ + if (!track) return; + NSInteger index = [self.internalTracks indexOfObject:track]; + if (index == NSNotFound) return; + [self removeObjectFromInternalTracksAtIndex:index]; +} + +- (void)removeObjectFromInternalTracksAtIndex:(NSUInteger)index +{ + if (index >= [self.internalTracks count]) return; + [self.internalTracks removeObjectAtIndex:index]; +} + - (MusicTimeStamp)length { if (_length != MIKMIDISequenceLongestTrackLength) return _length;