Initializing MIDIMetaEvent structure
Asked Answered
F

1

5

I am struggling to initialize the MIDIMetaEvent structure found in MusicPlayer.h with swift The header file defines the structure as follows:

struct MIDIMetaEvent {
  var metaEventType: UInt8
  var unused1: UInt8
  var unused2: UInt8
  var unused3: UInt8
  var dataLength: UInt32
  var data: (UInt8)
}

Which seems fairly straightforward up until that 'data' member. Is that a 1 element tuple definition? I can easily initialize all other struct elements but have tried in vain to set 'data' to anything else than a single value. In my code I used an UInt8 array called myData and attempted to init the structure like so:

var msg = MIDIMetaEvent(
  metaEventType : UInt8(0x7F),
  unused1       : UInt8(0),
  unused2       : UInt8(0),
  unused3       : UInt8(0),
  dataLength    : UInt32(myData.count),
  data          : UnsafeBufferPointer<UInt8>(start: UnsafePointer<UInt8>(myData), count:myData.count) )

But the compiler is not happy with this and complains about "UnsafeBufferPointer no convertible to UInt8". If I simply set data to a single value but set dataLength to a value more than 1, the resulting MIDIEventData shows that the first value in the event is what I stuck in 'data' followed by gibberish data bytes in accordance with 'dataLength' bytes. So clearly 'data' is seen as some sort of continuous memory.

So how do I set that 'data' element to UInt8 elements from an array?

Fusibility answered 31/12, 2014 at 17:0 Comment(0)
L
9

The AudioToolbox framework defines MIDIMetaEvent as

typedef struct MIDIMetaEvent
{
    UInt8       metaEventType;
    UInt8       unused1;
    UInt8       unused2;
    UInt8       unused3;
    UInt32      dataLength;
    UInt8       data[1];
} MIDIMetaEvent;

where data[1] is actually used as a "variable length array". In (Objective-)C one can just allocate a pointer to a memory block of the actually needed size:

MIDIMetaEvent *mep = malloc(sizeof(MIDIMetaEvent) + data.count);

Swift is more strict with pointer casts and fixed size arrays are mapped to Swift tuples (which can be cumbersome to handle with).

The following utility class shows how this could be solved:

class MyMetaEvent {
    private let size: Int
    private let mem : UnsafeMutablePointer<UInt8>

    let metaEventPtr : UnsafeMutablePointer<MIDIMetaEvent>

    init(type: UInt8, data: [UInt8]) {
        // Allocate memory of the required size:
        size = sizeof(MIDIMetaEvent) + data.count
        mem = UnsafeMutablePointer<UInt8>.alloc(size)
        // Convert pointer:
        metaEventPtr = UnsafeMutablePointer(mem)

        // Fill data:
        metaEventPtr.memory.metaEventType = type
        metaEventPtr.memory.dataLength = UInt32(data.count)
        memcpy(mem + 8, data, UInt(data.count))
    }

    deinit {
        // Release the allocated memory:
        mem.dealloc(size)
    }
}

Then you can create an instance with

let me = MyMetaEvent(type: 0x7F, data: myData)

and pass me.metaEventPtr to the Swift functions taking a UnsafePointer<MIDIMetaEvent> argument.


Update for Swift 3/4:

Simply converting a pointer to a different type is no longer possible, it must be "rebound":

class MyMetaEvent {
    private let size: Int
    private let mem: UnsafeMutablePointer<UInt8>

    init(type: UInt8, data: [UInt8]) {
        // Allocate memory of the required size:
        size = MemoryLayout<MIDIMetaEvent>.size + data.count
        mem = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
        mem.initialize(to: 0, count: size)

        // Fill data:
        mem.withMemoryRebound(to: MIDIMetaEvent.self, capacity: 1) { metaEventPtr in
            metaEventPtr.pointee.metaEventType = type
            metaEventPtr.pointee.dataLength = UInt32(data.count)
            memcpy(&metaEventPtr.pointee.data, data, data.count)
        }
    }

    deinit {
        // Release the allocated memory:
        mem.deallocate(capacity: size)
    }

    func withMIDIMetaEventPtr(body: (UnsafePointer<MIDIMetaEvent>) -> Void) {
        mem.withMemoryRebound(to: MIDIMetaEvent.self, capacity: 1) { metaEventPtr in
            body(metaEventPtr)
        }
    }
}

Create an instance with custom data:

let me = MyMetaEvent(type: 0x7F, data: ...)

Pass to a function taking a UnsafePointer<MIDIMetaEvent> argument:

