[go: nahoru, domu]

Add HTMLMediaElement.preservesPitch

To adjust the playback rate of audio, we use two algorithms: resampling,
which shifts the audio's pitch, and WSOLA, which doesn't. At rates close
to 1.0, WSOLA can introduce undesirable artifacts for certain types of,
audio content, so we use resampling instead. However, this pitch shift
is noticeable can be less desirable than the WSOLA artifacts to some
users.

This CL implements the preservesPitch flag, which is already present in
Firefox and Safari. The flag allows web developers to choose between
pitch preserving and pitch shifting time stretch algorithms (resampling
and WSOLA in our case). This will allow users to chose the algorithm
that suites their needs at playback rates close to 1.0. It will also
allow users to use resampling at lower or higher playback rates, when
pitch shifting is desirable for aesthetic or performance purposes.

Bug: 1072067, 1096238
Change-Id: Ie0e7ff337da77c902a12259c30c33125104101ef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2255179
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Reviewed-by: Chris Harrelson <chrishtr@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/master@{#782141}
diff --git a/media/base/audio_renderer.h b/media/base/audio_renderer.h
index eac3c82..533c461 100644
--- a/media/base/audio_renderer.h
+++ b/media/base/audio_renderer.h
@@ -65,6 +65,10 @@
   // (restore UA default).
   virtual void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) = 0;
 
+  // Sets a flag indicating that the AudioRenderer should use or avoid pitch
+  // preservation when playing back at speeds other than 1.0.
+  virtual void SetPreservesPitch(bool preserves_pitch) = 0;
+
  private:
   DISALLOW_COPY_AND_ASSIGN(AudioRenderer);
 };
diff --git a/media/base/mock_filters.h b/media/base/mock_filters.h
index 0ab0d102..8f012bd 100644
--- a/media/base/mock_filters.h
+++ b/media/base/mock_filters.h
@@ -114,6 +114,7 @@
   MOCK_CONST_METHOD0(GetVolume, float());
   MOCK_METHOD1(SetVolume, void(float));
   MOCK_METHOD1(SetLatencyHint, void(base::Optional<base::TimeDelta>));
+  MOCK_METHOD1(SetPreservesPitch, void(bool));
 
   // TODO(sandersd): These should probably have setters too.
   MOCK_CONST_METHOD0(GetMediaTime, base::TimeDelta());
@@ -357,6 +358,7 @@
   MOCK_METHOD1(SetVolume, void(float volume));
   MOCK_METHOD1(SetLatencyHint,
                void(base::Optional<base::TimeDelta> latency_hint));
+  MOCK_METHOD1(SetPreservesPitch, void(bool));
 
  private:
   DISALLOW_COPY_AND_ASSIGN(MockAudioRenderer);
@@ -378,7 +380,8 @@
                     RendererClient* client,
                     PipelineStatusCallback& init_cb));
   MOCK_METHOD1(SetLatencyHint, void(base::Optional<base::TimeDelta>));
-  void Flush(base::OnceClosure flush_cb) { OnFlush(flush_cb); }
+  MOCK_METHOD1(SetPreservesPitch, void(bool));
+  void Flush(base::OnceClosure flush_cb) override { OnFlush(flush_cb); }
   MOCK_METHOD1(OnFlush, void(base::OnceClosure& flush_cb));
   MOCK_METHOD1(StartPlayingFrom, void(base::TimeDelta timestamp));
   MOCK_METHOD1(SetPlaybackRate, void(double playback_rate));
diff --git a/media/base/pipeline.h b/media/base/pipeline.h
index e8c4342..72a22a0 100644
--- a/media/base/pipeline.h
+++ b/media/base/pipeline.h
@@ -225,6 +225,10 @@
   // can choose its own default.
   virtual void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) = 0;
 
+  // Sets whether pitch adjustment should be applied when the playback rate is
+  // different than 1.0.
+  virtual void SetPreservesPitch(bool preserves_pitch) = 0;
+
   // Returns the current media playback time, which progresses from 0 until
   // GetMediaDuration().
   virtual base::TimeDelta GetMediaTime() const = 0;
diff --git a/media/base/pipeline_impl.cc b/media/base/pipeline_impl.cc
index fdf107f..32bf075 100644
--- a/media/base/pipeline_impl.cc
+++ b/media/base/pipeline_impl.cc
@@ -70,6 +70,7 @@
   void SetPlaybackRate(double playback_rate);
   void SetVolume(float volume);
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint);
+  void SetPreservesPitch(bool preserves_pitch);
   base::TimeDelta GetMediaTime() const;
   Ranges<base::TimeDelta> GetBufferedTimeRanges() const;
   bool DidLoadingProgress();
