diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp index 217ae90b3..e67ad1a06 100644 --- a/src/AudioIO.cpp +++ b/src/AudioIO.cpp @@ -372,32 +372,35 @@ So a small, fixed queue size should be adequate. struct AudioIO::ScrubQueue { ScrubQueue(double t0, double t1, wxLongLong startClockMillis, - double rate, double maxSpeed, + double rate, long maxDebt, const ScrubbingOptions &options) : mTrailingIdx(0) , mMiddleIdx(1) - , mLeadingIdx(2) + , mLeadingIdx(1) , mRate(rate) , mLastScrubTimeMillis(startClockMillis) , mUpdating() + , mMaxDebt { maxDebt } { - // Ignore options.adjustStart, pass false. - - bool success = InitEntry(mEntries[mMiddleIdx], nullptr, - t0, t1, maxSpeed, false, false, options); - if (!success) - { - // StartClock equals now? Really? - --mLastScrubTimeMillis; - success = InitEntry(mEntries[mMiddleIdx], nullptr, - t0, t1, maxSpeed, false, false, options); + const long s0 = std::max(options.minSample, std::min(options.maxSample, + lrint(t0 * mRate) + )); + const long s1 = lrint(t1 * mRate); + Duration dd { *this }; + long actualDuration = std::max(1L, dd.duration); + auto success = mEntries[mMiddleIdx].Init(nullptr, + s0, s1, actualDuration, options); + if (success) + ++mLeadingIdx; + else { + // If not, we can wait to enqueue again later + dd.Cancel(); } - wxASSERT(success); // So the play indicator starts out unconfused: { Entry &entry = mEntries[mTrailingIdx]; - entry.mS0 = entry.mS1 = mEntries[mMiddleIdx].mS0; + entry.mS0 = entry.mS1 = s0; entry.mPlayed = entry.mDuration = 1; } } @@ -420,30 +423,57 @@ struct AudioIO::ScrubQueue mAvailable.Signal(); } - bool Producer(double end, double maxSpeed, const ScrubbingOptions &options) + bool Producer(double end, const ScrubbingOptions &options) { // Main thread indicates a scrubbing interval // MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx. wxMutexLocker locker(mUpdating); - const unsigned next = (mLeadingIdx + 1) % Size; + bool result = true; + unsigned next = (mLeadingIdx + 1) % Size; if (next != mTrailingIdx) { - Entry &previous = mEntries[(mLeadingIdx + Size - 1) % Size]; + auto current = &mEntries[mLeadingIdx]; + auto previous = &mEntries[(mLeadingIdx + Size - 1) % Size]; // Use the previous end as NEW start. - const double startTime = previous.mS1 / mRate; - // Might reject the request because of zero duration, - // or a too-short "stutter" - const bool success = - (InitEntry(mEntries[mLeadingIdx], &previous, startTime, end, maxSpeed, - options.enqueueBySpeed, options.adjustStart, options)); - if (success) { + const long s0 = previous->mS1; + Duration dd { *this }; + const auto &origDuration = dd.duration; + if (origDuration <= 0) + return false; + + auto actualDuration = origDuration; + const long s1 = options.enqueueBySpeed + ? s0 + lrint(origDuration * end) // end is a speed + : lrint(end * mRate); // end is a time + auto success = + current->Init(previous, s0, s1, actualDuration, options); + if (success) mLeadingIdx = next; - mAvailable.Signal(); + else { + dd.Cancel(); + return false; } - return success; + + // Fill up the queue with some silence if there was trimming + wxASSERT(actualDuration <= origDuration); + if (actualDuration < origDuration) { + next = (mLeadingIdx + 1) % Size; + if (next != mTrailingIdx) { + previous = &mEntries[(mLeadingIdx + Size - 1) % Size]; + current = &mEntries[mLeadingIdx]; + current->InitSilent(*previous, origDuration - actualDuration); + mLeadingIdx = next; + } + else + // Oops, can't enqueue the silence -- so do what? + ; + } + + mAvailable.Signal(); + return result; } else { @@ -456,33 +486,85 @@ struct AudioIO::ScrubQueue } } - void Transformer(long &startSample, long &endSample, long &duration) + void Transformer(long &startSample, long &endSample, long &duration, + Maybe &cleanup) { // Audio thread is ready for the next interval. // MAY ADVANCE mMiddleIdx, WHICH MAY EQUAL mLeadingIdx, BUT DOES NOT PASS IT. - wxMutexLocker locker(mUpdating); + bool checkDebt = false; + if (!cleanup) { + cleanup.create(mUpdating); + + // Check for cancellation of work only when re-enetering the cricial section + checkDebt = true; + } while(!mNudged && mMiddleIdx == mLeadingIdx) mAvailable.Wait(); mNudged = false; - if (mMiddleIdx != mLeadingIdx) - { - // There is work in the queue + auto now = ::wxGetLocalTimeMillis(); + + if (checkDebt && + mLastTransformerTimeMillis >= 0 && // Not the first time for this scrub + mMiddleIdx != mLeadingIdx) { + // There is work in the queue, but if Producer is outrunning us, discard some, + // which may make a skip yet keep playback better synchronized with user gestures. + const auto interval = (now - mLastTransformerTimeMillis).ToDouble() / 1000.0; + const Entry &previous = mEntries[(mMiddleIdx + Size - 1) % Size]; + const auto deficit = + static_cast(interval * mRate) - // Samples needed in the last time interval + mCredit; // Samples done in the last time interval + mCredit = 0; + mDebt += deficit; + auto toDiscard = mDebt - mMaxDebt; + while (toDiscard > 0 && mMiddleIdx != mLeadingIdx) { + // Cancel some debt (discard some new work) + auto &entry = mEntries[mMiddleIdx]; + auto &dur = entry.mDuration; + if (toDiscard >= dur) { + // Discard entire queue entry + mDebt -= dur; + toDiscard -= dur; + dur = 0; // So Consumer() will handle abandoned entry correctly + mMiddleIdx = (mMiddleIdx + 1) % Size; + } + else { + // Adjust the start time + auto &start = entry.mS0; + const auto end = entry.mS1; + const auto ratio = static_cast(toDiscard) / static_cast(dur); + const auto adjustment = static_cast(std::abs(end - start) * ratio); + if (start <= end) + start += adjustment; + else + start -= adjustment; + + mDebt -= toDiscard; + dur -= toDiscard; + toDiscard = 0; + } + } + } + + if (mMiddleIdx != mLeadingIdx) { + // There is still work in the queue, after cancelling debt Entry &entry = mEntries[mMiddleIdx]; startSample = entry.mS0; endSample = entry.mS1; duration = entry.mDuration; - const unsigned next = (mMiddleIdx + 1) % Size; - mMiddleIdx = next; + mMiddleIdx = (mMiddleIdx + 1) % Size; + mCredit += duration; } - else - { - // We got the shut-down signal, or we got nudged + else { + // We got the shut-down signal, or we got nudged, or we discarded all the work. startSample = endSample = duration = -1L; } + + if (checkDebt) + mLastTransformerTimeMillis = now; } double Consumer(unsigned long frames) @@ -531,21 +613,26 @@ private: , mPlayed(0) {} - bool Init(Entry *previous, long s0, long s1, long duration, - double maxSpeed, bool adjustStart, + bool Init(Entry *previous, long s0, long s1, + long &duration /* in/out */, const ScrubbingOptions &options) { - if (duration <= 0) - return false; - double speed = double(abs(s1 - s0)) / duration; - bool maxed = false; + const bool &adjustStart = options.adjustStart; - // May change the requested speed (or reject) - if (!adjustStart && speed > maxSpeed) + wxASSERT(duration > 0); + double speed = static_cast(std::abs(s1 - s0)) / duration; + bool adjustedSpeed = false; + + auto minSpeed = std::min(options.minSpeed, options.maxSpeed); + wxASSERT(minSpeed == options.minSpeed); + + // May change the requested speed and duration + if (!adjustStart && speed > options.maxSpeed) { // Reduce speed to the maximum selected in the user interface. - speed = maxSpeed; - maxed = true; + speed = options.maxSpeed; + mGoal = s1; + adjustedSpeed = true; } else if (!adjustStart && previous && @@ -557,86 +644,76 @@ private: // continue at no less than maximum. (Without this // the final catch-up can make a slow scrub interval // that drops the pitch and sounds wrong.) - duration = lrint(speed * duration / maxSpeed); - if (duration <= 0) - { - previous->mGoal = -1; - return false; - } - speed = maxSpeed; - maxed = true; - } - - if (speed < ScrubbingOptions::MinAllowedScrubSpeed()) - // Mixers were set up to go only so slowly, not slower. - // This will put a request for some silence in the work queue. - speed = 0.0; - - // Before we change s1: - mGoal = maxed ? s1 : -1; - - // May change s1 or s0 to match speed change: - if (adjustStart) - { - bool silent = false; - - // Adjust s1 first, and duration, if s1 is out of bounds. - // (Assume s0 is in bounds, because it is the last scrub's s1 which was checked.) - if (s1 != s0) - { - const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1)); - if (s1 != newS1) - { - long newDuration = long(duration * double(newS1 - s0) / (s1 - s0)); - s1 = newS1; - if (newDuration == 0) - // Enqueue a silent scrub with s0 == s1 - silent = true; - else - // Shorten - duration = newDuration; - } - } - - if (!silent) - { - // When playback follows a fast mouse movement by "stuttering" - // at maximum playback, don't make stutters too short to be useful. - if (duration < options.minStutter) - return false; - // Limit diff because this is seeking. - const long diff = lrint(std::min(1.0, speed) * duration); - if (s0 < s1) - s0 = s1 - diff; - else - s0 = s1 + diff; - } + minSpeed = options.maxSpeed; + mGoal = s1; + adjustedSpeed = true; } else + mGoal = -1; + + if (speed < minSpeed) { + // Trim the duration. + duration = std::max(0L, lrint(speed * duration / minSpeed)); + speed = minSpeed; + adjustedSpeed = true; + } + + if (speed < ScrubbingOptions::MinAllowedScrubSpeed()) { + // Mixers were set up to go only so slowly, not slower. + // This will put a request for some silence in the work queue. + adjustedSpeed = true; + speed = 0.0; + } + + // May change s1 or s0 to match speed change or stay in bounds of the project + + if (adjustedSpeed && !adjustStart) { - // adjust end + // adjust s1 const long diff = lrint(speed * duration); if (s0 < s1) s1 = s0 + diff; else s1 = s0 - diff; + } - // Adjust s1 again, and duration, if s1 is out of bounds. (Assume s0 is in bounds.) - if (s1 != s0) - { - const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1)); - if (s1 != newS1) - { - long newDuration = long(duration * double(newS1 - s0) / (s1 - s0)); - s1 = newS1; - if (newDuration == 0) - // Enqueue a silent scrub with s0 == s1 - ; - else - // Shorten - duration = newDuration; - } + bool silent = false; + + // Adjust s1 (again), and duration, if s1 is out of bounds, + // or abandon if a stutter is too short. + // (Assume s0 is in bounds, because it equals the last scrub's s1 which was checked.) + if (s1 != s0) + { + long newDuration = duration; + const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1)); + if(s1 != newS1) + newDuration = std::max(0L, + static_cast(duration * static_cast(newS1 - s0) / (s1 - s0)) + ); + // When playback follows a fast mouse movement by "stuttering" + // at maximum playback, don't make stutters too short to be useful. + if (options.adjustStart && newDuration < options.minStutter) + return false; + else if (newDuration == 0) { + // Enqueue a silent scrub with s0 == s1 + silent = true; + s1 = s0; } + else if (s1 != newS1) { + // Shorten + duration = newDuration; + s1 = newS1; + } + } + + if (adjustStart && !silent) + { + // Limit diff because this is seeking. + const long diff = lrint(std::min(options.maxSpeed, speed) * duration); + if (s0 < s1) + s0 = s1 - diff; + else + s0 = s1 + diff; } mS0 = s0; @@ -646,9 +723,20 @@ private: return true; } + void InitSilent(const Entry &previous, long duration) + { + mGoal = previous.mGoal; + mS0 = mS1 = previous.mS1; + mPlayed = 0; + mDuration = duration; + } + double GetTime(double rate) const { - return (mS0 + ((mS1 - mS0) * mPlayed) / double(mDuration)) / rate; + return + (mS0 + + (mS1 - mS0) * static_cast(mPlayed) / static_cast(mDuration)) + / rate; } // These sample counts are initialized in the UI, producer, thread: @@ -668,23 +756,23 @@ private: long mPlayed; }; - bool InitEntry(Entry &entry, Entry *previous, double t0, double end, double maxSpeed, - bool bySpeed, bool adjustStart, - const ScrubbingOptions &options) - { - const wxLongLong clockTime(::wxGetLocalTimeMillis()); - const long duration = - mRate * (clockTime - mLastScrubTimeMillis).ToDouble() / 1000.0; - const long s0 = t0 * mRate; - const long s1 = bySpeed - ? s0 + lrint(duration * end) // end is a speed - : lrint(end * mRate); // end is a time - const bool success = - entry.Init(previous, s0, s1, duration, maxSpeed, adjustStart, options); - if (success) - mLastScrubTimeMillis = clockTime; - return success; - } + struct Duration { + Duration (ScrubQueue &queue_) : queue(queue_) {} + ~Duration () + { + if(!cancelled) + queue.mLastScrubTimeMillis = clockTime; + } + + void Cancel() { cancelled = true; } + + ScrubQueue &queue; + const wxLongLong clockTime { ::wxGetLocalTimeMillis() }; + const long duration { static_cast + (queue.mRate * (clockTime - queue.mLastScrubTimeMillis).ToDouble() / 1000.0) + }; + bool cancelled { false }; + }; enum { Size = 10 }; Entry mEntries[Size]; @@ -693,6 +781,12 @@ private: unsigned mLeadingIdx; const double mRate; wxLongLong mLastScrubTimeMillis; + + wxLongLong mLastTransformerTimeMillis { -1LL }; + long mCredit { 0L }; + long mDebt { 0L }; + const long mMaxDebt; + mutable wxMutex mUpdating; mutable wxCondition mAvailable { mUpdating }; bool mNudged { false }; @@ -1863,8 +1957,8 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks, const auto &scrubOptions = *options.pScrubbingOptions; mScrubQueue = new ScrubQueue(mT0, mT1, scrubOptions.startClockTimeMillis, - sampleRate, scrubOptions.maxSpeed, - *options.pScrubbingOptions); + sampleRate, 2 * scrubOptions.minStutter, + scrubOptions); mScrubDuration = 0; mSilentScrub = false; } @@ -2462,10 +2556,10 @@ bool AudioIO::IsPaused() #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT bool AudioIO::EnqueueScrub - (double endTimeOrSpeed, double maxSpeed, const ScrubbingOptions &options) + (double endTimeOrSpeed, const ScrubbingOptions &options) { if (mScrubQueue) - return mScrubQueue->Producer(endTimeOrSpeed, maxSpeed, options); + return mScrubQueue->Producer(endTimeOrSpeed, options); else return false; } @@ -3367,6 +3461,7 @@ void AudioIO::FillBuffers() // PRL: or, when scrubbing, we may get work repeatedly from the // scrub queue. bool done = false; + Maybe cleanup; do { // How many samples to produce for each channel. long frames = available; @@ -3442,7 +3537,7 @@ void AudioIO::FillBuffers() if (!done && mScrubDuration <= 0) { long startSample, endSample; - mScrubQueue->Transformer(startSample, endSample, mScrubDuration); + mScrubQueue->Transformer(startSample, endSample, mScrubDuration, cleanup); if (mScrubDuration < 0) { // Can't play anything @@ -3458,7 +3553,7 @@ void AudioIO::FillBuffers() double startTime, endTime, speed; startTime = startSample / mRate; endTime = endSample / mRate; - speed = double(abs(endSample - startSample)) / mScrubDuration; + speed = double(std::abs(endSample - startSample)) / mScrubDuration; for (i = 0; i < mPlaybackTracks->size(); i++) mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed); } @@ -4179,7 +4274,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer, (gAudioIO->mT0, gAudioIO->mTime); else gAudioIO->mWarpedTime = gAudioIO->mTime - gAudioIO->mT0; - gAudioIO->mWarpedTime = abs(gAudioIO->mWarpedTime); + gAudioIO->mWarpedTime = std::abs(gAudioIO->mWarpedTime); // Reset mixer positions and flush buffers for all tracks for (i = 0; i < (unsigned int)numPlaybackTracks; i++) diff --git a/src/AudioIO.h b/src/AudioIO.h index a628b3384..ca7b2a81d 100644 --- a/src/AudioIO.h +++ b/src/AudioIO.h @@ -169,11 +169,10 @@ class AUDACITY_DLL_API AudioIO final { * If options.adjustStart is true, then when mouse movement exceeds maximum scrub speed, * adjust the beginning of the scrub interval rather than the end, so that * the scrub skips or "stutters" to stay near the cursor. - * But if the "stutter" is too short for the minimum, then there is no effect - * on the work queue. - * Return true if some work was really enqueued. + * Return true if some sound was really enqueued. + * But if the "stutter" is too short for the minimum, enqueue nothing and return false. */ - bool EnqueueScrub(double endTimeOrSpeed, double maxSpeed, const ScrubbingOptions &options); + bool EnqueueScrub(double endTimeOrSpeed, const ScrubbingOptions &options); /** \brief return the ending time of the last enqueued scrub interval. */ diff --git a/src/Project.cpp b/src/Project.cpp index 24be2951a..1b8a42a8f 100644 --- a/src/Project.cpp +++ b/src/Project.cpp @@ -5378,8 +5378,18 @@ void AudacityProject::PlaybackScroller::OnTimer(wxCommandEvent &event) // Let other listeners get the notification event.Skip(); - if (mMode != Mode::Off && mProject->IsAudioActive()) - { + if(!mProject->IsAudioActive()) + return; + else if (mMode == Mode::Refresh) { + // PRL: see comments in Scrubbing.cpp for why this is sometimes needed. + // These unnecessary refreshes cause wheel rotation events to be delivered more uniformly + // to the application, so scrub speed control is smoother. + // (So I see at least with OS 10.10 and wxWidgets 3.0.2.) + // Is there another way to ensure that than by refreshing? + const auto trackPanel = mProject->GetTrackPanel(); + trackPanel->Refresh(false); + } + else if (mMode != Mode::Off) { // Pan the view, so that we center the play indicator. ViewInfo &viewInfo = mProject->GetViewInfo(); diff --git a/src/Project.h b/src/Project.h index 7d9a0aacd..c31019e8f 100644 --- a/src/Project.h +++ b/src/Project.h @@ -730,6 +730,7 @@ public: enum class Mode { Off, + Refresh, Centered, Right, }; diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 1520c4087..44e00f498 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(); } @@ -5623,6 +5626,7 @@ void TrackPanel::HandleWheelRotation(wxMouseEvent & event) #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL if (GetProject()->GetScrubber().IsScrubbing()) { GetProject()->GetScrubber().HandleScrollWheel(steps); + event.Skip(false); } else #endif diff --git a/src/TrackPanel.h b/src/TrackPanel.h index 23b0dd7d2..49f5dd355 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -73,7 +73,6 @@ DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_TRACK_PANEL_TIMER, -1); enum { kTimerInterval = 50, // milliseconds - kOneSecondCountdown = 1000 / kTimerInterval, }; class AUDACITY_DLL_API TrackInfo diff --git a/src/tracks/ui/Scrubbing.cpp b/src/tracks/ui/Scrubbing.cpp index 728a65d59..4351e4aee 100644 --- a/src/tracks/ui/Scrubbing.cpp +++ b/src/tracks/ui/Scrubbing.cpp @@ -46,6 +46,8 @@ enum { #endif ScrubPollInterval_ms = 50, + + kOneSecondCountdown = 1000 / ScrubPollInterval_ms, }; static const double MinStutter = 0.2; @@ -123,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: @@ -140,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) @@ -167,6 +201,11 @@ Scrubber::Scrubber(AudacityProject *project) Scrubber::~Scrubber() { +#ifdef USE_SCRUB_THREAD + if (mpThread) + mpThread->Delete(); +#endif + mProject->PopEventHandler(); if (wxTheApp) wxTheApp->Disconnect @@ -301,18 +340,19 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions()); options.pScrubbingOptions = &mOptions; options.timeTrack = NULL; - mOptions.delay = (ScrubPollInterval_ms / 1000.0); + mOptions.delay = (ScrubPollInterval_ms * 0.9 / 1000.0); + mOptions.minSpeed = 0.0; #ifdef USE_TRANSCRIPTION_TOOLBAR if (!mAlwaysSeeking) { // Take the starting speed limit from the transcription toolbar, // but it may be varied during the scrub. - mOptions.maxSpeed = + mMaxSpeed = mOptions.maxSpeed = mProject->GetTranscriptionToolBar()->GetPlaySpeed(); } #else // That idea seems unpopular... just make it one for move-scrub, // but big for drag-scrub - mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0; + mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0; #endif mOptions.minSample = 0; mOptions.maxSample = @@ -328,7 +368,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) static const double maxScrubSpeedBase = pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); mLogMaxScrubSpeed = floor(0.5 + - log(mOptions.maxSpeed) / log(maxScrubSpeedBase) + log(mMaxSpeed) / log(maxScrubSpeedBase) ); #endif mScrubSpeedDisplayCountdown = 0; @@ -342,12 +382,17 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx) mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis(); if (IsScrubbing()) { - using Mode = AudacityProject::PlaybackScroller::Mode; - mProject->GetPlaybackScroller().Activate - (mSmoothScrollingScrub ? Mode::Centered : Mode::Off); + ActivateScroller(); 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); } @@ -356,7 +401,67 @@ 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.minSpeed = 0.0; + mOptions.maxSpeed = mMaxSpeed; + mOptions.adjustStart = false; + mOptions.enqueueBySpeed = true; + result = gAudioIO->EnqueueScrub(0, 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.minSpeed = 0.0; + mOptions.maxSpeed = mMaxSpeed; + mOptions.adjustStart = true; + mOptions.enqueueBySpeed = false; + result = gAudioIO->EnqueueScrub(time, mOptions); + mLastScrubPosition = position.x; + } + else { + const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset()); + mOptions.adjustStart = seek; + mOptions.minSpeed = (mDragging || !seek) ? 0.0 : 1.0; + mOptions.maxSpeed = (mDragging || !seek) ? mMaxSpeed : 1.0; + + if (mSmoothScrollingScrub) { + const double speed = FindScrubSpeed(seek, time); + mOptions.enqueueBySpeed = true; + result = gAudioIO->EnqueueScrub(speed, mOptions); + } + else { + mOptions.enqueueBySpeed = false; + result = gAudioIO->EnqueueScrub(time, 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()); @@ -366,70 +471,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; - 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, mOptions.maxSpeed, mOptions); - } - else { - mOptions.enqueueBySpeed = false; - auto maxSpeed = - (mDragging || !seek) ? mOptions.maxSpeed : 1.0; - 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) ; @@ -441,6 +496,13 @@ void Scrubber::ContinueScrubbing() void Scrubber::StopScrubbing() { +#ifdef USE_SCRUB_THREAD + if (mpThread) { + mpThread->Delete(); + mpThread = nullptr; + } +#endif + mPoller->Stop(); UncheckAllMenuItems(); @@ -494,7 +556,7 @@ double Scrubber::FindScrubSpeed(bool seeking, double time) const ViewInfo &viewInfo = mProject->GetViewInfo(); const double screen = mProject->GetScreenEndTime() - viewInfo.h; return (seeking ? FindSeekSpeed : FindScrubbingSpeed) - (viewInfo, mOptions.maxSpeed, screen, time); + (viewInfo, mMaxSpeed, screen, time); } void Scrubber::HandleScrollWheel(int steps) @@ -503,6 +565,9 @@ void Scrubber::HandleScrollWheel(int steps) // Not likely you would spin it with the left button down, but... return; + if (steps == 0) + return; + const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps; static const double maxScrubSpeedBase = pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); @@ -510,7 +575,7 @@ void Scrubber::HandleScrollWheel(int steps) if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() && newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) { mLogMaxScrubSpeed = newLogMaxScrubSpeed; - mOptions.maxSpeed = newSpeed; + mMaxSpeed = newSpeed; if (!mSmoothScrollingScrub) // Show the speed for one second mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1; @@ -717,6 +782,24 @@ bool Scrubber::PollIsSeeking() return mDragging || (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown()); } +void Scrubber::ActivateScroller() +{ + using Mode = AudacityProject::PlaybackScroller::Mode; + mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub + ? Mode::Centered + : +#ifdef __WXMAC__ + // PRL: cause many "unnecessary" refreshes. For reasons I don't understand, + // doing this causes wheel rotation events (mapped from the double finger vertical + // swipe) to be delivered more uniformly to the application, so that spped control + // works better. + Mode::Refresh +#else + Mode::Off +#endif + ); +} + void Scrubber::DoScrub(bool scroll, bool seek) { const bool wasScrubbing = IsScrubbing(); @@ -735,9 +818,7 @@ void Scrubber::DoScrub(bool scroll, bool seek) } else if(!match) { mSmoothScrollingScrub = scroll; - using Mode = AudacityProject::PlaybackScroller::Mode; - mProject->GetPlaybackScroller().Activate - (scroll ? Mode::Centered : Mode::Off); + ActivateScroller(); mAlwaysSeeking = seek; UncheckAllMenuItems(); CheckMenuItem(); diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h index b1c639a31..6b0d84f4c 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() {} @@ -35,7 +40,8 @@ struct ScrubbingOptions { double delay {}; - // A limiting value for the speed of a scrub interval: + // Limiting values for the speed of a scrub interval: + double minSpeed { 0.0 }; double maxSpeed { 1.0 }; @@ -71,7 +77,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(); @@ -122,6 +129,7 @@ public: bool IsPaused() const; private: + void ActivateScroller(); void DoScrub(bool scroll, bool seek); void OnActivateOrDeactivateApp(wxActivateEvent & event); void UncheckAllMenuItems(); @@ -158,9 +166,20 @@ 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; + double mMaxSpeed { 1.0 }; }; // Specialist in drawing the scrub speed, and listening for certain events