1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-08-16 08:34:10 +02:00

More responsive scrub engine; improved speed control on Mac

* scrubbing:
  Don't let seek make a stutter at less than unit speed
  Further simplified argument passing
  Improve scrubbing speed control (2 finger swipe) on Mac
  Improve scrub responsiveness: a secondary thread polls the mouse
  Don't let the consumers discard too much from the scrub queue...
  Reduce scrub lag yet more, at expense of possible skips in play...
  Scrub lag: lock mutex not more than once per call to FillBuffers
  Reorganize logic of initializing queue entries
  One second countdown now based on the correct timer interval
This commit is contained in:
Paul Licameli 2016-05-28 12:13:20 -04:00
commit 94325b0ffb
8 changed files with 428 additions and 220 deletions

View File

@ -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<wxMutexLocker> &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<long>(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<double>(toDiscard) / static_cast<double>(dur);
const auto adjustment = static_cast<long>(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<double>(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<long>(duration * static_cast<double>(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<double>(mPlayed) / static_cast<double>(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<long>
(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<wxMutexLocker> 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++)

View File

@ -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.
*/

View File

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

View File

@ -730,6 +730,7 @@ public:
enum class Mode {
Off,
Refresh,
Centered,
Right,
};

View File

@ -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

View File

@ -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

View File

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

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() {}
@ -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<ScrubPoller> mPoller;
ScrubbingOptions mOptions;
double mMaxSpeed { 1.0 };
};
// Specialist in drawing the scrub speed, and listening for certain events