Error: OSC type tags

Im experimenting with i2c controllers and sc. i set up my osc defs and build to find that it all crashes down followed by
ERROR: OSC messages must have type tags.
i suspect that because i run a .cpp file in the background to run a multiplexer and the encoders that i need to set up some tags to follow suit with the .cpp parse?
any light on this topic would be much appreciated thanks

Probably your C++ is producing malformed OSC packets.

The OSC specification is well documented; if your c++ thingy isn’t following the spec, of course there would be problems.

Since you haven’t posted any of that code, or any examples of raw OSC data coming out of the multiplexer, there’s no way to answer your question in any detail.

hjh

@jamshark70 thanks jams, my c++ side is set up to out put integers which i am receiving fine in supercollider post window and this is my light example,
i was just reading about \n_set, does that apply to my situation?

//open port
thisProcess.openUDPPort(5555);

//define control

(
OSCdef.new(\knob0, {
	arg msg, time, addr, port;
	x.set(\freq, msg[1].linexp(0,1,20,500));
}, '/encoder', nil, 5555, [0], );
);

(
OSCdef.new(\knob1, {
	arg msg, time, addr, port;
	x.set(\pan, msg[1].linlin(0,1,-1,1));
}, '/encoder', nil, 5555, [1], );
);

(
OSCdef.new(\knob2, {
	arg msg, time, addr, port;
	x.set(\amp, msg[1].linexp(0.1,0.01,1));
}, '/encoder', 5555, [2], );
);

(
OSCdef.new(\knob3, {
	arg msg, time, addr, port;
	x.set(\nharm, msg[1].linlin(0,1,1,50));
}, '/encoder', nil, 5555, [3], );
);

(
OSCdef.new(\knob4, {
	arg msg, time, addr, port;
	x.set(\detune, msg[1].linexp(0,1,0.01,12));
}, '/encoder', \detune, 5555, [4], );

)

//compile synthdef
(
SynthDef.new(\example, {
	arg freq=40, nharm=12, detune=0.2, gate=0, 
	pan=0, amp=1, out=0;
	var sig, env;
	env = EnvGen.kr(Env.adsr(0.05,0.1,0.5,3),gate);
	sig = Blip.ar (
		freq *
		LFNoise1.kr(0.2!16).bipolar(detune.neg,detune).midiratio,
		nharm
	);
		sig = sig * LFNoise1.kr(0.5!16).exprange(0.1,1);
		sig = Splay.ar(sig);
	    sig = Balance2.ar(sig[0], sig[1], pan);
			sig = sig * env * amp;
			Out.ar(out, sig);
}).add
)

// open gate

x = Synth.new(\example, [\gate, 1]);

Hi, thanks for the additional information, but there are still many missing details to troubleshoot.

my c++ side is set up to out put integers

How is it set up to output integers?

That is, I was asking to look at the sending code, specifically the part that is constructing the outgoing messages. But in reply, you sent the receiving code.

If you’re not able to send the message-construction code, then it would be helpful to have an example of exactly the format of message that you’re trying to send. I can guess some of it from the example you posted, but a/ guesswork is usually not effective as a troubleshooting strategy and b/ see below, there are details that don’t make sense.

which i am receiving fine in supercollider post window

How are you receiving them fine in the post window?

Message format:

Also, there is an inconsistency in your OSCdefs. I see that you’re using the argsTemplate to differentiate between e.g. /encoder, 0 and /encoder, 1. argsTemplate always begins with message array index 1 – the second item in the message array. (Why doesn’t it begin with the first? Because the first is the OSC command path, and this has already been matched.)

Based on argsTemplate, then, I could guess that your message looks like ['/encoder', 0, 0.373646].

But then you write msg[1].linexp(...)msg[1] will be the 0, 1, 2, 3, and not the parameter value.

Based on the available information, I can’t tell if your message format is ['/encoder', 0, 0.373646] (should be msg[2].lin...) or ['/encoder0', 0.373646].

