• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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