@@ -193,6 +194,9 @@
   base::Optional<base::TimeDelta> latency_hint_;
   CdmContext* cdm_context_;
 
+  // By default, apply pitch adjustments.
+  bool preserves_pitch_ = true;
+
   // Lock used to serialize |shared_state_|.
   // TODO(crbug.com/893739): Add GUARDED_BY annotations.
   mutable base::Lock shared_state_lock_;
@@ -482,6 +486,17 @@
     shared_state_.renderer->SetLatencyHint(latency_hint_);
 }
 
+void PipelineImpl::RendererWrapper::SetPreservesPitch(bool preserves_pitch) {
+  DCHECK(media_task_runner_->BelongsToCurrentThread());
+
+  if (preserves_pitch_ == preserves_pitch)
+    return;
+
+  preserves_pitch_ = preserves_pitch;
+  if (shared_state_.renderer)
+    shared_state_.renderer->SetPreservesPitch(preserves_pitch_);
+}
+
 base::TimeDelta PipelineImpl::RendererWrapper::GetMediaTime() const {
   DCHECK(main_task_runner_->BelongsToCurrentThread());
 
@@ -1041,6 +1056,8 @@
   if (latency_hint_)
     shared_state_.renderer->SetLatencyHint(latency_hint_);
 
+  shared_state_.renderer->SetPreservesPitch(preserves_pitch_);
+
   shared_state_.renderer->Initialize(demuxer_, this, std::move(done_cb));
 }
 
@@ -1357,6 +1374,15 @@
                      base::Unretained(renderer_wrapper_.get()), latency_hint));
 }
 
+void PipelineImpl::SetPreservesPitch(bool preserves_pitch) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+
+  media_task_runner_->PostTask(
+      FROM_HERE, base::BindOnce(&RendererWrapper::SetPreservesPitch,
+                                base::Unretained(renderer_wrapper_.get()),
+                                preserves_pitch));
+}
+
 base::TimeDelta PipelineImpl::GetMediaTime() const {
   DCHECK(thread_checker_.CalledOnValidThread());
 
diff --git a/media/base/pipeline_impl.h b/media/base/pipeline_impl.h
index 101de818..d9444600 100644
--- a/media/base/pipeline_impl.h
+++ b/media/base/pipeline_impl.h
@@ -104,6 +104,7 @@
   float GetVolume() const override;
   void SetVolume(float volume) override;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
   base::TimeDelta GetMediaTime() const override;
   Ranges<base::TimeDelta> GetBufferedTimeRanges() const override;
   base::TimeDelta GetMediaDuration() const override;
diff --git a/media/base/pipeline_impl_unittest.cc b/media/base/pipeline_impl_unittest.cc
index 444f22c..01b1e8b 100644
--- a/media/base/pipeline_impl_unittest.cc
+++ b/media/base/pipeline_impl_unittest.cc
@@ -128,6 +128,8 @@
         .WillRepeatedly(Return(base::TimeDelta()));
 
     EXPECT_CALL(*demuxer_, GetStartTime()).WillRepeatedly(Return(start_time_));
+
+    EXPECT_CALL(*renderer_, SetPreservesPitch(true)).Times(AnyNumber());
   }
 
   ~PipelineImplTest() override {
@@ -302,6 +304,7 @@
     // |renderer_| has been deleted, replace it.
     scoped_renderer_.reset(new StrictMock<MockRenderer>());
     renderer_ = scoped_renderer_.get();
+    EXPECT_CALL(*renderer_, SetPreservesPitch(_)).Times(AnyNumber());
   }
 
   void ExpectResume(const base::TimeDelta& seek_time) {
@@ -606,6 +609,10 @@
   EXPECT_EQ(stats.video_memory_usage,
             pipeline_->GetStatistics().video_memory_usage);
 
+  // Make sure the preserves pitch flag is preserved between after resuming.
+  EXPECT_CALL(*renderer_, SetPreservesPitch(false)).Times(1);
+  pipeline_->SetPreservesPitch(false);
+
   ExpectSuspend();
   DoSuspend();
 
@@ -614,6 +621,8 @@
 
   base::TimeDelta expected = base::TimeDelta::FromSeconds(2000);
   ExpectResume(expected);
+  EXPECT_CALL(*renderer_, SetPreservesPitch(false)).Times(1);
+
   DoResume(expected);
 }
 
