hammerspoon/Pods/MIKMIDI/Source/MIKMIDIDeviceManager.m

503 lines
18 KiB
Objective-C

//
// MIKMIDIDeviceManager.m
// MIDI Testbed
//
// Created by Andrew Madsen on 3/7/13.
// Copyright (c) 2013 Mixed In Key. All rights reserved.
//
#import "MIKMIDIDeviceManager.h"
#import <CoreMIDI/CoreMIDI.h>
#import "MIKMIDIDevice.h"
#import "MIKMIDISourceEndpoint.h"
#import "MIKMIDIDestinationEndpoint.h"
#import "MIKMIDIInputPort.h"
#import "MIKMIDIOutputPort.h"
#import "MIKMIDIClientSourceEndpoint.h"
#import "MIKMIDIErrors.h"
#if TARGET_OS_IPHONE
#import <UIKit/UIApplication.h>
#endif
#if !__has_feature(objc_arc)
#error MIKMIDIDeviceManager.m must be compiled with ARC. Either turn on ARC for the project or set the -fobjc-arc flag for MIKMIDIDeviceManager.m in the Build Phases for this target
#endif
// Notifications
NSString * const MIKMIDIDeviceWasAddedNotification = @"MIKMIDIDeviceWasAddedNotification";
NSString * const MIKMIDIDeviceWasRemovedNotification = @"MIKMIDIDeviceWasRemovedNotification";
NSString * const MIKMIDIVirtualEndpointWasAddedNotification = @"MIKMIDIVirtualEndpointWasAddedNotification";
NSString * const MIKMIDIVirtualEndpointWasRemovedNotification = @"MIKMIDIVirtualEndpointWasRemovedNotification";
// Notification Keys
NSString * const MIKMIDIDeviceKey = @"MIKMIDIDeviceKey";
NSString * const MIKMIDIEndpointKey = @"MIKMIDIEndpointKey";
static MIKMIDIDeviceManager *sharedDeviceManager;
@interface MIKMIDIDeviceManager ()
@property (nonatomic) MIDIClientRef client;
@property (nonatomic, strong) NSMutableArray *internalDevices;
- (void)addInternalDevicesObject:(MIKMIDIDevice *)device;
- (void)removeInternalDevicesObject:(MIKMIDIDevice *)device;
@property (nonatomic, strong) NSMutableArray *internalVirtualSources;
- (void)addInternalVirtualSourcesObject:(MIKMIDISourceEndpoint *)source;
- (void)removeInternalVirtualSourcesObject:(MIKMIDISourceEndpoint *)source;
@property (nonatomic, strong) NSMutableArray *internalVirtualDestinations;
- (void)addInternalVirtualDestinationsObject:(MIKMIDIDestinationEndpoint *)destination;
- (void)removeInternalVirtualDestinationsObject:(MIKMIDIDestinationEndpoint *)destination;
@property (nonatomic, strong) MIKMIDIInputPort *inputPort;
@property (nonatomic, strong) MIKMIDIOutputPort *outputPort;
@end
@implementation MIKMIDIDeviceManager
+ (instancetype)sharedDeviceManager;
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedDeviceManager = [(MIKMIDIDeviceManager *)[super allocWithZone:NULL] init];
});
return sharedDeviceManager;
}
- (id)init
{
if (self == sharedDeviceManager) return sharedDeviceManager;
self = [super init];
if (self) {
[self createClient];
[self retrieveAvailableDevices];
[self retrieveVirtualEndpoints];
#if TARGET_OS_IPHONE
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(appDidBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
#endif
}
return self;
}
+ (id)allocWithZone:(NSZone *)zone
{
return [self sharedDeviceManager];
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Public
- (nullable id)connectDevice:(MIKMIDIDevice *)device error:(NSError **)error eventHandler:(MIKMIDIEventHandlerBlock)eventHandler
{
error = error ?: &(NSError *__autoreleasing){ nil };
NSMutableArray *sources = [device.entities valueForKeyPath:@"@unionOfArrays.sources"];
if (![sources count]) {
*error = [NSError MIKMIDIErrorWithCode:MIKMIDIDeviceHasNoSourcesErrorCode userInfo:nil];
return nil;
}
NSMutableArray *tokens = [NSMutableArray array];
for (MIKMIDISourceEndpoint *source in sources) {
id token = [self.inputPort connectToSource:source error:error eventHandler:eventHandler];
if (!token) {
for (id token in tokens) { [self disconnectConnectionForToken:token]; }
return nil;
}
[tokens addObject:token];
}
return tokens;
}
- (id)connectInput:(MIKMIDISourceEndpoint *)endpoint error:(NSError **)error eventHandler:(MIKMIDIEventHandlerBlock)eventHandler
{
id result = [self.inputPort connectToSource:endpoint error:error eventHandler:eventHandler];
if (!result) return nil;
return @[result];
}
- (void)disconnectConnectionForToken:(id)connectionToken
{
for (id token in (NSArray *)connectionToken) {
[self.inputPort disconnectConnectionForToken:token];
}
}
- (BOOL)sendCommands:(NSArray *)commands toEndpoint:(MIKMIDIDestinationEndpoint *)endpoint error:(NSError **)error;
{
return [self.outputPort sendCommands:commands toDestination:endpoint error:error];
}
- (BOOL)sendCommands:(NSArray *)commands toVirtualEndpoint:(MIKMIDIClientSourceEndpoint *)endpoint error:(NSError **)error
{
return [endpoint sendCommands:commands error:error];
}
#pragma mark - Private
- (void)createClient
{
MIDIClientRef client;
OSStatus error = MIDIClientCreate(CFSTR("MIKMIDIDeviceManager"), MIKMIDIDeviceManagerNotifyCallback, (__bridge void *)self, &client);
if (error != noErr) { NSLog(@"Unable to create MIDI client"); return; }
self.client = client;
}
- (void)retrieveAvailableDevices
{
ItemCount numDevices = MIDIGetNumberOfDevices();
NSMutableArray *devices = [NSMutableArray arrayWithCapacity:numDevices];
for (ItemCount i=0; i<numDevices; i++) {
MIDIDeviceRef deviceRef = MIDIGetDevice(i);
MIKMIDIDevice *device = [MIKMIDIDevice MIDIObjectWithObjectRef:deviceRef];
if (!device || !device.isOnline) continue;
[devices addObject:device];
}
self.internalDevices = devices;
}
- (void)retrieveVirtualEndpoints
{
NSMutableArray *sources = [NSMutableArray array];
ItemCount numSources = MIDIGetNumberOfSources();
for (ItemCount i=0; i<numSources; i++) {
MIDIEndpointRef sourceRef = MIDIGetSource(i);
MIKMIDIEndpoint *source = [MIKMIDIEndpoint MIDIObjectWithObjectRef:sourceRef];
if (!source) continue;
[sources addObject:source];
}
self.internalVirtualSources = sources;
NSMutableArray *destinations = [NSMutableArray array];
ItemCount numDestinations = MIDIGetNumberOfDestinations();
for (ItemCount i=0; i<numDestinations; i++) {
MIDIEndpointRef destinationRef = MIDIGetDestination(i);
MIKMIDIEndpoint *destination = [MIKMIDIEndpoint MIDIObjectWithObjectRef:destinationRef];
if (!destination) continue;
[destinations addObject:destination];
}
self.internalVirtualDestinations = destinations;
}
#pragma mark - Notifications
- (void)appDidBecomeActiveNotification:(NSNotification *)notification
{
[self retrieveAvailableDevices];
}
#pragma mark - Callbacks
- (void)handleMIDIObjectPropertyChangeNotification:(MIDIObjectPropertyChangeNotification *)notification
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
NSString *changedProperty = (__bridge NSString *)notification->propertyName;
switch (notification->objectType) {
case kMIDIObjectType_Device: {
if (![changedProperty isEqualToString:(__bridge NSString *)kMIDIPropertyOffline]) break;
MIKMIDIDevice *changedObject = [MIKMIDIDevice MIDIObjectWithObjectRef:notification->object];
if (!changedObject) break;
if (changedObject.isOnline && ![self.internalDevices containsObject:changedObject]) {
[self addInternalDevicesObject:changedObject];
[nc postNotificationName:MIKMIDIDeviceWasAddedNotification object:self userInfo:@{MIKMIDIDeviceKey : changedObject}];
}
if (!changedObject.isOnline) {
[self removeInternalDevicesObject:changedObject];
[nc postNotificationName:MIKMIDIDeviceWasRemovedNotification object:self userInfo:@{MIKMIDIDeviceKey : changedObject}];
}
}
break;
case kMIDIObjectType_Source: {
if (![changedProperty isEqualToString:(__bridge NSString *)kMIDIPropertyPrivate]) break;
MIKMIDISourceEndpoint *changedObject = [MIKMIDISourceEndpoint MIDIObjectWithObjectRef:notification->object];
if (!changedObject) break;
if (!changedObject.isPrivate && ![self.internalVirtualSources containsObject:changedObject]) {
[self addInternalVirtualSourcesObject:changedObject];
[nc postNotificationName:MIKMIDIVirtualEndpointWasAddedNotification object:self userInfo:@{MIKMIDIEndpointKey : changedObject}];
}
if (changedObject.isPrivate) {
[self removeInternalVirtualSourcesObject:changedObject];
[nc postNotificationName:MIKMIDIVirtualEndpointWasRemovedNotification object:self userInfo:@{MIKMIDIEndpointKey : changedObject}];
}
}
break;
case kMIDIObjectType_Destination: {
if (![changedProperty isEqualToString:(__bridge NSString *)kMIDIPropertyPrivate]) break;
MIKMIDIDestinationEndpoint *changedObject = [MIKMIDIDestinationEndpoint MIDIObjectWithObjectRef:notification->object];
if (!changedObject) break;
if (!changedObject.isPrivate && ![self.internalVirtualDestinations containsObject:changedObject]) {
[self addInternalVirtualDestinationsObject:changedObject];
[nc postNotificationName:MIKMIDIVirtualEndpointWasAddedNotification object:self userInfo:@{MIKMIDIEndpointKey : changedObject}];
}
if (changedObject.isPrivate) {
[self removeInternalVirtualDestinationsObject:changedObject];
[nc postNotificationName:MIKMIDIVirtualEndpointWasRemovedNotification object:self userInfo:@{MIKMIDIEndpointKey : changedObject}];
}
}
break;
default:
break;
}
}
- (void)handleMIDIObjectRemoveNotification:(MIDIObjectAddRemoveNotification *)notification
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
switch (notification->childType) {
case kMIDIObjectType_Device: {
MIKMIDIDevice *removedDevice = [MIKMIDIDevice MIDIObjectWithObjectRef:notification->child];
if (!removedDevice) break;
[self removeInternalDevicesObject:removedDevice];
}
break;
case kMIDIObjectType_Source: {
MIKMIDISourceEndpoint *removedSource = [MIKMIDISourceEndpoint MIDIObjectWithObjectRef:notification->child];
if (!removedSource) {
// Sometimes that fails even though the MIDIObjectRef is for an object we already have an instance for
// FIXME: It might be better to have MIKMIDIObject maintain a table of instances and return an existing
// instance if a known object ref is passed into MIDIObjectWithObjectRef:
for (MIKMIDISourceEndpoint *source in self.virtualSources) {
if (source.objectRef == notification->child) {
removedSource = source;
break;
}
}
}
if (!removedSource) break;
[self removeInternalVirtualSourcesObject:removedSource];
[nc postNotificationName:MIKMIDIVirtualEndpointWasRemovedNotification object:self userInfo:@{MIKMIDIEndpointKey : removedSource}];
}
break;
case kMIDIObjectType_Destination: {
MIKMIDIDestinationEndpoint *removedDestination = [MIKMIDIDestinationEndpoint MIDIObjectWithObjectRef:notification->child];
if (!removedDestination) {
// Sometimes that fails even though the MIDIObjectRef is for an object we already have an instance for
for (MIKMIDIDestinationEndpoint *destination in self.virtualDestinations) {
if (destination.objectRef == notification->child) {
removedDestination = destination;
break;
}
}
}
if (!removedDestination) break;
[self removeInternalVirtualDestinationsObject:removedDestination];
[nc postNotificationName:MIKMIDIVirtualEndpointWasRemovedNotification object:self userInfo:@{MIKMIDIEndpointKey : removedDestination}];
}
break;
default:
break;
}
}
- (void)handleMIDIObjectAddNotification:(MIDIObjectAddRemoveNotification *)notification
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
switch (notification->childType) {
case kMIDIObjectType_Device: {
MIKMIDIDevice *addedDevice = [MIKMIDIDevice MIDIObjectWithObjectRef:notification->child];
if (addedDevice && ![self.internalDevices containsObject:addedDevice]) {
[self addInternalDevicesObject:addedDevice];
[nc postNotificationName:MIKMIDIDeviceWasAddedNotification object:self userInfo:@{MIKMIDIDeviceKey : addedDevice}];
}
}
break;
case kMIDIObjectType_Source: {
MIKMIDISourceEndpoint *addedSource = [MIKMIDISourceEndpoint MIDIObjectWithObjectRef:notification->child];
if (addedSource && ![self.internalVirtualSources containsObject:addedSource]) {
[self addInternalVirtualSourcesObject:addedSource];
[nc postNotificationName:MIKMIDIVirtualEndpointWasAddedNotification object:self userInfo:@{MIKMIDIEndpointKey : addedSource}];
}
}
break;
case kMIDIObjectType_Destination: {
MIKMIDIDestinationEndpoint *addedDestination = [MIKMIDIDestinationEndpoint MIDIObjectWithObjectRef:notification->child];
if (addedDestination && ![self.internalVirtualDestinations containsObject:addedDestination]) {
[self addInternalVirtualDestinationsObject:addedDestination];
[nc postNotificationName:MIKMIDIVirtualEndpointWasAddedNotification object:self userInfo:@{MIKMIDIEndpointKey : addedDestination}];
}
}
break;
default:
break;
}
}
void MIKMIDIDeviceManagerNotifyCallback(const MIDINotification *message, void *refCon)
{
MIKMIDIDeviceManager *self = (__bridge MIKMIDIDeviceManager *)refCon;
switch (message->messageID) {
case kMIDIMsgPropertyChanged:
[self handleMIDIObjectPropertyChangeNotification:(MIDIObjectPropertyChangeNotification *)message];
break;
case kMIDIMsgObjectRemoved:
[self handleMIDIObjectRemoveNotification:(MIDIObjectAddRemoveNotification *)message];
break;
case kMIDIMsgObjectAdded:
[self handleMIDIObjectAddNotification:(MIDIObjectAddRemoveNotification *)message];
break;
default:
break;
}
}
#pragma mark - Properties
+ (BOOL)automaticallyNotifiesObserversOfAvailableDevices { return NO; }
- (NSArray *)availableDevices { return [self.internalDevices copy]; }
- (void)addInternalDevicesObject:(MIKMIDIDevice *)device;
{
NSUInteger index = self.internalDevices.count;
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"availableDevices"];
[self.internalDevices insertObject:device atIndex:index];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"availableDevices"];
}
- (void)removeInternalDevicesObject:(MIKMIDIDevice *)device;
{
NSUInteger index = [self.internalDevices indexOfObject:device];
if (index == NSNotFound) return;
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"availableDevices"];
[self.internalDevices removeObjectAtIndex:index];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"availableDevices"];
}
+ (BOOL)automaticallyNotifiesObserversOfInternalVirtualSources { return NO; }
- (NSArray *)virtualSources { return [self.internalVirtualSources copy]; }
- (void)addInternalVirtualSourcesObject:(MIKMIDISourceEndpoint *)source
{
NSUInteger index = [self.internalVirtualSources count];
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualSources"];
[self.internalVirtualSources insertObject:source atIndex:index];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualSources"];
}
- (void)removeInternalVirtualSourcesObject:(MIKMIDISourceEndpoint *)source
{
NSUInteger index = [self.internalVirtualSources indexOfObject:source];
if (index == NSNotFound) return;
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualSources"];
[self.internalVirtualSources removeObjectAtIndex:index];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualSources"];
}
+ (BOOL)automaticallyNotifiesObserversOfVirtualSources { return NO; }
- (NSArray *)virtualDestinations { return [self.internalVirtualDestinations copy]; }
- (void)addInternalVirtualDestinationsObject:(MIKMIDIDestinationEndpoint *)destination
{
NSUInteger index = [self.internalVirtualDestinations count];
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualDestinations"];
[self.internalVirtualDestinations insertObject:destination atIndex:index];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualDestinations"];
}
- (void)removeInternalVirtualDestinationsObject:(MIKMIDIDestinationEndpoint *)destination
{
NSUInteger index = [self.internalVirtualDestinations indexOfObject:destination];
if (index == NSNotFound) return;
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualDestinations"];
[self.internalVirtualDestinations removeObjectAtIndex:index];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"virtualDestinations"];
}
+ (NSSet *)keyPathsForValuesAffectingConnectedInputSources
{
return [NSSet setWithObjects:@"inputPort.connectedSources", nil];
}
- (NSArray *)connectedInputSources
{
NSArray *result = self.inputPort.connectedSources;
if (!result) result = @[];
return result;
}
+ (NSSet *)keyPathsForValuesAffectingConnectedDevices
{
return [NSSet setWithObjects:@"connectedInputSources", @"availableDevices", nil];
}
- (NSArray<MIKMIDIDevice *> *)connectedDevices
{
NSSet *connectedSources = [NSSet setWithArray:self.connectedInputSources];
NSMutableArray *result = [NSMutableArray array];
for (MIKMIDIDevice *device in self.availableDevices) {
NSMutableArray *sources = [device.entities valueForKeyPath:@"@unionOfArrays.sources"];
for (MIKMIDISourceEndpoint *source in sources) {
if ([connectedSources containsObject:source]) {
[result addObject:device];
break;
}
}
}
return result;
}
- (MIKMIDIInputPort *)inputPort
{
if (!_inputPort) {
_inputPort = [[MIKMIDIInputPort alloc] initWithClient:self.client name:@"InputPort"];
}
return _inputPort;
}
- (MIKMIDIOutputPort *)outputPort
{
if (!_outputPort) {
_outputPort = [[MIKMIDIOutputPort alloc] initWithClient:self.client name:@"OutputPort"];
}
return _outputPort;
}
@end
#pragma mark -
@implementation MIKMIDIDeviceManager (Deprecated)
- (void)disconnectInput:(MIKMIDISourceEndpoint *)endpoint forConnectionToken:(id)connectionToken
{
[self disconnectConnectionForToken:connectionToken];
}
@end