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 struct AudioIO::ScrubQueue
{ {
ScrubQueue(double t0, double t1, wxLongLong startClockMillis, ScrubQueue(double t0, double t1, wxLongLong startClockMillis,
double rate, double maxSpeed, double rate, long maxDebt,
const ScrubbingOptions &options) const ScrubbingOptions &options)
: mTrailingIdx(0) : mTrailingIdx(0)
, mMiddleIdx(1) , mMiddleIdx(1)
, mLeadingIdx(2) , mLeadingIdx(1)
, mRate(rate) , mRate(rate)
, mLastScrubTimeMillis(startClockMillis) , mLastScrubTimeMillis(startClockMillis)
, mUpdating() , mUpdating()
, mMaxDebt { maxDebt }
{ {
// Ignore options.adjustStart, pass false. const long s0 = std::max(options.minSample, std::min(options.maxSample,
lrint(t0 * mRate)
bool success = InitEntry(mEntries[mMiddleIdx], nullptr, ));
t0, t1, maxSpeed, false, false, options); const long s1 = lrint(t1 * mRate);
if (!success) Duration dd { *this };
{ long actualDuration = std::max(1L, dd.duration);
// StartClock equals now? Really? auto success = mEntries[mMiddleIdx].Init(nullptr,
--mLastScrubTimeMillis; s0, s1, actualDuration, options);
success = InitEntry(mEntries[mMiddleIdx], nullptr, if (success)
t0, t1, maxSpeed, false, false, options); ++mLeadingIdx;
else {
// If not, we can wait to enqueue again later
dd.Cancel();
} }
wxASSERT(success);
// So the play indicator starts out unconfused: // So the play indicator starts out unconfused:
{ {
Entry &entry = mEntries[mTrailingIdx]; Entry &entry = mEntries[mTrailingIdx];
entry.mS0 = entry.mS1 = mEntries[mMiddleIdx].mS0; entry.mS0 = entry.mS1 = s0;
entry.mPlayed = entry.mDuration = 1; entry.mPlayed = entry.mDuration = 1;
} }
} }
@ -420,30 +423,57 @@ struct AudioIO::ScrubQueue
mAvailable.Signal(); mAvailable.Signal();
} }
bool Producer(double end, double maxSpeed, const ScrubbingOptions &options) bool Producer(double end, const ScrubbingOptions &options)
{ {
// Main thread indicates a scrubbing interval // Main thread indicates a scrubbing interval
// MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx. // MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx.
wxMutexLocker locker(mUpdating); wxMutexLocker locker(mUpdating);
const unsigned next = (mLeadingIdx + 1) % Size; bool result = true;
unsigned next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx) 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. // Use the previous end as NEW start.
const double startTime = previous.mS1 / mRate; const long s0 = previous->mS1;
// Might reject the request because of zero duration, Duration dd { *this };
// or a too-short "stutter" const auto &origDuration = dd.duration;
const bool success = if (origDuration <= 0)
(InitEntry(mEntries[mLeadingIdx], &previous, startTime, end, maxSpeed, return false;
options.enqueueBySpeed, options.adjustStart, options));
if (success) { 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; 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 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. // Audio thread is ready for the next interval.
// MAY ADVANCE mMiddleIdx, WHICH MAY EQUAL mLeadingIdx, BUT DOES NOT PASS IT. // 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) while(!mNudged && mMiddleIdx == mLeadingIdx)
mAvailable.Wait(); mAvailable.Wait();
mNudged = false; mNudged = false;
if (mMiddleIdx != mLeadingIdx) auto now = ::wxGetLocalTimeMillis();
{
// There is work in the queue 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]; Entry &entry = mEntries[mMiddleIdx];
startSample = entry.mS0; startSample = entry.mS0;
endSample = entry.mS1; endSample = entry.mS1;
duration = entry.mDuration; duration = entry.mDuration;
const unsigned next = (mMiddleIdx + 1) % Size; mMiddleIdx = (mMiddleIdx + 1) % Size;
mMiddleIdx = next; mCredit += duration;
} }
else else {
{ // We got the shut-down signal, or we got nudged, or we discarded all the work.
// We got the shut-down signal, or we got nudged
startSample = endSample = duration = -1L; startSample = endSample = duration = -1L;
} }
if (checkDebt)
mLastTransformerTimeMillis = now;
} }
double Consumer(unsigned long frames) double Consumer(unsigned long frames)
@ -531,21 +613,26 @@ private:
, mPlayed(0) , mPlayed(0)
{} {}
bool Init(Entry *previous, long s0, long s1, long duration, bool Init(Entry *previous, long s0, long s1,
double maxSpeed, bool adjustStart, long &duration /* in/out */,
const ScrubbingOptions &options) const ScrubbingOptions &options)
{ {
if (duration <= 0) const bool &adjustStart = options.adjustStart;
return false;
double speed = double(abs(s1 - s0)) / duration;
bool maxed = false;
// May change the requested speed (or reject) wxASSERT(duration > 0);
if (!adjustStart && speed > maxSpeed) 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. // Reduce speed to the maximum selected in the user interface.
speed = maxSpeed; speed = options.maxSpeed;
maxed = true; mGoal = s1;
adjustedSpeed = true;
} }
else if (!adjustStart && else if (!adjustStart &&
previous && previous &&
@ -557,86 +644,76 @@ private:
// continue at no less than maximum. (Without this // continue at no less than maximum. (Without this
// the final catch-up can make a slow scrub interval // the final catch-up can make a slow scrub interval
// that drops the pitch and sounds wrong.) // that drops the pitch and sounds wrong.)
duration = lrint(speed * duration / maxSpeed); minSpeed = options.maxSpeed;
if (duration <= 0) mGoal = s1;
{ adjustedSpeed = true;
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;
}
} }
else 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); const long diff = lrint(speed * duration);
if (s0 < s1) if (s0 < s1)
s1 = s0 + diff; s1 = s0 + diff;
else else
s1 = s0 - diff; s1 = s0 - diff;
}
// Adjust s1 again, and duration, if s1 is out of bounds. (Assume s0 is in bounds.) bool silent = false;
if (s1 != s0)
{ // Adjust s1 (again), and duration, if s1 is out of bounds,
const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1)); // or abandon if a stutter is too short.
if (s1 != newS1) // (Assume s0 is in bounds, because it equals the last scrub's s1 which was checked.)
{ if (s1 != s0)
long newDuration = long(duration * double(newS1 - s0) / (s1 - s0)); {
s1 = newS1; long newDuration = duration;
if (newDuration == 0) const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1));
// Enqueue a silent scrub with s0 == s1 if(s1 != newS1)
; newDuration = std::max(0L,
else static_cast<long>(duration * static_cast<double>(newS1 - s0) / (s1 - s0))
// Shorten );
duration = newDuration; // 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; mS0 = s0;
@ -646,9 +723,20 @@ private:
return true; 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 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: // These sample counts are initialized in the UI, producer, thread:
@ -668,23 +756,23 @@ private:
long mPlayed; long mPlayed;
}; };
bool InitEntry(Entry &entry, Entry *previous, double t0, double end, double maxSpeed, struct Duration {
bool bySpeed, bool adjustStart, Duration (ScrubQueue &queue_) : queue(queue_) {}
const ScrubbingOptions &options) ~Duration ()
{ {
const wxLongLong clockTime(::wxGetLocalTimeMillis()); if(!cancelled)
const long duration = queue.mLastScrubTimeMillis = clockTime;
mRate * (clockTime - mLastScrubTimeMillis).ToDouble() / 1000.0; }
const long s0 = t0 * mRate;
const long s1 = bySpeed void Cancel() { cancelled = true; }
? s0 + lrint(duration * end) // end is a speed
: lrint(end * mRate); // end is a time ScrubQueue &queue;
const bool success = const wxLongLong clockTime { ::wxGetLocalTimeMillis() };
entry.Init(previous, s0, s1, duration, maxSpeed, adjustStart, options); const long duration { static_cast<long>
if (success) (queue.mRate * (clockTime - queue.mLastScrubTimeMillis).ToDouble() / 1000.0)
mLastScrubTimeMillis = clockTime; };
return success; bool cancelled { false };
} };
enum { Size = 10 }; enum { Size = 10 };
Entry mEntries[Size]; Entry mEntries[Size];
@ -693,6 +781,12 @@ private:
unsigned mLeadingIdx; unsigned mLeadingIdx;
const double mRate; const double mRate;
wxLongLong mLastScrubTimeMillis; wxLongLong mLastScrubTimeMillis;
wxLongLong mLastTransformerTimeMillis { -1LL };
long mCredit { 0L };
long mDebt { 0L };
const long mMaxDebt;
mutable wxMutex mUpdating; mutable wxMutex mUpdating;
mutable wxCondition mAvailable { mUpdating }; mutable wxCondition mAvailable { mUpdating };
bool mNudged { false }; bool mNudged { false };
@ -1863,8 +1957,8 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
const auto &scrubOptions = *options.pScrubbingOptions; const auto &scrubOptions = *options.pScrubbingOptions;
mScrubQueue = mScrubQueue =
new ScrubQueue(mT0, mT1, scrubOptions.startClockTimeMillis, new ScrubQueue(mT0, mT1, scrubOptions.startClockTimeMillis,
sampleRate, scrubOptions.maxSpeed, sampleRate, 2 * scrubOptions.minStutter,
*options.pScrubbingOptions); scrubOptions);
mScrubDuration = 0; mScrubDuration = 0;
mSilentScrub = false; mSilentScrub = false;
} }
@ -2462,10 +2556,10 @@ bool AudioIO::IsPaused()
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT #ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool AudioIO::EnqueueScrub bool AudioIO::EnqueueScrub
(double endTimeOrSpeed, double maxSpeed, const ScrubbingOptions &options) (double endTimeOrSpeed, const ScrubbingOptions &options)
{ {
if (mScrubQueue) if (mScrubQueue)
return mScrubQueue->Producer(endTimeOrSpeed, maxSpeed, options); return mScrubQueue->Producer(endTimeOrSpeed, options);
else else
return false; return false;
} }
@ -3367,6 +3461,7 @@ void AudioIO::FillBuffers()
// PRL: or, when scrubbing, we may get work repeatedly from the // PRL: or, when scrubbing, we may get work repeatedly from the
// scrub queue. // scrub queue.
bool done = false; bool done = false;
Maybe<wxMutexLocker> cleanup;
do { do {
// How many samples to produce for each channel. // How many samples to produce for each channel.
long frames = available; long frames = available;
@ -3442,7 +3537,7 @@ void AudioIO::FillBuffers()
if (!done && mScrubDuration <= 0) if (!done && mScrubDuration <= 0)
{ {
long startSample, endSample; long startSample, endSample;
mScrubQueue->Transformer(startSample, endSample, mScrubDuration); mScrubQueue->Transformer(startSample, endSample, mScrubDuration, cleanup);
if (mScrubDuration < 0) if (mScrubDuration < 0)
{ {
// Can't play anything // Can't play anything
@ -3458,7 +3553,7 @@ void AudioIO::FillBuffers()
double startTime, endTime, speed; double startTime, endTime, speed;
startTime = startSample / mRate; startTime = startSample / mRate;
endTime = endSample / mRate; endTime = endSample / mRate;
speed = double(abs(endSample - startSample)) / mScrubDuration; speed = double(std::abs(endSample - startSample)) / mScrubDuration;
for (i = 0; i < mPlaybackTracks->size(); i++) for (i = 0; i < mPlaybackTracks->size(); i++)
mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed); mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed);
} }
@ -4179,7 +4274,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
(gAudioIO->mT0, gAudioIO->mTime); (gAudioIO->mT0, gAudioIO->mTime);
else else
gAudioIO->mWarpedTime = gAudioIO->mTime - gAudioIO->mT0; 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 // Reset mixer positions and flush buffers for all tracks
for (i = 0; i < (unsigned int)numPlaybackTracks; i++) 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, * 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 * adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor. * 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 * Return true if some sound was really enqueued.
* on the work queue. * But if the "stutter" is too short for the minimum, enqueue nothing and return false.
* Return true if some work was really enqueued.
*/ */
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. /** \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 // Let other listeners get the notification
event.Skip(); 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. // Pan the view, so that we center the play indicator.
ViewInfo &viewInfo = mProject->GetViewInfo(); ViewInfo &viewInfo = mProject->GetViewInfo();

View File

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

View File

@ -2774,8 +2774,11 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
#endif #endif
ExtendSelection(x, rect.x, clickedTrack); 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. // 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(); // UpdateSelectionDisplay();
} }
@ -5623,6 +5626,7 @@ void TrackPanel::HandleWheelRotation(wxMouseEvent & event)
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL #ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
if (GetProject()->GetScrubber().IsScrubbing()) { if (GetProject()->GetScrubber().IsScrubbing()) {
GetProject()->GetScrubber().HandleScrollWheel(steps); GetProject()->GetScrubber().HandleScrollWheel(steps);
event.Skip(false);
} }
else else
#endif #endif

View File

@ -73,7 +73,6 @@ DECLARE_EXPORTED_EVENT_TYPE(AUDACITY_DLL_API, EVT_TRACK_PANEL_TIMER, -1);
enum { enum {
kTimerInterval = 50, // milliseconds kTimerInterval = 50, // milliseconds
kOneSecondCountdown = 1000 / kTimerInterval,
}; };
class AUDACITY_DLL_API TrackInfo class AUDACITY_DLL_API TrackInfo

View File

@ -46,6 +46,8 @@ enum {
#endif #endif
ScrubPollInterval_ms = 50, ScrubPollInterval_ms = 50,
kOneSecondCountdown = 1000 / ScrubPollInterval_ms,
}; };
static const double MinStutter = 0.2; 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 class Scrubber::ScrubPoller : public wxTimer
{ {
public: public:
@ -140,7 +168,13 @@ void Scrubber::ScrubPoller::Notify()
// rather than in SelectionHandleDrag() // rather than in SelectionHandleDrag()
// so that even without drag events, we can instruct the play head to // so that even without drag events, we can instruct the play head to
// keep approaching the mouse cursor, when its maximum speed is limited. // 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) Scrubber::Scrubber(AudacityProject *project)
@ -167,6 +201,11 @@ Scrubber::Scrubber(AudacityProject *project)
Scrubber::~Scrubber() Scrubber::~Scrubber()
{ {
#ifdef USE_SCRUB_THREAD
if (mpThread)
mpThread->Delete();
#endif
mProject->PopEventHandler(); mProject->PopEventHandler();
if (wxTheApp) if (wxTheApp)
wxTheApp->Disconnect wxTheApp->Disconnect
@ -301,18 +340,19 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions()); AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions());
options.pScrubbingOptions = &mOptions; options.pScrubbingOptions = &mOptions;
options.timeTrack = NULL; 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 #ifdef USE_TRANSCRIPTION_TOOLBAR
if (!mAlwaysSeeking) { if (!mAlwaysSeeking) {
// Take the starting speed limit from the transcription toolbar, // Take the starting speed limit from the transcription toolbar,
// but it may be varied during the scrub. // but it may be varied during the scrub.
mOptions.maxSpeed = mMaxSpeed = mOptions.maxSpeed =
mProject->GetTranscriptionToolBar()->GetPlaySpeed(); mProject->GetTranscriptionToolBar()->GetPlaySpeed();
} }
#else #else
// That idea seems unpopular... just make it one for move-scrub, // That idea seems unpopular... just make it one for move-scrub,
// but big for drag-scrub // but big for drag-scrub
mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0; mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
#endif #endif
mOptions.minSample = 0; mOptions.minSample = 0;
mOptions.maxSample = mOptions.maxSample =
@ -328,7 +368,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
static const double maxScrubSpeedBase = static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
mLogMaxScrubSpeed = floor(0.5 + mLogMaxScrubSpeed = floor(0.5 +
log(mOptions.maxSpeed) / log(maxScrubSpeedBase) log(mMaxSpeed) / log(maxScrubSpeedBase)
); );
#endif #endif
mScrubSpeedDisplayCountdown = 0; mScrubSpeedDisplayCountdown = 0;
@ -342,12 +382,17 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis(); mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis();
if (IsScrubbing()) { if (IsScrubbing()) {
using Mode = AudacityProject::PlaybackScroller::Mode; ActivateScroller();
mProject->GetPlaybackScroller().Activate
(mSmoothScrollingScrub ? Mode::Centered : Mode::Off);
mPaused = false; mPaused = false;
mLastScrubPosition = xx; 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); 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()); const wxMouseState state(::wxGetMouseState());
@ -366,70 +471,20 @@ void Scrubber::ContinueScrubbing()
return; return;
} }
// Thus scrubbing relies mostly on periodic polling of mouse and keys, const bool seek = PollIsSeeking();
// 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;
{ {
// Show the correct status for seeking. // Show the correct status for seeking.
bool backup = mAlwaysSeeking; bool backup = mAlwaysSeeking;
mAlwaysSeeking = seek; mAlwaysSeeking = seek;
const auto ctb = mProject->GetControlToolBar(); const auto ctb = mProject->GetControlToolBar();
ctb->UpdateStatusBar(mProject); if (ctb)
ctb->UpdateStatusBar(mProject);
mAlwaysSeeking = backup; mAlwaysSeeking = backup;
} }
const wxPoint position = trackPanel->ScreenToClient(state.GetPosition()); if (seek)
const auto &viewInfo = mProject->GetViewInfo(); mScrubSpeedDisplayCountdown = 0;
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 (mSmoothScrollingScrub) if (mSmoothScrollingScrub)
; ;
@ -441,6 +496,13 @@ void Scrubber::ContinueScrubbing()
void Scrubber::StopScrubbing() void Scrubber::StopScrubbing()
{ {
#ifdef USE_SCRUB_THREAD
if (mpThread) {
mpThread->Delete();
mpThread = nullptr;
}
#endif
mPoller->Stop(); mPoller->Stop();
UncheckAllMenuItems(); UncheckAllMenuItems();
@ -494,7 +556,7 @@ double Scrubber::FindScrubSpeed(bool seeking, double time) const
ViewInfo &viewInfo = mProject->GetViewInfo(); ViewInfo &viewInfo = mProject->GetViewInfo();
const double screen = mProject->GetScreenEndTime() - viewInfo.h; const double screen = mProject->GetScreenEndTime() - viewInfo.h;
return (seeking ? FindSeekSpeed : FindScrubbingSpeed) return (seeking ? FindSeekSpeed : FindScrubbingSpeed)
(viewInfo, mOptions.maxSpeed, screen, time); (viewInfo, mMaxSpeed, screen, time);
} }
void Scrubber::HandleScrollWheel(int steps) 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... // Not likely you would spin it with the left button down, but...
return; return;
if (steps == 0)
return;
const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps; const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
static const double maxScrubSpeedBase = static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave); pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
@ -510,7 +575,7 @@ void Scrubber::HandleScrollWheel(int steps)
if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() && if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() &&
newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) { newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) {
mLogMaxScrubSpeed = newLogMaxScrubSpeed; mLogMaxScrubSpeed = newLogMaxScrubSpeed;
mOptions.maxSpeed = newSpeed; mMaxSpeed = newSpeed;
if (!mSmoothScrollingScrub) if (!mSmoothScrollingScrub)
// Show the speed for one second // Show the speed for one second
mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1; mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1;
@ -717,6 +782,24 @@ bool Scrubber::PollIsSeeking()
return mDragging || (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown()); 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) void Scrubber::DoScrub(bool scroll, bool seek)
{ {
const bool wasScrubbing = IsScrubbing(); const bool wasScrubbing = IsScrubbing();
@ -735,9 +818,7 @@ void Scrubber::DoScrub(bool scroll, bool seek)
} }
else if(!match) { else if(!match) {
mSmoothScrollingScrub = scroll; mSmoothScrollingScrub = scroll;
using Mode = AudacityProject::PlaybackScroller::Mode; ActivateScroller();
mProject->GetPlaybackScroller().Activate
(scroll ? Mode::Centered : Mode::Off);
mAlwaysSeeking = seek; mAlwaysSeeking = seek;
UncheckAllMenuItems(); UncheckAllMenuItems();
CheckMenuItem(); CheckMenuItem();

View File

@ -21,6 +21,11 @@ Paul Licameli split from TrackPanel.cpp
class AudacityProject; 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 // For putting an increment of work in the scrubbing queue
struct ScrubbingOptions { struct ScrubbingOptions {
ScrubbingOptions() {} ScrubbingOptions() {}
@ -35,7 +40,8 @@ struct ScrubbingOptions {
double delay {}; 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 }; double maxSpeed { 1.0 };
@ -71,7 +77,8 @@ public:
// Assume xx is relative to the left edge of TrackPanel! // Assume xx is relative to the left edge of TrackPanel!
bool MaybeStartScrubbing(wxCoord xx); bool MaybeStartScrubbing(wxCoord xx);
void ContinueScrubbing(); void ContinueScrubbingUI();
void ContinueScrubbingPoll();
// This is meant to be called only from ControlToolBar // This is meant to be called only from ControlToolBar
void StopScrubbing(); void StopScrubbing();
@ -122,6 +129,7 @@ public:
bool IsPaused() const; bool IsPaused() const;
private: private:
void ActivateScroller();
void DoScrub(bool scroll, bool seek); void DoScrub(bool scroll, bool seek);
void OnActivateOrDeactivateApp(wxActivateEvent & event); void OnActivateOrDeactivateApp(wxActivateEvent & event);
void UncheckAllMenuItems(); void UncheckAllMenuItems();
@ -158,9 +166,20 @@ private:
DECLARE_EVENT_TABLE() 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; class ScrubPoller;
std::unique_ptr<ScrubPoller> mPoller; std::unique_ptr<ScrubPoller> mPoller;
ScrubbingOptions mOptions; ScrubbingOptions mOptions;
double mMaxSpeed { 1.0 };
}; };
// Specialist in drawing the scrub speed, and listening for certain events // Specialist in drawing the scrub speed, and listening for certain events