| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #import "MIDIClient.h" |
| |
| #include <CoreMIDI/CoreMIDI.h> |
| |
| #import "MIDIEndpoint.h" |
| #import "MIDIMessage.h" |
| |
| NSString * const MIDIClientErrorDomain = @"MIDIClientErrorDomain"; |
| |
| @interface MIDIClient () |
| @property (readwrite, nonatomic) MIDISource *source; |
| @property (readwrite, nonatomic) MIDIDestination *destination; |
| // Used by midiRead() for SysEx messages spanning multiple packets. |
| @property (readwrite, nonatomic) NSMutableData *sysExBuffer; |
| |
| /** Returns whether the client's source or destination is attached to a particular device. */ |
| - (BOOL)attachedToDevice:(MIDIDeviceRef)device; |
| @end |
| |
| // Note: These functions (midiStateChanged and midiRead) are not called on the main thread! |
| static void midiStateChanged(const MIDINotification *message, void *context) { |
| MIDIClient *client = (__bridge MIDIClient *)context; |
| |
| switch (message->messageID) { |
| case kMIDIMsgObjectAdded: { |
| const MIDIObjectAddRemoveNotification *notification = |
| (const MIDIObjectAddRemoveNotification *)message; |
| |
| @autoreleasepool { |
| if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && |
| [client.delegate respondsToSelector:@selector(MIDIClientEndpointAdded:)]) { |
| [client.delegate MIDIClientEndpointAdded:client]; |
| } |
| } |
| break; |
| } |
| |
| case kMIDIMsgObjectRemoved: { |
| const MIDIObjectAddRemoveNotification *notification = |
| (const MIDIObjectAddRemoveNotification *)message; |
| |
| @autoreleasepool { |
| if ((notification->childType & (kMIDIObjectType_Source|kMIDIObjectType_Destination)) != 0 && |
| [client.delegate respondsToSelector:@selector(MIDIClientEndpointRemoved:)]) { |
| [client.delegate MIDIClientEndpointRemoved:client]; |
| } |
| } |
| break; |
| } |
| |
| case kMIDIMsgSetupChanged: |
| case kMIDIMsgPropertyChanged: |
| case kMIDIMsgSerialPortOwnerChanged: |
| case kMIDIMsgThruConnectionsChanged: { |
| @autoreleasepool { |
| if ([client.delegate respondsToSelector:@selector(MIDIClientConfigurationChanged:)]) { |
| [client.delegate MIDIClientConfigurationChanged:client]; |
| } |
| } |
| break; |
| } |
| |
| case kMIDIMsgIOError: { |
| const MIDIIOErrorNotification *notification = (const MIDIIOErrorNotification *)message; |
| |
| if ([client attachedToDevice:notification->driverDevice]) { |
| @autoreleasepool { |
| NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain |
| code:notification->errorCode |
| userInfo:nil]; |
| if ([client.delegate respondsToSelector:@selector(MIDIClient:receivedError:)]) { |
| [client.delegate MIDIClient:client receivedError:error]; |
| } |
| } |
| } |
| break; |
| } |
| |
| default: { |
| NSLog(@"Unhandled MIDI state change: %d", (int)message->messageID); |
| } |
| } |
| } |
| |
| static void midiRead(const MIDIPacketList *packets, void *portContext, void *sourceContext) { |
| MIDIClient *client = (__bridge MIDIClient *)portContext; |
| |
| // Read the data out of each packet and forward it to the client's delegate. |
| // Each MIDIPacket will contain either some MIDI commands, or the start/continuation of a SysEx |
| // command. The start of a command is detected with a byte greater than or equal to 0x80 (all data |
| // must be 7-bit friendly). The end of a SysEx command is marked with 0x7F. |
| |
| // TODO(pquinn): Should something be done with the timestamp data? |
| |
| UInt32 packetCount = packets->numPackets; |
| const MIDIPacket *packet = &packets->packet[0]; |
| @autoreleasepool { |
| while (packetCount--) { |
| if (packet->length == 0) { |
| continue; |
| } |
| |
| const Byte firstByte = packet->data[0]; |
| const Byte lastByte = packet->data[packet->length - 1]; |
| |
| if (firstByte >= 0x80 && firstByte != MIDIMessageSysEx && firstByte != MIDIMessageSysExEnd) { |
| // Packet describes non-SysEx MIDI messages. |
| NSMutableData *data = nil; |
| for (UInt16 i = 0; i < packet->length; ++i) { |
| // Packets can contain multiple MIDI messages. |
| if (packet->data[i] >= 0x80) { |
| if (data.length > 0) { // Tell the delegate about the last extracted command. |
| [client.delegate MIDIClient:client receivedData:data]; |
| } |
| data = [[NSMutableData alloc] init]; |
| } |
| [data appendBytes:&packet->data[i] length:1]; |
| } |
| |
| if (data.length > 0) { |
| [client.delegate MIDIClient:client receivedData:data]; |
| } |
| } |
| |
| if (firstByte == MIDIMessageSysEx) { |
| // The start of a SysEx message; collect data into sysExBuffer. |
| client.sysExBuffer = [[NSMutableData alloc] initWithBytes:packet->data |
| length:packet->length]; |
| } else if (firstByte < 0x80 || firstByte == MIDIMessageSysExEnd) { |
| // Continuation or end of a SysEx message. |
| [client.sysExBuffer appendBytes:packet->data length:packet->length]; |
| } |
| |
| if (lastByte == MIDIMessageSysExEnd) { |
| // End of a SysEx message. |
| [client.delegate MIDIClient:client receivedData:client.sysExBuffer]; |
| client.sysExBuffer = nil; |
| } |
| |
| packet = MIDIPacketNext(packet); |
| } |
| } |
| } |
| |
| @implementation MIDIClient { |
| NSString *_name; |
| MIDIClientRef _client; |
| MIDIPortRef _input; |
| MIDIPortRef _output; |
| } |
| |
| - (instancetype)initWithName:(NSString *)name error:(NSError **)error { |
| if ((self = [super init])) { |
| _name = name; // Hold onto the name because MIDIClientCreate() doesn't retain it. |
| OSStatus result = MIDIClientCreate((__bridge CFStringRef)name, |
| midiStateChanged, |
| (__bridge void *)self, |
| &_client); |
| if (result != noErr) { |
| if (error) { |
| *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; |
| } |
| self = nil; |
| } |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| MIDIClientDispose(_client); // Automatically disposes of the ports too. |
| } |
| |
| - (BOOL)connectToSource:(MIDISource *)source error:(NSError **)error { |
| OSStatus result = noErr; |
| if (!_input) { // Lazily create the input port. |
| result = MIDIInputPortCreate(_client, |
| (__bridge CFStringRef)_name, |
| midiRead, |
| (__bridge void *)self, |
| &_input); |
| if (result != noErr) { |
| if (error) { |
| *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; |
| } |
| return NO; |
| } |
| } |
| |
| // Connect the source to the port. |
| result = MIDIPortConnectSource(_input, source.endpoint, (__bridge void *)self); |
| if (result != noErr) { |
| if (error) { |
| *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; |
| } |
| return NO; |
| } |
| |
| self.source = source; |
| return YES; |
| } |
| |
| - (BOOL)connectToDestination:(MIDIDestination *)destination error:(NSError **)error { |
| if (!_output) { // Lazily create the output port. |
| OSStatus result = MIDIOutputPortCreate(_client, |
| (__bridge CFStringRef)_name, |
| &_output); |
| if (result != noErr) { |
| if (error) { |
| *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; |
| } |
| return NO; |
| } |
| } |
| |
| self.destination = destination; |
| return YES; |
| } |
| |
| - (BOOL)sendData:(NSData *)data error:(NSError **)error { |
| if (data.length > sizeof(((MIDIPacket *)0)->data)) { |
| // TODO(pquinn): Dynamically allocate a buffer. |
| if (error) { |
| *error = [NSError errorWithDomain:MIDIClientErrorDomain |
| code:0 |
| userInfo:@{NSLocalizedDescriptionKey: |
| @"Too much data for a basic MIDIPacket."}]; |
| } |
| return NO; |
| } |
| |
| MIDIPacketList packetList; |
| MIDIPacket *packet = MIDIPacketListInit(&packetList); |
| packet = MIDIPacketListAdd(&packetList, sizeof(packetList), packet, 0, data.length, data.bytes); |
| if (!packet) { |
| if (error) { |
| *error = [NSError errorWithDomain:MIDIClientErrorDomain |
| code:0 |
| userInfo:@{NSLocalizedDescriptionKey: |
| @"Packet too large for buffer."}]; |
| } |
| return NO; |
| } |
| |
| OSStatus result = MIDISend(_output, self.destination.endpoint, &packetList); |
| if (result != noErr) { |
| if (error) { |
| *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil]; |
| } |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (BOOL)attachedToDevice:(MIDIDeviceRef)device { |
| MIDIDeviceRef sourceDevice = 0, destinationDevice = 0; |
| MIDIEntityGetDevice(self.source.endpoint, &sourceDevice); |
| MIDIEntityGetDevice(self.destination.endpoint, &destinationDevice); |
| |
| SInt32 sourceID = 0, destinationID = 0, deviceID = 0; |
| MIDIObjectGetIntegerProperty(sourceDevice, kMIDIPropertyUniqueID, &sourceID); |
| MIDIObjectGetIntegerProperty(destinationDevice, kMIDIPropertyUniqueID, &destinationID); |
| MIDIObjectGetIntegerProperty(device, kMIDIPropertyUniqueID, &deviceID); |
| |
| return (deviceID == sourceID || deviceID == destinationID); |
| } |
| @end |