[mixer_service] Add SincSampler

Added more tracing to internal Sampler functions.
Fixed minor bug where the cache length was calculated by an additional
full frame instead of a single fractional subframe in order to include
the center position on top of the filter widths.

This change results in small performance gains in SincSampler by up to
~5% in all devices, whereas ~1 microsecond performance regression in the
mixer creation time, which is likely caused by the additional shim layer
that is introduced to create a `SincSampler` internally inside the old
`Mixer` creation code of audio_core (expected to be temporary).

Bug: 87651

Multiply: audio-libprocessing-unittests
Multiply: audio_core_unittests
Multiply: audio_mixer_unittests
Multiply: audio-core-api-pipeline-tests
Multiply: audio-core-fidelity-test
Multiply: audio_fidelity_tests
Multiply: audio_core_fidelity_tests

Change-Id: Id268065b58c2f1ae9b60bd4621ee1aa9081cc6a5
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/695491
Commit-Queue: Alper Gungormusler <alperg@google.com>
Reviewed-by: Tom Bergan <tombergan@google.com>
diff --git a/src/media/audio/audio_core/mixer/point_sampler.h b/src/media/audio/audio_core/mixer/point_sampler.h
index 4a70160..92afe2b 100644
--- a/src/media/audio/audio_core/mixer/point_sampler.h
+++ b/src/media/audio/audio_core/mixer/point_sampler.h
@@ -28,8 +28,8 @@
 
  protected:
   PointSampler(Gain::Limits gain_limits, std::shared_ptr<media_audio::Sampler> point_sampler)
-      : Mixer(Fixed::FromRaw(point_sampler->pos_filter_length().raw_value() - 1),
-              Fixed::FromRaw(point_sampler->neg_filter_length().raw_value() - 1), gain_limits),
+      : Mixer(point_sampler->pos_filter_length() - Fixed::FromRaw(1),
+              point_sampler->neg_filter_length() - Fixed::FromRaw(1), gain_limits),
         point_sampler_(std::move(point_sampler)) {}
 
  private:
diff --git a/src/media/audio/audio_core/mixer/sinc_sampler.cc b/src/media/audio/audio_core/mixer/sinc_sampler.cc
index 775a0c6..4e981c2 100644
--- a/src/media/audio/audio_core/mixer/sinc_sampler.cc
+++ b/src/media/audio/audio_core/mixer/sinc_sampler.cc
@@ -7,410 +7,98 @@
 #include <lib/trace/event.h>
 
 #include <algorithm>
-#include <limits>
+#include <memory>
 
-#include "src/media/audio/audio_core/mixer/constants.h"
-#include "src/media/audio/lib/processing/channel_strip.h"
-#include "src/media/audio/lib/processing/filter.h"
+#include "fidl/fuchsia.mediastreams/cpp/wire_types.h"
 #include "src/media/audio/lib/processing/position_manager.h"
 #include "src/media/audio/lib/processing/sampler.h"