@@ -631,6 +640,21 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(PipelineImplTest, SetPreservesPitch) {
+  CreateAudioStream();
+  SetDemuxerExpectations();
+
+  // The audio renderer preserve pitch by default.
+  EXPECT_CALL(*renderer_, SetPreservesPitch(true));
+  StartPipelineAndExpect(PIPELINE_OK);
+  base::RunLoop().RunUntilIdle();
+
+  // Changes to the preservesPitch flag should be propagated.
+  EXPECT_CALL(*renderer_, SetPreservesPitch(false));
+  pipeline_->SetPreservesPitch(false);
+  base::RunLoop().RunUntilIdle();
+}
+
 TEST_F(PipelineImplTest, Properties) {
   CreateVideoStream();
   const auto kDuration = base::TimeDelta::FromSeconds(100);
diff --git a/media/base/renderer.cc b/media/base/renderer.cc
index f81f66b..c1970f9 100644
--- a/media/base/renderer.cc
+++ b/media/base/renderer.cc
@@ -30,4 +30,8 @@
   std::move(change_completed_cb).Run();
 }
 
+void Renderer::SetPreservesPitch(bool preserves_pitch) {
+  // Not supported by most renderers.
+}
+
 }  // namespace media
diff --git a/media/base/renderer.h b/media/base/renderer.h
index 7dc9b6b0..b6e1a73 100644
--- a/media/base/renderer.h
+++ b/media/base/renderer.h
@@ -51,6 +51,10 @@
   // thresholds.
   virtual void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) = 0;
 
+  // Sets whether pitch adjustment should be applied when the playback rate is
+  // different than 1.0.
+  virtual void SetPreservesPitch(bool preserves_pitch);
+
   // The following functions must be called after Initialize().
 
   // Discards any buffered data, executing |flush_cb| when completed.
diff --git a/media/blink/webmediaplayer_impl.cc b/media/blink/webmediaplayer_impl.cc
index fc18c34..7f6bf082 100644
--- a/media/blink/webmediaplayer_impl.cc
+++ b/media/blink/webmediaplayer_impl.cc
@@ -1001,6 +1001,11 @@
   pipeline_controller_->SetLatencyHint(latency_hint);
 }
 
+void WebMediaPlayerImpl::SetPreservesPitch(bool preserves_pitch) {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
+  pipeline_controller_->SetPreservesPitch(preserves_pitch);
+}
+
 void WebMediaPlayerImpl::OnRequestPictureInPicture() {
   if (!surface_layer_for_video_enabled_)
     ActivateSurfaceLayerForVideo();
diff --git a/media/blink/webmediaplayer_impl.h b/media/blink/webmediaplayer_impl.h
index 09959e4..f4e48c4 100644
--- a/media/blink/webmediaplayer_impl.h
+++ b/media/blink/webmediaplayer_impl.h
@@ -130,6 +130,7 @@
   void SetRate(double rate) override;
   void SetVolume(double volume) override;
   void SetLatencyHint(double seconds) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
   void OnRequestPictureInPicture() override;
   void OnTimeUpdate() override;
   void SetSinkId(
diff --git a/media/filters/audio_renderer_algorithm.cc b/media/filters/audio_renderer_algorithm.cc
index e9f759a..a07630b 100644
--- a/media/filters/audio_renderer_algorithm.cc
+++ b/media/filters/audio_renderer_algorithm.cc
@@ -190,6 +190,15 @@
                             base::Unretained(this)));
   }
 
+  if (reached_end_of_stream_ && !audio_buffer_.frames()) {
+    // Previous calls to ResampleAndFill() and OnResamplerRead() have used all
+    // of the available buffers from |audio_buffer_|. Some valid input buffers
+    // might be stuck in |resampler_.BufferedFrames()|, but the rest is silence.
+    // Forgo the few remaining valid buffers, or else we will keep playing out
+    // silence forever and never trigger any "ended" events.
+    return 0;
+  }
+
   // |resampler_| can request more than |requested_frames|, due to the
   // requests size not being aligned. To prevent having to fill it with silence,
   // we find the max number of reads it could request, and make sure we have
@@ -252,15 +261,9 @@
     return frames_read;
   }
 
-  // WSOLA at playback rates that are close to 1.0 produces noticeable
-  // warbling and stuttering. We prefer resampling the audio at these speeds.
-  // This does results in a noticeable pitch shift.
-  // NOTE: The cutoff values are arbitrary, and picked based off of a tradeoff
-  // between "resample pitch shift" vs "WSOLA distortions".
-  if (kLowerResampleThreshold <= playback_rate &&
-      playback_rate <= kUpperResampleThreshold) {
+  // Use resampling when no pitch adjustments are needed.
+  if (!preserves_pitch_)
     return ResampleAndFill(dest, dest_offset, requested_frames, playback_rate);
-  }
 
   // Destroy the resampler if it was used before, but it's no longer needed
   // (e.g. before playback rate has changed). This ensures that we don't try to
