1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 PlaybackSchedule.cpp
6
7 Paul Licameli split from AudioIOBase.cpp
8
9 **********************************************************************/
10
11 #include "PlaybackSchedule.h"
12
13 #include "AudioIOBase.h"
14 #include "Envelope.h"
15 #include "Mix.h"
16 #include "Project.h"
17 #include "ProjectAudioIO.h"
18 #include "SampleCount.h"
19 #include "ViewInfo.h" // for PlayRegionEvent
20
21 #include <cmath>
22
23 PlaybackPolicy::~PlaybackPolicy() = default;
24
Initialize(PlaybackSchedule &,double rate)25 void PlaybackPolicy::Initialize( PlaybackSchedule &, double rate )
26 {
27 mRate = rate;
28 }
29
Finalize(PlaybackSchedule &)30 void PlaybackPolicy::Finalize( PlaybackSchedule & ){}
31
MixerWarpOptions(PlaybackSchedule & schedule)32 Mixer::WarpOptions PlaybackPolicy::MixerWarpOptions(PlaybackSchedule &schedule)
33 {
34 return Mixer::WarpOptions{ schedule.mEnvelope };
35 }
36
37 PlaybackPolicy::BufferTimes
SuggestedBufferTimes(PlaybackSchedule &)38 PlaybackPolicy::SuggestedBufferTimes(PlaybackSchedule &)
39 {
40 return { 4.0, 4.0, 10.0 };
41 }
42
AllowSeek(PlaybackSchedule &)43 bool PlaybackPolicy::AllowSeek(PlaybackSchedule &)
44 {
45 return true;
46 }
47
Done(PlaybackSchedule & schedule,unsigned long outputFrames)48 bool PlaybackPolicy::Done( PlaybackSchedule &schedule,
49 unsigned long outputFrames)
50 {
51 // Called from portAudio thread, use GetTrackTime()
52 auto diff = schedule.GetTrackTime() - schedule.mT1;
53 if (schedule.ReversedTime())
54 diff *= -1;
55 return sampleCount(floor(diff * mRate + 0.5)) >= 0 &&
56 // Require also that output frames are all consumed from ring buffer
57 outputFrames == 0;
58 }
59
OffsetTrackTime(PlaybackSchedule & schedule,double offset)60 double PlaybackPolicy::OffsetTrackTime(
61 PlaybackSchedule &schedule, double offset )
62 {
63 const auto time = schedule.GetTrackTime() + offset;
64 schedule.RealTimeInit( time );
65 return time;
66 }
67
SleepInterval(PlaybackSchedule &)68 std::chrono::milliseconds PlaybackPolicy::SleepInterval(PlaybackSchedule &)
69 {
70 using namespace std::chrono;
71 return 10ms;
72 }
73
74 PlaybackSlice
GetPlaybackSlice(PlaybackSchedule & schedule,size_t available)75 PlaybackPolicy::GetPlaybackSlice(PlaybackSchedule &schedule, size_t available)
76 {
77 // How many samples to produce for each channel.
78 const auto realTimeRemaining = schedule.RealTimeRemaining();
79 auto frames = available;
80 auto toProduce = frames;
81 double deltat = frames / mRate;
82
83 if (deltat > realTimeRemaining)
84 {
85 // Produce some extra silence so that the time queue consumer can
86 // satisfy its end condition
87 const double extraRealTime = (TimeQueueGrainSize + 1) / mRate;
88 auto extra = std::min( extraRealTime, deltat - realTimeRemaining );
89 auto realTime = realTimeRemaining + extra;
90 frames = realTime * mRate;
91 toProduce = realTimeRemaining * mRate;
92 schedule.RealTimeAdvance( realTime );
93 }
94 else
95 schedule.RealTimeAdvance( deltat );
96
97 return { available, frames, toProduce };
98 }
99
100 std::pair<double, double>
AdvancedTrackTime(PlaybackSchedule & schedule,double trackTime,size_t nSamples)101 PlaybackPolicy::AdvancedTrackTime( PlaybackSchedule &schedule,
102 double trackTime, size_t nSamples )
103 {
104 auto realDuration = nSamples / mRate;
105 if (schedule.ReversedTime())
106 realDuration *= -1.0;
107
108 if (schedule.mEnvelope)
109 trackTime =
110 schedule.SolveWarpedLength(trackTime, realDuration);
111 else
112 trackTime += realDuration;
113
114 if ( trackTime >= schedule.mT1 )
115 return { schedule.mT1, std::numeric_limits<double>::infinity() };
116 else
117 return { trackTime, trackTime };
118 }
119
RepositionPlayback(PlaybackSchedule &,const Mixers &,size_t,size_t)120 bool PlaybackPolicy::RepositionPlayback(
121 PlaybackSchedule &, const Mixers &, size_t, size_t)
122 {
123 return true;
124 }
125
Looping(const PlaybackSchedule &) const126 bool PlaybackPolicy::Looping(const PlaybackSchedule &) const
127 {
128 return false;
129 }
130
131 namespace {
132 //! The old default playback policy plays once and consumes no messages
133 struct OldDefaultPlaybackPolicy final : PlaybackPolicy {
134 ~OldDefaultPlaybackPolicy() override = default;
135 };
136 }
137
GetPolicy()138 PlaybackPolicy &PlaybackSchedule::GetPolicy()
139 {
140 if (mPolicyValid.load(std::memory_order_acquire) && mpPlaybackPolicy)
141 return *mpPlaybackPolicy;
142
143 static OldDefaultPlaybackPolicy defaultPolicy;
144 return defaultPolicy;
145 }
146
GetPolicy() const147 const PlaybackPolicy &PlaybackSchedule::GetPolicy() const
148 {
149 return const_cast<PlaybackSchedule&>(*this).GetPolicy();
150 }
151
NewDefaultPlaybackPolicy(AudacityProject & project,double trackEndTime,double loopEndTime,bool loopEnabled,bool variableSpeed)152 NewDefaultPlaybackPolicy::NewDefaultPlaybackPolicy( AudacityProject &project,
153 double trackEndTime, double loopEndTime,
154 bool loopEnabled, bool variableSpeed )
155 : mProject{ project }
156 , mTrackEndTime{ trackEndTime }
157 , mLoopEndTime{ loopEndTime }
158 , mLoopEnabled{ loopEnabled }
159 , mVariableSpeed{ variableSpeed }
160 {}
161
162 NewDefaultPlaybackPolicy::~NewDefaultPlaybackPolicy() = default;
163
Initialize(PlaybackSchedule & schedule,double rate)164 void NewDefaultPlaybackPolicy::Initialize(
165 PlaybackSchedule &schedule, double rate )
166 {
167 PlaybackPolicy::Initialize(schedule, rate);
168 mLastPlaySpeed = GetPlaySpeed();
169 mMessageChannel.Write( { mLastPlaySpeed,
170 schedule.mT0, mLoopEndTime, mLoopEnabled } );
171
172 ViewInfo::Get( mProject ).playRegion.Bind( EVT_PLAY_REGION_CHANGE,
173 &NewDefaultPlaybackPolicy::OnPlayRegionChange, this);
174 if (mVariableSpeed)
175 mProject.Bind( EVT_PLAY_SPEED_CHANGE,
176 &NewDefaultPlaybackPolicy::OnPlaySpeedChange, this);
177 }
178
MixerWarpOptions(PlaybackSchedule & schedule)179 Mixer::WarpOptions NewDefaultPlaybackPolicy::MixerWarpOptions(
180 PlaybackSchedule &schedule)
181 {
182 if (mVariableSpeed)
183 // Enable variable rate mixing
184 return Mixer::WarpOptions(0.01, 32.0, GetPlaySpeed());
185 else
186 return PlaybackPolicy::MixerWarpOptions(schedule);
187 }
188
189 PlaybackPolicy::BufferTimes
SuggestedBufferTimes(PlaybackSchedule &)190 NewDefaultPlaybackPolicy::SuggestedBufferTimes(PlaybackSchedule &)
191 {
192 // Shorter times than in the default policy so that responses to changes of
193 // loop region or speed slider don't lag too much
194 return { 0.05, 0.05, 0.25 };
195 }
196
RevertToOldDefault(const PlaybackSchedule & schedule) const197 bool NewDefaultPlaybackPolicy::RevertToOldDefault(const PlaybackSchedule &schedule) const
198 {
199 return !mLoopEnabled ||
200 // Even if loop is enabled, ignore it if right of looping region
201 schedule.mTimeQueue.GetLastTime() > mLoopEndTime;
202 }
203
Done(PlaybackSchedule & schedule,unsigned long outputFrames)204 bool NewDefaultPlaybackPolicy::Done(
205 PlaybackSchedule &schedule, unsigned long outputFrames )
206 {
207 if (RevertToOldDefault(schedule)) {
208 auto diff = schedule.GetTrackTime() - schedule.mT1;
209 if (schedule.ReversedTime())
210 diff *= -1;
211 return sampleCount(floor(diff * mRate + 0.5)) >= 0;
212 }
213 return false;
214 }
215
216 PlaybackSlice
GetPlaybackSlice(PlaybackSchedule & schedule,size_t available)217 NewDefaultPlaybackPolicy::GetPlaybackSlice(
218 PlaybackSchedule &schedule, size_t available)
219 {
220 // How many samples to produce for each channel.
221 const auto realTimeRemaining = std::max(0.0, schedule.RealTimeRemaining());
222 mRemaining = realTimeRemaining * mRate / mLastPlaySpeed;
223
224 auto frames = available;
225 auto toProduce = frames;
226 double deltat = (frames / mRate) * mLastPlaySpeed;
227
228 if (deltat > realTimeRemaining) {
229 toProduce = frames = (realTimeRemaining * mRate) / mLastPlaySpeed;
230 auto realTime = realTimeRemaining;
231 double extra = 0;
232 if (RevertToOldDefault(schedule)) {
233 // Produce some extra silence so that the time queue consumer can
234 // satisfy its end condition
235 const double extraRealTime =
236 ((TimeQueueGrainSize + 1) / mRate) * mLastPlaySpeed;
237 auto extra = std::min( extraRealTime, deltat - realTimeRemaining );
238 frames = ((realTimeRemaining + extra) * mRate) / mLastPlaySpeed;
239 }
240 schedule.RealTimeAdvance( realTimeRemaining + extra );
241 }
242 else
243 schedule.RealTimeAdvance( deltat );
244
245 // Don't fall into an infinite loop, if loop-playing a selection
246 // that is so short, it has no samples: detect that case
247 if (frames == 0) {
248 bool progress = (schedule.mWarpedTime != 0.0);
249 if (!progress)
250 // Cause FillPlayBuffers to make progress, filling all available with 0
251 frames = available, toProduce = 0;
252 }
253 return { available, frames, toProduce };
254 }
255
AdvancedTrackTime(PlaybackSchedule & schedule,double trackTime,size_t nSamples)256 std::pair<double, double> NewDefaultPlaybackPolicy::AdvancedTrackTime(
257 PlaybackSchedule &schedule, double trackTime, size_t nSamples )
258 {
259 bool revert = RevertToOldDefault(schedule);
260 if (!mVariableSpeed && revert)
261 return PlaybackPolicy::AdvancedTrackTime(schedule, trackTime, nSamples);
262
263 mRemaining -= std::min(mRemaining, nSamples);
264 if ( mRemaining == 0 && !revert )
265 // Wrap to start
266 return { schedule.mT1, schedule.mT0 };
267
268 // Defense against cases that might cause loops not to terminate
269 if ( fabs(schedule.mT0 - schedule.mT1) < 1e-9 )
270 return {schedule.mT0, schedule.mT0};
271
272 auto realDuration = (nSamples / mRate) * mLastPlaySpeed;
273 if (schedule.ReversedTime())
274 realDuration *= -1.0;
275
276 if (schedule.mEnvelope)
277 trackTime =
278 schedule.SolveWarpedLength(trackTime, realDuration);
279 else
280 trackTime += realDuration;
281
282 return { trackTime, trackTime };
283 }
284
RepositionPlayback(PlaybackSchedule & schedule,const Mixers & playbackMixers,size_t frames,size_t available)285 bool NewDefaultPlaybackPolicy::RepositionPlayback(
286 PlaybackSchedule &schedule, const Mixers &playbackMixers,
287 size_t frames, size_t available )
288 {
289 // This executes in the TrackBufferExchange thread
290 auto data = mMessageChannel.Read();
291
292 bool speedChange = false;
293 if (mVariableSpeed) {
294 speedChange = (mLastPlaySpeed != data.mPlaySpeed);
295 mLastPlaySpeed = data.mPlaySpeed;
296 }
297
298 bool empty = (data.mT0 >= data.mT1);
299 bool kicked = false;
300
301 // Amount in seconds by which right boundary can be moved left of the play
302 // head, yet loop play in progress will still capture the head
303 constexpr auto allowance = 0.5;
304
305 // Looping may become enabled if the main thread said so, but require too
306 // that the loop region is non-empty and the play head is not far to its
307 // right
308 bool loopWasEnabled = !RevertToOldDefault(schedule);
309 mLoopEnabled = data.mLoopEnabled && !empty &&
310 schedule.mTimeQueue.GetLastTime() <= data.mT1 + allowance;
311
312 // Four cases: looping transitions off, or transitions on, or stays on,
313 // or stays off.
314 // Besides which, the variable speed slider may have changed.
315
316 // If looping transitions on, or remains on and the region changed,
317 // adjust the schedule...
318 auto mine = std::tie(schedule.mT0, mLoopEndTime);
319 auto theirs = std::tie(data.mT0, data.mT1);
320 if ( mLoopEnabled ? (mine != theirs) : loopWasEnabled ) {
321 kicked = true;
322 if (!empty) {
323 mine = theirs;
324 schedule.mT1 = data.mT1;
325 }
326 if (!mLoopEnabled)
327 // Continue play to the end
328 schedule.mT1 = std::max(schedule.mT0, mTrackEndTime);
329 schedule.mWarpedLength = schedule.RealDuration(schedule.mT1);
330
331 auto newTime = schedule.mTimeQueue.GetLastTime();
332 #if 0
333 // This would make play jump forward or backward into the adjusted
334 // looping region if not already in it
335 newTime = std::clamp(newTime, schedule.mT0, schedule.mT1);
336 #endif
337
338 if (newTime >= schedule.mT1 && mLoopEnabled)
339 newTime = schedule.mT0;
340
341 // So that the play head will redraw in the right place:
342 schedule.mTimeQueue.SetLastTime(newTime);
343
344 schedule.RealTimeInit(newTime);
345 const auto realTimeRemaining = std::max(0.0, schedule.RealTimeRemaining());
346 mRemaining = realTimeRemaining * mRate / mLastPlaySpeed;
347 }
348 else if (speedChange)
349 // Don't return early
350 kicked = true;
351 else {
352 // ... else the region did not change, or looping is now off, in
353 // which case we have nothing special to do
354 if (RevertToOldDefault(schedule))
355 return PlaybackPolicy::RepositionPlayback( schedule, playbackMixers,
356 frames, available);
357 }
358
359 // msmeyer: If playing looped, check if we are at the end of the buffer
360 // and if yes, restart from the beginning.
361 if (mRemaining <= 0)
362 {
363 // Looping jumps left
364 for (auto &pMixer : playbackMixers)
365 pMixer->SetTimesAndSpeed(
366 schedule.mT0, schedule.mT1, mLastPlaySpeed, true );
367 schedule.RealTimeRestart();
368 }
369 else if (kicked)
370 {
371 // Play bounds need redefinition
372 const auto time = schedule.mTimeQueue.GetLastTime();
373 for (auto &pMixer : playbackMixers) {
374 // So that the mixer will fetch the next samples from the right place:
375 pMixer->SetTimesAndSpeed( time, schedule.mT1, mLastPlaySpeed );
376 pMixer->Reposition(time, true);
377 }
378 }
379 return false;
380 }
381
Looping(const PlaybackSchedule &) const382 bool NewDefaultPlaybackPolicy::Looping( const PlaybackSchedule & ) const
383 {
384 return true;
385 }
386
OnPlayRegionChange(PlayRegionEvent & evt)387 void NewDefaultPlaybackPolicy::OnPlayRegionChange( PlayRegionEvent &evt)
388 {
389 // This executes in the main thread
390 evt.Skip(); // Let other listeners hear the event too
391 WriteMessage();
392 }
393
OnPlaySpeedChange(wxCommandEvent & evt)394 void NewDefaultPlaybackPolicy::OnPlaySpeedChange(wxCommandEvent &evt)
395 {
396 evt.Skip(); // Let other listeners hear the event too
397 WriteMessage();
398 }
399
WriteMessage()400 void NewDefaultPlaybackPolicy::WriteMessage()
401 {
402 const auto ®ion = ViewInfo::Get( mProject ).playRegion;
403 mMessageChannel.Write( { GetPlaySpeed(),
404 region.GetStart(), region.GetEnd(), region.Active()
405 } );
406 }
407
GetPlaySpeed()408 double NewDefaultPlaybackPolicy::GetPlaySpeed()
409 {
410 return mVariableSpeed
411 ? ProjectAudioIO::Get(mProject).GetPlaySpeed()
412 : 1.0;
413 }
414
Init(const double t0,const double t1,const AudioIOStartStreamOptions & options,const RecordingSchedule * pRecordingSchedule)415 void PlaybackSchedule::Init(
416 const double t0, const double t1,
417 const AudioIOStartStreamOptions &options,
418 const RecordingSchedule *pRecordingSchedule )
419 {
420 mpPlaybackPolicy.reset();
421
422 if ( pRecordingSchedule )
423 // It does not make sense to apply the time warp during overdub recording,
424 // which defeats the purpose of making the recording synchronized with
425 // the existing audio. (Unless we figured out the inverse warp of the
426 // captured samples in real time.)
427 // So just quietly ignore the time track.
428 mEnvelope = nullptr;
429 else
430 mEnvelope = options.envelope;
431
432 mT0 = t0;
433 if (pRecordingSchedule)
434 mT0 -= pRecordingSchedule->mPreRoll;
435
436 mT1 = t1;
437 if (pRecordingSchedule)
438 // adjust mT1 so that we don't give paComplete too soon to fill up the
439 // desired length of recording
440 mT1 -= pRecordingSchedule->mLatencyCorrection;
441
442 // Main thread's initialization of mTime
443 SetTrackTime( mT0 );
444
445 if (options.policyFactory)
446 mpPlaybackPolicy = options.policyFactory(options);
447
448 mWarpedTime = 0.0;
449 mWarpedLength = RealDuration(mT1);
450
451 mPolicyValid.store(true, std::memory_order_release);
452 }
453
ComputeWarpedLength(double t0,double t1) const454 double PlaybackSchedule::ComputeWarpedLength(double t0, double t1) const
455 {
456 if (mEnvelope)
457 return mEnvelope->IntegralOfInverse(t0, t1);
458 else
459 return t1 - t0;
460 }
461
SolveWarpedLength(double t0,double length) const462 double PlaybackSchedule::SolveWarpedLength(double t0, double length) const
463 {
464 if (mEnvelope)
465 return mEnvelope->SolveIntegralOfInverse(t0, length);
466 else
467 return t0 + length;
468 }
469
RealDuration(double trackTime1) const470 double PlaybackSchedule::RealDuration(double trackTime1) const
471 {
472 return fabs(RealDurationSigned(trackTime1));
473 }
474
RealDurationSigned(double trackTime1) const475 double PlaybackSchedule::RealDurationSigned(double trackTime1) const
476 {
477 return ComputeWarpedLength(mT0, trackTime1);
478 }
479
RealTimeRemaining() const480 double PlaybackSchedule::RealTimeRemaining() const
481 {
482 return mWarpedLength - mWarpedTime;
483 }
484
RealTimeAdvance(double increment)485 void PlaybackSchedule::RealTimeAdvance( double increment )
486 {
487 mWarpedTime += increment;
488 }
489
RealTimeInit(double trackTime)490 void PlaybackSchedule::RealTimeInit( double trackTime )
491 {
492 mWarpedTime = RealDurationSigned( trackTime );
493 }
494
RealTimeRestart()495 void PlaybackSchedule::RealTimeRestart()
496 {
497 mWarpedTime = 0;
498 }
499
ToConsume() const500 double RecordingSchedule::ToConsume() const
501 {
502 return mDuration - Consumed();
503 }
504
Consumed() const505 double RecordingSchedule::Consumed() const
506 {
507 return std::max( 0.0, mPosition + TotalCorrection() );
508 }
509
ToDiscard() const510 double RecordingSchedule::ToDiscard() const
511 {
512 return std::max(0.0, -( mPosition + TotalCorrection() ) );
513 }
514
Clear()515 void PlaybackSchedule::TimeQueue::Clear()
516 {
517 mData = Records{};
518 mHead = {};
519 mTail = {};
520 }
521
Resize(size_t size)522 void PlaybackSchedule::TimeQueue::Resize(size_t size)
523 {
524 mData.resize(size);
525 }
526
Producer(PlaybackSchedule & schedule,size_t nSamples)527 void PlaybackSchedule::TimeQueue::Producer(
528 PlaybackSchedule &schedule, size_t nSamples )
529 {
530 auto &policy = schedule.GetPolicy();
531
532 if ( mData.empty() )
533 // Recording only. Don't fill the queue.
534 return;
535
536 // Don't check available space: assume it is enough because of coordination
537 // with RingBuffer.
538 auto index = mTail.mIndex;
539 auto time = mLastTime;
540 auto remainder = mTail.mRemainder;
541 auto space = TimeQueueGrainSize - remainder;
542 const auto size = mData.size();
543
544 while ( nSamples >= space ) {
545 auto times = policy.AdvancedTrackTime( schedule, time, space );
546 time = times.second;
547 if (!std::isfinite(time))
548 time = times.first;
549 index = (index + 1) % size;
550 mData[ index ].timeValue = time;
551 nSamples -= space;
552 remainder = 0;
553 space = TimeQueueGrainSize;
554 }
555
556 // Last odd lot
557 if ( nSamples > 0 ) {
558 auto times = policy.AdvancedTrackTime( schedule, time, nSamples );
559 time = times.second;
560 if (!std::isfinite(time))
561 time = times.first;
562 }
563
564 mLastTime = time;
565 mTail.mRemainder = remainder + nSamples;
566 mTail.mIndex = index;
567 }
568
GetLastTime() const569 double PlaybackSchedule::TimeQueue::GetLastTime() const
570 {
571 return mLastTime;
572 }
573
SetLastTime(double time)574 void PlaybackSchedule::TimeQueue::SetLastTime(double time)
575 {
576 mLastTime = time;
577 }
578
Consumer(size_t nSamples,double rate)579 double PlaybackSchedule::TimeQueue::Consumer( size_t nSamples, double rate )
580 {
581 if ( mData.empty() ) {
582 // Recording only. No scrub or playback time warp. Don't use the queue.
583 return ( mLastTime += nSamples / rate );
584 }
585
586 // Don't check available space: assume it is enough because of coordination
587 // with RingBuffer.
588 auto remainder = mHead.mRemainder;
589 auto space = TimeQueueGrainSize - remainder;
590 const auto size = mData.size();
591 if ( nSamples >= space ) {
592 remainder = 0,
593 mHead.mIndex = (mHead.mIndex + 1) % size,
594 nSamples -= space;
595 if ( nSamples >= TimeQueueGrainSize )
596 mHead.mIndex =
597 (mHead.mIndex + ( nSamples / TimeQueueGrainSize ) ) % size,
598 nSamples %= TimeQueueGrainSize;
599 }
600 mHead.mRemainder = remainder + nSamples;
601 return mData[ mHead.mIndex ].timeValue;
602 }
603
Prime(double time)604 void PlaybackSchedule::TimeQueue::Prime(double time)
605 {
606 mHead = mTail = {};
607 mLastTime = time;
608 if ( !mData.empty() )
609 mData[0].timeValue = time;
610 }
611