Audacity 3.2.0
ScrubState.cpp
Go to the documentation of this file.
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
15namespace {
16struct ScrubQueue : NonInterferingBase
17{
19
21
22 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
36 void Update(double end, const ScrubbingOptions &options)
37 {
38 // Called by another thread
39 mMessage.Write({ end, options });
40 }
41
42 void Get(sampleCount &startSample, sampleCount &endSample,
43 sampleCount inDuration, sampleCount &duration)
44 {
45 // Called by the thread that calls AudioIO::SequenceBufferExchange
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
110 void Stop()
111 {
112 mStopped.store( true, std::memory_order_relaxed );
113 mStarted = false;
114 }
115
116 // Should make mS1 atomic?
117 double LastTrackTime() const
118 {
119 // Needed by the main thread sometimes
120 return mData.mS1.as_double() / mRate;
121 }
122
124
125 bool Started() const { return mStarted; }
126
127private:
128 struct Data
129 {
131 : mS0(0)
132 , mS1(0)
133 , mGoal(0)
134 , mDuration(0)
135 , mSilence(0)
136 {}
137
138 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
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.count() * 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 },
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
273 };
274
275 double mStartTime{};
276 bool mStarted{ false };
277 std::atomic<bool> mStopped { false };
279 double mRate{};
280 struct Message {
281 Message() = default;
282 Message(const Message&) = default;
283 double end;
285 };
287 sampleCount mAccumulatedSeekDuration{};
288};
289
290ScrubQueue ScrubQueue::Instance;
291}
292
294 const ScrubbingOptions &options)
295 : mOptions{ options }
296{}
297
299
301 double rate )
302{
303 PlaybackPolicy::Initialize(schedule, rate);
306 mScrubSpeed = 0;
307 mSilentScrub = mReplenish = false;
309 ScrubQueue::Instance.Init( schedule.mT0, rate, mOptions );
310}
311
313{
314 ScrubQueue::Instance.Stop();
315}
316
318{
319 return Mixer::WarpOptions{
322}
323
326{
327 using namespace std::chrono;
328 return {
329 // For useful scrubbing, we can't run too far ahead without checking
330 // mouse input, so make fillings more and shorter.
331 // Specify a very short minimum batch for non-seek scrubbing, to allow
332 // more frequent polling of the mouse
334
335 // Specify enough playback RingBuffer latency so we can refill
336 // once every seek stutter without falling behind the demand.
337 // (Scrub might switch in and out of seeking with left mouse
338 // presses in the ruler)
340
341 // Same as for default policy
342 10.0s
343 };
344}
345
347{
348 // While scrubbing, ignore seek requests
349 return false;
350}
351
353 PlaybackSchedule &schedule, unsigned long )
354{
355 return false;
356}
357
358std::chrono::milliseconds
360{
361 return ScrubPollInterval;
362}
363
365 PlaybackSchedule &, size_t available)
366{
367 if (mReplenish)
368 return { available, 0, 0 };
369
370 auto gAudioIO = AudioIO::Get();
371
372 // How many samples to produce for each channel.
373 auto frames = available;
374 auto toProduce = frames;
375
376 // scrubbing and play-at-speed are not limited by the real time
377 // and length accumulators
378 toProduce =
379 frames = limitSampleBufferSize(frames, mScrubDuration);
380
381 if (mSilentScrub)
382 toProduce = 0;
383
384 mScrubDuration -= frames;
385 wxASSERT(mScrubDuration >= 0);
386
388 if (mScrubDuration <= 0) {
389 mReplenish = true;
390 auto oldEndSample = mEndSample;
391 mOldEndTime = oldEndSample.as_long_long() / mRate;
392 ScrubQueue::Instance.Get(
395 if(mScrubDuration >= 0 && oldEndSample != mStartSample)
396 mUntilDiscontinuity = frames;
397 }
398
399 return { available, frames, toProduce };
400}
401
403 PlaybackSchedule &schedule, double trackTime, size_t nSamples )
404{
405 auto realDuration = nSamples / mRate;
406 auto result = trackTime + realDuration * mScrubSpeed;
407 bool discontinuity = nSamples > 0 &&
410 if (discontinuity)
411 return { mOldEndTime, mNewStartTime };
412 else
413 return { result, result };
414}
415
417 PlaybackSchedule &schedule, const Mixers &playbackMixers,
418 size_t frames, size_t available)
419{
420 auto gAudioIO = AudioIO::Get();
421
422 if (available > 0 && mReplenish)
423 {
424 mReplenish = false;
425 if (mScrubDuration < 0)
426 {
427 // Can't play anything
428 // Stop even if we don't fill up available
429 mScrubDuration = 0;
430
431 // Force stop of filling of buffers
432 return true;
433 }
434 else
435 {
437 double startTime, endTime;
438 startTime = mStartSample.as_double() / mRate;
439 endTime = mEndSample.as_double() / mRate;
440 auto diff = (mEndSample - mStartSample).as_long_long();
441 if (mScrubDuration == 0)
442 mScrubSpeed = 0;
443 else
445 double(diff) / mScrubDuration.as_double();
446 if (!mSilentScrub)
447 {
448 for (auto &pMixer : playbackMixers) {
450 pMixer->SetSpeedForKeyboardScrubbing(mScrubSpeed, startTime);
451 else
452 pMixer->SetTimesAndSpeed(
453 startTime, endTime, fabs( mScrubSpeed ));
454 }
455 }
456 }
457 }
458
459 return false;
460}
461
463 (double endTimeOrSpeed, const ScrubbingOptions &options)
464{
465 auto &queue = ScrubQueue::Instance;
466 queue.Update(endTimeOrSpeed, options);
467}
468
470{
471 auto &queue = ScrubQueue::Instance;
472 queue.Stop();
473}
474
475// Only for DRAG_SCRUB
477{
478 auto &queue = ScrubQueue::Instance;
479 return queue.LastTrackTime();
480}
481
483{
484 auto gAudioIO = AudioIOBase::Get();
485 auto &queue = ScrubQueue::Instance;
486 return gAudioIO->IsBusy() && queue.Started();
487}
int min(int a, int b)
static ProjectFileIORegistry::AttributeWriterEntry entry
size_t limitSampleBufferSize(size_t bufferSize, sampleCount limit)
Definition: SampleCount.cpp:22
static constexpr auto ScrubPollInterval
Definition: ScrubState.h:109
static AudioIOBase * Get()
Definition: AudioIOBase.cpp:94
static AudioIO * Get()
Definition: AudioIO.cpp:126
Communicate data atomically from one writer thread to one reader.
Definition: MessageBuffer.h:23
std::vector< std::unique_ptr< Mixer > > Mixers
virtual void Initialize(PlaybackSchedule &schedule, double rate)
Called before starting an audio stream.
const ScrubbingOptions mOptions
Definition: ScrubState.h:88
std::chrono::milliseconds SleepInterval(PlaybackSchedule &) override
How long to wait between calls to AudioIO::SequenceBufferExchange.
Definition: ScrubState.cpp:359
BufferTimes SuggestedBufferTimes(PlaybackSchedule &schedule) override
Provide hints for construction of playback RingBuffer objects.
Definition: ScrubState.cpp:325
void Finalize(PlaybackSchedule &schedule) override
Called after stopping of an audio stream or an unsuccessful start.
Definition: ScrubState.cpp:312
bool Done(PlaybackSchedule &schedule, unsigned long) override
Returns true if schedule.GetSequenceTime() has reached the end of playback.
Definition: ScrubState.cpp:352
bool RepositionPlayback(PlaybackSchedule &schedule, const Mixers &playbackMixers, size_t frames, size_t available) override
AudioIO::FillPlayBuffers calls this to update its cursors into tracks for changes of position or spee...
Definition: ScrubState.cpp:416
sampleCount mEndSample
Definition: ScrubState.h:81
~ScrubbingPlaybackPolicy() override
sampleCount mStartSample
Definition: ScrubState.h:81
sampleCount mScrubDuration
Definition: ScrubState.h:81
PlaybackSlice GetPlaybackSlice(PlaybackSchedule &schedule, size_t available) override
Choose length of one fetch of samples from tracks in a call to AudioIO::FillPlayBuffers.
Definition: ScrubState.cpp:364
ScrubbingPlaybackPolicy(const ScrubbingOptions &)
Definition: ScrubState.cpp:293
Mixer::WarpOptions MixerWarpOptions(PlaybackSchedule &schedule) override
Options to use when constructing mixers for each playback track.
Definition: ScrubState.cpp:317
bool AllowSeek(PlaybackSchedule &) override
Whether repositioning commands are allowed during playback.
Definition: ScrubState.cpp:346
std::pair< double, double > AdvancedTrackTime(PlaybackSchedule &schedule, double trackTime, size_t nSamples) override
Compute a new point in a track's timeline from an old point and a real duration.
Definition: ScrubState.cpp:402
void Initialize(PlaybackSchedule &schedule, double rate) override
Called before starting an audio stream.
Definition: ScrubState.cpp:300
Positions or offsets within audio files need a wide type.
Definition: SampleCount.h:19
long long as_long_long() const
Definition: SampleCount.h:48
double as_double() const
Definition: SampleCount.h:46
#define lrint(dbl)
Definition: float_cast.h:169
const char * end(const char *str) noexcept
Definition: StringUtils.h:106
Immutable structure is an argument to Mixer's constructor.
Definition: MixerOptions.h:56
Times are in seconds.
double mT0
Playback starts at offset of mT0, which is measured in seconds.
Describes an amount of contiguous (but maybe time-warped) data to be extracted from tracks to play.
static void StopScrub()
Definition: ScrubState.cpp:469
static bool IsScrubbing()
Definition: ScrubState.cpp:482
static double GetLastScrubTime()
return the ending time of the last scrub interval.
Definition: ScrubState.cpp:476
static void UpdateScrub(double endTimeOrSpeed, const ScrubbingOptions &options)
Notify scrubbing engine of desired position or speed. If options.adjustStart is true,...
Definition: ScrubState.cpp:463
PlaybackPolicy::Duration minStutterTime
Definition: ScrubState.h:40
bool isKeyboardScrubbing
Definition: ScrubState.h:28
PlaybackPolicy::Duration delay
Definition: ScrubState.h:30
static double MinAllowedScrubSpeed()
Definition: ScrubState.h:44
static double MaxAllowedScrubSpeed()
Definition: ScrubState.h:42
double initSpeed
Definition: ScrubState.h:33
bool Init(Data &rPrevious, sampleCount s0, sampleCount s1, sampleCount duration, const ScrubbingOptions &options, double rate)
Definition: ScrubState.cpp:138
void Init(double t0, double rate, const ScrubbingOptions &options)
Definition: ScrubState.cpp:22
void Get(sampleCount &startSample, sampleCount &endSample, sampleCount inDuration, sampleCount &duration)
Definition: ScrubState.cpp:42
void Update(double end, const ScrubbingOptions &options)
Definition: ScrubState.cpp:36