diff --git a/libraries/lib-audio-devices/AudioIOBase.h b/libraries/lib-audio-devices/AudioIOBase.h
index 2ab870cf64babd3e99dd7158ac99bd3f2e07eb42..291d0539be5c0a32ac1c10acdc9f1ff1de01f0c6 100644
--- a/libraries/lib-audio-devices/AudioIOBase.h
+++ b/libraries/lib-audio-devices/AudioIOBase.h
@@ -70,10 +70,12 @@ struct AudioIOStartStreamOptions
    // The return value is a number of milliseconds to sleep before calling again
    std::function< unsigned long() > playbackStreamPrimer;
 
-   using PolicyFactory = std::function< std::unique_ptr<PlaybackPolicy>() >;
+   using PolicyFactory = std::function<
+      std::unique_ptr<PlaybackPolicy>(const AudioIOStartStreamOptions&) >;
    PolicyFactory policyFactory;
 
    bool loopEnabled{ false };
+   bool variableSpeed{ false };
 };
 
 struct AudioIODiagnostics{
diff --git a/libraries/lib-sample-track/Mix.cpp b/libraries/lib-sample-track/Mix.cpp
index b7442e8bfda311041c1aaca6d138127fc5309c7d..f4091af616742581dac54046e038f12e31a80217 100644
--- a/libraries/lib-sample-track/Mix.cpp
+++ b/libraries/lib-sample-track/Mix.cpp
@@ -62,8 +62,8 @@ Mixer::WarpOptions::WarpOptions(const BoundedEnvelope *e)
     : envelope(e), minSpeed(0.0), maxSpeed(0.0)
  {}
 
-Mixer::WarpOptions::WarpOptions(double min, double max)
-   : minSpeed(min), maxSpeed(max)
+Mixer::WarpOptions::WarpOptions(double min, double max, double initial)
+   : minSpeed{min}, maxSpeed{max}, initialSpeed{initial}
 {
    if (minSpeed < 0)
    {
@@ -123,7 +123,7 @@ Mixer::Mixer(const SampleTrackConstArray &inputTracks,
    mTime = startTime;
    mBufferSize = outBufferSize;
    mInterleaved = outInterleaved;
-   mSpeed = 1.0;
+   mSpeed = warpOptions.initialSpeed;
    if( mixerSpec && mixerSpec->GetNumChannels() == mNumChannels &&
          mixerSpec->GetNumTracks() == mNumInputTracks )
       mMixerSpec = mixerSpec;
@@ -627,12 +627,6 @@ void Mixer::SetTimesAndSpeed(double t0, double t1, double speed, bool bSkipping)
    Reposition(t0, bSkipping);
 }
 
-void Mixer::SetSpeedForPlayAtSpeed(double speed)
-{
-   wxASSERT(std::isfinite(speed));
-   mSpeed = fabs(speed);
-}
-
 void Mixer::SetSpeedForKeyboardScrubbing(double speed, double startTime)
 {
    wxASSERT(std::isfinite(speed));
diff --git a/libraries/lib-sample-track/Mix.h b/libraries/lib-sample-track/Mix.h
index e89ea6686e4372c900b68efeb6d1da563e4d150e..a06f2761ea42cf3e12464c9138a9f619bb96ca42 100644
--- a/libraries/lib-sample-track/Mix.h
+++ b/libraries/lib-sample-track/Mix.h
@@ -78,12 +78,13 @@ class SAMPLE_TRACK_API Mixer {
        explicit WarpOptions(const BoundedEnvelope *e);
 
        //! Construct with no time warp
-       WarpOptions(double min, double max);
+       WarpOptions(double min, double max, double initial = 1.0);
 
     private:
        friend class Mixer;
        const BoundedEnvelope *envelope = nullptr;
        double minSpeed, maxSpeed;
+       double initialSpeed{ 1.0 };
     };
 
     //
@@ -121,7 +122,6 @@ class SAMPLE_TRACK_API Mixer {
    // Used in scrubbing and other nonuniform playback policies.
    void SetTimesAndSpeed(
       double t0, double t1, double speed, bool bSkipping = false);
-   void SetSpeedForPlayAtSpeed(double speed);
    void SetSpeedForKeyboardScrubbing(double speed, double startTime);
 
    /// Current time in seconds (unwarped, i.e. always between startTime and stopTime)
diff --git a/libraries/lib-screen-geometry/ViewInfo.cpp b/libraries/lib-screen-geometry/ViewInfo.cpp
index ef4af54be60c6308164971f058197f254e24f5fe..664dd6df294fde12a2ae26bfa7f2e7f77cfac3be 100644
--- a/libraries/lib-screen-geometry/ViewInfo.cpp
+++ b/libraries/lib-screen-geometry/ViewInfo.cpp
@@ -158,7 +158,6 @@ wxDEFINE_EVENT( EVT_PLAY_REGION_CHANGE, PlayRegionEvent );
 PlayRegionEvent::PlayRegionEvent(
    wxEventType commandType, PlayRegion *pReg )
 : wxEvent{ 0, commandType }
-, pRegion{ pReg }
 {}
 
 wxEvent *PlayRegionEvent::Clone() const
diff --git a/libraries/lib-screen-geometry/ViewInfo.h b/libraries/lib-screen-geometry/ViewInfo.h
index 4390d6c497c5356fc01aab984208d32176b89214..451c008702e67dfb498a7c1c1f96bb3be12419ce 100644
--- a/libraries/lib-screen-geometry/ViewInfo.h
+++ b/libraries/lib-screen-geometry/ViewInfo.h
@@ -118,8 +118,6 @@ struct PlayRegionEvent : public wxEvent
 {
    PlayRegionEvent( wxEventType commandType, PlayRegion *pRegion );
    wxEvent *Clone() const override;
-
-   wxWeakRef< PlayRegion > pRegion;
 };
 
 wxDECLARE_EXPORTED_EVENT( SCREEN_GEOMETRY_API,
diff --git a/src/AudioIO.cpp b/src/AudioIO.cpp
index f19c49bff8975443d6361ecbb6b32f76dbaaa6a0..9a10da80dbf20e28408ca11a5bf8bb1d39c83b69 100644
--- a/src/AudioIO.cpp
+++ b/src/AudioIO.cpp
@@ -119,8 +119,6 @@ time warp info and AudioIOListener and whether the playback is looped.
 #include "Prefs.h"
 #include "Project.h"
 #include "ProjectWindows.h"
-#include "TransactionScope.h"
-#include "ViewInfo.h" // for PlayRegionEvent
 #include "WaveTrack.h"
 #include "TransactionScope.h"
 
@@ -686,27 +684,13 @@ void AudioIO::SetOwningProject(
    }
 
    mOwningProject = pProject;
-
-   if (pProject)
-      ViewInfo::Get( *pProject ).playRegion.Bind(
-         EVT_PLAY_REGION_CHANGE, AudioIO::LoopPlayUpdate);
 }
 
 void AudioIO::ResetOwningProject()
 {
-   if ( auto pOwningProject = mOwningProject.lock() )
-      ViewInfo::Get( *pOwningProject ).playRegion.Unbind(
-         EVT_PLAY_REGION_CHANGE, AudioIO::LoopPlayUpdate);
-
    mOwningProject.reset();
 }
 
-void AudioIO::LoopPlayUpdate( PlayRegionEvent &evt )
-{
-   evt.Skip();
-   AudioIO::Get()->mPlaybackSchedule.MessageProducer( evt );
-}
-
 void AudioIO::StartMonitoring( const AudioIOStartStreamOptions &options )
 {
    if ( mPortStreamV19 || mStreamToken )
diff --git a/src/AudioIO.h b/src/AudioIO.h
index 30ef3d2c787860d64d7de0d0de528c4a09700804..f5969c03a7f8d970b6995c334bf04b7aa6e92f1c 100644
--- a/src/AudioIO.h
+++ b/src/AudioIO.h
@@ -36,7 +36,6 @@ class RingBuffer;
 class Mixer;
 class Resample;
 class AudioThread;
-class PlayRegionEvent;
 
 class AudacityProject;
 
@@ -513,7 +512,6 @@ private:
 
    void SetOwningProject( const std::shared_ptr<AudacityProject> &pProject );
    void ResetOwningProject();
-   static void LoopPlayUpdate( PlayRegionEvent &evt );
 
    /*!
     Called in a loop from another worker thread that does not have the low-latency constraints
diff --git a/src/PlaybackSchedule.cpp b/src/PlaybackSchedule.cpp
index 27c6f863cf0e274da120853a7ab00d4ba4375b3f..531b8ec35e3134afa039ae9327777c725f3be90c 100644
--- a/src/PlaybackSchedule.cpp
+++ b/src/PlaybackSchedule.cpp
@@ -13,7 +13,10 @@
 #include "AudioIOBase.h"
 #include "Envelope.h"
 #include "Mix.h"
+#include "Project.h"
+#include "ProjectSettings.h"
 #include "SampleCount.h"
+#include "ViewInfo.h" // for PlayRegionEvent
 
 #include <cmath>
 
@@ -146,11 +149,14 @@ const PlaybackPolicy &PlaybackSchedule::GetPolicy() const
    return const_cast<PlaybackSchedule&>(*this).GetPolicy();
 }
 
-NewDefaultPlaybackPolicy::NewDefaultPlaybackPolicy(
-   double trackEndTime, double loopEndTime, bool loopEnabled )
-   : mTrackEndTime{ trackEndTime }
+NewDefaultPlaybackPolicy::NewDefaultPlaybackPolicy( AudacityProject &project,
+   double trackEndTime, double loopEndTime,
+   bool loopEnabled, bool variableSpeed )
+   : mProject{ project }
+   , mTrackEndTime{ trackEndTime }
    , mLoopEndTime{ loopEndTime }
    , mLoopEnabled{ loopEnabled }
+   , mVariableSpeed{ variableSpeed }
 {}
 
 NewDefaultPlaybackPolicy::~NewDefaultPlaybackPolicy() = default;
@@ -159,16 +165,33 @@ void NewDefaultPlaybackPolicy::Initialize(
    PlaybackSchedule &schedule, double rate )
 {
    PlaybackPolicy::Initialize(schedule, rate);
-   schedule.mMessageChannel.Write( {
+   mLastPlaySpeed = GetPlaySpeed();
+   mMessageChannel.Write( { mLastPlaySpeed,
       schedule.mT0, mLoopEndTime, mLoopEnabled } );
+
+   ViewInfo::Get( mProject ).playRegion.Bind( EVT_PLAY_REGION_CHANGE,
+      &NewDefaultPlaybackPolicy::OnPlayRegionChange, this);
+   if (mVariableSpeed)
+      mProject.Bind( EVT_PROJECT_SETTINGS_CHANGE,
+         &NewDefaultPlaybackPolicy::OnPlaySpeedChange, this);
+}
+
+Mixer::WarpOptions NewDefaultPlaybackPolicy::MixerWarpOptions(
+   PlaybackSchedule &schedule)
+{
+   if (mVariableSpeed)
+      // Enable variable rate mixing
+      return Mixer::WarpOptions(0.01, 32.0, GetPlaySpeed());
+   else
+      return PlaybackPolicy::MixerWarpOptions(schedule);
 }
 
 PlaybackPolicy::BufferTimes
 NewDefaultPlaybackPolicy::SuggestedBufferTimes(PlaybackSchedule &)
 {
    // Shorter times than in the default policy so that responses to changes of
-   // selection don't lag too much
-   return { 0.5, 0.5, 1.0 };
+   // loop region or speed slider don't lag too much
+   return { 0.05, 0.05, 0.25 };
 }
 
 bool NewDefaultPlaybackPolicy::RevertToOldDefault(const PlaybackSchedule &schedule) const
@@ -190,16 +213,17 @@ PlaybackSlice
 NewDefaultPlaybackPolicy::GetPlaybackSlice(
    PlaybackSchedule &schedule, size_t available)
 {
+   // How many samples to produce for each channel.
+   const auto realTimeRemaining = std::max(0.0, schedule.RealTimeRemaining());
+   mRemaining = realTimeRemaining * mRate;
+
    if (RevertToOldDefault(schedule))
       return PlaybackPolicy::GetPlaybackSlice(schedule, available);
 
-   // How many samples to produce for each channel.
-   const auto realTimeRemaining = std::max(0.0, schedule.RealTimeRemaining());
    auto frames = available;
    auto toProduce = frames;
    double deltat = frames / mRate;
 
-   mRemaining = realTimeRemaining * mRate;
    if (deltat > realTimeRemaining)
    {
       toProduce = frames = mRemaining;
@@ -222,7 +246,7 @@ NewDefaultPlaybackPolicy::GetPlaybackSlice(
 std::pair<double, double> NewDefaultPlaybackPolicy::AdvancedTrackTime(
    PlaybackSchedule &schedule, double trackTime, size_t nSamples )
 {
-   if (RevertToOldDefault(schedule))
+   if (!mVariableSpeed && RevertToOldDefault(schedule))
       return PlaybackPolicy::AdvancedTrackTime(schedule, trackTime, nSamples);
 
    mRemaining -= std::min(mRemaining, nSamples);
@@ -234,7 +258,7 @@ std::pair<double, double> NewDefaultPlaybackPolicy::AdvancedTrackTime(
    if ( fabs(schedule.mT0 - schedule.mT1) < 1e-9 )
       return {schedule.mT0, schedule.mT0};
 
-   auto realDuration = nSamples / mRate;
+   auto realDuration = (nSamples / mRate) * mLastPlaySpeed;
    if (schedule.ReversedTime())
       realDuration *= -1.0;
 
@@ -252,7 +276,13 @@ bool NewDefaultPlaybackPolicy::RepositionPlayback(
    size_t frames, size_t available )
 {
    // This executes in the TrackBufferExchange thread
-   auto data = schedule.mMessageChannel.Read();
+   auto data = mMessageChannel.Read();
+
+   bool speedChange = false;
+   if (mVariableSpeed) {
+      speedChange = (mLastPlaySpeed != data.mPlaySpeed);
+      mLastPlaySpeed = data.mPlaySpeed;
+   }
 
    bool empty = (data.mT0 >= data.mT1);
    bool kicked = false;
@@ -270,6 +300,7 @@ bool NewDefaultPlaybackPolicy::RepositionPlayback(
 
    // Four cases:  looping transitions off, or transitions on, or stays on,
    // or stays off.
+   // Besides which, the variable speed slider may have changed.
 
    // If looping transitions on, or remains on and the region changed,
    // adjust the schedule...
@@ -303,6 +334,9 @@ bool NewDefaultPlaybackPolicy::RepositionPlayback(
       const auto realTimeRemaining = std::max(0.0, schedule.RealTimeRemaining());
       mRemaining = realTimeRemaining * mRate;
    }
+   else if (speedChange)
+      // Don't return early
+      kicked = true;
    else {
       // ... else the region did not change, or looping is now off, in
       // which case we have nothing special to do
@@ -317,7 +351,8 @@ bool NewDefaultPlaybackPolicy::RepositionPlayback(
    {
       // Looping jumps left
       for (auto &pMixer : playbackMixers)
-         pMixer->SetTimesAndSpeed( schedule.mT0, schedule.mT1, 1.0, true );
+         pMixer->SetTimesAndSpeed(
+            schedule.mT0, schedule.mT1, mLastPlaySpeed, true );
       schedule.RealTimeRestart();
    }
    else if (kicked)
@@ -326,7 +361,7 @@ bool NewDefaultPlaybackPolicy::RepositionPlayback(
       const auto time = schedule.mTimeQueue.GetLastTime();
       for (auto &pMixer : playbackMixers) {
          // So that the mixer will fetch the next samples from the right place:
-         pMixer->SetTimesAndSpeed( time, schedule.mT1, 1.0 );
+         pMixer->SetTimesAndSpeed( time, schedule.mT1, mLastPlaySpeed );
          pMixer->Reposition(time, true);
       }
    }
@@ -338,6 +373,34 @@ bool NewDefaultPlaybackPolicy::Looping( const PlaybackSchedule & ) const
    return mLoopEnabled;
 }
 
+void NewDefaultPlaybackPolicy::OnPlayRegionChange( PlayRegionEvent &evt)
+{
+   // This executes in the main thread
+   evt.Skip(); // Let other listeners hear the event too
+   WriteMessage();
+}
+
+void NewDefaultPlaybackPolicy::OnPlaySpeedChange(wxCommandEvent &evt)
+{
+   evt.Skip(); // Let other listeners hear the event too
+   WriteMessage();
+}
+
+void NewDefaultPlaybackPolicy::WriteMessage()
+{
+   const auto &region = ViewInfo::Get( mProject ).playRegion;
+   mMessageChannel.Write( { GetPlaySpeed(),
+      region.GetStart(), region.GetEnd(), region.Active()
+   } );
+}
+
+double NewDefaultPlaybackPolicy::GetPlaySpeed()
+{
+   return mVariableSpeed
+      ? ProjectSettings::Get(mProject).GetPlaySpeed()
+      : 1.0;
+}
+
 void PlaybackSchedule::Init(
    const double t0, const double t1,
    const AudioIOStartStreamOptions &options,
@@ -369,14 +432,12 @@ void PlaybackSchedule::Init(
    SetTrackTime( mT0 );
 
    if (options.policyFactory)
-      mpPlaybackPolicy = options.policyFactory();
+      mpPlaybackPolicy = options.policyFactory(options);
 
    mWarpedTime = 0.0;
    mWarpedLength = RealDuration(mT1);
 
    mPolicyValid.store(true, std::memory_order_release);
-
-   mMessageChannel.Initialize();
 }
 
 double PlaybackSchedule::ComputeWarpedLength(double t0, double t1) const
@@ -549,17 +610,3 @@ void PlaybackSchedule::TimeQueue::Prime(double time)
    if ( !mData.empty() )
       mData[0].timeValue = time;
 }
-
-#include "ViewInfo.h"
-void PlaybackSchedule::MessageProducer( PlayRegionEvent &evt)
-{
-   // This executes in the main thread
-   auto *pRegion = evt.pRegion.get();
-   if ( !pRegion )
-      return;
-   const auto &region = *pRegion;
-
-   mMessageChannel.Write( {
-      region.GetStart(), region.GetEnd(), region.Active()
-   } );
-}
diff --git a/src/PlaybackSchedule.h b/src/PlaybackSchedule.h
index 55d4c30b54c1dc044da0eecadd8ddb9bfa22d8a2..5363aaff8d7af6dc77f95fa9ff5ceeb777a1cc9d 100644
--- a/src/PlaybackSchedule.h
+++ b/src/PlaybackSchedule.h
@@ -17,6 +17,9 @@
 #include <chrono>
 #include <vector>
 
+#include <wx/event.h>
+
+class AudacityProject;
 struct AudioIOStartStreamOptions;
 class BoundedEnvelope;
 using PRCrossfadeData = std::vector< std::vector < float > >;
@@ -354,15 +357,6 @@ struct AUDACITY_DLL_API PlaybackSchedule {
    PlaybackPolicy &GetPolicy();
    const PlaybackPolicy &GetPolicy() const;
 
-   // The main thread writes changes in response to user events, and
-   // the audio thread later reads, and changes the playback.
-   struct SlotData {
-      double mT0;
-      double mT1;
-      bool mLoopEnabled;
-   };
-   MessageBuffer<SlotData> mMessageChannel;
-
    void Init(
       double t0, double t1,
       const AudioIOStartStreamOptions &options,
@@ -388,8 +382,6 @@ struct AUDACITY_DLL_API PlaybackSchedule {
     */
    double SolveWarpedLength(double t0, double length) const;
 
-   void MessageProducer( PlayRegionEvent &evt );
-
    /** \brief True if the end time is before the start time */
    bool ReversedTime() const
    {
@@ -437,14 +429,21 @@ private:
    std::atomic<bool> mPolicyValid{ false };
 };
 
-class NewDefaultPlaybackPolicy final : public PlaybackPolicy {
+class NewDefaultPlaybackPolicy final
+   : public PlaybackPolicy
+   , public NonInterferingBase
+   , public wxEvtHandler
+{
 public:
-   NewDefaultPlaybackPolicy(
-      double trackEndTime, double loopEndTime, bool loopEnabled);
+   NewDefaultPlaybackPolicy( AudacityProject &project,
+      double trackEndTime, double loopEndTime,
+      bool loopEnabled, bool variableSpeed);
    ~NewDefaultPlaybackPolicy() override;
 
    void Initialize( PlaybackSchedule &schedule, double rate ) override;
 
+   Mixer::WarpOptions MixerWarpOptions(PlaybackSchedule &schedule) override;
+
    BufferTimes SuggestedBufferTimes(PlaybackSchedule &schedule) override;
 
    bool Done( PlaybackSchedule &schedule, unsigned long ) override;
@@ -464,11 +463,29 @@ public:
 
 private:
    bool RevertToOldDefault( const PlaybackSchedule &schedule ) const;
+   void OnPlayRegionChange(PlayRegionEvent &evt);
+   void OnPlaySpeedChange(wxCommandEvent &evt);
+   void WriteMessage();
+   double GetPlaySpeed();
+
+   AudacityProject &mProject;
+
+   // The main thread writes changes in response to user events, and
+   // the audio thread later reads, and changes the playback.
+   struct SlotData {
+      double mPlaySpeed;
+      double mT0;
+      double mT1;
+      bool mLoopEnabled;
+   };
+   MessageBuffer<SlotData> mMessageChannel;
 
+   double mLastPlaySpeed{ 1.0 };
    const double mTrackEndTime;
    double mLoopEndTime;
    size_t mRemaining{ 0 };
    bool mProgress{ true };
    bool mLoopEnabled{ true };
+   bool mVariableSpeed{ false };
 };
 #endif
diff --git a/src/ProjectAudioManager.cpp b/src/ProjectAudioManager.cpp
index 1964eadef3fd9491c7aea05a7b622c9079b48c86..7c68cea53c58dd2db0cd01d1087d419aadd46f29 100644
--- a/src/ProjectAudioManager.cpp
+++ b/src/ProjectAudioManager.cpp
@@ -419,7 +419,7 @@ int ProjectAudioManager::PlayPlayRegion(const SelectedRegion &selectedRegion,
             std::swap(tcp0, tcp1);
          AudioIOStartStreamOptions myOptions = options;
          myOptions.policyFactory =
-            [tless, diff]() -> std::unique_ptr<PlaybackPolicy> {
+            [tless, diff](auto&) -> std::unique_ptr<PlaybackPolicy> {
                return std::make_unique<CutPreviewPlaybackPolicy>(tless, diff);
             };
          token = gAudioIO->StartStream(
@@ -1191,9 +1191,14 @@ DefaultPlayOptions( AudacityProject &project, bool newDefault )
    if (newDefault) {
       const double trackEndTime = TrackList::Get(project).GetEndTime();
       const double loopEndTime = ViewInfo::Get(project).playRegion.GetEnd();
-      options.policyFactory = [trackEndTime, loopEndTime, loopEnabled]() -> std::unique_ptr<PlaybackPolicy> {
-         return std::make_unique<NewDefaultPlaybackPolicy>(
-            trackEndTime, loopEndTime, loopEnabled); };
+      options.policyFactory = [&project, trackEndTime, loopEndTime](
+         const AudioIOStartStreamOptions &options)
+            -> std::unique_ptr<PlaybackPolicy>
+      {
+         return std::make_unique<NewDefaultPlaybackPolicy>( project,
+            trackEndTime, loopEndTime,
+            options.loopEnabled, options.variableSpeed);
+      };
 
       // Start play from left edge of selection
       options.pStartTime.emplace(ViewInfo::Get(project).selectedRegion.t0());
diff --git a/src/ProjectSettings.cpp b/src/ProjectSettings.cpp
index a90b8401a89f6d898fba9563ce4fb6398f635664..970aa3c2ebc982b5dcb7559d1fd816af4c01ee19 100644
--- a/src/ProjectSettings.cpp
+++ b/src/ProjectSettings.cpp
@@ -140,6 +140,14 @@ void ProjectSettings::SetBandwidthSelectionFormatName(
    mBandwidthSelectionFormatName = formatName;
 }
 
+void ProjectSettings::SetPlaySpeed(double value)
+{
+   if (auto oldValue = GetPlaySpeed(); value != oldValue) {
+      mPlaySpeed.store( value, std::memory_order_relaxed );
+      Notify( mProject, ChangedPlaySpeed, oldValue );
+   }
+}
+
 void ProjectSettings::SetSelectionFormat(const NumericFormatSymbol & format)
 {
    mSelectionFormat = format;
diff --git a/src/ProjectSettings.h b/src/ProjectSettings.h
index e22afdbe6bf4f2c5cd08c8b9e455b7c0f6ea58ef..ecfaff54f064230915b3f1ada5a4226ce62a7f60 100644
--- a/src/ProjectSettings.h
+++ b/src/ProjectSettings.h
@@ -65,8 +65,8 @@ public:
    // Values retrievable from GetInt() of the event for settings change
    enum EventCode : int {
       ChangedSyncLock,
-      ChangedProjectRate,
-      ChangedTool
+      ChangedTool,
+      ChangedPlaySpeed,
    };
 
    explicit ProjectSettings( AudacityProject &project );
@@ -106,9 +106,8 @@ public:
    // Speed play
    double GetPlaySpeed() const {
       return mPlaySpeed.load( std::memory_order_relaxed ); }
-   void SetPlaySpeed( double value ) {
-      mPlaySpeed.store( value, std::memory_order_relaxed ); }
-
+   void SetPlaySpeed( double value );
+   
    // Selection Format
    void SetSelectionFormat(const NumericFormatSymbol & format);
    const NumericFormatSymbol & GetSelectionFormat() const;
diff --git a/src/ScrubState.cpp b/src/ScrubState.cpp
index 691512f29ef567cf555421120e00469e62256d6f..72c2e9399a7c7a4779a8a9d403d1f1b63e0d0001 100644
--- a/src/ScrubState.cpp
+++ b/src/ScrubState.cpp
@@ -55,8 +55,7 @@ struct ScrubQueue : NonInterferingBase
          // Make some initial silence. This is not needed in the case of
          // keyboard scrubbing or play-at-speed, because the initial speed
          // is known when this function is called the first time.
-         if ( !(message.options.isKeyboardScrubbing ||
-            message.options.isPlayingAtSpeed) ) {
+         if ( !(message.options.isKeyboardScrubbing) ) {
             mData.mS0 = mData.mS1 = s0Init;
             mData.mGoal = -1;
             mData.mDuration = duration = inDuration;
@@ -64,8 +63,7 @@ struct ScrubQueue : NonInterferingBase
          }
       }
 
-      if (mStarted || message.options.isKeyboardScrubbing ||
-         message.options.isPlayingAtSpeed) {
+      if (mStarted || message.options.isKeyboardScrubbing) {
          Data newData;
          inDuration += mAccumulatedSeekDuration;
 
@@ -353,11 +351,7 @@ bool ScrubbingPlaybackPolicy::AllowSeek( PlaybackSchedule & )
 bool ScrubbingPlaybackPolicy::Done(
    PlaybackSchedule &schedule, unsigned long )
 {
-   if (mOptions.isPlayingAtSpeed)
-      // some leftover length allowed in this case; ignore outputFrames
-      return PlaybackPolicy::Done(schedule, 0);
-   else
-      return false;
+   return false;
 }
 
 std::chrono::milliseconds
@@ -451,9 +445,7 @@ bool ScrubbingPlaybackPolicy::RepositionPlayback(
          if (!mSilentScrub)
          {
             for (auto &pMixer : playbackMixers) {
-               if (mOptions.isPlayingAtSpeed)
-                  pMixer->SetSpeedForPlayAtSpeed(mScrubSpeed);
-               else if (mOptions.isKeyboardScrubbing)
+               if (mOptions.isKeyboardScrubbing)
                   pMixer->SetSpeedForKeyboardScrubbing(mScrubSpeed, startTime);
                else
                   pMixer->SetTimesAndSpeed(
diff --git a/src/ScrubState.h b/src/ScrubState.h
index c0004fd01161bea4ee066662753448dfdcab7d30..300ee5e4e1eefaa9ec7fe9d97aa6b0e14908910e 100644
--- a/src/ScrubState.h
+++ b/src/ScrubState.h
@@ -25,7 +25,6 @@ struct ScrubbingOptions {
    double minTime {};
 
    bool bySpeed {};
-   bool isPlayingAtSpeed{};
    bool isKeyboardScrubbing{};
 
    double delay {};
diff --git a/src/menus/TransportMenus.cpp b/src/menus/TransportMenus.cpp
index f42a755f6cb4474dbedf8da418637ff4c5826df6..ab95edc9b077ab41a5a3262490d834e7445452ba 100644
--- a/src/menus/TransportMenus.cpp
+++ b/src/menus/TransportMenus.cpp
@@ -1176,10 +1176,11 @@ BaseItemSharedPtr TransportMenu()
                          return IsLoopingEnabled(project);
                      } )),
                Command( wxT("ClearPlayRegion"), XXO("&Clear Loop"),
-                  FN(OnClearPlayRegion), AlwaysEnabledFlag ),
+                  FN(OnClearPlayRegion), AlwaysEnabledFlag, L"Shift+Alt+L" ),
                Command( wxT("SetPlayRegionToSelection"),
                   XXO("&Set Loop to Selection"),
-                  FN(OnSetPlayRegionToSelection), AlwaysEnabledFlag ),
+                  FN(OnSetPlayRegionToSelection), AlwaysEnabledFlag,
+                     L"Shift+L" ),
                Command( wxT("SetPlayRegionIn"),
                   SetLoopInTitle,
                   FN(OnSetPlayRegionIn), AlwaysEnabledFlag ),
diff --git a/src/toolbars/TranscriptionToolBar.cpp b/src/toolbars/TranscriptionToolBar.cpp
index 9fb868dfe446e29beac738c83e915dda0738d52e..73a25334649023d4481fd58ad28248f9aa44752e 100644
--- a/src/toolbars/TranscriptionToolBar.cpp
+++ b/src/toolbars/TranscriptionToolBar.cpp
@@ -491,11 +491,10 @@ void TranscriptionToolBar::PlayAtSpeed(bool newDefault, bool cutPreview)
    if ( TrackList::Get( *p ).Any< NoteTrack >() )
       bFixedSpeedPlay = true;
 
-   // Scrubbing only supports straight through play.
-   // So if newDefault or cutPreview, we have to fall back to fixed speed.
+   // If cutPreview, we have to fall back to fixed speed.
    if (newDefault)
       cutPreview = false;
-   bFixedSpeedPlay = bFixedSpeedPlay || newDefault || cutPreview;
+   bFixedSpeedPlay = bFixedSpeedPlay || cutPreview;
    if (bFixedSpeedPlay)
    {
       // Create a BoundedEnvelope if we haven't done so already
@@ -528,11 +527,12 @@ void TranscriptionToolBar::PlayAtSpeed(bool newDefault, bool cutPreview)
    // Start playing
    if (playRegion.GetStart() < 0)
       return;
-   if (bFixedSpeedPlay)
+
    {
       auto options = DefaultPlayOptions( *p, newDefault );
       // No need to set cutPreview options.
-      options.envelope = mEnvelope.get();
+      options.envelope = bFixedSpeedPlay ? mEnvelope.get() : nullptr;
+      options.variableSpeed = !bFixedSpeedPlay;
       auto mode =
          cutPreview ? PlayMode::cutPreviewPlay
          : newDefault ? PlayMode::loopedPlay
@@ -542,16 +542,10 @@ void TranscriptionToolBar::PlayAtSpeed(bool newDefault, bool cutPreview)
             options,
             mode);
    }
-   else
-   {
-      auto &scrubber = Scrubber::Get( *p );
-      scrubber.StartSpeedPlay(GetPlaySpeed(),
-         playRegion.GetStart(), playRegion.GetEnd());
-   }
 }
 
 // Come here from button clicks only
-void TranscriptionToolBar::OnPlaySpeed(wxCommandEvent & WXUNUSED(event))
+void TranscriptionToolBar::OnPlaySpeed(wxCommandEvent & event)
 {
    auto button = mButtons[TTB_PlaySpeed];
 
@@ -559,6 +553,7 @@ void TranscriptionToolBar::OnPlaySpeed(wxCommandEvent & WXUNUSED(event))
    const bool cutPreview = mButtons[TTB_PlaySpeed]->WasControlDown();
    const bool looped = !cutPreview &&
       !button->WasShiftDown();
+   OnSpeedSlider(event);
    PlayAtSpeed(looped, cutPreview);
 }
 
diff --git a/src/tracks/ui/Scrubbing.cpp b/src/tracks/ui/Scrubbing.cpp
index 888aae0fc3c3941ccb438b4560cd68e66c34d678..0f33d37d9913b5af464c6813c7ea577d30f50525 100644
--- a/src/tracks/ui/Scrubbing.cpp
+++ b/src/tracks/ui/Scrubbing.cpp
@@ -348,7 +348,7 @@ void Scrubber::MarkScrubStart(
 static AudioIOStartStreamOptions::PolicyFactory
 ScrubbingPlaybackPolicyFactory(const ScrubbingOptions &options)
 {
-   return [options]() -> std::unique_ptr<PlaybackPolicy>
+   return [options](auto&) -> std::unique_ptr<PlaybackPolicy>
    {
       return std::make_unique<ScrubbingPlaybackPolicy>(options);
    };
@@ -423,7 +423,6 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
             options.playNonWaveTracks = false;
             options.envelope = nullptr;
             mOptions.delay = (ScrubPollInterval_ms / 1000.0);
-            mOptions.isPlayingAtSpeed = false;
             mOptions.isKeyboardScrubbing = false;
             mOptions.initSpeed = 0;
             mOptions.minSpeed = 0.0;
@@ -499,96 +498,6 @@ bool Scrubber::MaybeStartScrubbing(wxCoord xx)
    }
 }
 
-
-
-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.playNonWaveTracks = false;
-   options.envelope = nullptr;
-   mOptions.delay = (ScrubPollInterval_ms / 1000.0);
-   mOptions.initSpeed = speed;
-   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;
-   options.policyFactory = ScrubbingPlaybackPolicyFactory(mOptions);
-   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())
@@ -630,7 +539,6 @@ bool Scrubber::StartKeyboardScrubbing(double time0, bool backwards)
    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
diff --git a/src/tracks/ui/Scrubbing.h b/src/tracks/ui/Scrubbing.h
index ab31d888e85b01af3e802e3f78424bda0b8518bd..a32453a38b6e97b575ec9709d5ab8383ef25d571 100644
--- a/src/tracks/ui/Scrubbing.h
+++ b/src/tracks/ui/Scrubbing.h
@@ -61,7 +61,6 @@ public:
    // Returns true iff the event should be considered consumed by this:
    // 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();