+#include "src/media/audio/lib/processing/sinc_sampler.h"
 
 namespace media::audio::mixer {
 
-using ::media_audio::ChannelStrip;
+namespace {
+
 using ::media_audio::PositionManager;
-using ::media_audio::SincFilter;
+using ::media_audio::Sampler;
 
-template <int32_t DestChanCount, typename SourceSampleType, int32_t SourceChanCount>
-class SincSamplerImpl : public SincSampler {
- public:
-  SincSamplerImpl(int32_t source_frame_rate, int32_t dest_frame_rate, Gain::Limits gain_limits)
-      : SincSampler(
-            // TODO(fxbug.dev/72561): Convert Mixer and the rest of audio_core to a filter_width
-            // definition that includes [0] in its count (as SincFilter::Length does).
-            SincFilter::Length(source_frame_rate, dest_frame_rate) - Fixed::FromRaw(1),
-            // Sinc filters are symmetric (pos == neg, for coefficient values)
-            SincFilter::Length(source_frame_rate, dest_frame_rate) - Fixed::FromRaw(1),
-            gain_limits),
-        frac_filter_length_(SincFilter::Length(source_frame_rate, dest_frame_rate).raw_value()),
-        source_rate_(source_frame_rate),
-        dest_rate_(dest_frame_rate),
-        position_(SourceChanCount, DestChanCount, frac_filter_length_, frac_filter_length_),
-        working_data_(DestChanCount, kDataCacheLength),
-        // SincFilter holds one side of coefficients; we invert position to calc the negative side.
-        filter_(source_rate_, dest_rate_, frac_filter_length_) {
-    FX_CHECK(pos_filter_width() == neg_filter_width())
-        << "SincSampler assumes a symmetric filter, pos_filter_width (" << ffl::String::DecRational
-        << pos_filter_width() << ") != neg_filter_width (" << neg_filter_width() << ")";
-    // SincFilter draws from range [ceil(frac_pos - neg_width), floor(frac_pos + pos_width)].
-    // Including the center, SincFilter can require floor(neg_width + one + pos_width) samples.
-    // Be careful: this is not (necessarily) the same as floor(neg_width) + one + floor(pos_width)
-    const int64_t kCacheFramesNeeded =
-        Floor(neg_filter_width().raw_value() + kFracFrame + pos_filter_width().raw_value());
-    // We set our data cache length to the maximum filter width that might ever be needed.
-    FX_CHECK(kDataCacheLength >= kCacheFramesNeeded)
-        << "Data cache (len " << kDataCacheLength << ") must be at least " << kCacheFramesNeeded
-        << " long to support SRC ratio " << source_frame_rate << "/" << dest_frame_rate;
-  }
-
-  void Mix(float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr,
-           const void* source_void_ptr, int64_t source_frames, Fixed* source_offset_ptr,
-           bool accumulate) override;
-
-  // TODO(fxbug.dev/45074): This is for tests only and can be removed once filter creation is eager.
-  virtual void EagerlyPrepare() override { filter_.EagerlyPrepare(); }
-
- private:
-  // As an optimization, we work with raw fixed-point values internally, but we pass Fixed types
-  // through our public interfaces (to MixStage etc.) for source position/filter width/step size.
-  static constexpr int64_t kFracFrame = kOneFrame.raw_value();
-
-  static constexpr int64_t Ceiling(int64_t frac_position) {
-    return ((frac_position - 1) >> Fixed::Format::FractionalBits) + 1;
-  }
-  static constexpr int64_t Floor(int64_t frac_position) {
-    return frac_position >> Fixed::Format::FractionalBits;
-  }
-
-  // Our ChannelStrip must fit even the widest filter, and Filter::ComputeSample requires
-  // floor(neg_width + 1 + pos_width) samples at minimum (incl 1 full sample for the center).
-  static constexpr int64_t kDataCacheLength =
-      Floor(SincFilter::kMaxFracSideLength + kFracFrame + SincFilter::kMaxFracSideLength);
-
-  static constexpr int64_t kDataCacheFracLength = kDataCacheLength << Fixed::Format::FractionalBits;
-
-  template <media_audio::GainType GainType, bool DoAccumulate>
-  inline void Mix(float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr,
-                  const void* source_void_ptr, int64_t source_frames, Fixed* source_offset_ptr);
-
-  static inline void PopulateFramesToChannelStrip(const void* source_void_ptr,
-                                                  int64_t next_source_idx_to_copy,
-                                                  const int64_t frames_needed,
-                                                  ChannelStrip* channel_strip,
-                                                  int64_t next_cache_idx_to_fill);
-
-  const int64_t frac_filter_length_;
-  const int32_t source_rate_;
-  const int32_t dest_rate_;
-
-  PositionManager position_;
-  ChannelStrip working_data_;
-  SincFilter filter_;
-};
-
-// Implementation
-//
-// static
-template <int32_t DestChanCount, typename SourceSampleType, int32_t SourceChanCount>
-inline void
-SincSamplerImpl<DestChanCount, SourceSampleType, SourceChanCount>::PopulateFramesToChannelStrip(
-    const void* source_void_ptr, int64_t next_source_idx_to_copy, const int64_t frames_needed,
-    ChannelStrip* channel_strip, int64_t next_cache_idx_to_fill) {
-  if (kMixerPositionTraceEvents) {
-    TRACE_DURATION("audio", __func__, "next_source_idx_to_copy", next_source_idx_to_copy,
-                   "frames_needed", frames_needed, "next_cache_idx_to_fill",
-                   next_cache_idx_to_fill);
-  }
-  using SR = SourceReader<SourceSampleType, SourceChanCount, DestChanCount>;
-
-  const SourceSampleType* source_ptr = static_cast<const SourceSampleType*>(source_void_ptr);
-
-  for (int64_t source_idx = next_source_idx_to_copy;
-       source_idx < next_source_idx_to_copy + frames_needed;
-       ++source_idx, ++next_cache_idx_to_fill) {
-    auto current_source_ptr = source_ptr + (source_idx * SourceChanCount);
-
-    // Do this one dest_chan at a time
-    for (size_t dest_chan = 0; dest_chan < DestChanCount; ++dest_chan) {
-      (*channel_strip)[dest_chan][next_cache_idx_to_fill] = SR::Read(current_source_ptr, dest_chan);
-    }
-  }
-}
-
-// If upper layers call with GainType MUTED, they must set DoAccumulate=TRUE. They guarantee new
-// buffers are cleared before usage; we optimize accordingly.
-template <int32_t DestChanCount, typename SourceSampleType, int32_t SourceChanCount>
-template <media_audio::GainType GainType, bool DoAccumulate>
-inline void SincSamplerImpl<DestChanCount, SourceSampleType, SourceChanCount>::Mix(
-    float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr, const void* source_void_ptr,
-    int64_t source_frames, Fixed* source_offset_ptr) {
-  TRACE_DURATION("audio", "SincSamplerImpl::MixInternal", "source_rate", source_rate_, "dest_rate",
-                 dest_rate_, "source_chans", SourceChanCount, "dest_chans", DestChanCount);
-
-  static_assert(GainType != media_audio::GainType::kSilent || DoAccumulate == true,
-                "Mixing muted streams without accumulation is explicitly unsupported");
-
-  auto info = &bookkeeping();
-  auto frac_source_offset = source_offset_ptr->raw_value();
-  const auto frac_neg_width = neg_filter_width().raw_value();
-  position_.SetSourceValues(source_void_ptr, source_frames, source_offset_ptr);
-  position_.SetDestValues(dest_ptr, dest_frames, dest_offset_ptr);
-  position_.SetRateValues(info->step_size.raw_value(), info->rate_modulo(), info->denominator(),
-                          &info->source_pos_modulo);
-
-  int64_t next_cache_idx_to_fill = 0;
-  auto next_source_idx_to_copy = Ceiling(frac_source_offset - frac_neg_width);
-
-  // Do we need previously-cached values?
-  if (next_source_idx_to_copy < 0) {
-    next_cache_idx_to_fill = -next_source_idx_to_copy;
-    next_source_idx_to_copy = 0;
-  }
-
-  // If we don't have enough source or dest to mix even one frame, get out. Before leaving, if we've
-  // reached the end of the source buffer, then cache the last few source frames for the next mix.
-  if (!position_.CanFrameBeMixed()) {
-    if (position_.IsSourceConsumed()) {
-      const auto frames_needed = source_frames - next_source_idx_to_copy;
-      if (frac_source_offset > 0) {
-        working_data_.ShiftBy(Ceiling(frac_source_offset));
-      }
-
-      // Calculate and store the last few source frames to start of channel_strip, for next time.
-      // If muted, this is unnecessary because we've already shifted in zeroes (silence).
-      if constexpr (GainType != media_audio::GainType::kSilent) {
-        PopulateFramesToChannelStrip(source_void_ptr, next_source_idx_to_copy, frames_needed,
-                                     &working_data_, next_cache_idx_to_fill);
-      }
-    }
-    return;
-  }
-
-  if constexpr (GainType != media_audio::GainType::kSilent) {
-    Gain::AScale amplitude_scale;
-    int64_t dest_ramp_start;  // only used when ramping
-    if constexpr (GainType != media_audio::GainType::kRamping) {
-      amplitude_scale = info->gain.GetGainScale();
-    } else {
-      dest_ramp_start = position_.dest_offset();
-    }
-
-    auto frac_source_offset_to_cache = Ceiling(frac_source_offset - frac_neg_width) * kFracFrame;
-    auto frames_needed = std::min(source_frames - next_source_idx_to_copy,
-                                  kDataCacheLength - next_cache_idx_to_fill);
-
-    // Bring in as much as a channel strip of source data (while channel/format-converting).
-    PopulateFramesToChannelStrip(source_void_ptr, next_source_idx_to_copy, frames_needed,
-                                 &working_data_, next_cache_idx_to_fill);
-
-    while (position_.CanFrameBeMixed()) {
-      next_source_idx_to_copy += frames_needed;
-
-      int64_t frac_cache_offset = frac_source_offset - frac_source_offset_to_cache;
-      int64_t frac_interp_fraction = frac_cache_offset & Fixed::Format::FractionalMask;
-      auto cache_center_idx = Floor(frac_cache_offset);
-      FX_CHECK(Ceiling(frac_cache_offset - frac_neg_width) >= 0)
-          << Ceiling(frac_cache_offset - frac_neg_width) << " should be >= 0";
-      if (kMixerPositionTraceEvents) {
-        TRACE_DURATION("audio", "SincSampler::Mix chunk", "next_source_idx_to_copy",
-                       next_source_idx_to_copy, "cache_center_idx", cache_center_idx);
-      }
-
-      while (position_.CanFrameBeMixed() &&
-             frac_cache_offset + pos_filter_width().raw_value() < kDataCacheFracLength) {
-        auto dest_frame = position_.CurrentDestFrame();
-        if constexpr (GainType == media_audio::GainType::kRamping) {
-          amplitude_scale = info->scale_arr[position_.dest_offset() - dest_ramp_start];
-        }
-
-        for (size_t dest_chan = 0; dest_chan < DestChanCount; ++dest_chan) {
-          float sample = filter_.ComputeSample(frac_interp_fraction,
-                                               &(working_data_[dest_chan][cache_center_idx]));
-          media_audio::MixSample<GainType, DoAccumulate>(sample, &dest_frame[dest_chan],
-                                                         amplitude_scale);
-        }
-
-        frac_source_offset = position_.AdvanceFrame();
-
-        frac_cache_offset = frac_source_offset - frac_source_offset_to_cache;
-        frac_interp_fraction = frac_cache_offset & Fixed::Format::FractionalMask;
-        cache_center_idx = Floor(frac_cache_offset);
-      }
-
-      // idx of the earliest cached frame we must retain == the amount by which we can left-shift
-      auto num_frames_to_shift = Ceiling(frac_cache_offset - frac_neg_width);
-      working_data_.ShiftBy(num_frames_to_shift);
-
-      cache_center_idx -= num_frames_to_shift;
-      next_cache_idx_to_fill = kDataCacheLength - num_frames_to_shift;
-
-      frac_source_offset_to_cache = Ceiling(frac_source_offset - frac_neg_width) * kFracFrame;
-      frames_needed = std::min(source_frames - next_source_idx_to_copy,
-                               kDataCacheLength - next_cache_idx_to_fill);
-
-      PopulateFramesToChannelStrip(source_void_ptr, next_source_idx_to_copy, frames_needed,
-                                   &working_data_, next_cache_idx_to_fill);
-    }
-  } else {
-    auto num_source_frames_skipped = position_.AdvanceToEnd();
-    working_data_.ShiftBy(num_source_frames_skipped);
-  }
-
-  position_.UpdateOffsets();
-}
-
-template <int32_t DestChanCount, typename SourceSampleType, int32_t SourceChanCount>
-void SincSamplerImpl<DestChanCount, SourceSampleType, SourceChanCount>::Mix(
-    float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr, const void* source_void_ptr,
-    int64_t source_frames, Fixed* source_offset_ptr, bool accumulate) {
-  auto info = &bookkeeping();
-
-  // CheckPositions expects a frac_pos_filter_length param that _includes_ [0], so we use
-  // frac_filter_length_ instead of pos_filter_width().
-  // TODO(fxbug.dev/72561): Convert Mixer class (and audio_core all-up) to define filter width as
-  // including the center in its count (as PositionManager and Filter::Length do). Any distinction
-  // between filter length/filter width would go away. We would use pos_filter_width() here.
-  PositionManager::CheckPositions(dest_frames, dest_offset_ptr, source_frames,
-                                  source_offset_ptr->raw_value(), frac_filter_length_,
-                                  info->step_size.raw_value(), info->rate_modulo(),
-                                  info->denominator(), info->source_pos_modulo);
-
-  if (info->gain.IsUnity()) {
-    return accumulate ? Mix<media_audio::GainType::kUnity, true>(dest_ptr, dest_frames,
-                                                                 dest_offset_ptr, source_void_ptr,
-                                                                 source_frames, source_offset_ptr)
-                      : Mix<media_audio::GainType::kUnity, false>(dest_ptr, dest_frames,
-                                                                  dest_offset_ptr, source_void_ptr,
-                                                                  source_frames, source_offset_ptr);
-  }
-
-  if (info->gain.IsSilent()) {
-    return Mix<media_audio::GainType::kSilent, true>(
-        dest_ptr, dest_frames, dest_offset_ptr, source_void_ptr, source_frames, source_offset_ptr);
-  }
-
-  if (info->gain.IsRamping()) {
-    const auto max_frames = Mixer::Bookkeeping::kScaleArrLen + *dest_offset_ptr;
-    if (dest_frames > max_frames) {
-      dest_frames = max_frames;
-    }
-
-    return accumulate ? Mix<media_audio::GainType::kRamping, true>(dest_ptr, dest_frames,
-                                                                   dest_offset_ptr, source_void_ptr,
-                                                                   source_frames, source_offset_ptr)
-                      : Mix<media_audio::GainType::kRamping, false>(
-                            dest_ptr, dest_frames, dest_offset_ptr, source_void_ptr, source_frames,
-                            source_offset_ptr);
-  }
-
-  return accumulate ? Mix<media_audio::GainType::kNonUnity, true>(dest_ptr, dest_frames,
-                                                                  dest_offset_ptr, source_void_ptr,
-                                                                  source_frames, source_offset_ptr)
-                    : Mix<media_audio::GainType::kNonUnity, false>(
-                          dest_ptr, dest_frames, dest_offset_ptr, source_void_ptr, source_frames,
-                          source_offset_ptr);
-}
-
-// Templates used to expand  the different combinations of possible SincSampler configurations.
-template <int32_t DestChanCount, typename SourceSampleType, int32_t SourceChanCount>
-static inline std::unique_ptr<Mixer> SelectSSM(const fuchsia::media::AudioStreamType& source_format,
-                                               const fuchsia::media::AudioStreamType& dest_format,
-                                               Gain::Limits gain_limits) {
-  return std::make_unique<SincSamplerImpl<DestChanCount, SourceSampleType, SourceChanCount>>(
-      source_format.frames_per_second, dest_format.frames_per_second, gain_limits);
-}
-
-template <int32_t DestChanCount, typename SourceSampleType>
-static inline std::unique_ptr<Mixer> SelectSSM(const fuchsia::media::AudioStreamType& source_format,
-                                               const fuchsia::media::AudioStreamType& dest_format,
-                                               Gain::Limits gain_limits) {
-  TRACE_DURATION("audio", "SelectSSM(dChan,sType)");
-
-  switch (source_format.channels) {
-    case 1:
-      if constexpr (DestChanCount <= 4) {
-        return SelectSSM<DestChanCount, SourceSampleType, 1>(source_format, dest_format,
-                                                             gain_limits);
-      }
-      break;
-    case 2:
-      if constexpr (DestChanCount <= 4) {
-        return SelectSSM<DestChanCount, SourceSampleType, 2>(source_format, dest_format,
-                                                             gain_limits);
-      }
-      break;
-    case 3:
-      // Unlike other samplers, we handle 3:3 here since there is no NxN sinc sampler variant.
-      if constexpr (DestChanCount <= 3) {
-        return SelectSSM<DestChanCount, SourceSampleType, 3>(source_format, dest_format,
-                                                             gain_limits);
-      }
-      break;
-    case 4:
-      // Unlike other samplers, we handle 4:4 here since there is no NxN sinc sampler variant.
-      // Like other samplers, to mix 4-channel sources to Mono or Stereo destinations, we mix
-      // (average) multiple source channels to each destination channel. Given a 4-channel source
-      // (call it A|B|C|D), the Stereo output channel-mix would be [avg(A,C), avg(B,D)] and the Mono
-      // output channel-mix would be [avg(A,B,C,D)].
-      //
-      // Audio formats do not include info needed to filter frequencies or 3D-locate channels.
-      // TODO(fxbug.dev/13679): enable the mixer to rechannelize in a more sophisticated way.
-      // TODO(fxbug.dev/13682): account for frequency range (e.g. "4-channel" stereo woofer+tweeter)
-      if constexpr (DestChanCount <= 2 || DestChanCount == 4) {
-        return SelectSSM<DestChanCount, SourceSampleType, 4>(source_format, dest_format,
-                                                             gain_limits);
-      }
-      break;
-    default:
-      break;
-  }
-  FX_LOGS(WARNING) << "SincSampler does not support this channelization: " << source_format.channels
-                   << " -> " << dest_format.channels;
-  return nullptr;
-}
-
-template <int32_t DestChanCount>
-static inline std::unique_ptr<Mixer> SelectSSM(const fuchsia::media::AudioStreamType& source_format,
-                                               const fuchsia::media::AudioStreamType& dest_format,
-                                               Gain::Limits gain_limits) {
-  TRACE_DURATION("audio", "SelectSSM(dChan)");
-
-  switch (source_format.sample_format) {
+fuchsia_mediastreams::wire::AudioSampleFormat ToNewSampleFormat(
+    fuchsia::media::AudioSampleFormat sample_format) {
+  switch (sample_format) {
     case fuchsia::media::AudioSampleFormat::UNSIGNED_8:
-      return SelectSSM<DestChanCount, uint8_t>(source_format, dest_format, gain_limits);
+      return fuchsia_mediastreams::wire::AudioSampleFormat::kUnsigned8;
     case fuchsia::media::AudioSampleFormat::SIGNED_16:
-      return SelectSSM<DestChanCount, int16_t>(source_format, dest_format, gain_limits);
+      return fuchsia_mediastreams::wire::AudioSampleFormat::kSigned16;
     case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32:
-      return SelectSSM<DestChanCount, int32_t>(source_format, dest_format, gain_limits);
+      return fuchsia_mediastreams::wire::AudioSampleFormat::kSigned24In32;
     case fuchsia::media::AudioSampleFormat::FLOAT:
-      return SelectSSM<DestChanCount, float>(source_format, dest_format, gain_limits);
     default:
-      FX_LOGS(WARNING) << "SincSampler does not support this sample_format: "
-                       << static_cast<int64_t>(source_format.sample_format);
-      return nullptr;
+      return fuchsia_mediastreams::wire::AudioSampleFormat::kFloat;
   }
 }
 
+media_audio::Format ToNewFormat(const fuchsia::media::AudioStreamType& format) {
+  return media_audio::Format::CreateOrDie(
+      {ToNewSampleFormat(format.sample_format), format.channels, format.frames_per_second});
+}
+
+}  // namespace
+
 std::unique_ptr<Mixer> SincSampler::Select(const fuchsia::media::AudioStreamType& source_format,
                                            const fuchsia::media::AudioStreamType& dest_format,
                                            Gain::Limits gain_limits) {
   TRACE_DURATION("audio", "SincSampler::Select");
 
-  if (source_format.channels > 4) {
-    FX_LOGS(WARNING) << "SincSampler does not support this channelization: "
-                     << source_format.channels << " -> " << dest_format.channels;
+  auto sinc_sampler =
+      media_audio::SincSampler::Create(ToNewFormat(source_format), ToNewFormat(dest_format));
+  if (!sinc_sampler) {
     return nullptr;
   }
 
-  switch (dest_format.channels) {
-    case 1:
-      return SelectSSM<1>(source_format, dest_format, gain_limits);
-    case 2:
-      return SelectSSM<2>(source_format, dest_format, gain_limits);
-    case 3:
-      return SelectSSM<3>(source_format, dest_format, gain_limits);
-    case 4:
-      // For now, to mix Mono and Stereo sources to 4-channel destinations, we duplicate source
-      // channels across multiple destinations (Stereo LR becomes LRLR, Mono M becomes MMMM).
-      // Audio formats do not include info needed to filter frequencies or 3D-locate channels.
-      // TODO(fxbug.dev/13679): enable the mixer to rechannelize in a more sophisticated way.
-      // TODO(fxbug.dev/13682): account for frequency range (e.g. a "4-channel" stereo
-      // woofer+tweeter).
-      return SelectSSM<4>(source_format, dest_format, gain_limits);
-    default:
-      FX_LOGS(WARNING) << "SincSampler does not support this channelization: "
-                       << source_format.channels << " -> " << dest_format.channels;
-      return nullptr;
+  struct MakePublicCtor : SincSampler {
+    MakePublicCtor(Gain::Limits gain_limits, std::shared_ptr<Sampler> sinc_sampler)
+        : SincSampler(gain_limits, std::move(sinc_sampler)) {}
+  };
+  return std::make_unique<MakePublicCtor>(gain_limits, std::move(sinc_sampler));
+}
+
+void SincSampler::EagerlyPrepare() { sinc_sampler_->EagerlyPrepare(); }
+
+void SincSampler::Mix(float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr,
+                      const void* source_void_ptr, int64_t source_frames, Fixed* source_offset_ptr,
+                      bool accumulate) {
+  TRACE_DURATION("audio", "SincSampler::Mix");
+
+  auto info = &bookkeeping();
+  PositionManager::CheckPositions(
+      dest_frames, dest_offset_ptr, source_frames, source_offset_ptr->raw_value(),
+      sinc_sampler_->pos_filter_length().raw_value(), info->step_size.raw_value(),
+      info->rate_modulo(), info->denominator(), info->source_pos_modulo);
+  static_cast<media_audio::SincSampler*>(sinc_sampler_.get())
+      ->SetRateValues(info->step_size.raw_value(), info->rate_modulo(), info->denominator(),
+                      &info->source_pos_modulo);
+
+  Sampler::Source source{source_void_ptr, source_offset_ptr, source_frames};
+  Sampler::Dest dest{dest_ptr, dest_offset_ptr, dest_frames};
+  if (info->gain.IsSilent()) {
+    // If the gain is silent, the mixer simply skips over the appropriate range in the destination
+    // buffer, leaving whatever data is already there. We do not take further effort to clear the
+    // buffer if `accumulate` is false. In fact, we IGNORE `accumulate` if silent. The caller is
+    // responsible for clearing the destination buffer before Mix is initially called.
+    sinc_sampler_->Process(source, dest, Sampler::Gain{.type = media_audio::GainType::kSilent},
+                           true);
+  } else if (info->gain.IsUnity()) {
+    sinc_sampler_->Process(source, dest, Sampler::Gain{.type = media_audio::GainType::kUnity},
+                           accumulate);
+  } else if (info->gain.IsRamping()) {
+    sinc_sampler_->Process(
+        source, dest,
+        Sampler::Gain{.type = media_audio::GainType::kRamping, .scale_ramp = info->scale_arr.get()},
+        accumulate);
+  } else {
+    sinc_sampler_->Process(
+        source, dest,
+        Sampler::Gain{.type = media_audio::GainType::kNonUnity, .scale = info->gain.GetGainScale()},
+        accumulate);
   }
 }
 
diff --git a/src/media/audio/audio_core/mixer/sinc_sampler.h b/src/media/audio/audio_core/mixer/sinc_sampler.h
index 965b990..df4090b 100644
--- a/src/media/audio/audio_core/mixer/sinc_sampler.h
+++ b/src/media/audio/audio_core/mixer/sinc_sampler.h
@@ -6,7 +6,12 @@
 
 #include <fuchsia/media/cpp/fidl.h>
 
+#include <memory>
+#include <utility>
+
 #include "src/media/audio/audio_core/mixer/mixer.h"
+#include "src/media/audio/lib/format2/fixed.h"
+#include "src/media/audio/lib/processing/sampler.h"
 
 namespace media::audio::mixer {
 
@@ -16,9 +21,19 @@
                                        const fuchsia::media::AudioStreamType& dest_format,
                                        Gain::Limits gain_limits = {});
 
+  void EagerlyPrepare() override;
+  void Mix(float* dest_ptr, int64_t dest_frames, int64_t* dest_offset_ptr,
+           const void* source_void_ptr, int64_t source_frames, Fixed* source_offset_ptr,
+           bool accumulate) override;
+
  protected:
-  SincSampler(Fixed pos_filter_width, Fixed neg_filter_width, Gain::Limits gain_limits)
-      : Mixer(pos_filter_width, neg_filter_width, gain_limits) {}
+  SincSampler(Gain::Limits gain_limits, std::shared_ptr<media_audio::Sampler> sinc_sampler)
+      : Mixer(sinc_sampler->pos_filter_length() - Fixed::FromRaw(1),
+              sinc_sampler->neg_filter_length() - Fixed::FromRaw(1), gain_limits),
+        sinc_sampler_(std::move(sinc_sampler)) {}
+
+ private:
+  std::shared_ptr<media_audio::Sampler> sinc_sampler_;
 };
 
 }  // namespace media::audio::mixer
diff --git a/src/media/audio/lib/processing/BUILD.gn b/src/media/audio/lib/processing/BUILD.gn
index 3d764f2..3be6f8a 100644
--- a/src/media/audio/lib/processing/BUILD.gn
+++ b/src/media/audio/lib/processing/BUILD.gn
@@ -48,6 +48,8 @@
     "position_manager.cc",
     "position_manager.h",
     "sampler.h",
+    "sinc_sampler.cc",
+    "sinc_sampler.h",
   ]
 
   deps = [
@@ -128,6 +130,7 @@
     "point_sampler_unittest.cc",
     "position_manager_unittest.cc",
     "sampler_unittest.cc",
+    "sinc_sampler_unittest.cc",
   ]
 
   deps = [
diff --git a/src/media/audio/lib/processing/point_sampler.cc b/src/media/audio/lib/processing/point_sampler.cc
index 9d8b020..990465d 100644
--- a/src/media/audio/lib/processing/point_sampler.cc
+++ b/src/media/audio/lib/processing/point_sampler.cc
@@ -4,6 +4,8 @@
 
 #include "src/media/audio/lib/processing/point_sampler.h"
 
+#include <lib/trace/event.h>
+
 #include <algorithm>
 #include <cstdint>
 #include <memory>
@@ -38,8 +40,11 @@
       : PointSampler(Fixed::FromRaw(kFracPositiveFilterLength),
                      Fixed::FromRaw(kFracNegativeFilterLength)) {}
 
-  // Processes `source` into `dest` with `gain`.
+  void EagerlyPrepare() final {}
+
   void Process(Source source, Dest dest, Gain gain, bool accumulate) final {
+    TRACE_DURATION("audio", "PointSampler::Process");
+
     switch (gain.type) {
       case GainType::kSilent:
         if (accumulate) {
@@ -75,16 +80,6 @@
   }
 
  private:
-  // As an optimization, we work with raw fixed-point values internally although we pass `Fixed`
-  // types through our public interfaces.
-  static int64_t Ceiling(int64_t frac_position) {
-    return ((frac_position - 1) >> Fixed::Format::FractionalBits) + 1;
-  }
-
-  static int64_t Floor(int64_t frac_position) {
-    return frac_position >> Fixed::Format::FractionalBits;
-  }
-
   // Processes `source` into `dest` with gain `Type`.
   template <GainType Type, bool Accumulate>
   static void ProcessWith(const Source& source, const Dest& dest, const Gain& gain) {
@@ -105,12 +100,13 @@
     }
 
     // Process destination frames.
-    const int64_t frames_to_process = std::min(Ceiling(source_frac_end - source_frac_offset),
-                                               dest.frame_count - dest_frame_offset);
+    const int64_t frames_to_process =
+        std::min(Sampler::Ceiling(source_frac_end - source_frac_offset),
+                 dest.frame_count - dest_frame_offset);
 
     if constexpr (Type != GainType::kSilent) {
       const auto* source_frame = &static_cast<const SourceSampleType*>(
-          source.samples)[Floor(source_frac_offset + kFracPositiveFilterLength - 1) *
+          source.samples)[Sampler::Floor(source_frac_offset + kFracPositiveFilterLength - 1) *
                           SourceChannelCount];
       float* dest_frame = &dest.samples[dest_frame_offset * DestChannelCount];
 
@@ -222,6 +218,8 @@
 
 std::shared_ptr<Sampler> PointSampler::Create(const Format& source_format,
                                               const Format& dest_format) {
+  TRACE_DURATION("audio", "PointSampler::Create");
+
   if (source_format.frames_per_second() != dest_format.frames_per_second()) {
     FX_LOGS(WARNING) << "PointSampler source frame rate " << source_format.frames_per_second()
                      << " must be equal to dest frame rate " << dest_format.frames_per_second();
diff --git a/src/media/audio/lib/processing/position_manager.cc b/src/media/audio/lib/processing/position_manager.cc
index 2aebbc6..784cf8b 100644
--- a/src/media/audio/lib/processing/position_manager.cc
+++ b/src/media/audio/lib/processing/position_manager.cc
@@ -15,15 +15,6 @@
 
 namespace media_audio {
 
-namespace {
-
-// Enable to emit trace events containing the position state.
-// TODO(fxbug.dev/87651): Move this constant to a common location if needed during audio core
-// `media::audio::Mixer` migration.
-constexpr bool kTracePositionEvents = false;
-
-}  // namespace
-
 PositionManager::PositionManager(int32_t source_channel_count, int32_t dest_channel_count,
                                  int64_t frac_positive_length, int64_t frac_negative_length)
     : source_channel_count_(source_channel_count),
@@ -109,7 +100,7 @@
 
 void PositionManager::SetDestValues(float* dest_ptr, int64_t dest_frame_count,
                                     int64_t* dest_offset_ptr) {
-  if (kTracePositionEvents) {
+  if constexpr (kTracePositionEvents) {
     TRACE_DURATION("audio", __func__, "dest_frame_count", dest_frame_count, "dest_offset",
                    *dest_offset_ptr);
   }
@@ -123,7 +114,7 @@
 
 void PositionManager::SetSourceValues(const void* source_void_ptr, int64_t source_frame_count,
                                       Fixed* source_offset_ptr) {
-  if (kTracePositionEvents) {
+  if constexpr (kTracePositionEvents) {
     TRACE_DURATION("audio", __func__, "source_frame_count", source_frame_count, "source_offset",
                    source_offset_ptr->Integral().Floor(), "source_offset.frac",
                    source_offset_ptr->Fraction().raw_value());
@@ -140,7 +131,7 @@
 
 void PositionManager::SetRateValues(int64_t frac_step_size, uint64_t rate_modulo,
                                     uint64_t denominator, uint64_t* source_pos_mod) {
-  if (kTracePositionEvents) {
+  if constexpr (kTracePositionEvents) {
     TRACE_DURATION("audio", __func__, "step_size",
                    Fixed::FromRaw(frac_step_size).Integral().Floor(), "step_size.frac",
                    Fixed::FromRaw(frac_step_size).Fraction().raw_value(), "rate_modulo",
diff --git a/src/media/audio/lib/processing/position_manager.h b/src/media/audio/lib/processing/position_manager.h
index 5e38203..7836ffc 100644
--- a/src/media/audio/lib/processing/position_manager.h
+++ b/src/media/audio/lib/processing/position_manager.h
@@ -11,6 +11,11 @@
 
 namespace media_audio {
 
+// Enable to emit trace events containing the position state.
+// TODO(fxbug.dev/87651): Move this constant to a common location if needed during audio core
+// `media::audio::Mixer` migration.
+inline constexpr bool kTracePositionEvents = false;
+
 // Class that handles the updating of source and destination positions, as a resampler steps through
 // source buffers with a specific step size (based on the resampling ratio). This class extracts a
 // significant amount of duplicate code across the samplers.
diff --git a/src/media/audio/lib/processing/sampler.h b/src/media/audio/lib/processing/sampler.h
index 0671c71..a9e882d 100644
--- a/src/media/audio/lib/processing/sampler.h
+++ b/src/media/audio/lib/processing/sampler.h
@@ -72,11 +72,11 @@
   // Gain to be applied to the processed destination data.
   struct Gain {
     // Gain type.
-    GainType type;
+    GainType type = GainType::kUnity;
 
     union {
       // Constant gain scale. This will be valid iff the gain `type != GainType::kRamping`.
-      float scale;
+      float scale = kUnityGainScale;
       // Pointer to the array of gain scale ramp, where each value represents the gain scale for
       // each destination frame. The length of this ramp must match the destination frame count.
       // This will be valid iff the gain `type == GainType::kRamping`.
@@ -87,6 +87,11 @@
   // Default destructor.
   virtual ~Sampler() = default;
 
+  // Eagerly precomputes any needed data. If not called, that data will be lazily computed on the
+  // first call to `Process`.
+  // TODO(fxbug.dev/45074): This is for tests only and can be removed once filter creation is eager.
+  virtual void EagerlyPrepare() = 0;
+
   // Processes `source` into `dest` with `gain`.
   virtual void Process(Source source, Dest dest, Gain gain, bool accumulate) = 0;
 
@@ -100,6 +105,16 @@
   Sampler(Fixed pos_filter_length, Fixed neg_filter_length)
       : pos_filter_length_(pos_filter_length), neg_filter_length_(neg_filter_length) {}
 
+  // Ceils `frac_position` in frames.
+  static constexpr int64_t Ceiling(int64_t frac_position) {
+    return ((frac_position - 1) >> Fixed::Format::FractionalBits) + 1;
+  }
+
+  // Floors `frac_position` in frames.
+  static constexpr int64_t Floor(int64_t frac_position) {
+    return frac_position >> Fixed::Format::FractionalBits;
+  }
+
  private:
   const Fixed pos_filter_length_;
   const Fixed neg_filter_length_;
diff --git a/src/media/audio/lib/processing/sinc_sampler.cc b/src/media/audio/lib/processing/sinc_sampler.cc
new file mode 100644
index 0000000..30a7579
--- /dev/null
+++ b/src/media/audio/lib/processing/sinc_sampler.cc
@@ -0,0 +1,329 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/media/audio/lib/processing/sinc_sampler.h"
+
+#include <lib/trace/event.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+
+#include <ffl/string.h>
+
+#include "fidl/fuchsia.mediastreams/cpp/wire_types.h"
+#include "lib/syslog/cpp/macros.h"
+#include "src/media/audio/lib/format2/channel_mapper.h"
+#include "src/media/audio/lib/format2/fixed.h"
+#include "src/media/audio/lib/format2/format.h"
+#include "src/media/audio/lib/processing/channel_strip.h"
+#include "src/media/audio/lib/processing/filter.h"
+#include "src/media/audio/lib/processing/gain.h"
+#include "src/media/audio/lib/processing/position_manager.h"
+
+namespace media_audio {
+
+namespace {
+
+template <typename SourceSampleType, size_t SourceChannelCount, size_t DestChannelCount>
+class SincSamplerImpl : public SincSampler {
+ public:
+  SincSamplerImpl(int32_t source_frame_rate, int32_t dest_frame_rate)
+      : SincSampler(SincFilter::Length(source_frame_rate, dest_frame_rate),
+                    // Sinc filters are symmetric.
+                    SincFilter::Length(source_frame_rate, dest_frame_rate)),
+        source_frame_rate_(source_frame_rate),
+        dest_frame_rate_(dest_frame_rate),
+        position_(SourceChannelCount, DestChannelCount, pos_filter_length().raw_value(),
+                  neg_filter_length().raw_value()),
+        working_data_(DestChannelCount, kDataCacheLength),
+        // `SincFilter` holds one side of coefficients (positive), we invert the position to
+        // calculate the other side (negative).
+        filter_(source_frame_rate_, dest_frame_rate_, pos_filter_length().raw_value()) {
+    FX_CHECK(pos_filter_length() == neg_filter_length())
+        << "SincSampler assumes a symmetric filter, pos_filter_length (" << ffl::String::DecRational
+        << pos_filter_length() << ") != neg_filter_length (" << neg_filter_length() << ")";
+
+    const int64_t cache_length_needed =
+        Sampler::Floor(neg_filter_length().raw_value() + pos_filter_length().raw_value() - 1);
+    FX_CHECK(kDataCacheLength >= cache_length_needed)
+        << "Data cache (len " << kDataCacheLength << ") must be at least " << cache_length_needed
+        << " long to support SRC ratio " << source_frame_rate << "/" << dest_frame_rate;
+  }
+
+  void EagerlyPrepare() final { filter_.EagerlyPrepare(); }
+
+  void Process(Source source, Dest dest, Gain gain, bool accumulate) final {
+    TRACE_DURATION("audio", "SincSamplerImpl::Process", "source_rate", source_frame_rate_,
+                   "dest_rate", dest_frame_rate_, "source_chans", SourceChannelCount, "dest_chans",
+                   DestChannelCount);
+
+    position_.SetSourceValues(source.samples, source.frame_count, source.frame_offset_ptr);
+    position_.SetDestValues(dest.samples, dest.frame_count, dest.frame_offset_ptr);
+
+    switch (gain.type) {
+      case GainType::kSilent:
+        if (accumulate) {
+          ProcessWith<GainType::kSilent, true>(source, dest, gain);
+        } else {
+          ProcessWith<GainType::kSilent, false>(source, dest, gain);
+        }
+        break;
+      case GainType::kNonUnity:
+        if (accumulate) {
+          ProcessWith<GainType::kNonUnity, true>(source, dest, gain);
+        } else {
+          ProcessWith<GainType::kNonUnity, false>(source, dest, gain);
+        }
+        break;
+      case GainType::kUnity:
+        if (accumulate) {
+          ProcessWith<GainType::kUnity, true>(source, dest, gain);
+        } else {
+          ProcessWith<GainType::kUnity, false>(source, dest, gain);
+        }
+        break;
+      case GainType::kRamping:
+        if (accumulate) {
+          ProcessWith<GainType::kRamping, true>(source, dest, gain);
+        } else {
+          ProcessWith<GainType::kRamping, false>(source, dest, gain);
+        }
+        break;
+      default:
+        break;
+    }
+  }
+
+  void SetRateValues(int64_t step_size, uint64_t rate_modulo, uint64_t denominator,
+                     uint64_t* source_pos_mod) final {
+    position_.SetRateValues(step_size, rate_modulo, denominator, source_pos_mod);
+  }
+
+ private:
+  static void PopulateFramesToChannelStrip(const void* source_void_ptr,
+                                           int64_t next_source_idx_to_copy,
+                                           const int64_t frames_needed, ChannelStrip* channel_strip,
+                                           int64_t next_cache_idx_to_fill) {
+    if constexpr (kTracePositionEvents) {
+      TRACE_DURATION("audio", __func__, "next_source_idx_to_copy", next_source_idx_to_copy,
+                     "frames_needed", frames_needed, "next_cache_idx_to_fill",
+                     next_cache_idx_to_fill);
+    }
+
+    const SourceSampleType* source_ptr = static_cast<const SourceSampleType*>(source_void_ptr);
+    for (int64_t source_idx = next_source_idx_to_copy;
+         source_idx < next_source_idx_to_copy + frames_needed;
+         ++source_idx, ++next_cache_idx_to_fill) {
+      const SourceSampleType* source_frame = &source_ptr[source_idx * SourceChannelCount];
+      for (size_t dest_chan = 0; dest_chan < DestChannelCount; ++dest_chan) {
+        (*channel_strip)[dest_chan][next_cache_idx_to_fill] = mapper_.Map(source_frame, dest_chan);
+      }
+    }
+  }
+
+  // Processes `source` into `dest` with gain `Type`.
+  template <GainType Type, bool Accumulate>
+  void ProcessWith(const Source& source, const Dest& dest, const Gain& gain) {
+    int64_t frac_source_offset = source.frame_offset_ptr->raw_value();
+    const auto frac_filter_width = pos_filter_length().raw_value() - 1;
+
+    int64_t next_cache_idx_to_fill = 0;
+    auto next_source_idx_to_copy = Sampler::Ceiling(frac_source_offset - frac_filter_width);
+
+    // Do we need previously-cached values?
+    if (next_source_idx_to_copy < 0) {
+      next_cache_idx_to_fill = -next_source_idx_to_copy;
+      next_source_idx_to_copy = 0;
+    }
+
+    // If we don't have enough source or dest to mix even one frame, get out. Before leaving, if
+    // we've reached the end of the source buffer, then cache the last few source frames for the
+    // next mix.
+    if (!position_.CanFrameBeMixed()) {
+      if (position_.IsSourceConsumed()) {
+        const auto frames_needed = source.frame_count - next_source_idx_to_copy;
+        if (frac_source_offset > 0) {
+          working_data_.ShiftBy(Sampler::Ceiling(frac_source_offset));
+        }
+
+        // Calculate and store the last few source frames to start of channel_strip, for next time.
+        // If muted, this is unnecessary because we've already shifted in zeroes (silence).
+        if constexpr (Type != GainType::kSilent) {
+          PopulateFramesToChannelStrip(source.samples, next_source_idx_to_copy, frames_needed,
+                                       &working_data_, next_cache_idx_to_fill);
+        }
+      }
+      return;
+    }
+
+    if constexpr (Type != media_audio::GainType::kSilent) {
+      auto frac_source_offset_to_cache =
+          Sampler::Ceiling(frac_source_offset - frac_filter_width) * kFracOneFrame;
+      auto frames_needed = std::min(source.frame_count - next_source_idx_to_copy,
+                                    kDataCacheLength - next_cache_idx_to_fill);
+
+      // Bring in as much as a channel strip of source data (while channel/format-converting).
+      PopulateFramesToChannelStrip(source.samples, next_source_idx_to_copy, frames_needed,
+                                   &working_data_, next_cache_idx_to_fill);
+
+      float scale = gain.scale;
+      int64_t dest_ramp_start = position_.dest_offset();  // only used when ramping
+      while (position_.CanFrameBeMixed()) {
+        next_source_idx_to_copy += frames_needed;
+
+        int64_t frac_cache_offset = frac_source_offset - frac_source_offset_to_cache;
+        int64_t frac_interp_fraction = frac_cache_offset & Fixed::Format::FractionalMask;
+        auto cache_center_idx = Sampler::Floor(frac_cache_offset);
+        FX_CHECK(Sampler::Ceiling(frac_cache_offset - frac_filter_width) >= 0)
+            << Sampler::Ceiling(frac_cache_offset - frac_filter_width) << " should be >= 0";
+        if constexpr (kTracePositionEvents) {
+          TRACE_DURATION("audio", "SincSampler::Process chunk", "next_source_idx_to_copy",
+                         next_source_idx_to_copy, "cache_center_idx", cache_center_idx);
+        }
+
+        while (position_.CanFrameBeMixed() &&
+               frac_cache_offset + frac_filter_width < kFracDataCacheLength) {
+          float* dest_frame = position_.CurrentDestFrame();
+          if constexpr (Type == media_audio::GainType::kRamping) {
+            scale = gain.scale_ramp[position_.dest_offset() - dest_ramp_start];
+          }
+
+          for (size_t dest_chan = 0; dest_chan < DestChannelCount; ++dest_chan) {
+            const float sample = filter_.ComputeSample(
+                frac_interp_fraction, &(working_data_[dest_chan][cache_center_idx]));
+            MixSample<Type, Accumulate>(sample, &dest_frame[dest_chan], scale);
+          }
+
+          frac_source_offset = position_.AdvanceFrame();
+
+          frac_cache_offset = frac_source_offset - frac_source_offset_to_cache;
+          frac_interp_fraction = frac_cache_offset & Fixed::Format::FractionalMask;
+          cache_center_idx = Sampler::Floor(frac_cache_offset);
+        }
+
+        // idx of the earliest cached frame we must retain == the amount by which we can left-shift.
+        const auto num_frames_to_shift = Sampler::Ceiling(frac_cache_offset - frac_filter_width);
+        working_data_.ShiftBy(num_frames_to_shift);
+
+        cache_center_idx -= num_frames_to_shift;
+        next_cache_idx_to_fill = kDataCacheLength - num_frames_to_shift;
+
+        frac_source_offset_to_cache =
+            Sampler::Ceiling(frac_source_offset - frac_filter_width) * kFracOneFrame;
+        frames_needed = std::min(source.frame_count - next_source_idx_to_copy,
+                                 kDataCacheLength - next_cache_idx_to_fill);
+
+        PopulateFramesToChannelStrip(source.samples, next_source_idx_to_copy, frames_needed,
+                                     &working_data_, next_cache_idx_to_fill);
+      }
+    } else {
+      if constexpr (!Accumulate) {
+        // Zero fill destination frames.
+        const int64_t dest_frame_offset = *dest.frame_offset_ptr;
+        std::fill_n(&dest.samples[dest_frame_offset * DestChannelCount],
+                    (dest.frame_count - dest_frame_offset) * DestChannelCount, 0.0f);
+      }
+      auto num_source_frames_skipped = position_.AdvanceToEnd();
+      working_data_.ShiftBy(num_source_frames_skipped);
+    }
+
+    position_.UpdateOffsets();
+  }
+
+  // Our `ChannelStrip` must fit even the widest filter.
+  static constexpr int64_t kDataCacheLength = Sampler::Floor(
+      SincFilter::kMaxFracSideLength + kFracOneFrame + SincFilter::kMaxFracSideLength);
+  static constexpr int64_t kFracDataCacheLength = kDataCacheLength << Fixed::Format::FractionalBits;
+
+  static inline ChannelMapper<SourceSampleType, SourceChannelCount, DestChannelCount> mapper_;
+
+  const int32_t source_frame_rate_;
+  const int32_t dest_frame_rate_;
+
+  PositionManager position_;
+  ChannelStrip working_data_;
+  SincFilter filter_;
+};
+
+// Helper functions to expand the combinations of possible `SincSamplerImpl` configurations.
+template <typename SourceSampleType, size_t SourceChannelCount, size_t DestChannelCount>
+std::shared_ptr<Sampler> CreateWith(const Format& source_format, const Format& dest_format) {
+  return std::make_shared<SincSamplerImpl<SourceSampleType, SourceChannelCount, DestChannelCount>>(
+      static_cast<int32_t>(source_format.frames_per_second()),
+      static_cast<int32_t>(dest_format.frames_per_second()));
+}
+
+template <typename SourceSampleType, size_t SourceChannelCount>
+std::shared_ptr<Sampler> CreateWith(const Format& source_format, const Format& dest_format) {
+  switch (dest_format.channels()) {
+    case 1:
+      return CreateWith<SourceSampleType, SourceChannelCount, 1>(source_format, dest_format);
+    case 2:
+      return CreateWith<SourceSampleType, SourceChannelCount, 2>(source_format, dest_format);
+    case 3:
+      if constexpr (SourceChannelCount <= 3) {
+        return CreateWith<SourceSampleType, SourceChannelCount, 3>(source_format, dest_format);
+      }
+      break;
+    case 4:
+      if constexpr (SourceChannelCount != 3) {
+        return CreateWith<SourceSampleType, SourceChannelCount, 4>(source_format, dest_format);
+      }
+      break;
+    default:
+      break;
+  }
+  FX_LOGS(WARNING) << "SincSampler does not support this channelization: " << SourceChannelCount
+                   << " -> " << dest_format.channels();
+  return nullptr;
+}
+
+template <typename SourceSampleType>
+std::shared_ptr<Sampler> CreateWith(const Format& source_format, const Format& dest_format) {
+  switch (source_format.channels()) {
+    case 1:
+      return CreateWith<SourceSampleType, 1>(source_format, dest_format);
+    case 2:
+      return CreateWith<SourceSampleType, 2>(source_format, dest_format);
+    case 3:
+      return CreateWith<SourceSampleType, 3>(source_format, dest_format);
+    case 4:
+      return CreateWith<SourceSampleType, 4>(source_format, dest_format);
+    default:
+      FX_LOGS(WARNING) << "SincSampler does not support this channelization: "
+                       << source_format.channels() << " -> " << dest_format.channels();
+      return nullptr;
+  }
+}
+
+}  // namespace
+
+std::shared_ptr<Sampler> SincSampler::Create(const Format& source_format,
+                                             const Format& dest_format) {
+  TRACE_DURATION("audio", "SincSampler::Create");
+
+  if (dest_format.sample_format() != fuchsia_mediastreams::wire::AudioSampleFormat::kFloat) {
+    FX_LOGS(WARNING) << "SincSampler does not support this dest sample format: "
+                     << static_cast<uint32_t>(dest_format.sample_format());
+    return nullptr;
+  }
+
+  switch (source_format.sample_format()) {
+    case fuchsia_mediastreams::wire::AudioSampleFormat::kUnsigned8:
+      return CreateWith<uint8_t>(source_format, dest_format);
+    case fuchsia_mediastreams::wire::AudioSampleFormat::kSigned16:
+      return CreateWith<int16_t>(source_format, dest_format);
+    case fuchsia_mediastreams::wire::AudioSampleFormat::kSigned24In32:
+      return CreateWith<int32_t>(source_format, dest_format);
+    case fuchsia_mediastreams::wire::AudioSampleFormat::kFloat:
+      return CreateWith<float>(source_format, dest_format);
+    default:
+      FX_LOGS(WARNING) << "SincSampler does not support this source sample format: "
+                       << static_cast<uint32_t>(source_format.sample_format());
+      return nullptr;
+  }
+}
+
+}  // namespace media_audio
diff --git a/src/media/audio/lib/processing/sinc_sampler.h b/src/media/audio/lib/processing/sinc_sampler.h
new file mode 100644
index 0000000..8e2a415
--- /dev/null
+++ b/src/media/audio/lib/processing/sinc_sampler.h
@@ -0,0 +1,33 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_MEDIA_AUDIO_LIB_PROCESSING_SINC_SAMPLER_H_
+#define SRC_MEDIA_AUDIO_LIB_PROCESSING_SINC_SAMPLER_H_
+
+#include <cstdint>
+#include <memory>
+
+#include "src/media/audio/lib/format2/format.h"
+#include "src/media/audio/lib/processing/sampler.h"
+
+namespace media_audio {
+
+class SincSampler : public Sampler {
+ public:
+  // Creates new `SincSampler` for a given `source_format` and `dest_format`.
+  static std::shared_ptr<Sampler> Create(const Format& source_format, const Format& dest_format);
+
+  // TODO(fxbug.dev/87651): This is temporary to preserve the existing `media::audio::Mixer` API, to
+  // be refactored once we switch to the new mixer service mix stage.
+  virtual void SetRateValues(int64_t step_size, uint64_t rate_modulo, uint64_t denominator,
+                             uint64_t* source_pos_mod) = 0;
+
+ protected:
+  SincSampler(Fixed pos_filter_length, Fixed neg_filter_length)
+      : Sampler(pos_filter_length, neg_filter_length) {}
+};
+
+}  // namespace media_audio
+
+#endif  // SRC_MEDIA_AUDIO_LIB_PROCESSING_SINC_SAMPLER_H_
diff --git a/src/media/audio/lib/processing/sinc_sampler_unittest.cc b/src/media/audio/lib/processing/sinc_sampler_unittest.cc
new file mode 100644
index 0000000..94ecbf1
--- /dev/null
+++ b/src/media/audio/lib/processing/sinc_sampler_unittest.cc
@@ -0,0 +1,213 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/media/audio/lib/processing/sinc_sampler.h"
+
+#include <cstdint>
+#include <utility>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "ffl/fixed.h"
+#include "fidl/fuchsia.mediastreams/cpp/wire_types.h"
+#include "lib/syslog/cpp/macros.h"
+#include "src/media/audio/lib/format2/fixed.h"
+#include "src/media/audio/lib/processing/gain.h"
+
+namespace media_audio {
+namespace {
+
+using ::fuchsia_mediastreams::wire::AudioSampleFormat;
+using ::testing::Each;
+using ::testing::NotNull;
+
+constexpr std::pair<uint32_t, uint32_t> kChannelConfigs[] = {
+    {1, 1}, {1, 2}, {1, 3}, {1, 4}, {2, 1}, {2, 2}, {2, 3},
+    {2, 4}, {3, 1}, {3, 2}, {3, 3}, {4, 1}, {4, 2}, {4, 4},
+};
+
+constexpr uint32_t kFrameRates[] = {
+    8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000,
+};
+
+constexpr AudioSampleFormat kSampleFormats[] = {
+    AudioSampleFormat::kUnsigned8,
+    AudioSampleFormat::kSigned16,
+    AudioSampleFormat::kSigned24In32,
+    AudioSampleFormat::kFloat,
+};
+
+Format CreateFormat(uint32_t channel_count, uint32_t frame_rate, AudioSampleFormat sample_format) {
+  return Format::CreateOrDie({sample_format, channel_count, frame_rate});
+}
+
+TEST(SincSamplerTest, CreateWithValidConfigs) {
+  for (const auto& [source_channel_count, dest_channel_count] : kChannelConfigs) {
+    for (const auto& source_frame_rate : kFrameRates) {
+      for (const auto& dest_frame_rate : kFrameRates) {
+        for (const auto& sample_format : kSampleFormats) {
+          EXPECT_THAT(
+              SincSampler::Create(
+                  CreateFormat(source_channel_count, source_frame_rate, sample_format),
+                  CreateFormat(dest_channel_count, dest_frame_rate, AudioSampleFormat::kFloat)),
+              NotNull());
+        }
+      }
+    }
+  }
+}
+
+TEST(SincSamplerTest, ProcessSilentGain) {
+  auto sampler = SincSampler::Create(CreateFormat(1, 48000, AudioSampleFormat::kFloat),
+                                     CreateFormat(1, 48000, AudioSampleFormat::kFloat));
+  ASSERT_THAT(sampler, NotNull());
+
+  const int64_t dest_frame_count = 5;
+  // Make sure to provide enough samples to compensate for the filter length.
+  const int64_t source_frame_count = dest_frame_count + sampler->pos_filter_length().Floor();
+
+  std::vector<float> source_samples(source_frame_count);
+  for (int i = 0; i < source_frame_count; ++i) {
+    source_samples[i] = static_cast<float>(i + 1);
+  }
+  std::vector<float> dest_samples(dest_frame_count, 1.0f);
+
+  Fixed source_offset = Fixed(0);
+  int64_t dest_offset = 0;
+
+  const Sampler::Gain gain = {.type = GainType::kSilent, .scale = kMinGainScale};
+
+  // Process with silent gain with accumulation, `dest_samples` should remain as-is.
+  sampler->Process({source_samples.data(), &source_offset, source_frame_count},
+                   {dest_samples.data(), &dest_offset, dest_frame_count}, gain,
+                   /*accumulate=*/true);
+  EXPECT_THAT(dest_samples, Each(1.0f));
+
+  // Reset offsets and process with silent gain again, but this time with no accumulation, which
+  // should fill `dest_samples` with all zeros now.
+  source_offset = Fixed(0);
+  dest_offset = 0;
+  sampler->Process({source_samples.data(), &source_offset, source_frame_count},
+                   {dest_samples.data(), &dest_offset, dest_frame_count}, gain,
+                   /*accumulate=*/false);
+  EXPECT_THAT(dest_samples, Each(0.0f));
+}
+
+class SincSamplerOutputTest : public testing::Test {
+ protected:
+  // Based on an arbitrary near-zero source position (-1/128), with a sinc curve for unity rate
+  // conversion, we use data values calculated so that if these first 13 values (the filter's
+  // negative wing) are ignored, we expect a generated output value of
+  // `kValueWithoutPreviousFrames`. If they are NOT ignored, then we expect the result
+  // `kValueWithPreviousFrames`.
+  static constexpr float kSource[] = {
+      1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f,
+      1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f, 1330.10897f, -1330.10897f,
+      1330.10897f,  // ... source frames to satisfy negative filter length.
+      -10.001010f,  // Center source frame
+      268.88298f,   // Source frames to satisfy positive filter length ...
+      -268.88298f, 268.88298f,   -268.88298f, 268.88298f,   -268.88298f, 268.88298f,
+      -268.88298f, 268.88298f,   -268.88298f, 268.88298f,   -268.88298f, 268.88298f,
+  };
+  static constexpr Fixed kProcessOneFrameSourceOffset = ffl::FromRatio(1, 128);
+  // The center frame should contribute -10.0, the positive wing -5.0, and the negative wing +25.0.
+  static constexpr float kValueWithoutPreviousFrames = -15.0;
+  static constexpr float kValueWithPreviousFrames = 10.0;
+
+  // Processes a single frame of output based on `kSource[0]`.
+  static float ProcessOneFrame(Sampler& sampler, Fixed source_offset) {
+    auto neg_length = sampler.neg_filter_length().Floor();
+    auto pos_length = sampler.pos_filter_length().Floor();
+    EXPECT_NE(Fixed(pos_length).raw_value(), sampler.neg_filter_length().raw_value() - 1)
+        << "This test assumes SincSampler is symmetric, and that negative width includes a "
+           "fraction";
+
+    float dest_sample = 0.0f;
+    int64_t dest_offset = 0;
+
+    sampler.Process(Sampler::Source{&kSource[neg_length - 1], &source_offset, pos_length},
+                    Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false);
+    EXPECT_EQ(dest_offset, 1u) << "No output frame was produced";
+
+    FX_LOGS(INFO) << "Coefficients " << std::setprecision(12) << kSource[12] << " " << kSource[13]
+                  << " " << kSource[14] << ", value " << dest_sample;
+
+    return dest_sample;
+  }
+};
+
+TEST_F(SincSamplerOutputTest, ProcessOneNoCache) {
+  auto sampler = SincSampler::Create(CreateFormat(1, 44100, AudioSampleFormat::kFloat),
+                                     CreateFormat(1, 44100, AudioSampleFormat::kFloat));
+  ASSERT_THAT(sampler, NotNull());
+
+  // Process a single frame. We use a slightly non-zero position because at true 0, only the sample
+  // (not the positive or negative wings) are used. In this case we not provided previous frames.
+  const float dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset);
+
+  // If we incorrectly shifted/retained even a single frame of the above data, this won't match.
+  EXPECT_FLOAT_EQ(dest_sample, kValueWithoutPreviousFrames) << std::setprecision(12) << dest_sample;
+}
+
+TEST_F(SincSamplerOutputTest, ProcessOneWithCache) {
+  auto sampler = SincSampler::Create(CreateFormat(1, 44100, AudioSampleFormat::kFloat),
+                                     CreateFormat(1, 44100, AudioSampleFormat::kFloat));
+  ASSERT_THAT(sampler, NotNull());
+  auto neg_length = sampler->neg_filter_length().Floor();
+
+  // Now, populate the cache with previous frames, instead of using default (silence) values.
+  // The output value of `source_offset` tells us the cache is populated with `neg_length - 1`
+  // frames, which is ideal for sampling a subsequent source buffer starting at source position 0.
+  float dest_sample = 0.0f;
+  int64_t dest_offset = 0;
+  const auto source_frame_count = neg_length - 1;
+  Fixed source_offset = Fixed(source_frame_count) - kProcessOneFrameSourceOffset;
+
+  sampler->Process(Sampler::Source{&kSource[0], &source_offset, source_frame_count},
+                   Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false);
+  EXPECT_EQ(source_offset, Fixed(source_frame_count) - kProcessOneFrameSourceOffset);
+  EXPECT_EQ(dest_offset, 0u) << "Unexpectedly produced output " << dest_sample;
+
+  // Process a single frame. We use a slightly non-zero position because at true 0, only the sample
+  // itself (not positive or negative widths) are needed. In this case we provide previous frames.
+  dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset);
+
+  // If we incorrectly shifted/retained even a single frame of the above data, this won't match.
+  EXPECT_FLOAT_EQ(dest_sample, kValueWithPreviousFrames) << std::setprecision(12) << dest_sample;
+}
+
+TEST_F(SincSamplerOutputTest, ProcessFrameByFrameCached) {
+  auto sampler = SincSampler::Create(CreateFormat(1, 44100, AudioSampleFormat::kFloat),
+                                     CreateFormat(1, 44100, AudioSampleFormat::kFloat));
+  ASSERT_THAT(sampler, NotNull());
+  auto neg_length = sampler->neg_filter_length().Floor();
+
+  // Now, populate the cache with previous data, one frame at a time.
+  float dest_sample = 0.0f;
+  int64_t dest_offset = 0;
+  const auto source_frame_count = 1;
+  Fixed source_offset = Fixed(source_frame_count) - kProcessOneFrameSourceOffset;
+
+  for (auto neg_idx = 0; neg_idx < neg_length - 1; ++neg_idx) {
+    sampler->Process(Sampler::Source{&kSource[neg_idx], &source_offset, source_frame_count},
+                     Sampler::Dest{&dest_sample, &dest_offset, 1}, {}, false);
+    EXPECT_EQ(source_offset, Fixed(source_frame_count) - kProcessOneFrameSourceOffset);
+    EXPECT_EQ(dest_offset, 0u) << "Unexpectedly produced output " << dest_sample;
+  }
+
+  // Process a single frame. We use a slightly non-zero position because at true 0, only the sample
+  // itself (not positive or negative widths) are needed. In this case we provide previous frames.
+  dest_sample = ProcessOneFrame(*sampler, -kProcessOneFrameSourceOffset);
+
+  // If we incorrectly shifted/retained even a single frame of the above data, this won't match.
+  EXPECT_FLOAT_EQ(dest_sample, kValueWithPreviousFrames) << std::setprecision(12) << dest_sample;
+}
+
+// TODO(fxbug.dev/87651): Move the rest of the `media::audio::mixer::SincSampler` unit tests once
+// `media::audio::mixer::Mixer` code is fully migrated.
+
+}  // namespace
+}  // namespace media_audio