Introduction

Every music track you have ever burned to a CD was stored at 44 100 samples per second (44.1 kHz). Every video your phone records carries audio at 48 000 samples per second (48 kHz). Those two numbers come from different industries — one from digital audio in the 1970s, one from broadcast television — and they never fully agreed to use the same clock.

The result is that audio must cross that boundary constantly: when a music track lands in a video edit, when a streaming service transcodes a podcast, when a game engine mixes a sound effect with a voiceover recorded in a different studio. If you simply drop some samples or repeat others, the result sounds wrong — pitched artifacts, hissing, muffled highs.

The clean solution has been known since the 1970s and rests on two ideas from signal processing: the Nyquist–Shannon sampling theorem, which tells you how fast you need to sample to capture a signal faithfully, and polyphase filter banks, which let you interpolate and anti-alias in a single, computationally efficient pass. Together they turn the awkward fraction 160147\tfrac{160}{147} (the ratio 48000/4410048000 / 44100 in lowest terms) into a practical, artifact-free operation.

Try It

The demo below resamples a pure tone from 44.1 kHz to 48 kHz in your browser. Choose Polyphase to hear the clean conversion, or Naive (drop/repeat) to hear what aliasing and imaging artifacts sound like. The waveform canvas shows the resampled output so you can see the smoothness — or the jagged edges.

<!-- Demo layout — method selector, frequency slider, waveform canvas, info row -->
<div class="controls">
  <label class="ctrl-label">Resampling method</label>
  <div class="btn-group">
    <button id="btn-poly" class="method-btn active" data-method="polyphase">Polyphase (clean)</button>
    <button id="btn-naive" class="method-btn" data-method="naive">Naive (drop/repeat)</button>
  </div>
  <label class="ctrl-label">Tone frequency</label>
  <div class="freq-row">
    <input id="freq-slider" type="range" min="200" max="8000" step="100" value="1000">
    <span id="freq-display">1000 Hz</span>
  </div>
</div>
<canvas id="wave" width="560" height="140"></canvas>
<div class="info-row">
  <span class="badge in-badge">In: 44 100 Hz</span>
  <span class="arrow">&#8594;</span>
  <span class="badge out-badge">Out: 48 000 Hz</span>
  <span id="status-msg" class="status-msg"></span>
