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;