diff --git a/Examples/macOS/MIDI Files Testbed/MIDI Files Testbed.xcodeproj/project.pbxproj b/Examples/macOS/MIDI Files Testbed/MIDI Files Testbed.xcodeproj/project.pbxproj index 4ab83e8b..fd8bcd6a 100644 --- a/Examples/macOS/MIDI Files Testbed/MIDI Files Testbed.xcodeproj/project.pbxproj +++ b/Examples/macOS/MIDI Files Testbed/MIDI Files Testbed.xcodeproj/project.pbxproj @@ -214,12 +214,12 @@ isa = PBXProject; attributes = { CLASSPREFIX = MIK; - LastUpgradeCheck = 0920; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Mixed In Key"; }; buildConfigurationList = 9DB2A5EE192D184D0047A3EB /* Build configuration list for PBXProject "MIDI Files Testbed" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -343,6 +343,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -351,14 +352,17 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -393,6 +397,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -401,14 +406,17 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -436,6 +444,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Source/MIDI Files Testbed-Prefix.pch"; @@ -451,6 +460,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Source/MIDI Files Testbed-Prefix.pch"; diff --git a/Examples/macOS/MIDI Files Testbed/Resources/Base.lproj/MainWindow.xib b/Examples/macOS/MIDI Files Testbed/Resources/Base.lproj/MainWindow.xib index b098e60d..f49bec6b 100644 --- a/Examples/macOS/MIDI Files Testbed/Resources/Base.lproj/MainWindow.xib +++ b/Examples/macOS/MIDI Files Testbed/Resources/Base.lproj/MainWindow.xib @@ -1,8 +1,9 @@ - - + + - + + @@ -17,7 +18,7 @@ - + @@ -41,10 +42,10 @@ - + - + @@ -109,7 +110,7 @@ - + @@ -139,28 +140,52 @@ - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Framework/MIKMIDI Tests/MIKMIDICommandTests.m b/Framework/MIKMIDI Tests/MIKMIDICommandTests.m index 5772bbd1..4cca33a1 100644 --- a/Framework/MIKMIDI Tests/MIKMIDICommandTests.m +++ b/Framework/MIKMIDI Tests/MIKMIDICommandTests.m @@ -66,6 +66,39 @@ - (void)testChannelPressureCommand XCTAssertEqual(mutableCommand.pressure, 27, @"Setting the pressure on a MIKMutableMIDIChannelPressureCommand instance failed."); } +- (void)testPitchBendChangeCommand +{ + MIDIPacket packet = MIKMIDIPacketCreate(0, 1, @[@0xef]); + XCTAssertTrue([[MIKMIDICommand commandWithMIDIPacket:&packet] isKindOfClass:[MIKMIDIPitchBendChangeCommand class]]); + + Class immutableClass = [MIKMIDIPitchBendChangeCommand class]; + Class mutableClass = [MIKMutableMIDIPitchBendChangeCommand class]; + + MIKMIDIPitchBendChangeCommand *command = [[immutableClass alloc] init]; + XCTAssert([command isMemberOfClass:[immutableClass class]], @"[[MIKMIDIPitchBendChangeCommand alloc] init] did not return an MIKMIDIPitchBendChangeCommand instance."); + XCTAssert([[MIKMIDICommand commandForCommandType:MIKMIDICommandTypePitchWheelChange] isMemberOfClass:[immutableClass class]], @"[MIKMIDICommand commandForCommandType:MIKMIDICommandTypeSystemExclusive] did not return an MIKMIDIPitchBendChangeCommand instance."); + XCTAssert([[command copy] isMemberOfClass:[immutableClass class]], @"[MIKMIDIPitchBendChangeCommand copy] did not return an MIKMIDIPitchBendChangeCommand instance."); + XCTAssertEqual(command.commandType, MIKMIDICommandTypePitchWheelChange, @"[[MIKMIDIPitchBendChangeCommand alloc] init] produced a command instance with the wrong command type."); + XCTAssertEqual(command.data.length, 3, "MIKMIDIPitchBendChangeCommand had an incorrect data length %@ (should be 3)", @(command.data.length)); + + MIKMutableMIDIPitchBendChangeCommand *mutableCommand = [command mutableCopy]; + XCTAssert([mutableCommand isMemberOfClass:[mutableClass class]], @"-[MIKMIDIPitchBendChangeCommand mutableCopy] did not return an mutableClass instance."); + XCTAssert([[mutableCommand copy] isMemberOfClass:[immutableClass class]], @"-[mutableClass mutableCopy] did not return an MIKMIDIPitchBendChangeCommand instance."); + + NSDate *date = [NSDate date]; + MIKMIDIPitchBendChangeCommand *convenienceTest = [MIKMIDIPitchBendChangeCommand pitchBendChangeCommandWithPitchChange:42 channel:2 timestamp:date]; + XCTAssertNotNil(convenienceTest); + XCTAssert([convenienceTest isMemberOfClass:[immutableClass class]], @"[MIKMIDIPitchBendChangeCommand pitchBendChangeCommandWithPitchChange:...] did not return an MIKMIDIPitchBendChangeCommand instance."); + XCTAssertEqual(convenienceTest.pitchChange, 42); + XCTAssertEqual(convenienceTest.channel, 2); + + MIKMutableMIDIPitchBendChangeCommand *mutableConvenienceTest = [MIKMutableMIDIPitchBendChangeCommand pitchBendChangeCommandWithPitchChange:42 channel:2 timestamp:date]; + XCTAssertNotNil(mutableConvenienceTest); + XCTAssert([mutableConvenienceTest isMemberOfClass:[mutableClass class]], @"[MIKMutableMIDIPitchBendChangeCommand pitchBendChangeCommandWithPitchChange:...] did not return an MIKMutableMIDIPitchBendChangeCommand instance."); + XCTAssertEqual(mutableConvenienceTest.pitchChange, 42); + XCTAssertEqual(mutableConvenienceTest.channel, 2); +} + - (void)testKeepAliveCommand { MIDIPacket packet = MIKMIDIPacketCreate(0, 1, @[@0xfe]); diff --git a/Framework/MIKMIDI Tests/MIKMIDIEventCachingTests.m b/Framework/MIKMIDI Tests/MIKMIDIEventCachingTests.m index f78a19bd..fde89336 100644 --- a/Framework/MIKMIDI Tests/MIKMIDIEventCachingTests.m +++ b/Framework/MIKMIDI Tests/MIKMIDIEventCachingTests.m @@ -29,7 +29,7 @@ - (void)setUp self.sequence = sequence; } -- (void)testPerformanceExample { +- (void)testTempoEventsPerformance { // This is an example of a performance test case. [self measureBlock:^{ for (NSInteger i=0; i<5000; i++) { diff --git a/Framework/MIKMIDI Tests/MIKMIDIMachineControlTests.m b/Framework/MIKMIDI Tests/MIKMIDIMachineControlTests.m new file mode 100644 index 00000000..76080d62 --- /dev/null +++ b/Framework/MIKMIDI Tests/MIKMIDIMachineControlTests.m @@ -0,0 +1,101 @@ +// +// MIKMIDIMachineControlTests.m +// MIKMIDI Tests +// +// Created by Andrew R Madsen on 2/13/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import +#import + +@interface MIKMIDIMachineControlTests : XCTestCase + +@end + +@implementation MIKMIDIMachineControlTests + +- (void)testGenericMIDIMachineControlCommand +{ + Class immutableClass = [MIKMIDIMachineControlCommand class]; + Class mutableClass = [MIKMutableMIDIMachineControlCommand class]; + + NSArray *bytes = @[@(0xf0), @(0x7f), @(0xab), @(0x07), @(0x01)]; + MIDIPacket packet = MIKMIDIPacketCreate(0, bytes.count, bytes); + + MIKMIDIMachineControlCommand *command = [MIKMIDIMachineControlCommand commandWithMIDIPacket:&packet]; + XCTAssertTrue([command isMemberOfClass:[MIKMIDIMachineControlCommand class]]); + XCTAssertEqual(command.deviceAddress, 0xab); + XCTAssertEqual(command.direction, MIKMIDIMachineControlDirectionResponse); + XCTAssertEqual(command.MMCCommandType, MIKMIDIMachineControlCommandTypeStop); + XCTAssertEqual(command.commandType, MIKMIDICommandTypeSystemExclusive, @"[[MIKMIDIMachineControlCommand alloc] init] produced a command instance with the wrong command type."); + XCTAssert([[command copy] isMemberOfClass:immutableClass], @"[MIKMIDIMachineControlCommand copy] did not return an MIKMIDIMachineControlCommand instance."); + XCTAssert([[command mutableCopy] isMemberOfClass:mutableClass], @"-[MIKMIDIMachineControlCommand mutableCopy] did not return a mutableClass instance."); + + + MIKMutableMIDIMachineControlCommand *mutableCommand = [[MIKMutableMIDIMachineControlCommand alloc] init]; + XCTAssert([mutableCommand isMemberOfClass:mutableClass], @"-[MIKMIDIMachineControlCommand mutableCopy] did not return a mutableClass instance."); + XCTAssert([[mutableCommand copy] isMemberOfClass:immutableClass], @"[MIKMutableMIDIMachineControlCommand copy] did not return an MIKMIDIMachineControlCommand instance."); + XCTAssertEqual(mutableCommand.commandType, MIKMIDICommandTypeSystemExclusive, @"[[MIKMIDIMachineControlCommand alloc] init] produced a command instance with the wrong command type."); + + XCTAssertEqual(mutableCommand.deviceAddress, 0x7f); + XCTAssertEqual(mutableCommand.direction, MIKMIDIMachineControlDirectionCommand); + XCTAssertEqual(mutableCommand.MMCCommandType, MIKMIDIMachineControlCommandTypeUnknown); + + XCTAssertThrows([(MIKMutableMIDIMachineControlCommand *)command setDirection:MIKMIDIMachineControlDirectionResponse]); + XCTAssertThrows([(MIKMutableMIDIMachineControlCommand *)command setMMCCommandType:MIKMIDIMachineControlCommandTypePlay]); + + XCTAssertNoThrow([mutableCommand setDirection:MIKMIDIMachineControlDirectionResponse]); + XCTAssertNoThrow([mutableCommand setMMCCommandType:MIKMIDIMachineControlCommandTypePlay]); + + mutableCommand.deviceAddress = 0x9f; + mutableCommand.direction = MIKMIDIMachineControlDirectionResponse; + mutableCommand.MMCCommandType = MIKMIDIMachineControlCommandTypeRecordExit; + XCTAssertEqual(mutableCommand.deviceAddress, 0x9f); + XCTAssertEqual(mutableCommand.direction, MIKMIDIMachineControlDirectionResponse); + XCTAssertEqual(mutableCommand.MMCCommandType, MIKMIDIMachineControlCommandTypeRecordExit); + + MIKMIDIMachineControlCommand *createdCommand = + [MIKMIDIMachineControlCommand machineControlCommandWithDeviceAddress:0xcd + direction:MIKMIDIMachineControlDirectionResponse + MMCCommandType:MIKMIDIMachineControlCommandTypePause]; + XCTAssertEqual(createdCommand.deviceAddress, 0xcd); + XCTAssertEqual(createdCommand.direction, MIKMIDIMachineControlDirectionResponse); + XCTAssertEqual(createdCommand.MMCCommandType, MIKMIDIMachineControlCommandTypePause); +} + +- (void)testMMCLocateTargetCommand +{ + Class immutableClass = [MIKMMCLocateTargetCommand class]; + Class mutableClass = [MIKMutableMMCLocateTargetCommand class]; + + NSArray *bytes = @[@(0xf0), @(0x7f), @(0x7f), @(0x06), @(0x44), @(0x06), @(0x01), @(0x21), @(0x07), @(0x13), @(0x15), @(0x00), @(0xf7)]; + MIDIPacket packet = MIKMIDIPacketCreate(0, bytes.count, bytes); + + MIKMMCLocateTargetCommand *command = (MIKMMCLocateTargetCommand *)[MIKMIDICommand commandWithMIDIPacket:&packet]; + XCTAssertTrue([command isMemberOfClass:[MIKMMCLocateTargetCommand class]]); + XCTAssertEqual(command.timeCodeInSeconds, 4039.84); + XCTAssertThrows([(MIKMutableMMCLocateTargetCommand *)command setTimeCodeInSeconds:27.0]); + + bytes = @[@(0xf0), @(0x7f), @(0x7f), @(0x06), @(0x45), @(0x06), @(0x01), @(0x21), @(0x00), @(0x00), @(0x00), @(0x00), @(0xf7)]; + packet = MIKMIDIPacketCreate(0, bytes.count, bytes); + command = (MIKMMCLocateTargetCommand *)[MIKMIDICommand commandWithMIDIPacket:&packet]; // Should not be a locate command because message type byte is 0x45, not 0x44 + XCTAssertFalse([command isMemberOfClass:[MIKMMCLocateTargetCommand class]]); + XCTAssertTrue([command isMemberOfClass:[MIKMIDIMachineControlCommand class]]); + + MIKMutableMMCLocateTargetCommand *mutableCommand = [[MIKMutableMMCLocateTargetCommand alloc] init]; + XCTAssert([mutableCommand isMemberOfClass:mutableClass], @"-[MIKMMCLocateTargetCommand mutableCopy] did not return a mutableClass instance."); + XCTAssert([[mutableCommand copy] isMemberOfClass:immutableClass], @"[MIKMutableMMCLocateTargetCommand copy] did not return an MIKMMCLocateTargetCommand instance."); + XCTAssertEqual(mutableCommand.commandType, MIKMIDICommandTypeSystemExclusive, @"[[MIKMMCLocateTargetCommand alloc] init] produced a command instance with the wrong command type."); + + mutableCommand.timeType = MIKMMCLocateTargetCommandTimeType25FPS; + mutableCommand.timeCodeInSeconds = 4039.84; + XCTAssertEqual(mutableCommand.timeType, MIKMMCLocateTargetCommandTimeType25FPS); + XCTAssertEqual(mutableCommand.timeCodeInSeconds, 4039.84); + + XCTAssertNoThrow([mutableCommand setTimeCodeInSeconds:27.0]); + + MIKMMCLocateTargetCommand *createdCommand = [[MIKMMCLocateTargetCommand alloc] init]; + XCTAssertEqual(createdCommand.MMCCommandType, MIKMIDIMachineControlCommandTypeLocate); +} +@end diff --git a/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m b/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m index 49c3fc11..2fba4e5e 100644 --- a/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m +++ b/Framework/MIKMIDI Tests/MIKMIDISequenceTests.m @@ -139,11 +139,101 @@ - (void)testLength XCTAssertTrue([self.receivedNotificationKeyPaths containsObject:@"durationInSeconds"], @"KVO notification for durationInSeconds failed after removing longest child track."); } +- (void)testConversionFromMusicTimeStampToSeconds +{ + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSURL *testMIDIFileURL = [bundle URLForResource:@"tempochanges" withExtension:@"mid"]; + NSError *error = nil; + MIKMIDISequence *sequence = [MIKMIDISequence sequenceWithFileAtURL:testMIDIFileURL convertMIDIChannelsToTracks:NO error:&error]; + XCTAssertNotNil(sequence); + + XCTAssertEqual([sequence timeInSecondsForMusicTimeStamp:0], 0.0); + + MIKMIDITempoEvent *firstTempo = [sequence.tempoEvents firstObject]; + XCTAssertNotNil(firstTempo); + NSTimeInterval expectedTimeAt3 = 60 * 3.0 / firstTempo.bpm; + XCTAssertEqualWithAccuracy([sequence timeInSecondsForMusicTimeStamp:3], expectedTimeAt3, 1e-6); + + MIKMIDITempoEvent *secondTempo = sequence.tempoEvents[1]; + NSTimeInterval expectedTimeAtSecondTempo = 60 * secondTempo.timeStamp / firstTempo.bpm; + MusicTimeStamp nextCheck = secondTempo.timeStamp+1; + NSTimeInterval expectedTimeAtNextCheck = 60 * (nextCheck - secondTempo.timeStamp) / secondTempo.bpm + expectedTimeAtSecondTempo; + XCTAssertEqualWithAccuracy([sequence timeInSecondsForMusicTimeStamp:nextCheck], expectedTimeAtNextCheck, 1e-6); +} + +- (void)testConversionFromSecondsToMusicTimeStamp +{ + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSURL *testMIDIFileURL = [bundle URLForResource:@"tempochanges" withExtension:@"mid"]; + NSError *error = nil; + MIKMIDISequence *sequence = [MIKMIDISequence sequenceWithFileAtURL:testMIDIFileURL convertMIDIChannelsToTracks:NO error:&error]; + XCTAssertNotNil(sequence); + + XCTAssertEqual([sequence musicTimeStampForTimeInSeconds:0.0], 0); + + MIKMIDITempoEvent *firstTempo = [sequence.tempoEvents firstObject]; + XCTAssertNotNil(firstTempo); + NSTimeInterval firstCheckTime = 1.5; + MusicTimeStamp firstExpectedTimeStamp = firstTempo.bpm * firstCheckTime / 60.0; + XCTAssertEqualWithAccuracy([sequence musicTimeStampForTimeInSeconds:firstCheckTime], firstExpectedTimeStamp, 1e-6); + + MIKMIDITempoEvent *secondTempo = sequence.tempoEvents[1]; + NSTimeInterval timeAtSecondTempo = 60 * secondTempo.timeStamp / firstTempo.bpm; + NSTimeInterval secondCheckTime = 4; + MusicTimeStamp expectedTimeStampAtNextCheck = secondTempo.bpm * (secondCheckTime - timeAtSecondTempo) / 60.0 + secondTempo.timeStamp; + XCTAssertEqualWithAccuracy([sequence musicTimeStampForTimeInSeconds:secondCheckTime], expectedTimeStampAtNextCheck, 1e-6); +} + - (void)testSetTimeSignature { [self.sequence setTimeSignature:MIKMIDITimeSignatureMake(2, 4) atTimeStamp:0]; } +- (void)testCopyingSequence +{ + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSURL *testMIDIFileURL = [bundle URLForResource:@"Parallax-Loader" withExtension:@"mid"]; + NSError *error = nil; + MIKMIDISequence *sequence = [MIKMIDISequence sequenceWithFileAtURL:testMIDIFileURL convertMIDIChannelsToTracks:NO error:&error]; + XCTAssertNotNil(sequence); + + MIKMIDISequence *copiedSequence = [sequence copy]; + XCTAssertNotIdentical(sequence, copiedSequence, @"Copied sequence was same instance as original"); + XCTAssertEqual(sequence.tracks.count, copiedSequence.tracks.count); + XCTAssertEqual(sequence.length, copiedSequence.length); + XCTAssertEqual(sequence.durationInSeconds, copiedSequence.durationInSeconds); + + NSMutableArray *allTracks = [NSMutableArray arrayWithObject:sequence.tempoTrack]; + [allTracks addObjectsFromArray:sequence.tracks]; + NSMutableArray *allCopiedTracks = [NSMutableArray arrayWithObject:copiedSequence.tempoTrack]; + [allCopiedTracks addObjectsFromArray:copiedSequence.tracks]; + for (NSUInteger i=0; i + + + + classNames + + MIKMIDISequenceTests + + testCopyingSequencePerformance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.066800 + baselineIntegrationDisplayName + Local Baseline + + + testMIDIFileReadPerformance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.056909 + baselineIntegrationDisplayName + Local Baseline + + + + + + 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 66738b5a..46b9304e 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 - 0.05 + 0.050000 baselineIntegrationDisplayName Local Baseline @@ -21,11 +21,11 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 1.09 + 1.090000 baselineIntegrationDisplayName Nov 9, 2017, 3:28:48 PM maxPercentRelativeStandardDeviation - 25 + 25.000000 @@ -36,11 +36,11 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 0.253 + 0.253000 baselineIntegrationDisplayName Nov 9, 2017, 3:28:56 PM maxPercentRelativeStandardDeviation - 10 + 10.000000 diff --git a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C98DAFC3-AC32-4BFB-848B-A1B88CB141E1.plist b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C98DAFC3-AC32-4BFB-848B-A1B88CB141E1.plist index 6e732abd..09693cf5 100644 --- a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C98DAFC3-AC32-4BFB-848B-A1B88CB141E1.plist +++ b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/C98DAFC3-AC32-4BFB-848B-A1B88CB141E1.plist @@ -11,7 +11,7 @@ com.apple.XCTPerformanceMetric_WallClockTime baselineAverage - 1.6332 + 1.633200 baselineIntegrationDisplayName Nov 4, 2019 at 10:50:41 PM diff --git a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/Info.plist b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/Info.plist index 91894063..efab976f 100644 --- a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/Info.plist +++ b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcbaselines/9D4DF1391AAB57430065F004.xcbaseline/Info.plist @@ -4,6 +4,30 @@ runDestinationsByUUID + 3C4B92F8-480E-48B6-B83C-917B91EB0717 + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 Max + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 10 + modelCode + MacBookPro18,2 + physicalCPUCoresPerPackage + 10 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + C1C9286E-763A-4210-B3E7-DF2205A6AA20 localComputer diff --git a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcschemes/MIKMIDI-iOS.xcscheme b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcschemes/MIKMIDI-iOS.xcscheme index 42ea00f2..a33a0e8f 100644 --- a/Framework/MIKMIDI.xcodeproj/xcshareddata/xcschemes/MIKMIDI-iOS.xcscheme +++ b/Framework/MIKMIDI.xcodeproj/xcshareddata/xcschemes/MIKMIDI-iOS.xcscheme @@ -1,6 +1,6 @@ - - - - diff --git a/MIKMIDI.podspec b/MIKMIDI.podspec index a56897b8..fcb413d3 100644 --- a/MIKMIDI.podspec +++ b/MIKMIDI.podspec @@ -15,8 +15,8 @@ Pod::Spec.new do |s| s.author = { 'Andrew Madsen' => 'andrew@mixedinkey.com' } s.social_media_url = 'https://twitter.com/armadsen' - s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.8' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.9' s.source = { :git => 'https://github.com/mixedinkey-opensource/MIKMIDI.git', :tag => s.version.to_s } s.source_files = 'Source/**/*.{h,m}' diff --git a/Source/MIKMIDI.h b/Source/MIKMIDI.h index 8112e221..b8f72bf8 100644 --- a/Source/MIKMIDI.h +++ b/Source/MIKMIDI.h @@ -9,92 +9,93 @@ /** Umbrella header for MIKMIDI public interface. */ // Core MIDI object wrapper -#import "MIKMIDIObject.h" +#import // MIDI port -#import "MIKMIDIPort.h" -#import "MIKMIDIInputPort.h" -#import "MIKMIDIOutputPort.h" +#import +#import +#import // MIDI Device support -#import "MIKMIDIDevice.h" -#import "MIKMIDIDeviceManager.h" -#import "MIKMIDIConnectionManager.h" +#import +#import +#import -#import "MIKMIDIEntity.h" +#import // Endpoints -#import "MIKMIDIEndpoint.h" -#import "MIKMIDIDestinationEndpoint.h" -#import "MIKMIDISourceEndpoint.h" -#import "MIKMIDIClientDestinationEndpoint.h" -#import "MIKMIDIClientSourceEndpoint.h" +#import +#import +#import +#import +#import // MIDI Commands/Messages -#import "MIKMIDICommand.h" -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDINoteCommand.h" -#import "MIKMIDIChannelPressureCommand.h" -#import "MIKMIDIControlChangeCommand.h" -#import "MIKMIDIProgramChangeCommand.h" -#import "MIKMIDIPitchBendChangeCommand.h" -#import "MIKMIDINoteOnCommand.h" -#import "MIKMIDINoteOffCommand.h" -#import "MIKMIDIPolyphonicKeyPressureCommand.h" -#import "MIKMIDISystemExclusiveCommand.h" -#import "MIKMIDISystemMessageCommand.h" -#import "MIKMIDISystemKeepAliveCommand.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import // Includes many individual MMC command types // MIDI Sequence/File support -#import "MIKMIDISequence.h" -#import "MIKMIDITrack.h" +#import +#import // MIDI Events -#import "MIKMIDIEvent.h" -#import "MIKMIDITempoEvent.h" -#import "MIKMIDINoteEvent.h" +#import +#import +#import // Channel Events -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDIPolyphonicKeyPressureEvent.h" -#import "MIKMIDIControlChangeEvent.h" -#import "MIKMIDIProgramChangeEvent.h" -#import "MIKMIDIChannelPressureEvent.h" -#import "MIKMIDIPitchBendChangeEvent.h" +#import +#import +#import +#import +#import +#import // Meta Events -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDIMetaCopyrightEvent.h" -#import "MIKMIDIMetaCuePointEvent.h" -#import "MIKMIDIMetaInstrumentNameEvent.h" -#import "MIKMIDIMetaKeySignatureEvent.h" -#import "MIKMIDIMetaLyricEvent.h" -#import "MIKMIDIMetaMarkerTextEvent.h" -#import "MIKMIDIMetaSequenceEvent.h" -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDIMetaTimeSignatureEvent.h" -#import "MIKMIDIMetaTrackSequenceNameEvent.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import // Sequencing and Synthesis -#import "MIKMIDISequencer.h" -#import "MIKMIDIMetronome.h" -#import "MIKMIDIClock.h" -#import "MIKMIDIPlayer.h" -#import "MIKMIDIEndpointSynthesizer.h" +#import +#import +#import +#import +#import // MIDI Mapping -#import "MIKMIDIMapping.h" -#import "MIKMIDIMappingItem.h" -#import "MIKMIDIMappableResponder.h" -#import "MIKMIDIMappingManager.h" -#import "MIKMIDIMappingGenerator.h" +#import +#import +#import +#import +#import // Intra-application MIDI command routing -#import "NSUIApplication+MIKMIDI.h" -#import "MIKMIDIResponder.h" -#import "MIKMIDICommandThrottler.h" +#import +#import +#import // Utilities -#import "MIKMIDIUtilities.h" -#import "MIKMIDIErrors.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import diff --git a/Source/MIKMIDIChannelEvent.h b/Source/MIKMIDIChannelEvent.h index 1abe326e..1d92b4cf 100644 --- a/Source/MIKMIDIChannelEvent.h +++ b/Source/MIKMIDIChannelEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_END #pragma mark - -#import "MIKMIDICommand.h" +#import @class MIKMIDIClock; @@ -71,4 +71,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIChannelPressureCommand.h b/Source/MIKMIDIChannelPressureCommand.h index 8cdbacff..af545c18 100644 --- a/Source/MIKMIDIChannelPressureCommand.h +++ b/Source/MIKMIDIChannelPressureCommand.h @@ -6,8 +6,8 @@ // Copyright © 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDIChannelPressureCommand.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDIChannelPressureEvent.h b/Source/MIKMIDIChannelPressureEvent.h index 27ba7afd..b83142b1 100644 --- a/Source/MIKMIDIChannelPressureEvent.h +++ b/Source/MIKMIDIChannelPressureEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -42,4 +42,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIChannelVoiceCommand.h b/Source/MIKMIDIChannelVoiceCommand.h index 93e606d6..751e8dde 100644 --- a/Source/MIKMIDIChannelVoiceCommand.h +++ b/Source/MIKMIDIChannelVoiceCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDICommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -53,4 +53,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIChannelVoiceCommand_SubclassMethods.h b/Source/MIKMIDIChannelVoiceCommand_SubclassMethods.h index 7806d69f..3cc814c7 100644 --- a/Source/MIKMIDIChannelVoiceCommand_SubclassMethods.h +++ b/Source/MIKMIDIChannelVoiceCommand_SubclassMethods.h @@ -6,9 +6,9 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDICommand_SubclassMethods.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -18,4 +18,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIClientDestinationEndpoint.h b/Source/MIKMIDIClientDestinationEndpoint.h index 67d61ebb..bb7afd94 100644 --- a/Source/MIKMIDIClientDestinationEndpoint.h +++ b/Source/MIKMIDIClientDestinationEndpoint.h @@ -6,8 +6,8 @@ // // -#import "MIKMIDIDestinationEndpoint.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIClientDestinationEndpoint; @class MIKMIDICommand; @@ -50,4 +50,4 @@ typedef void(^MIKMIDIClientDestinationEndpointEventHandler)(MIKMIDIClientDestina @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIClientSourceEndpoint.h b/Source/MIKMIDIClientSourceEndpoint.h index 0986b0cd..0d8fcf79 100644 --- a/Source/MIKMIDIClientSourceEndpoint.h +++ b/Source/MIKMIDIClientSourceEndpoint.h @@ -5,8 +5,8 @@ // Created by Dan Rosenstark on 2015-01-07 // -#import "MIKMIDISourceEndpoint.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDICommand; @@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An instance of MIKMIDIClientSourceEndpoint, or nil if an error occurs. */ -- (instancetype)initWithName:(NSString *)name error:(NSError **)error; +- (nullable instancetype)initWithName:(NSString *)name error:(NSError **)error; /** * Used to send MIDI messages/commands from your application to a MIDI output endpoint. @@ -66,8 +66,10 @@ NS_ASSUME_NONNULL_BEGIN * * @return An instance of MIKMIDIClientSourceEndpoint, or nil if an error occurs. */ -- (nullable instancetype)initWithName:(NSString *)name DEPRECATED_ATTRIBUTE; +- (nullable instancetype)initWithName:(NSString *)name +DEPRECATED_ATTRIBUTE +NS_SWIFT_UNAVAILABLE("Use the error throwing variant instead."); @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIClock.h b/Source/MIKMIDIClock.h index 088e65b0..c4a4b265 100644 --- a/Source/MIKMIDIClock.h +++ b/Source/MIKMIDIClock.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import /** * Returns the number of MIDITimeStamps that would occur during a specified time interval. diff --git a/Source/MIKMIDIClock.m b/Source/MIKMIDIClock.m index 1ce9ffdd..6aac3bf4 100644 --- a/Source/MIKMIDIClock.m +++ b/Source/MIKMIDIClock.m @@ -367,7 +367,7 @@ Float64 MIKMIDIClockSecondsPerMIDITimeStamp() dispatch_once(&onceToken, ^{ mach_timebase_info_data_t timeBaseInfoData; mach_timebase_info(&timeBaseInfoData); - secondsPerMIDITimeStamp = ((Float64)timeBaseInfoData.numer / (Float64)timeBaseInfoData.denom) / 1.0e9; + secondsPerMIDITimeStamp = ((Float64)timeBaseInfoData.numer / (Float64)timeBaseInfoData.denom) / NSEC_PER_SEC; }); return secondsPerMIDITimeStamp; diff --git a/Source/MIKMIDICommand.h b/Source/MIKMIDICommand.h index 4ca0b188..d5164774 100644 --- a/Source/MIKMIDICommand.h +++ b/Source/MIKMIDICommand.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import /** * Types of MIDI messages. These values correspond directly to the MIDI command type values diff --git a/Source/MIKMIDICommand.m b/Source/MIKMIDICommand.m index d7078f51..4c638935 100644 --- a/Source/MIKMIDICommand.m +++ b/Source/MIKMIDICommand.m @@ -35,6 +35,7 @@ + (void)registerSubclass:(Class)subclass; + (BOOL)isMutable { return NO; } + (BOOL)supportsMIDICommandType:(MIKMIDICommandType)type { return [[self supportedMIDICommandTypes] containsObject:@(type)]; } ++ (MIKMIDICommandPacketHandlingIntent)handlingIntentForMIDIPacket:(MIDIPacket *)packet { return MIKMIDICommandPacketHandlingIntentAccept; } + (NSArray *)supportedMIDICommandTypes { return @[]; } + (Class)immutableCounterpartClass; { return [MIKMIDICommand class]; } + (Class)mutableCounterpartClass; { return [MIKMutableMIDICommand class]; } @@ -43,8 +44,7 @@ + (instancetype)commandWithMIDIPacket:(MIDIPacket *)packet; { Class subclass = Nil; if (packet) { - MIKMIDICommandType commandType = packet->data[0]; - subclass = [[self class] subclassForCommandType:commandType]; + subclass = [[self class] subclassForMIDIPacket:packet]; } if (!subclass) { subclass = self; } @@ -55,16 +55,36 @@ + (instancetype)commandWithMIDIPacket:(MIDIPacket *)packet; + (NSArray *)commandsWithMIDIPacket:(MIDIPacket *)inputPacket { NSMutableArray *result = [NSMutableArray array]; - NSInteger dataOffset = 0; + ByteCount dataOffset = 0; while (dataOffset < inputPacket->length) { - const Byte *packetData = inputPacket->data + dataOffset; - MIKMIDICommandType commandType = (MIKMIDICommandType)packetData[0]; - NSInteger standardLength = MIKMIDIStandardLengthOfMessageForCommandType(commandType); - if (commandType == MIKMIDICommandTypeSystemExclusive) { - // For sysex, the packet can only contain a single MIDI message (as per documentation for MIDIPacket) - standardLength = inputPacket->length; + ByteCount eventDataLength = 0; + const Byte *eventData = inputPacket->data + dataOffset; + MIKMIDICommandType commandType = (MIKMIDICommandType)eventData[0]; + switch (commandType) { + // For sysex, the packet can only contain a single MIDI message (as per documentation for MIDIPacket) + case MIKMIDICommandTypeSystemExclusive: + eventDataLength = inputPacket->length; + break; + + // Is MIKMIDIStandardLengthOfMessageForCommandType() correct returning -1? + // This seems to be the realtime 'System Reset' message coming from a device + // (but could be a meta packet when coming from a file?) + case MIKMIDICommandTypeSystemMessage: + eventDataLength = 1; + break; + + default: { + __auto_type standardLength = MIKMIDIStandardLengthOfMessageForCommandType(commandType); + if ( standardLength > 0 ) { + eventDataLength = (ByteCount)standardLength; + } else { /* -1 or NSIntegerMin */ + eventDataLength = 1; /* assume 1 and hope for the best */ + } + break; + } } - if (dataOffset > (inputPacket->length - standardLength)) break; + + if (dataOffset > (inputPacket->length - eventDataLength)) break; // This is gross, but it's the only way I can find to reliably create a // single-message MIDIPacket. @@ -74,12 +94,12 @@ + (NSArray *)commandsWithMIDIPacket:(MIDIPacket *)inputPacket sizeof(MIDIPacketList), midiPacket, inputPacket->timeStamp, - standardLength, - packetData); + eventDataLength, + eventData); MIKMIDICommand *command = [MIKMIDICommand commandWithMIDIPacket:midiPacket]; if (command) [result addObject:command]; - dataOffset += standardLength; + dataOffset += eventDataLength; } return result; @@ -87,7 +107,7 @@ + (NSArray *)commandsWithMIDIPacket:(MIDIPacket *)inputPacket + (instancetype)commandForCommandType:(MIKMIDICommandType)commandType; // Most useful for mutable commands { - Class subclass = [[self class] subclassForCommandType:commandType]; + Class subclass = [[[self class] allSubclassesForCommandType:commandType] firstObject]; if (!subclass) subclass = self; if ([self isMutable]) subclass = [subclass mutableCounterpartClass]; return [[subclass alloc] init]; @@ -150,26 +170,70 @@ - (BOOL)isEqualToCommand:(MIKMIDICommand *)command #pragma mark - Private -+ (Class)subclassForCommandType:(MIKMIDICommandType)commandType ++ (NSArray *)allSubclassesForCommandType:(MIKMIDICommandType)commandType { - Class result = nil; - for (Class subclass in registeredMIKMIDICommandSubclasses) { - if ([[subclass supportedMIDICommandTypes] containsObject:@(commandType)]) { - result = subclass; - break; - } - } - if (!result) { - // Try again ignoring lower 4 bits - commandType |= 0x0f; - for (Class subclass in registeredMIKMIDICommandSubclasses) { - if ([[subclass supportedMIDICommandTypes] containsObject:@(commandType)]) { - result = subclass; - break; - } - } - } - return result; + NSMutableArray *result = [NSMutableArray array]; + for (Class subclass in registeredMIKMIDICommandSubclasses) { + if ([[subclass supportedMIDICommandTypes] containsObject:@(commandType)]) { + [result addObject:subclass]; + } + } + if (!result.count) { + // Try again ignoring lower 4 bits + commandType |= 0x0f; + for (Class subclass in registeredMIKMIDICommandSubclasses) { + if ([[subclass supportedMIDICommandTypes] containsObject:@(commandType)]) { + [result addObject:subclass]; + } + } + } + + // Sort so that deepest subclass hierarchy children come last + return [result sortedArrayWithOptions:0 usingComparator:^NSComparisonResult(Class class1, Class class2) { + if ([class1 isEqual:class2]) { return NSOrderedSame; } + if ([class1 isSubclassOfClass:class2]) { return NSOrderedDescending; } + if ([class2 isSubclassOfClass:class1]) { return NSOrderedAscending; } + return NSOrderedAscending; + }]; +} + ++ (Class)subclassForMIDIPacket:(MIDIPacket *)packet +{ + MIKMIDICommandType commandType = packet->data[0]; + + NSArray *allSubclasses = [self allSubclassesForCommandType:commandType]; + NSMutableArray *subclasses = [NSMutableArray array]; + NSMutableArray *specificHandlingSubclasses = [NSMutableArray array]; + + for (Class subclass in allSubclasses) { + MIKMIDICommandPacketHandlingIntent intent = [subclass handlingIntentForMIDIPacket:packet]; + if (intent == MIKMIDICommandPacketHandlingIntentReject) { + continue; + } + [subclasses addObject:subclass]; + if (intent == MIKMIDICommandPacketHandlingIntentAcceptWithHigherPrecedence) { + [specificHandlingSubclasses addObject:subclass]; + } + } + + if (specificHandlingSubclasses.count > 1) { + NSData *packetData = [NSData dataWithBytes:packet->data length:packet->length]; + NSLog(@"[MIKMIDI] Warning: More than one subclass of MIKMIDICommand was found to handle MIDI message data (%@). Candidates are: %@. Which one is used is random/undefined. This is likely a bug, and should be reported to the maintainers of MIKMIDI.", packetData, specificHandlingSubclasses); + } + + if (specificHandlingSubclasses.count) { + subclasses = specificHandlingSubclasses; + } + + // Return the deepest child subclass that doesn't reject this MIDI packet + for (Class subclass in subclasses.reverseObjectEnumerator) { + if ([subclass handlingIntentForMIDIPacket:packet] == MIKMIDICommandPacketHandlingIntentReject) { + continue; + } + return subclass; + } + + return nil; } #pragma mark - NSCopying @@ -340,6 +404,18 @@ ByteCount MIKMIDIPacketListSizeForCommands(NSArray *commands) return 0; } +#if defined(__arm__) || defined(__aarch64__) + // [4-byte aligned] + // Compute the size of static members of MIDIPacketList + ByteCount packetListSize = offsetof(MIDIPacketList, packet); + + for (MIKMIDICommand *command in commands) { + // Compute the size of MIDIPacket + ByteCount packetSize = offsetof(MIDIPacket, data) + command.data.length; + packetListSize += 4 * ((packetSize + 3) / 4); + } +#else + // [packed] // Compute the size of static members of MIDIPacketList and (MIDIPacket * [commands count]) ByteCount packetListSize = offsetof(MIDIPacketList, packet) + offsetof(MIDIPacket, data) * [commands count]; @@ -347,7 +423,7 @@ ByteCount MIKMIDIPacketListSizeForCommands(NSArray *commands) for (MIKMIDICommand *command in commands) { packetListSize += [[command data] length]; } - +#endif return packetListSize; } diff --git a/Source/MIKMIDICommandScheduler.h b/Source/MIKMIDICommandScheduler.h index 3a5d423a..3f2f69bb 100644 --- a/Source/MIKMIDICommandScheduler.h +++ b/Source/MIKMIDICommandScheduler.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDICommand; diff --git a/Source/MIKMIDICommandThrottler.h b/Source/MIKMIDICommandThrottler.h index bcf45673..d23c723a 100644 --- a/Source/MIKMIDICommandThrottler.h +++ b/Source/MIKMIDICommandThrottler.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDIChannelVoiceCommand; diff --git a/Source/MIKMIDICommand_SubclassMethods.h b/Source/MIKMIDICommand_SubclassMethods.h index 9b33bb87..aa1444b8 100644 --- a/Source/MIKMIDICommand_SubclassMethods.h +++ b/Source/MIKMIDICommand_SubclassMethods.h @@ -6,8 +6,23 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDICommand.h" -#import "MIKMIDITransmittable.h" +#import +#import + +/** + Used by MIKMIDICommand subclasses to communicate their desire to handle a specific + MIDIPacket. + + @see +[MIKMIDICommand handlingIntentForMIDIPacket:] + */ +typedef NS_ENUM(NSInteger, MIKMIDICommandPacketHandlingIntent) { + /** The receiver never wants to be initialized with the given MIDIPacket */ + MIKMIDICommandPacketHandlingIntentReject, + /** The receiver can be initialized with the given MIDIPacket but doesn't need precedence over other supporters of the same command type. The default. */ + MIKMIDICommandPacketHandlingIntentAccept, + /** The receiver implements special support for the passed-in MIDI packet and should be used to handle it instead of other supports (e.g. the superclass) */ + MIKMIDICommandPacketHandlingIntentAcceptWithHigherPrecedence, +}; /** * These methods can be called and/or overridden by subclasses of MIKMIDICommand, but are not @@ -41,6 +56,20 @@ */ + (MIKArrayOf(NSNumber *) *)supportedMIDICommandTypes; +/** + * Subclasses of MIKMIDICommand can implement this to indicate that they want to handle + * a specific MIDI packet, even if other commands (e.g. superclass(es)) can also handle the + * more general MIDI message type. For example, this is used for the MIDI Machine Control-spefic + * subclasses of MIKMIDISystemExclusiveCommand to indicate that they should take precedence + * over MIKMIDISystemExclusiveCommand itself for their specific MMC sysex messages. + * + * Note that this method will only be called if the receiver is registered as a subclass with MIKMIDICommand + * and has already returned the MIDI command type represented by packet from its +supportedMIDICommandTypes method. + * + * @return One of the values in MIKMIDICommandPacketHandlingIntent + */ ++ (MIKMIDICommandPacketHandlingIntent)handlingIntentForMIDIPacket:(MIDIPacket *)packet; + /** * The immutable counterpart class of the receiver. * diff --git a/Source/MIKMIDIConnectionManager.h b/Source/MIKMIDIConnectionManager.h index 75157b81..adb6ff4f 100644 --- a/Source/MIKMIDIConnectionManager.h +++ b/Source/MIKMIDIConnectionManager.h @@ -7,8 +7,8 @@ // #import -#import "MIKMIDICompilerCompatibility.h" -#import "MIKMIDISourceEndpoint.h" +#import +#import @class MIKMIDIDevice; @class MIKMIDINoteOnCommand; diff --git a/Source/MIKMIDIControlChangeCommand.h b/Source/MIKMIDIControlChangeCommand.h index 683288ec..9ed22e40 100644 --- a/Source/MIKMIDIControlChangeCommand.h +++ b/Source/MIKMIDIControlChangeCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDIControlChangeEvent.h b/Source/MIKMIDIControlChangeEvent.h index 30bd79f1..a6d7bddf 100644 --- a/Source/MIKMIDIControlChangeEvent.h +++ b/Source/MIKMIDIControlChangeEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -50,4 +50,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIDestinationEndpoint.h b/Source/MIKMIDIDestinationEndpoint.h index 5057716d..5064cf07 100644 --- a/Source/MIKMIDIDestinationEndpoint.h +++ b/Source/MIKMIDIDestinationEndpoint.h @@ -6,9 +6,9 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIEndpoint.h" -#import "MIKMIDICommandScheduler.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDIDevice.h b/Source/MIKMIDIDevice.h index 235e58b6..53d27f54 100644 --- a/Source/MIKMIDIDevice.h +++ b/Source/MIKMIDIDevice.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIObject.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIEntity; @class MIKMIDIEndpoint; @@ -131,4 +131,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIDeviceManager.h b/Source/MIKMIDIDeviceManager.h index d1727e21..af5cc534 100644 --- a/Source/MIKMIDIDeviceManager.h +++ b/Source/MIKMIDIDeviceManager.h @@ -7,8 +7,8 @@ // #import -#import "MIKMIDIInputPort.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIDevice; @class MIKMIDISourceEndpoint; diff --git a/Source/MIKMIDIEndpoint.h b/Source/MIKMIDIEndpoint.h index 4faccd49..f907f436 100644 --- a/Source/MIKMIDIEndpoint.h +++ b/Source/MIKMIDIEndpoint.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIObject.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIEntity; @@ -31,4 +31,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIEndpointSynthesizer.h b/Source/MIKMIDIEndpointSynthesizer.h index fef9c20f..acfa3c5e 100644 --- a/Source/MIKMIDIEndpointSynthesizer.h +++ b/Source/MIKMIDIEndpointSynthesizer.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDISynthesizer.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIEndpoint; @class MIKMIDISourceEndpoint; diff --git a/Source/MIKMIDIEntity.h b/Source/MIKMIDIEntity.h index 02a99888..cd27dce0 100644 --- a/Source/MIKMIDIEntity.h +++ b/Source/MIKMIDIEntity.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIObject.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIDevice; @class MIKMIDIEndpoint; @@ -71,4 +71,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIErrors.h b/Source/MIKMIDIErrors.h index a6b035d0..334ca02d 100644 --- a/Source/MIKMIDIErrors.h +++ b/Source/MIKMIDIErrors.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -88,4 +88,4 @@ NSString *MIKMIDIDefaultLocalizedErrorDescriptionForErrorCode(MIKMIDIErrorCode c @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIEvent.h b/Source/MIKMIDIEvent.h index 105737d1..b404bfe6 100644 --- a/Source/MIKMIDIEvent.h +++ b/Source/MIKMIDIEvent.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import /** * Types of MIDI events. These values are used to determine which subclass to @@ -153,6 +153,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable instancetype)initWithTimeStamp:(MusicTimeStamp)timeStamp midiEventType:(MIKMIDIEventType)eventType data:(nullable NSData *)data NS_DESIGNATED_INITIALIZER; +/** + * Compares two events for equality. + * @param otherEvent The event with which to compare the receiver. + * + * @return YES if the events have the same type, timeStamp, and data, NO otherwise. + */ +- (BOOL)isEqualToEvent:(MIKMIDIEvent *)otherEvent; + /** * The MIDI event type. */ @@ -187,7 +195,7 @@ NS_ASSUME_NONNULL_END #pragma mark - MIKMIDICommand+MIKMIDIEventToCommands -#import "MIKMIDICommand.h" +#import @class MIKMIDIClock; @@ -199,4 +207,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIEvent.m b/Source/MIKMIDIEvent.m index 95eefbb9..77e1bba0 100644 --- a/Source/MIKMIDIEvent.m +++ b/Source/MIKMIDIEvent.m @@ -85,14 +85,18 @@ - (NSString *)description #pragma mark - Equality +- (BOOL)isEqualToEvent:(MIKMIDIEvent *)otherEvent +{ + if (otherEvent.eventType != self.eventType) return NO; + return (self.timeStamp == otherEvent.timeStamp && [self.internalData isEqualToData:otherEvent.internalData]); +} + - (BOOL)isEqual:(id)object { if (object == self) return YES; if (![object isKindOfClass:[MIKMIDIEvent class]]) return NO; - - MIKMIDIEvent *otherEvent = (MIKMIDIEvent *)object; - if (otherEvent.eventType != self.eventType) return NO; - return (self.timeStamp == otherEvent.timeStamp && [self.internalData isEqualToData:otherEvent.internalData]); + + return [self isEqualToEvent:(MIKMIDIEvent *)object]; } - (NSUInteger)hash diff --git a/Source/MIKMIDIEventIterator.h b/Source/MIKMIDIEventIterator.h index 21f09642..940a3d78 100644 --- a/Source/MIKMIDIEventIterator.h +++ b/Source/MIKMIDIEventIterator.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDITrack; @class MIKMIDIEvent; @@ -37,4 +37,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIEvent_SubclassMethods.h b/Source/MIKMIDIEvent_SubclassMethods.h index e7887657..01386bea 100644 --- a/Source/MIKMIDIEvent_SubclassMethods.h +++ b/Source/MIKMIDIEvent_SubclassMethods.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -112,4 +112,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIInputPort.h b/Source/MIKMIDIInputPort.h index 00329257..a8435748 100644 --- a/Source/MIKMIDIInputPort.h +++ b/Source/MIKMIDIInputPort.h @@ -6,9 +6,9 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIPort.h" -#import "MIKMIDISourceEndpoint.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import @class MIKMIDIEndpoint; @class MIKMIDICommand; diff --git a/Source/MIKMIDIMachineControl.h b/Source/MIKMIDIMachineControl.h new file mode 100644 index 00000000..541b12be --- /dev/null +++ b/Source/MIKMIDIMachineControl.h @@ -0,0 +1,12 @@ +// +// MIKMIDIMachineControl.h +// MIKMIDI +// +// Created by Andrew R Madsen on 2/6/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import + +#import +#import diff --git a/Source/MIKMIDIMachineControlCommand.h b/Source/MIKMIDIMachineControlCommand.h new file mode 100644 index 00000000..c2246bcb --- /dev/null +++ b/Source/MIKMIDIMachineControlCommand.h @@ -0,0 +1,129 @@ +// +// MIKMIDIMachineControlCommand.h +// MIKMIDI +// +// Created by Andrew R Madsen on 2/13/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import + +/** + * The MMC sub-ID. + * As defined by the MMC spec, a message can be either a command or a response. + */ +typedef NS_ENUM(UInt8, MIKMIDIMachineControlDirection) { + MIKMIDIMachineControlDirectionCommand = 0x06, // aka. 'mcc' + MIKMIDIMachineControlDirectionResponse = 0x07, // aka. 'mcr' +}; + +/** + * The possible command types represented by an MMC message. These are set forth and described in the + * MMC part of the MIDI spec. + */ +typedef NS_ENUM(UInt8, MIKMIDIMachineControlCommandType) { + MIKMIDIMachineControlCommandTypeUnknown = 0x00, + + MIKMIDIMachineControlCommandTypeStop = 0x01, + MIKMIDIMachineControlCommandTypePlay = 0x02, + MIKMIDIMachineControlCommandTypeDeferredPlay = 0x03, + MIKMIDIMachineControlCommandTypeFastForward = 0x04, + MIKMIDIMachineControlCommandTypeRewind = 0x05, + MIKMIDIMachineControlCommandTypeRecordStrobe = 0x06, + MIKMIDIMachineControlCommandTypeRecordExit = 0x07, + MIKMIDIMachineControlCommandTypeRecordPause = 0x08, + MIKMIDIMachineControlCommandTypePause = 0x09, + MIKMIDIMachineControlCommandTypeEject = 0x0a, + MIKMIDIMachineControlCommandTypeChase = 0x0b, + MIKMIDIMachineControlCommandTypeCommandErrorReset = 0xc, + MIKMIDIMachineControlCommandTypeMMCRest = 0xd, + MIKMIDIMachineControlCommandTypeWrite = 0x40, + MIKMIDIMachineControlCommandTypeMaskedWrite = 0x41, + MIKMIDIMachineControlCommandTypeRead = 0x42, + MIKMIDIMachineControlCommandTypeUpdate = 0x43, + MIKMIDIMachineControlCommandTypeLocate = 0x44, + MIKMIDIMachineControlCommandTypeVariablePlay = 0x45, + MIKMIDIMachineControlCommandTypeSearch = 0x46, + MIKMIDIMachineControlCommandTypeShuttle = 0x47, + MIKMIDIMachineControlCommandTypeStep = 0x48, + MIKMIDIMachineControlCommandTypeAssignSystemMaster = 0x49, + MIKMIDIMachineControlCommandTypeGeneratorCommand = 0x4a, + MIKMIDIMachineControlCommandTypeMIDITimeCodeCommand = 0x4b, + MIKMIDIMachineControlCommandTypeMove = 0x4c, + MIKMIDIMachineControlCommandTypeAdd = 0x4d, + MIKMIDIMachineControlCommandTypeSubtract = 0x4e, + MIKMIDIMachineControlCommandTypeDropFrameAdjust = 0x4f, + MIKMIDIMachineControlCommandTypeProcedure = 0x50, + MIKMIDIMachineControlCommandTypeEvent = 0x51, + MIKMIDIMachineControlCommandTypeGroup = 0x52, + MIKMIDIMachineControlCommandTypeCommandSegment = 0x53, + MIKMIDIMachineControlCommandTypeDeferredVariablePlay = 0x54, + MIKMIDIMachineControlCommandTypeRecordStrobeVariable = 0x55, + MIKMIDIMachineControlCommandTypeWait = 0x7C, + MIKMIDIMachineControlCommandTypeResume = 0x7F, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + * MIKMIDIMachineControlCommand is used to represent MIDI messages that are used for + * MIDI Machine Control (MMC), per the MIDI spec. Specific support for MMC command + * subtypes is provided by subclasses of MIKMIDIMachineControlCommand (e.g. + * MIKMIDIMachineControlLocatedTargetCommand, etc.) + */ +@interface MIKMIDIMachineControlCommand : MIKMIDISystemExclusiveCommand + +/** + * Convenience method for creating a machine control command. + * + * @param deviceAddress The device address for the command. Destination address for commands, source address for responses + * @param direction The direction the command is going, either a command or a response + * @param mmcCommandType The sub-type for the command. See MIKMIDIMachineControlCommandType for values + * + * @return An initialized MIKMIDIMachineControlCommand (or subclass) instance + */ ++ (instancetype)machineControlCommandWithDeviceAddress:(UInt8)deviceAddress + direction:(MIKMIDIMachineControlDirection)direction + MMCCommandType:(MIKMIDIMachineControlCommandType)mmcCommandType; + + +/** + * The device address in the message represented by the receiver. Per the MMC spec, this is the destination + * device for commands, and the source device address for responses. + */ +@property (nonatomic, readonly) UInt8 deviceAddress; + +/** + * The direction this message is going. As defined by the MMC spec, a message can be either a command + * or a response. See MIKMIDIMachineControlDirection for possible values. + */ +@property (nonatomic, readonly) MIKMIDIMachineControlDirection direction; + +/** + * The MMC command type represented by the receiver. For a complete list of possible values + * see MIKMIDIMachineControlCommandType. + */ +@property (nonatomic, readonly) MIKMIDIMachineControlCommandType MMCCommandType; + +@end + +/** + * The mutable counterpart of MIKMIDIMachineControlCommand. + */ +@interface MIKMutableMIDIMachineControlCommand : MIKMIDIMachineControlCommand + +@property (nonatomic, readwrite) UInt8 deviceAddress; +@property (nonatomic, readwrite) MIKMIDIMachineControlDirection direction; +@property (nonatomic, readwrite) MIKMIDIMachineControlCommandType MMCCommandType; + +@property (nonatomic, strong, readwrite) NSDate *timestamp; +@property (nonatomic, readwrite) MIKMIDICommandType commandType; +@property (nonatomic, readwrite) UInt8 dataByte1; +@property (nonatomic, readwrite) UInt8 dataByte2; + +@property (nonatomic, readwrite) MIDITimeStamp midiTimestamp; +@property (nonatomic, copy, readwrite, null_resettable) NSData *data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMachineControlCommand.m b/Source/MIKMIDIMachineControlCommand.m new file mode 100644 index 00000000..dc805888 --- /dev/null +++ b/Source/MIKMIDIMachineControlCommand.m @@ -0,0 +1,152 @@ +// +// MIKMIDIMachineControlCommand.m +// MIKMIDI +// +// Created by Andrew R Madsen on 2/13/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import "MIKMIDIMachineControlCommand.h" +#import "MIKMIDICommand_SubclassMethods.h" +#import "MIKMMCLocateTargetCommand.h" +#import "MIKMIDIUtilities.h" + +@implementation MIKMIDIMachineControlCommand + ++ (void)load { [super load]; [MIKMIDICommand registerSubclass:self]; } ++ (NSArray *)supportedMIDICommandTypes { return @[@(MIKMIDICommandTypeSystemExclusive)]; } + ++ (MIKMIDICommandPacketHandlingIntent)handlingIntentForMIDIPacket:(MIDIPacket *)packet +{ + if (packet->length < 5) { return MIKMIDICommandPacketHandlingIntentReject; } + uint8_t directionByteIndex = 3; + uint8_t firstByte = packet->data[1]; + if (firstByte == 0) { // Three byte manufacturer ID + directionByteIndex += 2; + } + uint8_t directionByte = packet->data[directionByteIndex]; + if (directionByte != 0x06 && directionByte != 0x07) { return MIKMIDICommandPacketHandlingIntentReject; } + + return MIKMIDICommandPacketHandlingIntentAccept; +} + ++ (Class)immutableCounterpartClass; { return [MIKMIDIMachineControlCommand class]; } ++ (Class)mutableCounterpartClass; { return [MIKMutableMIDIMachineControlCommand class]; } + +- (id)initWithMIDIPacket:(MIDIPacket *)packet +{ + self = [super initWithMIDIPacket:packet]; + if (self) { + if (!packet) { + if ([self.internalData length] < 5) { + [self.internalData increaseLengthBy:5-[self.internalData length]]; + } + UInt8 *data = (UInt8 *)[self.internalData mutableBytes]; + data[0] = 0xf0; + data[1] = 0x7f; // Sysex start + data[2] = 0x7f; // generic device address + data[3] = MIKMIDIMachineControlDirectionCommand; + data[4] = MIKMIDIMachineControlCommandTypeUnknown; + } + } + return self; +} + ++ (instancetype)machineControlCommandWithDeviceAddress:(UInt8)deviceAddress + direction:(MIKMIDIMachineControlDirection)direction + MMCCommandType:(MIKMIDIMachineControlCommandType)mmcCommandType +{ + Class resultClass = [MIKMIDIMachineControlCommand class]; + if (mmcCommandType == MIKMIDIMachineControlCommandTypeLocate) { resultClass = [MIKMMCLocateTargetCommand class]; } + + MIKMutableMIDIMachineControlCommand *result = [[[resultClass mutableCounterpartClass] alloc] init]; + result.deviceAddress = deviceAddress; + result.direction = direction; + result.MMCCommandType = mmcCommandType; + + return [self isMutable] ? result : [result copy]; +} + +#pragma mark - Properties + +- (UInt8)deviceAddress +{ + UInt8 deviceIDByteIndex = self.includesThreeByteManufacturerID ? 4 : 2; + UInt8 deviceID = ((UInt8 *)self.data.bytes)[deviceIDByteIndex]; + return deviceID; +} + +- (void)setDeviceAddress:(UInt8)deviceAddress +{ + if (![[self class] isMutable]) return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; + + UInt8 deviceIDByteIndex = self.includesThreeByteManufacturerID ? 4 : 2; + if ([self.internalData length] <= deviceIDByteIndex) { + [self.internalData increaseLengthBy:deviceIDByteIndex + 1 - [self.internalData length]]; + } + + UInt8 *data = (UInt8 *)[self.internalData mutableBytes]; + data[deviceIDByteIndex] = deviceAddress; +} + +- (MIKMIDIMachineControlDirection)direction +{ + UInt8 directionByteIndex = self.includesThreeByteManufacturerID ? 5 : 3; + MIKMIDIMachineControlDirection direction = ((UInt8 *)self.data.bytes)[directionByteIndex]; + return direction; +} + +- (void)setDirection:(MIKMIDIMachineControlDirection)direction +{ + if (![[self class] isMutable]) return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; + + UInt8 directionByteIndex = self.includesThreeByteManufacturerID ? 5 : 3; + if ([self.internalData length] <= directionByteIndex) { + [self.internalData increaseLengthBy:directionByteIndex + 1 - [self.internalData length]]; + } + + UInt8 *data = (UInt8 *)[self.internalData mutableBytes]; + data[directionByteIndex] = direction; +} + +- (MIKMIDIMachineControlCommandType)MMCCommandType +{ + UInt8 commandTypeByteIndex = self.includesThreeByteManufacturerID ? 6 : 4; + MIKMIDIMachineControlCommandType commandType = ((UInt8 *)self.data.bytes)[commandTypeByteIndex]; + return commandType; +} + +- (void)setMMCCommandType:(MIKMIDIMachineControlCommandType)MMCCommandType +{ + if (![[self class] isMutable]) return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; + + UInt8 commandTypeByteIndex = self.includesThreeByteManufacturerID ? 6 : 4; + if ([self.internalData length] <= commandTypeByteIndex) { + [self.internalData increaseLengthBy:commandTypeByteIndex + 1 - [self.internalData length]]; + } + + UInt8 *data = (UInt8 *)[self.internalData mutableBytes]; + data[commandTypeByteIndex] = MMCCommandType; +} + +@end + +@implementation MIKMutableMIDIMachineControlCommand + ++ (BOOL)isMutable { return YES; } + +#pragma mark - Properties + +@dynamic deviceAddress; +@dynamic direction; +@dynamic MMCCommandType; + +// MIKMIDICommand already implements these. This keeps the compiler happy. +@dynamic timestamp; +@dynamic dataByte1; +@dynamic dataByte2; +@dynamic midiTimestamp; +@dynamic data; +@dynamic commandType; + +@end diff --git a/Source/MIKMIDIMappableResponder.h b/Source/MIKMIDIMappableResponder.h index c8d4d46a..75c1f52c 100644 --- a/Source/MIKMIDIMappableResponder.h +++ b/Source/MIKMIDIMappableResponder.h @@ -7,8 +7,8 @@ // #import -#import "MIKMIDIResponder.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import /** * Bit-mask constants used to specify MIDI responder types for mapping. @@ -146,4 +146,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMapping.h b/Source/MIKMIDIMapping.h index 68e30bf4..430d9e4f 100644 --- a/Source/MIKMIDIMapping.h +++ b/Source/MIKMIDIMapping.h @@ -7,10 +7,10 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import -#import "MIKMIDICommand.h" -#import "MIKMIDIResponder.h" +#import +#import @protocol MIKMIDIMappableResponder; @@ -87,7 +87,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An initialized MIKMIDIMapping instance, or nil if an error occurred. */ -- (nullable instancetype)initWithFileAtURL:(NSURL *)url error:(NSError **)error; +- (nullable instancetype)initWithFileAtURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NAME(init(fileAt:)); /** * Creates and initializes an MIKMIDIMapping object that is the same as the passed in bundled mapping @@ -263,4 +263,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMapping.m b/Source/MIKMIDIMapping.m index a42e10a8..45d70e4d 100644 --- a/Source/MIKMIDIMapping.m +++ b/Source/MIKMIDIMapping.m @@ -378,11 +378,11 @@ - (BOOL)loadPropertiesFromXMLDocument:(NSXMLDocument *)xmlDocument NSArray *nameAttributes = [mapping nodesForXPath:@"./@MappingName" error:&error]; if (!nameAttributes) NSLog(@"Unable to get name attributes from MIDI Mapping XML: %@", error); - self.name = [[nameAttributes lastObject] stringValue]; + self.name = [[nameAttributes lastObject] stringValue] ? : @""; NSArray *controllerNameAttributes = [mapping nodesForXPath:@"./@ControllerName" error:&error]; if (!controllerNameAttributes) NSLog(@"Unable to get controller name attributes from MIDI Mapping XML: %@", error); - self.controllerName = [[controllerNameAttributes lastObject] stringValue]; + self.controllerName = [[controllerNameAttributes lastObject] stringValue] ? : @""; NSArray *mappingItemElements = [mapping nodesForXPath:@"./MappingItems/MappingItem" error:&error]; if (!mappingItemElements) { @@ -471,4 +471,4 @@ - (instancetype)initWithFileAtURL:(NSURL *)url return [self initWithFileAtURL:url error:NULL]; } -@end \ No newline at end of file +@end diff --git a/Source/MIKMIDIMappingGenerator.h b/Source/MIKMIDIMappingGenerator.h index 27832d22..45516258 100644 --- a/Source/MIKMIDIMappingGenerator.h +++ b/Source/MIKMIDIMappingGenerator.h @@ -7,9 +7,9 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import -#import "MIKMIDIMapping.h" +#import @class MIKMIDIDevice; @class MIKMIDIMapping; @@ -55,7 +55,7 @@ typedef void(^MIKMIDIMappingGeneratorMappingCompletionBlock)(MIKMIDIMappingItem * * @return An initialized MIKMIDIMappingGenerator instance, or nil if an error occurred. */ -+ (instancetype)mappingGeneratorWithDevice:(MIKMIDIDevice *)device error:(NSError **)error; ++ (nullable instancetype)mappingGeneratorWithDevice:(MIKMIDIDevice *)device error:(NSError **)error; /** * Creates and initializes a mapping generator for a MIKMIDIDevice. @@ -68,7 +68,7 @@ typedef void(^MIKMIDIMappingGeneratorMappingCompletionBlock)(MIKMIDIMappingItem * * @return An initialized MIKMIDIMappingGenerator instance, or nil if an error occurred. */ -- (instancetype)initWithDevice:(MIKMIDIDevice *)device error:(NSError **)error NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithDevice:(MIKMIDIDevice *)device error:(NSError **)error NS_DESIGNATED_INITIALIZER; /** * Begins mapping a given MIDIResponder. This method returns immediately. @@ -232,4 +232,4 @@ shouldRemoveExistingMappingItems:(MIKSetOf(MIKMIDIMappingItem *) *)mappingItems @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMappingItem.h b/Source/MIKMIDIMappingItem.h index fcf8b21f..7c677080 100644 --- a/Source/MIKMIDIMappingItem.h +++ b/Source/MIKMIDIMappingItem.h @@ -7,9 +7,9 @@ // #import -#import "MIKMIDIMappableResponder.h" -#import "MIKMIDICommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import @class MIKMIDIMapping; @@ -105,4 +105,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMappingManager.h b/Source/MIKMIDIMappingManager.h index 3cb14459..935b5e21 100644 --- a/Source/MIKMIDIMappingManager.h +++ b/Source/MIKMIDIMappingManager.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import #define kMIKMIDIMappingFileExtension @"midimap" diff --git a/Source/MIKMIDIMappingXMLParser.h b/Source/MIKMIDIMappingXMLParser.h index 29915dd5..1dece34b 100644 --- a/Source/MIKMIDIMappingXMLParser.h +++ b/Source/MIKMIDIMappingXMLParser.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDIMapping; @@ -26,4 +26,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaCopyrightEvent.h b/Source/MIKMIDIMetaCopyrightEvent.h index 66c34096..f6658294 100644 --- a/Source/MIKMIDIMetaCopyrightEvent.h +++ b/Source/MIKMIDIMetaCopyrightEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaCuePointEvent.h b/Source/MIKMIDIMetaCuePointEvent.h index a9afaecf..8fcce859 100644 --- a/Source/MIKMIDIMetaCuePointEvent.h +++ b/Source/MIKMIDIMetaCuePointEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaEvent.h b/Source/MIKMIDIMetaEvent.h index 6213e681..67001252 100644 --- a/Source/MIKMIDIMetaEvent.h +++ b/Source/MIKMIDIMetaEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import static const NSUInteger MIKMIDIEventMetadataStartOffset = 8; @@ -110,4 +110,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaEvent_SubclassMethods.h b/Source/MIKMIDIMetaEvent_SubclassMethods.h index cfe2dd78..ed3f1cf2 100644 --- a/Source/MIKMIDIMetaEvent_SubclassMethods.h +++ b/Source/MIKMIDIMetaEvent_SubclassMethods.h @@ -6,8 +6,8 @@ // Copyright © 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDIEvent_SubclassMethods.h" +#import +#import @interface MIKMIDIMetaEvent () diff --git a/Source/MIKMIDIMetaInstrumentNameEvent.h b/Source/MIKMIDIMetaInstrumentNameEvent.h index 0d4db2ec..16313969 100644 --- a/Source/MIKMIDIMetaInstrumentNameEvent.h +++ b/Source/MIKMIDIMetaInstrumentNameEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -30,4 +30,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaKeySignatureEvent.h b/Source/MIKMIDIMetaKeySignatureEvent.h index 4950c3f6..7050084f 100644 --- a/Source/MIKMIDIMetaKeySignatureEvent.h +++ b/Source/MIKMIDIMetaKeySignatureEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import typedef NS_ENUM(int8_t, MIKMIDIMusicalKey) { MIKMIDIMusicalKeyCFlatMajor = -7, @@ -119,4 +119,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaLyricEvent.h b/Source/MIKMIDIMetaLyricEvent.h index 29f557be..da9fabcb 100644 --- a/Source/MIKMIDIMetaLyricEvent.h +++ b/Source/MIKMIDIMetaLyricEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaMarkerTextEvent.h b/Source/MIKMIDIMetaMarkerTextEvent.h index bc1e49ad..1e758494 100644 --- a/Source/MIKMIDIMetaMarkerTextEvent.h +++ b/Source/MIKMIDIMetaMarkerTextEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaSequenceEvent.h b/Source/MIKMIDIMetaSequenceEvent.h index ee446d20..542d2280 100644 --- a/Source/MIKMIDIMetaSequenceEvent.h +++ b/Source/MIKMIDIMetaSequenceEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -29,4 +29,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaTextEvent.h b/Source/MIKMIDIMetaTextEvent.h index e603504e..60c46f81 100644 --- a/Source/MIKMIDIMetaTextEvent.h +++ b/Source/MIKMIDIMetaTextEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -37,4 +37,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaTimeSignatureEvent.h b/Source/MIKMIDIMetaTimeSignatureEvent.h index f3f9682e..771632b9 100644 --- a/Source/MIKMIDIMetaTimeSignatureEvent.h +++ b/Source/MIKMIDIMetaTimeSignatureEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import /** * Represents a time signature. Note that in contrast to time signature events in raw MIDI, @@ -105,4 +105,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetaTrackSequenceNameEvent.h b/Source/MIKMIDIMetaTrackSequenceNameEvent.h index ae6d8850..4a4f9d47 100644 --- a/Source/MIKMIDIMetaTrackSequenceNameEvent.h +++ b/Source/MIKMIDIMetaTrackSequenceNameEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIMetaTextEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -36,4 +36,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIMetronome.h b/Source/MIKMIDIMetronome.h index 05117f71..247767a1 100644 --- a/Source/MIKMIDIMetronome.h +++ b/Source/MIKMIDIMetronome.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIEndpointSynthesizer.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDINoteCommand.h b/Source/MIKMIDINoteCommand.h index 6ea4d2c7..1b20a775 100644 --- a/Source/MIKMIDINoteCommand.h +++ b/Source/MIKMIDINoteCommand.h @@ -6,8 +6,8 @@ // Copyright © 2017 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDINoteCommand_SubclassMethods.h b/Source/MIKMIDINoteCommand_SubclassMethods.h index 78c71052..3f1d2e36 100644 --- a/Source/MIKMIDINoteCommand_SubclassMethods.h +++ b/Source/MIKMIDINoteCommand_SubclassMethods.h @@ -6,9 +6,7 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDINoteCommand.h" -#import "MIKMIDIChannelVoiceCommand_SubclassMethods.h" -#import "MIKMIDICompilerCompatibility.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDINoteEvent.h b/Source/MIKMIDINoteEvent.h index 35531473..f77d8638 100644 --- a/Source/MIKMIDINoteEvent.h +++ b/Source/MIKMIDINoteEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDIClock; @@ -115,8 +115,8 @@ NS_ASSUME_NONNULL_END #pragma mark - -#import "MIKMIDINoteOnCommand.h" -#import "MIKMIDINoteOffCommand.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -134,4 +134,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDINoteOffCommand.h b/Source/MIKMIDINoteOffCommand.h index ffba1715..051c41dc 100644 --- a/Source/MIKMIDINoteOffCommand.h +++ b/Source/MIKMIDINoteOffCommand.h @@ -6,7 +6,7 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDINoteCommand.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDINoteOffCommand.m b/Source/MIKMIDINoteOffCommand.m index ccc5b3e3..be812a58 100644 --- a/Source/MIKMIDINoteOffCommand.m +++ b/Source/MIKMIDINoteOffCommand.m @@ -9,6 +9,7 @@ #import "MIKMIDINoteOffCommand.h" #import "MIKMIDINoteCommand_SubclassMethods.h" #import "MIKMIDINoteOnCommand.h" +#import "MIKMIDICommand_SubclassMethods.h" #import "MIKMIDIUtilities.h" #if !__has_feature(objc_arc) diff --git a/Source/MIKMIDINoteOnCommand.h b/Source/MIKMIDINoteOnCommand.h index 9e3e08be..c1f0aba6 100644 --- a/Source/MIKMIDINoteOnCommand.h +++ b/Source/MIKMIDINoteOnCommand.h @@ -6,7 +6,7 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDINoteCommand.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDINoteOnCommand.m b/Source/MIKMIDINoteOnCommand.m index 539e845c..69b4dd82 100644 --- a/Source/MIKMIDINoteOnCommand.m +++ b/Source/MIKMIDINoteOnCommand.m @@ -8,6 +8,7 @@ #import "MIKMIDINoteOnCommand.h" #import "MIKMIDINoteCommand_SubclassMethods.h" +#import "MIKMIDICommand_SubclassMethods.h" #import "MIKMIDIUtilities.h" #if !__has_feature(objc_arc) diff --git a/Source/MIKMIDIObject.h b/Source/MIKMIDIObject.h index d7026d35..ddcdf909 100644 --- a/Source/MIKMIDIObject.h +++ b/Source/MIKMIDIObject.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -101,4 +101,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIObject_SubclassMethods.h b/Source/MIKMIDIObject_SubclassMethods.h index d4b1d95a..88237ff7 100644 --- a/Source/MIKMIDIObject_SubclassMethods.h +++ b/Source/MIKMIDIObject_SubclassMethods.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIObject.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -59,4 +59,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIOutputPort.h b/Source/MIKMIDIOutputPort.h index 9a6e7aae..f4503b49 100644 --- a/Source/MIKMIDIOutputPort.h +++ b/Source/MIKMIDIOutputPort.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIPort.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDICommand; @class MIKMIDIDestinationEndpoint; diff --git a/Source/MIKMIDIPitchBendChangeCommand.h b/Source/MIKMIDIPitchBendChangeCommand.h index fbb442bf..9b3d81f8 100644 --- a/Source/MIKMIDIPitchBendChangeCommand.h +++ b/Source/MIKMIDIPitchBendChangeCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -18,6 +18,16 @@ NS_ASSUME_NONNULL_BEGIN */ @interface MIKMIDIPitchBendChangeCommand : MIKMIDIChannelVoiceCommand +/** + * Convenience method for creating a pitch bend change command. + * + * @param pitchChange The pitch change for the command. Valid range: 0-16383, center (no pitch change) at 8192. + * @param channel The channel for the command. Must be between 0 and 15. + * @param timestamp The timestamp for the command. Pass nil to use the current date/time. + * @return An initialized MIKMIDIChannelPressureCommand instance. + */ ++ (instancetype)pitchBendChangeCommandWithPitchChange:(UInt16)pitchChange channel:(UInt8)channel timestamp:(nullable NSDate *)timestamp; + /** * A 14-bit value indicating the pitch bend. * Center is 0x2000 (8192). @@ -44,4 +54,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIPitchBendChangeCommand.m b/Source/MIKMIDIPitchBendChangeCommand.m index 00763ccc..04cb911c 100644 --- a/Source/MIKMIDIPitchBendChangeCommand.m +++ b/Source/MIKMIDIPitchBendChangeCommand.m @@ -31,6 +31,17 @@ - (NSString *)additionalCommandDescription return [NSString stringWithFormat:@"pitch change: %u", (unsigned)self.pitchChange]; } ++ (instancetype)pitchBendChangeCommandWithPitchChange:(UInt16)pitchChange channel:(UInt8)channel timestamp:(nullable NSDate *)timestamp +{ + MIKMutableMIDIPitchBendChangeCommand *result = [[MIKMutableMIDIPitchBendChangeCommand alloc] init]; + result.pitchChange = pitchChange; + result.channel = channel; + result.timestamp = timestamp ?: [NSDate date]; + + return [self isMutable] ? result : [result copy]; +} + + #pragma mark - Properties - (UInt16)pitchChange @@ -84,4 +95,4 @@ - (void)setCommandType:(MIKMIDICommandType)commandType data[0] &= 0x0F | (commandType & 0xF0); // Need to avoid changing channel } -@end \ No newline at end of file +@end diff --git a/Source/MIKMIDIPitchBendChangeEvent.h b/Source/MIKMIDIPitchBendChangeEvent.h index 2c84e07f..01411cdf 100644 --- a/Source/MIKMIDIPitchBendChangeEvent.h +++ b/Source/MIKMIDIPitchBendChangeEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -43,4 +43,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIPlayer.h b/Source/MIKMIDIPlayer.h index 79b08225..2b0915c1 100644 --- a/Source/MIKMIDIPlayer.h +++ b/Source/MIKMIDIPlayer.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDISequence; @class MIKMIDIMetronome; diff --git a/Source/MIKMIDIPolyphonicKeyPressureCommand.h b/Source/MIKMIDIPolyphonicKeyPressureCommand.h index 57250a03..b5fa74df 100644 --- a/Source/MIKMIDIPolyphonicKeyPressureCommand.h +++ b/Source/MIKMIDIPolyphonicKeyPressureCommand.h @@ -6,7 +6,7 @@ // Copyright © 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" +#import /** * A MIDI polyphonic key pressure message. This message is most often sent by pressing diff --git a/Source/MIKMIDIPolyphonicKeyPressureEvent.h b/Source/MIKMIDIPolyphonicKeyPressureEvent.h index 132317da..ecb83cd7 100644 --- a/Source/MIKMIDIPolyphonicKeyPressureEvent.h +++ b/Source/MIKMIDIPolyphonicKeyPressureEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -46,4 +46,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIPort.h b/Source/MIKMIDIPort.h index e1cc52bb..c44cadcc 100644 --- a/Source/MIKMIDIPort.h +++ b/Source/MIKMIDIPort.h @@ -6,9 +6,9 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIObject.h" +#import #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDIEndpoint; @@ -26,4 +26,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIPrivateUtilities.h b/Source/MIKMIDIPrivateUtilities.h index df1bf0ac..c77f8475 100644 --- a/Source/MIKMIDIPrivateUtilities.h +++ b/Source/MIKMIDIPrivateUtilities.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDIChannelVoiceCommand; diff --git a/Source/MIKMIDIProgramChangeCommand.h b/Source/MIKMIDIProgramChangeCommand.h index 032422cc..3d473bd9 100644 --- a/Source/MIKMIDIProgramChangeCommand.h +++ b/Source/MIKMIDIProgramChangeCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelVoiceCommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -39,4 +39,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIProgramChangeEvent.h b/Source/MIKMIDIProgramChangeEvent.h index 10b5c35a..857318f7 100644 --- a/Source/MIKMIDIProgramChangeEvent.h +++ b/Source/MIKMIDIProgramChangeEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDIChannelEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -50,4 +50,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIResponder.h b/Source/MIKMIDIResponder.h index 7ae52e0b..937595dc 100644 --- a/Source/MIKMIDIResponder.h +++ b/Source/MIKMIDIResponder.h @@ -7,7 +7,7 @@ // #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDICommand; @@ -75,4 +75,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDISequence+MIKMIDIPrivate.h b/Source/MIKMIDISequence+MIKMIDIPrivate.h index 290d26d7..00bec8e6 100644 --- a/Source/MIKMIDISequence+MIKMIDIPrivate.h +++ b/Source/MIKMIDISequence+MIKMIDIPrivate.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDISequence.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDISequencer; diff --git a/Source/MIKMIDISequence.h b/Source/MIKMIDISequence.h index 2c442ff5..93feb58d 100644 --- a/Source/MIKMIDISequence.h +++ b/Source/MIKMIDISequence.h @@ -8,8 +8,8 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" -#import "MIKMIDIMetaTimeSignatureEvent.h" +#import +#import @class MIKMIDITrack; @class MIKMIDISequencer; @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN * @see MIKMIDITrack * @see MIKMIDISequencer */ -@interface MIKMIDISequence : NSObject +@interface MIKMIDISequence : NSObject /** * Creates and initializes a new instance of MIKMIDISequence. @@ -120,7 +120,7 @@ NS_ASSUME_NONNULL_BEGIN * @return If an error occurs, upon return contains an NSError object that describes the problem. If you are not interested in possible errors, * you may pass in NULL. */ -- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)error; +- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)error NS_SWIFT_NAME(init(data:)); /** * Initializes a new instance of MIKMIDISequence from MIDI data. @@ -271,6 +271,43 @@ NS_ASSUME_NONNULL_BEGIN */ - (MIKMIDITimeSignature)timeSignatureAtTimeStamp:(MusicTimeStamp)timeStamp; +#pragma mark - Timing + + +/** Returns the time in seconds for a given MusicTimeStamp (time in beats) in the sequence. + * + * This method converts a time in beats to the corresponding time in seconds in the sequence, taking into account the tempo of the sequence, including tempo changes. + * + * @note This methhod only considers the sequence itself. If you're playing the sequence using an MIKMIDISequencer, + * you should use the corresponding methods on MIKMIDISequencer, which take into account looping, tempo overrides, and provide options + * to control the details of the conversion algorithm. + * + * @param musicTimeStamp The time in beats you want to convert to seconds. + * + * @return A time in seconds as an NSTimeInterval. + * + * @see -musicTimeStampForTimeInSeconds: + * @see -[MIKMIDISequencer timeInSecondsForMusicTimeStamp:options:] + */ +- (NSTimeInterval)timeInSecondsForMusicTimeStamp:(MusicTimeStamp)musicTimeStamp; + +/** Returns the time in beats for a given time in seconds in the sequence. +* +* This method converts a time in seconds to the corresponding time in beats in the sequence, taking into account the tempo of the sequence, including tempo changes. +* +* @note This methhod only considers the sequence itself. If you're playing the sequence using an MIKMIDISequencer, +* you should use the corresponding methods on MIKMIDISequencer, which take into account looping, tempo overrides, and provide options +* to control the details of the conversion algorithm. +* +* @param timeInSeconds The time in seconds you want to convert to a MusicTimeStamp (beats). +* +* @return A time in beats as a MusicTimeStamp. +* +* @see -timeInSecondsForMusicTimeStamp: +* @see -[MIKMIDISequencer timeInSecondsForMusicTimeStamp:options:] +*/ +- (MusicTimeStamp)musicTimeStampForTimeInSeconds:(NSTimeInterval)timeInSeconds; + #pragma mark - Properties /** diff --git a/Source/MIKMIDISequence.m b/Source/MIKMIDISequence.m index a6b641bd..dded4fb1 100644 --- a/Source/MIKMIDISequence.m +++ b/Source/MIKMIDISequence.m @@ -10,6 +10,7 @@ #import #import "MIKMIDITrack.h" #import "MIKMIDITrack_Protected.h" +#import "MIKMIDITempoTrack.h" #import "MIKMIDITempoEvent.h" #import "MIKMIDIMetaTimeSignatureEvent.h" #import "MIKMIDIDestinationEndpoint.h" @@ -32,6 +33,8 @@ @interface MIKMIDISequence () @property (nonatomic, strong) NSMutableArray *internalTracks; @property (nonatomic) MusicTimeStamp lengthDefinedByTracks; +@property (nonatomic, getter=isLengthUpdatingDisabled) BOOL lengthUpdatingDisabled; + @end @@ -141,7 +144,7 @@ - (instancetype)initWithMusicSequence:(MusicSequence)musicSequence error:(NSErro *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]; return nil; } - self.tempoTrack = [MIKMIDITrack trackWithSequence:self musicTrack:tempoTrack]; + self.tempoTrack = [MIKMIDITempoTrack trackWithSequence:self musicTrack:tempoTrack]; UInt32 numTracks = 0; err = MusicSequenceGetTrackCount(musicSequence, &numTracks); @@ -184,6 +187,27 @@ - (void)dealloc if (err) NSLog(@"DisposeMusicSequence() failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__); } +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + NSError *error = nil; + MIKMIDISequence *newSequence = [MIKMIDISequence sequence]; + newSequence.lengthUpdatingDisabled = YES; + [newSequence.tempoTrack copyEventsFromMIDITrack:self.tempoTrack fromTimeStamp:0 toTimeStamp:self.tempoTrack.length andInsertAtTimeStamp:0]; + for (MIKMIDITrack *track in self.tracks) { + MIKMIDITrack *newTrack = [newSequence addTrackWithError:&error]; + if (!newTrack) { + NSLog(@"Error creating new track during copy of %@: %@", self, error); + return nil; + } + [newTrack copyEventsFromMIDITrack:track fromTimeStamp:0 toTimeStamp:track.length andInsertAtTimeStamp:0]; + } + + newSequence.lengthUpdatingDisabled = NO; + return newSequence; +} + #pragma mark - Sequencer Synchronization - (void)dispatchSyncToSequencerProcessingQueueAsNeeded:(void (^)(void))block @@ -291,7 +315,7 @@ - (MusicTimeStamp)equivalentTimeStampForLoopedTimeStamp:(MusicTimeStamp)loopedTi - (NSArray *)tempoEvents { - return [self.tempoTrack eventsOfClass:[MIKMIDITempoEvent class] fromTimeStamp:0 toTimeStamp:kMusicTimeStamp_EndOfTrack]; + return [(MIKMIDITempoTrack *)self.tempoTrack tempoEvents]; } - (BOOL)setOverallTempo:(Float64)bpm @@ -361,6 +385,22 @@ - (MIKMIDITimeSignature)timeSignatureAtTimeStamp:(MusicTimeStamp)timeStamp return result; } +#pragma mark - Timing + +- (NSTimeInterval)timeInSecondsForMusicTimeStamp:(MusicTimeStamp)musicTimeStamp +{ + Float64 result = 0; + MusicSequenceGetSecondsForBeats(self.musicSequence, musicTimeStamp, &result); + return (NSTimeInterval)result; +} + +- (MusicTimeStamp)musicTimeStampForTimeInSeconds:(NSTimeInterval)timeInSeconds +{ + MusicTimeStamp result = 0; + MusicSequenceGetBeatsForSeconds(self.musicSequence, (Float64)timeInSeconds, &result); + return result; +} + #pragma mark - Description - (NSString *)description @@ -498,6 +538,16 @@ - (NSData *)dataValue return (__bridge_transfer NSData *)data; } +- (void)setLengthUpdatingDisabled:(BOOL)lengthUpdatingDisabled +{ + if (lengthUpdatingDisabled != _lengthUpdatingDisabled) { + _lengthUpdatingDisabled = lengthUpdatingDisabled; + if (!_lengthUpdatingDisabled) { + [self updateLengthDefinedByTracks]; + } + } +} + #pragma mark - Deprecated + (instancetype)sequenceWithData:(NSData *)data diff --git a/Source/MIKMIDISequencer+MIKMIDIPrivate.h b/Source/MIKMIDISequencer+MIKMIDIPrivate.h index 9e87687c..b215fe6e 100644 --- a/Source/MIKMIDISequencer+MIKMIDIPrivate.h +++ b/Source/MIKMIDISequencer+MIKMIDIPrivate.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDISequencer.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDISequencer.h b/Source/MIKMIDISequencer.h index 790212d3..bd81a0e3 100644 --- a/Source/MIKMIDISequencer.h +++ b/Source/MIKMIDISequencer.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDISequence; @class MIKMIDITrack; @@ -35,6 +35,29 @@ typedef NS_ENUM(NSInteger, MIKMIDISequencerClickTrackStatus) { MIKMIDISequencerClickTrackStatusAlwaysEnabled }; +typedef NS_OPTIONS(NSInteger, MIKMIDISequencerTimeConversionOptions) { + /** Use default options (consider tempo override and looping, don't unroll loops) */ + MIKMIDISequencerTimeConversionOptionsNone = 0, + /** Use the sequence's tempo events to calculate conversion, even if the sequencer has a tempo override set. The default is to use the overridden tempo for calculation if one is set.*/ + MIKMIDISequencerTimeConversionOptionsIgnoreTempoOverride = 1 << 0, + /** Calculate conversion as if looping were disabled. The default is to take into account looping if it is enabled on the sequencer.*/ + MIKMIDISequencerTimeConversionOptionsIgnoreLooping = 1 << 1, + /** When this option is set, conversion will return the time of events currently being played relative to the start of the sequence, and the result will never been greater than the end of the loop. The default, where this option is not set, is to calculate and return the absolute time since the sequence start. + + For example, consider a sequence that is 16 beats long, the tempo is a constant 75 bpm and looping is enabled for first 8 beats. The sequence will be exactly 20 seconds long, and the loop will consist of the first 10 seconds. + + If this option is *set*, and a time of 25 seconds is passed in, the result will be 4 beats, because the sequencer will be at the half way point of the loop on its third time through. If this option is *not set*, the result will be 20 beats, because 20 beats total will have elapsed since the start of the sequence. + + Setting the option allows you to determine what part of the raw sequence is currently being played, while leaving it unset allows you to determine total playback time. + + The same concept applies for conversion from beats to seconds.*/ + MIKMIDISequencerTimeConversionOptionsDontUnrollLoop = 1 << 2, + /** + When this option is set, the sequencer's rate will be ignore, and the default rate of 1.0 will be used for time conversion calculations. + */ + MIKMIDISequencerTimeConversionOptionsIgnoreRate = 1 << 3, +}; + NS_ASSUME_NONNULL_BEGIN /** @@ -255,6 +278,36 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable MIKMIDISynthesizer *)builtinSynthesizerForTrack:(MIKMIDITrack *)track; +#pragma mark - Time Conversion + + +/** Returns the time in seconds for a given MusicTimeStamp (time in beats). + * + * This method converts a time in beats to the corresponding time in seconds on the sequencer, taking into account the tempo of the sequence, including tempo changes. + * By default, looping and an overridden tempo, if enabled, will be considered when calculating the result. This behavior can be changed by passing in the appropriate options. + * + * @param musicTimeStamp The time in beats you want to convert to seconds. + * @param options Options to control the details of the conversion algorithm. See MIKMIDISequencerTimeConversionOptions for a list of possible options. + * + * @return A time in seconds as an NSTimeInterval. + * + * @see -musicTimeStampForTimeInSeconds:options: + * @see -[MIKMIDISequence musicTimeStampForTimeInSeconds:] + */ +- (NSTimeInterval)timeInSecondsForMusicTimeStamp:(MusicTimeStamp)musicTimeStamp options:(MIKMIDISequencerTimeConversionOptions)options; + +/** Returns the time in beats for a given time in seconds. + * + * @param timeInSeconds The time in seconds you want to convert to a MusicTimeStamp (beats). + * @param options Options to control the details of the conversion algorithm. See MIKMIDISequencerTimeConversionOptions for a list of possible options. + * + * @return A time in beats as a MusicTimeStamp. + * + * @see -timeInSecondsForMusicTimeStamp:options: + * @see -[MIKMIDISequence timeInSecondsForMusicTimeStamp:] + */ +- (MusicTimeStamp)musicTimeStampForTimeInSeconds:(NSTimeInterval)timeInSeconds options:(MIKMIDISequencerTimeConversionOptions)options; + #pragma mark - Properties /** @@ -281,6 +334,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly, nonatomic, getter=isRecording) BOOL recording; +/** + * @property rate + * @abstract The playback rate of the sequencer. For example, if rate is 2.0, the sequencer will play twice as fast as normal. + * Unlike the tempo property, this does not override the tempos in the sequence's tempo track. Rather, they are adjusted by multiplying by this rate. + * @discussion + * 1.0 is normal playback rate. Rate must be > 0.0. +*/ +@property (nonatomic) float rate; + /** * The tempo the sequencer should play its sequence at. When set to 0, the sequence will be played using * the tempo events from the sequence's tempo track. Default is 0. @@ -475,4 +537,4 @@ FOUNDATION_EXPORT NSString * const MIKMIDISequencerWillLoopNotification; */ FOUNDATION_EXPORT const MusicTimeStamp MIKMIDISequencerEndOfSequenceLoopEndTimeStamp; -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDISequencer.m b/Source/MIKMIDISequencer.m index b226efe2..0b45b1e3 100644 --- a/Source/MIKMIDISequencer.m +++ b/Source/MIKMIDISequencer.m @@ -119,6 +119,7 @@ - (instancetype)initWithSequence:(MIKMIDISequence *)sequence _processingQueueKey = &_processingQueueKey; _processingQueueContext = &_processingQueueContext; _maximumLookAheadInterval = 0.1; + _rate = 1.0; } return self; } @@ -193,6 +194,7 @@ - (void)startPlaybackAtTimeStamp:(MusicTimeStamp)timeStamp MIDITimeStamp:(MIDITi Float64 startingTempo = [self.sequence tempoAtTimeStamp:timeStamp]; if (!startingTempo) startingTempo = kDefaultTempo; + startingTempo *= self.rate; [self updateClockWithMusicTimeStamp:timeStamp tempo:startingTempo atMIDITimeStamp:midiTimeStamp]; }); @@ -314,8 +316,8 @@ - (void)processSequenceStartingFromMIDITimeStamp:(MIDITimeStamp)fromMIDITimeStam if (self.needsCurrentTempoUpdate) { if (!tempoEventsByTimeStamp.count) { - if (!overrideTempo) overrideTempo = [sequence tempoAtTimeStamp:fromMusicTimeStamp]; - if (!overrideTempo) overrideTempo = kDefaultTempo; + if (!overrideTempo) overrideTempo = [sequence tempoAtTimeStamp:fromMusicTimeStamp] * self.rate; + if (!overrideTempo) overrideTempo = kDefaultTempo * self.rate; MIKMIDITempoEvent *tempoEvent = [MIKMIDITempoEvent tempoEventWithTimeStamp:fromMusicTimeStamp tempo:overrideTempo]; NSNumber *timeStampKey = @(fromMusicTimeStamp); @@ -393,7 +395,7 @@ - (void)processSequenceStartingFromMIDITimeStamp:(MIDITimeStamp)fromMIDITimeStam 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]; + if (tempoEventAtTimeStamp) [self updateClockWithMusicTimeStamp:musicTimeStamp tempo:tempoEventAtTimeStamp.bpm * self.rate atMIDITimeStamp:midiTimeStamp]; NSArray *events = allEventsByTimeStamp[timeStampKey]; for (id eventObject in events) { @@ -657,6 +659,185 @@ - (MIKMIDISynthesizer *)builtinSynthesizerForTrack:(MIKMIDITrack *)track return [self.tracksToDefaultSynthsMap objectForKey:track]; } +#pragma mark - Time Conversion + +- (NSTimeInterval)timeInSecondsForMusicTimeStamp:(MusicTimeStamp)musicTimeStamp options:(MIKMIDISequencerTimeConversionOptions)options +{ + if (musicTimeStamp < 0) { return 0; } + + BOOL shouldIgnoreTempoOverride = options & MIKMIDISequencerTimeConversionOptionsIgnoreTempoOverride; + BOOL shouldIgnoreRate = options & MIKMIDISequencerTimeConversionOptionsIgnoreRate; + BOOL ignoreLooping = options & MIKMIDISequencerTimeConversionOptionsIgnoreLooping; + + // If result is beyond the end of the loop + if (!ignoreLooping && self.shouldLoop && musicTimeStamp >= self.loopEndTimeStamp) { + options |= MIKMIDISequencerTimeConversionOptionsIgnoreLooping; + MusicTimeStamp loopDuration = self.loopEndTimeStamp - self.loopStartTimeStamp; + NSTimeInterval loopStartTimeInSeconds = [self timeInSecondsForMusicTimeStamp:self.loopStartTimeStamp options:options]; + NSTimeInterval loopEndTimeInSeconds = [self timeInSecondsForMusicTimeStamp:self.loopEndTimeStamp options:options]; + NSTimeInterval loopDurationInSeconds = loopEndTimeInSeconds - loopStartTimeInSeconds; + + MusicTimeStamp scratch = musicTimeStamp; + scratch -= self.loopStartTimeStamp; // Subtract off time before the loop + NSTimeInterval result = loopStartTimeInSeconds; + BOOL shouldUnroll = !(options & MIKMIDISequencerTimeConversionOptionsDontUnrollLoop); + // "Use up" the loops until we're down to a fraction of a loop + while (scratch >= loopDuration) { + // Only add time for full loops to result if we're unrolling loops + if (shouldUnroll) { + result += loopDurationInSeconds; + } + scratch -= loopDuration; + } + // Add the remaining fraction of a loop + result += [self timeInSecondsForMusicTimeStamp:(self.loopStartTimeStamp + scratch) options:options]; + result -= loopStartTimeInSeconds; + return result; + } + + // Calculate initial tempo, handling case where sequence doesn't specify one. + NSArray *tempoEvents = self.sequence.tempoEvents; + if (!shouldIgnoreRate) { + NSMutableArray *scratch = [NSMutableArray array]; + for (MIKMIDITempoEvent *event in tempoEvents) { + MIKMutableMIDITempoEvent *adjustedEvent = [event mutableCopy]; + adjustedEvent.bpm *= self.rate; + [scratch addObject:[adjustedEvent copy]]; + } + tempoEvents = [scratch copy]; + } + if (self.tempo != 0 && !shouldIgnoreTempoOverride) { // Overridden tempo that should be used instead of events in the tempo track + Float64 tempo = self.tempo * (shouldIgnoreRate ? 1.0 : self.rate); + tempoEvents = @[[MIKMIDITempoEvent tempoEventWithTimeStamp:0 tempo:tempo]]; + } else { + NSUInteger tempoAtZeroIndex = [tempoEvents indexOfObjectPassingTest:^BOOL(MIKMIDITempoEvent *event, NSUInteger i, BOOL *s) { + return event.timeStamp == 0; + }]; + if (tempoAtZeroIndex == NSNotFound) { + NSMutableArray *scratch = [tempoEvents mutableCopy]; + Float64 tempo = kDefaultTempo * (shouldIgnoreRate ? 1.0 : self.rate); + MIKMIDITempoEvent *initialTempo = [MIKMIDITempoEvent tempoEventWithTimeStamp:0 tempo:tempo]; + [scratch insertObject:initialTempo atIndex:0]; + tempoEvents = [scratch copy]; + } + } + + // Get tempo events that affect the result (ie. come before musicTimeStamp) and sort them in ascending order + // Check if result would be affected by loop + BOOL timeIsInLoop = self.shouldLoop && musicTimeStamp >= self.loopEndTimeStamp && !ignoreLooping; + NSIndexSet *indexesOfTempoEventsAffectingResult = + [tempoEvents indexesOfObjectsPassingTest:^BOOL(MIKMIDITempoEvent *event, NSUInteger i, BOOL *s) { + // if musicTimeStamp is within the loop region, include all tempo events up to the end of the loop + MusicTimeStamp limit = timeIsInLoop ? self.loopEndTimeStamp : musicTimeStamp; + return event.timeStamp <= limit; + }]; + tempoEvents = [tempoEvents objectsAtIndexes:indexesOfTempoEventsAffectingResult]; + + NSTimeInterval result = 0.0; + MIKMIDITempoEvent *lastTempoEvent = tempoEvents[0]; + for (MIKMIDITempoEvent *tempoEvent in tempoEvents) { + result += 60.0 * (tempoEvent.timeStamp - lastTempoEvent.timeStamp) / lastTempoEvent.bpm; + lastTempoEvent = tempoEvent; + } + result += 60.0 * (musicTimeStamp - lastTempoEvent.timeStamp) / lastTempoEvent.bpm; + return result; +} + +- (MusicTimeStamp)musicTimeStampForTimeInSeconds:(NSTimeInterval)timeInSeconds options:(MIKMIDISequencerTimeConversionOptions)options +{ + BOOL shouldIgnoreTempoOverride = (options & MIKMIDISequencerTimeConversionOptionsIgnoreTempoOverride) != 0; + BOOL shouldIgnoreRate = (options & MIKMIDISequencerTimeConversionOptionsIgnoreRate) != 0; + BOOL ignoreLooping = (options & MIKMIDISequencerTimeConversionOptionsIgnoreLooping) != 0; + + MIKMIDISequencerTimeConversionOptions loopCalcOptions = MIKMIDISequencerTimeConversionOptionsIgnoreLooping; + if (shouldIgnoreRate) { loopCalcOptions |= MIKMIDISequencerTimeConversionOptionsIgnoreRate; } + NSTimeInterval loopEndTimeInSeconds = self.loopEndTimeStamp > 0 ? [self timeInSecondsForMusicTimeStamp:self.loopEndTimeStamp options:loopCalcOptions] : self.sequence.durationInSeconds; + // If result is beyond the end of the loop + if (!ignoreLooping && self.shouldLoop && timeInSeconds >= loopEndTimeInSeconds) { + options |= MIKMIDISequencerTimeConversionOptionsIgnoreLooping; + MusicTimeStamp loopDuration = self.loopEndTimeStamp - self.loopStartTimeStamp; + NSTimeInterval loopStartTimeInSeconds = [self timeInSecondsForMusicTimeStamp:self.loopStartTimeStamp options:loopCalcOptions]; + NSTimeInterval loopDurationInSeconds = loopEndTimeInSeconds - loopStartTimeInSeconds; + + NSTimeInterval scratch = timeInSeconds; + scratch -= loopStartTimeInSeconds; // Subtract off time before the loop + MusicTimeStamp result = self.loopStartTimeStamp; + BOOL shouldUnroll = !(options & MIKMIDISequencerTimeConversionOptionsDontUnrollLoop); + // "Use up" the loops until we're down to a fraction of a loop + while (scratch >= loopDurationInSeconds) { + // Only add time for full loops to result if we're unrolling loops + if (shouldUnroll) { + result += loopDuration; + } + scratch -= loopDurationInSeconds; + } + // Add the remaining fraction of a loop + result += [self musicTimeStampForTimeInSeconds:(loopStartTimeInSeconds + scratch) options:options]; + result -= self.loopStartTimeStamp; + return result; + } + + // Calculate initial tempo, handling case where sequence doesn't specify one. + NSArray *tempoEvents = self.sequence.tempoEvents; + if (!shouldIgnoreRate) { + NSMutableArray *scratch = [NSMutableArray array]; + for (MIKMIDITempoEvent *event in tempoEvents) { + MIKMutableMIDITempoEvent *adjustedEvent = [event mutableCopy]; + adjustedEvent.bpm *= self.rate; + [scratch addObject:[adjustedEvent copy]]; + } + tempoEvents = [scratch copy]; + } + if (self.tempo != 0 && !shouldIgnoreTempoOverride) { // Overridden tempo that should be used instead of events in the tempo track + Float64 tempo = self.tempo * (shouldIgnoreRate ? 1.0 : self.rate); + tempoEvents = @[[MIKMIDITempoEvent tempoEventWithTimeStamp:0 tempo:tempo]]; + } else { + NSUInteger tempoAtZeroIndex = [tempoEvents indexOfObjectPassingTest:^BOOL(MIKMIDITempoEvent *event, NSUInteger i, BOOL *s) { + return event.timeStamp == 0; + }]; + if (tempoAtZeroIndex == NSNotFound) { + NSMutableArray *scratch = [tempoEvents mutableCopy]; + Float64 tempo = kDefaultTempo * (shouldIgnoreRate ? 1.0 : self.rate); + MIKMIDITempoEvent *initialTempo = [MIKMIDITempoEvent tempoEventWithTimeStamp:0 tempo:tempo]; + [scratch insertObject:initialTempo atIndex:0]; + tempoEvents = [scratch copy]; + } + } + + MIKMIDISequencerTimeConversionOptions ignoreOverridesIfNeeded = MIKMIDISequencerTimeConversionOptionsNone; + if (shouldIgnoreTempoOverride) { ignoreOverridesIfNeeded |= MIKMIDISequencerTimeConversionOptionsIgnoreTempoOverride; } + if (shouldIgnoreRate) { ignoreOverridesIfNeeded |= MIKMIDISequencerTimeConversionOptionsIgnoreRate; } + if (ignoreLooping) { ignoreOverridesIfNeeded |= MIKMIDISequencerTimeConversionOptionsIgnoreLooping; } + + // Get tempo events that affect the result (ie. come before timeInSeconds) and sort them in ascending order + // Check if result would be affected by loop + BOOL timeIsInLoop = self.shouldLoop && timeInSeconds >= loopEndTimeInSeconds && !ignoreLooping; + NSIndexSet *indexesOfTempoEventsAffectingResult = + [tempoEvents indexesOfObjectsPassingTest:^BOOL(MIKMIDITempoEvent *event, NSUInteger i, BOOL *s) { + // if timeInSeconds is within the loop region, include all tempo events up to the end of the loop + NSTimeInterval limit = timeIsInLoop ? loopEndTimeInSeconds : timeInSeconds; + NSTimeInterval timeInSeconds = [self.sequence timeInSecondsForMusicTimeStamp:event.timeStamp]; + if (!shouldIgnoreRate) { timeInSeconds /= self.rate; } + return timeInSeconds <= limit; + }]; + NSArray *tempoEventsAffectingResult = [tempoEvents objectsAtIndexes:indexesOfTempoEventsAffectingResult]; + tempoEvents = [tempoEventsAffectingResult sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:YES]]]; + + MusicTimeStamp result = 0.0; + MIKMIDITempoEvent *lastTempoEvent = tempoEvents[0]; + NSTimeInterval tempoTimeInSeconds = 0; + NSTimeInterval lastTempoTimeInSeconds = [self timeInSecondsForMusicTimeStamp:lastTempoEvent.timeStamp options:ignoreOverridesIfNeeded]; + for (MIKMIDITempoEvent *tempoEvent in tempoEvents) { + lastTempoTimeInSeconds = tempoTimeInSeconds; + tempoTimeInSeconds = [self timeInSecondsForMusicTimeStamp:tempoEvent.timeStamp options:ignoreOverridesIfNeeded]; + result += lastTempoEvent.bpm * (tempoTimeInSeconds - lastTempoTimeInSeconds) / 60.0; + lastTempoEvent = tempoEvent; + } + lastTempoTimeInSeconds = [self timeInSecondsForMusicTimeStamp:lastTempoEvent.timeStamp options:ignoreOverridesIfNeeded]; + result += lastTempoEvent.bpm * (timeInSeconds - lastTempoTimeInSeconds) / 60.0; + return result; +} + #pragma mark - Click Track - (NSMutableArray *)clickTrackEventsFromTimeStamp:(MusicTimeStamp)fromTimeStamp toTimeStamp:(MusicTimeStamp)toTimeStamp @@ -809,6 +990,14 @@ - (MIKMIDIMetronome *)metronome #endif } +- (void)setRate:(float)rate +{ + if (rate != _rate && rate > 0.0) { + _rate = rate; + if (self.isPlaying) self.needsCurrentTempoUpdate = YES; + } +} + - (void)setTempo:(Float64)tempo { if (tempo < 0) tempo = 0; diff --git a/Source/MIKMIDISourceEndpoint.h b/Source/MIKMIDISourceEndpoint.h index 6a0027e5..126330ac 100644 --- a/Source/MIKMIDISourceEndpoint.h +++ b/Source/MIKMIDISourceEndpoint.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDIEndpoint.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import @class MIKMIDISourceEndpoint; @class MIKMIDICommand; @@ -41,4 +41,4 @@ typedef void(^MIKMIDIEventHandlerBlock)(MIKMIDISourceEndpoint *source, MIKArrayO @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDISynthesizer.h b/Source/MIKMIDISynthesizer.h index 332a21a6..3e78ae3c 100644 --- a/Source/MIKMIDISynthesizer.h +++ b/Source/MIKMIDISynthesizer.h @@ -8,9 +8,9 @@ #import #import -#import "MIKMIDISynthesizerInstrument.h" -#import "MIKMIDICommandScheduler.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -40,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An initialized MIKMIDIEndpointSynthesizer or nil if an error occurs. */ -- (nullable instancetype)initWithError:(NSError **)error; +- (nullable instancetype)initWithError:(NSError **)error NS_SWIFT_NAME(init()); /** * Initializes an MIKMIDISynthesizer instance which uses an audio unit matching @@ -52,7 +52,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An initialized MIKMIDIEndpointSynthesizer or nil if an error occurs. */ -- (nullable instancetype)initWithAudioUnitDescription:(AudioComponentDescription)componentDescription error:(NSError **)error NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithAudioUnitDescription:(AudioComponentDescription)componentDescription error:(NSError **)error NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(audioUnitDescription:)); /** * This synthesizer's available instruments. An array of @@ -76,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN * * @see +[MIKMIDISynthesizerInstrument availableInstruments] */ -- (BOOL)selectInstrument:(MIKMIDISynthesizerInstrument *)instrument error:(NSError **)error; +- (BOOL)selectInstrument:(MIKMIDISynthesizerInstrument *)instrument error:(NSError **)error NS_SWIFT_NAME(select(instrument:)); /** * Loads the sound font (.dls or .sf2) file at fileURL. diff --git a/Source/MIKMIDISynthesizerInstrument.h b/Source/MIKMIDISynthesizerInstrument.h index 97fb1c53..70a0f9ee 100644 --- a/Source/MIKMIDISynthesizerInstrument.h +++ b/Source/MIKMIDISynthesizerInstrument.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -67,4 +67,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDISynthesizer_SubclassMethods.h b/Source/MIKMIDISynthesizer_SubclassMethods.h index 763e2dd7..11b7340b 100644 --- a/Source/MIKMIDISynthesizer_SubclassMethods.h +++ b/Source/MIKMIDISynthesizer_SubclassMethods.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDISynthesizer.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDISystemExclusiveCommand.h b/Source/MIKMIDISystemExclusiveCommand.h index d3cff44e..17aea2b7 100644 --- a/Source/MIKMIDISystemExclusiveCommand.h +++ b/Source/MIKMIDISystemExclusiveCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDISystemMessageCommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import extern uint32_t const kMIKMIDISysexNonRealtimeManufacturerID; extern uint32_t const kMIKMIDISysexRealtimeManufacturerID; @@ -38,13 +38,28 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)identityRequestCommand; +/** + * Creates a SysEx command. + * + * @param manufacturerID The manufacturer ID for the command, + * @param sysexChannel The channel of the message. Only valid for universal exclusive messages, + * will always be ignored for non-universal messages. + * @param sysexData The system exclusive data for the message. Should not include status byte, manufacturer ID, channel. + * End delimiter (0x7F) is optional and will be added if not present. + * @param timestamp The timestamp for the command. Pass nil to use the current date/time. + */ ++ (instancetype)systemExclusiveCommandWithManufacturerID:(UInt32)manufacturerID + sysexChannel:(UInt8)sysexChannel + sysexData:(NSData *)sysexData + timestamp:(nullable NSDate *)timestamp; + /** * Initializes the command with raw sysex data and timestamp. * * @param data Assumed to be valid with begin+end delimiters. * @param timeStamp Time at which the first sysex byte was received. */ -- (id)initWithRawData:(NSData *)data timeStamp:(MIDITimeStamp)timeStamp; +- (instancetype)initWithRawData:(NSData *)data timeStamp:(MIDITimeStamp)timeStamp; /** * The manufacturer ID for the command. This is used by devices to determine @@ -62,6 +77,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly) UInt32 manufacturerID; +/** + * Whether or not the command's data will include three bytes for the manufacturer ID. + * If the first three bytes of the manufacturer ID are non-zero, this *always* returns YES. + * By default, if only the last byte of the manufacturer ID is non-zero, this returns NO. + * It can be set to YES (in MIKMutableMIDISystemExclusiveCommand) to explicitly + * Include all three bytes, including the two leading zero bytes. + */ +@property (nonatomic, readonly) BOOL includesThreeByteManufacturerID; + /** * The channel of the message. Only valid for universal exclusive messages, * will always be 0 for non-universal messages. @@ -73,7 +97,7 @@ NS_ASSUME_NONNULL_BEGIN * * For universal messages subID's are included in sysexData, for non-universal * messages, any device specific information (such as modelID, versionID or - * whatever manufactures decide to include) will be included in sysexData. + * whatever manufacturers decide to include) will be included in sysexData. */ @property (nonatomic, strong, readonly) NSData *sysexData; @@ -90,6 +114,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MIKMutableMIDISystemExclusiveCommand : MIKMIDISystemExclusiveCommand @property (nonatomic, readwrite) UInt32 manufacturerID; +@property (nonatomic, readwrite) BOOL includesThreeByteManufacturerID; @property (nonatomic, readwrite) UInt8 sysexChannel; @property (nonatomic, strong, readwrite) NSData *sysexData; diff --git a/Source/MIKMIDISystemExclusiveCommand.m b/Source/MIKMIDISystemExclusiveCommand.m index 5767d1bf..220fa7e4 100644 --- a/Source/MIKMIDISystemExclusiveCommand.m +++ b/Source/MIKMIDISystemExclusiveCommand.m @@ -32,6 +32,8 @@ @interface MIKMIDISystemExclusiveCommand () @implementation MIKMIDISystemExclusiveCommand { BOOL _has3ByteManufacturerID; + BOOL _includesThreeByteManufacturerID; + BOOL _threeByteManufacturerIDInInternalData; } + (void)load { [super load]; [MIKMIDICommand registerSubclass:self]; } @@ -67,6 +69,19 @@ + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key return result; } ++ (instancetype)systemExclusiveCommandWithManufacturerID:(UInt32)manufacturerID + sysexChannel:(UInt8)sysexChannel + sysexData:(NSData *)sysexData + timestamp:(nullable NSDate *)timestamp +{ + MIKMutableMIDISystemExclusiveCommand *result = [[MIKMutableMIDISystemExclusiveCommand alloc] initWithMIDIPacket:NULL]; + result.manufacturerID = manufacturerID; + result.sysexChannel = sysexChannel; + result.sysexData = sysexData; + result.timestamp = timestamp ?: [NSDate date]; + return [self isMutable] ? result : [result copy]; +} + - (id)initWithMIDIPacket:(MIDIPacket *)packet { self = [super initWithMIDIPacket:packet]; @@ -76,6 +91,7 @@ - (id)initWithMIDIPacket:(MIDIPacket *)packet UInt8 firstByte = self.dataByte1; if (firstByte == 0) { _has3ByteManufacturerID = YES; + _threeByteManufacturerIDInInternalData = YES; if ([self.internalData length] < 4) [self.internalData increaseLengthBy:4-[self.internalData length]]; } } @@ -101,7 +117,7 @@ - (UInt32)manufacturerID { if ([self.internalData length] < 2) return 0; - NSUInteger manufacturerIDLength = _has3ByteManufacturerID ? 3 : 1; + NSUInteger manufacturerIDLength = _threeByteManufacturerIDInInternalData ? 3 : 1; NSData *idData = [self.internalData subdataWithRange:NSMakeRange(1, manufacturerIDLength)]; UInt8 *bytes = (UInt8 *)[idData bytes]; if (manufacturerIDLength == 1) { return bytes[0]; } @@ -112,16 +128,27 @@ - (void)setManufacturerID:(UInt32)manufacturerID { if (![[self class] isMutable]) return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; - NSUInteger numExistingBytes = _has3ByteManufacturerID ? 3 : 1; + NSUInteger numExistingBytes = _threeByteManufacturerIDInInternalData ? 3 : 1; NSUInteger numNewBytes = (manufacturerID & 0xFFFF00) != 0 ? 3 : 1; + _has3ByteManufacturerID = (numNewBytes == 3); + if (self.includesThreeByteManufacturerID) { numNewBytes = 3; } manufacturerID = CFSwapInt32HostToBig(manufacturerID); - NSUInteger numRequiredBytes = MAX(numExistingBytes, numNewBytes) + 1; - if ([self.internalData length] < numRequiredBytes) [self.internalData increaseLengthBy:numRequiredBytes-[self.internalData length]]; - UInt8 *replacementBytes = (UInt8 *)(&manufacturerID) + 4 - numNewBytes; [self.internalData replaceBytesInRange:NSMakeRange(1, numExistingBytes) withBytes:replacementBytes length:numNewBytes]; - - _has3ByteManufacturerID = (numNewBytes == 3); + _threeByteManufacturerIDInInternalData = numNewBytes == 3; +} + +- (BOOL)includesThreeByteManufacturerID +{ + if (_has3ByteManufacturerID) { return YES; } + return _includesThreeByteManufacturerID; +} + +- (void)setIncludesThreeByteManufacturerID:(BOOL)includesThreeByteManufacturerID +{ + if (![[self class] isMutable]) return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; + _includesThreeByteManufacturerID = includesThreeByteManufacturerID; + self.manufacturerID = self.manufacturerID; } - (BOOL)isUniversal @@ -159,7 +186,7 @@ - (void)setSysexChannel:(UInt8)sysexChannel - (NSUInteger)sysexDataStartLocation { - NSUInteger sysexStartLocation = _has3ByteManufacturerID ? 4 : 2; + NSUInteger sysexStartLocation = _threeByteManufacturerIDInInternalData ? 4 : 2; if (self.isUniversal) { sysexStartLocation++; } @@ -226,6 +253,7 @@ + (BOOL)isMutable { return YES; } // One of the super classes already implements a getter *and* setter for these. @dynamic keeps the compiler happy. @dynamic manufacturerID; +@dynamic includesThreeByteManufacturerID; @dynamic sysexChannel; @dynamic sysexData; @dynamic timestamp; diff --git a/Source/MIKMIDISystemKeepAliveCommand.h b/Source/MIKMIDISystemKeepAliveCommand.h index f1843ef1..e15efee8 100644 --- a/Source/MIKMIDISystemKeepAliveCommand.h +++ b/Source/MIKMIDISystemKeepAliveCommand.h @@ -6,7 +6,7 @@ // Copyright © 2017 Mixed In Key. All rights reserved. // -#import "MIKMIDISystemMessageCommand.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMIDISystemMessageCommand.h b/Source/MIKMIDISystemMessageCommand.h index 7a537b84..bc7e864a 100644 --- a/Source/MIKMIDISystemMessageCommand.h +++ b/Source/MIKMIDISystemMessageCommand.h @@ -6,8 +6,8 @@ // Copyright (c) 2013 Mixed In Key. All rights reserved. // -#import "MIKMIDICommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -34,4 +34,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDITempoEvent.h b/Source/MIKMIDITempoEvent.h index dd8fbb31..995d6b76 100644 --- a/Source/MIKMIDITempoEvent.h +++ b/Source/MIKMIDITempoEvent.h @@ -6,8 +6,8 @@ // Copyright (c) 2014 Mixed In Key. All rights reserved. // -#import "MIKMIDIEvent.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -45,4 +45,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDITempoTrack.h b/Source/MIKMIDITempoTrack.h new file mode 100644 index 00000000..8a266ff8 --- /dev/null +++ b/Source/MIKMIDITempoTrack.h @@ -0,0 +1,23 @@ +// +// MIKMIDITempoTrack.h +// MIKMIDI +// +// Created by Andrew R Madsen on 12/15/19. +// Copyright © 2019 Mixed In Key. All rights reserved. +// + +#import +#import +#import + +@class MIKMIDITempoEvent; + +NS_ASSUME_NONNULL_BEGIN + +@interface MIKMIDITempoTrack : MIKMIDITrack + +@property (nonatomic, strong, readonly) MIKArrayOf(MIKMIDITempoEvent *) *tempoEvents; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDITempoTrack.m b/Source/MIKMIDITempoTrack.m new file mode 100644 index 00000000..2381a20a --- /dev/null +++ b/Source/MIKMIDITempoTrack.m @@ -0,0 +1,42 @@ +// +// MIKMIDITempoTrack.m +// MIKMIDI +// +// Created by Andrew R Madsen on 12/15/19. +// Copyright © 2019 Mixed In Key. All rights reserved. +// + +#import "MIKMIDITempoTrack.h" +#import "MIKMIDITrack_Protected.h" +#import "MIKMIDITempoEvent.h" + +@interface MIKMIDITempoTrack () + +@property (nonatomic, copy) NSArray *tempoEventsCache; + +@end + +@implementation MIKMIDITempoTrack + +- (void)updateTempoEventsCache +{ + self.tempoEventsCache = [self eventsOfClass:[MIKMIDITempoEvent class] fromTimeStamp:0 toTimeStamp:kMusicTimeStamp_EndOfTrack]; +} + +- (NSArray *)tempoEvents +{ + [self dispatchSyncToSequencerProcessingQueueAsNeeded:^{ + if (!self.tempoEventsCache) { + [self updateTempoEventsCache]; + } + }]; + return self.tempoEventsCache; +} + +- (void)setSortedEventsCache:(NSArray *)sortedEventsCache +{ + [super setSortedEventsCache:sortedEventsCache]; + [self updateTempoEventsCache]; +} + +@end diff --git a/Source/MIKMIDITrack.h b/Source/MIKMIDITrack.h index fc1de1a7..981f9362 100644 --- a/Source/MIKMIDITrack.h +++ b/Source/MIKMIDITrack.h @@ -8,7 +8,7 @@ #import #import -#import "MIKMIDICompilerCompatibility.h" +#import @class MIKMIDISequence; @class MIKMIDIEvent; diff --git a/Source/MIKMIDITrack.m b/Source/MIKMIDITrack.m index d3454eac..da32c028 100644 --- a/Source/MIKMIDITrack.m +++ b/Source/MIKMIDITrack.m @@ -8,6 +8,7 @@ #import "MIKMIDISequence.h" #import "MIKMIDITrack.h" +#import "MIKMIDITrack_Protected.h" #import "MIKMIDIEvent.h" #import "MIKMIDINoteEvent.h" #import "MIKMIDITempoEvent.h" @@ -25,7 +26,6 @@ @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; @@ -557,9 +557,8 @@ - (void)addInternalEventsObject:(MIKMIDIEvent *)event - (void)addInternalEvents:(NSSet *)events { - for (MIKMIDIEvent *event in events) { - [self addInternalEventsObject:[event copy]]; - } + [self.internalEvents addObjectsFromArray:[events allObjects]]; + self.sortedEventsCache = nil; } - (void)removeInternalEventsObject:(MIKMIDIEvent *)event diff --git a/Source/MIKMIDITrack_Protected.h b/Source/MIKMIDITrack_Protected.h index 6bbeff4a..02ef7a1a 100644 --- a/Source/MIKMIDITrack_Protected.h +++ b/Source/MIKMIDITrack_Protected.h @@ -6,8 +6,8 @@ // Copyright (c) 2015 Mixed In Key. All rights reserved. // -#import "MIKMIDITrack.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -41,6 +41,10 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)restoreLengthAndLoopInfo; +- (void)dispatchSyncToSequencerProcessingQueueAsNeeded:(void (^)(void))block; + +@property (nonatomic, strong, nullable) NSArray *sortedEventsCache; + @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMIDIUtilities.h b/Source/MIKMIDIUtilities.h index 96256f8e..38daf62f 100644 --- a/Source/MIKMIDIUtilities.h +++ b/Source/MIKMIDIUtilities.h @@ -8,9 +8,9 @@ #import #import -#import "MIKMIDIMappableResponder.h" -#import "MIKMIDICommand.h" -#import "MIKMIDICompilerCompatibility.h" +#import +#import +#import #include NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MIKMMCLocateTargetCommand.h b/Source/MIKMMCLocateTargetCommand.h new file mode 100644 index 00000000..c6921c2b --- /dev/null +++ b/Source/MIKMMCLocateTargetCommand.h @@ -0,0 +1,48 @@ +// +// MIKMMCLocateTargetCommand.h +// MIKMIDI +// +// Created by Andrew R Madsen on 2/6/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import + +typedef NS_ENUM(UInt8, MIKMMCLocateTargetCommandTimeType) { + MIKMMCLocateTargetCommandTimeType24FPS = 0x00, + MIKMMCLocateTargetCommandTimeType25FPS = 0x01, + MIKMMCLocateTargetCommandTimeType30FPSDropFrame = 0x02, + MIKMMCLocateTargetCommandTimeType30FPS = 0x03, +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface MIKMMCLocateTargetCommand : MIKMIDIMachineControlCommand + ++ (instancetype)locateTargetCommandWithTimeCodeInSeconds:(NSTimeInterval)timecode + timeType:(MIKMMCLocateTargetCommandTimeType)timeType; + +@property (nonatomic, readonly) NSTimeInterval timeCodeInSeconds; +@property (nonatomic, readonly) MIKMMCLocateTargetCommandTimeType timeType; + +@end + +@interface MIKMutableMMCLocateTargetCommand : MIKMMCLocateTargetCommand + +@property (nonatomic, readwrite) NSTimeInterval timeCodeInSeconds; +@property (nonatomic, readwrite) MIKMMCLocateTargetCommandTimeType timeType; + +@property (nonatomic, readwrite) UInt8 deviceAddress; +@property (nonatomic, readwrite) MIKMIDIMachineControlDirection direction; + +@property (nonatomic, strong, readwrite) NSDate *timestamp; +@property (nonatomic, readwrite) MIKMIDICommandType commandType; +@property (nonatomic, readwrite) UInt8 dataByte1; +@property (nonatomic, readwrite) UInt8 dataByte2; + +@property (nonatomic, readwrite) MIDITimeStamp midiTimestamp; +@property (nonatomic, copy, readwrite, null_resettable) NSData *data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/MIKMMCLocateTargetCommand.m b/Source/MIKMMCLocateTargetCommand.m new file mode 100644 index 00000000..8b0885eb --- /dev/null +++ b/Source/MIKMMCLocateTargetCommand.m @@ -0,0 +1,255 @@ +// +// MIKMMCLocateTargetCommand.m +// MIKMIDI +// +// Created by Andrew R Madsen on 2/6/22. +// Copyright © 2022 Mixed In Key. All rights reserved. +// + +#import "MIKMMCLocateTargetCommand.h" +#import "MIKMIDICommand_SubclassMethods.h" +#import "MIKMIDIUtilities.h" + +@implementation MIKMMCLocateTargetCommand + ++ (void)load { [super load]; [MIKMIDICommand registerSubclass:self]; } ++ (NSArray *)supportedMIDICommandTypes { return @[@(MIKMIDICommandTypeSystemExclusive)]; } + ++ (MIKMIDICommandPacketHandlingIntent)handlingIntentForMIDIPacket:(MIDIPacket *)packet +{ + if (packet->length < 5) { return MIKMIDICommandPacketHandlingIntentReject; } + UInt8 directionByteIndex = 3; + UInt8 firstByte = packet->data[1]; + if (firstByte == 0) { // Three byte manufacturer ID + directionByteIndex += 2; + } + UInt8 messageTypeIndex = directionByteIndex + 1; + if (packet->length <= messageTypeIndex) { return MIKMIDICommandPacketHandlingIntentReject; } + + UInt8 messageType = packet->data[messageTypeIndex]; + + if (messageType == 0x44) { return MIKMIDICommandPacketHandlingIntentAcceptWithHigherPrecedence; } + + UInt8 subtypeIndex = messageTypeIndex + 2; + if (packet->length <= subtypeIndex) { return MIKMIDICommandPacketHandlingIntentReject; } + + UInt8 subtype = packet->data[subtypeIndex]; + if (subtype != 0x01) { return MIKMIDICommandPacketHandlingIntentReject; } // Otherwise, it's not a target command + + return MIKMIDICommandPacketHandlingIntentReject; +} + ++ (Class)immutableCounterpartClass; { return [MIKMMCLocateTargetCommand class]; } ++ (Class)mutableCounterpartClass; { return [MIKMutableMMCLocateTargetCommand class]; } + +- (id)initWithMIDIPacket:(MIDIPacket *)packet +{ + self = [super initWithMIDIPacket:packet]; + if (self) { + if (!packet) { + if ([self.internalData length] < 5) { + [self.internalData increaseLengthBy:5-[self.internalData length]]; + } + UInt8 *data = (UInt8 *)[self.internalData mutableBytes]; + data[4] = MIKMIDIMachineControlCommandTypeLocate; + } + } + return self; +} + ++ (instancetype)locateTargetCommandWithTimeCodeInSeconds:(NSTimeInterval)timecode + timeType:(MIKMMCLocateTargetCommandTimeType)timeType +{ + MIKMutableMMCLocateTargetCommand *result = [[MIKMutableMMCLocateTargetCommand alloc] init]; + result.timeType = timeType; + result.timeCodeInSeconds = timecode; + return [self isMutable] ? result : [result copy]; +} + +- (NSString *)additionalCommandDescription +{ + return [NSString stringWithFormat:@"timecode: %@", @(self.timeCodeInSeconds)]; +} + +#pragma mark - Properties + +#pragma mark Public + +- (NSTimeInterval)timeCodeInSeconds +{ + NSData *timecodeData = [self timecodeData]; + if (!timecodeData) { return -1; } + + UInt8 *timecodeBytes = (UInt8 *)timecodeData.bytes; + UInt8 hoursAndTypeByte = timecodeBytes[0]; + UInt8 minutesByte = timecodeBytes[1]; + UInt8 secondsByte = timecodeBytes[2]; + UInt8 framesByte = timecodeBytes[3]; + UInt8 finalByte = timecodeBytes[4]; + + UInt8 hours = (hoursAndTypeByte & 0x1F); + NSTimeInterval frameRate = [self frameRate]; + +// UInt8 colorFrameFlag = (minutesByte & 0x40) >> 6; + UInt8 minutes = (minutesByte & 0x3F); + +// UInt8 blankBit = (secondsByte & 0x40) >> 6; + UInt8 seconds = (secondsByte & 0x3F); + + UInt8 sign = (framesByte & 0x40) >> 6; + UInt8 finalByteID = (framesByte & 0x20) >> 5; + UInt8 frames = (framesByte & 0x1F); + + NSTimeInterval result = 0.0; + + result += hours * 3600.0; + result += minutes * 60.0; + result += seconds; + result += (NSTimeInterval)frames / frameRate; + + if (finalByteID == 0) { + UInt8 subframes = finalByte; + // Final byte is subframes + result += ((NSTimeInterval)subframes/100.0) / frameRate; + } + + if (sign) { + result = -result; + } + + return result; +} + +- (void)setTimeCodeInSeconds:(NSTimeInterval)timeCodeInSeconds +{ + if (![[self class] isMutable]) { return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; } + + NSTimeInterval frameRate = [self frameRate]; + + UInt8 hours = (UInt8)(timeCodeInSeconds / 3600.0) & 0x1F; // Hours + UInt8 minutes = (UInt8)((timeCodeInSeconds - hours * 3600) / 60); + UInt8 seconds = (UInt8)(timeCodeInSeconds - hours * 3600 - minutes * 60); + UInt8 frames = (UInt8)((timeCodeInSeconds - hours * 3600 - minutes * 60 - seconds) * frameRate); + UInt8 subframes = (UInt8)((timeCodeInSeconds - hours * 3600 - minutes * 60 - seconds - (NSTimeInterval)frames/frameRate) * 100.0); + + NSMutableData *newTimecodeData = [NSMutableData dataWithLength:5]; + UInt8 *timecodeBytes = (UInt8 *)newTimecodeData.mutableBytes; + timecodeBytes[0] = ((self.timeType & 0x03) << 5) + hours; + timecodeBytes[1] = (timecodeBytes[1] & 0xC0) | (minutes & 0x3F); + timecodeBytes[2] = (timecodeBytes[2] & 0xC0) | (seconds & 0x3F); + timecodeBytes[3] = (timecodeBytes[2] & 0xE0) | (frames & 0x1F); + timecodeBytes[3] &= 0xDF; // set final byte ID to 0 for subframes + timecodeBytes[4] = subframes; + [self setTimecodeData:newTimecodeData]; +} + +- (MIKMMCLocateTargetCommandTimeType)timeType +{ + NSData *timecodeData = [self timecodeData]; + if (timecodeData.length < 1) { return MIKMMCLocateTargetCommandTimeType30FPS; } + + UInt8 *timecodeBytes = (UInt8 *)timecodeData.bytes; + UInt8 hoursAndTypeByte = timecodeBytes[0]; + + MIKMMCLocateTargetCommandTimeType type = (hoursAndTypeByte & 0x60) >> 5; + return type; +} + +- (void)setTimeType:(MIKMMCLocateTargetCommandTimeType)timeType +{ + if (![[self class] isMutable]) { return MIKMIDI_RAISE_MUTATION_ATTEMPT_EXCEPTION; } + + NSMutableData *timecodeData = [[self timecodeData] mutableCopy]; + if (!timecodeData) { + timecodeData = [NSMutableData dataWithLength:5]; + } + UInt8 *timecodeBytes = (UInt8 *)timecodeData.mutableBytes; + timecodeBytes[0] = (timecodeBytes[0] & 0x9F) + ((timeType & 0x03) << 5); + [self setTimecodeData:timecodeData]; +} + +#pragma mark Private + +- (NSTimeInterval)frameRate +{ + switch ([self timeType]) { + case MIKMMCLocateTargetCommandTimeType24FPS: + return 24.0; + break; + case MIKMMCLocateTargetCommandTimeType25FPS: + return 25.0; + case MIKMMCLocateTargetCommandTimeType30FPSDropFrame: // Not currently handling drop frame rates exactly correctly + return 29.97; + default: + case MIKMMCLocateTargetCommandTimeType30FPS: + return 30.0; + } +} + +- (NSData *)timecodeData +{ + NSUInteger byteCountIndex = 5; + if (self.includesThreeByteManufacturerID) { + byteCountIndex += 2; + } + if (self.data.length <= byteCountIndex) { return nil; } + + UInt8 byteCount = ((UInt8 *)self.data.bytes)[byteCountIndex]; + if (self.data.length < (byteCountIndex + byteCount)) { return nil; } + + UInt8 subcommandIndex = byteCountIndex += 1; + UInt8 subcommand = ((UInt8 *)self.data.bytes)[subcommandIndex]; + if (subcommand != 0x01) { return nil; } // Not a locate target command + + NSUInteger timecodeDataLocation = subcommandIndex+1; + NSData *timecodeData = [self.data subdataWithRange:NSMakeRange(timecodeDataLocation, byteCount-1)]; + return timecodeData; +} + +- (void)setTimecodeData:(NSData *)data +{ + NSUInteger requiredLength = 13; + if (self.includesThreeByteManufacturerID) { requiredLength += 2; } + if ([self.internalData length] < requiredLength) { + [self.internalData increaseLengthBy:requiredLength - [self.internalData length]]; + } + + NSUInteger byteCountIndex = 5; + if (self.includesThreeByteManufacturerID) { + byteCountIndex += 2; + } + ((UInt8 *)self.internalData.mutableBytes)[byteCountIndex] = 6; + + UInt8 subcommandIndex = byteCountIndex += 1; + ((UInt8 *)self.internalData.mutableBytes)[subcommandIndex] = 0x01; + + NSUInteger timecodeDataLocation = subcommandIndex+1; + [self.internalData replaceBytesInRange:NSMakeRange(timecodeDataLocation, 5) + withBytes:data.bytes length:data.length]; +} + +@end + +@implementation MIKMutableMMCLocateTargetCommand + ++ (BOOL)isMutable { return YES; } + +#pragma mark - Properties + +@dynamic timeCodeInSeconds; +@dynamic timeType; + +// MIKMIDICommand or MIKMIDIMachineControlCommand already implements these. This keeps the compiler happy. + +@dynamic deviceAddress; +@dynamic direction; +@dynamic MMCCommandType; + +@dynamic timestamp; +@dynamic dataByte1; +@dynamic dataByte2; +@dynamic midiTimestamp; +@dynamic data; +@dynamic commandType; + +@end diff --git a/Source/NSUIApplication+MIKMIDI.h b/Source/NSUIApplication+MIKMIDI.h index 77186491..62d94313 100644 --- a/Source/NSUIApplication+MIKMIDI.h +++ b/Source/NSUIApplication+MIKMIDI.h @@ -24,7 +24,7 @@ #endif -#import "MIKMIDICompilerCompatibility.h" +#import /** * Define MIKMIDI_SEARCH_VIEW_HIERARCHY_FOR_RESPONDERS as a non-zero value to (re)enable searching @@ -140,4 +140,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END