(Also, \knob2 has a typo: linexp(0.1,0.01,1) should be linexp(0, 1, 0.01, 1), shouldn’t it? BTW thanks for this – I’ve been looking for a concrete example of why it’s a bad idea to write commas without spaces in code – here is a perfect one.)

I can say a little more about type tags, but this message is already long enough. :grin:

hjh

OK, type tags…

First, here’s a function to hex-print streams of bytes.

(
f = { |int8array, stream(Post)|
	var bytes8 = { |array, start|
		8.do { |i|
			var byte = array[start + i];
			if(i > 0) { stream << " " };
			if(byte.notNil) {
				stream << byte.asHexString(2);
			} {
				stream << "  ";
			};
		};
	};
	var chars8 = { |array, start|
		8.do { |i|
			var ch = array[start + i];
			if(ch.notNil) {
				ch = ch.asAscii;
				if(ch.isPrint.not) { ch = $. };
				stream << ch;
			} {
				stream << " ";
			};
		};
	};
	forBy(0, int8array.size-1, 16) { |rowI|
		bytes8.(int8array, rowI);
		stream << "  ";
		bytes8.(int8array, rowI+8);
		stream << "\t";
		chars8.(int8array, rowI);
		stream << " ";
		chars8.(int8array, rowI+8);
		stream << "\n";
	};
};
)

Assuming that your desired message format is ['/encoder', 0, 0.843], we can use this to look at a correctly formed OSC message:

m = ['/encoder', 0, 0.843].asRawOSC;

f.(m);

2F 65 6E 63 6F 64 65 72  00 00 00 00 2C 69 66 00	/encoder ....,if.
00 00 00 00 3F 57 CE D9                         	....?W..       
  • First is the command-path string. Strings are C-style (characters followed by a 0x00 terminator).

  • OSC generally expects parts of the message to begin on 4-byte boundaries. /encoder is 8 characters, and the 0 is the ninth, so it has to pad 9 bytes up to 12.

  • Type tag section begins with a comma (2C).

  • Type tags are characters: i = integer, f = float. There are others that you can look up on the OSC website that I sent before.

  • Type tag section ends with a 0 byte. In this example, the trailing 0 brings it up to the 4-byte boundary, so there’s no additional padding. (But, if there are more or fewer data values, there might be padding 0s).

  • Then come the data values: 00 00 00 00 is a 32-bit integer 0; 3F 57 CE D9 is the 32-bit representation of 0.843.

  • There’s no terminator at the end because the type tag section tells you how many data values will be expected.

What happens if we delete the type tag section?

m = m[0..11] ++ m[16..];
-> Int8Array[ 47, 101, 110, 99, 111, 100, 101, 114, 0, 0, 0, 0, 0, 0, 0, 0, 63, 87, -50, -39 ]

f.(m);
2F 65 6E 63 6F 64 65 72  00 00 00 00 00 00 00 00	/encoder ........
3F 57 CE D9                                     	?W..   

// now send it

OSCFunc.trace(true);
n = NetAddr.localAddr;

n.sendRaw(m);

ERROR: OSC messages must have type tags.  /encoder
OSC Message Received:
	time: 2414.040007302
	address: a NetAddr(127.0.0.1, 57120)
	recvPort: 57120
	msg: [ /encoder ]

So – because of a mistake in the message construction (your C++ side), we get the type tag error upon receipt in SC.

Also SC gives up trying to interpret the message at this point – the numbers do not come through in the post window.

In any case, maybe this will give you some hints about how the C++ should be building the message.

hjh

Ah, just thought of something, last one for now.

Maybe your encoder messages are fine, but the multiplexer may be sending other messages that are malformed.

Because you didn’t quote the entire error message, there’s no way to know based on available information here.

hjh

@jamshark70 thanks jam for your detailed response!
ill leave my c++ code here whilst i dig into your suggestions!

