• Trill
  • specific touch point IDs?

I'm using a Trill bar in Pd, it's lots of fun.

I'm wondering whether it's possible to assign a specific ID to a touch, so that each touch can be tracked separately?

At the moment I'm using the number of touches to trigger different things, so each time I touch with a new finger it plays a new voice. however the downside of that approach is if I lift my fingers up in a different order, the voices change which finger they are assigned to. Does that make sense?

Like this:
if I press one finger down: sound1 plays
if I keep that finger down and press a second finger down: sound1 and sound2 play
if I lift my first finger up: sound2 stops and sound1 is still playing
so sound1 has switched from finger1 to finger2. I'd rather the sounds be linked directly to the touches.

as I write this out I realise that this is essentially like polyphonic voice allocation...so maybe there's a roundabout way I can do it, but I'm wondering if I can just get the data from the Trill with a touch identifier instead of just the number of touches...

Hi Yann, glad to hear you're enjoying working with the Trill Bars.

We don't currently have an example which deals with touch order in the way you describe it, although it has been on our list of example to create for a while. The firmware always puts the lowest positioned touch as the first entry in reading array which can be counter intuitive for things like voice allocation.

Like you mentioned it's just about keeping track of when a touch appears, and then assigning an index to it. You could then lock the position of that touch in an array or just keep a touchID assigned to that touch. If you wanted to work with the current firmware then every time the number of touches changes you would need to check the current touch readings against previously stored touch readings, look at which readings are closest and so deduce when a touch has continued or when there is a actual new one. This is relatively easy to get working for 90% of the times but quite a challenge to make bullet-proof, hence the lack of example.

If you have any ideas about how to deal with this in Pd we'd love to hear them. We'll also see if we can push this up the todo list as it has been sitting there for a long time!

10 days later

I only just saw your response Robert, thanks! I'll keep poking at it a bit and see if I come up with anything...

7 months later

@yannseznec I pushed something similar here. This is based on the code that was in the d-box example, but improved. This class works pretty well as far as I can tell. It's not Pd, but this is something that's somehow easier to do in C++. Could add an integration with Bela + Trill + Pd I guess ...

