blob: 1c51f5fafcd0df7d13c9b3482adce7f15d963c5b [file] [log] [blame]
/*
* 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