Skip to content
Permalink
master
Go to file
 
 
Cannot retrieve contributors at this time
884 lines (754 sloc) 20.4 KB
// Part of the Soundplane client software by Madrona Labs.
// Copyright (c) 2017 Madrona Labs LLC. http://www.madronalabs.com
// Distributed under the MIT license: http://madrona-labs.mit-license.org/
#include <iostream>
#include <cmath>
#include <list>
#include <thread>
#include <mutex>
#include <array>
#include <bitset>
#include <algorithm>
#include "TouchTracker.h"
constexpr float kTwoPi = 3.1415926535f*2.f;
template <class c>
inline c (clamp)(const c& x, const c& min, const c& max)
{
return (x < min) ? min : (x > max ? max : x);
}
// within range, including start, excluding end value.
template <class c>
inline bool (within)(const c& x, const c& min, const c& max)
{
return ((x >= min) && (x < max));
}
inline float lerp(const float a, const float b, const float m)
{
return(a + m*(b-a));
}
inline float unlerp(const float a, const float b, const float x)
{
return(x - a)/(b-a);
}
inline float mapRange(const float a, const float b, const float c, const float d, const float x)
{
return lerp(c, d, unlerp(a, b, x));
}
inline float mapAndClipRange(const float a, const float b, const float c, const float d, const float x)
{
return lerp(c, d, clamp(unlerp(a, b, x), 0.f, 1.f));
}
// return city block distance between two touches with a "z importance" parameter.
// if z scale is too small, zero touches will get matched with new active ones in the same position.
// if too big, z is more important than position and nothing works.
inline float cityBlockDistanceXYZ(Touch a, Touch b, float zScale)
{
return fabs(a.x - b.x) + fabs(a.y - b.y) + zScale*fabs(a.z - b.z);
}
// TouchTracker
TouchTracker::TouchTracker() :
mSampleRate(1000.f),
mMaxTouchesPerFrame(0),
mLopassZ(50.),
mRotate(false)
{
setThresh(0.1);
for(int i = 0; i < kMaxTouches; i++)
{
mRotateShuffleOrder[i] = i;
}
}
TouchTracker::~TouchTracker()
{
}
void TouchTracker::setMaxTouches(int t)
{
int kmax = kMaxTouches;
int newT = clamp(t, 0, kmax);
if(newT != mMaxTouchesPerFrame)
{
mMaxTouchesPerFrame = newT;
// reset shuffle order
for(int i = 0; i < kMaxTouches; i++)
{
mRotateShuffleOrder[i] = i;
}
mClearNextFrame = true;
}
}
void TouchTracker::setRotate(bool b)
{
mRotate = b;
for(int i = 0; i < kMaxTouches; i++)
{
mRotateShuffleOrder[i] = i;
}
}
void TouchTracker::clear()
{
for (int i=0; i<kMaxTouches; i++)
{
mTouches[i] = Touch();
}
}
// set the threshold of curvature that will cause a touch. Note that this will not correspond with the pressure (z) values reported by touches.
void TouchTracker::setThresh(float f)
{
mOnThreshold = clamp(f, 0.005f, 1.f);
mFilterThreshold = mOnThreshold * 0.5f;
mOffThreshold = mOnThreshold * 0.75f;
}
void TouchTracker::setLopassZ(float k)
{
mLopassZ = k;
}
SensorFrame smoothPressureX(const SensorFrame& in)
{
int i, j;
const float * pr2;
float * prOut;
SensorFrame out{};
for(j = 0; j < SensorGeometry::height; j++)
{
// row ptrs
pr2 = (in.data() + (j*SensorGeometry::width));
prOut = (out.data() + (j*SensorGeometry::width));
i = 0; // left side
{
prOut[i] = (pr2[i] + pr2[i+1]);
}
for(i = 1; i < SensorGeometry::width - 1; i++) // center
{
prOut[i] = (pr2[i-1] + pr2[i] + pr2[i+1]);
}
i = SensorGeometry::width - 1; // right side
{
prOut[i] = (pr2[i-1] + pr2[i]);
}
}
return out;
}
SensorFrame smoothPressureY(const SensorFrame& in)
{
int i, j;
const float * pr1, * pr2, * pr3; // input row ptrs
float * prOut;
SensorFrame out{};
j = 0; // top row
{
pr2 = (in.data() + j*SensorGeometry::width);
pr3 = (in.data() + (j + 1)*SensorGeometry::width);
prOut = (out.data() + j*SensorGeometry::width);
for(i = 0; i < SensorGeometry::width; i++)
{
(prOut[i] = pr2[i] + pr3[i]);
}
}
for(j = 1; j < SensorGeometry::height - 1; j++) // center rows
{
pr1 = (in.data() + (j - 1)*SensorGeometry::width);
pr2 = (in.data() + j*SensorGeometry::width);
pr3 = (in.data() + (j + 1)*SensorGeometry::width);
prOut = (out.data() + j*SensorGeometry::width);
for(i = 0; i < SensorGeometry::width; i++)
{
(prOut[i] = pr1[i] + pr2[i] + pr3[i]);
}
}
j = SensorGeometry::height - 1; // bottom row
{
pr1 = (in.data() + (j - 1)*SensorGeometry::width);
pr2 = (in.data() + j*SensorGeometry::width);
prOut = (out.data() + j*SensorGeometry::width);
for(i = 0; i < SensorGeometry::width; i++)
{
(prOut[i] = pr1[i] + pr2[i]);
}
}
return out;
}
SensorFrame TouchTracker::preprocess(const SensorFrame& in)
{
SensorFrame y;
// fixed IIR filter input
float k = 0.25f;
y = multiply(in, k);
mInputZ1 = multiply(mInputZ1, 1.0 - k);
y = add(y, mInputZ1);
mInputZ1 = y;
// filter out any negative values. negative values can show up from capacitive coupling near edges,
// from motion or bending of the whole instrument,
// from the elastic layer deforming and pushing up on the sensors near a touch.
y = max(y, 0.f);
// a lot of filtering is needed here for Soundplane A to make sure peaks are in centers of touches.
// it also reduces noise.
// the down side is, contiguous touches are harder to tell apart. a smart blob-shape algorithm
// can make up for this later, with this filtering still intact.
y = smoothPressureX(smoothPressureX(smoothPressureX(smoothPressureX(y))));
y = smoothPressureY(smoothPressureY(smoothPressureY(y)));
y = getCurvatureXY(multiply(y, 1.f/64.f));
return y;
}
// to clear the next frame, all touch z values must be set to 0 and states to kTouchStateOff
// so that the frame is guaranteed to be sent.
void TouchTracker::clearAndSendNextFrameIfNeeded()
{
if(mClearNextFrame)
{
mClearNextFrame = false;
mTouches = TouchArray{};
for(int i=0; i<kMaxTouches; ++i)
{
mTouches[i].state = kTouchStateOff;
}
}
}
TouchArray TouchTracker::process(const SensorFrame& in, int maxTouches)
{
setMaxTouches(maxTouches);
mTouches = TouchArray{};
if(mMaxTouchesPerFrame > 0)
{
mTouches = findTouches(in);
// match -> position filter -> feedback
mTouches = matchTouches(mTouches, mTouchesMatch1);
mTouches = filterTouchesXYAdaptive(mTouches, mTouchesMatch1);
mTouchesMatch1 = mTouches;
// asymmetrical z filter from user setting. Ages are created here.
mTouches = filterTouchesZ(mTouches, mTouches2, mLopassZ*2.f, mLopassZ*0.25f);
mTouches2 = mTouches;
// after variable filter, exile decayed touches so they are not matched. Note this affects match feedback!
mTouchesMatch1 = exileUnusedTouches(mTouchesMatch1, mTouches);
// TODO hysteresis after matching to prevent glitching when there are more
// physical touches than mMaxTouchesPerFrame and touches are stolen
if(mRotate)
{
mTouches = rotateTouches(mTouches);
}
mTouches = clampAndScaleTouches(mTouches);
}
clearAndSendNextFrameIfNeeded();
return mTouches;
}
Touch correctPeakX(Touch pos, const SensorFrame& in)
{
Touch newPos = pos;
const float maxCorrect = 0.5f;
const int w = SensorGeometry::width;
int x = pos.x;
int y = pos.y;
if(within(x, 1, w - 1))
{
float a = in[y*w + x - 1];
float b = in[y*w + x];
float c = in[y*w + x + 1];
float p = ((a - c)/(a - 2.f*b + c))*0.5f;
float fx = x + clamp(p, -maxCorrect, maxCorrect);
newPos = Touch{.x = fx, .y = pos.y, .z = pos.z};
}
return newPos;
}
Touch correctPeakY(Touch pos, const SensorFrame& in)
{
const float maxCorrect = 0.5f;
const int w = SensorGeometry::width;
const int h = SensorGeometry::height;
int x = pos.x;
int y = pos.y;
float a, b, c;
if(y <= 0)
{
a = 0;
b = in[y*w + x];
c = in[(y + 1)*w + x];
}
else if(y >= h - 1)
{
a = in[(y - 1)*w + x];
b = in[y*w + x];
c = 0;
}
else
{
a = in[(y - 1)*w + x];
b = in[y*w + x];
c = in[(y + 1)*w + x];
}
float p = ((a - c)/(a - 2.f*b + c))*0.5f;
float fy = y + clamp(p, -maxCorrect, maxCorrect);
return Touch{.x = pos.x, .y = fy, .z = pos.z};
}
float sensorToKeyY(float sy)
{
float ky = 0.f;
constexpr int mapSize = 6;
// Soundplane A as measured.
// NOTE: these y locations depend on the amount of smoothing done in smoothPressureX / smoothPressureY.
constexpr std::array<float, mapSize> sensorMap{{0.7, 1.2, 2.7, 4.3, 5.8, 6.3}};
constexpr std::array<float, mapSize> keyMap{{0.01, 1., 2., 3., 4., 4.99}};
if(sy < sensorMap[0])
{
ky = keyMap[0];
}
else if(sy > sensorMap[mapSize - 1])
{
ky = keyMap[mapSize - 1];
}
else
{
// piecewise linear map
for(int i = 1; i<mapSize; ++i)
{
if(sy <= sensorMap[i])
{
// piecewise linear
float m = (sy - sensorMap[i - 1])/(sensorMap[i] - sensorMap[i - 1]);
ky = lerp(keyMap[i - 1], keyMap[i], m);
break;
}
}
}
return ky;
}
Touch peakToTouch(Touch p)
{
return Touch{.x = mapRange(3.5f, 59.5f, 1.f, 29.f, p.x), .y = sensorToKeyY(p.y), .z = p.z};
}
// quick touch finder based on peaks of curvature.
// this works well, but a different approach based on blob sizes / shapes could do a much better
// job with contiguous keys.
TouchArray TouchTracker::findTouches(const SensorFrame& in)
{
constexpr int kMaxPeaks = kMaxTouches*2;
constexpr int w = SensorGeometry::width;
constexpr int h = SensorGeometry::height;
const float* pIn = in.data();
int i, j;
std::array<Touch, kMaxPeaks> peaks;
TouchArray touches{};
std::array<std::bitset<w>, h> map;
// get peaks
float f11, f12, f13;
float f21, f22, f23;
float f31, f32, f33;
j = 0;
{
auto& row = map[j];
const float* pRow2 = pIn + (j)*w;
const float* pRow3 = pIn + (j + 1)*w;
for(i = 1; i < w - 1; ++i)
{
f21 = pRow2[i - 1]; f22 = pRow2[i]; f23 = pRow2[i + 1];
f31 = pRow3[i - 1]; f32 = pRow3[i]; f33 = pRow3[1 + 1];
row[i] = (f22 > f21) && (f22 > mFilterThreshold) && (f22 > f23)
&& (f22 > f31) && (f22 > f32) && (f22 > f33);
}
}
for (j = 1; j < h - 1; ++j)
{
auto& row = map[j];
const float* pRow1 = pIn + (j - 1)*w;
const float* pRow2 = pIn + (j)*w;
const float* pRow3 = pIn + (j + 1)*w;
for(i = 1; i < w - 1; ++i)
{
f11 = pRow1[i - 1]; f12 = pRow1[i]; f13 = pRow1[i + 1];
f21 = pRow2[i - 1]; f22 = pRow2[i]; f23 = pRow2[i + 1];
f31 = pRow3[i - 1]; f32 = pRow3[i]; f33 = pRow3[1 + 1];
row[i] = (f22 > f11) && (f22 > f12) && (f22 > f13)
&& (f22 > f21) && (f22 > mFilterThreshold) && (f22 > f23)
&& (f22 > f31) && (f22 > f32) && (f22 > f33);
}
}
j = h - 1;
{
auto& row = map[j];
const float* pRow1 = pIn + (j - 1)*w;
const float* pRow2 = pIn + (j)*w;
for(i = 1; i < w - 1; ++i)
{
f11 = pRow1[i - 1]; f12 = pRow1[i]; f13 = pRow1[i + 1];
f21 = pRow2[i - 1]; f22 = pRow2[i]; f23 = pRow2[i + 1];
row[i] = (f22 > f11) && (f22 > f12) && (f22 > f13)
&& (f22 > f21) && (f22 > mFilterThreshold) && (f22 > f23);
}
}
// gather all peaks.
int nPeaks = 0;
for (int j=0; j<h; j++)
{
auto& mapRow = map[j];
for (int i=0; i < w; i++)
{
// if peak
if (mapRow[i])
{
float z = in[j*w + i];
peaks[nPeaks++] = Touch{.x = static_cast<float>(i), .y = static_cast<float>(j), .z = z};
if (nPeaks >= kMaxPeaks) break;
}
}
}
if(nPeaks > 1)
{
std::sort(peaks.begin(), peaks.begin() + nPeaks, [](Touch a, Touch b){ return a.z > b.z; } );
}
// correct and clip
int nTouches = std::min(nPeaks, (int)kMaxTouches);
for(int i=0; i<nTouches; ++i)
{
Touch p = peaks[i];
if(p.z < mFilterThreshold) break;
Touch px = correctPeakX(p, in);
Touch pxy = correctPeakY(px, in);
touches[i] = peakToTouch(pxy);
}
return touches;
}
// match incoming touches in x with previous frame of touches in x1.
// for each possible touch slot, output the touch x closest in location to the previous frame.
// if the incoming touch is a continuation of the previous one, set its age (w) to 1, otherwise to 0.
// if there is no incoming touch to match with a previous one at index i, and no new touch needs index i, the position at index i will be maintained.
// TODO first touch below filter threshold(?) is on one index, then active touch switches index?! investigate.
TouchArray TouchTracker::matchTouches(const TouchArray& x, const TouchArray& x1)
{
const float kMaxConnectDist = 2.f;
TouchArray newTouches{};
std::array<int, kMaxTouches> forwardMatchIdx;
forwardMatchIdx.fill(-1);
std::array<int, kMaxTouches> reverseMatchIdx;
reverseMatchIdx.fill(-1);
// for each previous touch, find minimum distance to a current touch.
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
float minDist = MAXFLOAT;
Touch prev = x1[i];
for(int j=0; j < mMaxTouchesPerFrame; ++j)
{
Touch curr = x[j];
if((curr.z > mFilterThreshold) && (prev.z > mFilterThreshold))
{
float distToCurrentTouch = cityBlockDistanceXYZ(prev, curr, 20.f);
if(distToCurrentTouch < minDist)
{
forwardMatchIdx[i] = j;
minDist = distToCurrentTouch;
}
}
}
}
// for each current touch, find minimum distance to a previous touch
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
float minDist = MAXFLOAT;
Touch curr = x[i];
for(int j=0; j < mMaxTouchesPerFrame; ++j)
{
Touch prev = x1[j];
if((curr.z > mFilterThreshold) && (prev.z > mFilterThreshold))
{
float distToPreviousTouch = cityBlockDistanceXYZ(prev, curr, 20.f);
if(distToPreviousTouch < minDist)
{
reverseMatchIdx[i] = j;
minDist = distToPreviousTouch;
}
}
}
}
// get mutual matches
std::array<int, kMaxTouches> mutualMatches;
mutualMatches.fill(0);
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
int prevIdx = reverseMatchIdx[i];
if(prevIdx >= 0)
{
if(forwardMatchIdx[prevIdx] == i)
{
mutualMatches[i] = true;
}
}
}
std::array<bool, kMaxTouches> currWrittenToNew;
currWrittenToNew.fill(false);
// first, continue mutually matched touches
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
if(mutualMatches[i])
{
int j = reverseMatchIdx[i];
Touch curr = x[i];
Touch prev = x1[j];
{
// touch is continued, mark as connected and write to new touches
curr.age = (cityBlockDistanceXYZ(prev, curr, 0.f) < kMaxConnectDist);
newTouches[j] = curr;
currWrittenToNew[i] = true;
}
}
}
// now take care of any remaining nonzero current touches
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
Touch curr = x[i];
if((!currWrittenToNew[i]) && (curr.z > mFilterThreshold))
{
int freeIdx = -1;
float minDist = MAXFLOAT;
// first, try to match same touch index (important for decay!)
Touch prev = x1[i];
if(prev.z <= mFilterThreshold)
{
freeIdx = i;
}
// then try closest free touch
if(freeIdx < 0)
{
for(int j=0; j<mMaxTouchesPerFrame; ++j)
{
Touch prev = x1[j];
if(prev.z <= mFilterThreshold)
{
float d = cityBlockDistanceXYZ(curr, prev, 0.f);
if(d < minDist)
{
minDist = d;
freeIdx = j;
}
}
}
}
// if a free index was found, write the current touch
if(freeIdx >= 0)
{
Touch free = x1[freeIdx];
curr.age = (cityBlockDistanceXYZ(free, curr, 0.f) < kMaxConnectDist);
newTouches[freeIdx] = curr;
}
}
}
// fill in any free touches with previous touches at those indices. This will allow old touches to re-link if not reused.
for(int i=0; i < mMaxTouchesPerFrame; ++i)
{
Touch t = newTouches[i];
if(t.z <= mFilterThreshold)
{
newTouches[i].x = (x1[i].x);
newTouches[i].y = (x1[i].y);
}
}
return newTouches;
}
// input: vec4<x, y, z, k> where k is 1 if the touch is connected to the previous touch at the same index.
//
TouchArray TouchTracker::filterTouchesXYAdaptive(const TouchArray& in, const TouchArray& inz1)
{
// these filter settings have a big and sort of delicate impact on play feel, so they are not user settable
const float kFixedXYFreqMax = 20.f;
const float kFixedXYFreqMin = 1.f;
TouchArray out{};
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
float x = in[i].x;
float y = in[i].y;
float z = in[i].z;
int age = in[i].age;
float x1 = inz1[i].x;
float y1 = inz1[i].y;
// filter, or not, based on w from matchTouches
float newX, newY;
if(age > 0)
{
// get xy coeffs, adaptive based on z
float freq = mapAndClipRange(0., 0.02, kFixedXYFreqMin, kFixedXYFreqMax, z);
float omegaXY = freq*kTwoPi/mSampleRate;
float kXY = expf(-omegaXY);
float a0XY = 1.f - kXY;
float b1XY = kXY;
// onepole filters
newX = (x*a0XY) + (x1*b1XY);
newY = (y*a0XY) + (y1*b1XY);
}
else
{
newX = x;
newY = y;
}
out[i] = Touch{.x = newX, .y = newY, .z = z, .age = age};
}
return out;
}
TouchArray TouchTracker::filterTouchesZ(const TouchArray& in, const TouchArray& inz1, float upFreq, float downFreq)
{
const float omegaUp = upFreq*kTwoPi/mSampleRate;
const float kUp = expf(-omegaUp);
const float a0Up = 1.f - kUp;
const float b1Up = kUp;
const float omegaDown = downFreq*kTwoPi/mSampleRate;
const float kDown = expf(-omegaDown);
const float a0Down = 1.f - kDown;
const float b1Down = kDown;
TouchArray out{};
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
float x = in[i].x;
float y = in[i].y;
float z = in[i].z;
float z1 = inz1[i].z;
int age1 = inz1[i].age;
float newZ;
// filter z variable
float dz = z - z1;
if(dz > 0.f)
{
newZ = (z*a0Up) + (z1*b1Up);
}
else
{
newZ = (z*a0Down) + (z1*b1Down);
}
// gate with hysteresis
bool gate1 = (age1 > 0);
bool newGate = gate1;
if(newZ > mOnThreshold)
{
newGate = true;
}
else if (newZ < mOffThreshold)
{
newGate = false;
}
// increment age
int newAge = newGate ? (age1 + 1) : 0;
// set state
int newState = newGate ? (gate1 ? kTouchStateContinue : kTouchStateOn) : (gate1 ? kTouchStateOff : kTouchStateInactive);
out[i] = Touch{.x=x, .y=y, .z=newZ, .dz=dz, .age=newAge, .state=newState};
}
return out;
}
// if a touch has decayed below the filter threshold after z filtering, move it off the scene so it won't match to other nearby touches.
TouchArray TouchTracker::exileUnusedTouches(const TouchArray& preFiltered, const TouchArray& postFiltered)
{
TouchArray out(preFiltered);
for(int i = 0; i < mMaxTouchesPerFrame; ++i)
{
Touch a = preFiltered[i];
Touch b = postFiltered[i];
if(b.x > 0.f)
{
if(b.z <= mFilterThreshold)
{
a.x = (-1.f);
a.y = (-10.f);
a.z = (0.f);
}
}
out[i] = a;
}
return out;
}
// rotate order of touches, changing order every time there is a new touch in a frame.
// side effect: writes to mRotateShuffleOrder
TouchArray TouchTracker::rotateTouches(const TouchArray& in)
{
TouchArray touches(in);
if(mMaxTouchesPerFrame > 1)
{
bool doRotate = false;
for(int i = 0; i < mMaxTouchesPerFrame; ++i)
{
if(in[i].age == 1)
{
// we have a new touch at index i.
doRotate = true;
break;
}
}
if(doRotate)
{
// rotate the indices of all free or new touches in the shuffle order
int nFree = 0;
std::array<int, kMaxTouches> freeIndexes;
for(int i=0; i<mMaxTouchesPerFrame; ++i)
{
if((in[i].z < mFilterThreshold) || (in[i].age == 1))
{
freeIndexes[nFree++] = i;
}
}
if(nFree > 1)
{
int first = mRotateShuffleOrder[freeIndexes[0]];
for(int i=0; i < nFree - 1; ++i)
{
mRotateShuffleOrder[freeIndexes[i]] = mRotateShuffleOrder[freeIndexes[i + 1]];
}
mRotateShuffleOrder[freeIndexes[nFree - 1]] = first;
}
}
// shuffle
for(int i = 0; i < mMaxTouchesPerFrame; ++i)
{
touches[mRotateShuffleOrder[i]] = in[i];
}
}
return touches;
}
TouchArray TouchTracker::clampAndScaleTouches(const TouchArray& in)
{
const float kTouchOutputScale = 4.f;
TouchArray out{};
for(int i = 0; i < mMaxTouchesPerFrame; ++i)
{
Touch t = in[i];
if(t.x != t.x)
{
t.x = (0.f);
}
if(t.y != t.y)
{
t.y = (0.f);
}
out[i] = t;
float newZ = (clamp((t.z - mOnThreshold)*kTouchOutputScale, 0.f, 8.f));
if(t.age == 0)
{
newZ = 0.f;
}
out[i].z = (newZ);
}
return out;
}
TouchArray TouchTracker::getTestTouches(time_point<system_clock> now, int maxTouches)
{
TouchArray t{};
setMaxTouches(maxTouches);
system_clock::duration dtn = now.time_since_epoch();
auto m = std::chrono::duration_cast<std::chrono::milliseconds>(dtn).count();
auto c = m%60000;
int ic = c;
float fc = ic;
for(int i=0; i<maxTouches; ++i)
{
float minusI = 16-i;
float rpm = 2.f*(i + 1);
float theta = fc*rpm*kTwoPi/60000.f;
float rx = 12.f/16.*minusI;
float ry = 2.f/16.*minusI;
float cx = 15.f;
float cy = 2.5f;
float x = cosf(theta)*rx + cx;
float y = sinf(theta)*ry + cy;
float theta2 = theta*8.f;
float amp = sinf(theta2)*0.25f;
t[i] = Touch{.x = x, .y = y, .z = amp};
}
mTouches = t;
// asymmetrical z filter from user setting. Ages are created here.
mTouches = filterTouchesZ(mTouches, mTouches2, mLopassZ*2.f, mLopassZ*0.25f);
mTouches2 = mTouches;
mTouches = clampAndScaleTouches(mTouches);
clearAndSendNextFrameIfNeeded();
return mTouches;
}