diff --git a/Framework/MIKMIDI.xcodeproj/project.pbxproj b/Framework/MIKMIDI.xcodeproj/project.pbxproj index 134c39bc..5a2c2a12 100644 --- a/Framework/MIKMIDI.xcodeproj/project.pbxproj +++ b/Framework/MIKMIDI.xcodeproj/project.pbxproj @@ -58,6 +58,10 @@ 83FB360E1B42D58000F91DCD /* MIKMIDISequence+MIKMIDIPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 83FB360C1B42D58000F91DCD /* MIKMIDISequence+MIKMIDIPrivate.h */; }; 9D07CAC71BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D07CAC61BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D07CAC81BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D07CAC61BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9D07CB221BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D07CB201BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9D07CB231BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D07CB201BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9D07CB241BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D07CB211BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m */; }; + 9D07CB251BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D07CB211BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m */; }; 9D0895EE1B0D29F200A5872E /* MIKMIDIMappingItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D0895EC1B0D29F200A5872E /* MIKMIDIMappingItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D0895EF1B0D29F200A5872E /* MIKMIDIMappingItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D0895EC1B0D29F200A5872E /* MIKMIDIMappingItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D0895F01B0D29F200A5872E /* MIKMIDIMappingItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D0895ED1B0D29F200A5872E /* MIKMIDIMappingItem.m */; }; @@ -351,6 +355,8 @@ 83C850C81B7AA452001D71B0 /* MIKMIDIMappingManager_SubclassMethods.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MIKMIDIMappingManager_SubclassMethods.h; sourceTree = ""; }; 83FB360C1B42D58000F91DCD /* MIKMIDISequence+MIKMIDIPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MIKMIDISequence+MIKMIDIPrivate.h"; sourceTree = ""; }; 9D07CAC61BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIKMIDICompilerCompatibility.h; sourceTree = ""; }; + 9D07CB201BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIKMIDIConnectionManager.h; sourceTree = ""; }; + 9D07CB211BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIKMIDIConnectionManager.m; sourceTree = ""; }; 9D0895EC1B0D29F200A5872E /* MIKMIDIMappingItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIKMIDIMappingItem.h; sourceTree = ""; }; 9D0895ED1B0D29F200A5872E /* MIKMIDIMappingItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIKMIDIMappingItem.m; sourceTree = ""; }; 9D0895F21B0D2A4700A5872E /* MIKMIDIMappableResponder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIKMIDIMappableResponder.h; sourceTree = ""; }; @@ -749,6 +755,8 @@ children = ( 9D74EF3C17A713A100BEE89F /* MIKMIDIDeviceManager.h */, 9D74EF3D17A713A100BEE89F /* MIKMIDIDeviceManager.m */, + 9D07CB201BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h */, + 9D07CB211BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m */, 9D74EF5017A713A100BEE89F /* MIKMIDIObject.h */, 9D74EF5117A713A100BEE89F /* MIKMIDIObject.m */, 9D74EF5217A713A100BEE89F /* MIKMIDIObject_SubclassMethods.h */, @@ -902,6 +910,7 @@ 9D74EF8317A713A100BEE89F /* MIKMIDIObject.h in Headers */, 9D07CAC71BEA70E200C4ABB0 /* MIKMIDICompilerCompatibility.h in Headers */, 9D74EF8617A713A100BEE89F /* MIKMIDIOutputPort.h in Headers */, + 9D07CB221BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h in Headers */, 9D74EF8817A713A100BEE89F /* MIKMIDIPort.h in Headers */, 9D74EF8B17A713A100BEE89F /* MIKMIDIResponder.h in Headers */, 9D74EF8C17A713A100BEE89F /* MIKMIDISourceEndpoint.h in Headers */, @@ -959,6 +968,7 @@ 9D0895EF1B0D29F200A5872E /* MIKMIDIMappingItem.h in Headers */, 9DAF8B701A7B00A700F46528 /* MIKMIDIMetaCuePointEvent.h in Headers */, 9DAF8B671A7B008A00F46528 /* MIKMIDIProgramChangeCommand.h in Headers */, + 9D07CB231BEC13E400C4ABB0 /* MIKMIDIConnectionManager.h in Headers */, 9DED4E231AA77DAC00DA8356 /* MIKMIDIPitchBendChangeEvent.h in Headers */, 9DED4E371AA90CA700DA8356 /* MIKMIDIPitchBendChangeCommand.h in Headers */, 9DB366F71A964D4A001D1CF3 /* MIKMIDISynthesizerInstrument.h in Headers */, @@ -1196,6 +1206,7 @@ 9DED4E381AA90CA700DA8356 /* MIKMIDIPitchBendChangeCommand.m in Sources */, 9D74EF8F17A713A100BEE89F /* MIKMIDISystemExclusiveCommand.m in Sources */, 9D74EF9117A713A100BEE89F /* MIKMIDISystemMessageCommand.m in Sources */, + 9D07CB241BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m in Sources */, 839D935419C3A2F5007589C3 /* MIKMIDIMetaEvent.m in Sources */, 9D7027D31ACC9D7A009AFAED /* MIKMIDIMacDebugQuickLookSupport.m in Sources */, 9D74EF9317A713A100BEE89F /* MIKMIDIUtilities.m in Sources */, @@ -1264,6 +1275,7 @@ 9DAF8B4C1A7AFF7500F46528 /* MIKMIDISequencer.m in Sources */, 9DB366F31A964C55001D1CF3 /* MIKMIDISynthesizer.m in Sources */, 9DAF8B4D1A7AFF7500F46528 /* MIKMIDIUtilities.m in Sources */, + 9D07CB251BEC13E400C4ABB0 /* MIKMIDIConnectionManager.m in Sources */, 9DAF8B4E1A7AFF7500F46528 /* MIKMIDICommandThrottler.m in Sources */, 9DAF8B4F1A7AFF7500F46528 /* MIKMIDIClock.m in Sources */, 9DAF8B501A7AFF7500F46528 /* MIKMIDIPrivateUtilities.m in Sources */, diff --git a/Source/MIKMIDI.h b/Source/MIKMIDI.h index 5a77cefc..67baa167 100644 --- a/Source/MIKMIDI.h +++ b/Source/MIKMIDI.h @@ -19,6 +19,7 @@ // MIDI Device support #import "MIKMIDIDevice.h" #import "MIKMIDIDeviceManager.h" +#import "MIKMIDIConnectionManager.h" #import "MIKMIDIEntity.h" diff --git a/Source/MIKMIDIConnectionManager.h b/Source/MIKMIDIConnectionManager.h new file mode 100644 index 00000000..fc61dffd --- /dev/null +++ b/Source/MIKMIDIConnectionManager.h @@ -0,0 +1,177 @@ +// +// MIKMIDIConnectionManager.h +// MIKMIDI +// +// Created by Andrew Madsen on 11/5/15. +// Copyright © 2015 Mixed In Key. All rights reserved. +// + +#import +#import "MIKMIDICompilerCompatibility.h" +#import "MIKMIDISourceEndpoint.h" + +@class MIKMIDIDevice; + +@protocol MIKMIDIConnectionManagerDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + * MIKMIDIConnectionManager can be used to manage a set of connected devices. It can be configured to automatically + * connect to devices as they are added, and disconnect from them as they are removed. It also supports saving + * the list of connected to NSUserDefaults and restoring them upon relaunch. + * + * The use of MIKMIDIConnectionManager is optional. It is meant to be useful in implementing functionality that + * many MIDI device enabled apps need. However, simple connection to devices or MIDI endpoints can be done with + * MIKMIDIDeviceManager directly, if desired. + */ +@interface MIKMIDIConnectionManager : NSObject + +/** + * This method will throw an exception if called. Use -initWithName: instead. + * + * @return nil + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Initializes an instance of MIKMIDIConnectionManager. The passed in name is used to independently + * store and load the connection manager's configuration using NSUserDefaults. The passed in name + * should be unique across your application, and the same from launch to launch. + * + * @param name The name to give the connection manager. + * + * @return An initialized MIKMIDIConnectionManager instance. + */ +- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER; + +/** + * Connect to the specified device. When MIDI messages are received, the connection manager's event handler + * block will be executed. + * + * @param device An MIKMIDIDevice instance. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. + * If you are not interested in possible errors, you may pass in NULL. + * + * @return YES if the connection was successful, NO if an error occurred. + */ +- (BOOL)connectToDevice:(MIKMIDIDevice *)device error:(NSError **)error; + +/** + * Disconnect from a connected device. No further messages from the specified device will be processed. + * + * Note that you don't need to call this method when a previously-connected device was removed from the system. + * Disconnection in that situation is handled automatically. + * + * @param device An MIKMIDIDevice instance. + */ +- (void)disconnectFromDevice:(MIKMIDIDevice *)device; + +/** + * This method can be used to determine if the receiver is connected to a given MIDI device. + * + * @param device An MIKMIDIDevice instance. + * + * @return YES if the receiver is connected to and processing MIDI input from the device, NO otherwise. + */ +- (BOOL)isConnectedToDevice:(MIKMIDIDevice *)device; + +/** + * If YES (the default), the connection manager will automatically save its configuration at appropriate + * times. If this property is NO, -saveConfiguration can still be used to manually trigger saving the + * receiver's configuration. Note that -loadConfiguration must always be called manually, e.g. at launch. + */ +@property (nonatomic) BOOL automaticallySavesConfiguration; + +/** + * Save the receiver's list of connected devices to disk for later restoration. + */ +- (void)saveConfiguration; + +/** + * Load and reconnect to the devices previously saved to disk by a call to -saveConfiguration. For + * this to work, the receiver's name must be the same as it was upon the previous call to -saveConfiguration. + * + * @note: This method will only connect to new devices. It will not disconnect from devices not found in the + * saved configuration. + */ +- (void)loadConfiguration; + +/** + * The name of the receiver. Used for configuration save/load. + */ +@property (nonatomic, copy, readonly) NSString *name; + +/** + * An MIKMIDIEventHandlerBlock to be called with incoming MIDI messages from any connected device. + * + * If you need to determine which device sent the passed in messages, call source.entity.device on the + * passed in MIKMIDISourceEndpoint argument. + */ +@property (nonatomic, copy, null_resettable) MIKMIDIEventHandlerBlock eventHandler; + +/** + * A delegate, used to customize MIKMIDIConnectionManager behavior. + */ +@property (nonatomic, weak) iddelegate; + +/** + * Controls whether the receiver's availableDevices property includes virtual devices (i.e. devices made + * up of automatically paired virtual sources and destinations). + * + * If this property is YES (the default), the connection manager will attempt to automtically related + * associated virtual sources and destinations and create "virtual" MIKMIDIDevice instances for them. + * + * If this property is NO, the connection manager's availableDevices array will _only_ contain non-virtual + * MIKMIDIDevices. + * + * For most applications, this should be left at the default YES, as even many physical MIDI devices present as + * "virtual" devices in software. + * + * @note: The caveat here is that this relies on some heuristics to match up source endpoints with destination endpoints. + * These heuristics are based on the way certain common MIDI devices behave, but may not be universal, and therefore + * may miss, or fail to properly associate endpoints for some devices. If this is a problem for your application, + * you should obtain and connect to virtual sources/endpoints using MIKMIDIDeviceManager directly instead. + */ +@property (nonatomic) BOOL includesVirtualDevices; // Default is YES + +/** + * An array of available MIDI devices. + * + * This property is observable using Key Value Observing. + */ +@property (nonatomic, strong, readonly) MIKArrayOf(MIKMIDIDevice *) *availableDevices; + +/** + * The set of MIDI devices to which the receiver is connected. + * + * This property is observable using Key Value Observing. + */ +@property (nonatomic, strong, readonly) MIKSetOf(MIKMIDIDevice *) *connectedDevices; + +@end + +/** + * Protocol containing method(s) to be implemented by delegates of MIKMIDIConnectionManager. + */ +@protocol MIKMIDIConnectionManagerDelegate + +@optional + +/** + * A connection manager's delegate can implement this method to determine whether or not to automatically connect + * to a newly added MIDI device. + * + * If this method is not implemented or returns NO, the device will be connected to if the most + * + * @param manager An instance of MIKMIDIConnectionManager. + * @param device The newly added MIDI device. + * + * @return YES to connect to device, NO to leave it unconnected. + */ +- (BOOL)connectionManager:(MIKMIDIConnectionManager *)manager shouldConnectToNewlyAddedDevice:(MIKMIDIDevice *)device; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Source/MIKMIDIConnectionManager.m b/Source/MIKMIDIConnectionManager.m new file mode 100644 index 00000000..f24bddd6 --- /dev/null +++ b/Source/MIKMIDIConnectionManager.m @@ -0,0 +1,358 @@ +// +// MIKMIDIConnectionManager.m +// MIKMIDI +// +// Created by Andrew Madsen on 11/5/15. +// Copyright © 2015 Mixed In Key. All rights reserved. +// + +#import "MIKMIDIConnectionManager.h" +#import "MIKMIDIDeviceManager.h" +#import "MIKMIDIDevice.h" + +void *MIKMIDIConnectionManagerKVOContext = &MIKMIDIConnectionManagerKVOContext; + +NSString * const MIKMIDIConnectionManagerConnectedDevicesKey = @"MIKMIDIConnectionManagerConnectedDevicesKey"; + +@interface MIKMIDIConnectionManager () + +@property (nonatomic, strong, readwrite) MIKArrayOf(MIKMIDIDevice *) *availableDevices; + +@property (nonatomic, strong, readonly) MIKMutableSetOf(MIKMIDIDevice *) *internalConnectedDevices; +@property (nonatomic, strong, readonly) MIKMIDIEventHandlerBlock internalEventHandler; +@property (nonatomic, strong, readonly) MIKMapTableOf(MIKMIDIDevice *, id) *connectionTokensByDevice; + +@property (nonatomic, readonly) MIKMIDIDeviceManager *deviceManager; + +@end + +@implementation MIKMIDIConnectionManager + +- (instancetype)init +{ + [NSException raise:NSInternalInconsistencyException format:@"-initWithName: is the designated initializer for %@", NSStringFromClass([self class])]; + return nil; +} + +- (instancetype)initWithName:(NSString *)name +{ + self = [super init]; + if (self) { + _name = [name copy]; + _internalConnectedDevices = [[NSMutableSet alloc] init]; + + __weak typeof(self) weakSelf = self; + _internalEventHandler = ^(MIKMIDISourceEndpoint *endpoint, NSArray *commands) { + weakSelf.eventHandler(endpoint, commands); + }; + + _connectionTokensByDevice = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory]; + + NSKeyValueObservingOptions options = NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; + [self.deviceManager addObserver:self forKeyPath:@"availableDevices" options:options context:MIKMIDIConnectionManagerKVOContext]; + [self.deviceManager addObserver:self forKeyPath:@"virtualSources" options:options context:MIKMIDIConnectionManagerKVOContext]; + [self.deviceManager addObserver:self forKeyPath:@"virtualDestinations" options:options context:MIKMIDIConnectionManagerKVOContext]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self selector:@selector(deviceWasPluggedIn:) name:MIKMIDIDeviceWasAddedNotification object:nil]; + [nc addObserver:self selector:@selector(deviceWasUnplugged:) name:MIKMIDIDeviceWasRemovedNotification object:nil]; + [nc addObserver:self selector:@selector(endpointWasPluggedIn:) name:MIKMIDIVirtualEndpointWasAddedNotification object:nil]; + [nc addObserver:self selector:@selector(endpointWasUnplugged:) name:MIKMIDIVirtualEndpointWasRemovedNotification object:nil]; + } + return self; +} + +- (void)dealloc +{ + [self.deviceManager removeObserver:self forKeyPath:@"availableDevices" context:MIKMIDIConnectionManagerKVOContext]; + [self.deviceManager removeObserver:self forKeyPath:@"virtualSources" context:MIKMIDIConnectionManagerKVOContext]; + [self.deviceManager removeObserver:self forKeyPath:@"virtualDestinations" context:MIKMIDIConnectionManagerKVOContext]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Public + +#pragma mark Device Connection / Disconnection + +- (BOOL)connectToDevice:(MIKMIDIDevice *)device error:(NSError **)error +{ + if ([self isConnectedToDevice:device]) return YES; + error = error ?: &(NSError *__autoreleasing){ nil }; + + id token = [self.deviceManager connectDevice:device error:error eventHandler:self.internalEventHandler]; + if (!token) return NO; + + [self.connectionTokensByDevice setObject:token forKey:device]; + + if (self.automaticallySavesConfiguration) [self saveConfiguration]; + + return YES; +} + +- (void)disconnectFromDevice:(MIKMIDIDevice *)device +{ + if (![self isConnectedToDevice:device]) return; + + id token = [self.connectionTokensByDevice objectForKey:device]; + if (!token) return; + + [self.deviceManager disconnectConnectionforToken:token]; + + if (self.automaticallySavesConfiguration) [self saveConfiguration]; +} + +- (BOOL)isConnectedToDevice:(MIKMIDIDevice *)device; +{ + return [self.connectedDevices containsObject:device]; +} + +#pragma mark Configuration Persistence + +- (void)saveConfiguration +{ + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + + NSMutableDictionary *configuration = [NSMutableDictionary dictionaryWithDictionary:[self savedConfiguration]]; + + // Save connected device names + NSMutableArray *connectedDeviceNames = configuration[MIKMIDIConnectionManagerConnectedDevicesKey]; + if (!connectedDeviceNames) { + connectedDeviceNames = [NSMutableArray array]; + configuration[MIKMIDIConnectionManagerConnectedDevicesKey] = connectedDeviceNames; + } + + // For devices that were connected in saved configuration but are now unavailable, leave them + // connected in the configuration so they'll reconnect automatically. + for (MIKMIDIDevice *device in self.availableDevices) { + NSString *name = device.name; + if (![name length]) continue; + if ([self isConnectedToDevice:device]) { + [connectedDeviceNames addObject:name]; + } else { + [connectedDeviceNames removeObject:name]; + } + } + + [userDefaults setObject:configuration forKey:[self userDefaultsConfigurationKey]]; +} + +- (void)loadConfiguration +{ + for (MIKMIDIDevice *device in self.availableDevices) { + if ([self deviceIsConnectedInSavedConfiguration:device]) { + NSError *error = nil; + if (![self connectToDevice:device error:&error]) { + NSLog(@"Unable to connect to MIDI device %@: %@", device, error); + return; + } + } + } +} + +#pragma mark - Private + +- (void)updateAvailableDevices +{ + NSArray *regularDevices = self.deviceManager.availableDevices; + NSMutableSet *result = [NSMutableSet setWithArray:regularDevices]; + + if (self.includesVirtualDevices) { + NSMutableSet *endpointsInDevices = [NSMutableSet set]; + for (MIKMIDIDevice *device in regularDevices) { + NSSet *sources = [NSSet setWithArray:[device.entities valueForKeyPath:@"@distinctUnionOfArrays.sources"]]; + NSSet *destinations = [NSSet setWithArray:[device.entities valueForKeyPath:@"@distinctUnionOfArrays.destinations"]]; + [endpointsInDevices unionSet:sources]; + [endpointsInDevices unionSet:destinations]; + } + + NSMutableSet *devicelessSources = [NSMutableSet setWithArray:self.deviceManager.virtualSources]; + NSMutableSet *devicelessDestinations = [NSMutableSet setWithArray:self.deviceManager.virtualDestinations]; + [devicelessSources minusSet:endpointsInDevices]; + [devicelessDestinations minusSet:endpointsInDevices]; + + // Now we need to try to associate each source with its corresponding destination on the same device + NSMapTable *destinationToSourceMap = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; + NSMapTable *deviceNamesBySource = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; + + for (MIKMIDIEndpoint *source in devicelessSources) { + NSString *sourceName = [self deviceNameFromVirtualEndpoint:source]; + for (MIKMIDIEndpoint *destination in devicelessDestinations) { + NSString *destinationName = [self deviceNameFromVirtualEndpoint:destination]; + if ([sourceName isEqualToString:destinationName]) { // Source and destination match + [destinationToSourceMap setObject:destination forKey:source]; + [deviceNamesBySource setObject:sourceName forKey:source]; + break; + } + } + } + + for (MIKMIDIEndpoint *source in destinationToSourceMap) { + MIKMIDIEndpoint *destination = [destinationToSourceMap objectForKey:source]; + [devicelessSources removeObject:source]; + [devicelessDestinations removeObject:destination]; + + MIKMIDIDevice *device = [MIKMIDIDevice deviceWithVirtualEndpoints:@[source, destination]]; + device.name = [deviceNamesBySource objectForKey:source]; + if (device) [result addObject:device]; + } + for (MIKMIDIEndpoint *endpoint in devicelessSources) { + MIKMIDIDevice *device = [MIKMIDIDevice deviceWithVirtualEndpoints:@[endpoint]]; + if (device) [result addObject:device]; + } + for (MIKMIDIEndpoint *endpoint in devicelessSources) { + MIKMIDIDevice *device = [MIKMIDIDevice deviceWithVirtualEndpoints:@[endpoint]]; + if (device) [result addObject:device]; + } + } + + self.availableDevices = [result copy]; +} + +- (MIKMIDIDevice *)firstAvailableDeviceWithName:(NSString *)deviceName +{ + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", deviceName]; + return [[self.availableDevices filteredArrayUsingPredicate:predicate] firstObject]; +} + +- (void)connectToNewlyAddedDeviceIfAppropriate:(MIKMIDIDevice *)device +{ + if (!device) return; + + BOOL shouldConnect = [self deviceIsConnectedInSavedConfiguration:device]; + + if ([self.delegate respondsToSelector:@selector(connectionManager:shouldConnectToNewlyAddedDevice:)]) { + shouldConnect = [self.delegate connectionManager:self shouldConnectToNewlyAddedDevice:device]; + } + + if (shouldConnect) { + NSError *error = nil; + if (![self connectToDevice:device error:&error]) { + NSLog(@"Unable to connect to MIDI device %@: %@", device, error); + return; + } + } +} + +#pragma mark Configuration Persistence + +- (NSString *)userDefaultsConfigurationKey +{ + NSString *name = self.name; + if (![name length]) name = NSStringFromClass([self class]); + return [NSString stringWithFormat:@"%@SavedMIDIConnectionConfiguration", name]; +} + +- (NSDictionary *)savedConfiguration +{ + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + return [userDefaults objectForKey:[self userDefaultsConfigurationKey]]; +} + +- (BOOL)deviceIsConnectedInSavedConfiguration:(MIKMIDIDevice *)device +{ + NSString *deviceName = device.name; + if (![deviceName length]) return NO; + + NSDictionary *configuration = [self savedConfiguration]; + NSArray *connectedDeviceNames = configuration[MIKMIDIConnectionManagerConnectedDevicesKey]; + return [connectedDeviceNames containsObject:deviceName]; +} + +#pragma mark Virtual Endpoints + +- (NSString *)deviceNameFromVirtualEndpoint:(MIKMIDIEndpoint *)endpoint +{ + NSString *name = endpoint.name; + if (![name length]) name = [endpoint description]; + NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; + NSMutableArray *nameComponents = [[name componentsSeparatedByCharactersInSet:whitespace] mutableCopy]; + [nameComponents removeLastObject]; + return [nameComponents componentsJoinedByString:@" "]; +} + +- (MIKMIDIDevice *)deviceContainingEndpoint:(MIKMIDIEndpoint *)endpoint +{ + if (!endpoint) return nil; + NSMutableSet *devices = [self.availableDevices mutableCopy]; + [devices unionSet:self.connectedDevices]; + for (MIKMIDIDevice *device in devices) { + NSMutableSet *deviceEndpoints = [NSMutableSet setWithArray:[device.entities valueForKeyPath:@"@distinctUnionOfArrays.sources"]]; + [deviceEndpoints unionSet:[NSSet setWithArray:[device.entities valueForKeyPath:@"@distinctUnionOfArrays.destinations"]]]; + if ([deviceEndpoints containsObject:endpoint]) return device; + } + return nil; +} + +#pragma mark - Notifications + +- (void)deviceWasPluggedIn:(NSNotification *)notification +{ + MIKMIDIDevice *device = [notification userInfo][MIKMIDIDeviceKey]; + [self connectToNewlyAddedDeviceIfAppropriate:device]; +} + +- (void)deviceWasUnplugged:(NSNotification *)notification +{ + MIKMIDIDevice *unpluggedDevice = [notification userInfo][MIKMIDIDeviceKey]; + [self disconnectFromDevice:unpluggedDevice]; +} + +- (void)endpointWasPluggedIn:(NSNotification *)notification +{ + MIKMIDIEndpoint *pluggedInEndpoint = [notification userInfo][MIKMIDIEndpointKey]; + MIKMIDIDevice *pluggedInDevice = [self deviceContainingEndpoint:pluggedInEndpoint]; + [self connectToNewlyAddedDeviceIfAppropriate:pluggedInDevice]; +} + +- (void)endpointWasUnplugged:(NSNotification *)notification +{ + MIKMIDIEndpoint *unpluggedEndpoint = [notification userInfo][MIKMIDIEndpointKey]; + MIKMIDIDevice *unpluggedDevice = [self deviceContainingEndpoint:unpluggedEndpoint]; + if (unpluggedDevice) [self disconnectFromDevice:unpluggedDevice]; +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context != MIKMIDIConnectionManagerKVOContext) { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } + + if (object != self.deviceManager) return; + + if ([keyPath isEqualToString:@"availableDevices"]) { + [self updateAvailableDevices]; + } + + if (self.includesVirtualDevices && + ([keyPath isEqualToString:@"virtualSources"] || [keyPath isEqualToString:@"virtualDestinations"])) { + [self updateAvailableDevices]; + } +} + +#pragma mark - Properties + +- (MIKMIDIDeviceManager *)deviceManager { return [MIKMIDIDeviceManager sharedDeviceManager]; } + +- (MIKMIDIEventHandlerBlock)eventHandler +{ + return _eventHandler ?: ^(MIKMIDISourceEndpoint *s, NSArray *c){}; +} + +- (void)setIncludesVirtualDevices:(BOOL)includesVirtualDevices +{ + if (includesVirtualDevices != _includesVirtualDevices) { + _includesVirtualDevices = includesVirtualDevices; + [self updateAvailableDevices]; + } +} + +- (MIKSetOf(MIKMIDIDevice *) *)connectedDevices +{ + return [self.internalConnectedDevices copy]; +} + +@end diff --git a/Source/MIKMIDIDevice.m b/Source/MIKMIDIDevice.m index d9035339..e110eb80 100644 --- a/Source/MIKMIDIDevice.m +++ b/Source/MIKMIDIDevice.m @@ -78,6 +78,12 @@ - (BOOL)isEqual:(id)object return [self.entities isEqualToArray:[(MIKMIDIDevice *)object entities]]; } +- (NSUInteger)hash +{ + if (!self.isVirtual) return (NSUInteger)self.uniqueID; + return [self.entities hash]; +} + #pragma mark - Public - (NSString *)description