@@ -592,4 +595,8 @@
       AudioBus::WrapVector(search_block_->frames(), active_search_channels);
 }
 
+void AudioRendererAlgorithm::SetPreservesPitch(bool preserves_pitch) {
+  preserves_pitch_ = preserves_pitch;
+}
+
 }  // namespace media
diff --git a/media/filters/audio_renderer_algorithm.h b/media/filters/audio_renderer_algorithm.h
index 89187f7..1511fbb 100644
--- a/media/filters/audio_renderer_algorithm.h
+++ b/media/filters/audio_renderer_algorithm.h
@@ -40,11 +40,6 @@
 
 class MEDIA_EXPORT AudioRendererAlgorithm {
  public:
-  // Upper and lower bounds at which we prefer to use a resampler rather than
-  // WSOLA, to prevent audio artifacts.
-  static constexpr double kUpperResampleThreshold = 1.06;
-  static constexpr double kLowerResampleThreshold = 0.95;
-
   AudioRendererAlgorithm(MediaLog* media_log);
   AudioRendererAlgorithm(MediaLog* media_log,
                          AudioRendererAlgorithmParameters params);
@@ -90,6 +85,11 @@
   // value of nullopt indicates the algorithm should restore the default value.
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint);
 
+  // Sets a flag indicating whether apply pitch adjustments when playing back
+  // at rates other than 1.0. Concretely, we use WSOLA when this is true, and
+  // resampling when this is false.
+  void SetPreservesPitch(bool preserves_pitch);
+
   // Returns true if the |audio_buffer_| is >= |playback_threshold_|.
   bool IsQueueAdequateForPlayback();
 
@@ -202,6 +202,11 @@
   // start latency. See SetLatencyHint();
   base::Optional<base::TimeDelta> latency_hint_;
 
+  // Whether to apply pitch adjusments or not when playing back at rates other
+  // than 1.0. In other words, we use WSOLA to preserve pitch when this is on,
+  // and resampling when this
+  bool preserves_pitch_ = true;
+
   // How many frames to have in queue before beginning playback.
   int64_t playback_threshold_;
 
diff --git a/media/filters/audio_renderer_algorithm_unittest.cc b/media/filters/audio_renderer_algorithm_unittest.cc
index 7e22b92f..405d0b6 100644
--- a/media/filters/audio_renderer_algorithm_unittest.cc
+++ b/media/filters/audio_renderer_algorithm_unittest.cc
@@ -282,13 +282,9 @@
     EXPECT_NEAR(playback_rate, actual_playback_rate, playback_rate / 100.0);
   }
 
