summaryrefslogtreecommitdiffstats
path: root/src/engine/positionscratchcontroller.cpp
blob: 44e8cd4d43bab3ece566fabbef0a26d4c1588ca1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#include <QtDebug>

#include "engine/positionscratchcontroller.h"
#include "engine/bufferscalers/enginebufferscale.h" // for MIN_SEEK_SPEED
#include "util/math.h"

class VelocityController {
  public:
    VelocityController()
        : m_last_error(0.0),
          m_p(0.0),
          m_d(0.0) {
    }

    void setPD(double p, double d) {
        m_p = p;
        m_d = d;
    }

    void reset(double last_error) {
        m_last_error = last_error;
    }

    double observation(double error) {
        // Main PD calculation
        m_last_error = m_p * error + m_d * (error - m_last_error);
        return m_last_error;
    }

  private:
    double m_last_error;
    double m_p, m_d;
};

class RateIIFilter {
  public:
    RateIIFilter()
        : m_factor(1.0),
          m_last_rate(0.0) {
    }

    void setFactor(double factor) {
        m_factor = factor;
    }

    void reset(double last_rate) {
        m_last_rate = last_rate;
    }

    double filter(double rate) {
        if (fabs(rate) - fabs(m_last_rate) > -0.1) {
            m_last_rate = m_last_rate * (1 - m_factor) + rate * m_factor;
        }  else {
            // do not filter strong decelerations to avoid overshooting
            m_last_rate = rate;
        }
        return m_last_rate;
    }

  private:
    double m_factor;
    double m_last_rate;
};

PositionScratchController::PositionScratchController(QString group)
    : m_group(group),
      m_bScratching(false),
      m_bEnableInertia(false),
      m_dLastPlaypos(0),
      m_dPositionDeltaSum(0),
      m_dTargetDelta(0),
      m_dStartScratchPosition(0),
      m_dRate(0),
      m_dMoveDelay(0),
      m_dMouseSampeTime(0) {
    m_pScratchEnable = new ControlObject(ConfigKey(group, "scratch_position_enable"));
    m_pScratchPosition = new ControlObject(ConfigKey(group, "scratch_position"));
    m_pMasterSampleRate = ControlObject::getControl(ConfigKey("[Master]", "samplerate"));
    m_pVelocityController = new VelocityController();
    m_pRateIIFilter = new RateIIFilter;
}

PositionScratchController::~PositionScratchController() {
    delete m_pRateIIFilter;
    delete m_pVelocityController;
    delete m_pScratchPosition;
    delete m_pScratchEnable;
}

//volatile double _p = 0.3;
//volatile double _d = -0.15;
//volatile double _f = 0.5;

