Keyboard interface for scrubbing

- There are two new commands: Scrub Backwards and Scrub Forwards.
- These commands appear on the Transport sub menu of the Extra menu.
- The commands have default shortcuts U and I, and are in the standard default set.
- After pressing one of the two keys, playback continues until the key is released. (Note that this means that the command on the  Extra > Transport menu can't actually be used for scrubbing as it executes a KeyDown immediately followed by a KeyUp, but the menu items are needed so that the current keystrokes can be seen and changed.)
- Playback starts from the cursor position, or the start of a time selection if there is one.
- The speed of playback is determined by the zoom level. If the zoom level is normal, then the playback speed is one quarter of the normal playback speed. Zooming in (Ctrl + 1), halves the playback speed, and zooming out (Ctrl + 3) doubles the playback speed. There are minimum and maximum playback speeds of one sixteenth, and four respectively.
- You can scrub to the end of the audio, even if there is an initial selection. In other words, scrubbing forwards does not automatically stop at the end of the selection.
- Normally, when one of the keys is released, the position of the cursor is set to the time when the key was released.
- If during the time one of the keys is pressed the left bracket and or right bracket keys are pressed to set the start and/or end of the selection, then when the scrubbing key is released, the change to the selection made by pressing the bracket keys is preserved - the position of the cursor is not set to the time when the key was released.

This implementation is affected by two existing bugs:
1. Bug 1954 - Clicks may occur starting/pausing play-at-speed or Scrub. (See comment 19 and attached image).
2. Bug 1956 - Windows: MME and WDS playback cursor is buffer length ahead of actual audio playing. This means that on Windows, WASAPI is preferable if scrubbing is being used for the accurate positioning of the cursor.
This commit is contained in:
David Bailes 2019-11-13 10:19:57 +00:00
parent eba984303c
commit 3a453126e8
6 changed files with 189 additions and 6 deletions

View File

@ -210,7 +210,8 @@ void AdornedRulerPanel::QuickPlayRulerOverlay::Update()
// Hide during transport, or if mouse is not in the ruler, unless scrubbing
if ((!ruler->LastCell() || ProjectAudioIO::Get( *project ).IsAudioActive())
&& (!scrubber.IsScrubbing() || scrubber.IsSpeedPlaying()))
&& (!scrubber.IsScrubbing() || scrubber.IsSpeedPlaying()
|| scrubber.IsKeyboardScrubbing()))
mNewQPIndicatorPos = -1;
else {
const auto &selectedRegion = ViewInfo::Get( *project ).selectedRegion;

View File

@ -740,7 +740,8 @@ void ProjectAudioManager::OnPause()
// it is confusing to be in a paused scrub state.
bool bStopInstead = paused &&
gAudioIO->IsScrubbing() &&
!scrubber.IsSpeedPlaying();
!scrubber.IsSpeedPlaying() &&
!scrubber.IsKeyboardScrubbing();
if (bStopInstead) {
Stop();
@ -1033,8 +1034,8 @@ bool ProjectAudioManager::DoPlayStopSelect( bool click, bool shift )
// change the selection
auto time = gAudioIO->GetStreamTime();
// Test WasSpeedPlaying(), not IsSpeedPlaying()
// as we could be stopped now.
if (click && scrubber.WasSpeedPlaying())
// as we could be stopped now. Similarly WasKeyboardScrubbing().
if (click && (scrubber.WasSpeedPlaying() || scrubber.WasKeyboardScrubbing()))
{
;// don't change the selection.
}

View File

@ -190,6 +190,53 @@ void DoMoveToLabel(AudacityProject &project, bool next)
}
}
void DoKeyboardScrub(AudacityProject& project, bool backwards, bool keyUp)
{
static double initT0 = 0;
static double initT1 = 0;
if (keyUp) {
auto &scrubber = Scrubber::Get(project);
if (scrubber.IsKeyboardScrubbing()) {
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 (!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);
}
}
}
}
}
// Menu handler functions
@ -786,6 +833,32 @@ void OnPlayCutPreview(const CommandContext &context)
);
}
void OnKeyboardScrubBackwards(const CommandContext &context)
{
auto &project = context.project;
auto evt = context.pEvt;
if (evt)
DoKeyboardScrub(project, true, evt->GetEventType() == wxEVT_KEY_UP);
else { // called from menu, so simulate keydown and keyup
DoKeyboardScrub(project, true, false);
DoKeyboardScrub(project, true, true);
}
}
void OnKeyboardScrubForwards(const CommandContext &context)
{
auto &project = context.project;
auto evt = context.pEvt;
if (evt)
DoKeyboardScrub(project, false, evt->GetEventType() == wxEVT_KEY_UP);
else { // called from menu, so simulate keydown and keyup
DoKeyboardScrub(project, false, false);
DoKeyboardScrub(project, false, true);
}
}
void OnPlayAtSpeed(const CommandContext &context)
{
auto &project = context.project;
@ -1049,7 +1122,13 @@ MenuTable::BaseItemPtr ExtraTransportMenu( AudacityProject & )
wxT("Ctrl+Shift+F7") ),
Command( wxT("PlayCutPreview"), XXO("Play C&ut Preview"),
FN(OnPlayCutPreview),
CaptureNotBusyFlag, wxT("C") )
CaptureNotBusyFlag, wxT("C") ),
Command(wxT("KeyboardScrubBackwards"), XXO("Scrub Bac&kwards"),
FN(OnKeyboardScrubBackwards),
CaptureNotBusyFlag | CanStopAudioStreamFlag, wxT("U\twantKeyup")),
Command(wxT("KeyboardScrubForwards"), XXO("Scrub For&wards"),
FN(OnKeyboardScrubForwards),
CaptureNotBusyFlag | CanStopAudioStreamFlag, wxT("I\twantKeyup"))
);
}

