1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-11-14 17:14:07 +01:00

Improve scrub responsiveness: a secondary thread polls the mouse

This commit is contained in:
Paul Licameli
2016-05-24 23:17:41 -04:00
parent 9a256d3790
commit af7a92c2ab
3 changed files with 134 additions and 59 deletions

View File

@@ -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();

View File

@@ -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<ScrubPoller> mPoller;
ScrubbingOptions mOptions;
};