void PositionScratchController::process(double currentSample, double releaseRate,
        int iBufferSize, double baserate) {
    bool scratchEnable = m_pScratchEnable->get() != 0;

    if (!m_bScratching && !scratchEnable) {
        // We were not previously in scratch mode are still not in scratch
        // mode. Do nothing
        return;
    }

    // The latency or time difference between process calls.
    const double dt = static_cast<double>(iBufferSize)
            / m_pMasterSampleRate->get() / 2;

    // Sample Mouse with fixed timing intervals to iron out significant jitters
    // that are added on the way from mouse to engine thread
    // Normally the Mouse is sampled every 8 ms so with this 16 ms window we
    // have 0 ... 3 samples. The remaining jitter is ironed by the following IIR
    // lowpass filter
    const double m_dMouseSampeIntervall = 0.016;
    const int callsPerDt = ceil(m_dMouseSampeIntervall/dt);
    double scratchPosition = 0;
    m_dMouseSampeTime += dt;
    if (m_dMouseSampeTime >= m_dMouseSampeIntervall || !m_bScratching) {
        scratchPosition = m_pScratchPosition->get();
        m_dMouseSampeTime = 0;
    }

    // Tweak PD controller for different latencies
    double p = 0.3;
    double d = p/-2;
    double f = 0.4;
    if (dt > m_dMouseSampeIntervall * 2) {
        f = 1;
    }
    m_pVelocityController->setPD(p, d);
    m_pRateIIFilter->setFactor(f);
    //m_pVelocityController->setPID(_p, _i, _d);
    //m_pMouseRateIIFilter->setFactor(_f);

    if (m_bScratching) {
        if (m_bEnableInertia) {
            // If we got here then we're not scratching and we're in inertia
            // mode. Take the previous rate that was set and apply a
            // deceleration.

            // If we're playing, then do not decay rate below 1. If we're not playing,
            // then we want to decay all the way down to below 0.01
            double decayThreshold = fabs(releaseRate);
            if (decayThreshold < MIN_SEEK_SPEED) {
                decayThreshold = MIN_SEEK_SPEED;
            }

            // Max velocity we would like to stop in a given time period.
            const double kMaxVelocity = 100;
            // Seconds to stop a throw at the max velocity.
            const double kTimeToStop = 1.0;

            // We calculate the exponential decay constant based on the above
            // constants. Roughly we backsolve what the decay should be if we want to
            // stop a throw of max velocity kMaxVelocity in kTimeToStop seconds. Here is
            // the derivation:
            // kMaxVelocity * alpha ^ (# callbacks to stop in) = decayThreshold
            // # callbacks = kTimeToStop / dt
            // alpha = (decayThreshold / kMaxVelocity) ^ (dt / kTimeToStop)
            const double kExponentialDecay = pow(decayThreshold / kMaxVelocity, dt / kTimeToStop);

            m_dRate *= kExponentialDecay;

            // If the rate has decayed below the threshold, or scratching is
            // re-enabled then leave inertia mode.
            if (fabs(m_dRate) < decayThreshold || scratchEnable) {
                m_bEnableInertia = false;
                m_bScratching = false;
            }
            //qDebug() << m_dRate << kExponentialDecay << dt;
        } else if (scratchEnable) {
            // If we're scratching, clear the inertia flag. This case should
            // have been caught by the 'enable' case below, but just to make
            // sure.
            m_bEnableInertia = false;

            // Measure the total distance traveled since last frame and add
            // it to the running total. This is required to scratch within loop
            // boundaries. And normalize to one buffer
            m_dPositionDeltaSum += (currentSample - m_dLastPlaypos) /
                    (iBufferSize * baserate);

            // Continue with the last rate if we do not have a new
            // Mouse position
            if (m_dMouseSampeTime ==  0) {

                // Set the scratch target to the current set position
                // and normalize to one buffer
                double targetDelta = (scratchPosition - m_dStartScratchPosition) /
                        (iBufferSize * baserate);

                bool calcRate = true;

                if (m_dTargetDelta == targetDelta) {
                    // we get here, if the next mouse position is delayed
                    // the mouse is stopped or moves slow. Since we don't know the case
                    // we assume delayed mouse updates for 40 ms
                    m_dMoveDelay += dt * callsPerDt;
                    if (m_dMoveDelay < 0.04) {
                        // Assume a missing Mouse Update and continue with the
                        // previously calculated rate.
                        calcRate = false;
                    } else {
                        // Mouse has stopped
                        m_pVelocityController->setPD(p, 0);
                        if (targetDelta == 0) {
                            // Mouse was not moved at all
                            // Stop immediately by restarting the controller
                            // in stopped mode
                            m_pVelocityController->reset(0);
                            m_pRateIIFilter->reset(0);
                            m_dPositionDeltaSum = 0;
                        }
                    }
                } else {
                    m_dMoveDelay = 0;
                    m_dTargetDelta = targetDelta;
                }

                if (calcRate) {
                    double ctrlError = m_pRateIIFilter->filter(targetDelta - m_dPositionDeltaSum);
                    m_dRate = m_pVelocityController->observation(ctrlError);
                    m_dRate /= ceil(m_dMouseSampeIntervall/dt);
                    // Note: The following SoundTouch changes the also rate by a ramp
                    // This looks like average of the new and the old rate independent
                    // from dt. Ramping is disabled when direction changes or rate = 0;
                    // (determined experimentally)
                    if (fabs(m_dRate) < MIN_SEEK_SPEED) {
                        // we cannot get closer
                        m_dRate = 0;
                    }
                }

                //qDebug() << m_dRate << targetDelta << m_dPositionDeltaSum << dt;
            }
        } else {
            // We were previously in scratch mode and are no longer in scratch
            // mode. Disable everything, or optionally enable inertia mode if
            // the previous rate was high enough to count as a 'throw'

            // The rate threshold above which disabling position scratching will enable
            // an 'inertia' mode.
            const double kThrowThreshold = 2.5;

            if (fabs(m_dRate) > kThrowThreshold) {
                m_bEnableInertia = true;
            } else {
                m_bScratching = false;
            }
            //qDebug() << "disable";
        }
    } else if (scratchEnable) {
            // We were not previously in scratch mode but now are in scratch
            // mode. Enable scratching.
            m_bScratching = true;
            m_bEnableInertia = false;
            m_dMoveDelay = 0;
            // Set up initial values, in a way that the system is settled
            m_dRate = releaseRate;
            m_dPositionDeltaSum = -(releaseRate / p) * callsPerDt; // Set to the remaining error of a p controller
            m_pVelocityController->reset(-m_dPositionDeltaSum);
            m_pRateIIFilter->reset(-m_dPositionDeltaSum);
            m_dStartScratchPosition = scratchPosition;
            //qDebug() << "scratchEnable()" << currentSample;
    }
    m_dLastPlaypos = currentSample;
}

bool PositionScratchController::isEnabled() {
    // return true only if m_dRate is valid.
    return m_bScratching;
}

double PositionScratchController::getRate() {
    return m_dRate;
}

void PositionScratchController::notifySeek(double currentSample) {
    // scratching continues after seek due to calculating the relative distance traveled
    // in m_dPositionDeltaSum
    m_dLastPlaypos = currentSample;
}