Audacity 3.2.0
Scrubbing.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5Scrubbing.cpp
6
7Paul Licameli split from TrackPanel.cpp
8
9**********************************************************************/
10
11
12#include "Scrubbing.h"
13
14#include <functional>
15
16#include "AudioIO.h"
17#include "CommandManager.h"
18#include "../../CommonCommandFlags.h"
19#include "Project.h"
20#include "ProjectAudioIO.h"
21#include "../../ProjectAudioManager.h"
22#include "ProjectHistory.h"
23#include "../../ProjectWindows.h"
24#include "ProjectStatus.h"
25#include "../../ScrubState.h"
26#include "Track.h"
27#include "ViewInfo.h"
28#include "WaveTrack.h"
29#include "../../prefs/PlaybackPrefs.h"
30#include "../../prefs/TracksPrefs.h"
31
32#undef USE_TRANSCRIPTION_TOOLBAR
33
34
35#include <algorithm>
36
37#include <wx/app.h>
38#include <wx/menu.h>
39#include <wx/timer.h>
40
41// Yet another experimental scrub would drag the track under a
42// stationary play head
43#undef DRAG_SCRUB
44
45enum {
46 // PRL:
47 // Mouse must move at least this far to distinguish ctrl-drag to scrub
48 // from ctrl+click for playback.
50
51#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
52 ScrubSpeedStepsPerOctave = 4,
53#endif
54
56 1000 / std::chrono::milliseconds{ScrubPollInterval}.count(),
57};
58
60// static const double MaxDragSpeed = 1.0;
61
62namespace {
63 double FindScrubbingSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
64 {
65 // Map a time (which was mapped from a mouse position)
66 // to a speed.
67 // Map times to positive and negative speeds,
68 // with the time at the midline of the screen mapping to 0,
69 // and the extremes to the maximum scrub speed.
70
71 auto partScreen = screen * TracksPrefs::GetPinnedHeadPositionPreference();
72 const double origin = viewInfo.hpos + partScreen;
73 if (timeAtMouse >= origin)
74 partScreen = screen - partScreen;
75
76 // There are various snapping zones that are this fraction of screen:
77 const double snap = 0.05;
78
79 // By shrinking denom a bit, we make margins left and right
80 // that snap to maximum and negative maximum speeds.
81 const double factor = 1.0 - (snap * 2);
82 const double denom = factor * partScreen;
83 double fraction = (denom <= 0.0) ? 0.0 :
84 std::min(1.0, fabs(timeAtMouse - origin) / denom);
85
86 // Snap to 1.0 and -1.0
87 const double unity = 1.0 / maxScrubSpeed;
88 const double tolerance = snap / factor;
89 // Make speeds near 1 available too by remapping fractions outside
90 // this snap zone
91 if (fraction <= unity - tolerance)
92 fraction *= unity / (unity - tolerance);
93 else if (fraction < unity + tolerance)
94 fraction = unity;
95 else
96 fraction = unity + (fraction - (unity + tolerance)) *
97 (1.0 - unity) / (1.0 - (unity + tolerance));
98
99 double result = fraction * maxScrubSpeed;
100 if (timeAtMouse < origin)
101 result *= -1.0;
102 return result;
103 }
104
105 double FindSeekSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
106 {
107 // Map a time (which was mapped from a mouse position)
108 // to a signed skip speed: a multiplier of the stutter duration,
109 // by which to advance the play position.
110 // (The stutter will play at unit speed.)
111
112 // Times near the midline of the screen map to skip-less play,
113 // and the extremes to a value proportional to maximum scrub speed.
114
115 // If the maximum scrubbing speed defaults to 1.0 when you begin to scroll-scrub,
116 // the extreme skipping for scroll-seek needs to be larger to be useful.
117 static const double ARBITRARY_MULTIPLIER = 10.0;
118 const double extreme = std::max(1.0, maxScrubSpeed * ARBITRARY_MULTIPLIER);
119
120 // Width of visible track area, in time terms:
121 auto partScreen = screen * TracksPrefs::GetPinnedHeadPositionPreference();
122 const double origin = viewInfo.hpos + partScreen;
123 if (timeAtMouse >= origin)
124 partScreen = screen - partScreen;
125
126 // The snapping zone is this fraction of screen, on each side of the
127 // center line:
128 const double snap = 0.05;
129 const double fraction = (partScreen <= 0.0) ? 0.0 :
130 std::max(snap, std::min(1.0, fabs(timeAtMouse - origin) / partScreen));
131
132 double result = 1.0 + ((fraction - snap) / (1.0 - snap)) * (extreme - 1.0);
133 if (timeAtMouse < origin)
134 result *= -1.0;
135 return result;
136 }
137}
138
139#ifdef USE_SCRUB_THREAD
140
142{
143 while (!mFinishThread.load(std::memory_order_acquire)) {
144 std::this_thread::sleep_for(ScrubPollInterval);
146 }
147}
148
149#endif
150
152{
155}
156
157class Scrubber::ScrubPoller : public wxTimer
158{
159public:
160 ScrubPoller(Scrubber &scrubber) : mScrubber( scrubber ) {}
161
162private:
163 void Notify() override;
164
166};
167
169{
170 // Call Continue functions here in a timer handler
171 // rather than in SelectionHandleDrag()
172 // so that even without drag events, we can instruct the play head to
173 // keep approaching the mouse cursor, when its maximum speed is limited.
174
175#ifndef USE_SCRUB_THREAD
176 // If there is no helper thread, this main thread timer is responsible
177 // for playback and for UI
179#endif
181}
182
184 []( AudacityProject &parent ){
185 return std::make_shared< Scrubber >( &parent ); }
186};
187
189{
190 return project.AttachedObjects::Get< Scrubber >( key );
191}
192
194{
195 return Get( const_cast< AudacityProject & >( project ) );
196}
197
199 : mScrubToken(-1)
202 , mSmoothScrollingScrub(false)
203 , mPaused(true)
204#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
205 , mLogMaxScrubSpeed(0)
206#endif
207
209 , mPoller { std::make_unique<ScrubPoller>(*this) }
210 , mOptions {}
211
212{
213 if (wxTheApp)
214 wxTheApp->Bind
215 (wxEVT_ACTIVATE_APP,
217
218 UpdatePrefs();
219}
220
222{
223#ifdef USE_SCRUB_THREAD
224 if (mThread.joinable()) {
225 mFinishThread.store(true, std::memory_order_release);
226 mThread.join();
227 }
228#endif
229}
230
232{
233 JoinThread();
234}
235
236static const auto HasWaveDataPred =
237 [](const AudacityProject &project){
238 auto range = TrackList::Get(project).Any<const WaveTrack>()
239 + [](const WaveTrack *pTrack){
240 return pTrack->GetEndTime() > pTrack->GetStartTime();
241 };
242 return !range.empty();
243 };
244
245static const ReservedCommandFlag
248}; return flag; } // jkc
249
250namespace {
251 struct MenuItem {
256 void (Scrubber::*memFn)(const CommandContext&);
257 bool seek;
258 bool (Scrubber::*StatusTest)() const;
259
260 const TranslatableString &GetStatus() const { return status; }
261 };
262 using MenuItems = std::vector< MenuItem >;
264 {
265 static MenuItems theItems{
266 /* i18n-hint: These commands assist the user in finding a sound by ear. ...
267 "Scrubbing" is variable-speed playback, ...
268 "Seeking" is normal speed playback but with skips, ...
269 */
270 { wxT("Scrub"), XXO("&Scrub"), XO("Scrubbing"),
273 },
274
275 /* i18n-hint: These commands assist the user in finding a sound by ear. ...
276 "Scrubbing" is variable-speed playback, ...
277 "Seeking" is normal speed playback but with skips, ...
278 */
279 { wxT("Seek"), XXO("See&k"), XO("Seeking"),
282 },
283
284 /* i18n-hint: These commands assist the user in finding a sound by ear. ...
285 "Scrubbing" is variable-speed playback, ...
286 "Seeking" is normal speed playback but with skips, ...
287 */
288 { wxT("ToggleScrubRuler"), XXO("Scrub &Ruler"), {},
291 },
292 };
293 return theItems;
294 };
295
296 inline const MenuItem &FindMenuItem(bool seek)
297 {
298 return *std::find_if(menuItems().begin(), menuItems().end(),
299 [=](const MenuItem &item) {
300 return seek == item.seek;
301 }
302 );
303 }
304
305}
306
308 // Assume xx is relative to the left edge of TrackPanel!
309 wxCoord xx, bool smoothScrolling, bool seek
310)
311{
312 // Don't actually start scrubbing, but collect some information
313 // needed for the decision to start scrubbing later when handling
314 // drag events.
315 mSmoothScrollingScrub = smoothScrolling;
316
317 auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
318
319 // Stop any play in progress
320 // Bug 1492: mCancelled to stop us collapsing the selected region.
321 mCancelled = true;
322 projectAudioManager.Stop();
323 mCancelled = false;
324
325 // Usually the timer handler of TrackPanel does this, but we do this now,
326 // so that same timer does not StopPlaying() again after this function and destroy
327 // scrubber state
328 ProjectAudioIO::Get( *mProject ).SetAudioIOToken(0);
329
330 mSeeking = seek;
332
333 // Commented out for Bug 1421
334 // mSeeking
335 // ? ControlToolBar::PlayAppearance::Seek
336 // : ControlToolBar::PlayAppearance::Scrub);
337
339 mCancelled = false;
340}
341
344{
345 return [options](auto&) -> std::unique_ptr<PlaybackPolicy>
346 {
347 return std::make_unique<ScrubbingPlaybackPolicy>(options);
348 };
349}
350
351
352#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
353// Assume xx is relative to the left edge of TrackPanel!
354bool Scrubber::MaybeStartScrubbing(wxCoord xx)
355{
356 if (mScrubStartPosition < 0)
357 return false;
358 if (IsScrubbing())
359 return false;
360#ifdef USE_SCRUB_THREAD
361 if (mThread.joinable())
362 return false;
363#endif
364 else {
365 const auto state = ::wxGetMouseState();
366 mDragging = state.LeftIsDown();
367
368 auto gAudioIO = AudioIO::Get();
369 const bool busy = gAudioIO->IsBusy();
370 if (busy && gAudioIO->GetNumCaptureChannels() > 0) {
371 // Do not stop recording, and don't try to start scrubbing after
372 // recording stops
374 return false;
375 }
376
377 wxCoord position = xx;
378 if (abs(mScrubStartPosition - position) >= SCRUBBING_PIXEL_TOLERANCE) {
379 auto &viewInfo = ViewInfo::Get(*mProject);
380 auto &projectAudioManager = ProjectAudioManager::Get(*mProject);
381 double maxTime = TrackList::Get(*mProject).GetEndTime();
382 const int leftOffset = viewInfo.GetLeftOffset();
383 double time0 = std::min(maxTime,
384 viewInfo.PositionToTime(mScrubStartPosition, leftOffset)
385 );
386 double time1 = std::min(maxTime,
387 viewInfo.PositionToTime(position, leftOffset)
388 );
389 if (time1 != time0) {
390 if (busy) {
391 position = mScrubStartPosition;
392 projectAudioManager.Stop();
393 mScrubStartPosition = position;
394 }
395
396#ifdef DRAG_SCRUB
398 auto delta = time0 - time1;
399 time0 = std::max(0.0, std::min(maxTime,
400 viewInfo.h +
401 (viewInfo.GetScreenEndTime() - viewInfo.h)
403 ));
404 time1 = time0 + delta;
405 }
406#endif
407 mSpeedPlaying = false;
408 mKeyboardScrubbing = false;
409 auto options =
411
412#ifndef USE_SCRUB_THREAD
413 // Yuck, we either have to poll "by hand" when scrub polling doesn't
414 // work with a thread, or else yield to timer messages, but that would
415 // execute too much else
416 options.playbackStreamPrimer = [this](){
418 return ScrubPollInterval;
419 };
420#endif
421 options.playNonWaveTracks = false;
422 options.envelope = nullptr;
426 mOptions.minSpeed = 0.0;
427#ifdef USE_TRANSCRIPTION_TOOLBAR
428 if (!mAlwaysSeeking) {
429 // Take the starting speed limit from the transcription toolbar,
430 // but it may be varied during the scrub.
432 ProjectSettings::Get( *mProject ).GetPlaySpeed();
433 }
434#else
435 // That idea seems unpopular... just make it one for move-scrub,
436 // but big for drag-scrub
437#ifdef DRAG_SCRUB
438 mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
439#else
441#endif
442
443#endif
444 mOptions.minTime = 0;
446 std::max(0.0, TrackList::Get(*mProject).GetEndTime());
448#ifdef DRAG_SCRUB
450#endif
452
453 const bool backwards = time1 < time0;
454#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
455 static const double maxScrubSpeedBase =
456 pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
457 mLogMaxScrubSpeed = floor(0.5 +
458 log(mMaxSpeed) / log(maxScrubSpeedBase)
459 );
460#endif
462
463 // Must start the thread and poller first or else PlayPlayRegion
464 // will insert some silence
465 StartPolling();
466 auto cleanup = finally([this]{
467 if (mScrubToken < 0)
468 StopPolling();
469 });
470
471 options.policyFactory = ScrubbingPlaybackPolicyFactory(mOptions);
473 projectAudioManager.PlayPlayRegion(
474 SelectedRegion(time0, time1), options,
475 PlayMode::normalPlay, backwards);
476 if (mScrubToken <= 0) {
477 // Bug1627 (part of it):
478 // infinite error spew when trying to start scrub:
479 // If failed for reasons of audio device problems, do not try
480 // again with repeated timer ticks.
482 return false;
483 }
484 }
485 }
486 else
487 // Wait to test again
488 ;
489
490 if (IsScrubbing()) {
492 }
493
494 // Return true whether we started scrub, or are still waiting to decide.
495 return true;
496 }
497}
498
499bool Scrubber::StartKeyboardScrubbing(double time0, bool backwards)
500{
501 if (HasMark() || AudioIO::Get()->IsBusy())
502 return false;
503#ifdef USE_SCRUB_THREAD
504 if (mThread.joinable())
505 return false;
506#endif
507
508 mScrubStartPosition = 0; // so that HasMark() is true
509 mSpeedPlaying = false;
510 mKeyboardScrubbing = true;
511 mBackwards = backwards;
513 mDragging = false;
514
515 auto options = DefaultSpeedPlayOptions(*mProject);
516
517#ifndef USE_SCRUB_THREAD
518 // Yuck, we either have to poll "by hand" when scrub polling doesn't
519 // work with a thread, or else yield to timer messages, but that would
520 // execute too much else
521 options.playbackStreamPrimer = [this]() {
523 return ScrubPollInterval;
524 };
525#endif
526
527 options.playNonWaveTracks = false;
528 options.envelope = nullptr;
529
530 // delay and minStutterTime are used in AudioIO::AllocateBuffers() for setting the
531 // values of mPlaybackQueueMinimum and mPlaybackSamplesToCopy respectively.
534
536 if (backwards)
537 mOptions.initSpeed *= -1.0;
540 mOptions.minTime = 0;
541 mOptions.maxTime = std::max(0.0, TrackList::Get(*mProject).GetEndTime());
542 mOptions.bySpeed = true;
543 mOptions.adjustStart = false;
545
546 // Must start the thread and poller first or else PlayPlayRegion
547 // will insert some silence
548 StartPolling();
549 auto cleanup = finally([this] {
550 if (mScrubToken < 0)
551 StopPolling();
552 });
553
554 options.policyFactory = ScrubbingPlaybackPolicyFactory(mOptions);
557 SelectedRegion(time0, backwards ? mOptions.minTime : mOptions.maxTime),
558 options,
560 backwards);
561
562 return true;
563}
564
565
567{
568 const double speedAtDefaultZoom = 0.5;
569 const double maxSpeed = 3.0;
570 const double minSpeed = 0.0625;
571
572 auto &viewInfo = ViewInfo::Get(*mProject);
573 double speed = speedAtDefaultZoom*viewInfo.GetDefaultZoom() / viewInfo.GetZoom();
574 speed = std::min(speed, maxSpeed);
575 speed = std::max(speed, minSpeed);
576 return speed;
577}
578
579
581{
582 // Thus scrubbing relies mostly on periodic polling of mouse and keys,
583 // not event notifications. But there are a few event handlers that
584 // leave messages for this routine, in mScrubSeekPress and in mPaused.
585
586 // Decide whether to skip play, because either mouse is down now,
587 // or there was a left click event. (This is then a delayed reaction, in a
588 // timer callback, to a left click event detected elsewhere.)
589 const bool seek = TemporarilySeeks() || Seeks();
590
591 auto gAudioIO = AudioIO::Get();
592 if (mPaused) {
593 // When paused, make silent scrubs.
594 mOptions.minSpeed = 0.0;
596 mOptions.adjustStart = false;
597 mOptions.bySpeed = true;
599 }
600 else if (mSpeedPlaying) {
601 // default speed of 1.3 set, so that we can hear there is a problem
602 // when playAtSpeedTB not found.
603 double speed = 1.3;
604 const auto &projectAudioIO = ProjectAudioIO::Get( *mProject );
605 speed = projectAudioIO.GetPlaySpeed();
606 mOptions.minSpeed = speed -0.01;
607 mOptions.maxSpeed = speed +0.01;
608 mOptions.adjustStart = false;
609 mOptions.bySpeed = true;
611 }
612 else if (mKeyboardScrubbing) {
615 mOptions.adjustStart = false;
616 mOptions.bySpeed = true;
617 double speed = GetKeyboardScrubbingSpeed();
618 if (mBackwards)
619 speed *= -1.0;
621 } else {
622 const wxMouseState state(::wxGetMouseState());
623 auto &trackPanel = GetProjectPanel( *mProject );
624 const wxPoint position = trackPanel.ScreenToClient(state.GetPosition());
625 auto &viewInfo = ViewInfo::Get( *mProject );
626#ifdef DRAG_SCRUB
628 const auto lastTime = ScrubState::GetLastScrubTime();
629 const auto delta = mLastScrubPosition - position.x;
630 const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
631 mOptions.minSpeed = 0.0;
633 mOptions.adjustStart = true;
634 mOptions.bySpeed = false;
635 gAudioIO->UpdateScrub(time, mOptions);
636 mLastScrubPosition = position.x;
637 }
638 else
639#endif
640 {
641 const auto origin = viewInfo.GetLeftOffset();
642 auto xx = position.x;
643 if (!seek && !mSmoothScrollingScrub) {
644 // If mouse is out-of-bounds, so that we scrub at maximum speed
645 // toward the mouse position, then move the target time to a more
646 // extreme position to avoid catching-up and halting before the
647 // screen scrolls.
648 auto width = viewInfo.GetTracksUsableWidth();
649 auto delta = xx - origin;
650 if (delta < 0)
651 delta -= width;
652 else if (delta >= width)
653 delta += width;
654 xx = origin + delta;
655 }
656 const double time = viewInfo.PositionToTime(xx, origin);
657 mOptions.adjustStart = seek;
658 mOptions.minSpeed = seek ? 1.0 : 0.0;
659 mOptions.maxSpeed = seek ? 1.0 : mMaxSpeed;
660
662 const double speed = FindScrubSpeed(seek, time);
663 mOptions.bySpeed = true;
665 }
666 else {
667 mOptions.bySpeed = false;
669 }
670 }
671 }
672
673 mScrubSeekPress = false;
674
675 // else, if seek requested, try again at a later time when we might
676 // enqueue a long enough stutter
677}
678
680{
681 const wxMouseState state(::wxGetMouseState());
682
683 if (mDragging && !state.LeftIsDown()) {
684 // Dragging scrub can stop with mouse up
685 // Stop and set cursor
686 bool bShift = state.ShiftDown();
687 auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
688 projectAudioManager.DoPlayStopSelect( true, bShift );
689 projectAudioManager.Stop();
690 return;
691 }
692
693 const bool seek = Seeks() || TemporarilySeeks();
694
695 {
696 // Show the correct status for seeking.
697 bool backup = mSeeking;
698 mSeeking = seek;
699 mSeeking = backup;
700 }
701
702 if (seek)
704
706 ;
707 else {
710 }
711}
712
714{
716 return false;
717 return
718 !(HasMark() &&
719 !WasSpeedPlaying() &&
721}
722
724{
725 mPaused = false;
726
727#ifdef USE_SCRUB_THREAD
728 assert(!mThread.joinable());
729 mFinishThread.store(false, std::memory_order_relaxed);
730 mThread = std::thread{
731 std::mem_fn( &Scrubber::ScrubPollerThread ), std::ref(*this) };
732#endif
733
734 mPoller->Start( 0.9 *
735 std::chrono::duration<double, std::milli>{ScrubPollInterval}.count());
736}
737
739{
740 mPaused = true;
741
742#ifdef USE_SCRUB_THREAD
743 JoinThread();
744#endif
745
746 mPoller->Stop();
747}
748
750{
751 auto gAudioIO = AudioIO::Get();
753 StopPolling();
754
755 if (HasMark() && !mCancelled) {
756 const wxMouseState state(::wxGetMouseState());
757 // Stop and set cursor
758 bool bShift = state.ShiftDown();
759 auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
760 projectAudioManager.DoPlayStopSelect(true, bShift);
761 }
762
764 mDragging = false;
765 mSeeking = false;
766
768}
769
770bool Scrubber::ShowsBar() const
771{
772 return mShowScrubbing;
773}
774
775bool Scrubber::IsScrubbing() const
776{
777 if (mScrubToken <= 0)
778 return false;
779 auto &projectAudioIO = ProjectAudioIO::Get( *mProject );
780 if (mScrubToken == projectAudioIO.GetAudioIOToken() &&
781 projectAudioIO.IsAudioActive())
782 return true;
783 else {
784 const_cast<Scrubber&>(*this).mScrubToken = -1;
785 const_cast<Scrubber&>(*this).mScrubStartPosition = -1;
786 const_cast<Scrubber&>(*this).mSmoothScrollingScrub = false;
787 return false;
788 }
789}
790
791bool Scrubber::ChoseSeeking() const
792{
793 return
794#if !defined(DRAG_SCRUB)
795 // Drag always seeks
796 mDragging ||
797#endif
798 mSeeking;
799}
800
802{
803 return mScrubSeekPress ||
804 (::wxGetMouseState().LeftIsDown() && MayDragToSeek());
805}
806
807bool Scrubber::Seeks() const
808{
809 return (HasMark() || IsScrubbing()) && ChoseSeeking();
810}
811
812bool Scrubber::Scrubs() const
813{
814 if( Seeks() )
815 return false;
816 return (HasMark() || IsScrubbing()) && !ChoseSeeking();
817}
818
820{
821 return IsScrubbing() &&
822 !mPaused && (
823 // Draw for (non-scroll) scrub, sometimes, but never for seek
825 // Draw always for scroll-scrub and for scroll-seek
827 );
828}
829
830double Scrubber::FindScrubSpeed(bool seeking, double time) const
831{
832 auto &viewInfo = ViewInfo::Get( *mProject );
833 const double screen =
834 viewInfo.GetScreenEndTime() - viewInfo.hpos;
835 return (seeking ? FindSeekSpeed : FindScrubbingSpeed)
836 (viewInfo, mMaxSpeed, screen, time);
837}
838
839void Scrubber::HandleScrollWheel(int steps)
840{
841 if (steps == 0)
842 return;
843
844 const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
845 static const double maxScrubSpeedBase =
846 pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
847 double newSpeed = pow(maxScrubSpeedBase, newLogMaxScrubSpeed);
848 if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() &&
850 mLogMaxScrubSpeed = newLogMaxScrubSpeed;
851 mMaxSpeed = newSpeed;
853 // Show the speed for one second
855 }
856}
857
858void Scrubber::Pause( bool paused )
859{
860 mPaused = paused;
861}
862
863bool Scrubber::IsPaused() const
864{
865 return mPaused;
866}
867
868void Scrubber::OnActivateOrDeactivateApp(wxActivateEvent &event)
869{
870 // First match priority logic...
871 // Pause if Pause down, or not scrubbing.
872 if (!mProject)
873 Pause(true);
874 else if (ProjectAudioManager::Get( *mProject ).Paused())
875 Pause( true );
876 else if (!IsScrubbing())
877 Pause( true );
878
879 // Stop keyboard scrubbing if losing focus
880 else if (mKeyboardScrubbing && !event.GetActive()) {
881 Cancel();
882 ProjectAudioManager::Get(*mProject).Stop();
883 }
884
885 // Speed playing does not pause if losing focus.
886 else if (mSpeedPlaying)
887 Pause( false );
888
889 // But scrub and seek do.
890 else if (!event.GetActive())
891 Pause( true );
892 else
893 Pause(false);
894
895 event.Skip();
896}
897
898void Scrubber::DoScrub(bool seek)
899{
900 if( !CanScrub() )
901 return;
902 const bool wasScrubbing = HasMark() || IsScrubbing();
903 const bool scroll = ShouldScrubPinned();
904 if (!wasScrubbing) {
905 auto &tp = GetProjectPanel( *mProject );
906 const auto &viewInfo = ViewInfo::Get( *mProject );
907 wxCoord xx = tp.ScreenToClient(::wxGetMouseState().GetPosition()).x;
908
909 // Limit x
910 auto width = viewInfo.GetTracksUsableWidth();
911 const auto offset = viewInfo.GetLeftOffset();
912 xx = (std::max(offset, std::min(offset + width - 1, xx)));
913
914 MarkScrubStart(xx, scroll, seek);
915 }
916 else if (mSeeking != seek) {
917 // just switching mode
918 }
919 else {
920 auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
921 projectAudioManager.Stop();
922 }
923}
924
925void Scrubber::OnScrubOrSeek(bool seek)
926{
927 DoScrub(seek);
928
929 mSeeking = seek;
931}
932
934{
935 OnScrubOrSeek(false);
937}
938
940{
941 OnScrubOrSeek(true);
943}
944
945#if 1
946namespace {
947 static const wxChar *scrubEnabledPrefName = wxT("/QuickPlay/ScrubbingEnabled");
948
949 bool ReadScrubEnabledPref()
950 {
951 bool result {};
952 gPrefs->Read(scrubEnabledPrefName, &result, false);
953
954 return result;
955 }
956
957 void WriteScrubEnabledPref(bool value)
958 {
959 gPrefs->Write(scrubEnabledPrefName, value);
960 }
961}
962#endif
963
965{
966 mShowScrubbing = ReadScrubEnabledPref();
967}
968
970{
972 WriteScrubEnabledPref(mShowScrubbing);
973 gPrefs->Flush();
975}
976
977enum { CMD_ID = 8000 };
978
979#define THUNK(Name) Scrubber::Thunk<&Scrubber::Name>
980
981BEGIN_EVENT_TABLE(Scrubber, wxEvtHandler)
982 EVT_MENU(CMD_ID, THUNK(OnScrub))
983 EVT_MENU(CMD_ID + 1, THUNK(OnSeek))
984 EVT_MENU(CMD_ID + 2, THUNK(OnToggleScrubRuler))
986
987//static_assert(menuItems().size() == 3, "wrong number of items");
988
989static auto sPlayAtSpeedStatus = XO("Playing at Speed");
990
991static auto sKeyboardScrubbingStatus = XO("Scrubbing");
992
993
994const TranslatableString &Scrubber::GetUntranslatedStateString() const
995{
996 static TranslatableString empty;
997
998 if (IsSpeedPlaying()) {
999 return sPlayAtSpeedStatus;
1000 }
1001 else if (IsKeyboardScrubbing()) {
1002 return sKeyboardScrubbingStatus;
1003 }
1004 else if (HasMark()) {
1005 auto &item = FindMenuItem(Seeks() || TemporarilySeeks());
1006 return item.status;
1007 }
1008 else
1009 return empty;
1010}
1011
1012wxString Scrubber::StatusMessageForWave() const
1013{
1014 wxString result;
1015
1016 if( Seeks() )
1017 result = _("Move mouse pointer to Seek");
1018 else if( Scrubs() )
1019 result = _("Move mouse pointer to Scrub");
1020 return result;
1021}
1022
1023
1024
1027 []( const AudacityProject &, StatusBarField field )
1029 {
1030 if ( field == stateStatusBarField ) {
1031 TranslatableStrings strings;
1032 // Note that Scrubbing + Paused is not allowed.
1033 for (const auto &item : menuItems())
1034 strings.push_back( item.GetStatus() );
1035 strings.push_back(
1036 XO("%s Paused.").Format( sPlayAtSpeedStatus )
1037 );
1038 // added constant needed because xMax isn't large enough for some reason, plus some space.
1039 return { std::move( strings ), 30 };
1040 }
1041 return {};
1042 }
1043};
1044
1045bool Scrubber::CanScrub() const
1046{
1047 // Recheck the same condition as enables the Scrub/Seek menu item.
1048 auto gAudioIO = AudioIO::Get();
1049 return !( gAudioIO->IsBusy() && gAudioIO->GetNumCaptureChannels() > 0 ) &&
1051}
1052
1053void Scrubber::DoKeyboardScrub(bool backwards, bool keyUp)
1054{
1055 auto &project = *mProject;
1056
1057 static double initT0 = 0;
1058 static double initT1 = 0;
1059
1060 if (keyUp) {
1061 auto &scrubber = Scrubber::Get(project);
1062 if (scrubber.IsKeyboardScrubbing() && scrubber.IsBackwards() == backwards) {
1063 auto gAudioIO = AudioIO::Get();
1064 auto time = gAudioIO->GetStreamTime();
1065 auto &viewInfo = ViewInfo::Get(project);
1066 auto &selection = viewInfo.selectedRegion;
1067
1068 // If the time selection has not changed during scrubbing
1069 // set the cursor position
1070 if (selection.t0() == initT0 && selection.t1() == initT1) {
1071 double endTime = TrackList::Get(project).GetEndTime();
1072 time = std::min(time, endTime);
1073 time = std::max(time, 0.0);
1074 selection.setTimes(time, time);
1076 }
1077
1078 scrubber.Cancel();
1080 }
1081 }
1082 else { // KeyDown
1083 auto gAudioIO = AudioIOBase::Get();
1084 auto &scrubber = Scrubber::Get(project);
1085 if (scrubber.IsKeyboardScrubbing() && scrubber.IsBackwards() != backwards) {
1086 // change direction
1087 scrubber.SetBackwards(backwards);
1088 }
1089 else if (!gAudioIO->IsBusy() && !scrubber.HasMark()) {
1090 auto &viewInfo = ViewInfo::Get(project);
1091 auto &selection = viewInfo.selectedRegion;
1092 double endTime = TrackList::Get(project).GetEndTime();
1093 double t0 = selection.t0();
1094
1095 if ((!backwards && t0 >= 0 && t0 < endTime) ||
1096 (backwards && t0 > 0 && t0 <= endTime)) {
1097 initT0 = t0;
1098 initT1 = selection.t1();
1099 scrubber.StartKeyboardScrubbing(t0, backwards);
1100 }
1101 }
1102 }
1103}
1104
1106{
1107 auto evt = context.pEvt;
1108 if (evt)
1109 DoKeyboardScrub(true, evt->GetEventType() == wxEVT_KEY_UP);
1110 else { // called from menu, so simulate keydown and keyup
1111 DoKeyboardScrub(true, false);
1112 DoKeyboardScrub(true, true);
1113 }
1114}
1115
1117{
1118 auto evt = context.pEvt;
1119 if (evt)
1120 DoKeyboardScrub(false, evt->GetEventType() == wxEVT_KEY_UP);
1121 else { // called from menu, so simulate keydown and keyup
1122 DoKeyboardScrub(false, false);
1123 DoKeyboardScrub(false, true);
1124 }
1125}
1126
1127namespace {
1128
1129static const auto finder =
1131 { return Scrubber::Get( project ); };
1132
1133using namespace MenuRegistry;
1134auto ToolbarMenu()
1135{
1136 static auto menu = []{
1138 auto menu = std::shared_ptr{ Menu("Scrubbing", XXO("Scru&bbing")) };
1139 for (const auto &item : menuItems()) {
1140 menu->push_back(Command(item.name, item.label,
1141 item.memFn,
1142 item.flags,
1143 item.StatusTest
1144 ? // a checkmark item
1146 return (Scrubber::Get(project).*(item.StatusTest))(); } )
1147 : // not a checkmark item
1148 Options{}
1149 ));
1150 }
1151 return menu;
1152 }();
1153 return menu;
1154}
1155
1156AttachedItem sAttachment{ Indirect(ToolbarMenu()), wxT("Transport/Basic") };
1157
1158auto KeyboardScrubbingItems()
1159{
1160 static auto items = std::shared_ptr{
1161 ( FinderScope{ finder },
1162 Items( wxT("KeyboardScrubbing"),
1163 Command(wxT("KeyboardScrubBackwards"), XXO("Scrub Bac&kwards"),
1166 Options{ wxT("U") }.WantKeyUp() ),
1167 Command(wxT("KeyboardScrubForwards"), XXO("Scrub For&wards"),
1170 Options{ wxT("I") }.WantKeyUp() )
1171 ) ) };
1172 return items;
1173}
1174
1175AttachedItem sAttachment2{ Indirect(KeyboardScrubbingItems()),
1176 wxT("Optional/Extra/Part1/Transport")
1177};
1178
1179}
1180
1181void Scrubber::PopulatePopupMenu(wxMenu &menu)
1182{
1183 int id = CMD_ID;
1184 auto &cm = CommandManager::Get( *mProject );
1185 for (const auto &item : menuItems()) {
1186 if (cm.GetEnabled(item.name)) {
1187 auto test = item.StatusTest;
1188 menu.Append(id, item.label.Translation(), wxString{},
1189 test ? wxITEM_CHECK : wxITEM_NORMAL);
1190 if(test && (this->*test)())
1191 menu.FindItem(id)->Check();
1192 }
1193 ++id;
1194 }
1195}
1196
1198{
1199 auto &cm = CommandManager::Get( *mProject );
1200 for (const auto &item : menuItems()) {
1201 auto test = item.StatusTest;
1202 if (test)
1203 cm.Check(item.name, (this->*test)());
1204 }
1205}
1206
1207#endif
EVT_MENU(OnSetPlayRegionToSelectionID, AdornedRulerPanel::OnSetPlayRegionToSelection) EVT_COMMAND(OnTogglePinnedStateID
wxT("CloseDown"))
END_EVENT_TABLE()
AttachedItem sAttachment2
constexpr CommandFlag AlwaysEnabledFlag
Definition: CommandFlag.h:34
std::bitset< NCommandFlags > CommandFlag
Definition: CommandFlag.h:30
wxEvtHandler CommandHandlerObject
const ReservedCommandFlag & CaptureNotBusyFlag()
int min(int a, int b)
static ProjectStatus::RegisteredStatusWidthFunction registeredStatusWidthFunction
XO("Cut/Copy/Paste")
XXO("&Cut/Copy/Paste Toolbar")
#define field(n, t)
Definition: ImportAUP.cpp:165
#define _(s)
Definition: Internat.h:73
audacity::BasicSettings * gPrefs
Definition: Prefs.cpp:68
AudioIOStartStreamOptions DefaultSpeedPlayOptions(AudacityProject &project)
const ReservedCommandFlag & CanStopAudioStreamFlag()
StatusBarField
Definition: ProjectStatus.h:24
@ stateStatusBarField
Definition: ProjectStatus.h:25
AUDACITY_DLL_API wxWindow & GetProjectPanel(AudacityProject &project)
Get the main sub-window of the project frame that displays track data.
static constexpr auto ScrubPollInterval
Definition: ScrubState.h:109
@ SCRUBBING_PIXEL_TOLERANCE
Definition: Scrubbing.cpp:49
@ kOneSecondCountdown
Definition: Scrubbing.cpp:55
static const auto HasWaveDataPred
Definition: Scrubbing.cpp:236
static const AudacityProject::AttachedObjects::RegisteredFactory key
Definition: Scrubbing.cpp:183
static constexpr PlaybackPolicy::Duration MinStutter
Definition: Scrubbing.cpp:59
static const ReservedCommandFlag & HasWaveDataFlag()
Definition: Scrubbing.cpp:246
static AudioIOStartStreamOptions::PolicyFactory ScrubbingPlaybackPolicyFactory(const ScrubbingOptions &options)
Definition: Scrubbing.cpp:343
const auto project
declares abstract base class Track, TrackList, and iterators over TrackList
std::vector< TranslatableString > TranslatableStrings
int id
static std::once_flag flag
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
static AudioIOBase * Get()
Definition: AudioIOBase.cpp:94
static AudioIO * Get()
Definition: AudioIO.cpp:126
Client code makes static instance from a factory of attachments; passes it to Get or Find as a retrie...
Definition: ClientData.h:274
CommandContext provides additional information to an 'Apply()' command. It provides the project,...
const wxEvent * pEvt
static CommandManager & Get(AudacityProject &project)
std::chrono::duration< double > Duration
static bool GetUnpinnedScrubbingPreference()
void SetAudioIOToken(int token)
static AudioIOStartStreamOptions GetDefaultOptions(AudacityProject &project, bool newDefaults=false)
Invoke the global hook, supplying a default argument.
static ProjectAudioIO & Get(AudacityProject &project)
void Stop(bool stopStream=true)
static ProjectAudioManager & Get(AudacityProject &project)
int PlayPlayRegion(const SelectedRegion &selectedRegion, const AudioIOStartStreamOptions &options, PlayMode playMode, bool backwards=false)
void ModifyState(bool bWantsAutoSave)
static ProjectHistory & Get(AudacityProject &project)
static ProjectSettings & Get(AudacityProject &project)
std::pair< std::vector< TranslatableString >, unsigned > StatusWidthResult
Definition: ProjectStatus.h:49
Generates classes whose instances register items at construction.
Definition: Registry.h:388
void Notify() override
Definition: Scrubbing.cpp:168
ScrubPoller(Scrubber &scrubber)
Definition: Scrubbing.cpp:160
void ContinueScrubbingUI()
void MarkScrubStart(wxCoord xx, bool smoothScrolling, bool seek)
Definition: Scrubbing.cpp:307
bool mBackwards
Definition: Scrubbing.h:171
bool ShouldDrawScrubSpeed()
void CheckMenuItems()
bool mCancelled
Definition: Scrubbing.h:174
int mScrubToken
Definition: Scrubbing.h:160
void DoKeyboardScrub(bool backwards, bool keyUp)
bool Seeks() const
double GetKeyboardScrubbingSpeed()
void HandleScrollWheel(int steps)
bool mScrubSeekPress
Definition: Scrubbing.h:164
bool mDragging
Definition: Scrubbing.h:172
bool MaybeStartScrubbing(wxCoord xx)
void StopPolling()
wxCoord mLastScrubPosition
Definition: Scrubbing.h:163
wxCoord mScrubStartPosition
Definition: Scrubbing.h:162
bool mSmoothScrollingScrub
Definition: Scrubbing.h:165
bool Scrubs() const
void OnToggleScrubRuler(const CommandContext &)
bool ChoseSeeking() const
bool TemporarilySeeks() const
void JoinThread()
Definition: Scrubbing.cpp:221
std::unique_ptr< ScrubPoller > mPoller
Definition: Scrubbing.h:193
static bool ShouldScrubPinned()
Definition: Scrubbing.cpp:151
bool IsScrubbing() const
void OnActivateOrDeactivateApp(wxActivateEvent &event)
void StopScrubbing()
void Pause(bool paused)
int mScrubSpeedDisplayCountdown
Definition: Scrubbing.h:161
static Scrubber & Get(AudacityProject &project)
Definition: Scrubbing.cpp:188
double mMaxSpeed
Definition: Scrubbing.h:197
AudacityProject * mProject
Definition: Scrubbing.h:180
void UpdatePrefs() override
ScrubbingOptions mOptions
Definition: Scrubbing.h:196
bool IsPaused() const
void OnSeek(const CommandContext &)
bool StartKeyboardScrubbing(double time0, bool backwards)
void ScrubPollerThread()
void Cancel()
Definition: Scrubbing.h:105
void OnScrub(const CommandContext &)
bool mSpeedPlaying
Definition: Scrubbing.h:169
void OnKeyboardScrubForwards(const CommandContext &)
bool CanScrub() const
void StartPolling()
bool MayDragToSeek() const
Definition: Scrubbing.h:99
bool WasSpeedPlaying() const
Definition: Scrubbing.h:74
bool mKeyboardScrubbing
Definition: Scrubbing.h:170
void ContinueScrubbingPoll()
void OnKeyboardScrubBackwards(const CommandContext &)
bool IsTransportingPinned() const
bool mPaused
Definition: Scrubbing.h:167
void OnScrubOrSeek(bool seek)
bool mSeeking
Definition: Scrubbing.h:168
void PopulatePopupMenu(wxMenu &menu)
wxString StatusMessageForWave() const
bool ShowsBar() const
bool mShowScrubbing
Definition: Scrubbing.h:199
double FindScrubSpeed(bool seeking, double time) const
bool HasMark() const
Definition: Scrubbing.h:88
Scrubber(AudacityProject *project)
Definition: Scrubbing.cpp:198
void DoScrub(bool seek)
Defines a selected portion of a project.
double GetEndTime() const
Return the greatest end time of the tracks, or 0 when no tracks.
Definition: Track.cpp:991
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:1097
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:347
static bool GetPinnedHeadPreference()
static double GetPinnedHeadPositionPreference()
Holds a msgid for the translation catalog; may also bind format arguments.
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
A Track that contains audio waveform data.
Definition: WaveTrack.h:227
double GetStartTime() const override
Implement WideSampleSequence.
Definition: WaveTrack.cpp:3087
double GetEndTime() const override
Implement WideSampleSequence.
Definition: WaveTrack.cpp:3092
double hpos
Leftmost visible timeline position in seconds.
Definition: ZoomInfo.h:54
virtual bool Flush() noexcept=0
virtual bool Write(const wxString &key, bool value)=0
virtual bool Read(const wxString &key, bool *value) const =0
constexpr auto Items
Definition: MenuRegistry.h:427
constexpr auto Command
Definition: MenuRegistry.h:456
constexpr auto Menu
Items will appear in a main toolbar menu or in a sub-menu.
Definition: MenuRegistry.h:445
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
auto begin(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:150
std::unique_ptr< detail::IndirectItem< Item > > Indirect(const std::shared_ptr< Item > &ptr)
A convenience function.
Definition: Registry.h:175
auto finder(TrackId id, int &distance)
std::vector< MenuItem > MenuItems
Definition: Scrubbing.cpp:262
double FindSeekSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
Definition: Scrubbing.cpp:105
const MenuItem & FindMenuItem(bool seek)
Definition: Scrubbing.cpp:296
double FindScrubbingSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
Definition: Scrubbing.cpp:63
static CommandContext::TargetFactory::SubstituteInUnique< InteractiveOutputTargets > scope
STL namespace.
std::function< std::unique_ptr< PlaybackPolicy >(const AudioIOStartStreamOptions &) > PolicyFactory
Definition: AudioIOBase.h:72
Options && CheckTest(const CheckFn &fn) &&
Definition: MenuRegistry.h:74
static void StopScrub()
Definition: ScrubState.cpp:469
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
const TranslatableString & GetStatus() const
Definition: Scrubbing.cpp:260