-  void TestPlaybackRateWithUnderflow(double playback_rate, bool end_of_stream) {
-    if (playback_rate > AudioRendererAlgorithm::kUpperResampleThreshold ||
-        playback_rate < AudioRendererAlgorithm::kLowerResampleThreshold) {
-      // This test is only used for the range in which we resample data instead
-      // of using WSOLA.
-      return;
-    }
+  void TestResamplingWithUnderflow(double playback_rate, bool end_of_stream) {
+    // We are only testing the behavior of the resampling case.
+    algorithm_.SetPreservesPitch(false);
 
     if (end_of_stream) {
       algorithm_.MarkEndOfStream();
@@ -452,13 +448,20 @@
 // The range of playback rates in which we use resampling is [0.95, 1.06].
 TEST_F(AudioRendererAlgorithmTest, FillBuffer_ResamplingRates) {
   Initialize();
-  TestPlaybackRate(0.94);  // WSOLA.
-  TestPlaybackRate(AudioRendererAlgorithm::kLowerResampleThreshold);
-  TestPlaybackRate(0.97);
+  // WSOLA.
+  TestPlaybackRate(0.50);
+  TestPlaybackRate(0.95);
   TestPlaybackRate(1.00);
-  TestPlaybackRate(1.04);
-  TestPlaybackRate(AudioRendererAlgorithm::kUpperResampleThreshold);
-  TestPlaybackRate(1.07);  // WSOLA.
+  TestPlaybackRate(1.05);
+  TestPlaybackRate(2.00);
+
+  // Resampling.
+  algorithm_.SetPreservesPitch(false);
+  TestPlaybackRate(0.50);
+  TestPlaybackRate(0.95);
+  TestPlaybackRate(1.00);
+  TestPlaybackRate(1.05);
+  TestPlaybackRate(2.00);
 }
 
 TEST_F(AudioRendererAlgorithmTest, FillBuffer_WithOffset) {
@@ -480,14 +483,10 @@
 
 TEST_F(AudioRendererAlgorithmTest, FillBuffer_UnderFlow) {
   Initialize();
-  TestPlaybackRateWithUnderflow(AudioRendererAlgorithm::kLowerResampleThreshold,
-                                true);
-  TestPlaybackRateWithUnderflow(AudioRendererAlgorithm::kLowerResampleThreshold,
-                                false);
-  TestPlaybackRateWithUnderflow(AudioRendererAlgorithm::kUpperResampleThreshold,
-                                true);
-  TestPlaybackRateWithUnderflow(AudioRendererAlgorithm::kUpperResampleThreshold,
-                                false);
+  TestResamplingWithUnderflow(0.75, true);
+  TestResamplingWithUnderflow(0.75, false);
+  TestResamplingWithUnderflow(1.25, true);
+  TestResamplingWithUnderflow(1.25, false);
 }
 
 TEST_F(AudioRendererAlgorithmTest, FillBuffer_OneAndAQuarterRate) {
diff --git a/media/filters/pipeline_controller.cc b/media/filters/pipeline_controller.cc
index c2f32d9..97e4306 100644
--- a/media/filters/pipeline_controller.cc
+++ b/media/filters/pipeline_controller.cc
@@ -389,6 +389,10 @@
   pipeline_->SetLatencyHint(latency_hint);
 }
 
+void PipelineController::SetPreservesPitch(bool preserves_pitch) {
+  pipeline_->SetPreservesPitch(preserves_pitch);
+}
+
 base::TimeDelta PipelineController::GetMediaTime() const {
   return pipeline_->GetMediaTime();
 }
diff --git a/media/filters/pipeline_controller.h b/media/filters/pipeline_controller.h
index 686b62c..08db9dcf 100644
--- a/media/filters/pipeline_controller.h
+++ b/media/filters/pipeline_controller.h
@@ -132,6 +132,7 @@
   float GetVolume() const;
   void SetVolume(float volume);
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint);
+  void SetPreservesPitch(bool preserves_pitch);
   base::TimeDelta GetMediaTime() const;
   Ranges<base::TimeDelta> GetBufferedTimeRanges() const;
   base::TimeDelta GetMediaDuration() const;
diff --git a/media/filters/pipeline_controller_unittest.cc b/media/filters/pipeline_controller_unittest.cc
index bf795f3..ffc509b 100644
--- a/media/filters/pipeline_controller_unittest.cc
+++ b/media/filters/pipeline_controller_unittest.cc
@@ -548,4 +548,13 @@
       PipelineController::State::SUSPENDED);
 }
 
+TEST_F(PipelineControllerTest, PreservesPitch) {
+  Complete(StartPipeline());
+  EXPECT_CALL(*pipeline_, SetPreservesPitch(false));
+  pipeline_controller_.SetPreservesPitch(false);
+
+  EXPECT_CALL(*pipeline_, SetPreservesPitch(true));
+  pipeline_controller_.SetPreservesPitch(true);
+}
+
 }  // namespace media
diff --git a/media/fuchsia/audio/fuchsia_audio_renderer.cc b/media/fuchsia/audio/fuchsia_audio_renderer.cc
index 325f05d..0b1c04b 100644
--- a/media/fuchsia/audio/fuchsia_audio_renderer.cc
+++ b/media/fuchsia/audio/fuchsia_audio_renderer.cc
@@ -234,6 +234,8 @@
   // shape and usefulness outside of fuchsia.
 }
 
+void FuchsiaAudioRenderer::SetPreservesPitch(bool preserves_pitch) {}
+
 void FuchsiaAudioRenderer::StartTicking() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
diff --git a/media/fuchsia/audio/fuchsia_audio_renderer.h b/media/fuchsia/audio/fuchsia_audio_renderer.h
index cf778e9..fec88cb1 100644
--- a/media/fuchsia/audio/fuchsia_audio_renderer.h
+++ b/media/fuchsia/audio/fuchsia_audio_renderer.h
@@ -43,6 +43,7 @@
   void StartPlaying() final;
   void SetVolume(float volume) final;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) final;
+  void SetPreservesPitch(bool preserves_pitch) final;
 
   // TimeSource implementation.
   void StartTicking() final;
