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; }
void TouchTracker::process(int) { if (!mpIn) return; const MLSignal& in(*mpIn); if (mNeedsClear) { mBackground.copy(in); mBackgroundFilter.clear(); mNeedsClear = false; return; } mFilteredInput.copy(in); if (mCalibrator.isCalibrating()) { int done = mCalibrator.addSample(mFilteredInput); if(done == 1) { // Tell the listener we have a new calibration. We still do the calibration here in the Tracker, // but the Model will be responsible for saving and restoring the calibration maps. if(mpListener) { mpListener->hasNewCalibration(mCalibrator.mCalibrateSignal, mCalibrator.mNormalizeMap, mCalibrator.mAvgDistance); } } } else { if(mDoNormalize) { mCalibrator.normalizeInput(mFilteredInput); } if(mMaxTouchesPerFrame > 0) { // smooth input float kc, ke, kk; kc = 4./16.; ke = 2./16.; kk=1./16.; mFilteredInput.convolve3x3r(kc, ke, kk); // build sum of currently tracked touches // mSumOfTouches.clear(); int numActiveTouches = 0; for(int i = 0; i < mMaxTouchesPerFrame; ++i) { const Touch& t(mTouches[i]); if(t.isActive()) { Vec2 touchPos(t.x, t.y); mTemplateScaled.clear(); mTemplateScaled.add2D(mCalibrator.getTemplate(touchPos), 0, 0); mTemplateScaled.scale(t.z*mCalibrator.getZAdjust(touchPos)); mSumOfTouches.add2D(mTemplateScaled, touchPos - Vec2(kTemplateRadius, kTemplateRadius)); numActiveTouches++; } } // to make sum of touches a bit bigger //mSumOfTouches.scale(1.5f); //mSumOfTouches.convolve3x3r(kc, ke, kk); // // TODO lots of optimization here in onepole, 2D filter // // TODO the mean of lowpass background can be its own control source that will // act like an accelerometer! tilt controls even. mBackgroundFilterFrequency.fill(mBackgroundFilterFreq); // build background: lowpass filter rest state. Filter freq. // is nonzero where there are no touches, 0 where there are touches. mTemp.copy(mSumOfTouches); mTemp.scale(100.f); mBackgroundFilterFrequency.subtract(mTemp); mBackgroundFilterFrequency.sigMax(0.); // TODO allow filter to move a little if touch template distance is near threshold // this will fix most stuck touches // filter background in up direction mBackgroundFilterFrequency2.fill(mBackgroundFilterFreq); mBackgroundFilter.setInputSignal(&mFilteredInput); mBackgroundFilter.setOutputSignal(&mBackground); // set asymmetric filter coeffs and get background mBackgroundFilter.setCoeffs(mBackgroundFilterFrequency, mBackgroundFilterFrequency2); mBackgroundFilter.process(1); } // subtract background from input // mInputMinusBackground.copy(mFilteredInput); mInputMinusBackground.subtract(mBackground); // move or remove and filter existing touches // updateTouches(mInputMinusBackground); // TODO can negative values be used to inhibit nearby touches? // this might prevent sticking touches when a lot of force is // applied then quickly released. // TODO look for retriggers here, touches not fallen to 0 but where // dz warrants a new note-on. The way to do this is keep a second, // separate set of key states that do not get cleared by current // touches. These can be used to get the dz values and using the exact // same math, velocities will match other note-ons. // // we can also look for a nearby release just beforehand when // retriggering. this will increase confidence in a retrigger as // opposed to simply moving the touch. // after update Touches, subtract sum of touches to get residual R // R = input - T. // This represents any pressure data not currently part of a touch. if(mMaxTouchesPerFrame > 0) { mInputMinusBackground.sigMax(0.); mResidual.copy(mInputMinusBackground); mResidual.subtract(mSumOfTouches); mResidual.sigMax(0.); } // get signals for viewer // TODO optimize: we only have to copy these each time a view is needed mCalibratedSignal.copy(mInputMinusBackground); mCookedSignal.copy(mSumOfTouches); mTestSignal.copy(mResidual); // get subpixel xyz peak from residual addPeakToKeyState(mResidual); // update key states for(int i=0; i<mNumKeys; ++i) { mKeyStates[i].tick(); } findTouches(); // filter touches // filter x and y for output // filter touches and write touch data to one frame of output signal. // MLSignal& out = *mpOut; for(int i = 0; i < mMaxTouchesPerFrame; ++i) { Touch& t = mTouches[i]; if(t.age > 1) { float xyc = 1.0f - powf(2.71828f, -kMLTwoPi * mLopass*0.1f / (float)mSampleRate); t.xf += (t.x - t.xf)*xyc; t.yf += (t.y - t.yf)*xyc; } else if(t.age == 1) { t.xf = t.x; t.yf = t.y; } out(xColumn, i) = t.xf; out(yColumn, i) = t.yf; out(zColumn, i) = (t.age > 0) ? t.zf : 0.; out(dzColumn, i) = t.dz; out(ageColumn, i) = t.age; out(dtColumn, i) = t.tDist; } } #if DEBUG // TEMP if (mCount++ > 1000) { mCount = 0; // dumpTouches(); } #endif }