me.withMIDIMetaEventPtr { metaEventPtr in
    let status = MusicTrackNewMetaEvent(track, 0, metaEventPtr)
}
Laveralavergne answered 31/12, 2014 at 21:29 Comment(23)
Ah, didn't realize I needed to go down that low. I haven't used memcpy in ages! Rather than using the UnsafeMutablePointer<> you demonstrate above, I simply resolved to memcpy( &msg.data, data, UInt(data.count)) which seems to work fine. Thanks! Seems practical work with Swift is still a rather messy business....Fusibility
@Yohst: You are right, the withUnsafeMutablePointer was unnecessary complicated. I have updated the answer and added an alternative to memcpy(). It would be easier if the Swift mapping of C arrays would be a Swift Array instead of a tuple.Laveralavergne
hmmm, I claimed victory too soon. The code above results in only the first byte of 'data' being copied to the output. Weird. When I investigate memory right after memcpy() with mem.advancedBy(x).memory with 'x' being 8, 9, 10 etc. I only see the first byte at position '8' but gibberish for the rest. What is memcpy() doing? Or is memory being stepped on by something else?Fusibility
@Yohst: Yes, for some reason the tuple-address-arithmetic did not work as I expected from my previous experiments with tuples in Swift. memcpy just copies the given number of bytes from a source address to a destination address. – I have updated the answer with code that seems to work. It is less elegant because it uses the constant 8 as offset to the data field. When I have more time then I will try to figure out a nicer solution.Laveralavergne
yes I arrived at that solution also, not nice but it works. Couldn't get a better solution with a computed pointer offset going either. I spent a lot of time studying how memcpy (seems to) work in a playground. Pretty messy stuff, Swift+Libs are not quite ready for prime time. Anyway, thanks for helping out.Fusibility
@Yohst: I have inspected the generated source code. It seems that in memcpy(&metaEventPtr.memory.data, data, UInt(data.count)) the (single byte) metaEventPtr.memory.data is copied to a new location, and then the address of that location taken and passed to memcpy. I have no idea why it is done this way.Laveralavergne
any update for swift 3 or 4? I would appreciate lot.Roomette
@Trevor: See update. That should work, but I can not test it with real music/midi data. Any feedback is welcome.Laveralavergne
@MartinR This look OK, It could compile and could pass the address sanitizer test. Thank you for your update. I'll analyse later other complications (if there is) 👍Roomette
Hi @MartinR, would it not be possible to use UnsafeMutableRawPointer(mem).bindMemory(to: MIDIMetaEvent.self, capacity: 1) to permanently change the type, rather than using .withMemoryRebound to do things on the fly, or would that still result in issues with properly deallocating it? I saw pointer type changes discussed here, and wondered if that might be viable: developer.apple.com/documentation/swift/unsafemutablepointer, swiftdoc.org/v3.1/type/unsafemutablepointerChrisse
Also, any thoughts on how to use this for setting Key signature? For signatures containing flat notes, that requires a negative number (csie.ntu.edu.tw/~r92092/ref/midi/#meta_event), which of course causes an error when placed into a UInt8 variable.Chrisse
@TheNeil: Yes, you could allocate a raw pointer and bind it to the desired type. But that is unrelated to proper (de)allocation, and is neither more nor less efficient. Binding memory (at present) is only a simple cast and actually does nothing at runtime. That may change in the future, compare forums.swift.org/t/…: “A code verification tool--like a hypothetcial pointer sanitizer--could call you out on this.””Laveralavergne
@TheNeil: I cannot really help with your other question because I haven't worked with MIDI and don't know how it interprets the data. But mem[offset] = UInt8(bitPattern: -3) may be what you are looking for.Laveralavergne
Hi @TheNeil, did you ever figure out how to change the setting of the key signature for flat note key signatures. The documentation is extremely sparse. It is a shame that Apple is still using this archaic API for this type of stuff. These APIs should have been ported to the AVFoundation framework :-(Mingrelian
I actually did manage the following, which creates the right pattern of data; however, adding it in a key meta event to the music sequence's tempo track messes up all the note data for no clear reason. Any example of this part working is more than welcome! public var midiEventData: [UInt8] { let accidentals = min(numberOfAccidentals, 7) let sharpsFlats = (notationStyle == .sharps) ? UInt8(accidentals) : UInt8(bitPattern: Int8(accidentals * -1)) let minorFlag = (type == .major) ? UInt8(0) : UInt8(1) return [sharpsFlats, minorFlag] }Chrisse
I think the issue caused by the key meta event actually manifests during the process by which I'm then converting it to MIDI data and passing to an AVAudioSequencer. If the sequence is saved to a .mid file, everything looks correct, including the key, but the sequencer plays the data all garbled.Chrisse
Worked it out. So long as the above is added as a meta event to a different track than the tempo track, it works, without garbling playback. I recommend adding it to a final, empty track, dedicated to metadata.Chrisse
Thanks @TheNeil, will give it a try.Mingrelian
No problem. Sorry for the awful formatting. Can’t really do much code markup in comments. Martin’s suggestion of using bitPattern is the key though. The negatives will look like positives counting down from 256 if you print them to console, but they work!Chrisse
I finally had time to implement this and it worked great! Thanks, so much.Mingrelian
@MartinR This seems to work (thanks!), however the documentation on withMemoryRebound states: Note Only use this method to rebind the pointer’s memory to a type with the same size and stride as the currently bound Pointee type. To bind a region of memory to a type that is a different size, convert the pointer to a raw pointer and use the bindMemory(to:capacity:) method. The size and stride of MIDIMetaEvent and UInt8 do not match. Wouldn't this present an issue as implemented above?Cavein
@nastynate13: That is a good question. I am fairly sure that there is no practical difference between an UInt8 pointer and a raw pointer, but I have no reference for that right now. Also all that binding (at present) does not do anything at runtime (except pointer casting) so that nothing bad can happen actually. But I'll think about it and let you know if I come up with a better answer.Laveralavergne
Thanks Martin- Agreed, perhaps the note in the Apple doc is warning that rebinding types whose size and stride "misalign" would either fail or give unexpected results. I can't imagine that rebinding from a UInt8 (size 1, stride 1) to any other type would present an issue, but maybe someone on the Swift forums could confirm. I'll let you know what I find out. For now, I'm taking the more verbose path of allocating an UnsafeMutableRawPointer, storing bytes in it, then binding to a MIDIMetaEvent.Cavein

© 2022 - 2024 — McMap. All rights reserved.