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