Arrays & Open Sound Control (malformed messages)

I am trying to get an OSC message from a p5js sketch, being an array of arrays. As far as I understand it, the OSC spec allows for sending several arrays in a message.
I am sending this:

[ [0, 5] , [56, 78] ]

And expecting this:

[/getVectors, [ [ 0.0, 5.0 ] , [ 56.0 , 78.0 ] ] ]

But messages looks like this:

[ /getVectors, [, 0.0, 5.0, ], [, 56.0, 78.0, ] ]

Messages are being sent from Javascript via a WebSocket dispatcher:

osc.js:

const sendSuper = function (address, args) {
  port.send({
    address: address,
    args: args
  });
};

sendSuper("/getVectors", [[0, 5],[56, 78]]);

This might be an issue with osc.js, but I’ve also tried with
I’ve also tried Python with pythonosc.

def localSclangSend(server_ip, osc_addr, args):
   sclang_server = udp_client.SimpleUDPClient(server_ip, 57120);
   sclang_server.send_message(osc_addr, args)

  localSclangSend("127.0.0.1", "/getVectors",[[0, 5],[56, 78]])

Which looks like this: [ /getVectors, [, 0, 5, ], [, 56, 78, ] ]

The OSC spec says that an array type tag is simply prefixing with the [ delimiter. From the documentation for both osc.js nor pyosc, I think the messages are formatted correctly at the sender, so I am leaning towards this being a SuperCollider issue?

I am thinking about workarounds atm, such as sending a string and reading that as YAML / JSON using the .parseJSON. I’d rather avoid that though. Any leads?

EDIT:

Ok so looking again at the OSC spec, the array type tag is “non-standard”. The documentation states that:

“r” (RGBA color), “S” (symbol), and “[” and “]” (array start and end) are not supported.

So my question is then, does anyone have a workaround for this?

“r” (RGBA color), “S” (symbol), and “[” and “]” (array start and end) are not supported.

Bummer. You may open a feature request on GitHub.

So my question is then, does anyone have a workaround for this?

If the receiving end is aware of the format, you may simply flatten the arrays and prepend the element count. (This is what sclang does for sending multidimensional data, such as synth controls, to the Server.)

In your case:
<outer_count> <inner_count1> <inner_elements1...> <inner_count2> <inner_elements2...>
resp.
2, 2, 0, 5, 2, 56, 78

Note that you may omit the element count if it is always the same. For example, if the inner arrays always have two elements, you can do this instead: 2, 0, 5, 56, 78.

I think I am following you, seems workable. Not completely sure about on how to recompose it on the other hand …

The solution I am testing now is simply sending a JSON object as a string and parsing it in sclang with the built-in .parseJSON method, which looks like it is working so far!

Not completely sure about on how to recompose it on the other hand …

In your case:

(
~parseData = { |data|
	var stream = CollStream(data);
	stream.next.collect {
		stream.next.collect { stream.next }
	}
};

~parseData.([ 2, 2, 0, 5, 2, 56, 78 ]); // -> [ [ 0, 5 ], [ 56, 78 ] ]
)

Again, this only works if the receiver is aware of the structure (in this case, a two-dimensional array). For arbitrary nested data structures, JSON might indeed be a better fit.

One thing to be aware of here is that strings come into SC not as String objects, but as Symbols. This is primarily to make OSC path matching faster.

Symbols stay in sclang memory, forever. If you have a symbol \abc now, and later get another symbol \abc, the only way to recognize these as identical is to have a memory of the first \abc.

So if you’re sending a lot of strings, with many slight variations, it will cause the symbol table to grow, which might look from the outside like a memory leak.

When you’re sending the arrays from Python, the format you’re receiving is already unambiguous. Each array is consistently delimited. So there is no need to change the outgoing message to introduce counts.

(
var parseArray = { |stream|
	var out = Array.new;
	var item;
	// assumes the first [ has already been swallowed
	while {
		item = stream.next;
		item.notNil and: { item != $] }
	} {
		if(item == $[) {
			out = out.add(parseArray.(stream));
		} {
			out = out.add(item);
		}
	};
	// here, item is either nil (end) or $] (end of array)
	// -- in neither case does the caller need that value
	out
};

f = { |msg|
	parseArray.(CollStream(msg));
};
)

// here I'm using your data from the original post
f.([ '/getVectors', $[, 0, 5, $], $[, 56, 78, $] ]);
-> [ /getVectors, [ 0, 5 ], [ 56, 78 ] ]

… and the result is a symbol followed by two legit arrays – you get the correct result from the original message format (no change on the Python side), and without symbol table growth.

(I’m assuming that the brackets are coming in as Char – if not, that should be easy to change.)

hjh

@jamshark70 nice trick! Here’s the relevant passage from the docs, for anyone wondering why this works:

If an unrecognized tag is encountered, sclang will make that unrecognized tag into a Char object and add that to the OSC message.


Ouch. I can see how this is useful for OSC address patterns, but IMO it does not make any sense at all for OSC string arguments. I guess we cannot really change this without potentionally breaking existing projects, but maybe there could be a global option for parsing OSC string arguments as Strings, similar to NetAddr.useDoubles.

In the meantime, one can use OSC blobs as a workaround for sending larger text.

:grin: It isn’t a solution that leaps out of the documentation, but… for chucklib-livecode, I’ve written more parsing code in sclang than anyone probably should write. That parseArray function is structurally the same: while over an input stream, branching inside the loop to handle different syntactic cases. I’ve actually written this up in a paper, but publication date is uncertain.

BTW I didn’t test it with nested arrays, but as it’s a recursive approach, it should be fine.

static PyrObject* ConvertOSCMessage(int inSize, char* inData) {

... snip ...

        case 's':
            SetSymbol(slots + i + 1, getsym(msg.gets()));
            // post("sym '%s'\n", slots[i+1].us->name);
            break;

I guess that could be:

        case 's':
            if(stringArgsFlagThatDoesntExistYet && (i > 0)) {
                        ... in here, make a string...
                        ... this involves GC stuff where I'm lost...
            } else {
                        SetSymbol(slots + i + 1, getsym(msg.gets()));
            }
            // post("sym '%s'\n", slots[i+1].us->name);
            break;

hjh

Thanks for pointing this out! That incoming strings are symbols was not very obvious to me.

I’ve basically copy-pasted your solution for now, and it works for what I am doing.
I’ll have a look at the specifics after my concert next week :))