use it elsewhere

Sinc interpolation

Put it on your own page with two lines, or fork it and make it yours. It is CC-BY-4.0.

Embed

The script finds its own origin, so nothing else is needed; the widget runs in a sandboxed frame and cannot touch your page. Set the controls and the two lines update; the preview beneath shows what your readers get.

<script src="https://learn.mimmsy.com/learn-widget.js"></script> <learn-widget name="sinc-interpolation"></learn-widget>

It is CC-BY-4.0: keep the credit line the frame shows.

Preview

Fork

A fork is a copy whose manifest names its parent and the parent's version; lineage is kept forever, and the copy is yours to change. The library is its own repository: clone it, copy sinc-interpolation/ to a new name (one DNS label), set forkedFrom to { "name": "sinc-interpolation", "version": 1 }, change what you want, run npm run check, and open a pull request. Passing the check is the whole gate. Without a checkout, submit the same files to POST /api/widgets and it is served instantly as an unreviewed draft.

Source

The whole widget is these files; the repository has their history.

widget.json

{
  "name": "sinc-interpolation",
  "title": "Sinc interpolation",
  "version": 1,
  "claim": "Each stored sample contributes one sinc pulse that is 1 at its own instant and 0 at every other sample instant, so the sum y(t) = Σ x[n]·sinc(fₛt − n) passes through every sample and, for a wave below fₛ/2, reproduces the original between them.",
  "summary": "Pick a sample rate and a wave frequency, highlight one sample to see its pulse, and watch the faint pulses of all the samples add up to the green curve through the dots. Readouts show that the other pulses sum to zero at the highlighted instant and how far the sum strays from the original; push the wave past half the sample rate and the sum traces a different, slower wave instead.",
  "topics": ["signals/sampling", "audio/digital"],
  "aliases": ["sinc", "reconstruction", "Whittaker–Shannon interpolation", "band-limited", "DAC filter", "ideal low-pass"],
  "params": {
    "rate": { "type": "integer", "default": 8, "min": 4, "max": 24, "label": "samples per second" },
    "freq": { "type": "number", "default": 1.5, "min": 0.5, "max": 6, "label": "wave frequency in Hz" },
    "highlight": { "type": "integer", "default": 3, "min": 0, "max": 23, "label": "highlighted sample index" },
    "pulses": { "type": "boolean", "default": true, "label": "draw every pulse" },
    "hold": { "type": "boolean", "default": false, "label": "draw the held staircase" }
  },
  "check": [
    {
      "q": "At the instant of sample n, the sinc pulses centred on all the other samples contribute…",
      "options": ["zero, so the sum equals x[n] exactly", "their peak values", "half of x[n] each", "an average of the neighbouring samples"],
      "answer": 0,
      "why": "sinc(u) is 0 at every non-zero integer, so at t = n/fₛ every pulse but sample n's own vanishes and the sum passes through x[n]."
    },
    {
      "q": "The reconstruction filter after a DAC exists to…",
      "options": ["smooth the staircase by removing everything above Nyquist", "add back the bits lost in quantization", "boost the bass", "speed up playback"],
      "answer": 0,
      "why": "The held staircase has sharp corners, content above Nyquist that was never in the signal; low-passing at Nyquist removes them, leaving the one smooth wave through the samples, which is the sinc sum."
    },
    {
      "q": "Raising the wave's frequency above half the sample rate makes the sum of sinc pulses…",
      "options": ["trace a different, slower wave through the same samples", "still reproduce the original wave", "collapse to zero", "grow without bound"],
      "answer": 0,
      "why": "The sum is always the unique wave below fₛ/2 through the samples; above Nyquist that is the alias, not the original, which is why the error readout jumps."
    }
  ],
  "capabilities": [],
  "height": 530,
  "requires": ["samples-per-cycle", "nyquist-fold"],
  "forkedFrom": null,
  "authors": []
}

