From af7a92c2ab480b375a1be0e5827588ae63a075ad Mon Sep 17 00:00:00 2001 From: Paul Licameli Date: Tue, 24 May 2016 23:17:41 -0400 Subject: [PATCH] Improve scrub responsiveness: a secondary thread polls the mouse --- src/TrackPanel.cpp | 5 +- src/tracks/ui/Scrubbing.cpp | 170 ++++++++++++++++++++++++------------ src/tracks/ui/Scrubbing.h | 18 +++- 3 files changed, 134 insertions(+), 59 deletions(-) diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 1520c4087..6ec83424b 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -2774,8 +2774,11 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack) #endif ExtendSelection(x, rect.x, clickedTrack); - // Don't do this at every mouse event, because it slows down seek-scrub. + // If scrubbing does not use the helper poller thread, then + // don't do this at every mouse event, because it slows down seek-scrub. // Instead, let OnTimer do it, which is often enough. + // And even if scrubbing does use the thread, then skipping this does not + // bring that advantage, but it is probably still a good idea anyway. // UpdateSelectionDisplay(); } diff --git a/src/tracks/ui/Scrubbing.cpp b/src/tracks/ui/Scrubbing.cpp index ee7e64136..8754d3422 100644 --- a/src/tracks/ui/Scrubbing.cpp +++ b/src/tracks/ui/Scrubbing.cpp @@ -125,6 +125,32 @@ namespace { } } +#ifdef USE_SCRUB_THREAD + +class Scrubber::ScrubPollerThread final : public wxThread { +public: + ScrubPollerThread(Scrubber &scrubber) + : wxThread { } + , mScrubber(scrubber) + {} + ExitCode Entry() override; + +private: + Scrubber &mScrubber; +}; + +auto Scrubber::ScrubPollerThread::Entry() -> ExitCode +{ + while( !TestDestroy() ) + { + wxThread::Sleep(ScrubPollInterval_ms); + mScrubber.ContinueScrubbingPoll(); + } + return 0; +} + +#endif + class Scrubber::ScrubPoller : public wxTimer { public: @@ -142,7 +168,13 @@ void Scrubber::ScrubPoller::Notify() // rather than in SelectionHandleDrag() // so that even without drag events, we can instruct the play head to // keep approaching the mouse cursor, when its maximum speed is limited. - mScrubber.ContinueScrubbing(); + +#ifndef USE_SCRUB_THREAD + // If there is no helper thread, this main thread timer is responsible + // for playback and for UI + mScrubber.ContinueScrubbingPoll(); +#endif + mScrubber.ContinueScrubbingUI(); } Scrubber::Scrubber(AudacityProject *project) @@ -169,6 +201,11 @@ Scrubber::Scrubber(AudacityProject *project) Scrubber::~Scrubber() { +#ifdef USE_SCRUB_THREAD + if (mpThread) + mpThread->Delete(); +#endif + mProject->PopEventHandler(); if (wxTheApp) wxTheApp->Disconnect @@ -350,6 +387,13 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) mPaused = false; mLastScrubPosition = xx; +#ifdef USE_SCRUB_THREAD + // Detached thread is self-deleting, after it receives the Delete() message + mpThread = safenew ScrubPollerThread{ *this }; + mpThread->Create(4096); + mpThread->Run(); +#endif + mPoller->Start(ScrubPollInterval_ms); } @@ -358,7 +402,62 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) } } -void Scrubber::ContinueScrubbing() +void Scrubber::ContinueScrubbingPoll() +{ + // Thus scrubbing relies mostly on periodic polling of mouse and keys, + // not event notifications. But there are a few event handlers that + // leave messages for this routine, in mScrubSeekPress and in mPaused. + + // Decide whether to skip play, because either mouse is down now, + // or there was a left click event. (This is then a delayed reaction, in a + // timer callback, to a left click event detected elsewhere.) + const bool seek = PollIsSeeking() || mScrubSeekPress; + + bool result = false; + if (mPaused) { + // When paused, enqueue silent scrubs. + mOptions.adjustStart = false; + mOptions.enqueueBySpeed = true; + result = gAudioIO->EnqueueScrub(0, mOptions.maxSpeed, mOptions); + } + else { + const wxMouseState state(::wxGetMouseState()); + const auto trackPanel = mProject->GetTrackPanel(); + const wxPoint position = trackPanel->ScreenToClient(state.GetPosition()); + const auto &viewInfo = mProject->GetViewInfo(); + if (mDragging && mSmoothScrollingScrub) { + const auto lastTime = gAudioIO->GetLastTimeInScrubQueue(); + const auto delta = mLastScrubPosition - position.x; + const double time = viewInfo.OffsetTimeByPixels(lastTime, delta); + mOptions.adjustStart = true; + mOptions.enqueueBySpeed = false; + result = gAudioIO->EnqueueScrub(time, mOptions.maxSpeed, mOptions); + mLastScrubPosition = position.x; + } + else { + const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset()); + mOptions.adjustStart = seek; + auto maxSpeed = (mDragging || !seek) ? mOptions.maxSpeed : 1.0; + + if (mSmoothScrollingScrub) { + const double speed = FindScrubSpeed(seek, time); + mOptions.enqueueBySpeed = true; + result = gAudioIO->EnqueueScrub(speed, maxSpeed, mOptions); + } + else { + mOptions.enqueueBySpeed = false; + result = gAudioIO->EnqueueScrub(time, maxSpeed, mOptions); + } + } + } + + if (result) + mScrubSeekPress = false; + // else, if seek requested, try again at a later time when we might + // enqueue a long enough stutter +} + +void Scrubber::ContinueScrubbingUI() { const wxMouseState state(::wxGetMouseState()); @@ -368,70 +467,20 @@ void Scrubber::ContinueScrubbing() return; } - // Thus scrubbing relies mostly on periodic polling of mouse and keys, - // not event notifications. But there are a few event handlers that - // leave messages for this routine, in mScrubSeekPress and in mPaused. - - // Seek only when the pointer is in the panel. Else, scrub. - TrackPanel *const trackPanel = mProject->GetTrackPanel(); - - // Decide whether to skip play, because either mouse is down now, - // or there was a left click event. (This is then a delayed reaction, in a - // timer callback, to a left click event detected elsewhere.) - const bool seek = PollIsSeeking() || mScrubSeekPress; + const bool seek = PollIsSeeking(); { // Show the correct status for seeking. bool backup = mAlwaysSeeking; mAlwaysSeeking = seek; const auto ctb = mProject->GetControlToolBar(); - ctb->UpdateStatusBar(mProject); + if (ctb) + ctb->UpdateStatusBar(mProject); mAlwaysSeeking = backup; } - const wxPoint position = trackPanel->ScreenToClient(state.GetPosition()); - const auto &viewInfo = mProject->GetViewInfo(); - - bool result = false; - if (mPaused) { - // When paused, enqueue silent scrubs. - mOptions.adjustStart = false; - mOptions.enqueueBySpeed = true; - result = gAudioIO->EnqueueScrub(0, mOptions.maxSpeed, mOptions); - } - else if (mDragging && mSmoothScrollingScrub) { - const auto lastTime = gAudioIO->GetLastTimeInScrubQueue(); - const auto delta = mLastScrubPosition - position.x; - const double time = viewInfo.OffsetTimeByPixels(lastTime, delta); - mOptions.adjustStart = true; - mOptions.enqueueBySpeed = false; - result = gAudioIO->EnqueueScrub(time, mOptions.maxSpeed, mOptions); - mLastScrubPosition = position.x; - } - else { - const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset()); - mOptions.adjustStart = seek; - auto maxSpeed = (mDragging || !seek) ? mOptions.maxSpeed : 1.0; - - if (seek) - // Cause OnTimer() to suppress the speed display - mScrubSpeedDisplayCountdown = 1; - - if (mSmoothScrollingScrub) { - const double speed = FindScrubSpeed(seek, time); - mOptions.enqueueBySpeed = true; - result = gAudioIO->EnqueueScrub(speed, maxSpeed, mOptions); - } - else { - mOptions.enqueueBySpeed = false; - result = gAudioIO->EnqueueScrub(time, maxSpeed, mOptions); - } - } - - if (result) - mScrubSeekPress = false; - // else, if seek requested, try again at a later time when we might - // enqueue a long enough stutter + if (seek) + mScrubSpeedDisplayCountdown = 0; if (mSmoothScrollingScrub) ; @@ -443,6 +492,13 @@ void Scrubber::ContinueScrubbing() void Scrubber::StopScrubbing() { +#ifdef USE_SCRUB_THREAD + if (mpThread) { + mpThread->Delete(); + mpThread = nullptr; + } +#endif + mPoller->Stop(); UncheckAllMenuItems(); diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h index b1c639a31..26ffb3c61 100644 --- a/src/tracks/ui/Scrubbing.h +++ b/src/tracks/ui/Scrubbing.h @@ -21,6 +21,11 @@ Paul Licameli split from TrackPanel.cpp class AudacityProject; +// Conditionally compile either a separate thead, or else use a timer in the main +// thread, to poll the mouse and update scrubbing speed and direction. The advantage of +// a thread may be immunity to choppy scrubbing in case redrawing takes too much time. +#define USE_SCRUB_THREAD + // For putting an increment of work in the scrubbing queue struct ScrubbingOptions { ScrubbingOptions() {} @@ -71,7 +76,8 @@ public: // Assume xx is relative to the left edge of TrackPanel! bool MaybeStartScrubbing(wxCoord xx); - void ContinueScrubbing(); + void ContinueScrubbingUI(); + void ContinueScrubbingPoll(); // This is meant to be called only from ControlToolBar void StopScrubbing(); @@ -158,8 +164,18 @@ private: DECLARE_EVENT_TABLE() +#ifdef USE_SCRUB_THREAD + // Course corrections in playback are done in a helper thread, unhindered by + // the complications of the main event dispatch loop + class ScrubPollerThread; + ScrubPollerThread *mpThread {}; +#endif + + // Other periodic update of the UI must be done in the main thread, + // by this object which is driven by timer events. class ScrubPoller; std::unique_ptr mPoller; + ScrubbingOptions mOptions; };