#include "src/MCP23017.h"
#include <libraries/Encoder/Encoder.h>
#include <libraries/UdpClient/UdpClient.h>
#include <oscpkt.hh>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <thread>
#include <vector>
#include <string>

std::string gOscAddress = "/encoder"; // OSC address. Message format: <address> <encoderId> <encoderValue>
std::string gDestinationIp = "192.168.7.1"; // 192.168.7.1 is the host computer (if it's a Mac or Linux, it would be 192.168.6.1 if it's Windows).
int gDestinationPort = 5555;

std::vector<Encoder> encoders;
std::vector<std::array<uint8_t, 2>> pinsPairs = {
	{{5, 6}},
	{{3, 2}},
	{{9, 10}},
	{{12, 13}},
};

int i2cBus = 1;
std::vector<uint8_t> i2cAddresses = {
	0x23,
	0x25,
	0x27,
	
};

std::vector<uint16_t> oldGpios;
std::vector<uint16_t> gpios;
std::vector<MCP23017> mcps;
int gStop = 0;

static void makeBinaryString(char* str, uint16_t data)
{
	for(unsigned int n = 0; n < sizeof(data) * 8; ++n, ++str)
		*str = (data & (1 << n)) ? '1' : '0';
	*str = '\0';
}

static void processEnc()
{
	static std::vector<uint16_t> oldGpios = gpios;
	for(unsigned int m = 0; m < mcps.size(); ++m)
	{
		MCP23017& mcp = mcps[m];
		uint16_t& oldGpio = oldGpios[m];
		uint16_t& gpio = gpios[m];
		// we always read from INTCAP (the state of the GPIO when the
		// interrupt was triggered). This resets the interrupt and
		// allows us to see which pin toggled first (useful for encoders!)
		gpio = mcp.readINTCAPAB();
		for(unsigned int n = 0; n < pinsPairs.size(); ++n)
		{
			auto& pins = pinsPairs[n % pinsPairs.size()];
			encoders[n + m * pinsPairs.size()].process(gpio & (1 << pins[0]), gpio & (1 << pins[1]));
		}
		if(oldGpio != gpio)
		{
			// we caught the device as an interrupt had just occurred.
			// add a little "debouncing" delay before reading again
			// TODO: do not hold back reading back other devices while debouncing one device
			usleep(1000);
			// read and ignore in order to clear any interrupt due to bounces that
			// may have occurred in the meantime.
			mcp.readINTCAP(0);
		}
	}
}

static void printEnc(UdpClient* socket)
{
	int ss = 20;
	char stars[ss + 1];
	char spaces[ss + 1];
	memset(stars, '*', ss);
	memset(spaces, ' ', ss);
	spaces[ss] = stars[ss] = '\0';
	std::vector<int> oldRots(encoders.size());
	std::vector<uint16_t> oldGpios = gpios;
	oscpkt::PacketWriter pw;
	while(!gStop){
		pw.init();
		//pw.startBundle();
		unsigned int numMsg = 0;
		for(unsigned int m = 0; m < mcps.size(); ++m)
		{
			uint16_t& gpio = gpios[m];
			uint16_t& oldGpio = oldGpios[m];
			bool shouldPrint = false;
			if(gpio != oldGpio)
				shouldPrint = true;
			oldGpio = gpio;
			unsigned int encStart = m * pinsPairs.size();
			unsigned int encEnd = (1 + m) * pinsPairs.size();
			for(unsigned int n = encStart; n < encEnd; ++n)
			{
				int& oldRot = oldRots[n];
				int rot = encoders[n].get();
				if(oldRot != rot)
				{
					shouldPrint = true;
					oscpkt::Message msg(gOscAddress);
					pw.addMessage(msg.pushInt32(int32_t(n)).pushInt32(int32_t(rot)));
					printf("%s %u %d\n", gOscAddress.c_str(), n, rot);
					++numMsg;
				}
				oldRot = rot;
			}
			if(shouldPrint)
			{
				char str[17];
				makeBinaryString(str, gpio);
				printf("[%d] %s: ", m, str);
				for(unsigned int n = encStart; n < encEnd; ++n)
				{
					int rot = encoders[n].get();
					int numStars = rot + 1;
					while(numStars < 0)
						numStars += ss;
					numStars %= ss;
					printf("%4d %.*s%.*s", rot, numStars, stars, ss - numStars, spaces);
				}
				printf("\n");
			}
		}
		if(numMsg)
		{
			//if(pw.endBundle().isOk())
			{
				socket->send((void*)pw.packetData(), pw.packetSize());
			}
		}
		usleep(10000);
	}
}

