Skip to content

Instantly share code, notes, and snippets.

@jemmons
Last active November 17, 2022 07:04
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jemmons/847cdc494eb332ba1b9f50f18ee8616a to your computer and use it in GitHub Desktop.
Save jemmons/847cdc494eb332ba1b9f50f18ee8616a to your computer and use it in GitHub Desktop.
Sample CoreMIDI code…
// See: https://forums.developer.apple.com/thread/65997
MIDIInputPortCreateWithBlock(midiClient, "Instrument" as CFString, &inPort, {
(unsafePacketList: UnsafePointer<MIDIPacketList>, pointer: UnsafeMutableRawPointer?) in
let packetList = unsafePacketList.pointee
if packetList.numPackets == 1 {
let packet = packetList.packet
if packet.length == 3 && packet.data.0 == 144 {
/* Note-On */
let note = packet.data.1
let velocity = packet.data.2
if velocity > 0 {
DispatchQueue.main.async(execute: {
// use note
})
}
}
}
})
//See: https://developer.apple.com/library/archive/qa/qa1374/_index.html
// ____________________________________________________________________________
// Obtain the name of an endpoint without regard for whether it has connections.
// The result should be released by the caller.
static CFStringRef GetEndpointDisplayName(MIDIEndpointRef endpoint)
{
CFStringRef result = CFSTR(""); // default
MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &result);
return result;
}
//See: https://developer.apple.com/library/archive/qa/qa1374/_index.html
// Obtain the name of an endpoint, following connections.
// The result should be released by the caller.
static CFStringRef CreateConnectedEndpointName(MIDIEndpointRef endpoint)
{
CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
CFStringRef str;
OSStatus err;
// Does the endpoint have connections?
CFDataRef connections = NULL;
int nConnected = 0;
bool anyStrings = false;
err = MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, &connections);
if (connections != NULL) {
// It has connections, follow them
// Concatenate the names of all connected devices
nConnected = CFDataGetLength(connections) / sizeof(MIDIUniqueID);
if (nConnected) {
const SInt32 *pid = reinterpret_cast<const SInt32 *>(CFDataGetBytePtr(connections));
for (int i = 0; i < nConnected; ++i, ++pid) {
MIDIUniqueID id = EndianS32_BtoN(*pid);
MIDIObjectRef connObject;
MIDIObjectType connObjectType;
err = MIDIObjectFindByUniqueID(id, &connObject, &connObjectType);
if (err == noErr) {
if (connObjectType == kMIDIObjectType_ExternalSource ||
connObjectType == kMIDIObjectType_ExternalDestination) {
// Connected to an external device's endpoint (10.3 and later).
str = EndpointName(static_cast<MIDIEndpointRef>(connObject), true);
} else {
// Connected to an external device (10.2) (or something else, catch-all)
str = NULL;
MIDIObjectGetStringProperty(connObject, kMIDIPropertyName, &str);
}
if (str != NULL) {
if (anyStrings)
CFStringAppend(result, CFSTR(", "));
else anyStrings = true;
CFStringAppend(result, str);
CFRelease(str);
}
}
}
}
CFRelease(connections);
}
if (anyStrings)
return result;
else
CFRelease(result);
// Here, either the endpoint had no connections, or we failed to obtain names for any of them.
return CreateEndpointName(endpoint, false);
}
//////////////////////////////////////
// Obtain the name of an endpoint without regard for whether it has connections.
// The result should be released by the caller.
static CFStringRef CreateEndpointName(MIDIEndpointRef endpoint, bool isExternal)
{
CFMutableStringRef result = CFStringCreateMutable(NULL, 0);
CFStringRef str;
// begin with the endpoint's name
str = NULL;
MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &str);
if (str != NULL) {
CFStringAppend(result, str);
CFRelease(str);
}
MIDIEntityRef entity = NULL;
MIDIEndpointGetEntity(endpoint, &entity);
if (entity == NULL)
// probably virtual
return result;
if (CFStringGetLength(result) == 0) {
// endpoint name has zero length -- try the entity
str = NULL;
MIDIObjectGetStringProperty(entity, kMIDIPropertyName, &str);
if (str != NULL) {
CFStringAppend(result, str);
CFRelease(str);
}
}
// now consider the device's name
MIDIDeviceRef device = NULL;
MIDIEntityGetDevice(entity, &device);
if (device == NULL) return result;
str = NULL;
MIDIObjectGetStringProperty(device, kMIDIPropertyName, &str);
if (str != NULL) {
// if an external device has only one entity, throw away
// the endpoint name and just use the device name
if (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2) {
CFRelease(result);
return str;
} else {
// does the entity name already start with the device name?
// (some drivers do this though they shouldn't)
// if so, do not prepend
if (CFStringCompareWithOptions(str /* device name */,
result /* endpoint name */,
CFRangeMake(0, CFStringGetLength(str)), 0) != kCFCompareEqualTo) {
// prepend the device name to the entity name
if (CFStringGetLength(result) > 0)
CFStringInsert(result, 0, CFSTR(" "));
CFStringInsert(result, 0, str);
}
CFRelease(str);
}
}
return result;
}
// See: http://mattg411.com/coremidi-swift-programming/
//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// Sources & Destinations
//
// Updated for Swift 3 / XCode 8
//
import Cocoa
import CoreMIDI
import PlaygroundSupport
func getDisplayName(_ obj: MIDIObjectRef) -> String
{
var param: Unmanaged<CFString>?
var name: String = "Error"
let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
if err == OSStatus(noErr)
{
name = param!.takeRetainedValue() as String
}
return name
}
func getDestinationNames() -> [String]
{
var names:[String] = [];
let count: Int = MIDIGetNumberOfDestinations();
for i in 0..<count {
let endpoint:MIDIEndpointRef = MIDIGetDestination(i);
if (endpoint != 0)
{
names.append(getDisplayName(endpoint));
}
}
return names;
}
func getSourceNames() -> [String]
{
var names:[String] = [];
let count: Int = MIDIGetNumberOfSources();
for i in 0..<count {
let endpoint:MIDIEndpointRef = MIDIGetSource(i);
if (endpoint != 0)
{
names.append(getDisplayName(endpoint));
}
}
return names;
}
let destNames = getDestinationNames();
print("Number of MIDI Destinations: \(destNames.count)");
for destName in destNames
{
print(" Destination: \(destName)");
}
let sourceNames = getSourceNames();
print("\nNumber of MIDI Sources: \(sourceNames.count)");
for sourceName in sourceNames
{
print(" Source: \(sourceName)");
}
// See: https://github.com/krevis/MIDIApps/blob/master/Frameworks/SnoizeMIDI/SMMessageParser.m
/*
Copyright (c) 2001-2008, Kurt Revis. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Kurt Revis, nor Snoize, nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "SMMessageParser.h"
#import "SMMessage.h"
#import "SMVoiceMessage.h"
#import "SMSystemCommonMessage.h"
#import "SMSystemRealTimeMessage.h"
#import "SMSystemExclusiveMessage.h"
#import "SMInvalidMessage.h"
@interface SMMessageParser (Private)
- (NSArray *)messagesForPacket:(const MIDIPacket *)packet;
- (SMSystemExclusiveMessage *)finishSysExMessageWithValidEnd:(BOOL)isEndValid;
- (void)sysExTimedOut;
@end
@implementation SMMessageParser
- (id)init;
{
if (!(self = [super init]))
return nil;
sysExTimeOut = 1.0; // seconds
ignoreInvalidData = NO;
return self;
}
- (void)dealloc;
{
[readingSysExData release];
readingSysExData = nil;
[sysExTimeOutTimer invalidate];
[sysExTimeOutTimer release];
sysExTimeOutTimer = nil;
[super dealloc];
}
- (id)delegate;
{
return nonretainedDelegate;
}
- (void)setDelegate:(id)value;
{
nonretainedDelegate = value;
}
- (SMEndpoint *)originatingEndpoint;
{
return nonretainedOriginatingEndpoint;
}
- (void)setOriginatingEndpoint:(SMEndpoint *)value;
{
nonretainedOriginatingEndpoint = value;
}
- (NSTimeInterval)sysExTimeOut;
{
return sysExTimeOut;
}
- (void)setSysExTimeOut:(NSTimeInterval)value;
{
sysExTimeOut = value;
}
- (BOOL)ignoresInvalidData
{
return ignoreInvalidData;
}
- (void)setIgnoresInvalidData:(BOOL)value
{
ignoreInvalidData = value;
}
- (void)takePacketList:(const MIDIPacketList *)packetList;
{
NSMutableArray *messages = nil;
UInt32 packetCount;
const MIDIPacket *packet;
packetCount = packetList->numPackets;
packet = packetList->packet;
while (packetCount--) {
NSArray *messagesForPacket;
messagesForPacket = [self messagesForPacket:packet];
if (messagesForPacket) {
if (!messages)
messages = [NSMutableArray arrayWithArray:messagesForPacket];
else
[messages addObjectsFromArray:messagesForPacket];
}
packet = MIDIPacketNext(packet);
}
if (messages)
[nonretainedDelegate parser:self didReadMessages:messages];
if (readingSysExData) {
if (!sysExTimeOutTimer) {
// Create a timer which will fire after we have received no sysex data for a while.
// This takes care of interruption in the data (devices being turned off or unplugged) as well as
// ill-behaved devices which don't terminate their sysex messages with 0xF7.
NSRunLoop *runLoop;
NSString *mode;
runLoop = [NSRunLoop currentRunLoop];
mode = [runLoop currentMode];
if (mode) {
sysExTimeOutTimer = [[NSTimer timerWithTimeInterval:sysExTimeOut target:self selector:@selector(sysExTimedOut) userInfo:nil repeats:NO] retain];
[runLoop addTimer:sysExTimeOutTimer forMode:mode];
} else {
#if DEBUG
NSLog(@"SMMessageParser trying to add timer but the run loop has no mode--giving up");
#endif
}
} else {
// We already have a timer, so just bump its fire date forward.
[sysExTimeOutTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:sysExTimeOut]];
}
} else {
// Not reading sysex, so if we have a timeout pending, forget about it
if (sysExTimeOutTimer) {
[sysExTimeOutTimer invalidate];
[sysExTimeOutTimer release];
sysExTimeOutTimer = nil;
}
}
}
- (BOOL)cancelReceivingSysExMessage;
{
BOOL cancelled = NO;
if (readingSysExData) {
[readingSysExData release];
readingSysExData = nil;
cancelled = YES;
}
return cancelled;
}
@end
@implementation SMMessageParser (Private)
- (NSArray *)messagesForPacket:(const MIDIPacket *)packet;
{
// Split this packet into separate MIDI messages
NSMutableArray *messages = nil;
const Byte *data;
UInt16 length;
Byte byte;
Byte pendingMessageStatus;
Byte pendingData[2];
UInt16 pendingDataIndex, pendingDataLength;
NSMutableData* readingInvalidData = nil;
pendingMessageStatus = 0;
pendingDataIndex = pendingDataLength = 0;
data = packet->data;
length = packet->length;
while (length--) {
SMMessage *message = nil;
BOOL byteIsInvalid = NO;
byte = *data++;
if (byte >= 0xF8) {
// Real Time message
switch (byte) {
case SMSystemRealTimeMessageTypeClock:
case SMSystemRealTimeMessageTypeStart:
case SMSystemRealTimeMessageTypeContinue:
case SMSystemRealTimeMessageTypeStop:
case SMSystemRealTimeMessageTypeActiveSense:
case SMSystemRealTimeMessageTypeReset:
message = [SMSystemRealTimeMessage systemRealTimeMessageWithTimeStamp:packet->timeStamp type:byte];
break;
default:
// Byte is invalid
byteIsInvalid = YES;
break;
}
} else {
if (byte < 0x80) {
if (readingSysExData) {
NSUInteger length;
[readingSysExData appendBytes:&byte length:1];
length = 1 + [readingSysExData length];
// Tell the delegate we're still reading, every 256 bytes
if (length % 256 == 0)
[nonretainedDelegate parser:self isReadingSysExWithLength:length];
} else if (pendingDataIndex < pendingDataLength) {
pendingData[pendingDataIndex] = byte;
pendingDataIndex++;
if (pendingDataIndex == pendingDataLength) {
// This message is now done--send it
if (pendingMessageStatus >= 0xF0)
message = [SMSystemCommonMessage systemCommonMessageWithTimeStamp:packet->timeStamp type:pendingMessageStatus data:pendingData length:pendingDataLength];
else
message = [SMVoiceMessage voiceMessageWithTimeStamp:packet->timeStamp statusByte:pendingMessageStatus data:pendingData length:pendingDataLength];
pendingDataLength = 0;
}
} else {
// Skip this byte -- it is invalid
byteIsInvalid = YES;
}
} else {
if (readingSysExData)
message = [self finishSysExMessageWithValidEnd:(byte == 0xF7)];
pendingMessageStatus = byte;
pendingDataLength = 0;
pendingDataIndex = 0;
switch (byte & 0xF0) {
case 0x80: // Note off
case 0x90: // Note on
case 0xA0: // Aftertouch
case 0xB0: // Controller
case 0xE0: // Pitch wheel
pendingDataLength = 2;
break;
case 0xC0: // Program change
case 0xD0: // Channel pressure
pendingDataLength = 1;
break;
case 0xF0: {
// System common message
switch (byte) {
case 0xF0:
// System exclusive
readingSysExData = [[NSMutableData alloc] init]; // This is atomic, so there's no need to lock
startSysExTimeStamp = packet->timeStamp;
[nonretainedDelegate parser:self isReadingSysExWithLength:1];
break;
case 0xF7:
// System exclusive ends--already handled above.
// But if this is showing up outside of sysex, it's invalid.
if (!message)
byteIsInvalid = YES;
break;
case SMSystemCommonMessageTypeTimeCodeQuarterFrame:
case SMSystemCommonMessageTypeSongSelect:
pendingDataLength = 1;
break;
case SMSystemCommonMessageTypeSongPositionPointer:
pendingDataLength = 2;
break;
case SMSystemCommonMessageTypeTuneRequest:
message = [SMSystemCommonMessage systemCommonMessageWithTimeStamp:packet->timeStamp type:byte data:NULL length:0];
break;
default:
// Invalid message
byteIsInvalid = YES;
break;
}
break;
}
default:
// This can't happen, but handle it anyway
byteIsInvalid = YES;
break;
}
}
}
if (!ignoreInvalidData) {
if (byteIsInvalid) {
if (!readingInvalidData)
readingInvalidData = [NSMutableData data];
[readingInvalidData appendBytes:&byte length:1];
}
if (readingInvalidData && (!byteIsInvalid || length == 0)) {
// We hit the end of a stretch of invalid data.
message = [SMInvalidMessage invalidMessageWithTimeStamp:packet->timeStamp data:readingInvalidData];
readingInvalidData = nil;
}
}
if (message) {
[message setOriginatingEndpoint:nonretainedOriginatingEndpoint];
if (!messages)
messages = [NSMutableArray arrayWithObject:message];
else
[messages addObject:message];
}
}
return messages;
}
- (SMSystemExclusiveMessage *)finishSysExMessageWithValidEnd:(BOOL)isEndValid;
{
SMSystemExclusiveMessage *message = nil;
// NOTE: If we want, we could refuse sysex messages that don't end in 0xF7.
// The MIDI spec says that messages should end with this byte, but apparently that is not always the case in practice.
if (readingSysExData) {
message = [SMSystemExclusiveMessage systemExclusiveMessageWithTimeStamp:startSysExTimeStamp data:readingSysExData];
[readingSysExData release];
readingSysExData = nil;
}
if (message) {
[message setOriginatingEndpoint:nonretainedOriginatingEndpoint];
[message setWasReceivedWithEOX:isEndValid];
[nonretainedDelegate parser:self finishedReadingSysExMessage:message];
}
return message;
}
- (void)sysExTimedOut;
{
SMSystemExclusiveMessage *message;
[sysExTimeOutTimer release];
sysExTimeOutTimer = nil;
message = [self finishSysExMessageWithValidEnd:NO];
if (message)
[nonretainedDelegate parser:self didReadMessages:[NSArray arrayWithObject:message]];
}
@end
// See: http://mattg411.com/coremidi-swift-programming/
//
//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// Sources & Destinations
//
// Updated for Swift 3 / XCode 8
// Added code to display destinations (11/25/2016)
//
import Cocoa
import CoreMIDI
import PlaygroundSupport
func getDisplayName(_ obj: MIDIObjectRef) -> String
{
var param: Unmanaged?
var name: String = "Error";
let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
if err == OSStatus(noErr)
{
name = param!.takeRetainedValue() as String
}
return name;
}
func getDestinationNames() -> [String]
{
var names:[String] = [String]();
let count: Int = MIDIGetNumberOfDestinations();
for i in 0 ..< count
{
let endpoint:MIDIEndpointRef = MIDIGetDestination(i);
if (endpoint != 0)
{
names.append(getDisplayName(endpoint));
}
}
return names;
}
var midiClient: MIDIClientRef = 0;
var outPort:MIDIPortRef = 0;
MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient);
MIDIOutputPortCreate(midiClient, "MidiTest_OutPort" as CFString, &outPort);
var packet1:MIDIPacket = MIDIPacket();
packet1.timeStamp = 0;
packet1.length = 3;
packet1.data.0 = 0x90 + 0; // Note On event channel 1
packet1.data.1 = 0x3C; // Note C3
packet1.data.2 = 100; // Velocity
var packetList:MIDIPacketList = MIDIPacketList(numPackets: 1, packet: packet1);
let destinationNames = getDestinationNames()
for (index,destName) in destinationNames.enumerated()
{
print("Destination #\(index): \(destName)")
}
let destNum = 0
print("Using destination #\(destNum)")
var dest:MIDIEndpointRef = MIDIGetDestination(destNum);
print("Playing note for 1 second on channel 1")
MIDISend(outPort, dest, &packetList);
packet1.data.0 = 0x80 + 0; // Note Off event channel 1
packet1.data.2 = 0; // Velocity
sleep(1);
packetList = MIDIPacketList(numPackets: 1, packet: packet1);
MIDISend(outPort, dest, &packetList);
print("Note off sent")
// See: http://mattg411.com/coremidi-swift-programming/
//
// Swift MIDI Playground : Matt Grippaldi 10/17/2016
//
// MIDI Callbacks
//
// Updated for Swift 3 / XCode 8
//
import Cocoa
import CoreMIDI
import PlaygroundSupport
func getDisplayName(_ obj: MIDIObjectRef) -> String
{
var param: Unmanaged<CFString>?
var name: String = "Error"
let err: OSStatus = MIDIObjectGetStringProperty(obj, kMIDIPropertyDisplayName, &param)
if err == OSStatus(noErr)
{
name = param!.takeRetainedValue() as String
}
return name
}
func MyMIDIReadProc(pktList: UnsafePointer<MIDIPacketList>,
readProcRefCon: UnsafeMutableRawPointer?, srcConnRefCon: UnsafeMutableRawPointer?) -> Void
{
let packetList:MIDIPacketList = pktList.pointee
let srcRef:MIDIEndpointRef = srcConnRefCon!.load(as: MIDIEndpointRef.self)
print("MIDI Received From Source: \(getDisplayName(srcRef))")
var packet:MIDIPacket = packetList.packet
for _ in 1...packetList.numPackets
{
let bytes = Mirror(reflecting: packet.data).children
var dumpStr = ""
// bytes mirror contains all the zero values in the ridiulous packet data tuple
// so use the packet length to iterate.
var i = packet.length
for (_, attr) in bytes.enumerated()
{
dumpStr += String(format:"$%02X ", attr.value as! UInt8)
i -= 1
if (i <= 0)
{
break
}
}
print(dumpStr)
packet = MIDIPacketNext(&packet).pointee
}
}
var midiClient: MIDIClientRef = 0
var inPort:MIDIPortRef = 0
var src:MIDIEndpointRef = MIDIGetSource(0)
MIDIClientCreate("MidiTestClient" as CFString, nil, nil, &midiClient)
MIDIInputPortCreate(midiClient, "MidiTest_InPort" as CFString, MyMIDIReadProc, nil, &inPort)
MIDIPortConnectSource(inPort, src, &src)
// Keep playground running
PlaygroundPage.current.needsIndefiniteExecution = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment