1
0
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:
Paul Licameli 2016-05-28 12:15:10 -04:00
commit 627fc1ca40
10 changed files with 476 additions and 224 deletions

View File

@ -372,32 +372,35 @@ So a small, fixed queue size should be adequate.
struct AudioIO::ScrubQueue
{
ScrubQueue(double t0, double t1, wxLongLong startClockMillis,
double rate, double maxSpeed,
double rate, long maxDebt,
const ScrubbingOptions &options)
: mTrailingIdx(0)
, mMiddleIdx(1)
, mLeadingIdx(2)
, mLeadingIdx(1)
, mRate(rate)
, mLastScrubTimeMillis(startClockMillis)
, mUpdating()
, mMaxDebt { maxDebt }
{
// Ignore options.adjustStart, pass false.
bool success = InitEntry(mEntries[mMiddleIdx], nullptr,
t0, t1, maxSpeed, false, false, options);
if (!success)
{
// StartClock equals now? Really?
--mLastScrubTimeMillis;
success = InitEntry(mEntries[mMiddleIdx], nullptr,
t0, t1, maxSpeed, false, false, options);
const long s0 = std::max(options.minSample, std::min(options.maxSample,
lrint(t0 * mRate)
));
const long s1 = lrint(t1 * mRate);
Duration dd { *this };
long actualDuration = std::max(1L, dd.duration);
auto success = mEntries[mMiddleIdx].Init(nullptr,
s0, s1, actualDuration, options);
if (success)
++mLeadingIdx;
else {
// If not, we can wait to enqueue again later
dd.Cancel();
}
wxASSERT(success);
// So the play indicator starts out unconfused:
{
Entry &entry = mEntries[mTrailingIdx];
entry.mS0 = entry.mS1 = mEntries[mMiddleIdx].mS0;
entry.mS0 = entry.mS1 = s0;
entry.mPlayed = entry.mDuration = 1;
}
}
@ -420,30 +423,57 @@ struct AudioIO::ScrubQueue
mAvailable.Signal();
}
bool Producer(double end, double maxSpeed, const ScrubbingOptions &options)
bool Producer(double end, const ScrubbingOptions &options)
{
// Main thread indicates a scrubbing interval
// MAY ADVANCE mLeadingIdx, BUT IT NEVER CATCHES UP TO mTrailingIdx.
wxMutexLocker locker(mUpdating);
const unsigned next = (mLeadingIdx + 1) % Size;
bool result = true;
unsigned next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx)
{
Entry &previous = mEntries[(mLeadingIdx + Size - 1) % Size];
auto current = &mEntries[mLeadingIdx];
auto previous = &mEntries[(mLeadingIdx + Size - 1) % Size];
// Use the previous end as NEW start.
const double startTime = previous.mS1 / mRate;
// Might reject the request because of zero duration,
// or a too-short "stutter"
const bool success =
(InitEntry(mEntries[mLeadingIdx], &previous, startTime, end, maxSpeed,
options.enqueueBySpeed, options.adjustStart, options));
if (success) {
const long s0 = previous->mS1;
Duration dd { *this };
const auto &origDuration = dd.duration;
if (origDuration <= 0)
return false;
auto actualDuration = origDuration;
const long s1 = options.enqueueBySpeed
? s0 + lrint(origDuration * end) // end is a speed
: lrint(end * mRate); // end is a time
auto success =
current->Init(previous, s0, s1, actualDuration, options);
if (success)
mLeadingIdx = next;
mAvailable.Signal();
else {
dd.Cancel();
return false;
}
return success;
// Fill up the queue with some silence if there was trimming
wxASSERT(actualDuration <= origDuration);
if (actualDuration < origDuration) {
next = (mLeadingIdx + 1) % Size;
if (next != mTrailingIdx) {
previous = &mEntries[(mLeadingIdx + Size - 1) % Size];
current = &mEntries[mLeadingIdx];
current->InitSilent(*previous, origDuration - actualDuration);
mLeadingIdx = next;
}
else
// Oops, can't enqueue the silence -- so do what?
;
}
mAvailable.Signal();
return result;
}
else
{
@ -456,33 +486,85 @@ struct AudioIO::ScrubQueue
}
}
void Transformer(long &startSample, long &endSample, long &duration)
void Transformer(long &startSample, long &endSample, long &duration,
Maybe<wxMutexLocker> &cleanup)
{
// Audio thread is ready for the next interval.
// MAY ADVANCE mMiddleIdx, WHICH MAY EQUAL mLeadingIdx, BUT DOES NOT PASS IT.
wxMutexLocker locker(mUpdating);
bool checkDebt = false;
if (!cleanup) {
cleanup.create(mUpdating);
// Check for cancellation of work only when re-enetering the cricial section
checkDebt = true;
}
while(!mNudged && mMiddleIdx == mLeadingIdx)
mAvailable.Wait();
mNudged = false;
if (mMiddleIdx != mLeadingIdx)
{
// There is work in the queue
auto now = ::wxGetLocalTimeMillis();
if (checkDebt &&
mLastTransformerTimeMillis >= 0 && // Not the first time for this scrub
mMiddleIdx != mLeadingIdx) {
// There is work in the queue, but if Producer is outrunning us, discard some,
// which may make a skip yet keep playback better synchronized with user gestures.
const auto interval = (now - mLastTransformerTimeMillis).ToDouble() / 1000.0;
const Entry &previous = mEntries[(mMiddleIdx + Size - 1) % Size];
const auto deficit =
static_cast<long>(interval * mRate) - // Samples needed in the last time interval
mCredit; // Samples done in the last time interval
mCredit = 0;
mDebt += deficit;
auto toDiscard = mDebt - mMaxDebt;
while (toDiscard > 0 && mMiddleIdx != mLeadingIdx) {
// Cancel some debt (discard some new work)
auto &entry = mEntries[mMiddleIdx];
auto &dur = entry.mDuration;
if (toDiscard >= dur) {
// Discard entire queue entry
mDebt -= dur;
toDiscard -= dur;
dur = 0; // So Consumer() will handle abandoned entry correctly
mMiddleIdx = (mMiddleIdx + 1) % Size;
}
else {
// Adjust the start time
auto &start = entry.mS0;
const auto end = entry.mS1;
const auto ratio = static_cast<double>(toDiscard) / static_cast<double>(dur);
const auto adjustment = static_cast<long>(std::abs(end - start) * ratio);
if (start <= end)
start += adjustment;
else
start -= adjustment;
mDebt -= toDiscard;
dur -= toDiscard;
toDiscard = 0;
}
}
}
if (mMiddleIdx != mLeadingIdx) {
// There is still work in the queue, after cancelling debt
Entry &entry = mEntries[mMiddleIdx];
startSample = entry.mS0;
endSample = entry.mS1;
duration = entry.mDuration;
const unsigned next = (mMiddleIdx + 1) % Size;
mMiddleIdx = next;
mMiddleIdx = (mMiddleIdx + 1) % Size;
mCredit += duration;
}
else
{
// We got the shut-down signal, or we got nudged
else {
// We got the shut-down signal, or we got nudged, or we discarded all the work.
startSample = endSample = duration = -1L;
}
if (checkDebt)
mLastTransformerTimeMillis = now;
}
double Consumer(unsigned long frames)
@ -531,21 +613,26 @@ private:
, mPlayed(0)
{}
bool Init(Entry *previous, long s0, long s1, long duration,
double maxSpeed, bool adjustStart,
bool Init(Entry *previous, long s0, long s1,
long &duration /* in/out */,
const ScrubbingOptions &options)
{
if (duration <= 0)
return false;
double speed = double(abs(s1 - s0)) / duration;
bool maxed = false;
const bool &adjustStart = options.adjustStart;
// May change the requested speed (or reject)
if (!adjustStart && speed > maxSpeed)
wxASSERT(duration > 0);
double speed = static_cast<double>(std::abs(s1 - s0)) / duration;
bool adjustedSpeed = false;
auto minSpeed = std::min(options.minSpeed, options.maxSpeed);
wxASSERT(minSpeed == options.minSpeed);
// May change the requested speed and duration
if (!adjustStart && speed > options.maxSpeed)
{
// Reduce speed to the maximum selected in the user interface.
speed = maxSpeed;
maxed = true;
speed = options.maxSpeed;
mGoal = s1;
adjustedSpeed = true;
}
else if (!adjustStart &&
previous &&
@ -557,86 +644,76 @@ private:
// continue at no less than maximum. (Without this
// the final catch-up can make a slow scrub interval
// that drops the pitch and sounds wrong.)
duration = lrint(speed * duration / maxSpeed);
if (duration <= 0)
{
previous->mGoal = -1;
return false;
}
speed = maxSpeed;
maxed = true;
}
if (speed < ScrubbingOptions::MinAllowedScrubSpeed())
// Mixers were set up to go only so slowly, not slower.
// This will put a request for some silence in the work queue.
speed = 0.0;
// Before we change s1:
mGoal = maxed ? s1 : -1;
// May change s1 or s0 to match speed change:
if (adjustStart)
{
bool silent = false;
// Adjust s1 first, and duration, if s1 is out of bounds.
// (Assume s0 is in bounds, because it is the last scrub's s1 which was checked.)
if (s1 != s0)
{
const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1));
if (s1 != newS1)
{
long newDuration = long(duration * double(newS1 - s0) / (s1 - s0));
s1 = newS1;
if (newDuration == 0)
// Enqueue a silent scrub with s0 == s1
silent = true;
else
// Shorten
duration = newDuration;
}
}
if (!silent)
{
// When playback follows a fast mouse movement by "stuttering"
// at maximum playback, don't make stutters too short to be useful.
if (duration < options.minStutter)
return false;
// Limit diff because this is seeking.
const long diff = lrint(std::min(1.0, speed) * duration);
if (s0 < s1)
s0 = s1 - diff;
else
s0 = s1 + diff;
}
minSpeed = options.maxSpeed;
mGoal = s1;
adjustedSpeed = true;
}
else
mGoal = -1;
if (speed < minSpeed) {
// Trim the duration.
duration = std::max(0L, lrint(speed * duration / minSpeed));
speed = minSpeed;
adjustedSpeed = true;
}
if (speed < ScrubbingOptions::MinAllowedScrubSpeed()) {
// Mixers were set up to go only so slowly, not slower.
// This will put a request for some silence in the work queue.
adjustedSpeed = true;
speed = 0.0;
}
// May change s1 or s0 to match speed change or stay in bounds of the project
if (adjustedSpeed && !adjustStart)
{
// adjust end
// adjust s1
const long diff = lrint(speed * duration);
if (s0 < s1)
s1 = s0 + diff;
else
s1 = s0 - diff;
}
// Adjust s1 again, and duration, if s1 is out of bounds. (Assume s0 is in bounds.)
if (s1 != s0)
{
const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1));
if (s1 != newS1)
{
long newDuration = long(duration * double(newS1 - s0) / (s1 - s0));
s1 = newS1;
if (newDuration == 0)
// Enqueue a silent scrub with s0 == s1
;
else
// Shorten
duration = newDuration;
}
bool silent = false;
// Adjust s1 (again), and duration, if s1 is out of bounds,
// or abandon if a stutter is too short.
// (Assume s0 is in bounds, because it equals the last scrub's s1 which was checked.)
if (s1 != s0)
{
long newDuration = duration;
const long newS1 = std::max(options.minSample, std::min(options.maxSample, s1));
if(s1 != newS1)
newDuration = std::max(0L,
static_cast<long>(duration * static_cast<double>(newS1 - s0) / (s1 - s0))
);
// When playback follows a fast mouse movement by "stuttering"
// at maximum playback, don't make stutters too short to be useful.
if (options.adjustStart && newDuration < options.minStutter)
return false;
else if (newDuration == 0) {
// Enqueue a silent scrub with s0 == s1
silent = true;
s1 = s0;
}
else if (s1 != newS1) {
// Shorten
duration = newDuration;
s1 = newS1;
}
}
if (adjustStart && !silent)
{
// Limit diff because this is seeking.
const long diff = lrint(std::min(options.maxSpeed, speed) * duration);
if (s0 < s1)
s0 = s1 - diff;
else
s0 = s1 + diff;
}
mS0 = s0;
@ -646,9 +723,20 @@ private:
return true;
}
void InitSilent(const Entry &previous, long duration)
{
mGoal = previous.mGoal;
mS0 = mS1 = previous.mS1;
mPlayed = 0;
mDuration = duration;
}
double GetTime(double rate) const
{
return (mS0 + ((mS1 - mS0) * mPlayed) / double(mDuration)) / rate;
return
(mS0 +
(mS1 - mS0) * static_cast<double>(mPlayed) / static_cast<double>(mDuration))
/ rate;
}
// These sample counts are initialized in the UI, producer, thread:
@ -668,23 +756,23 @@ private:
long mPlayed;
};
bool InitEntry(Entry &entry, Entry *previous, double t0, double end, double maxSpeed,
bool bySpeed, bool adjustStart,
const ScrubbingOptions &options)
{
const wxLongLong clockTime(::wxGetLocalTimeMillis());
const long duration =
mRate * (clockTime - mLastScrubTimeMillis).ToDouble() / 1000.0;
const long s0 = t0 * mRate;
const long s1 = bySpeed
? s0 + lrint(duration * end) // end is a speed
: lrint(end * mRate); // end is a time
const bool success =
entry.Init(previous, s0, s1, duration, maxSpeed, adjustStart, options);
if (success)
mLastScrubTimeMillis = clockTime;
return success;
}
struct Duration {
Duration (ScrubQueue &queue_) : queue(queue_) {}
~Duration ()
{
if(!cancelled)
queue.mLastScrubTimeMillis = clockTime;
}
void Cancel() { cancelled = true; }
ScrubQueue &queue;
const wxLongLong clockTime { ::wxGetLocalTimeMillis() };
const long duration { static_cast<long>
(queue.mRate * (clockTime - queue.mLastScrubTimeMillis).ToDouble() / 1000.0)
};
bool cancelled { false };
};
enum { Size = 10 };
Entry mEntries[Size];
@ -693,6 +781,12 @@ private:
unsigned mLeadingIdx;
const double mRate;
wxLongLong mLastScrubTimeMillis;
wxLongLong mLastTransformerTimeMillis { -1LL };
long mCredit { 0L };
long mDebt { 0L };
const long mMaxDebt;
mutable wxMutex mUpdating;
mutable wxCondition mAvailable { mUpdating };
bool mNudged { false };
@ -1863,8 +1957,8 @@ int AudioIO::StartStream(const WaveTrackArray &playbackTracks,
const auto &scrubOptions = *options.pScrubbingOptions;
mScrubQueue =
new ScrubQueue(mT0, mT1, scrubOptions.startClockTimeMillis,
sampleRate, scrubOptions.maxSpeed,
*options.pScrubbingOptions);
sampleRate, 2 * scrubOptions.minStutter,
scrubOptions);
mScrubDuration = 0;
mSilentScrub = false;
}
@ -2462,10 +2556,10 @@ bool AudioIO::IsPaused()
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
bool AudioIO::EnqueueScrub
(double endTimeOrSpeed, double maxSpeed, const ScrubbingOptions &options)
(double endTimeOrSpeed, const ScrubbingOptions &options)
{
if (mScrubQueue)
return mScrubQueue->Producer(endTimeOrSpeed, maxSpeed, options);
return mScrubQueue->Producer(endTimeOrSpeed, options);
else
return false;
}
@ -3367,6 +3461,7 @@ void AudioIO::FillBuffers()
// PRL: or, when scrubbing, we may get work repeatedly from the
// scrub queue.
bool done = false;
Maybe<wxMutexLocker> cleanup;
do {
// How many samples to produce for each channel.
long frames = available;
@ -3442,7 +3537,7 @@ void AudioIO::FillBuffers()
if (!done && mScrubDuration <= 0)
{
long startSample, endSample;
mScrubQueue->Transformer(startSample, endSample, mScrubDuration);
mScrubQueue->Transformer(startSample, endSample, mScrubDuration, cleanup);
if (mScrubDuration < 0)
{
// Can't play anything
@ -3458,7 +3553,7 @@ void AudioIO::FillBuffers()
double startTime, endTime, speed;
startTime = startSample / mRate;
endTime = endSample / mRate;
speed = double(abs(endSample - startSample)) / mScrubDuration;
speed = double(std::abs(endSample - startSample)) / mScrubDuration;
for (i = 0; i < mPlaybackTracks->size(); i++)
mPlaybackMixers[i]->SetTimesAndSpeed(startTime, endTime, speed);
}
@ -4179,7 +4274,7 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
(gAudioIO->mT0, gAudioIO->mTime);
else
gAudioIO->mWarpedTime = gAudioIO->mTime - gAudioIO->mT0;
gAudioIO->mWarpedTime = abs(gAudioIO->mWarpedTime);
gAudioIO->mWarpedTime = std::abs(gAudioIO->mWarpedTime);
// Reset mixer positions and flush buffers for all tracks
for (i = 0; i < (unsigned int)numPlaybackTracks; i++)

View File

@ -169,11 +169,10 @@ class AUDACITY_DLL_API AudioIO final {
* If options.adjustStart is true, then when mouse movement exceeds maximum scrub speed,
* adjust the beginning of the scrub interval rather than the end, so that
* the scrub skips or "stutters" to stay near the cursor.
* But if the "stutter" is too short for the minimum, then there is no effect
* on the work queue.
* Return true if some work was really enqueued.
* Return true if some sound was really enqueued.
* But if the "stutter" is too short for the minimum, enqueue nothing and return false.
*/
bool EnqueueScrub(double endTimeOrSpeed, double maxSpeed, const ScrubbingOptions &options);
bool EnqueueScrub(double endTimeOrSpeed, const ScrubbingOptions &options);
/** \brief return the ending time of the last enqueued scrub interval.
*/

View File

@ -5378,8 +5378,18 @@ void AudacityProject::PlaybackScroller::OnTimer(wxCommandEvent &event)
// Let other listeners get the notification
event.Skip();
if (mMode != Mode::Off && mProject->IsAudioActive())
{
if(!mProject->IsAudioActive())
return;
else if (mMode == Mode::Refresh) {
// PRL: see comments in Scrubbing.cpp for why this is sometimes needed.
// These unnecessary refreshes cause wheel rotation events to be delivered more uniformly
// to the application, so scrub speed control is smoother.
// (So I see at least with OS 10.10 and wxWidgets 3.0.2.)
// Is there another way to ensure that than by refreshing?
const auto trackPanel = mProject->GetTrackPanel();
trackPanel->Refresh(false);
}
else if (mMode != Mode::Off) {
// Pan the view, so that we center the play indicator.
ViewInfo &viewInfo = mProject->GetViewInfo();

View File

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

View File

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

View File

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

View File

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

View File

@ -46,6 +46,8 @@ enum {
#endif
ScrubPollInterval_ms = 50,
kOneSecondCountdown = 1000 / ScrubPollInterval_ms,
};
static const double MinStutter = 0.2;
@ -123,6 +125,32 @@ namespace {
}
}
#ifdef USE_SCRUB_THREAD
class Scrubber::ScrubPollerThread final : public wxThread {
public:
ScrubPollerThread(Scrubber &scrubber)
: wxThread { }
, mScrubber(scrubber)
{}
ExitCode Entry() override;
private:
Scrubber &mScrubber;
};
auto Scrubber::ScrubPollerThread::Entry() -> ExitCode
{
while( !TestDestroy() )
{
wxThread::Sleep(ScrubPollInterval_ms);
mScrubber.ContinueScrubbingPoll();
}
return 0;
}
#endif
class Scrubber::ScrubPoller : public wxTimer
{
public:
@ -140,7 +168,13 @@ void Scrubber::ScrubPoller::Notify()
// rather than in SelectionHandleDrag()
// so that even without drag events, we can instruct the play head to
// keep approaching the mouse cursor, when its maximum speed is limited.
mScrubber.ContinueScrubbing();
#ifndef USE_SCRUB_THREAD
// If there is no helper thread, this main thread timer is responsible
// for playback and for UI
mScrubber.ContinueScrubbingPoll();
#endif
mScrubber.ContinueScrubbingUI();
}
Scrubber::Scrubber(AudacityProject *project)
@ -167,6 +201,11 @@ Scrubber::Scrubber(AudacityProject *project)
Scrubber::~Scrubber()
{
#ifdef USE_SCRUB_THREAD
if (mpThread)
mpThread->Delete();
#endif
mProject->PopEventHandler();
if (wxTheApp)
wxTheApp->Disconnect
@ -301,18 +340,19 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
AudioIOStartStreamOptions options(mProject->GetDefaultPlayOptions());
options.pScrubbingOptions = &mOptions;
options.timeTrack = NULL;
mOptions.delay = (ScrubPollInterval_ms / 1000.0);
mOptions.delay = (ScrubPollInterval_ms * 0.9 / 1000.0);
mOptions.minSpeed = 0.0;
#ifdef USE_TRANSCRIPTION_TOOLBAR
if (!mAlwaysSeeking) {
// Take the starting speed limit from the transcription toolbar,
// but it may be varied during the scrub.
mOptions.maxSpeed =
mMaxSpeed = mOptions.maxSpeed =
mProject->GetTranscriptionToolBar()->GetPlaySpeed();
}
#else
// That idea seems unpopular... just make it one for move-scrub,
// but big for drag-scrub
mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
#endif
mOptions.minSample = 0;
mOptions.maxSample =
@ -328,7 +368,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
mLogMaxScrubSpeed = floor(0.5 +
log(mOptions.maxSpeed) / log(maxScrubSpeedBase)
log(mMaxSpeed) / log(maxScrubSpeedBase)
);
#endif
mScrubSpeedDisplayCountdown = 0;
@ -342,12 +382,17 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
mOptions.startClockTimeMillis = ::wxGetLocalTimeMillis();
if (IsScrubbing()) {
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate
(mSmoothScrollingScrub ? Mode::Centered : Mode::Off);
ActivateScroller();
mPaused = false;
mLastScrubPosition = xx;
#ifdef USE_SCRUB_THREAD
// Detached thread is self-deleting, after it receives the Delete() message
mpThread = safenew ScrubPollerThread{ *this };
mpThread->Create(4096);
mpThread->Run();
#endif
mPoller->Start(ScrubPollInterval_ms);
}
@ -356,7 +401,67 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
}
}
void Scrubber::ContinueScrubbing()
void Scrubber::ContinueScrubbingPoll()
{
// Thus scrubbing relies mostly on periodic polling of mouse and keys,
// not event notifications. But there are a few event handlers that
// leave messages for this routine, in mScrubSeekPress and in mPaused.
// Decide whether to skip play, because either mouse is down now,
// or there was a left click event. (This is then a delayed reaction, in a
// timer callback, to a left click event detected elsewhere.)
const bool seek = PollIsSeeking() || mScrubSeekPress;
bool result = false;
if (mPaused) {
// When paused, enqueue silent scrubs.
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = false;
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(0, mOptions);
}
else {
const wxMouseState state(::wxGetMouseState());
const auto trackPanel = mProject->GetTrackPanel();
const wxPoint position = trackPanel->ScreenToClient(state.GetPosition());
const auto &viewInfo = mProject->GetViewInfo();
if (mDragging && mSmoothScrollingScrub) {
const auto lastTime = gAudioIO->GetLastTimeInScrubQueue();
const auto delta = mLastScrubPosition - position.x;
const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = true;
mOptions.enqueueBySpeed = false;
result = gAudioIO->EnqueueScrub(time, mOptions);
mLastScrubPosition = position.x;
}
else {
const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset());
mOptions.adjustStart = seek;
mOptions.minSpeed = (mDragging || !seek) ? 0.0 : 1.0;
mOptions.maxSpeed = (mDragging || !seek) ? mMaxSpeed : 1.0;
if (mSmoothScrollingScrub) {
const double speed = FindScrubSpeed(seek, time);
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(speed, mOptions);
}
else {
mOptions.enqueueBySpeed = false;
result = gAudioIO->EnqueueScrub(time, mOptions);
}
}
}
if (result)
mScrubSeekPress = false;
// else, if seek requested, try again at a later time when we might
// enqueue a long enough stutter
}
void Scrubber::ContinueScrubbingUI()
{
const wxMouseState state(::wxGetMouseState());
@ -366,70 +471,20 @@ void Scrubber::ContinueScrubbing()
return;
}
// Thus scrubbing relies mostly on periodic polling of mouse and keys,
// not event notifications. But there are a few event handlers that
// leave messages for this routine, in mScrubSeekPress and in mPaused.
// Seek only when the pointer is in the panel. Else, scrub.
TrackPanel *const trackPanel = mProject->GetTrackPanel();
// Decide whether to skip play, because either mouse is down now,
// or there was a left click event. (This is then a delayed reaction, in a
// timer callback, to a left click event detected elsewhere.)
const bool seek = PollIsSeeking() || mScrubSeekPress;
const bool seek = PollIsSeeking();
{
// Show the correct status for seeking.
bool backup = mAlwaysSeeking;
mAlwaysSeeking = seek;
const auto ctb = mProject->GetControlToolBar();
ctb->UpdateStatusBar(mProject);
if (ctb)
ctb->UpdateStatusBar(mProject);
mAlwaysSeeking = backup;
}
const wxPoint position = trackPanel->ScreenToClient(state.GetPosition());
const auto &viewInfo = mProject->GetViewInfo();
bool result = false;
if (mPaused) {
// When paused, enqueue silent scrubs.
mOptions.adjustStart = false;
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(0, mOptions.maxSpeed, mOptions);
}
else if (mDragging && mSmoothScrollingScrub) {
const auto lastTime = gAudioIO->GetLastTimeInScrubQueue();
const auto delta = mLastScrubPosition - position.x;
const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
mOptions.adjustStart = true;
mOptions.enqueueBySpeed = false;
result = gAudioIO->EnqueueScrub(time, mOptions.maxSpeed, mOptions);
mLastScrubPosition = position.x;
}
else {
const double time = viewInfo.PositionToTime(position.x, trackPanel->GetLeftOffset());
mOptions.adjustStart = seek;
if (seek)
// Cause OnTimer() to suppress the speed display
mScrubSpeedDisplayCountdown = 1;
if (mSmoothScrollingScrub) {
const double speed = FindScrubSpeed(seek, time);
mOptions.enqueueBySpeed = true;
result = gAudioIO->EnqueueScrub(speed, mOptions.maxSpeed, mOptions);
}
else {
mOptions.enqueueBySpeed = false;
auto maxSpeed =
(mDragging || !seek) ? mOptions.maxSpeed : 1.0;
result = gAudioIO->EnqueueScrub(time, maxSpeed, mOptions);
}
}
if (result)
mScrubSeekPress = false;
// else, if seek requested, try again at a later time when we might
// enqueue a long enough stutter
if (seek)
mScrubSpeedDisplayCountdown = 0;
if (mSmoothScrollingScrub)
;
@ -441,6 +496,13 @@ void Scrubber::ContinueScrubbing()
void Scrubber::StopScrubbing()
{
#ifdef USE_SCRUB_THREAD
if (mpThread) {
mpThread->Delete();
mpThread = nullptr;
}
#endif
mPoller->Stop();
UncheckAllMenuItems();
@ -494,7 +556,7 @@ double Scrubber::FindScrubSpeed(bool seeking, double time) const
ViewInfo &viewInfo = mProject->GetViewInfo();
const double screen = mProject->GetScreenEndTime() - viewInfo.h;
return (seeking ? FindSeekSpeed : FindScrubbingSpeed)
(viewInfo, mOptions.maxSpeed, screen, time);
(viewInfo, mMaxSpeed, screen, time);
}
void Scrubber::HandleScrollWheel(int steps)
@ -503,6 +565,9 @@ void Scrubber::HandleScrollWheel(int steps)
// Not likely you would spin it with the left button down, but...
return;
if (steps == 0)
return;
const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
@ -510,7 +575,7 @@ void Scrubber::HandleScrollWheel(int steps)
if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() &&
newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) {
mLogMaxScrubSpeed = newLogMaxScrubSpeed;
mOptions.maxSpeed = newSpeed;
mMaxSpeed = newSpeed;
if (!mSmoothScrollingScrub)
// Show the speed for one second
mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1;
@ -717,6 +782,24 @@ bool Scrubber::PollIsSeeking()
return mDragging || (mAlwaysSeeking || ::wxGetMouseState().LeftIsDown());
}
void Scrubber::ActivateScroller()
{
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate(mSmoothScrollingScrub
? Mode::Centered
:
#ifdef __WXMAC__
// PRL: cause many "unnecessary" refreshes. For reasons I don't understand,
// doing this causes wheel rotation events (mapped from the double finger vertical
// swipe) to be delivered more uniformly to the application, so that spped control
// works better.
Mode::Refresh
#else
Mode::Off
#endif
);
}
void Scrubber::DoScrub(bool scroll, bool seek)
{
const bool wasScrubbing = IsScrubbing();
@ -735,9 +818,7 @@ void Scrubber::DoScrub(bool scroll, bool seek)
}
else if(!match) {
mSmoothScrollingScrub = scroll;
using Mode = AudacityProject::PlaybackScroller::Mode;
mProject->GetPlaybackScroller().Activate
(scroll ? Mode::Centered : Mode::Off);
ActivateScroller();
mAlwaysSeeking = seek;
UncheckAllMenuItems();
CheckMenuItem();

View File

@ -21,6 +21,11 @@ Paul Licameli split from TrackPanel.cpp
class AudacityProject;
// Conditionally compile either a separate thead, or else use a timer in the main
// thread, to poll the mouse and update scrubbing speed and direction. The advantage of
// a thread may be immunity to choppy scrubbing in case redrawing takes too much time.
#define USE_SCRUB_THREAD
// For putting an increment of work in the scrubbing queue
struct ScrubbingOptions {
ScrubbingOptions() {}
@ -35,7 +40,8 @@ struct ScrubbingOptions {
double delay {};
// A limiting value for the speed of a scrub interval:
// Limiting values for the speed of a scrub interval:
double minSpeed { 0.0 };
double maxSpeed { 1.0 };
@ -71,7 +77,8 @@ public:
// Assume xx is relative to the left edge of TrackPanel!
bool MaybeStartScrubbing(wxCoord xx);
void ContinueScrubbing();
void ContinueScrubbingUI();
void ContinueScrubbingPoll();
// This is meant to be called only from ControlToolBar
void StopScrubbing();
@ -122,6 +129,7 @@ public:
bool IsPaused() const;
private:
void ActivateScroller();
void DoScrub(bool scroll, bool seek);
void OnActivateOrDeactivateApp(wxActivateEvent & event);
void UncheckAllMenuItems();
@ -158,9 +166,20 @@ private:
DECLARE_EVENT_TABLE()
#ifdef USE_SCRUB_THREAD
// Course corrections in playback are done in a helper thread, unhindered by
// the complications of the main event dispatch loop
class ScrubPollerThread;
ScrubPollerThread *mpThread {};
#endif
// Other periodic update of the UI must be done in the main thread,
// by this object which is driven by timer events.
class ScrubPoller;
std::unique_ptr<ScrubPoller> mPoller;
ScrubbingOptions mOptions;
double mMaxSpeed { 1.0 };
};
// Specialist in drawing the scrub speed, and listening for certain events

View File

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