</div>
<p class="hint-text">The polyphase filter uses 160 sub-filters (one per interpolated phase) each with 64 taps — matching the ratio 160/147. Naive resampling simply picks the nearest input sample, creating visible steps and audible aliasing.</p>
/* Styles for the sample-rate conversion demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.ctrl-label { font-size: .8rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.btn-group { display: flex; gap: .4rem; flex-wrap: wrap; }
.method-btn { font: 600 13px system-ui; padding: .35rem .75rem; border: 1.5px solid #1d3557;
              background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; transition: all .15s; }
.method-btn.active { background: #1d3557; color: #fff; }
.freq-row { display: flex; align-items: center; gap: .6rem; }
#freq-slider { flex: 1; max-width: 280px; accent-color: #1d3557; }
#freq-display { font: 600 14px ui-monospace, monospace; color: #1d3557; min-width: 70px; }
canvas { display: block; width: 100%; border: 1px solid #dde3ea; border-radius: 8px;
         background: #f7f9fb; margin-bottom: .6rem; }
.info-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.badge { font: 600 12px ui-monospace; padding: .2rem .5rem; border-radius: 4px; }
.in-badge { background: #d1e8ff; color: #1a4a8a; }
.out-badge { background: #d4f4e2; color: #1a6640; }
.arrow { font-size: 1.1rem; color: #888; }
.status-msg { font-size: .88rem; font-weight: 600; color: #0a7d33; }
.hint-text { font-size: .84rem; color: #555; margin: 0; line-height: 1.5; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

// Sample-rate conversion demo: polyphase vs naive resampling.
// Ratio: L/M = 160/147 (48000/44100 in lowest terms). Upsample by L, filter, downsample by M.
const FS_IN  = 44100;  // input sample rate (Hz)
const FS_OUT = 48000;  // output sample rate (Hz)
const L = 160;         // upsample factor (interpolation)
const M = 147;         // downsample factor (decimation)

// Build a windowed-sinc low-pass FIR filter — cutoff at min(fs_in,fs_out)/2.
function sincLP(n, cutoff, N) {
  // windowed sinc — Hamming window * sinc
  const half = (N - 1) / 2;
  const x = n - half;
  const w = 0.54 - 0.46 * Math.cos((2 * Math.PI * n) / (N - 1)); // Hamming window coefficient
  if (x === 0) return 2 * cutoff * w;
  return w * Math.sin(2 * Math.PI * cutoff * x) / (Math.PI * x);
}

// Allocate and fill the polyphase filter bank
const TAPS = 64;              // taps per polyphase sub-filter
const TOTAL = TAPS * L;       // total FIR taps spread across all L phases
const CUT = 0.5 / Math.max(L, M); // cutoff = 0.5/max(L,M) in normalized frequency
const h = new Float32Array(TOTAL);
let norm = 0;
for (let i = 0; i < TOTAL; i++) {
  h[i] = sincLP(i, CUT, TOTAL);
  norm += h[i];
}
// normalize so passband gain = 1
for (let i = 0; i < TOTAL; i++) h[i] = h[i] * L / norm;

// Generate a pure sinusoidal input sample at the given frequency and index
function genInput(freq, n) {
  return Math.sin(2 * Math.PI * freq * n / FS_IN);
}

// Polyphase resampling: for each output sample, pick the correct sub-filter phase and convolve.
function polyphaseResample(freq, numOut) {
  const numIn = Math.ceil(numOut * M / L) + TAPS + 2;
  const x = new Float32Array(numIn);
  for (let i = 0; i < numIn; i++) x[i] = genInput(freq, i);
  const out = new Float32Array(numOut);
  for (let n = 0; n < numOut; n++) {
    // compute fractional input position and phase index
    const inPos = n * M / L;
    const inIdx = Math.floor(inPos);
    const phase = Math.round((inPos - inIdx) * L) % L;
    let acc = 0;
    // convolve with the chosen polyphase sub-filter
    for (let k = 0; k < TAPS; k++) {
      const xi = inIdx - k + TAPS - 1;
      if (xi >= 0 && xi < numIn) acc += x[xi] * h[phase + k * L];
    }
    out[n] = acc;
  }
  return out;
}

// Naive resampling: nearest-neighbor — no anti-aliasing filter.
function naiveResample(freq, numOut) {
  const out = new Float32Array(numOut);
  for (let n = 0; n < numOut; n++) {
    // round to nearest input index (creates aliasing)
    const srcIdx = Math.round(n * M / L);
    out[n] = genInput(freq, srcIdx);
  }
  return out;
}

// Draw the waveform on the canvas
function draw(samples, method) {
  const canvas = document.getElementById('wave');
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  ctx.clearRect(0, 0, W, H);

  // draw background grid lines
  ctx.strokeStyle = '#e0e6ed';
  ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(0, H / 2); ctx.lineTo(W, H / 2); ctx.stroke();
  for (let x = 0; x < W; x += 56) {
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
  }

  // green for polyphase (clean), red for naive (aliased)
  ctx.strokeStyle = method === 'polyphase' ? '#1a6640' : '#c92f3c';
  ctx.lineWidth = 1.8;
  ctx.beginPath();
  const step = W / samples.length;
  for (let i = 0; i < samples.length; i++) {
    const px = i * step;
    const py = H / 2 - samples[i] * (H / 2 - 10);
    if (i === 0) ctx.moveTo(px, py);
    else ctx.lineTo(px, py);
  }
  ctx.stroke();
}

// Compute samples and redraw when method or frequency changes.
let currentMethod = 'polyphase';
let currentFreq = 1000;

function update() {
  const numOut = 560; // number of output samples to render
  const samples = currentMethod === 'polyphase'
    ? polyphaseResample(currentFreq, numOut)
    : naiveResample(currentFreq, numOut);
  draw(samples, currentMethod);
  const msg = currentMethod === 'polyphase'
    ? i18nFmt('Polyphase — %freq% Hz tone resampled cleanly.', { freq: currentFreq })
    : i18nFmt('Naive — %freq% Hz tone with aliasing artifacts.', { freq: currentFreq });
  document.getElementById('status-msg').textContent = msg;
}

// Wire up button clicks and slider input
document.querySelectorAll('.method-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.method-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    currentMethod = btn.dataset.method;
    update();
  });
});

document.getElementById('freq-slider').addEventListener('input', e => {
  currentFreq = Number(e.target.value);
  document.getElementById('freq-display').textContent = currentFreq + ' Hz';
  update();
});

update();
Result
Console

Notice how the naive method introduces visible steps in the waveform and audible distortion, while the polyphase filter produces a smooth sinusoid at exactly the right frequency. The ratio 48000/44100=160/14748000 / 44100 = 160 / 147 means that for every 147 input samples the algorithm must produce 160 output samples — it upsamples by 160, low-pass filters, then downsamples by 147, all folded into one efficient polyphase structure.

The Real Complexity

What makes sample-rate conversion hard is not the ratio — it is the Nyquist constraint.

  • The problem: if you upsample a 44.1 kHz signal by inserting zeros between samples, you get L=160L = 160 copies of the original spectrum shifted to multiples of 44.1 kHz. Most of those copies are aliases and must be removed before you downsample.
  • The naive approach: resample by brute interpolation — nearest-neighbor (repeat or drop samples) or linear interpolation. Both leave spectral images in the output that fold into the audio band as aliasing distortion.
  • The polyphase solution: design a single low-pass FIR filter with cutoff at fc=12min⁡(fin,fout)f_c = \tfrac{1}{2} \min(f_\text{in}, f_\text{out}) and KK taps. Rearrange its coefficients into LL polyphases (sub-filters of length K/LK/L), one per interpolated phase. Each output sample then costs only K/LK/L multiplications instead of KK. Total cost: O(K)O(K) per output sample — the same as running a single FIR filter, regardless of how large LL or MM are.
  • Quality vs cost: a higher KK gives a sharper stopband and less aliasing, at proportionally more computation. Typical high-quality converters use K≈160×64=10240K \approx 160 \times 64 = 10240 taps split across 160 polyphases, giving 64 taps per phase — a handful of multiply-adds per output sample.
  • Arbitrary ratios: when the ratio fout/finf_\text{out} / f_\text{in} is irrational (e.g. converting to 96 kHz for pro audio), the polyphase indices are computed with a fractional accumulator — the same math, generalized. See also fast multiplication for how efficient multiply-accumulate hardware makes these filters practical in real time.

The 1970s result that made this tractable is due to Ronald Crochiere and Lawrence Rabiner, who showed in their 1983 book Multirate Digital Signal Processing that the polyphase decomposition reduces the arithmetic cost by exactly the interpolation factor LL.

Where It Matters

Polyphase sample-rate conversion is one of those algorithms that disappears into every pipeline it touches:

  • Professional audio interfaces: every USB audio device that bridges the computer's 48 kHz clock to an instrument running at 44.1 kHz (or 88.2, or 96 kHz) runs a polyphase SRC in hardware or firmware.
  • Streaming platforms: Spotify, Apple Music and YouTube re-encode tracks from whatever the artist submitted to a delivery format; each stage involves at least one SRC pass.
  • Game engines: a game may load sounds at 22.05 kHz, 44.1 kHz and 48 kHz and must mix them in real time to a single output rate — polyphase SRC runs on every non-native-rate asset.
  • Scientific instruments: seismographs, radio telescopes and medical scanners each run at their own sampling clock; stitching data from multiple sensors requires SRC before any joint analysis.
  • Software-defined radio (SDR): a receiver captures a wide band at a hardware rate, then digitally tunes to a channel by downsampling to the channel bandwidth — a decimation-only special case of the same polyphase structure.

The underlying mathematics — FIR filter design, the fast Fourier transform for computing filter responses, and multirate signal processing — also power speech recognition front-ends, hearing-aid chips and sonar processing.

Conclusion

The gap between 44.1 kHz and 48 kHz looks like a bureaucratic accident, but bridging it cleanly forced engineers to understand signals at a deep level. The answer — decompose a single large filter into polyphase sub-filters, one per interpolated phase — is both mathematically elegant and practically indispensable.

Every time you drop a music track into a video editor and the audio stays perfectly in tune, a polyphase filter just ran in the background without asking for credit. The ratio 160/147160 / 147 is ungainly; the algorithm that handles it is not.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/sample-rate-conversion/Content licensed under CC BY-NC 4.0.