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