diff --git a/media/remoting/end2end_test_renderer.cc b/media/remoting/end2end_test_renderer.cc
index dccffedf..932e0ab 100644
--- a/media/remoting/end2end_test_renderer.cc
+++ b/media/remoting/end2end_test_renderer.cc
@@ -364,6 +364,10 @@
   courier_renderer_->SetLatencyHint(latency_hint);
 }
 
+void End2EndTestRenderer::SetPreservesPitch(bool preserves_pitch) {
+  courier_renderer_->SetPreservesPitch(preserves_pitch);
+}
+
 void End2EndTestRenderer::Flush(base::OnceClosure flush_cb) {
   courier_renderer_->Flush(std::move(flush_cb));
 }
diff --git a/media/remoting/end2end_test_renderer.h b/media/remoting/end2end_test_renderer.h
index e0a8ff9e..905e27f 100644
--- a/media/remoting/end2end_test_renderer.h
+++ b/media/remoting/end2end_test_renderer.h
@@ -33,6 +33,7 @@
                   RendererClient* client,
                   PipelineStatusCallback init_cb) override;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
   void Flush(base::OnceClosure flush_cb) override;
   void StartPlayingFrom(base::TimeDelta time) override;
   void SetPlaybackRate(double playback_rate) override;
diff --git a/media/renderers/audio_renderer_impl.cc b/media/renderers/audio_renderer_impl.cc
index 95308f4..71421c8 100644
--- a/media/renderers/audio_renderer_impl.cc
+++ b/media/renderers/audio_renderer_impl.cc
@@ -619,6 +619,8 @@
   algorithm_->Initialize(audio_parameters_, is_encrypted_);
   if (latency_hint_)
     algorithm_->SetLatencyHint(latency_hint_);
+
+  algorithm_->SetPreservesPitch(preserves_pitch_);
   ConfigureChannelMask();
 
   ChangeState_Locked(kFlushed);
@@ -708,6 +710,15 @@
   }
 }
 
+void AudioRendererImpl::SetPreservesPitch(bool preserves_pitch) {
+  base::AutoLock auto_lock(lock_);
+
+  preserves_pitch_ = preserves_pitch;
+
+  if (algorithm_)
+    algorithm_->SetPreservesPitch(preserves_pitch);
+}
+
 void AudioRendererImpl::OnSuspend() {
   base::AutoLock auto_lock(lock_);
   is_suspending_ = true;
diff --git a/media/renderers/audio_renderer_impl.h b/media/renderers/audio_renderer_impl.h
index 33ef7e2..61c6b34 100644
--- a/media/renderers/audio_renderer_impl.h
+++ b/media/renderers/audio_renderer_impl.h
@@ -95,6 +95,7 @@
   void StartPlaying() override;
   void SetVolume(float volume) override;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
 
   // base::PowerObserver implementation.
   void OnSuspend() override;
@@ -293,6 +294,10 @@
   // during Initialize().
   base::Optional<base::TimeDelta> latency_hint_;
 
+  // Passed to |algorithm_|. Indicates whether |algorithm_| should or should not
+  // make pitch adjustments at playbacks other than 1.0.
+  bool preserves_pitch_ = true;
+
   // Simple state tracking variable.
   State state_;
 
diff --git a/media/renderers/decrypting_renderer.cc b/media/renderers/decrypting_renderer.cc
index e5c9856a..b231ec7 100644
--- a/media/renderers/decrypting_renderer.cc
+++ b/media/renderers/decrypting_renderer.cc
@@ -111,6 +111,10 @@
   renderer_->SetLatencyHint(latency_hint);
 }
 
+void DecryptingRenderer::SetPreservesPitch(bool preserves_pitch) {
+  renderer_->SetPreservesPitch(preserves_pitch);
+}
+
 void DecryptingRenderer::Flush(base::OnceClosure flush_cb) {
   renderer_->Flush(std::move(flush_cb));
 }
diff --git a/media/renderers/decrypting_renderer.h b/media/renderers/decrypting_renderer.h
index 6f443b4..84b9747 100644
--- a/media/renderers/decrypting_renderer.h
+++ b/media/renderers/decrypting_renderer.h
@@ -46,6 +46,7 @@
                   PipelineStatusCallback init_cb) override;
   void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) override;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
 
   void Flush(base::OnceClosure flush_cb) override;
   void StartPlayingFrom(base::TimeDelta time) override;
