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:
commit
94325b0ffb
377
src/AudioIO.cpp
377
src/AudioIO.cpp
@ -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++)
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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();
|
||||||
|
@ -730,6 +730,7 @@ public:
|
|||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
Off,
|
Off,
|
||||||
|
Refresh,
|
||||||
Centered,
|
Centered,
|
||||||
Right,
|
Right,
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user