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