diff --git a/media/renderers/renderer_impl.cc b/media/renderers/renderer_impl.cc
index 051d203..aa1c0126 100644
--- a/media/renderers/renderer_impl.cc
+++ b/media/renderers/renderer_impl.cc
@@ -206,6 +206,14 @@
     audio_renderer_->SetLatencyHint(latency_hint);
 }
 
+void RendererImpl::SetPreservesPitch(bool preserves_pitch) {
+  DVLOG(1) << __func__;
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  if (audio_renderer_)
+    audio_renderer_->SetPreservesPitch(preserves_pitch);
+}
+
 void RendererImpl::Flush(base::OnceClosure flush_cb) {
   DVLOG(1) << __func__;
   DCHECK(task_runner_->BelongsToCurrentThread());
diff --git a/media/renderers/renderer_impl.h b/media/renderers/renderer_impl.h
index 8471862..f6603d3 100644
--- a/media/renderers/renderer_impl.h
+++ b/media/renderers/renderer_impl.h
@@ -58,6 +58,7 @@
                   PipelineStatusCallback init_cb) final;
   void SetCdm(CdmContext* cdm_context, CdmAttachedCB cdm_attached_cb) final;
   void SetLatencyHint(base::Optional<base::TimeDelta> latency_hint) final;
+  void SetPreservesPitch(bool preserves_pitch) final;
   void Flush(base::OnceClosure flush_cb) final;
   void StartPlayingFrom(base::TimeDelta time) final;
   void SetPlaybackRate(double playback_rate) final;
diff --git a/third_party/blink/public/platform/web_media_player.h b/third_party/blink/public/platform/web_media_player.h
index 1791d41..6c7a0570 100644
--- a/third_party/blink/public/platform/web_media_player.h
+++ b/third_party/blink/public/platform/web_media_player.h
@@ -167,6 +167,10 @@
   // value if the hint is cleared.
   virtual void SetLatencyHint(double seconds) = 0;
 
+  // Sets a flag indicating that the WebMediaPlayer should apply pitch
+  // adjustments when using a playback rate other than 1.0.
+  virtual void SetPreservesPitch(bool preserves_pitch) = 0;
+
   // The associated media element is going to enter Picture-in-Picture. This
   // method should make sure the player is set up for this and has a SurfaceId
   // as it will be needed.
diff --git a/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h b/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
index ec8468c..c2c9efb 100644
--- a/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
+++ b/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
@@ -114,6 +114,7 @@
   void SetRate(double rate) override;
   void SetVolume(double volume) override;
   void SetLatencyHint(double seconds) override;
+  void SetPreservesPitch(bool preserves_pitch) override;
   void OnRequestPictureInPicture() override;
   void OnPictureInPictureAvailabilityChanged(bool available) override;
   void SetSinkId(const WebString& sink_id,
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.cc b/third_party/blink/renderer/core/html/media/html_media_element.cc
index 3503c16c..1bcde9e 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.cc
+++ b/third_party/blink/renderer/core/html/media/html_media_element.cc
@@ -1317,6 +1317,8 @@
 
   web_media_player_->SetLatencyHint(latencyHint());
 
+  web_media_player_->SetPreservesPitch(preservesPitch());
+
   OnLoadStarted();
 }
 
@@ -2576,6 +2578,17 @@
   UpdatePlayState();
 }
 