index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sinc interpolation</title>
<style>
  :root {
    color-scheme: light dark;
    --bg: #f7f3ea;
    --bg-card: #efe9da;
    --border: #d8cfba;
    --text: #211d14;
    --text-dim: #6e6553;
    --accent: #31597f;
    --accent2: #b04e1b;
    --hot: #a82433;
    --ok: #3d6b4f;
    --ink-rgb: 33, 29, 20;
    --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #161410;
      --bg-card: #1e1b15;
      --border: #383225;
      --text: #e9e3d3;
      --text-dim: #9c917c;
      --accent: #8fb8e0;
      --accent2: #dd9355;
      --hot: #df7a88;
      --ok: #82bd97;
      --ink-rgb: 233, 227, 211;
    }
  }

  * { box-sizing: border-box; }
  html, body { margin: 0; }
  body { background: transparent; color: var(--text); font-family: var(--serif); }

  .widget {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 22px;
  }

  .control-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
  .control-row.sym.single {
    display: grid; grid-template-columns: 88px 1fr; gap: 12px 16px;
    align-items: center; margin-bottom: 14px;
  }
  .control-row label { font-size: 14px; color: var(--text-dim); white-space: nowrap; }
  .ctl { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .ctl input[type="range"] { flex: 1; min-width: 0; }
  .ctl .val { flex: none; width: 72px; text-align: right; font-size: 13px; }
  input[type="range"] { accent-color: var(--accent); }
  @media (max-width: 600px) { .control-row.sym.single { grid-template-columns: 64px 1fr; } }

  .toggle { display: inline-flex; align-items: baseline; gap: 7px; font-size: 13.5px; color: var(--text-dim); cursor: pointer; white-space: normal; }
  .toggle input { accent-color: var(--accent2); }

  .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
  .dim { color: var(--text-dim); font-size: 12.5px; }

  .chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
  .btn, .chip {
    font-family: var(--mono); font-size: 12.5px; color: var(--text);
    background: transparent; border: 1px solid var(--border); border-radius: 3px;
    padding: 6px 13px; cursor: pointer; transition: border-color 0.15s, color 0.15s;
  }
  .btn:hover, .chip:hover { border-color: var(--accent); color: var(--accent); }
  .btn.primary { border-color: var(--accent); color: var(--accent); }
  .btn.playing, .btn.playing:hover { border-color: var(--hot); color: var(--hot); }
  .btn.off { border-color: var(--hot); color: var(--hot); opacity: 0.85; }

  .plot { display: block; width: 100%; height: 220px; margin-top: 8px; }
  .plot.tall { height: 280px; }

  .readout-row { display: flex; gap: 28px; flex-wrap: wrap; margin: 14px 0 0; }
  .readout { text-align: left; }
  .readout .big {
    display: block; font-size: 28px; font-weight: 600; color: var(--accent);
    line-height: 1.1; min-width: 5ch; font-variant-numeric: tabular-nums;
  }
  .readout.accent .big { color: var(--accent2); }
  .readout .big.hot { color: var(--hot); }
  .readout small { color: var(--text-dim); font-size: 12.5px; }
  @media (max-width: 560px) {
    .widget { padding: 16px; }
    .readout-row { gap: 12px 18px; }
    .readout .big { font-size: 22px; }
    .plot { height: 180px; }
    .plot.tall { height: 220px; }
    .plot.pipe { height: 200px; }
    .narrow-hide { display: none; }
  }
</style>
</head>
<body>
  <div class="widget" id="root">
    <div class="control-row sym single">
      <label for="si-fs">sample rate</label>
      <span class="ctl wide"><input type="range" id="si-fs" min="4" max="24" value="8"><span class="mono val" id="si-fs-label">8 /s</span></span>
    </div>
    <div class="control-row sym single">
      <label for="si-f">wave freq</label>
      <span class="ctl wide"><input type="range" id="si-f" min="2" max="24" value="6"><span class="mono val" id="si-f-label">1.50 Hz</span></span>
    </div>
    <div class="control-row sym single">
      <label for="si-n">highlight n</label>
      <span class="ctl wide"><input type="range" id="si-n" min="0" max="7" value="3"><span class="mono val" id="si-n-label">n = 3</span></span>
    </div>
    <div class="control-row">
      <label class="toggle"><input type="checkbox" id="si-pulses" checked> draw every sample&#8217;s sinc pulse</label>
      <label class="toggle"><input type="checkbox" id="si-hold"> draw the held staircase for comparison</label>
    </div>
    <canvas id="si-canvas" class="plot tall"></canvas>
    <div class="readout-row">
      <div class="readout">
        <span class="big mono" id="si-percycle">5.3</span>
        <small>samples per cycle</small>
      </div>
      <div class="readout accent">
        <span class="big mono" id="si-at">0.000</span>
        <small id="si-at-note">other pulses at t = 3/8 s &middot; sum</small>
      </div>
      <div class="readout">
        <span class="big mono" id="si-err">0.00</span>
        <small>max |sum &minus; original| in the window</small>
      </div>
    </div>
  </div>
  <script src="/w/_sdk/host.js?v=1"></script>
  <script src="/w/_lib/audio.js?v=1"></script>
  <script src="widget.js?v=1"></script>
</body>
</html>

widget.js

/* Sinc interpolation: every sample carries one sinc pulse; the pulses sum to the unique band-limited curve through the samples. */
(() => {
  'use strict';
  const K = window.AudioKit;
  const $ = (id) => document.getElementById(id);
  const PAD = 16;
  const PHASE = 0.6;
  let rate = 8, freq = 1.5, hi = 3, pulses = true, hold = false, host = null;

  const sig = (t) => 0.85 * Math.sin(2 * Math.PI * freq * t + PHASE);
  const sample = (n) => sig(n / rate);
  const pulse = (n, t) => sample(n) * K.sinc(t * rate - n);
  const sum = (t) => {
    let y = 0;
    for (let n = -PAD; n < rate + PAD; n++) y += pulse(n, t);
    return y;
  };
  const others = (t) => {
    let y = 0;
    for (let n = -PAD; n < rate + PAD; n++) if (n !== hi) y += pulse(n, t);
    return y;
  };

  function render() {
    $('si-fs-label').textContent = rate + ' /s';
    $('si-f-label').textContent = freq.toFixed(2) + ' Hz';
    $('si-n-label').textContent = 'n = ' + hi;
    $('si-pulses').checked = pulses;
    $('si-hold').checked = hold;
    const perCycle = rate / freq;
    const pc = $('si-percycle');
    pc.textContent = perCycle.toFixed(1);
    pc.classList.toggle('hot', perCycle < 2);
    $('si-at').textContent = Math.abs(others(hi / rate)).toFixed(3);
    $('si-at-note').textContent = 'other pulses at t = ' + hi + '/' + rate + ' s · sum';
    let err = 0;
    for (let i = 0; i <= 400; i++) {
      const t = i / 400;
      err = Math.max(err, Math.abs(sum(t) - sig(t)));
    }
    const er = $('si-err');
    er.textContent = err.toFixed(2);
    er.classList.toggle('hot', err > 0.1);

    const { ctx, w, h } = K.setupCanvas($('si-canvas'));
    const m = { l: 14, r: 14 };
    const pw = w - m.l - m.r;
    const mid = h / 2;
    const amp = h * 0.36;
    const X = (t) => m.l + t * pw;
    const Y = (v) => mid - K.clamp(v, -1.3, 1.3) * amp;

    ctx.clearRect(0, 0, w, h);
    K.hline(ctx, m.l, w - m.r, mid, K.T.GRID);
    for (let n = 0; n < rate; n++) {
      ctx.strokeStyle = K.T.GRID;
      ctx.beginPath();
      ctx.moveTo(X(n / rate), mid - amp * 1.15);
      ctx.lineTo(X(n / rate), mid + amp * 1.15);
      ctx.stroke();
    }

    K.trace(ctx, m.l, w - m.r, (u) => Y(sig(u)), K.T.ACCENT, 2, 0.45);

    if (hold) {
      ctx.strokeStyle = K.T.DIM;
      ctx.lineWidth = 1.2;
      ctx.setLineDash([3, 3]);
      ctx.beginPath();
      for (let n = 0; n < rate; n++) {
        const y = Y(sample(n));
        if (n === 0) ctx.moveTo(X(0), y);
        else ctx.lineTo(X(n / rate), y);
        ctx.lineTo(X((n + 1) / rate), y);
      }
      ctx.stroke();
      ctx.setLineDash([]);
    }

    if (pulses) {
      for (let n = -PAD; n < rate + PAD; n++) {
        if (n === hi) continue;
        K.trace(ctx, m.l, w - m.r, (u) => Y(pulse(n, u)), K.T.ACCENT2, 1, 0.22);
      }
    }
    K.trace(ctx, m.l, w - m.r, (u) => Y(pulse(hi, u)), K.T.ACCENT2, 1.8);
    K.trace(ctx, m.l, w - m.r, (u) => Y(sum(u)), K.T.OK, 2.4);

    for (let n = 0; n < rate; n++) {
      K.lollipop(ctx, X(n / rate), mid, Y(sample(n)), n === hi ? 4.4 : 3.4, n === hi ? K.T.ACCENT2 : K.T.INK);
    }

    ctx.font = '11px ui-monospace, Menlo, monospace';
    ctx.textAlign = 'left';
    ctx.fillStyle = K.T.DIM;
    ctx.fillText('1 s window · y(t) = Σ x[n]·sinc(fₛt − n)', m.l + 2, 13);
    ctx.textAlign = 'right';
    ctx.fillStyle = K.T.OK;
    ctx.fillText('sum of pulses', w - m.r - 2, 13);
    ctx.fillStyle = K.T.ACCENT2;
    ctx.fillText('pulse of sample ' + hi, w - m.r - 2, 26);
    ctx.fillStyle = K.T.ACCENT;
    ctx.fillText('original', w - m.r - 2, 39);
  }

  const ping = (data) => { if (host && host.signal) host.signal('interaction', data); };
  function set(p) {
    if (typeof p.rate === 'number') { rate = K.clamp(Math.round(p.rate), 4, 24); $('si-fs').value = rate; }
    if (typeof p.freq === 'number') { freq = K.clamp(Math.round(p.freq * 4) / 4, 0.5, 6); $('si-f').value = Math.round(freq * 4); }
    if (typeof p.highlight === 'number') hi = Math.round(p.highlight);
    if (typeof p.pulses === 'boolean') pulses = p.pulses;
    if (typeof p.hold === 'boolean') hold = p.hold;
    $('si-n').max = rate - 1;
    hi = K.clamp(hi, 0, rate - 1);
    $('si-n').value = hi;
    render();
  }

  $('si-fs').addEventListener('input', () => { set({ rate: +$('si-fs').value }); ping({ rate, freq }); });
  $('si-f').addEventListener('input', () => { set({ freq: $('si-f').value / 4 }); ping({ rate, freq }); });
  $('si-n').addEventListener('input', () => { set({ highlight: +$('si-n').value }); ping({ highlight: hi }); });
  $('si-pulses').addEventListener('input', () => { set({ pulses: $('si-pulses').checked }); ping({ pulses }); });
  $('si-hold').addEventListener('input', () => { set({ hold: $('si-hold').checked }); ping({ hold }); });
  K.debounceResize(render);
  set({});

  LearnWidget.connect().then((h) => {
    host = h;
    K.fitFrame(h, $('root'));
    set(h.params || {});
    h.on('theme.changed', () => { K.readTheme(); render(); });
    h.on('params.changed', set);
    K.readTheme();
    render();
  });
})();