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