View File

@ -116,7 +116,7 @@ void ScrubbingOverlay::OnTimer(wxCommandEvent &event)
auto &ruler = AdornedRulerPanel::Get( *mProject );
auto position = ::wxGetMousePosition();
if (scrubber.IsSpeedPlaying())
if (scrubber.IsSpeedPlaying() || scrubber.IsKeyboardScrubbing())
return;
{

View File

@ -389,6 +389,7 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
}
#endif
mSpeedPlaying = false;
mKeyboardScrubbing = false;
auto options =
DefaultPlayOptions( *mProject );
@ -499,6 +500,7 @@ bool Scrubber::StartSpeedPlay(double speed, double time0, double time1)
}
mScrubStartPosition = 0;
mSpeedPlaying = true;
mKeyboardScrubbing = false;
mMaxSpeed = speed;
mDragging = false;
@ -563,6 +565,76 @@ bool Scrubber::StartSpeedPlay(double speed, double time0, double time1)
}
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;
mOptions.delay = (ScrubPollInterval_ms / 1000.0);
mOptions.minSpeed = ScrubbingOptions::MinAllowedScrubSpeed();
mOptions.maxSpeed = ScrubbingOptions::MaxAllowedScrubSpeed();
mOptions.minTime = 0;
mOptions.maxTime = std::max(0.0, TrackList::Get(*mProject).GetEndTime());
mOptions.minStutterTime = std::max(0.0, MinStutter);
mOptions.bySpeed = true;
mOptions.adjustStart = false;
mOptions.isPlayingAtSpeed = false;
// 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.25;
const double maxSpeed = 4.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,
@ -594,6 +666,16 @@ void Scrubber::ContinueScrubbingPoll()
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 );
@ -854,6 +936,12 @@ void Scrubber::OnActivateOrDeactivateApp(wxActivateEvent &event)
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 );
@ -963,6 +1051,9 @@ static_assert(nMenuItems == 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;
@ -970,6 +1061,9 @@ const TranslatableString &Scrubber::GetUntranslatedStateString() const
if (IsSpeedPlaying()) {
return sPlayAtSpeedStatus;
}
else if (IsKeyboardScrubbing()) {
return sKeyboardScrubbingStatus;
}
else if (HasMark()) {
auto &item = FindMenuItem(Seeks() || TemporarilySeeks());
return item.status;

View File

@ -63,6 +63,8 @@ public:
// Assume xx is relative to the left edge of TrackPanel!
bool MaybeStartScrubbing(wxCoord xx);
bool StartSpeedPlay(double speed, double time0, double time1);
bool StartKeyboardScrubbing(double time0, bool backwards);
double GetKeyboardScrubbingSpeed();
void ContinueScrubbingUI();
void ContinueScrubbingPoll();
@ -77,6 +79,10 @@ public:
{ return mSpeedPlaying;}
bool IsSpeedPlaying() const
{ return IsScrubbing() && mSpeedPlaying; }
bool WasKeyboardScrubbing() const
{ return mKeyboardScrubbing; }
bool IsKeyboardScrubbing() const
{ return IsScrubbing() && mKeyboardScrubbing; }
// True iff the user has clicked to start scrub and not yet stopped,
// but IsScrubbing() may yet be false
bool HasMark() const
@ -155,6 +161,8 @@ private:
bool mPaused{};
bool mSeeking {};
bool mSpeedPlaying{true};
bool mKeyboardScrubbing{};
bool mBackwards{};
bool mDragging {};
bool mCancelled {};