+bool HTMLMediaElement::preservesPitch() const {
+  return preserves_pitch_;
+}
+
+void HTMLMediaElement::setPreservesPitch(bool preserves_pitch) {
+  preserves_pitch_ = preserves_pitch;
+
+  if (GetWebMediaPlayer())
+    GetWebMediaPlayer()->SetPreservesPitch(preserves_pitch_);
+}
+
 double HTMLMediaElement::latencyHint() const {
   // Parse error will fallback to std::numeric_limits<double>::quiet_NaN()
   double seconds = GetFloatingPointAttribute(html_names::kLatencyhintAttr);
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.h b/third_party/blink/renderer/core/html/media/html_media_element.h
index e85e368..f351646f 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.h
+++ b/third_party/blink/renderer/core/html/media/html_media_element.h
@@ -205,6 +205,8 @@
   void pause();
   double latencyHint() const;
   void setLatencyHint(double);
+  bool preservesPitch() const;
+  void setPreservesPitch(bool);
   void FlingingStarted();
   void FlingingStopped();
 
@@ -662,6 +664,10 @@
 
   bool was_always_muted_ : 1;
 
+  // Whether or not |web_media_player_| should apply pitch adjustments at
+  // playback raters other than 1.0.
+  bool preserves_pitch_ = true;
+
   Member<AudioTrackList> audio_tracks_;
   Member<VideoTrackList> video_tracks_;
   Member<TextTrackList> text_tracks_;
diff --git a/third_party/blink/renderer/core/html/media/html_media_element.idl b/third_party/blink/renderer/core/html/media/html_media_element.idl
index 3e38825..4bc7fd9 100644
--- a/third_party/blink/renderer/core/html/media/html_media_element.idl
+++ b/third_party/blink/renderer/core/html/media/html_media_element.idl
@@ -76,6 +76,8 @@
     void pause();
     [RuntimeEnabled=MediaLatencyHint, CEReactions]
     attribute double latencyHint;
+    [RuntimeEnabled=MediaPreservesPitch]
+    attribute boolean preservesPitch;
 
     // controls
     [CEReactions, Reflect] attribute boolean controls;
diff --git a/third_party/blink/renderer/modules/mediacapturefromelement/html_video_element_capturer_source_unittest.cc b/third_party/blink/renderer/modules/mediacapturefromelement/html_video_element_capturer_source_unittest.cc
index 4d24f21..2afb51fa 100644
--- a/third_party/blink/renderer/modules/mediacapturefromelement/html_video_element_capturer_source_unittest.cc
+++ b/third_party/blink/renderer/modules/mediacapturefromelement/html_video_element_capturer_source_unittest.cc
@@ -48,6 +48,7 @@
   void SetRate(double) override {}
   void SetVolume(double) override {}
   void SetLatencyHint(double) override {}
+  void SetPreservesPitch(bool) override {}
   void OnRequestPictureInPicture() override {}
   void OnPictureInPictureAvailabilityChanged(bool available) override {}
   WebTimeRanges Buffered() const override { return WebTimeRanges(); }
diff --git a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
index b9998ee..39a84a8 100644
--- a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
+++ b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
@@ -766,6 +766,12 @@
   // https://henbos.github.io/webrtc-timing/#dom-rtcrtpreceiver-playoutdelayhint
 }
 
+void WebMediaPlayerMS::SetPreservesPitch(bool preserves_pitch) {
+  // Since WebMediaPlayerMS::SetRate() is a no-op, it doesn't make sense to
+  // handle pitch preservation flags. The playback rate should always be 1.0,
+  // and thus there should be no pitch-shifting.
+}
+
 void WebMediaPlayerMS::OnRequestPictureInPicture() {
   if (!bridge_)
     ActivateSurfaceLayerForVideo();
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 7d461af..1148ec2a 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1093,6 +1093,10 @@
       status: "test",
     },
     {
+      name: "MediaPreservesPitch",
+      status: "experimental",
+    },
+    {
       name: "MediaQueryNavigationControls",
     },
     {
diff --git a/third_party/blink/renderer/platform/testing/empty_web_media_player.h b/third_party/blink/renderer/platform/testing/empty_web_media_player.h
index e6692af..2e7f32a 100644
--- a/third_party/blink/renderer/platform/testing/empty_web_media_player.h
+++ b/third_party/blink/renderer/platform/testing/empty_web_media_player.h
@@ -29,6 +29,7 @@
   void SetRate(double) override {}
   void SetVolume(double) override {}
   void SetLatencyHint(double) override {}
+  void SetPreservesPitch(bool) override {}
   void OnRequestPictureInPicture() override {}
   void OnPictureInPictureAvailabilityChanged(bool available) override {}
   SurfaceLayerMode GetVideoSurfaceLayerMode() const override {
diff --git a/third_party/blink/web_tests/webexposed/element-instance-property-listing-expected.txt b/third_party/blink/web_tests/webexposed/element-instance-property-listing-expected.txt
index aed5c3d..7767a9d 100644
--- a/third_party/blink/web_tests/webexposed/element-instance-property-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/element-instance-property-listing-expected.txt
@@ -417,6 +417,7 @@
     property playbackRate
     property played
     property preload
+    property preservesPitch
     property readyState
     property remote
     property seekable
@@ -1182,6 +1183,7 @@
     property playsInline
     property poster
     property preload
+    property preservesPitch
     property readyState
     property remote
     property requestPictureInPicture
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index c09b355..839f446 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -3667,6 +3667,7 @@
     getter playbackRate
     getter played
     getter preload
+    getter preservesPitch
     getter readyState
     getter remote
     getter seekable
@@ -3703,6 +3704,7 @@
     setter onwaitingforkey
     setter playbackRate
     setter preload
+    setter preservesPitch
     setter src
     setter srcObject
     setter volume