resample_to
Builtin index · Signal processing
result = resample_to(signal, new_fs)
Input: signal, new_fs · Output: result
What it is
signal: a length-N Signal (must carry a known Fs); new_fs: a number (Hz), the target sample rate.
Overloads
One call shape — this function is not overloaded.
| Code | Input | Output | Description |
result = resample_to(signal, new_fs) | signal Signalsignal: a length-N Signal (must carry a known Fs).new_fs numbernew_fs: a number (Hz), the target sample rate. | result Signal, vector, or number | Returns a new Signal at Fs=new_fs, whose length is whatever it takes to span the *same* start/end time as the input (generally not N). Real resampling, not a relabel: evaluates the signal's own piecewise-linear waveform (through its samples at their original instants i/Fs, via the same kernel interp1's "linear" method uses) at the new instants j/new_fs — e.g. a 4-sample, 1 Hz ramp [0,10,20,30] (spanning t=0..3s) resampled to 2 Hz becomes the 7-sample [0,5,10,15,20,25,30], still spanning t=0..3s — never a same-length signal with Fs merely swapped out (which would silently misstate the recorded duration). signal.resample_to(new_fs) also works, via the generic method-chain sugar. Wrap a plain vector first with signal(data, Fs) if it isn't already a Signal. Anti-aliased on the way down, and this changed the numbers (v0.3.0). When new_fs < fs the samples now pass through a lowpass at the new Nyquist (new_fs/2, order 60, designed and applied at the original rate) before the grid change; before v0.3.0 they did not, so anything above the new Nyquist folded back into the band and came out as a plausible-looking tone that was never recorded. The filter is causal, so the result is delayed by its group delay, 30/fs seconds (order/2 = 30 input samples), and the first and last ~30 samples are filter transient. The order is reduced automatically for inputs shorter than 61 samples, and no filter is applied below 5 samples. Upsampling (new_fs >= fs) cannot alias and is untouched — same values, no filter, no delay. To opt out of anti-aliasing entirely, interpolate onto the new grid yourself with interp1 and accept the aliasing. |
Examples
From the documentation, and run by the build -- the output and the figure below are what the engine actually produced. Simplest first.
x = signal([0, 10, 20, 30])
print(x)
print(x.resample_to(2))
from tour
How it works
resample_to(signal, new_fs) — REAL resampling, not a relabel: evaluates the original signal's implied continuous waveform (piecewise-linear through its own samples at their original sample instants i/Fs) at the new sample instants j/new_fs, via the same linear-interpolation kernel interp1 already uses (interp1_linear) — reused rather than reimplemented. The new signal spans the SAME start/end time as the original (t=0 to t=(n-1)/Fs), just resampled onto a different-density grid — e.g. a 4-sample signal at 1 Hz ([0,10,20,30], spanning t=0..3s) resampled to 2 Hz becomes the 7-sample [0,5,10,15,20,25,30] spanning the same t=0..3s, not a 4-sample signal relabeled to claim 2 Hz (which would silently claim a 1.5s duration for data that was actually recorded over 3s). Functional/pure like every other Qu builtin (no in-place Fs mutation exists to abuse here anyway — expr.field = value doesn't parse). Anti-aliased on the way DOWN (behavior change, 2026-09-19). Until this change resample_to was the linear interpolation below and nothing else, which is correct for upsampling and silently WRONG for downsampling: evaluating a waveform on a sparser grid folds everything above the new Nyquist back into the band as alias, and the returned signal looks perfectly plausible while containing tones that were never in the input. So when new_fs < fs the samples now go through a lowpass at the new Nyquist (new_fs/2, designed and applied at the ORIGINAL rate) before being interpolated onto the new grid — multirate_lowpass, the same fir1+sosfilt pair the upsample/downsample/resample_int family uses. This changes the numbers existing downsampling callers get, by design (QuMaster's ruling: correct beats silently aliased). Two specific differences, neither hidden: 1. The result is delayed by the filter's group delay, order/2 samples at the ORIGINAL rate — the filter is causal, exactly as sosfilt is. With the default order 60 that is 30 input samples, i.e. 30/fs seconds. Nothing here compensates it; if you need the old sample-for-sample time alignment, drop the first 30/fs seconds yourself, or use interp1 directly to opt out of anti-aliasing entirely and accept the aliasing that comes with it. 2. The first and last ~order/2 samples are filter startup/ run-out transient rather than signal. The order is reduced for short inputs (multirate_effective_ order) and filtering is skipped entirely below 5 samples, where no filter of useful length exists. Upsampling (new_fs >= fs) is untouched — it cannot alias, so it gets no filter, no delay and byte-identical results to before.
From the note above this builtin in the interpreter, where the reasoning was written down as it was built.