audacia/src/tracks/ui/Scrubbing.cpp

1298 lines
37 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Scrubbing.cpp
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#include "../../Audacity.h"
#include "Scrubbing.h"
#include "../../Experimental.h"
#include <functional>
#include "../../AudioIO.h"
#include "../../CommonCommandFlags.h"
#include "../../Menus.h"
#include "../../Project.h"
#include "../../ProjectAudioIO.h"
#include "../../ProjectAudioManager.h"
#include "../../ProjectHistory.h"
#include "../../ProjectSettings.h"
#include "../../ProjectStatus.h"
#include "../../Track.h"
#include "../../ViewInfo.h"
#include "../../WaveTrack.h"
#include "../../prefs/PlaybackPrefs.h"
#include "../../prefs/TracksPrefs.h"
#include "../../toolbars/ToolManager.h"
#undef USE_TRANSCRIPTION_TOOLBAR
#include <algorithm>
#include <wx/app.h>
#include <wx/dc.h>
#include <wx/dcclient.h>
#include <wx/menu.h>
// Yet another experimental scrub would drag the track under a
// stationary play head
#undef DRAG_SCRUB
enum {
// PRL:
// Mouse must move at least this far to distinguish ctrl-drag to scrub
// from ctrl-click for playback.
SCRUBBING_PIXEL_TOLERANCE = 10,
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
ScrubSpeedStepsPerOctave = 4,
#endif
kOneSecondCountdown = 1000 / ScrubPollInterval_ms,
};
static const double MinStutter = 0.2;
// static const double MaxDragSpeed = 1.0;
namespace {
double FindScrubbingSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
{
// Map a time (which was mapped from a mouse position)
// to a speed.
// Map times to positive and negative speeds,
// with the time at the midline of the screen mapping to 0,
// and the extremes to the maximum scrub speed.
auto partScreen = screen * TracksPrefs::GetPinnedHeadPositionPreference();
const double origin = viewInfo.h + partScreen;
if (timeAtMouse >= origin)
partScreen = screen - partScreen;
// There are various snapping zones that are this fraction of screen:
const double snap = 0.05;
// By shrinking denom a bit, we make margins left and right
// that snap to maximum and negative maximum speeds.
const double factor = 1.0 - (snap * 2);
const double denom = factor * partScreen;
double fraction = (denom <= 0.0) ? 0.0 :
std::min(1.0, fabs(timeAtMouse - origin) / denom);
// Snap to 1.0 and -1.0
const double unity = 1.0 / maxScrubSpeed;
const double tolerance = snap / factor;
// Make speeds near 1 available too by remapping fractions outside
// this snap zone
if (fraction <= unity - tolerance)
fraction *= unity / (unity - tolerance);
else if (fraction < unity + tolerance)
fraction = unity;
else
fraction = unity + (fraction - (unity + tolerance)) *
(1.0 - unity) / (1.0 - (unity + tolerance));
double result = fraction * maxScrubSpeed;
if (timeAtMouse < origin)
result *= -1.0;
return result;
}
double FindSeekSpeed(const ViewInfo &viewInfo, double maxScrubSpeed, double screen, double timeAtMouse)
{
// Map a time (which was mapped from a mouse position)
// to a signed skip speed: a multiplier of the stutter duration,
// by which to advance the play position.
// (The stutter will play at unit speed.)
// Times near the midline of the screen map to skip-less play,
// and the extremes to a value proportional to maximum scrub speed.
// If the maximum scrubbing speed defaults to 1.0 when you begin to scroll-scrub,
// the extreme skipping for scroll-seek needs to be larger to be useful.
static const double ARBITRARY_MULTIPLIER = 10.0;
const double extreme = std::max(1.0, maxScrubSpeed * ARBITRARY_MULTIPLIER);
// Width of visible track area, in time terms:
auto partScreen = screen * TracksPrefs::GetPinnedHeadPositionPreference();
const double origin = viewInfo.h + partScreen;
if (timeAtMouse >= origin)
partScreen = screen - partScreen;
// The snapping zone is this fraction of screen, on each side of the
// center line:
const double snap = 0.05;
const double fraction = (partScreen <= 0.0) ? 0.0 :
std::max(snap, std::min(1.0, fabs(timeAtMouse - origin) / partScreen));
double result = 1.0 + ((fraction - snap) / (1.0 - snap)) * (extreme - 1.0);
if (timeAtMouse < origin)
result *= -1.0;
return result;
}
}
#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
bool Scrubber::ShouldScrubPinned()
{
return TracksPrefs::GetPinnedHeadPreference() &&
!PlaybackPrefs::GetUnpinnedScrubbingPreference();
}
class Scrubber::ScrubPoller : public wxTimer
{
public:
ScrubPoller(Scrubber &scrubber) : mScrubber( scrubber ) {}
private:
void Notify() override;
Scrubber &mScrubber;
};
void Scrubber::ScrubPoller::Notify()
{
// Call Continue functions here in a timer handler
// 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.
#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();
}
static const AudacityProject::AttachedObjects::RegisteredFactory key{
[]( AudacityProject &parent ){
return std::make_shared< Scrubber >( &parent ); }
};
Scrubber &Scrubber::Get( AudacityProject &project )
{
return project.AttachedObjects::Get< Scrubber >( key );
}
const Scrubber &Scrubber::Get( const AudacityProject &project )
{
return Get( const_cast< AudacityProject & >( project ) );
}
Scrubber::Scrubber(AudacityProject *project)
: mScrubToken(-1)
, mPaused(true)
, mScrubSpeedDisplayCountdown(0)
, mScrubStartPosition(-1)
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
, mSmoothScrollingScrub(false)
, mLogMaxScrubSpeed(0)
#endif
, mProject(project)
, mWindow( FindProjectFrame( project ) )
, mPoller { std::make_unique<ScrubPoller>(*this) }
, mOptions {}
{
if (wxTheApp)
wxTheApp->Bind
(wxEVT_ACTIVATE_APP,
&Scrubber::OnActivateOrDeactivateApp, this);
UpdatePrefs();
}
Scrubber::~Scrubber()
{
#ifdef USE_SCRUB_THREAD
if (mpThread)
mpThread->Delete();
#endif
if ( mWindow )
mWindow->PopEventHandler();
}
static const auto HasWaveDataPred =
[](const AudacityProject &project){
auto range = TrackList::Get( project ).Any<const WaveTrack>()
+ [](const WaveTrack *pTrack){
return pTrack->GetEndTime() > pTrack->GetStartTime();
};
return !range.empty();
};
static const ReservedCommandFlag
&HasWaveDataFlag() { static ReservedCommandFlag flag{
HasWaveDataPred
}; return flag; } // jkc
namespace {
struct MenuItem {
CommandID name;
TranslatableString label;
TranslatableString status;
CommandFlag flags;
void (Scrubber::*memFn)(const CommandContext&);
bool seek;
bool (Scrubber::*StatusTest)() const;
const TranslatableString &GetStatus() const { return status; }
};
using MenuItems = std::vector< MenuItem >;
const MenuItems &menuItems()
{
static MenuItems theItems{
/* i18n-hint: These commands assist the user in finding a sound by ear. ...
"Scrubbing" is variable-speed playback, ...
"Seeking" is normal speed playback but with skips, ...
*/
{ wxT("Scrub"), XO("&Scrub"), XO("Scrubbing"),
CaptureNotBusyFlag() | HasWaveDataFlag(),
&Scrubber::OnScrub, false, &Scrubber::Scrubs,
},
{ wxT("Seek"), XO("See&k"), XO("Seeking"),
CaptureNotBusyFlag() | HasWaveDataFlag(),
&Scrubber::OnSeek, true, &Scrubber::Seeks,
},
{ wxT("ToggleScrubRuler"), XO("Scrub &Ruler"), {},
AlwaysEnabledFlag,
&Scrubber::OnToggleScrubRuler, false, &Scrubber::ShowsBar,
},
};
return theItems;
};
inline const MenuItem &FindMenuItem(bool seek)
{
return *std::find_if(menuItems().begin(), menuItems().end(),
[=](const MenuItem &item) {
return seek == item.seek;
}
);
}
}
void Scrubber::MarkScrubStart(
// Assume xx is relative to the left edge of TrackPanel!
wxCoord xx, bool smoothScrolling, bool seek
)
{
// Don't actually start scrubbing, but collect some information
// needed for the decision to start scrubbing later when handling
// drag events.
mSmoothScrollingScrub = smoothScrolling;
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
// Stop any play in progress
// Bug 1492: mCancelled to stop us collapsing the selected region.
mCancelled = true;
projectAudioManager.Stop();
mCancelled = false;
// Usually the timer handler of TrackPanel does this, but we do this now,
// so that same timer does not StopPlaying() again after this function and destroy
// scrubber state
ProjectAudioIO::Get( *mProject ).SetAudioIOToken(0);
mSeeking = seek;
CheckMenuItems();
// Commented out for Bug 1421
// mSeeking
// ? ControlToolBar::PlayAppearance::Seek
// : ControlToolBar::PlayAppearance::Scrub);
mScrubStartPosition = xx;
mCancelled = false;
}
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Assume xx is relative to the left edge of TrackPanel!
bool Scrubber::MaybeStartScrubbing(wxCoord xx)
{
if (mScrubStartPosition < 0)
return false;
if (IsScrubbing())
return false;
else {
const auto state = ::wxGetMouseState();
mDragging = state.LeftIsDown();
auto gAudioIO = AudioIO::Get();
const bool busy = gAudioIO->IsBusy();
if (busy && gAudioIO->GetNumCaptureChannels() > 0) {
// Do not stop recording, and don't try to start scrubbing after
// recording stops
mScrubStartPosition = -1;
return false;
}
wxCoord position = xx;
if (abs(mScrubStartPosition - position) >= SCRUBBING_PIXEL_TOLERANCE) {
auto &viewInfo = ViewInfo::Get( *mProject );
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
double maxTime = TrackList::Get( *mProject ).GetEndTime();
const int leftOffset = viewInfo.GetLeftOffset();
double time0 = std::min(maxTime,
viewInfo.PositionToTime(mScrubStartPosition, leftOffset)
);
double time1 = std::min(maxTime,
viewInfo.PositionToTime(position, leftOffset)
);
if (time1 != time0) {
if (busy) {
position = mScrubStartPosition;
projectAudioManager.Stop();
mScrubStartPosition = position;
}
#ifdef DRAG_SCRUB
if (mDragging && mSmoothScrollingScrub) {
auto delta = time0 - time1;
time0 = std::max(0.0, std::min(maxTime,
viewInfo.h +
(viewInfo.GetScreenEndTime() - viewInfo.h)
* TracksPrefs::GetPinnedHeadPositionPreference()
));
time1 = time0 + delta;
}
#endif
mSpeedPlaying = false;
mKeyboardScrubbing = false;
auto options =
DefaultPlayOptions( *mProject );
#ifndef USE_SCRUB_THREAD
// Yuck, we either have to poll "by hand" when scrub polling doesn't
// work with a thread, or else yield to timer messages, but that would
// execute too much else
options.playbackStreamPrimer = [this](){
ContinueScrubbingPoll();
return ScrubPollInterval_ms;
};
#endif
options.pScrubbingOptions = &mOptions;
options.envelope = nullptr;
mOptions.delay = (ScrubPollInterval_ms / 1000.0);
mOptions.isPlayingAtSpeed = false;
mOptions.isKeyboardScrubbing = false;
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.
mMaxSpeed = mOptions.maxSpeed =
ProjectSettings::Get( *mProject ).GetPlaySpeed();
}
#else
// That idea seems unpopular... just make it one for move-scrub,
// but big for drag-scrub
#ifdef DRAG_SCRUB
mMaxSpeed = mOptions.maxSpeed = mDragging ? MaxDragSpeed : 1.0;
#else
mMaxSpeed = mOptions.maxSpeed = 1.0;
#endif
#endif
mOptions.minTime = 0;
mOptions.maxTime =
std::max(0.0, TrackList::Get( *mProject ).GetEndTime());
mOptions.minStutterTime =
#ifdef DRAG_SCRUB
mDragging ? 0.0 :
#endif
std::max(0.0, MinStutter);
const bool backwards = time1 < time0;
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
mLogMaxScrubSpeed = floor(0.5 +
log(mMaxSpeed) / log(maxScrubSpeedBase)
);
#endif
mScrubSpeedDisplayCountdown = 0;
// Must start the thread and poller first or else PlayPlayRegion
// will insert some silence
StartPolling();
auto cleanup = finally([this]{
if (mScrubToken < 0)
StopPolling();
});
mScrubToken =
projectAudioManager.PlayPlayRegion(
SelectedRegion(time0, time1), options,
PlayMode::normalPlay, backwards);
if (mScrubToken <= 0) {
// Bug1627 (part of it):
// infinite error spew when trying to start scrub:
// If failed for reasons of audio device problems, do not try
// again with repeated timer ticks.
mScrubStartPosition = -1;
return false;
}
}
}
else
// Wait to test again
;
if (IsScrubbing()) {
mLastScrubPosition = xx;
}
// Return true whether we started scrub, or are still waiting to decide.
return true;
}
}
bool Scrubber::StartSpeedPlay(double speed, double time0, double time1)
{
if (IsScrubbing())
return false;
auto gAudioIO = AudioIO::Get();
const bool busy = gAudioIO->IsBusy();
if (busy && gAudioIO->GetNumCaptureChannels() > 0) {
// Do not stop recording, and don't try to start scrubbing after
// recording stops
mScrubStartPosition = -1;
return false;
}
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
if (busy) {
projectAudioManager.Stop();
}
mScrubStartPosition = 0;
mSpeedPlaying = true;
mKeyboardScrubbing = false;
mMaxSpeed = speed;
mDragging = false;
auto options = DefaultSpeedPlayOptions( *mProject );
#ifndef USE_SCRUB_THREAD
// Yuck, we either have to poll "by hand" when scrub polling doesn't
// work with a thread, or else yield to timer messages, but that would
// execute too much else
options.playbackStreamPrimer = [this](){
ContinueScrubbingPoll();
return ScrubPollInterval_ms;
};
#endif
options.pScrubbingOptions = &mOptions;
options.envelope = nullptr;
mOptions.delay = (ScrubPollInterval_ms / 1000.0);
mOptions.minSpeed = speed -0.01;
mOptions.maxSpeed = speed +0.01;
if (time1 == time0)
time1 = std::max(0.0, TrackList::Get( *mProject ).GetEndTime());
mOptions.minTime = 0;
mOptions.maxTime = time1;
mOptions.minStutterTime = std::max(0.0, MinStutter);
mOptions.bySpeed = true;
mOptions.adjustStart = false;
mOptions.isPlayingAtSpeed = true;
mOptions.isKeyboardScrubbing = false;
const bool backwards = time1 < time0;
#ifdef EXPERIMENTAL_SCRUBBING_SCROLL_WHEEL
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
mLogMaxScrubSpeed = floor(0.5 +
log(mMaxSpeed) / log(maxScrubSpeedBase)
);
#endif
// Must start the thread and poller first or else PlayPlayRegion
// will insert some silence
StartPolling();
auto cleanup = finally([this]{
if (mScrubToken < 0)
StopPolling();
});
mScrubSpeedDisplayCountdown = 0;
// Aim to stop within 20 samples of correct position.
double stopTolerance = 20.0 / options.rate;
mScrubToken =
// Reduce time by 'stopTolerance' fudge factor, so that the Play will stop.
projectAudioManager.PlayPlayRegion(
SelectedRegion(time0, time1-stopTolerance), options,
PlayMode::normalPlay, backwards);
if (mScrubToken >= 0) {
mLastScrubPosition = 0;
}
return true;
}
bool Scrubber::StartKeyboardScrubbing(double time0, bool backwards)
{
if (HasMark() || AudioIO::Get()->IsBusy())
return false;
mScrubStartPosition = 0; // so that HasMark() is true
mSpeedPlaying = false;
mKeyboardScrubbing = true;
mBackwards = backwards;
mMaxSpeed = ScrubbingOptions::MaxAllowedScrubSpeed();
mDragging = false;
auto options = DefaultSpeedPlayOptions(*mProject);
#ifndef USE_SCRUB_THREAD
// Yuck, we either have to poll "by hand" when scrub polling doesn't
// work with a thread, or else yield to timer messages, but that would
// execute too much else
options.playbackStreamPrimer = [this]() {
ContinueScrubbingPoll();
return ScrubPollInterval_ms;
};
#endif
options.pScrubbingOptions = &mOptions;
options.envelope = nullptr;
// delay and minStutterTime are used in AudioIO::AllocateBuffers() for setting the
// values of mPlaybackQueueMinimum and mPlaybackSamplesToCopy respectively. minStutterTime
// is set lower here than in mouse scrubbing to ensure that there is not a long
// delay before the start of the playback of the audio.
mOptions.delay = (ScrubPollInterval_ms / 1000.0);
mOptions.minStutterTime = mOptions.delay;
mOptions.minSpeed = ScrubbingOptions::MinAllowedScrubSpeed();
mOptions.maxSpeed = ScrubbingOptions::MaxAllowedScrubSpeed();
mOptions.minTime = 0;
mOptions.maxTime = std::max(0.0, TrackList::Get(*mProject).GetEndTime());
mOptions.bySpeed = true;
mOptions.adjustStart = false;
mOptions.isPlayingAtSpeed = false;
mOptions.isKeyboardScrubbing = true;
// Must start the thread and poller first or else PlayPlayRegion
// will insert some silence
StartPolling();
auto cleanup = finally([this] {
if (mScrubToken < 0)
StopPolling();
});
mScrubToken =
ProjectAudioManager::Get(*mProject).PlayPlayRegion(
SelectedRegion(time0, backwards ? mOptions.minTime : mOptions.maxTime),
options,
PlayMode::normalPlay,
backwards);
return true;
}
double Scrubber::GetKeyboardScrubbingSpeed()
{
const double speedAtDefaultZoom = 0.5;
const double maxSpeed = 3.0;
const double minSpeed = 0.0625;
auto &viewInfo = ViewInfo::Get(*mProject);
double speed = speedAtDefaultZoom*viewInfo.GetDefaultZoom() / viewInfo.GetZoom();
speed = std::min(speed, maxSpeed);
speed = std::max(speed, minSpeed);
return speed;
}
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 = TemporarilySeeks() || Seeks();
auto gAudioIO = AudioIO::Get();
if (mPaused) {
// When paused, make silent scrubs.
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = false;
mOptions.bySpeed = true;
gAudioIO->UpdateScrub(0, mOptions);
}
else if (mSpeedPlaying) {
// default speed of 1.3 set, so that we can hear there is a problem
// when playAtSpeedTB not found.
double speed = 1.3;
const auto &settings = ProjectSettings::Get( *mProject );
speed = settings.GetPlaySpeed();
mOptions.minSpeed = speed -0.01;
mOptions.maxSpeed = speed +0.01;
mOptions.adjustStart = false;
mOptions.bySpeed = true;
gAudioIO->UpdateScrub(speed, mOptions);
}
else if (mKeyboardScrubbing) {
mOptions.minSpeed = ScrubbingOptions::MinAllowedScrubSpeed();
mOptions.maxSpeed = ScrubbingOptions::MaxAllowedScrubSpeed();
mOptions.adjustStart = false;
mOptions.bySpeed = true;
double speed = GetKeyboardScrubbingSpeed();
if (mBackwards)
speed *= -1.0;
gAudioIO->UpdateScrub(speed, mOptions);
} else {
const wxMouseState state(::wxGetMouseState());
auto &trackPanel = GetProjectPanel( *mProject );
const wxPoint position = trackPanel.ScreenToClient(state.GetPosition());
auto &viewInfo = ViewInfo::Get( *mProject );
#ifdef DRAG_SCRUB
if (mDragging && mSmoothScrollingScrub) {
const auto lastTime = gAudioIO->GetLastScrubTime();
const auto delta = mLastScrubPosition - position.x;
const double time = viewInfo.OffsetTimeByPixels(lastTime, delta);
mOptions.minSpeed = 0.0;
mOptions.maxSpeed = mMaxSpeed;
mOptions.adjustStart = true;
mOptions.bySpeed = false;
gAudioIO->UpdateScrub(time, mOptions);
mLastScrubPosition = position.x;
}
else
#endif
{
const auto origin = viewInfo.GetLeftOffset();
auto xx = position.x;
if (!seek && !mSmoothScrollingScrub) {
// If mouse is out-of-bounds, so that we scrub at maximum speed
// toward the mouse position, then move the target time to a more
// extreme position to avoid catching-up and halting before the
// screen scrolls.
auto width = viewInfo.GetTracksUsableWidth();
auto delta = xx - origin;
if (delta < 0)
delta -= width;
else if (delta >= width)
delta += width;
xx = origin + delta;
}
const double time = viewInfo.PositionToTime(xx, origin);
mOptions.adjustStart = seek;
mOptions.minSpeed = seek ? 1.0 : 0.0;
mOptions.maxSpeed = seek ? 1.0 : mMaxSpeed;
if (mSmoothScrollingScrub) {
const double speed = FindScrubSpeed(seek, time);
mOptions.bySpeed = true;
gAudioIO->UpdateScrub(speed, mOptions);
}
else {
mOptions.bySpeed = false;
gAudioIO->UpdateScrub(time, mOptions);
}
}
}
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());
if (mDragging && !state.LeftIsDown()) {
// Dragging scrub can stop with mouse up
// Stop and set cursor
bool bShift = state.ShiftDown();
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
projectAudioManager.DoPlayStopSelect( true, bShift );
projectAudioManager.Stop();
return;
}
const bool seek = Seeks() || TemporarilySeeks();
{
// Show the correct status for seeking.
bool backup = mSeeking;
mSeeking = seek;
mSeeking = backup;
}
if (seek)
mScrubSpeedDisplayCountdown = 0;
if (mSmoothScrollingScrub)
;
else {
if (mScrubSpeedDisplayCountdown > 0)
--mScrubSpeedDisplayCountdown;
}
}
bool Scrubber::IsTransportingPinned() const
{
if (!TracksPrefs::GetPinnedHeadPreference())
return false;
return
!(HasMark() &&
!WasSpeedPlaying() &&
!ShouldScrubPinned());
}
void Scrubber::StartPolling()
{
mPaused = false;
#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 * 0.9);
}
void Scrubber::StopPolling()
{
mPaused = true;
#ifdef USE_SCRUB_THREAD
if (mpThread) {
mpThread->Delete();
mpThread = nullptr;
}
#endif
mPoller->Stop();
}
void Scrubber::StopScrubbing()
{
auto gAudioIO = AudioIO::Get();
gAudioIO->StopScrub();
StopPolling();
if (HasMark() && !mCancelled) {
const wxMouseState state(::wxGetMouseState());
// Stop and set cursor
bool bShift = state.ShiftDown();
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
projectAudioManager.DoPlayStopSelect(true, bShift);
}
mScrubStartPosition = -1;
mDragging = false;
mSeeking = false;
CheckMenuItems();
}
bool Scrubber::ShowsBar() const
{
return mShowScrubbing;
}
bool Scrubber::IsScrubbing() const
{
if (mScrubToken <= 0)
return false;
auto &projectAudioIO = ProjectAudioIO::Get( *mProject );
if (mScrubToken == projectAudioIO.GetAudioIOToken() &&
projectAudioIO.IsAudioActive())
return true;
else {
const_cast<Scrubber&>(*this).mScrubToken = -1;
const_cast<Scrubber&>(*this).mScrubStartPosition = -1;
const_cast<Scrubber&>(*this).mSmoothScrollingScrub = false;
return false;
}
}
bool Scrubber::ChoseSeeking() const
{
return
#if !defined(DRAG_SCRUB)
// Drag always seeks
mDragging ||
#endif
mSeeking;
}
bool Scrubber::TemporarilySeeks() const
{
return mScrubSeekPress ||
(::wxGetMouseState().LeftIsDown() && MayDragToSeek());
}
bool Scrubber::Seeks() const
{
return (HasMark() || IsScrubbing()) && ChoseSeeking();
}
bool Scrubber::Scrubs() const
{
if( Seeks() )
return false;
return (HasMark() || IsScrubbing()) && !ChoseSeeking();
}
bool Scrubber::ShouldDrawScrubSpeed()
{
return IsScrubbing() &&
!mPaused && (
// Draw for (non-scroll) scrub, sometimes, but never for seek
(!(Seeks() || TemporarilySeeks()) && mScrubSpeedDisplayCountdown > 0)
// Draw always for scroll-scrub and for scroll-seek
|| mSmoothScrollingScrub
);
}
double Scrubber::FindScrubSpeed(bool seeking, double time) const
{
auto &viewInfo = ViewInfo::Get( *mProject );
const double screen =
viewInfo.GetScreenEndTime() - viewInfo.h;
return (seeking ? FindSeekSpeed : FindScrubbingSpeed)
(viewInfo, mMaxSpeed, screen, time);
}
void Scrubber::HandleScrollWheel(int steps)
{
if (steps == 0)
return;
const int newLogMaxScrubSpeed = mLogMaxScrubSpeed + steps;
static const double maxScrubSpeedBase =
pow(2.0, 1.0 / ScrubSpeedStepsPerOctave);
double newSpeed = pow(maxScrubSpeedBase, newLogMaxScrubSpeed);
if (newSpeed >= ScrubbingOptions::MinAllowedScrubSpeed() &&
newSpeed <= ScrubbingOptions::MaxAllowedScrubSpeed()) {
mLogMaxScrubSpeed = newLogMaxScrubSpeed;
mMaxSpeed = newSpeed;
if (!mSmoothScrollingScrub)
// Show the speed for one second
mScrubSpeedDisplayCountdown = kOneSecondCountdown + 1;
}
}
void Scrubber::Pause( bool paused )
{
mPaused = paused;
}
bool Scrubber::IsPaused() const
{
return mPaused;
}
void Scrubber::OnActivateOrDeactivateApp(wxActivateEvent &event)
{
// First match priority logic...
// Pause if Pause down, or not scrubbing.
if (!mProject)
Pause(true);
else if (ProjectAudioManager::Get( *mProject ).Paused())
Pause( true );
else if (!IsScrubbing())
Pause( true );
// Stop keyboard scrubbing if losing focus
else if (mKeyboardScrubbing && !event.GetActive()) {
Cancel();
ProjectAudioManager::Get(*mProject).Stop();
}
// Speed playing does not pause if losing focus.
else if (mSpeedPlaying)
Pause( false );
// But scrub and seek do.
else if (!event.GetActive())
Pause( true );
else
Pause(false);
event.Skip();
}
void Scrubber::DoScrub(bool seek)
{
if( !CanScrub() )
return;
const bool wasScrubbing = HasMark() || IsScrubbing();
const bool scroll = ShouldScrubPinned();
if (!wasScrubbing) {
auto &tp = GetProjectPanel( *mProject );
const auto &viewInfo = ViewInfo::Get( *mProject );
wxCoord xx = tp.ScreenToClient(::wxGetMouseState().GetPosition()).x;
// Limit x
auto width = viewInfo.GetTracksUsableWidth();
const auto offset = viewInfo.GetLeftOffset();
xx = (std::max(offset, std::min(offset + width - 1, xx)));
MarkScrubStart(xx, scroll, seek);
}
else if (mSeeking != seek) {
// just switching mode
}
else {
auto &projectAudioManager = ProjectAudioManager::Get( *mProject );
projectAudioManager.Stop();
}
}
void Scrubber::OnScrubOrSeek(bool seek)
{
DoScrub(seek);
mSeeking = seek;
CheckMenuItems();
}
void Scrubber::OnScrub(const CommandContext&)
{
OnScrubOrSeek(false);
CheckMenuItems();
}
void Scrubber::OnSeek(const CommandContext&)
{
OnScrubOrSeek(true);
CheckMenuItems();
}
#if 1
namespace {
static const wxChar *scrubEnabledPrefName = wxT("/QuickPlay/ScrubbingEnabled");
bool ReadScrubEnabledPref()
{
bool result {};
gPrefs->Read(scrubEnabledPrefName, &result, false);
return result;
}
void WriteScrubEnabledPref(bool value)
{
gPrefs->Write(scrubEnabledPrefName, value);
}
}
#endif
void Scrubber::UpdatePrefs()
{
mShowScrubbing = ReadScrubEnabledPref();
}
void Scrubber::OnToggleScrubRuler(const CommandContext&)
{
mShowScrubbing = !mShowScrubbing;
WriteScrubEnabledPref(mShowScrubbing);
gPrefs->Flush();
const auto toolbar =
ToolManager::Get( *mProject ).GetToolBar( ScrubbingBarID );
toolbar->EnableDisableButtons();
CheckMenuItems();
}
enum { CMD_ID = 8000 };
#define THUNK(Name) Scrubber::Thunk<&Scrubber::Name>
BEGIN_EVENT_TABLE(Scrubber, wxEvtHandler)
EVT_MENU(CMD_ID, THUNK(OnScrub))
EVT_MENU(CMD_ID + 1, THUNK(OnSeek))
EVT_MENU(CMD_ID + 2, THUNK(OnToggleScrubRuler))
END_EVENT_TABLE()
//static_assert(menuItems().size() == 3, "wrong number of items");
static auto sPlayAtSpeedStatus = XO("Playing at Speed");
static auto sKeyboardScrubbingStatus = XO("Scrubbing");
const TranslatableString &Scrubber::GetUntranslatedStateString() const
{
static TranslatableString empty;
if (IsSpeedPlaying()) {
return sPlayAtSpeedStatus;
}
else if (IsKeyboardScrubbing()) {
return sKeyboardScrubbingStatus;
}
else if (HasMark()) {
auto &item = FindMenuItem(Seeks() || TemporarilySeeks());
return item.status;
}
else
return empty;
}
wxString Scrubber::StatusMessageForWave() const
{
wxString result;
if( Seeks() )
result = _("Move mouse pointer to Seek");
else if( Scrubs() )
result = _("Move mouse pointer to Scrub");
return result;
}
static ProjectStatus::RegisteredStatusWidthFunction
registeredStatusWidthFunction{
[]( const AudacityProject &, StatusBarField field )
-> ProjectStatus::StatusWidthResult
{
if ( field == stateStatusBarField ) {
TranslatableStrings strings;
// Note that Scrubbing + Paused is not allowed.
for (const auto &item : menuItems())
strings.push_back( item.GetStatus() );
strings.push_back(
XO("%s Paused.").Format( sPlayAtSpeedStatus )
);
// added constant needed because xMax isn't large enough for some reason, plus some space.
return { std::move( strings ), 30 };
}
return {};
}
};
bool Scrubber::CanScrub() const
{
// Recheck the same condition as enables the Scrub/Seek menu item.
auto gAudioIO = AudioIO::Get();
return !( gAudioIO->IsBusy() && gAudioIO->GetNumCaptureChannels() > 0 ) &&
HasWaveDataPred( *mProject );
}
void Scrubber::DoKeyboardScrub(bool backwards, bool keyUp)
{
auto &project = *mProject;
static double initT0 = 0;
static double initT1 = 0;
if (keyUp) {
auto &scrubber = Scrubber::Get(project);
if (scrubber.IsKeyboardScrubbing() && scrubber.IsBackwards() == backwards) {
auto gAudioIO = AudioIOBase::Get();
auto time = gAudioIO->GetStreamTime();
auto &viewInfo = ViewInfo::Get(project);
auto &selection = viewInfo.selectedRegion;
// If the time selection has not changed during scrubbing
// set the cursor position
if (selection.t0() == initT0 && selection.t1() == initT1) {
double endTime = TrackList::Get(project).GetEndTime();
time = std::min(time, endTime);
time = std::max(time, 0.0);
selection.setTimes(time, time);
ProjectHistory::Get(project).ModifyState(false);
}
scrubber.Cancel();
ProjectAudioManager::Get(project).Stop();
}
}
else { // KeyDown
auto gAudioIO = AudioIOBase::Get();
auto &scrubber = Scrubber::Get(project);
if (scrubber.IsKeyboardScrubbing() && scrubber.IsBackwards() != backwards) {
// change direction
scrubber.SetBackwards(backwards);
}
else if (!gAudioIO->IsBusy() && !scrubber.HasMark()) {
auto &viewInfo = ViewInfo::Get(project);
auto &selection = viewInfo.selectedRegion;
double endTime = TrackList::Get(project).GetEndTime();
double t0 = selection.t0();
if ((!backwards && t0 >= 0 && t0 < endTime) ||
(backwards && t0 > 0 && t0 <= endTime)) {
initT0 = t0;
initT1 = selection.t1();
scrubber.StartKeyboardScrubbing(t0, backwards);
}
}
}
}
void Scrubber::OnKeyboardScrubBackwards(const CommandContext &context)
{
auto evt = context.pEvt;
if (evt)
DoKeyboardScrub(true, evt->GetEventType() == wxEVT_KEY_UP);
else { // called from menu, so simulate keydown and keyup
DoKeyboardScrub(true, false);
DoKeyboardScrub(true, true);
}
}
void Scrubber::OnKeyboardScrubForwards(const CommandContext &context)
{
auto evt = context.pEvt;
if (evt)
DoKeyboardScrub(false, evt->GetEventType() == wxEVT_KEY_UP);
else { // called from menu, so simulate keydown and keyup
DoKeyboardScrub(false, false);
DoKeyboardScrub(false, true);
}
}
namespace {
static const auto finder =
[](AudacityProject &project) -> CommandHandlerObject&
{ return Scrubber::Get( project ); };
using namespace MenuTable;
BaseItemSharedPtr ToolbarMenu()
{
using Options = CommandManager::Options;
static BaseItemSharedPtr menu { (
FinderScope{ finder },
Menu( wxT("Scrubbing"),
XO("Scru&bbing"),
[]{
BaseItemPtrs ptrs;
for (const auto &item : menuItems()) {
ptrs.push_back( Command( item.name, item.label,
item.memFn,
item.flags,
item.StatusTest
? // a checkmark item
Options{}.CheckTest( [&item](AudacityProject &project){
return ( Scrubber::Get(project).*(item.StatusTest) )(); } )
: // not a checkmark item
Options{}
) );
}
return ptrs;
}()
)
) };
return menu;
}
AttachedItem sAttachment{
wxT("Transport/Basic"),
Shared( ToolbarMenu() )
};
BaseItemSharedPtr KeyboardScrubbingItems()
{
using Options = CommandManager::Options;
static BaseItemSharedPtr items{
( FinderScope{ finder },
Items( wxT("KeyboardScrubbing"),
Command(wxT("KeyboardScrubBackwards"), XXO("Scrub Bac&kwards"),
&Scrubber::OnKeyboardScrubBackwards,
CaptureNotBusyFlag() | CanStopAudioStreamFlag(), wxT("U\twantKeyup")),
Command(wxT("KeyboardScrubForwards"), XXO("Scrub For&wards"),
&Scrubber::OnKeyboardScrubForwards,
CaptureNotBusyFlag() | CanStopAudioStreamFlag(), wxT("I\twantKeyup"))
) ) };
return items;
}
AttachedItem sAttachment2{
wxT("Optional/Extra/Part1/Transport"),
Shared( KeyboardScrubbingItems() )
};
}
void Scrubber::PopulatePopupMenu(wxMenu &menu)
{
int id = CMD_ID;
auto &cm = CommandManager::Get( *mProject );
for (const auto &item : menuItems()) {
if (cm.GetEnabled(item.name)) {
auto test = item.StatusTest;
menu.Append(id, item.label.Translation(), wxString{},
test ? wxITEM_CHECK : wxITEM_NORMAL);
if(test && (this->*test)())
menu.FindItem(id)->Check();
}
++id;
}
}
void Scrubber::CheckMenuItems()
{
auto &cm = CommandManager::Get( *mProject );
for (const auto &item : menuItems()) {
auto test = item.StatusTest;
if (test)
cm.Check(item.name, (this->*test)());
}
}
#endif