4

Core MIDI: MIDIPacket, MIDIPacketList, and Builders

 3 years ago
source link: http://www.rockhoppertech.com/blog/core-midi-midipacket-midipacketlist-and-builders/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Core MIDI: MIDIPacket, MIDIPacketList, and Builders

By Gene De Lisa

|

Published: October 20, 2020

The “traditional” structures for sending and receiving MIDI data in Core MIDI are MIDIPacket and MIDIPacketList.

Here is the original MIDIPacketList – which is now deprecated – but still works well. You use it like this (sort of) to send.

var packetList = MIDIPacketList()
// ... fill 'er up
// send it out.
let osstatus = MIDISend(outputPort, destinationEndpoint, &packetList)

To receive MIDI data, you are handed one in an inputPort’s MIDIReadProc

let osstatus = MIDIInputPortCreateWithBlock(client, inputPortName, &inputPort, MyMIDIReadProc)
func MyMIDIReadProc(pktList: UnsafePointer<MIDIPacketList>,
                    srcConnRefCon: UnsafeMutableRawPointer?) {
// as a block (i.e. closure) in one swell foop:
let osstatus = MIDIInputPortCreateWithBlock(client, inputPortName, &inputPort) {
   (pktList: UnsafePointer<MIDIPacketList>,
    srcConnRefCon: UnsafeMutableRawPointer?) in

N.B. Besides MIDIPacketList there is the new MIDIEventPacket and its pals which are ready for MIDI 2. That’s another blog post!

MIDIPacket and MIDIPacketList

So, how do you fill the packet list. And what’s a packet?

Core MIDI defines a packet as the following struct. The timeStamp is in “host” time which is in nanoseconds. (q.v.) The actual MIDI data is in the tuple of 256 unsigned bytes. The tuple is always the same size, but you specify the number of bytes MIDISend and pals will pay attention to. If it’s a Note On message for example, you’d set the length field to 3, and then set the first 3 bytes of the tuple to the data you want.

public struct MIDIPacket {
    public var timeStamp: MIDITimeStamp
    public var length: UInt16
    public var data: (UInt8, UInt8 and 256 in total)
    init()
    init(public init(timeStamp: MIDITimeStamp, length: UInt16, data: (UInt8...) )

The easiest way to make a packet is to use the init that takes no parameters. You really don’t want to make that tuple and then pass it in, do you? Ok, I see that one guy over there, but no, I don’t.

// Make a packet
var packet = MIDIPacket()
packet.timeStamp = timestamp
packet.length = 3
packet.data.0 = midiStatus
packet.data.1 = data1
packet.data.2 = data2

Of course midiStatus, data1, and data2 are UInt8 values for a MIDI message that you probably passed in.

Now make a MIDIPacketList with your packet, then send it. See my post on setting up the MIDI Client, ports, and endpoints if that’s new to you.

// Make a list containing just this packet.
var list = MIDIPacketList(numPackets: 1, packet: packet)
// Bombs away!
let osstatus = MIDISend(outputPort, endpoint, &list)
// check OSStatus etc.

Yay, a stuck note if that message was a note on! Would you like to send a note off too? Of course. How do you add that using this init for the list? It takes one packet. You want two packets.

Sending more than one packet.

The MIDI data that MIDISend sends are simply the messages separated by variable-length values for the timestamps in between.

This time I’m going to make an empty packetList. This needs to be followed by a special init function. Why? Because this is a C API and they need to do “stuff”. What stuff? I’ve not seen the source code, and this is not open source, so who knows? Just call it.

var packetList = MIDIPacketList()
var packet = MIDIPacketListInit(&packetList)

The init func gives you the first empty packet. So we can safely assume that one item in the “stuff” is making a packet, frobbing around a bit, then giving it to you. Now what?

Let’s say your sending func passes in channel, pitch, and velocity values. Make local variables like this for note messages.

let midiStatus: UInt8 = (0x90 | channel)
let noteOnMidiData: [UInt8] = [midiStatus, pitch, velocity]
let noteOffMidiData: [UInt8] = [midiStatus, pitch, 0]

Then you use MIDIPacketListAdd to fill that empty packet with your data like this. Each call will return the packet that you can use in another call.

packet = MIDIPacketListAdd(&packetList, 1024, packet, noteOnTimestamp, noteOnMidiData.count, noteOnMidiData)
packet = MIDIPacketListAdd(&packetList, 1024, packet, noteOffTimestamp, noteOffMidiData.count, noteOffMidiData)

Yeah, a bit weird. But not as weird as Objective-C. The packetList was simply cast from a byte buffer. That would explain those weird params to the add func.

Byte packetBuffer[1024];
MIDIPacketList *packetList = (MIDIPacketList*)packetBuffer;
MIDIPacket *packet = MIDIPacketListInit(packetList);
Byte noteOn[] = [0x90, 60, 64];
packet = MIDIPacketListAdd(packetList, sizeof(packetBuffer), packet, timestamp, 3, noteOn);

If you’d like to see not just weird code, but psychotic, take a look at the Swift examples on Snack Overblown.

Packet Timestamp

If you assign 0 (zero) to the packet’s times stamp field, that means “send it right away”. But what if you want to send one packet, then another a while later as is necessary for the Note on/Note off messages.

My opinion is that in real life, you would either use a Sequencer such as the Audio Toolbox MusicPlayer – or write one yourself – or use MIDISend in response to UI events. Button press = note on, Button release = note off for example. But if you want those gestures to kick off multiple packets, you need to grok this timing thing.

The time stamps are nanoseconds in “host time”. In iOS, you would use mach_absolute_time() to get the current host time. In macOS, there are a few more hoops to jump through.

Here’s a first cut at timing.

let durationInNS = MIDITimeStamp(durationSeconds * 1_000_000_000)
var noteOffTimestamp: MIDITimeStamp = .zero
#if os(iOS)
noteOnTimestamp = MIDITimeStamp(mach_absolute_time())
#endif
noteOffTimestamp = noteOnTimestamp + durationInNS

Logical. But what I hear – and my MIDI monitor confirms – is a very short note. What’s gives?

I set the note on time to the host time “right now”. But that “right now” is in the past when the packet is sent!

So, add a delay to the note on time! How much? Not too much, otherwise the user will press a button, and then be waiting for the note on! Not too little, or you’ll get the short note thing. This works on my macBook:

noteOnTimestamp = MIDITimeStamp(mach_absolute_time() + 15_000_000)

Most likely, the best way to set the timestamps would be to iterate through the packet list to set each packet’s timestamp immediately before calling MIDISend to decrease the “in-between” time. More on iterating a packet list below.

I dislike hacks like this. If you know a better way, let me know!

Multiple Messages in one MIDIPacket

Why would you want to put multiple MIDI messages inside one MIDIPacket? The clue that this is possible is that huge tuple for 1…3 byte messages (besides Sysex messages). The other clue is that each message in the packet have the same time stamp. Yeah, a chord!

For a C Major chord (middle c root, mezzo forte), we need to make the packet’s data tuple look like this:

0x90, 0x3C, 0x40, 0x90, 0x40, 0x40, 0x90, 0x43, 0x40

Just the bytes for the midi messages! And of course, you’ll need a note off version of this packet.

So, how do you get all those bytes in there apart from filling a 256 byte tuple like the following?

let d = (UInt8(0x90), UInt8(0x3C), UInt8(0x40), UInt8(0x90), UInt8(0x40), UInt8(0x40), UInt8(0x90), UInt8(0x43), UInt8(0x40), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0))
let chpacket = MIDIPacket(timeStamp: 0, length: 12, data: d)

Yikes!

Builders

There are hacky pointer frobs I’ve used to fill in tuples. Apple might have taken my Radars to heart (unlikely), because they have introduced Builders in several Core MIDI structures in iOS 14. The Builders are embedded in their structures.

Here’s how to use the builder for MIDIPacket.

One unknown (to me) is that they specify the time stamp as an Int and not MIDITimeStamp (UInt64) which is used everyplace else.

// Some data for a Cmaj7 chord on channel 1
let midiStatus: UInt8 = 0x90
let velocity: UInt8 = 64
let root: UInt8 = 60
let third: UInt8 = 64
let fifth: UInt8 = 67
let seventh: UInt8 = 71
let noteOnTimestamp: MIDITimeStamp = .zero
// The data tuple has 256 bytes. This will zero out the tuple.      
// try setting this to 12 and inspect the tuple in the debugger.
let pb = MIDIPacket.Builder(maximumNumberMIDIBytes: 256)
// pb.timeStamp is: public var timeStamp: Int
// why?
pb.timeStamp = Int(noteOnTimestamp)
pb.append(midiStatus)
pb.append(root)
pb.append(velocity)
pb.append(midiStatus)
pb.append(third)
pb.append(velocity)
pb.append(midiStatus)
pb.append(fifth)
pb.append(velocity)
pb.append(midiStatus)
pb.append(seventh)
pb.append(velocity)

Groovy. You have your data in a builder now.

Rumination: Why doesn’t append use a fluent interface? It’s not hard. SwiftUI uses it all over the place.

How do you get the MIDIPacket out of it? builder.getPacket() ? Nah. Too easy.

let result = pb.withUnsafePointer {
   (p: UnsafePointer<MIDIPacket>) -> Result<MIDIPacket, Error> in
   let packet = p.pointee
   var r: Result<MIDIPacket, Error> = .success(packet)
   // one possible check
   if packet.length < 3 {
      r = .failure(MIDIPacketError.badPacket(msg: "bad count. \(packet.length)"))
   logger.debug("packet.length: \(packet.length)")
   return r

So, you need to use withUnsafePointer to extract it. The builder that you’e using to avoid unsafe pointers makes you use them anyway. Convenient.

The func signature specifies a Result as the return type. You can include an Error if any validation you wish to perform fails.

MIDIPacketList.Builder

MIDIPacketList.Builder works in much the same way as the packet builder.

I have no idea what byteSize means. There is no documentation. (Surprise!). The name could mean “number of bits in a byte” or “how many bytes in the packet’s data field”. I take it to mean the latter. Remember the Objective-C buffer casting thing? If I’m wrong (not a new thing!), let me know. This works, at least.

let plb = MIDIPacketList.Builder(byteSize: 1024)
// data as before
let noteOnMidiData: [UInt8] = [midiStatus, pitch, velocity]
let noteOffMidiData: [UInt8] = [midiStatus, pitch, 0]
plb.append(timestamp: noteOnTimestamp, data: noteOnMidiData)
plb.append(timestamp: noteOffTimestamp, data: noteOffMidiData)

Retrieving the result is done just like MIDIPacket.

let result = plb.withUnsafePointer {
   (p: UnsafePointer<MIDIPacketList>) -> Result<MIDIPacketList, Error> in
   let packetList = p.pointee
   var r: Result<MIDIPacketList, Error> = .success(packetList)
   if packetList.numPackets != 2 {
      r = .failure(MIDIPacketError.badPacket(msg: "No packets. \(packetList.numPackets)"))
   return r
// Now get it out of the result      
switch result {
case .success(let plist):
   return plist // Here it is! Returned by the enclosing func.
case .failure(let error):
   logger.error("\(error.localizedDescription)")
   // throw an error

Iterators

Yay, they addressed the need for updated iterators. Many people have “rolled their own” way to do this. AudioKit has them for example.

If you wish to iterate the data tuple in a MIDIPacket, you need to get an UnsafePointer to the packet. Then you can iterate using the sequence MIDIPacket.ByteSequence. The packet’s ByteSequence is obtained from the unsafe pointer via its sequence() function.

let result = pb.withUnsafePointer {
   (p: UnsafePointer<MIDIPacket>) -> Result<MIDIPacket, Error> in
   for dataByte in p.sequence() {
      logger.debug("data byte: \(dataByte, format: .hex)")

There is also the MIDIPacketList.UnsafeSequence to iterate over MIDIPacketList’s MIDIPackets.

let result = plb.withUnsafePointer {
   (p: UnsafePointer<MIDIPacketList>) -> Result<MIDIPacketList, Error> in
   for pa in p.unsafeSequence() {
      let packet = pa.pointee
//whammo

This MIDIReadProc shows the use of two of these iterations. Parsing the MIDI data is “left as an exercise for the reader.”

func MyMIDIReadProc(pktList: UnsafePointer<MIDIPacketList>,
                    srcConnRefCon: UnsafeMutableRawPointer?) {
    for p in pktList.unsafeSequence() {
        let packet = p.pointee
        logger.debug("packet time stamp \(packet.timeStamp)")
        logger.debug("packet status \(packet.data.0, format: .hex)")
        for d in p.sequence() {
            logger.debug("packet data byte \(d, format: .hex)")

Summary

MIDIPacket and its pal MIDIPacketList is old skool, but it has had useful updates recently.

MIDIEvent is the new frobbery, so that’s in my next blog post.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK