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