// Handle Ctrl-C by requesting that the threads stop
void interrupt_handler(int var)
{
	gStop = true;
}

int main(int argc, char** argv)
{
	UdpClient socket;
	if(!socket.setup(gDestinationPort, gDestinationIp.c_str()))
	{
		fprintf(stderr, "Unable to send to %s:%d\n", gDestinationIp.c_str(), gDestinationPort);
		return 1;
	}
	mcps.reserve(i2cAddresses.size()); // ensure no allocation happens in the below loop.
	for(unsigned int c = 0; c < i2cAddresses.size(); ++c)
	{
		mcps.emplace_back(i2cBus, i2cAddresses[c]);
		if(!mcps.back().openI2C())
		{
			fprintf(stderr, "Failed to open device on bus %d, address %#x\n", i2cBus, i2cAddresses[c]);
			mcps.erase(mcps.end() - 1, mcps.end());
			continue;
		}
		MCP23017& mcp = mcps.back();
		gpios.push_back(0);
		for(unsigned int n = 0; n < 16; ++n)
		{
			mcp.pinMode(n, MCP23017::INPUT);
			mcp.pullUp(n, MCP23017::HIGH);  // turn on a 100K pullup internally
			// we set up interrupts so we can read the INTCAP register
			mcp.setupInterrupts(true, false, MCP23017::HIGH);
			for(unsigned int n = 0; n < 16; ++n)
				mcp.setupInterruptPin(n, MCP23017::CHANGE);
		}
	}
	if(!mcps.size())
	{
		fprintf(stderr, "No device detected\n");
		return 1;
	} else {
		printf("%d encoder boards detected\n", mcps.size());
		
	}
	
	for(unsigned int n = 0; n < mcps.size() * pinsPairs.size(); ++n)
	{
		encoders.push_back({0, Encoder::ACTIVE_HIGH});
	}
	signal(SIGINT, interrupt_handler);
	signal(SIGTERM, interrupt_handler);
	std::thread printThread(printEnc, &socket);
	while(!gStop)
	{
		processEnc();
	}
	if(printThread.joinable())
		printThread.join();
	return 0;
}

Your OSCdef for \knob2 looks different than the others. They all have an additional arg between the ‘/encoder’ and the 5555, but knob2 doesn’t (but it has a trailing comma that looks sorta lonely)

No idea if it’s relevant, just a syntax-level discrepancy i noticed.

Cheers,
eddi
https://alln4tural.bandcamp.com

OK, the C++ seems to be using a legit OSC message constructor.

Could you confirm one thing – the type tag error message – does it say “ERROR: OSC messages must have type tags. /encoder” or is it a different command path?

I’m asking because if it’s the /encoder messages that are failing, then you wouldn’t see any numbers in the post window at all. (See my example above – when I took away the type tags, the complete message being passed through was [ '/encoder' ] – the numbers are missing.)

So if you are seeing numbers, then that makes me think maybe the /encoder messages are not the problem. Maybe something else in the system is sending other messages to sclang’s port.

Also, the C++ code confirms that the message format is [ '/encoder', id, value ] – so this means, everywhere that you wrote msg[1] in your original example should really be msg[2].

Now, there’s one other thing – the C++ is adding the value as an integer (.pushInt32(int32_t(rot))). But your receiving code seems to be assuming a value range of 0 to 1 – if the incoming data are integers, then you will not get meaningful results from the linlin or linexp conversions.

