views:

82

answers:

3

I want to pack a MIDI message into an NSData object.

int messageType = 3; // 0-15
int channel = 5;      // 0-15
int data1 = 56;       // 0-127
int data2 = 78;       // 0-127

int packed = data2;
packed += data1 * 127;
packed += channel * 16129; // 127^2
packed += messageType * 258064; // 127^2 * 16

NSLog(@"packed %d", packed);

NSData *packedData = [NSData dataWithBytes:&packed length:sizeof(packed)];

int recovered;
[packedData getBytes:&recovered];

NSLog(@"recovered %d", recovered);

This works wonderfully and while I'm proud of myself, I know that the conversion to bytes is not done correctly: it should be a direct conversion without a lot of addition and multiplication. How can that be done?

Edit: I'm now aware that I can just do this

char theBytes[] = {messageType, channel, data1, data2};
NSData *packedData = [NSData dataWithBytes:&theBytes length:sizeof(theBytes)];

and on the Java side

byte[] byteBuffer = new byte[4]; // Receive buffer
while (in.read(byteBuffer) != -1) {  
    System.out.println("data2="  + byteBuffer[3]);
}

and it will work, but I'd like the solution to get me an NSData with just 3 bytes.

A: 

Personally, I would go for an NSString:

NSString *dataString = [NSString stringWithFormat:@"%i+%i+%i+%i", messageType, channel, data1, data2];
NSData *packedData = [dataString dataUsingEncoding:NSUTF8StringEncoding];

Easy to use, and easy to transfer. Unpacking is a tiny bit more complicated, but not difficult at all either.

NSScanner *scanner = [NSScanner scannerWithString:[[[NSString alloc] initWithData:packedData encoding:NSUTF8StringEncoding] autorelease]];
int messageType, channel, data1, data2;
[scanner scanInt:&messageType];
[scanner scanInt:&channel];
[scanner scanInt:&data1];
[scanner scanInt:&data2];
itaiferber
@itaiferber, this looks fine (didn't know about the NSScanner, that's cute), but just in this tiny piece of code I would LOVE to avoid extra object instantiations since I might be processing tons of messages at once on the iOS. Also, and very important: the data will be sent to another computer, so the string implementation was ruled out.
Yar
@Yar, I see what you're saying about the `NSScanner` instantiation, though I don't know how performance intensive it is. About `NSString`'s, though: they're really portable. You should have no problem sending them through the network. Even better: packed as `NSData`, you should be good to go...
itaiferber
@Yar I'd say get it working (ie, get any sort of communication working), then analyze your code and optimize it if the analysis shows that you have specific bottlenecks. The question seems like it's a case of premature optimization. (though perhaps you've already found this to be a bottleneck, I don't know)
Dave DeLong
Thanks @Dave DeLong. In my opinion, you should skip premature optimizing where it's a lot of work. In this case, it's very easy, I just don't happen to have the skillz to do it. Hence I'm asking on SO. If I cannot get an answer here, of course I'll go with whatever cheap hack I can think of (plus-sign separated strings didn't make the list, actually).
Yar
Huh. Why the -2 votes?
itaiferber
A: 

you have several options.

since it looks like you want a contiguous glob of data in the NSData representation...

you'll want to create a packed struct, and pass the data to the NSData call as a predefined endianness (so both ends know how to unarchive the data glob).

/* pack this struct's ivars and and enable -Wreorder to sanity check that the compiler does not reorder members -- i see no reason for the compiler to do this since the fields are equal size/type */
struct t_midi_message {
    UInt8 message_type; /* 0-15 */
    UInt8 channel; /* 0-15 */
    UInt8 data1; /* 0-127 */
    UInt8 data2; /* 0-127 */
};

union t_midi_message_archive {
/* members - as a union for easy endian swapping */
    SInt32 glob;
    t_midi_message message;
    enum { ValidateSize = 1 / (4 == sizeof(t_midi_message)) };
/* nothing unusual here, although you may want a ctor which takes NSData as an argument */
    t_midi_message_archive();
    t_midi_message_archive(const t_midi_message&);
    t_midi_message_archive(const t_midi_message_archive&);
    t_midi_message_archive& operator=(const t_midi_message_archive&);

/* swap routines -- just pass @member glob to the system's endian routines */
    void swapToNativeEndianFromTransferEndian();
    void swapToTransferEndianFromNativeEndian();

};

void a(const t_midi_message_archive& msg) {

    t_midi_message_archive copy(msg);
    copy.swapToTransferEndianFromNativeEndian();

    NSData * packedData([NSData dataWithBytes:&copy.glob length:sizeof(copy.glob)]);
    assert(packedData);

    t_midi_message_archive recovered;
    [packedData getBytes:&recovered.glob];

    recovered.swapToNativeEndianFromTransferEndian();
    /* recovered may now be used safely */
}
Justin
Sorry, maybe I wasn't clear: I don't need a big glob of data in the NSData. I'd like to get my 3 bytes worth of info into an NSData any way possible. I'm not worried about the messages being super-small inside the iOS, but I'd like the bytes underlying the NSData to be 3.
Yar
i count 4 - compilation will bail if: enum { ValidateSize = 1 / (4 == sizeof(t_midi_message)) }; is false. EDIT: ok you could reduce it to 3 - which seems like an unnecessary micro-optimization.
Justin
if you really want 3 instead of 4, you'd just use bitfields in t_midi_message.
Justin
Thanks @Justin, see my solution below. I will use a struct now that you've mentioned it, though. Thanks.
Yar
There is no need to worry about endian transformations if the integers are one byte or less each. We control the byte order on the message, of course.
Yar
A: 

Here's a 3-byte solution that I put together.

char theBytes[] = {message_type  * 16 + channel, data1, data2};
NSData *packedData = [NSData dataWithBytes:&theBytes length:sizeof(theBytes)];

char theBytesRecovered[3];
[packedData getBytes:theBytesRecovered];

int messageTypeAgain = (int)theBytesRecovered[0]/16;
int channelAgain = (int)theBytesRecovered[0] % 16;
int data1Again = (int)theBytesRecovered[1];
int data2Again = (int)theBytesRecovered[2];

NSLog(@"packed %d %d %d %d", messageTypeAgain, channelAgain, data1Again, data2Again);

and on the other side of the wire, this is just as easy to pick up, because each byte is a byte. I just finished trying this on the iOS side and the Java side, and there are no problems on either. There is no problem with endian-ness, because each integer fits into one single byte (or two in one byte, in one case).

Yar
there *is* a problem with endianness in your example. passing through const void* when length > 1 requires endian consideration. the char array values will be reordered when devices with different endianness meet.
Justin
Thanks @Justin. I don't agree, but I'll ask yet another question here on SO shortly to clear up this discrepancy. I'll have to ask it at a good time-of-day so we get some expert opinions.
Yar
Thanks @Justin, I've asked the question here http://stackoverflow.com/questions/3710669/ints-to-bytes-endianess-a-concern
Yar