1 /*!********************************************************************
2 
3  Audacity: A Digital Audio Editor
4 
5  @file ScrubState.cpp
6 
7  Paul Licameli split from AudioIO.cpp
8 
9  **********************************************************************/
10 
11 #include "ScrubState.h"
12 #include "AudioIO.h"
13 #include "Mix.h"
14 
15 namespace {
16 struct ScrubQueue : NonInterferingBase
17 {
18    static ScrubQueue Instance;
19 
ScrubQueue__anon4985b97a0111::ScrubQueue20    ScrubQueue() {}
21 
Init__anon4985b97a0111::ScrubQueue22    void Init(double t0,
23               double rate,
24               const ScrubbingOptions &options)
25    {
26       mRate = rate;
27       mStartTime = t0;
28       const double t1 = options.bySpeed ? options.initSpeed : t0;
29       Update( t1, options );
30 
31       mStarted = false;
32       mStopped = false;
33       mAccumulatedSeekDuration = 0;
34    }
35 
Update__anon4985b97a0111::ScrubQueue36    void Update(double end, const ScrubbingOptions &options)
37    {
38       // Called by another thread
39       mMessage.Write({ end, options });
40    }
41 
Get__anon4985b97a0111::ScrubQueue42    void Get(sampleCount &startSample, sampleCount &endSample,
43          sampleCount inDuration, sampleCount &duration)
44    {
45       // Called by the thread that calls AudioIO::TrackBufferExchange
46       startSample = endSample = duration = -1LL;
47       sampleCount s0Init;
48 
49       Message message( mMessage.Read() );
50       if ( !mStarted ) {
51          s0Init = llrint( mRate *
52             std::max( message.options.minTime,
53                std::min( message.options.maxTime, mStartTime ) ) );
54 
55          // Make some initial silence. This is not needed in the case of
56          // keyboard scrubbing or play-at-speed, because the initial speed
57          // is known when this function is called the first time.
58          if ( !(message.options.isKeyboardScrubbing) ) {
59             mData.mS0 = mData.mS1 = s0Init;
60             mData.mGoal = -1;
61             mData.mDuration = duration = inDuration;
62             mData.mSilence = 0;
63          }
64       }
65 
66       if (mStarted || message.options.isKeyboardScrubbing) {
67          Data newData;
68          inDuration += mAccumulatedSeekDuration;
69 
70          // If already started, use the previous end as NEW start.
71          const auto s0 = mStarted ? mData.mS1 : s0Init;
72          const sampleCount s1 ( message.options.bySpeed
73             ? s0.as_double() +
74                lrint(inDuration.as_double() * message.end) // end is a speed
75             : lrint(message.end * mRate)            // end is a time
76          );
77          auto success =
78             newData.Init(mData, s0, s1, inDuration, message.options, mRate);
79          if (success)
80             mAccumulatedSeekDuration = 0;
81          else {
82             mAccumulatedSeekDuration += inDuration;
83             return;
84          }
85          mData = newData;
86       };
87 
88       mStarted = true;
89 
90       Data &entry = mData;
91       if (  mStopped.load( std::memory_order_relaxed ) ) {
92          // We got the shut-down signal, or we discarded all the work.
93          // Output the -1 values.
94       }
95       else if (entry.mDuration > 0) {
96          // First use of the entry
97          startSample = entry.mS0;
98          endSample = entry.mS1;
99          duration = entry.mDuration;
100          entry.mDuration = 0;
101       }
102       else if (entry.mSilence > 0) {
103          // Second use of the entry
104          startSample = endSample = entry.mS1;
105          duration = entry.mSilence;
106          entry.mSilence = 0;
107       }
108    }
109 
Stop__anon4985b97a0111::ScrubQueue110    void Stop()
111    {
112       mStopped.store( true, std::memory_order_relaxed );
113       mStarted = false;
114    }
115 
116    // Should make mS1 atomic?
LastTrackTime__anon4985b97a0111::ScrubQueue117    double LastTrackTime() const
118    {
119       // Needed by the main thread sometimes
120       return mData.mS1.as_double() / mRate;
121    }
122 
~ScrubQueue__anon4985b97a0111::ScrubQueue123    ~ScrubQueue() {}
124 
Started__anon4985b97a0111::ScrubQueue125    bool Started() const { return mStarted; }
126 
127 private:
128    struct Data
129    {
Data__anon4985b97a0111::ScrubQueue::Data130       Data()
131          : mS0(0)
132          , mS1(0)
133          , mGoal(0)
134          , mDuration(0)
135          , mSilence(0)
136       {}
137 
Init__anon4985b97a0111::ScrubQueue::Data138       bool Init(Data &rPrevious, sampleCount s0, sampleCount s1,
139          sampleCount duration,
140          const ScrubbingOptions &options, double rate)
141       {
142          auto previous = &rPrevious;
143          auto origDuration = duration;
144          mSilence = 0;
145 
146          const bool &adjustStart = options.adjustStart;
147 
148          wxASSERT(duration > 0);
149          double speed =
150             (std::abs((s1 - s0).as_long_long())) / duration.as_double();
151          bool adjustedSpeed = false;
152 
153          auto minSpeed = std::min(options.minSpeed, options.maxSpeed);
154          wxASSERT(minSpeed == options.minSpeed);
155 
156          // May change the requested speed and duration
157          if (!adjustStart && speed > options.maxSpeed)
158          {
159             // Reduce speed to the maximum selected in the user interface.
160             speed = options.maxSpeed;
161             mGoal = s1;
162             adjustedSpeed = true;
163          }
164          else if (!adjustStart &&
165             previous->mGoal >= 0 &&
166             previous->mGoal == s1)
167          {
168             // In case the mouse has not moved, and playback
169             // is catching up to the mouse at maximum speed,
170             // continue at no less than maximum.  (Without this
171             // the final catch-up can make a slow scrub interval
172             // that drops the pitch and sounds wrong.)
173             minSpeed = options.maxSpeed;
174             mGoal = s1;
175             adjustedSpeed = true;
176          }
177          else
178             mGoal = -1;
179 
180          if (speed < minSpeed) {
181             if (s0 != s1 && adjustStart)
182                // Do not trim the duration.
183                ;
184             else
185                // Trim the duration.
186                duration =
187                   std::max(0L, lrint(speed * duration.as_double() / minSpeed));
188 
189             speed = minSpeed;
190             adjustedSpeed = true;
191          }
192 
193          if (speed < ScrubbingOptions::MinAllowedScrubSpeed()) {
194             // Mixers were set up to go only so slowly, not slower.
195             // This will put a request for some silence in the work queue.
196             adjustedSpeed = true;
197             speed = 0.0;
198          }
199 
200          // May change s1 or s0 to match speed change or stay in bounds of the project
201 
202          if (adjustedSpeed && !adjustStart)
203          {
204             // adjust s1
205             const sampleCount diff = lrint(speed * duration.as_double());
206             if (s0 < s1)
207                s1 = s0 + diff;
208             else
209                s1 = s0 - diff;
210          }
211 
212          bool silent = false;
213 
214          // Adjust s1 (again), and duration, if s1 is out of bounds,
215          // or abandon if a stutter is too short.
216          // (Assume s0 is in bounds, because it equals the last scrub's s1 which was checked.)
217          if (s1 != s0)
218          {
219             // When playback follows a fast mouse movement by "stuttering"
220             // at maximum playback, don't make stutters too short to be useful.
221             if (options.adjustStart &&
222                 duration < llrint( options.minStutterTime * rate ) )
223                return false;
224 
225             sampleCount minSample { llrint(options.minTime * rate) };
226             sampleCount maxSample { llrint(options.maxTime * rate) };
227             auto newDuration = duration;
228             const auto newS1 = std::max(minSample, std::min(maxSample, s1));
229             if(s1 != newS1)
230                newDuration = std::max( sampleCount{ 0 },
231                   sampleCount(
232                      duration.as_double() * (newS1 - s0).as_double() /
233                         (s1 - s0).as_double()
234                   )
235                );
236             if (newDuration == 0) {
237                // A silent scrub with s0 == s1
238                silent = true;
239                s1 = s0;
240             }
241             else if (s1 != newS1) {
242                // Shorten
243                duration = newDuration;
244                s1 = newS1;
245             }
246          }
247 
248          if (adjustStart && !silent)
249          {
250             // Limit diff because this is seeking.
251             const sampleCount diff =
252                lrint(std::min(options.maxSpeed, speed) * duration.as_double());
253             if (s0 < s1)
254                s0 = s1 - diff;
255             else
256                s0 = s1 + diff;
257          }
258 
259          mS0 = s0;
260          mS1 = s1;
261          mDuration = duration;
262          if (duration < origDuration)
263             mSilence = origDuration - duration;
264 
265          return true;
266       }
267 
268       sampleCount mS0;
269       sampleCount mS1;
270       sampleCount mGoal;
271       sampleCount mDuration;
272       sampleCount mSilence;
273    };
274 
275    double mStartTime{};
276    bool mStarted{ false };
277    std::atomic<bool> mStopped { false };
278    Data mData;
279    double mRate{};
280    struct Message {
281       Message() = default;
282       Message(const Message&) = default;
283       double end;
284       ScrubbingOptions options;
285    };
286    MessageBuffer<Message> mMessage;
287    sampleCount mAccumulatedSeekDuration{};
288 };
289 
290 ScrubQueue ScrubQueue::Instance;
291 }
292 
ScrubbingPlaybackPolicy(const ScrubbingOptions & options)293 ScrubbingPlaybackPolicy::ScrubbingPlaybackPolicy(
294    const ScrubbingOptions &options)
295    : mOptions{ options }
296 {}
297 
298 ScrubbingPlaybackPolicy::~ScrubbingPlaybackPolicy() = default;
299 
Initialize(PlaybackSchedule & schedule,double rate)300 void ScrubbingPlaybackPolicy::Initialize( PlaybackSchedule &schedule,
301    double rate )
302 {
303    PlaybackPolicy::Initialize(schedule, rate);
304    mScrubDuration = mStartSample = mEndSample = 0;
305    mOldEndTime = mNewStartTime = 0;
306    mScrubSpeed = 0;
307    mSilentScrub = mReplenish = false;
308    mUntilDiscontinuity = 0;
309    ScrubQueue::Instance.Init( schedule.mT0, rate, mOptions );
310 }
311 
Finalize(PlaybackSchedule &)312 void ScrubbingPlaybackPolicy::Finalize(  PlaybackSchedule & )
313 {
314    ScrubQueue::Instance.Stop();
315 }
316 
MixerWarpOptions(PlaybackSchedule &)317 Mixer::WarpOptions ScrubbingPlaybackPolicy::MixerWarpOptions(PlaybackSchedule &)
318 {
319    return Mixer::WarpOptions{
320       ScrubbingOptions::MinAllowedScrubSpeed(),
321       ScrubbingOptions::MaxAllowedScrubSpeed() };
322 }
323 
324 PlaybackPolicy::BufferTimes
SuggestedBufferTimes(PlaybackSchedule &)325 ScrubbingPlaybackPolicy::SuggestedBufferTimes(PlaybackSchedule &)
326 {
327    return {
328       // For useful scrubbing, we can't run too far ahead without checking
329       // mouse input, so make fillings more and shorter.
330       // Specify a very short minimum batch for non-seek scrubbing, to allow
331       // more frequent polling of the mouse
332       mOptions.delay,
333 
334       // Specify enough playback RingBuffer latency so we can refill
335       // once every seek stutter without falling behind the demand.
336       // (Scrub might switch in and out of seeking with left mouse
337       // presses in the ruler)
338       2 * mOptions.minStutterTime,
339 
340       // Same as for default policy
341       10.0
342    };
343 }
344 
AllowSeek(PlaybackSchedule &)345 bool ScrubbingPlaybackPolicy::AllowSeek( PlaybackSchedule & )
346 {
347    // While scrubbing, ignore seek requests
348    return false;
349 }
350 
Done(PlaybackSchedule & schedule,unsigned long)351 bool ScrubbingPlaybackPolicy::Done(
352    PlaybackSchedule &schedule, unsigned long )
353 {
354    return false;
355 }
356 
357 std::chrono::milliseconds
SleepInterval(PlaybackSchedule &)358 ScrubbingPlaybackPolicy::SleepInterval( PlaybackSchedule & )
359 {
360    return std::chrono::milliseconds{ ScrubPollInterval_ms };
361 }
362 
GetPlaybackSlice(PlaybackSchedule &,size_t available)363 PlaybackSlice ScrubbingPlaybackPolicy::GetPlaybackSlice(
364    PlaybackSchedule &, size_t available)
365 {
366    if (mReplenish)
367       return { available, 0, 0 };
368 
369    auto gAudioIO = AudioIO::Get();
370 
371    // How many samples to produce for each channel.
372    auto frames = available;
373    auto toProduce = frames;
374 
375    // scrubbing and play-at-speed are not limited by the real time
376    // and length accumulators
377    toProduce =
378       frames = limitSampleBufferSize(frames, mScrubDuration);
379 
380    if (mSilentScrub)
381       toProduce = 0;
382 
383    mScrubDuration -= frames;
384    wxASSERT(mScrubDuration >= 0);
385 
386    mUntilDiscontinuity = 0;
387    if (mScrubDuration <= 0) {
388       mReplenish = true;
389       auto oldEndSample = mEndSample;
390       mOldEndTime = oldEndSample.as_long_long() / mRate;
391       ScrubQueue::Instance.Get(
392          mStartSample, mEndSample, available, mScrubDuration);
393       mNewStartTime = mStartSample.as_long_long() / mRate;
394       if(mScrubDuration >= 0 && oldEndSample != mStartSample)
395          mUntilDiscontinuity = frames;
396    }
397 
398    return { available, frames, toProduce };
399 }
400 
AdvancedTrackTime(PlaybackSchedule & schedule,double trackTime,size_t nSamples)401 std::pair<double, double> ScrubbingPlaybackPolicy::AdvancedTrackTime(
402    PlaybackSchedule &schedule, double trackTime, size_t nSamples )
403 {
404    auto realDuration = nSamples / mRate;
405    auto result = trackTime + realDuration * mScrubSpeed;
406    bool discontinuity = nSamples > 0 &&
407       mUntilDiscontinuity > 0 &&
408       0 == (mUntilDiscontinuity -= std::min(mUntilDiscontinuity, nSamples));
409    if (discontinuity)
410       return { mOldEndTime, mNewStartTime };
411    else
412       return { result, result };
413 }
414 
RepositionPlayback(PlaybackSchedule & schedule,const Mixers & playbackMixers,size_t frames,size_t available)415 bool ScrubbingPlaybackPolicy::RepositionPlayback(
416    PlaybackSchedule &schedule, const Mixers &playbackMixers,
417    size_t frames, size_t available)
418 {
419    auto gAudioIO = AudioIO::Get();
420 
421    if (available > 0 && mReplenish)
422    {
423       mReplenish = false;
424       if (mScrubDuration < 0)
425       {
426          // Can't play anything
427          // Stop even if we don't fill up available
428          mScrubDuration = 0;
429 
430          // Force stop of filling of buffers
431          return true;
432       }
433       else
434       {
435          mSilentScrub = (mEndSample == mStartSample);
436          double startTime, endTime;
437          startTime = mStartSample.as_double() / mRate;
438          endTime = mEndSample.as_double() / mRate;
439          auto diff = (mEndSample - mStartSample).as_long_long();
440          if (mScrubDuration == 0)
441             mScrubSpeed = 0;
442          else
443             mScrubSpeed =
444                double(diff) / mScrubDuration.as_double();
445          if (!mSilentScrub)
446          {
447             for (auto &pMixer : playbackMixers) {
448                if (mOptions.isKeyboardScrubbing)
449                   pMixer->SetSpeedForKeyboardScrubbing(mScrubSpeed, startTime);
450                else
451                   pMixer->SetTimesAndSpeed(
452                      startTime, endTime, fabs( mScrubSpeed ));
453             }
454          }
455       }
456    }
457 
458    return false;
459 }
460 
UpdateScrub(double endTimeOrSpeed,const ScrubbingOptions & options)461 void ScrubState::UpdateScrub
462    (double endTimeOrSpeed, const ScrubbingOptions &options)
463 {
464    auto &queue = ScrubQueue::Instance;
465    queue.Update(endTimeOrSpeed, options);
466 }
467 
StopScrub()468 void ScrubState::StopScrub()
469 {
470    auto &queue = ScrubQueue::Instance;
471    queue.Stop();
472 }
473 
474 // Only for DRAG_SCRUB
GetLastScrubTime()475 double ScrubState::GetLastScrubTime()
476 {
477    auto &queue = ScrubQueue::Instance;
478    return queue.LastTrackTime();
479 }
480 
IsScrubbing()481 bool ScrubState::IsScrubbing()
482 {
483    auto gAudioIO = AudioIOBase::Get();
484    auto &queue = ScrubQueue::Instance;
485    return gAudioIO->IsBusy() && queue.Started();
486 }
487