class TouchTracker
{
public:
	typedef uint32_t Id;
	static constexpr Id kIdInvalid = -1;
	typedef CentroidDetection::DATA_T Position;
	struct TouchWithId
	{
		centroid_t touch;
		Position startLocation;
		Id id = kIdInvalid;
	};
private:
	static_assert(std::is_signed<Position>::value); // if not signed, distance computation below may get fuzzy
	static_assert(std::is_same<decltype(centroid_t::location),Position>::value);
	static constexpr size_t kMaxTouches = 5;
	size_t numTouches = 0;
	size_t newId = 0;
	Position maxTrackingDistance = 0.2;
	std::array<unsigned int,kMaxTouches> sortedTouchIndices {};
	std::array<unsigned int,kMaxTouches> sortedTouchIds {};
	std::array<TouchWithId,kMaxTouches> sortedTouches {};
public:
	static constexpr TouchWithId kInvalidTouch = {
		.id = kIdInvalid,
	};
private:
	size_t getTouchOrderById(const Id id)
	{
		for(size_t n = 0; n < sortedTouches.size() && n < numTouches; ++n)
			if(id == sortedTouches[n].id)
				return n;
		return numTouches;
	}
	// these are stored only so that we can detect frame changes.
	// TODO: if there is a guarantee process() is called only on new frames,
	// you can save memory by making these local variables there.
	std::array<centroid_t,kMaxTouches> touches;
public:
	void setMaxTrackingDistance(Position d)
	{
		maxTrackingDistance = d;
	}
	void process(CentroidDetection& slider) {
		// cache previous readings
		std::array<TouchWithId,kMaxTouches> prevSortedTouches = sortedTouches;
		size_t prevNumTouches = numTouches;
		numTouches = slider.getNumTouches();
		bool changed = (numTouches != prevNumTouches);
		for(size_t n = 0; n < numTouches; ++n)
		{
			centroid_t newTouch = centroid_t{ .location = slider.touchLocation(n), .size = slider.touchSize(n) };
			changed |= memcmp(&newTouch, &touches[n], sizeof(newTouch));
			touches[n] = newTouch;
		}
		if(!changed)
		{
			// if we are a repetition of the previous frame, no need to process anything
			// NOTE: right now we are already avoiding calling on duplicate frames,
			// so we will return early only if two successive, distinct frames happen
			// to be _exactly_ the same
			return;
		}

		Id firstNewId = newId;
		constexpr size_t kMaxPermutations = kMaxTouches * (kMaxTouches - 1);
		Position distances[kMaxPermutations];
		size_t permCodes[kMaxPermutations];
		// calculate all distance permutations between previous and current touches
		for(size_t i = 0; i < numTouches; ++i)
		{
			for(size_t p = 0; p < prevNumTouches; ++p)
			{
				size_t index = i * prevNumTouches + p;	// permutation code [says between which touches we are calculating distance]
				distances[index] = std::abs(touches[i].location - prevSortedTouches[p].touch.location);
				permCodes[index] = index;
				// sort permCodes and distances by distances from min to max
				while(index && (distances[index] < distances[index - 1]))
				{
					std::swap(permCodes[index], permCodes[index - 1]);
					std::swap(distances[index], distances[index - 1]);
					index--;
				}
			}
		}

		size_t sorted = 0;
		bool currAssigned[kMaxTouches] = {false};
		bool prevAssigned[kMaxTouches] = {false};

		// track touches assigning index according to shortest distance
		for(size_t i = 0; i < numTouches * prevNumTouches; ++i)
		{
			size_t currentIndex = permCodes[i] / prevNumTouches;
			size_t prevIndex = permCodes[i] % prevNumTouches;
			if(distances[i] > maxTrackingDistance)
			{
				// if distance is too large, it must be a new touch
				// TODO: this heuristic could be improved, e.g.: by tracking
				// and considering past velocity
				continue;
			}
			// avoid double assignment
			if(!currAssigned[currentIndex] && !prevAssigned[prevIndex])
			{
				currAssigned[currentIndex] = true;
				prevAssigned[prevIndex] = true;
				sortedTouchIndices[currentIndex] = prevIndex;
				sortedTouchIds[currentIndex] = prevSortedTouches[prevIndex].id;
				sorted++;
			}
		}
		// assign a free index to new touches
		for(size_t i = 0; i < numTouches; i++)
		{
			if(!currAssigned[i])
			{
				sortedTouchIndices[i] = sorted++; // assign next free index
				sortedTouchIds[i] = newId++;
			}
		}
		// if some touches have disappeared...
		// ...we have to shift all indices...
		for(size_t i = prevNumTouches - 1; i != (size_t)-1; --i) // things you do to avoid warnings ...
		{
			if(!prevAssigned[i])
			{
				for(size_t j = 0; j < numTouches; ++j)
				{
					// ...only if touches that disappeared were before the current one
					if(sortedTouchIndices[j] > i)
						sortedTouchIndices[j]--;
				}
			}
		}

		// done! now update
		for(size_t i = 0; i < numTouches; ++i)
		{
			// update tracked value
			size_t idx = sortedTouchIndices[i];

			const Id id = sortedTouchIds[i];
			Position startLocation = -1;
			if(id >= firstNewId)
				startLocation = touches[i].location;
			else {
				// find it in the prev arrays
				// TODO: cache this value earlier so we can be faster here
				for(size_t n = 0; n < prevNumTouches; ++n)
				{
					if(id == prevSortedTouches[n].id)
					{
						startLocation = prevSortedTouches[n].startLocation;
						break;
					}
				}
			}
			assert(-1 != startLocation);
			sortedTouches[idx] = TouchWithId {
				.touch = touches[i],
				.startLocation = startLocation,
				.id = id,
			};
		}
		// empty remaining touches. Not that they should ever be accessed...
		for(size_t i = numTouches; i < sortedTouches.size(); ++i)
			sortedTouches[i].id = kIdInvalid;
#if 0
		for(size_t n = 0; n < numTouches; ++n)
		{
			auto& t = getTouchOrdered(n);
			printf("[%u]%lu %.2f %.1f ", n, t.id, t.touch.location, t.startLocation);
		}
		if(numTouches)
			printf("\n\r");
#endif
	}
	size_t getNumTouches()
	{
		return numTouches;
	}
	const TouchWithId& getTouchById(const Id id)
	{
		size_t n = getTouchOrderById(id);
		if(n >= numTouches)
			return kInvalidTouch;
		else
			return sortedTouches[n];
	}
	void setStartLocationById(const Id id, Position newLocation)
	{
		size_t n = getTouchOrderById(id);
		if(n < numTouches)
			sortedTouches[n].startLocation = newLocation;
	}
	// the last is the most recent
	const TouchWithId& getTouchOrdered(size_t n)
	{
		return sortedTouches[n];
	}
	const TouchWithId& getTouchMostRecent()
	{
		return sortedTouches[numTouches - 1];
	}
	const TouchWithId& getTouchOldest()
	{
		return sortedTouches[0];
	}
};

oh yeah that looks like just the thing! I definitely understand that it's not the kind of thing that's easy to do in Pd, though. Would be fun one day if you ever get around to it!

6 months later