So it would also be good to verify what is the actual range of values coming from the device.

hjh

@jamshark70 amazing, ok everything is functional. your suggestions where super helpful.
i still ocassionally run into error osc type tags but only when im rotating quickly

@jamshark70 this is weird, but im branching to use these oscdef templates with a bigger project i am working on, and everything is great for 30 seconds which then everything falls down and i get /n_set Node not found
not sure what im missing, the synthdef is fairly large but surley thats not the prob.
any guidance would be amazing.

Maybe the sending code is trying to construct two or more messages at once…?

Clearly LRFCLRFC isn’t a valid command path.

Three possibilities:

  • Something in your larger code somewhere is getting confused about the node ID and sending to the wrong ID.

  • Or, something is closing the gate prematurely and the synth is going away earlier than expected.

  • Or, maybe there’s a second unit in the synth that can free the node (and the synth is going away earlier than expected).

There’s not enough detail to be more specific… happy hunting…

hjh

@jamshark70 thanks jam, again really handy stuff, im going to dig into this.
one more off the cuff question…im experimenting with numerators and denominators for an FM synth,

	arg freq = 220, gate = 0, configuration = 1,
	    numerators = #[1,1,1,1], denominators = #[1,1,1,1],.....ect 

im curious to know, i have these setup in an array, how do i assign these 4 changeable values… in an osc def for four encoders? or four different oscdefs?

OSCdef.new(\knob0, {
	arg msg, time, addr, port;
	postf("%[%] is %\n", msg[0], msg[1], msg[2]);
	x.set(\numerators, msg[2].linexp(-50,50,0.01,150));
}, '/encoder', nil, 5555, [0, 1, 2, 3], );

I didn’t know the answer offhand, but a search of the help system turned this up: Synth:seti – “Set part of an arrayed control.”

I had searched for Synth:set, hoping there might be a note about partial setting of an arrayed control. There wasn’t – but the very next method in this helpfile is seti.

Nice to see documentation efforts over the years pay off :+1:

hjh

1 Like

you are missing a nil here, compared to the other OSCdefs. This means that 5555 is now passed as the source ID.

You are using \detune here as a srcID argument. Since srcID is supposed to be a NetAddr, this will cause problems.

hi jam,
ive been hitting walls with this one.
ive built a gui for my little fm synth which functions great but im struggling to use seti with an array of 4 values ( \tratios ) and my 4 encoders.
it is possible to control parameters of a gui with external hardware (osc) right?
ive left a little snippet of my gui and oscdef
any tips on connectivity are welcomed

~ratiotextboxes = Array.fill(4,
	{arg i;
	  var nb = NumberBox(w,Rect(65+(40*i),192,37,22))
		.action_({arg obj;
			      var floating = obj.value.asFloat;
				nb.scroll_step=(0.1);
			      if(floating != 0,
				     {~topratios[i] = vaf;
					  ~notes.do({arg item;
					             if(item.isPlaying,
						           {item.seti(\tratios,i,floating);});
					             })
			         });
			      obj.value = ~topratios[i].asString;
		         });
			
		nb.string = ~topratios[i].asString;
		nb;
    });

////////
OSCdef.new(\knob0, {
	arg msg, time, addr, port;
	postf("%[%] is %\n", msg[0], msg[1], msg[2]);
		x.seti(\tratios, 1, msg[2].linexp(-50,50,0.01,150));
}, '/encoder', nil, 5555, [0], );

If, in this example, the argTemplate is [0], I’m not clear why you’d seti to index 1?

Anyway, I think to control the GUI, you’d just include this in the OSCdef:

defer { ~topratios[0].value = msg[2].linlin(-50, 50, 0, 1) };

hjh

@jamshark70 in this example that [0] is to specify which encoder im using, is it important to also include the x.seti.... line?

@jamshark70 also i keep getting these