Home | History | Annotate | Download | only in WALT
      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 
     24 NSString * 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!
     37 static 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 
    101 static 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