1*bf47c682SAndroid Build Coastguard Worker/* 2*bf47c682SAndroid Build Coastguard Worker * Copyright (C) 2016 The Android Open Source Project 3*bf47c682SAndroid Build Coastguard Worker * 4*bf47c682SAndroid Build Coastguard Worker * Licensed under the Apache License, Version 2.0 (the "License"); 5*bf47c682SAndroid Build Coastguard Worker * you may not use this file except in compliance with the License. 6*bf47c682SAndroid Build Coastguard Worker * You may obtain a copy of the License at 7*bf47c682SAndroid Build Coastguard Worker * 8*bf47c682SAndroid Build Coastguard Worker * http://www.apache.org/licenses/LICENSE-2.0 9*bf47c682SAndroid Build Coastguard Worker * 10*bf47c682SAndroid Build Coastguard Worker * Unless required by applicable law or agreed to in writing, software 11*bf47c682SAndroid Build Coastguard Worker * distributed under the License is distributed on an "AS IS" BASIS, 12*bf47c682SAndroid Build Coastguard Worker * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*bf47c682SAndroid Build Coastguard Worker * See the License for the specific language governing permissions and 14*bf47c682SAndroid Build Coastguard Worker * limitations under the License. 15*bf47c682SAndroid Build Coastguard Worker */ 16*bf47c682SAndroid Build Coastguard Worker 17*bf47c682SAndroid Build Coastguard Worker#import "MIDIClient.h" 18*bf47c682SAndroid Build Coastguard Worker 19*bf47c682SAndroid Build Coastguard Worker#include <CoreMIDI/CoreMIDI.h> 20*bf47c682SAndroid Build Coastguard Worker 21*bf47c682SAndroid Build Coastguard Worker#import "MIDIEndpoint.h" 22*bf47c682SAndroid Build Coastguard Worker#import "MIDIMessage.h" 23*bf47c682SAndroid Build Coastguard Worker 24*bf47c682SAndroid Build Coastguard WorkerNSString * const MIDIClientErrorDomain = @"MIDIClientErrorDomain"; 25*bf47c682SAndroid Build Coastguard Worker 26*bf47c682SAndroid Build Coastguard Worker@interface MIDIClient () 27*bf47c682SAndroid Build Coastguard Worker@property (readwrite, nonatomic) MIDISource *source; 28*bf47c682SAndroid Build Coastguard Worker@property (readwrite, nonatomic) MIDIDestination *destination; 29*bf47c682SAndroid Build Coastguard Worker// Used by midiRead() for SysEx messages spanning multiple packets. 30*bf47c682SAndroid Build Coastguard Worker@property (readwrite, nonatomic) NSMutableData *sysExBuffer; 31*bf47c682SAndroid Build Coastguard Worker 32*bf47c682SAndroid Build Coastguard Worker/** Returns whether the client's source or destination is attached to a particular device. */ 33*bf47c682SAndroid Build Coastguard Worker- (BOOL)attachedToDevice:(MIDIDeviceRef)device; 34*bf47c682SAndroid Build Coastguard Worker@end 35*bf47c682SAndroid Build Coastguard Worker 36*bf47c682SAndroid Build Coastguard Worker// Note: These functions (midiStateChanged and midiRead) are not called on the main thread! 37*bf47c682SAndroid Build Coastguard Workerstatic void midiStateChanged(const MIDINotification *message, void *context) { 38*bf47c682SAndroid Build Coastguard Worker MIDIClient *client = (__bridge MIDIClient *)context; 39*bf47c682SAndroid Build Coastguard Worker 40*bf47c682SAndroid Build Coastguard Worker switch (message->messageID) { 41*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgObjectAdded: { 42*bf47c682SAndroid Build Coastguard Worker const MIDIObjectAddRemoveNotification *notification = 43*bf47c682SAndroid Build Coastguard Worker (const MIDIObjectAddRemoveNotification *)message; 44*bf47c682SAndroid Build Coastguard Worker 45*bf47c682SAndroid Build Coastguard Worker @autoreleasepool { 46*bf47c682SAndroid Build Coastguard Worker if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && 47*bf47c682SAndroid Build Coastguard Worker [client.delegate respondsToSelector:@selector(MIDIClientEndpointAdded:)]) { 48*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClientEndpointAdded:client]; 49*bf47c682SAndroid Build Coastguard Worker } 50*bf47c682SAndroid Build Coastguard Worker } 51*bf47c682SAndroid Build Coastguard Worker break; 52*bf47c682SAndroid Build Coastguard Worker } 53*bf47c682SAndroid Build Coastguard Worker 54*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgObjectRemoved: { 55*bf47c682SAndroid Build Coastguard Worker const MIDIObjectAddRemoveNotification *notification = 56*bf47c682SAndroid Build Coastguard Worker (const MIDIObjectAddRemoveNotification *)message; 57*bf47c682SAndroid Build Coastguard Worker 58*bf47c682SAndroid Build Coastguard Worker @autoreleasepool { 59*bf47c682SAndroid Build Coastguard Worker if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && 60*bf47c682SAndroid Build Coastguard Worker [client.delegate respondsToSelector:@selector(MIDIClientEndpointRemoved:)]) { 61*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClientEndpointRemoved:client]; 62*bf47c682SAndroid Build Coastguard Worker } 63*bf47c682SAndroid Build Coastguard Worker } 64*bf47c682SAndroid Build Coastguard Worker break; 65*bf47c682SAndroid Build Coastguard Worker } 66*bf47c682SAndroid Build Coastguard Worker 67*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgSetupChanged: 68*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgPropertyChanged: 69*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgSerialPortOwnerChanged: 70*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgThruConnectionsChanged: { 71*bf47c682SAndroid Build Coastguard Worker @autoreleasepool { 72*bf47c682SAndroid Build Coastguard Worker if ([client.delegate respondsToSelector:@selector(MIDIClientConfigurationChanged:)]) { 73*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClientConfigurationChanged:client]; 74*bf47c682SAndroid Build Coastguard Worker } 75*bf47c682SAndroid Build Coastguard Worker } 76*bf47c682SAndroid Build Coastguard Worker break; 77*bf47c682SAndroid Build Coastguard Worker } 78*bf47c682SAndroid Build Coastguard Worker 79*bf47c682SAndroid Build Coastguard Worker case kMIDIMsgIOError: { 80*bf47c682SAndroid Build Coastguard Worker const MIDIIOErrorNotification *notification = (const MIDIIOErrorNotification *)message; 81*bf47c682SAndroid Build Coastguard Worker 82*bf47c682SAndroid Build Coastguard Worker if ([client attachedToDevice:notification->driverDevice]) { 83*bf47c682SAndroid Build Coastguard Worker @autoreleasepool { 84*bf47c682SAndroid Build Coastguard Worker NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain 85*bf47c682SAndroid Build Coastguard Worker code:notification->errorCode 86*bf47c682SAndroid Build Coastguard Worker userInfo:nil]; 87*bf47c682SAndroid Build Coastguard Worker if ([client.delegate respondsToSelector:@selector(MIDIClient:receivedError:)]) { 88*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClient:client receivedError:error]; 89*bf47c682SAndroid Build Coastguard Worker } 90*bf47c682SAndroid Build Coastguard Worker } 91*bf47c682SAndroid Build Coastguard Worker } 92*bf47c682SAndroid Build Coastguard Worker break; 93*bf47c682SAndroid Build Coastguard Worker } 94*bf47c682SAndroid Build Coastguard Worker 95*bf47c682SAndroid Build Coastguard Worker default: { 96*bf47c682SAndroid Build Coastguard Worker NSLog(@"Unhandled MIDI state change: %d", (int)message->messageID); 97*bf47c682SAndroid Build Coastguard Worker } 98*bf47c682SAndroid Build Coastguard Worker } 99*bf47c682SAndroid Build Coastguard Worker} 100*bf47c682SAndroid Build Coastguard Worker 101*bf47c682SAndroid Build Coastguard Workerstatic void midiRead(const MIDIPacketList *packets, void *portContext, void *sourceContext) { 102*bf47c682SAndroid Build Coastguard Worker MIDIClient *client = (__bridge MIDIClient *)portContext; 103*bf47c682SAndroid Build Coastguard Worker 104*bf47c682SAndroid Build Coastguard Worker // Read the data out of each packet and forward it to the client's delegate. 105*bf47c682SAndroid Build Coastguard Worker // Each MIDIPacket will contain either some MIDI commands, or the start/continuation of a SysEx 106*bf47c682SAndroid Build Coastguard Worker // command. The start of a command is detected with a byte greater than or equal to 0x80 (all data 107*bf47c682SAndroid Build Coastguard Worker // must be 7-bit friendly). The end of a SysEx command is marked with 0x7F. 108*bf47c682SAndroid Build Coastguard Worker 109*bf47c682SAndroid Build Coastguard Worker // TODO(pquinn): Should something be done with the timestamp data? 110*bf47c682SAndroid Build Coastguard Worker 111*bf47c682SAndroid Build Coastguard Worker UInt32 packetCount = packets->numPackets; 112*bf47c682SAndroid Build Coastguard Worker const MIDIPacket *packet = &packets->packet[0]; 113*bf47c682SAndroid Build Coastguard Worker @autoreleasepool { 114*bf47c682SAndroid Build Coastguard Worker while (packetCount--) { 115*bf47c682SAndroid Build Coastguard Worker if (packet->length == 0) { 116*bf47c682SAndroid Build Coastguard Worker continue; 117*bf47c682SAndroid Build Coastguard Worker } 118*bf47c682SAndroid Build Coastguard Worker 119*bf47c682SAndroid Build Coastguard Worker const Byte firstByte = packet->data[0]; 120*bf47c682SAndroid Build Coastguard Worker const Byte lastByte = packet->data[packet->length - 1]; 121*bf47c682SAndroid Build Coastguard Worker 122*bf47c682SAndroid Build Coastguard Worker if (firstByte >= 0x80 && firstByte != MIDIMessageSysEx && firstByte != MIDIMessageSysExEnd) { 123*bf47c682SAndroid Build Coastguard Worker // Packet describes non-SysEx MIDI messages. 124*bf47c682SAndroid Build Coastguard Worker NSMutableData *data = nil; 125*bf47c682SAndroid Build Coastguard Worker for (UInt16 i = 0; i < packet->length; ++i) { 126*bf47c682SAndroid Build Coastguard Worker // Packets can contain multiple MIDI messages. 127*bf47c682SAndroid Build Coastguard Worker if (packet->data[i] >= 0x80) { 128*bf47c682SAndroid Build Coastguard Worker if (data.length > 0) { // Tell the delegate about the last extracted command. 129*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClient:client receivedData:data]; 130*bf47c682SAndroid Build Coastguard Worker } 131*bf47c682SAndroid Build Coastguard Worker data = [[NSMutableData alloc] init]; 132*bf47c682SAndroid Build Coastguard Worker } 133*bf47c682SAndroid Build Coastguard Worker [data appendBytes:&packet->data[i] length:1]; 134*bf47c682SAndroid Build Coastguard Worker } 135*bf47c682SAndroid Build Coastguard Worker 136*bf47c682SAndroid Build Coastguard Worker if (data.length > 0) { 137*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClient:client receivedData:data]; 138*bf47c682SAndroid Build Coastguard Worker } 139*bf47c682SAndroid Build Coastguard Worker } 140*bf47c682SAndroid Build Coastguard Worker 141*bf47c682SAndroid Build Coastguard Worker if (firstByte == MIDIMessageSysEx) { 142*bf47c682SAndroid Build Coastguard Worker // The start of a SysEx message; collect data into sysExBuffer. 143*bf47c682SAndroid Build Coastguard Worker client.sysExBuffer = [[NSMutableData alloc] initWithBytes:packet->data 144*bf47c682SAndroid Build Coastguard Worker length:packet->length]; 145*bf47c682SAndroid Build Coastguard Worker } else if (firstByte < 0x80 || firstByte == MIDIMessageSysExEnd) { 146*bf47c682SAndroid Build Coastguard Worker // Continuation or end of a SysEx message. 147*bf47c682SAndroid Build Coastguard Worker [client.sysExBuffer appendBytes:packet->data length:packet->length]; 148*bf47c682SAndroid Build Coastguard Worker } 149*bf47c682SAndroid Build Coastguard Worker 150*bf47c682SAndroid Build Coastguard Worker if (lastByte == MIDIMessageSysExEnd) { 151*bf47c682SAndroid Build Coastguard Worker // End of a SysEx message. 152*bf47c682SAndroid Build Coastguard Worker [client.delegate MIDIClient:client receivedData:client.sysExBuffer]; 153*bf47c682SAndroid Build Coastguard Worker client.sysExBuffer = nil; 154*bf47c682SAndroid Build Coastguard Worker } 155*bf47c682SAndroid Build Coastguard Worker 156*bf47c682SAndroid Build Coastguard Worker packet = MIDIPacketNext(packet); 157*bf47c682SAndroid Build Coastguard Worker } 158*bf47c682SAndroid Build Coastguard Worker } 159*bf47c682SAndroid Build Coastguard Worker} 160*bf47c682SAndroid Build Coastguard Worker 161*bf47c682SAndroid Build Coastguard Worker@implementation MIDIClient { 162*bf47c682SAndroid Build Coastguard Worker NSString *_name; 163*bf47c682SAndroid Build Coastguard Worker MIDIClientRef _client; 164*bf47c682SAndroid Build Coastguard Worker MIDIPortRef _input; 165*bf47c682SAndroid Build Coastguard Worker MIDIPortRef _output; 166*bf47c682SAndroid Build Coastguard Worker} 167*bf47c682SAndroid Build Coastguard Worker 168*bf47c682SAndroid Build Coastguard Worker- (instancetype)initWithName:(NSString *)name error:(NSError **)error { 169*bf47c682SAndroid Build Coastguard Worker if ((self = [super init])) { 170*bf47c682SAndroid Build Coastguard Worker _name = name; // Hold onto the name because MIDIClientCreate() doesn't retain it. 171*bf47c682SAndroid Build Coastguard Worker OSStatus result = MIDIClientCreate((__bridge CFStringRef)name, 172*bf47c682SAndroid Build Coastguard Worker midiStateChanged, 173*bf47c682SAndroid Build Coastguard Worker (__bridge void *)self, 174*bf47c682SAndroid Build Coastguard Worker &_client); 175*bf47c682SAndroid Build Coastguard Worker if (result != noErr) { 176*bf47c682SAndroid Build Coastguard Worker if (error) { 177*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; 178*bf47c682SAndroid Build Coastguard Worker } 179*bf47c682SAndroid Build Coastguard Worker self = nil; 180*bf47c682SAndroid Build Coastguard Worker } 181*bf47c682SAndroid Build Coastguard Worker } 182*bf47c682SAndroid Build Coastguard Worker return self; 183*bf47c682SAndroid Build Coastguard Worker} 184*bf47c682SAndroid Build Coastguard Worker 185*bf47c682SAndroid Build Coastguard Worker- (void)dealloc { 186*bf47c682SAndroid Build Coastguard Worker MIDIClientDispose(_client); // Automatically disposes of the ports too. 187*bf47c682SAndroid Build Coastguard Worker} 188*bf47c682SAndroid Build Coastguard Worker 189*bf47c682SAndroid Build Coastguard Worker- (BOOL)connectToSource:(MIDISource *)source error:(NSError **)error { 190*bf47c682SAndroid Build Coastguard Worker OSStatus result = noErr; 191*bf47c682SAndroid Build Coastguard Worker if (!_input) { // Lazily create the input port. 192*bf47c682SAndroid Build Coastguard Worker result = MIDIInputPortCreate(_client, 193*bf47c682SAndroid Build Coastguard Worker (__bridge CFStringRef)_name, 194*bf47c682SAndroid Build Coastguard Worker midiRead, 195*bf47c682SAndroid Build Coastguard Worker (__bridge void *)self, 196*bf47c682SAndroid Build Coastguard Worker &_input); 197*bf47c682SAndroid Build Coastguard Worker if (result != noErr) { 198*bf47c682SAndroid Build Coastguard Worker if (error) { 199*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; 200*bf47c682SAndroid Build Coastguard Worker } 201*bf47c682SAndroid Build Coastguard Worker return NO; 202*bf47c682SAndroid Build Coastguard Worker } 203*bf47c682SAndroid Build Coastguard Worker } 204*bf47c682SAndroid Build Coastguard Worker 205*bf47c682SAndroid Build Coastguard Worker // Connect the source to the port. 206*bf47c682SAndroid Build Coastguard Worker result = MIDIPortConnectSource(_input, source.endpoint, (__bridge void *)self); 207*bf47c682SAndroid Build Coastguard Worker if (result != noErr) { 208*bf47c682SAndroid Build Coastguard Worker if (error) { 209*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; 210*bf47c682SAndroid Build Coastguard Worker } 211*bf47c682SAndroid Build Coastguard Worker return NO; 212*bf47c682SAndroid Build Coastguard Worker } 213*bf47c682SAndroid Build Coastguard Worker 214*bf47c682SAndroid Build Coastguard Worker self.source = source; 215*bf47c682SAndroid Build Coastguard Worker return YES; 216*bf47c682SAndroid Build Coastguard Worker} 217*bf47c682SAndroid Build Coastguard Worker 218*bf47c682SAndroid Build Coastguard Worker- (BOOL)connectToDestination:(MIDIDestination *)destination error:(NSError **)error { 219*bf47c682SAndroid Build Coastguard Worker if (!_output) { // Lazily create the output port. 220*bf47c682SAndroid Build Coastguard Worker OSStatus result = MIDIOutputPortCreate(_client, 221*bf47c682SAndroid Build Coastguard Worker (__bridge CFStringRef)_name, 222*bf47c682SAndroid Build Coastguard Worker &_output); 223*bf47c682SAndroid Build Coastguard Worker if (result != noErr) { 224*bf47c682SAndroid Build Coastguard Worker if (error) { 225*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; 226*bf47c682SAndroid Build Coastguard Worker } 227*bf47c682SAndroid Build Coastguard Worker return NO; 228*bf47c682SAndroid Build Coastguard Worker } 229*bf47c682SAndroid Build Coastguard Worker } 230*bf47c682SAndroid Build Coastguard Worker 231*bf47c682SAndroid Build Coastguard Worker self.destination = destination; 232*bf47c682SAndroid Build Coastguard Worker return YES; 233*bf47c682SAndroid Build Coastguard Worker} 234*bf47c682SAndroid Build Coastguard Worker 235*bf47c682SAndroid Build Coastguard Worker- (BOOL)sendData:(NSData *)data error:(NSError **)error { 236*bf47c682SAndroid Build Coastguard Worker if (data.length > sizeof(((MIDIPacket *)0)->data)) { 237*bf47c682SAndroid Build Coastguard Worker // TODO(pquinn): Dynamically allocate a buffer. 238*bf47c682SAndroid Build Coastguard Worker if (error) { 239*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:MIDIClientErrorDomain 240*bf47c682SAndroid Build Coastguard Worker code:0 241*bf47c682SAndroid Build Coastguard Worker userInfo:@{NSLocalizedDescriptionKey: 242*bf47c682SAndroid Build Coastguard Worker @"Too much data for a basic MIDIPacket."}]; 243*bf47c682SAndroid Build Coastguard Worker } 244*bf47c682SAndroid Build Coastguard Worker return NO; 245*bf47c682SAndroid Build Coastguard Worker } 246*bf47c682SAndroid Build Coastguard Worker 247*bf47c682SAndroid Build Coastguard Worker MIDIPacketList packetList; 248*bf47c682SAndroid Build Coastguard Worker MIDIPacket *packet = MIDIPacketListInit(&packetList); 249*bf47c682SAndroid Build Coastguard Worker packet = MIDIPacketListAdd(&packetList, sizeof(packetList), packet, 0, data.length, data.bytes); 250*bf47c682SAndroid Build Coastguard Worker if (!packet) { 251*bf47c682SAndroid Build Coastguard Worker if (error) { 252*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:MIDIClientErrorDomain 253*bf47c682SAndroid Build Coastguard Worker code:0 254*bf47c682SAndroid Build Coastguard Worker userInfo:@{NSLocalizedDescriptionKey: 255*bf47c682SAndroid Build Coastguard Worker @"Packet too large for buffer."}]; 256*bf47c682SAndroid Build Coastguard Worker } 257*bf47c682SAndroid Build Coastguard Worker return NO; 258*bf47c682SAndroid Build Coastguard Worker } 259*bf47c682SAndroid Build Coastguard Worker 260*bf47c682SAndroid Build Coastguard Worker OSStatus result = MIDISend(_output, self.destination.endpoint, &packetList); 261*bf47c682SAndroid Build Coastguard Worker if (result != noErr) { 262*bf47c682SAndroid Build Coastguard Worker if (error) { 263*bf47c682SAndroid Build Coastguard Worker *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; 264*bf47c682SAndroid Build Coastguard Worker } 265*bf47c682SAndroid Build Coastguard Worker return NO; 266*bf47c682SAndroid Build Coastguard Worker } 267*bf47c682SAndroid Build Coastguard Worker return YES; 268*bf47c682SAndroid Build Coastguard Worker} 269*bf47c682SAndroid Build Coastguard Worker 270*bf47c682SAndroid Build Coastguard Worker- (BOOL)attachedToDevice:(MIDIDeviceRef)device { 271*bf47c682SAndroid Build Coastguard Worker MIDIDeviceRef sourceDevice = 0, destinationDevice = 0; 272*bf47c682SAndroid Build Coastguard Worker MIDIEntityGetDevice(self.source.endpoint, &sourceDevice); 273*bf47c682SAndroid Build Coastguard Worker MIDIEntityGetDevice(self.destination.endpoint, &destinationDevice); 274*bf47c682SAndroid Build Coastguard Worker 275*bf47c682SAndroid Build Coastguard Worker SInt32 sourceID = 0, destinationID = 0, deviceID = 0; 276*bf47c682SAndroid Build Coastguard Worker MIDIObjectGetIntegerProperty(sourceDevice, kMIDIPropertyUniqueID, &sourceID); 277*bf47c682SAndroid Build Coastguard Worker MIDIObjectGetIntegerProperty(destinationDevice, kMIDIPropertyUniqueID, &destinationID); 278*bf47c682SAndroid Build Coastguard Worker MIDIObjectGetIntegerProperty(device, kMIDIPropertyUniqueID, &deviceID); 279*bf47c682SAndroid Build Coastguard Worker 280*bf47c682SAndroid Build Coastguard Worker return (deviceID == sourceID || deviceID == destinationID); 281*bf47c682SAndroid Build Coastguard Worker} 282*bf47c682SAndroid Build Coastguard Worker@end 283