mirror of
https://github.com/cookiengineer/audacity
synced 2025-08-16 08:34:10 +02:00
Merge branch 'master' into scrubbing2
This commit is contained in:
commit
627fc1ca40
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,
|
||||
};
|
||||
|
@ -256,6 +256,19 @@ private:
|
||||
}
|
||||
#endif
|
||||
|
||||
friend inline bool operator ==
|
||||
(const SelectedRegion &lhs, const SelectedRegion &rhs)
|
||||
{
|
||||
return
|
||||
lhs.mT0 == rhs.mT0
|
||||
&& lhs.mT1 == rhs.mT1
|
||||
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
||||
&& lhs.mF0 == rhs.mF0
|
||||
&& lhs.mF1 == rhs.mF1
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
double mT0;
|
||||
double mT1;
|
||||
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
||||
@ -265,4 +278,9 @@ private:
|
||||
|
||||
};
|
||||
|
||||
inline bool operator != (const SelectedRegion &lhs, const SelectedRegion &rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -659,6 +659,7 @@ void TrackPanel::BuildMenus(void)
|
||||
mTimeTrackMenu->Append(OnSetTimeTrackRangeID, _("&Range..."));
|
||||
mTimeTrackMenu->AppendCheckItem(OnTimeTrackLogIntID, _("Logarithmic &Interpolation"));
|
||||
|
||||
/*
|
||||
mRulerWaveformMenu = new wxMenu();
|
||||
BuildVRulerMenuItems
|
||||
(mRulerWaveformMenu, OnFirstWaveformScaleID,
|
||||
@ -668,6 +669,7 @@ void TrackPanel::BuildMenus(void)
|
||||
BuildVRulerMenuItems
|
||||
(mRulerSpectrumMenu, OnFirstSpectrumScaleID,
|
||||
SpectrogramSettings::GetScaleNames());
|
||||
*/
|
||||
}
|
||||
|
||||
void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu)
|
||||
@ -686,6 +688,8 @@ void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
// left over from PRL's vertical ruler context menu experiment in 2.1.2
|
||||
// static
|
||||
void TrackPanel::BuildVRulerMenuItems
|
||||
(wxMenu * menu, int firstId, const wxArrayString &names)
|
||||
@ -698,6 +702,7 @@ void TrackPanel::BuildVRulerMenuItems
|
||||
menu->Append(OnZoomOutVerticalID, _("Zoom Out\tShift-Left-Click"));
|
||||
menu->Append(OnZoomFitVerticalID, _("Zoom to Fit\tShift-Right-Click"));
|
||||
}
|
||||
*/
|
||||
|
||||
void TrackPanel::DeleteMenus(void)
|
||||
{
|
||||
@ -944,6 +949,9 @@ void TrackPanel::OnTimer(wxTimerEvent& )
|
||||
//ANSWER-ME: Was DisplaySelection added to solve a repaint problem?
|
||||
DisplaySelection();
|
||||
}
|
||||
if (mLastDrawnSelectedRegion != mViewInfo->selectedRegion) {
|
||||
UpdateSelectionDisplay();
|
||||
}
|
||||
|
||||
// Notify listeners for timer ticks
|
||||
{
|
||||
@ -1045,6 +1053,8 @@ double TrackPanel::GetScreenEndTime() const
|
||||
/// completing a repaint operation.
|
||||
void TrackPanel::OnPaint(wxPaintEvent & /* event */)
|
||||
{
|
||||
mLastDrawnSelectedRegion = mViewInfo->selectedRegion;
|
||||
|
||||
#if DEBUG_DRAW_TIMING
|
||||
wxStopWatch sw;
|
||||
#endif
|
||||
@ -2764,7 +2774,12 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
|
||||
#endif
|
||||
|
||||
ExtendSelection(x, rect.x, clickedTrack);
|
||||
UpdateSelectionDisplay();
|
||||
// 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();
|
||||
}
|
||||
|
||||
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
||||
@ -5506,6 +5521,14 @@ void TrackPanel::HandleResize(wxMouseEvent & event)
|
||||
/// Handle mouse wheel rotation (for zoom in/out, vertical and horizontal scrolling)
|
||||
void TrackPanel::HandleWheelRotation(wxMouseEvent & event)
|
||||
{
|
||||
if(event.GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL) {
|
||||
// Two-fingered horizontal swipe on mac is treated like shift-mousewheel
|
||||
event.SetShiftDown(true);
|
||||
// This makes the wave move in the same direction as the fingers, and the scrollbar
|
||||
// thumb moves oppositely
|
||||
event.m_wheelRotation *= -1;
|
||||
}
|
||||
|
||||
if(!event.HasAnyModifiers()) {
|
||||
// We will later un-skip if we do anything, but if we don't,
|
||||
// propagate the event up for the sake of the scrubber
|
||||
@ -5603,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
|
||||
@ -242,7 +241,10 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel {
|
||||
* @param menu the menu to add the commands to.
|
||||
*/
|
||||
virtual void BuildCommonDropMenuItems(wxMenu * menu);
|
||||
static void BuildVRulerMenuItems(wxMenu * menu, int firstId, const wxArrayString &names);
|
||||
|
||||
// left over from PRL's vertical ruler context menu experiment in 2.1.2
|
||||
// static void BuildVRulerMenuItems(wxMenu * menu, int firstId, const wxArrayString &names);
|
||||
|
||||
virtual bool IsAudioActive();
|
||||
virtual bool IsUnsafe();
|
||||
virtual bool HandleLabelTrackClick(LabelTrack * lTrack, wxRect &rect, wxMouseEvent & event);
|
||||
@ -790,6 +792,8 @@ protected:
|
||||
// The screenshot class needs to access internals
|
||||
friend class ScreenshotCommand;
|
||||
|
||||
SelectedRegion mLastDrawnSelectedRegion {};
|
||||
|
||||
public:
|
||||
wxSize vrulerSize;
|
||||
|
||||
|
@ -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
|
||||
|
@ -36,13 +36,14 @@
|
||||
#include "ErrorDialog.h"
|
||||
#include "HelpSystem.h"
|
||||
|
||||
const wxString HelpSystem::HelpHostname = wxT("manual.audacityteam.org");
|
||||
#if IS_ALPHA
|
||||
const wxString HelpSystem::HelpHostname = wxT("alphamanual.audacityteam.org");
|
||||
const wxString HelpSystem::HelpServerHomeDir = wxT("/man/");
|
||||
const wxString HelpSystem::HelpServerManDir = wxT("/man/");
|
||||
#else
|
||||
const wxString HelpSystem::HelpServerHomeDir = wxT("/o/");
|
||||
const wxString HelpSystem::HelpServerManDir = wxT("/o/man/");
|
||||
const wxString HelpSystem::HelpHostname = wxT("manual.audacityteam.org");
|
||||
const wxString HelpSystem::HelpServerHomeDir = wxT("/");
|
||||
const wxString HelpSystem::HelpServerManDir = wxT("/man/");
|
||||
#endif
|
||||
const wxString HelpSystem::LocalHelpManDir = wxT("/man/");
|
||||
const wxString HelpSystem::ReleaseSuffix = wxT(".html");
|
||||
|
Loading…
x
Reference in New Issue
Block a user